Browse Source

lang: Framework defined error codes (#354)

Armani Ferrante 4 years ago
parent
commit
ba99c9c920

+ 4 - 0
CHANGELOG.md

@@ -20,6 +20,10 @@ incremented for features.
 
 * lang: Allows one to use `remaining_accounts` with `CpiContext` by implementing the `ToAccountMetas` trait on `CpiContext` ([#351](https://github.com/project-serum/anchor/pull/351/files)).
 
+### Breaking
+
+* lang, ts: Framework defined error codes are introduced, reserving error codes 0-300 for Anchor, and 300 and up for user defined error codes ([#354](https://github.com/project-serum/anchor/pull/354)).
+
 ## [0.7.0] - 2021-05-31
 
 ### Features

+ 2 - 2
Cargo.lock

@@ -4066,9 +4066,9 @@ dependencies = [
 
 [[package]]
 name = "url"
-version = "2.2.1"
+version = "2.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
 dependencies = [
  "form_urlencoded",
  "idna",

+ 1 - 1
examples/chat/tests/chat.js

@@ -89,7 +89,7 @@ describe("chat", () => {
         assert.ok(msg.from.equals(user));
         assert.ok(data.startsWith(messages[idx]));
       } else {
-        assert.ok(new anchor.web3.PublicKey());
+        assert.ok(anchor.web3.PublicKey.default);
         assert.ok(
           JSON.stringify(msg.data) === JSON.stringify(new Array(280).fill(0))
         );

+ 38 - 0
examples/errors/programs/errors/src/lib.rs

@@ -6,6 +6,7 @@ use anchor_lang::prelude::*;
 #[program]
 mod errors {
     use super::*;
+
     pub fn hello(_ctx: Context<Hello>) -> Result<()> {
         Err(MyError::Hello.into())
     }
@@ -17,11 +18,48 @@ mod errors {
     pub fn hello_next(_ctx: Context<Hello>) -> Result<()> {
         Err(MyError::HelloNext.into())
     }
+
+    pub fn mut_error(_ctx: Context<MutError>) -> Result<()> {
+        Ok(())
+    }
+
+    pub fn belongs_to_error(_ctx: Context<BelongsToError>) -> Result<()> {
+        Ok(())
+    }
+
+    pub fn signer_error(_ctx: Context<SignerError>) -> Result<()> {
+        Ok(())
+    }
 }
 
 #[derive(Accounts)]
 pub struct Hello {}
 
+#[derive(Accounts)]
+pub struct MutError<'info> {
+    #[account(mut)]
+    my_account: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct BelongsToError<'info> {
+    #[account(init, belongs_to = owner)]
+    my_account: ProgramAccount<'info, BelongsToAccount>,
+    owner: AccountInfo<'info>,
+    rent: Sysvar<'info, Rent>,
+}
+
+#[derive(Accounts)]
+pub struct SignerError<'info> {
+    #[account(signer)]
+    my_account: AccountInfo<'info>,
+}
+
+#[account]
+pub struct BelongsToAccount {
+    owner: Pubkey,
+}
+
 #[error]
 pub enum MyError {
     #[msg("This is an error message clients will automatically display")]

+ 72 - 3
examples/errors/tests/errors.js

@@ -1,5 +1,6 @@
 const assert = require("assert");
 const anchor = require('@project-serum/anchor');
+const { Account, Transaction, TransactionInstruction } = anchor.web3;
 
 describe("errors", () => {
   // Configure the client to use the local cluster.
@@ -16,7 +17,7 @@ describe("errors", () => {
         "This is an error message clients will automatically display";
       assert.equal(err.toString(), errMsg);
       assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 100);
+      assert.equal(err.code, 300);
     }
   });
 
@@ -28,7 +29,7 @@ describe("errors", () => {
       const errMsg = "HelloNoMsg";
       assert.equal(err.toString(), errMsg);
       assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 100 + 123);
+      assert.equal(err.code, 300 + 123);
     }
   });
 
@@ -40,7 +41,75 @@ describe("errors", () => {
       const errMsg = "HelloNext";
       assert.equal(err.toString(), errMsg);
       assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 100 + 124);
+      assert.equal(err.code, 300 + 124);
+    }
+  });
+
+  it("Emits a mut error", async () => {
+    try {
+      const tx = await program.rpc.mutError({
+        accounts: {
+          myAccount: anchor.web3.SYSVAR_RENT_PUBKEY,
+        },
+      });
+      assert.ok(false);
+    } catch (err) {
+      const errMsg = "A mut constraint was violated";
+      assert.equal(err.toString(), errMsg);
+      assert.equal(err.msg, errMsg);
+      assert.equal(err.code, 140);
+    }
+  });
+
+  it("Emits a belongs to error", async () => {
+    try {
+      const account = new Account();
+      const tx = await program.rpc.belongsToError({
+        accounts: {
+          myAccount: account.publicKey,
+          owner: anchor.web3.SYSVAR_RENT_PUBKEY,
+          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        },
+        instructions: [
+          await program.account.belongsToAccount.createInstruction(account),
+        ],
+        signers: [account],
+      });
+      assert.ok(false);
+    } catch (err) {
+      const errMsg = "A belongs_to constraint was violated";
+      assert.equal(err.toString(), errMsg);
+      assert.equal(err.msg, errMsg);
+      assert.equal(err.code, 141);
+    }
+  });
+
+  // This test uses a raw transaction and provider instead of a program
+  // instance since the client won't allow one to send a transaction
+  // with an invalid signer account.
+  it("Emits a signer error", async () => {
+    try {
+      const account = new Account();
+      const tx = new Transaction();
+      tx.add(
+        new TransactionInstruction({
+          keys: [
+            {
+              pubkey: anchor.web3.SYSVAR_RENT_PUBKEY,
+              isWritable: false,
+              isSigner: false,
+            },
+          ],
+          programId: program.programId,
+          data: program.coder.instruction.encode("signer_error", {}),
+        })
+      );
+      await program.provider.send(tx);
+      assert.ok(false);
+    } catch (err) {
+      const errMsg =
+        "Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x8e";
+      assert.equal(err.toString(), errMsg);
     }
   });
 });

+ 1 - 1
examples/lockup/migrations/deploy.js

@@ -31,7 +31,7 @@ module.exports = async function (provider) {
   });
 
   // Delete the default whitelist entries.
-  const defaultEntry = { programId: new anchor.web3.PublicKey() };
+  const defaultEntry = { programId: new anchor.web3.PublicKey.default };
   await lockup.state.rpc.whitelistDelete(defaultEntry, {
     accounts: {
       authority: provider.wallet.publicKey,

+ 7 - 7
examples/lockup/tests/lockup.js

@@ -42,12 +42,12 @@ describe("Lockup and Registry", () => {
     assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
     assert.ok(lockupAccount.whitelist.length === WHITELIST_SIZE);
     lockupAccount.whitelist.forEach((e) => {
-      assert.ok(e.programId.equals(new anchor.web3.PublicKey()));
+      assert.ok(e.programId.equals(anchor.web3.PublicKey.default));
     });
   });
 
   it("Deletes the default whitelisted addresses", async () => {
-    const defaultEntry = { programId: new anchor.web3.PublicKey() };
+    const defaultEntry = { programId: anchor.web3.PublicKey.default };
     await lockup.state.rpc.whitelistDelete(defaultEntry, {
       accounts: {
         authority: provider.wallet.publicKey,
@@ -116,7 +116,7 @@ describe("Lockup and Registry", () => {
         await lockup.state.rpc.whitelistAdd(e, { accounts });
       },
       (err) => {
-        assert.equal(err.code, 108);
+        assert.equal(err.code, 308);
         assert.equal(err.msg, "Whitelist is full");
         return true;
       }
@@ -216,7 +216,7 @@ describe("Lockup and Registry", () => {
         });
       },
       (err) => {
-        assert.equal(err.code, 107);
+        assert.equal(err.code, 307);
         assert.equal(err.msg, "Insufficient withdrawal balance.");
         return true;
       }
@@ -389,7 +389,7 @@ describe("Lockup and Registry", () => {
 
     assert.ok(memberAccount.registrar.equals(registrar.publicKey));
     assert.ok(memberAccount.beneficiary.equals(provider.wallet.publicKey));
-    assert.ok(memberAccount.metadata.equals(new anchor.web3.PublicKey()));
+    assert.ok(memberAccount.metadata.equals(anchor.web3.PublicKey.default));
     assert.equal(
       JSON.stringify(memberAccount.balances),
       JSON.stringify(balances)
@@ -781,7 +781,7 @@ describe("Lockup and Registry", () => {
       (err) => {
         // Solana doesn't propagate errors across CPI. So we receive the registry's error code,
         // not the lockup's.
-        const errorCode = "custom program error: 0x78";
+        const errorCode = "custom program error: 0x140";
         assert.ok(err.toString().split(errorCode).length === 2);
         return true;
       }
@@ -863,7 +863,7 @@ describe("Lockup and Registry", () => {
         await tryEndUnstake();
       },
       (err) => {
-        assert.equal(err.code, 109);
+        assert.equal(err.code, 309);
         assert.equal(err.msg, "The unstake timelock has not yet expired.");
         return true;
       }

+ 7 - 7
examples/zero-copy/tests/zero-copy.js

@@ -21,14 +21,14 @@ describe("zero-copy", () => {
     assert.ok(state.authority.equals(program.provider.wallet.publicKey));
     assert.ok(state.events.length === 250);
     state.events.forEach((event, idx) => {
-      assert.ok(event.from.equals(new PublicKey()));
+      assert.ok(event.from.equals(PublicKey.default));
       assert.ok(event.data.toNumber() === 0);
     });
   });
 
   it("Updates zero copy state", async () => {
     let event = {
-      from: new PublicKey(),
+      from: PublicKey.default,
       data: new BN(1234),
     };
     await program.state.rpc.setEvent(5, event, {
@@ -44,7 +44,7 @@ describe("zero-copy", () => {
         assert.ok(event.from.equals(event.from));
         assert.ok(event.data.eq(event.data));
       } else {
-        assert.ok(event.from.equals(new PublicKey()));
+        assert.ok(event.from.equals(PublicKey.default));
         assert.ok(event.data.toNumber() === 0);
       }
     });
@@ -175,7 +175,7 @@ describe("zero-copy", () => {
     const account = await program.account.eventQ.fetch(eventQ.publicKey);
     assert.ok(account.events.length === 25000);
     account.events.forEach((event) => {
-      assert.ok(event.from.equals(new PublicKey()));
+      assert.ok(event.from.equals(PublicKey.default));
       assert.ok(event.data.toNumber() === 0);
     });
   });
@@ -196,7 +196,7 @@ describe("zero-copy", () => {
         assert.ok(event.from.equals(program.provider.wallet.publicKey));
         assert.ok(event.data.toNumber() === 48);
       } else {
-        assert.ok(event.from.equals(new PublicKey()));
+        assert.ok(event.from.equals(PublicKey.default));
         assert.ok(event.data.toNumber() === 0);
       }
     });
@@ -219,7 +219,7 @@ describe("zero-copy", () => {
         assert.ok(event.from.equals(program.provider.wallet.publicKey));
         assert.ok(event.data.toNumber() === 1234);
       } else {
-        assert.ok(event.from.equals(new PublicKey()));
+        assert.ok(event.from.equals(PublicKey.default));
         assert.ok(event.data.toNumber() === 0);
       }
     });
@@ -245,7 +245,7 @@ describe("zero-copy", () => {
         assert.ok(event.from.equals(program.provider.wallet.publicKey));
         assert.ok(event.data.toNumber() === 99);
       } else {
-        assert.ok(event.from.equals(new PublicKey()));
+        assert.ok(event.from.equals(PublicKey.default));
         assert.ok(event.data.toNumber() === 0);
       }
     });

+ 10 - 10
lang/attribute/account/src/lib.rs

@@ -119,11 +119,11 @@ pub fn account(
                 impl anchor_lang::AccountDeserialize for #account_name {
                     fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
                         if buf.len() < #discriminator.len() {
-                            return Err(ProgramError::AccountDataTooSmall);
+                            return Err(anchor_lang::__private::ErrorCode::AccountDiscriminatorNotFound.into());
                         }
                         let given_disc = &buf[..8];
                         if &#discriminator != given_disc {
-                            return Err(ProgramError::InvalidInstructionData);
+                            return Err(anchor_lang::__private::ErrorCode::AccountDiscriminatorMismatch.into());
                         }
                         Self::try_deserialize_unchecked(buf)
                     }
@@ -144,12 +144,12 @@ pub fn account(
 
                 impl anchor_lang::AccountSerialize for #account_name {
                     fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> std::result::Result<(), ProgramError> {
-                        writer.write_all(&#discriminator).map_err(|_| ProgramError::InvalidAccountData)?;
+                        writer.write_all(&#discriminator).map_err(|_| anchor_lang::__private::ErrorCode::AccountDidNotSerialize)?;
                         AnchorSerialize::serialize(
                             self,
                             writer
                         )
-                            .map_err(|_| ProgramError::InvalidAccountData)?;
+                            .map_err(|_| anchor_lang::__private::ErrorCode::AccountDidNotSerialize)?;
                         Ok(())
                     }
                 }
@@ -157,11 +157,11 @@ pub fn account(
                 impl anchor_lang::AccountDeserialize for #account_name {
                     fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
                         if buf.len() < #discriminator.len() {
-                            return Err(ProgramError::AccountDataTooSmall);
+                            return Err(anchor_lang::__private::ErrorCode::AccountDiscriminatorNotFound.into());
                         }
                         let given_disc = &buf[..8];
                         if &#discriminator != given_disc {
-                            return Err(ProgramError::InvalidInstructionData);
+                            return Err(anchor_lang::__private::ErrorCode::AccountDiscriminatorMismatch.into());
                         }
                         Self::try_deserialize_unchecked(buf)
                     }
@@ -169,7 +169,7 @@ pub fn account(
                     fn try_deserialize_unchecked(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
                         let mut data: &[u8] = &buf[8..];
                         AnchorDeserialize::deserialize(&mut data)
-                            .map_err(|_| ProgramError::InvalidAccountData)
+                            .map_err(|_| anchor_lang::__private::ErrorCode::AccountDidNotDeserialize.into())
                     }
                 }
 
@@ -327,8 +327,8 @@ pub fn zero_copy(
     let account_strct = parse_macro_input!(item as syn::ItemStruct);
 
     proc_macro::TokenStream::from(quote! {
-            #[derive(anchor_lang::__private::ZeroCopyAccessor, Copy, Clone)]
-            #[repr(packed)]
-            #account_strct
+        #[derive(anchor_lang::__private::ZeroCopyAccessor, Copy, Clone)]
+        #[repr(packed)]
+        #account_strct
     })
 }

+ 7 - 2
lang/attribute/error/src/lib.rs

@@ -2,6 +2,7 @@ extern crate proc_macro;
 
 use anchor_syn::codegen::error as error_codegen;
 use anchor_syn::parser::error as error_parser;
+use anchor_syn::ErrorArgs;
 use syn::parse_macro_input;
 
 /// Generates `Error` and `type Result<T> = Result<T, Error>` types to be
@@ -47,10 +48,14 @@ use syn::parse_macro_input;
 /// parsers  and IDLs can map error codes to error messages.
 #[proc_macro_attribute]
 pub fn error(
-    _args: proc_macro::TokenStream,
+    args: proc_macro::TokenStream,
     input: proc_macro::TokenStream,
 ) -> proc_macro::TokenStream {
+    let args = match args.is_empty() {
+        true => None,
+        false => Some(parse_macro_input!(args as ErrorArgs)),
+    };
     let mut error_enum = parse_macro_input!(input as syn::ItemEnum);
-    let error = error_codegen::generate(error_parser::parse(&mut error_enum));
+    let error = error_codegen::generate(error_parser::parse(&mut error_enum, args));
     proc_macro::TokenStream::from(error)
 }

+ 2 - 10
lang/attribute/interface/src/lib.rs

@@ -101,15 +101,7 @@ use syn::parse_macro_input;
 ///     use super::*;
 ///
 ///     #[state]
-///     pub struct CounterAuth {}
-///
-///     // TODO: remove this impl block after addressing
-///     //       https://github.com/project-serum/anchor/issues/71.
-///     impl CounterAuth {
-///         pub fn new(_ctx: Context<Empty>) -> Result<Self, ProgramError> {
-///             Ok(Self {})
-///         }
-///     }
+///     pub struct CounterAuth;
 ///
 ///     impl<'info> Auth<'info, Empty> for CounterAuth {
 ///         fn is_authorized(_ctx: Context<Empty>, current: u64, new: u64) -> ProgramResult {
@@ -216,7 +208,7 @@ pub fn interface(
                             #(#args_no_tys),*
                         };
                         let mut ix_data = anchor_lang::AnchorSerialize::try_to_vec(&ix)
-                            .map_err(|_| anchor_lang::solana_program::program_error::ProgramError::InvalidInstructionData)?;
+                            .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotSerialize)?;
                         let mut data = #sighash_tts.to_vec();
                         data.append(&mut ix_data);
                         let accounts = ctx.accounts.to_account_metas(None);

+ 1 - 1
lang/attribute/state/src/lib.rs

@@ -41,7 +41,7 @@ pub fn state(
                     fn size(&self) -> std::result::Result<u64, anchor_lang::solana_program::program_error::ProgramError> {
                         Ok(8 + self
                            .try_to_vec()
-                           .map_err(|_| ProgramError::Custom(1))?
+                           .map_err(|_| anchor_lang::__private::ErrorCode::AccountDidNotSerialize)?
                            .len() as u64)
                     }
                 }

+ 4 - 3
lang/src/account_info.rs

@@ -1,3 +1,4 @@
+use crate::error::ErrorCode;
 use crate::{Accounts, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos, ToAccountMetas};
 use solana_program::account_info::AccountInfo;
 use solana_program::entrypoint::ProgramResult;
@@ -11,7 +12,7 @@ impl<'info> Accounts<'info> for AccountInfo<'info> {
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];
@@ -25,7 +26,7 @@ impl<'info> AccountsInit<'info> for AccountInfo<'info> {
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
 
         let account = &accounts[0];
@@ -37,7 +38,7 @@ impl<'info> AccountsInit<'info> for AccountInfo<'info> {
         disc_bytes.copy_from_slice(&data[..8]);
         let discriminator = u64::from_le_bytes(disc_bytes);
         if discriminator != 0 {
-            return Err(ProgramError::InvalidAccountData);
+            return Err(ErrorCode::AccountDiscriminatorAlreadySet.into());
         }
 
         Ok(account.clone())

+ 2 - 1
lang/src/cpi_account.rs

@@ -1,3 +1,4 @@
+use crate::error::ErrorCode;
 use crate::{
     AccountDeserialize, Accounts, AccountsExit, ToAccountInfo, ToAccountInfos, ToAccountMetas,
 };
@@ -51,7 +52,7 @@ where
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];

+ 2 - 1
lang/src/cpi_state.rs

@@ -1,3 +1,4 @@
+use crate::error::ErrorCode;
 use crate::{
     AccountDeserialize, AccountSerialize, Accounts, AccountsExit, CpiStateContext, ProgramState,
     ToAccountInfo, ToAccountInfos, ToAccountMetas,
@@ -67,7 +68,7 @@ where
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];

+ 65 - 28
lang/src/error.rs

@@ -1,34 +1,71 @@
-use solana_program::program_error::ProgramError;
+use crate::error;
 
-// Error type that can be returned by internal framework code.
-#[doc(hidden)]
-#[derive(thiserror::Error, Debug)]
-pub enum Error {
-    #[error(transparent)]
-    ProgramError(#[from] ProgramError),
-    #[error("{0:?}")]
-    ErrorCode(#[from] ErrorCode),
-}
-
-#[derive(Debug, Clone, Copy)]
-#[repr(u32)]
+// Error codes that can be returned by internal framework code.
+#[error(offset = 0)]
 pub enum ErrorCode {
-    WrongSerialization = 1,
-}
+    // Instructions.
+    #[msg("8 byte instruction identifier not provided")]
+    InstructionMissing = 100,
+    #[msg("Fallback functions are not supported")]
+    InstructionFallbackNotFound,
+    #[msg("The program could not deserialize the given instruction")]
+    InstructionDidNotDeserialize,
+    #[msg("The program could not serialize the given instruction")]
+    InstructionDidNotSerialize,
 
-impl std::fmt::Display for ErrorCode {
-    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        <Self as std::fmt::Debug>::fmt(self, fmt)
-    }
-}
+    // IDL instructions.
+    #[msg("The program was compiled without idl instructions")]
+    IdlInstructionStub = 120,
+    #[msg("Invalid program given to the IDL instruction")]
+    IdlInstructionInvalidProgram,
+
+    // Constraints.
+    #[msg("A mut constraint was violated")]
+    ConstraintMut = 140,
+    #[msg("A belongs to constraint was violated")]
+    ConstraintBelongsTo,
+    #[msg("A signer constraint as violated")]
+    ConstraintSigner,
+    #[msg("A raw constraint was violated")]
+    ConstraintRaw,
+    #[msg("An owner constraint was violated")]
+    ConstraintOwner,
+    #[msg("A rent exemption constraint was violated")]
+    ConstraintRentExempt,
+    #[msg("A seeds constraint was violated")]
+    ConstraintSeeds,
+    #[msg("An executable constraint was violated")]
+    ConstraintExecutable,
+    #[msg("A state constraint was violated")]
+    ConstraintState,
+    #[msg("An associated constraint was violated")]
+    ConstraintAssociated,
+    #[msg("An associated init constraint was violated")]
+    ConstraintAssociatedInit,
+
+    // Accounts.
+    #[msg("The account discriminator was already set on this account")]
+    AccountDiscriminatorAlreadySet = 160,
+    #[msg("No 8 byte discriminator was found on the account")]
+    AccountDiscriminatorNotFound,
+    #[msg("8 byte discriminator did not match what was expected")]
+    AccountDiscriminatorMismatch,
+    #[msg("Failed to deserialize the account")]
+    AccountDidNotDeserialize,
+    #[msg("Failed to serialize the account")]
+    AccountDidNotSerialize,
+    #[msg("Not enough account keys given to the instruction")]
+    AccountNotEnoughKeys,
+    #[msg("The given account is not mutable")]
+    AccountNotMutable,
+    #[msg("The given account is not owned by the executing program")]
+    AccountNotProgramOwned,
 
-impl std::error::Error for ErrorCode {}
+    // State.
+    #[msg("The given state account does not have the correct address")]
+    StateInvalidAddress = 180,
 
-impl std::convert::From<Error> for ProgramError {
-    fn from(e: Error) -> ProgramError {
-        match e {
-            Error::ProgramError(e) => e,
-            Error::ErrorCode(c) => ProgramError::Custom(c as u32),
-        }
-    }
+    // Used for APIs that shouldn't be used anymore.
+    #[msg("The API being used is deprecated and should no longer be used")]
+    Deprecated = 299,
 }

+ 4 - 1
lang/src/lib.rs

@@ -239,7 +239,7 @@ pub mod __private {
     use solana_program::pubkey::Pubkey;
 
     pub use crate::ctor::Ctor;
-    pub use crate::error::Error;
+    pub use crate::error::{Error, ErrorCode};
     pub use anchor_attribute_account::ZeroCopyAccessor;
     pub use anchor_attribute_event::EventIndex;
     pub use base64;
@@ -249,6 +249,9 @@ pub mod __private {
         pub use crate::state::*;
     }
 
+    // The starting point for user defined error codes.
+    pub const ERROR_CODE_OFFSET: u32 = 300;
+
     // Calculates the size of an account, which may be larger than the deserialized
     // data in it. This trait is currently only used for `#[state]` accounts.
     #[doc(hidden)]

+ 12 - 11
lang/src/loader.rs

@@ -1,3 +1,4 @@
+use crate::error::ErrorCode;
 use crate::{
     Accounts, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos, ToAccountMetas, ZeroCopy,
 };
@@ -44,7 +45,7 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
         let mut disc_bytes = [0u8; 8];
         disc_bytes.copy_from_slice(&data[..8]);
         if disc_bytes != T::discriminator() {
-            return Err(ProgramError::InvalidAccountData);
+            return Err(ErrorCode::AccountDiscriminatorMismatch.into());
         }
 
         Ok(Loader::new(acc_info.clone()))
@@ -60,7 +61,7 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
         disc_bytes.copy_from_slice(&data[..8]);
         let discriminator = u64::from_le_bytes(disc_bytes);
         if discriminator != 0 {
-            return Err(ProgramError::InvalidAccountData);
+            return Err(ErrorCode::AccountDiscriminatorAlreadySet.into());
         }
 
         Ok(Loader::new(acc_info.clone()))
@@ -73,7 +74,7 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
         let mut disc_bytes = [0u8; 8];
         disc_bytes.copy_from_slice(&data[..8]);
         if disc_bytes != T::discriminator() {
-            return Err(ProgramError::InvalidAccountData);
+            return Err(ErrorCode::AccountDiscriminatorMismatch.into());
         }
 
         Ok(Ref::map(data, |data| bytemuck::from_bytes(&data[8..])))
@@ -84,7 +85,7 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
         // AccountInfo api allows you to borrow mut even if the account isn't
         // writable, so add this check for a better dev experience.
         if !self.acc_info.is_writable {
-            return Err(ProgramError::Custom(87)); // todo: proper error
+            return Err(ErrorCode::AccountNotMutable.into());
         }
 
         let data = self.acc_info.try_borrow_mut_data()?;
@@ -92,7 +93,7 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
         let mut disc_bytes = [0u8; 8];
         disc_bytes.copy_from_slice(&data[..8]);
         if disc_bytes != T::discriminator() {
-            return Err(ProgramError::InvalidAccountData);
+            return Err(ErrorCode::AccountDiscriminatorMismatch.into());
         }
 
         Ok(RefMut::map(data, |data| {
@@ -106,7 +107,7 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
         // AccountInfo api allows you to borrow mut even if the account isn't
         // writable, so add this check for a better dev experience.
         if !self.acc_info.is_writable {
-            return Err(ProgramError::Custom(87)); // todo: proper error
+            return Err(ErrorCode::AccountNotMutable.into());
         }
 
         let data = self.acc_info.try_borrow_mut_data()?;
@@ -116,7 +117,7 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
         disc_bytes.copy_from_slice(&data[..8]);
         let discriminator = u64::from_le_bytes(disc_bytes);
         if discriminator != 0 {
-            return Err(ProgramError::InvalidAccountData);
+            return Err(ErrorCode::AccountDiscriminatorAlreadySet.into());
         }
 
         Ok(RefMut::map(data, |data| {
@@ -132,13 +133,13 @@ impl<'info, T: ZeroCopy> Accounts<'info> for Loader<'info, T> {
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];
         let l = Loader::try_from(account)?;
         if l.acc_info.owner != program_id {
-            return Err(ProgramError::Custom(1)); // todo: proper error
+            return Err(ErrorCode::AccountNotProgramOwned.into());
         }
         Ok(l)
     }
@@ -151,13 +152,13 @@ impl<'info, T: ZeroCopy> AccountsInit<'info> for Loader<'info, T> {
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];
         let l = Loader::try_from_init(account)?;
         if l.acc_info.owner != program_id {
-            return Err(ProgramError::Custom(1)); // todo: proper error
+            return Err(ErrorCode::AccountNotProgramOwned.into());
         }
         Ok(l)
     }

+ 6 - 5
lang/src/program_account.rs

@@ -1,3 +1,4 @@
+use crate::error::ErrorCode;
 use crate::{
     AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AccountsInit, CpiAccount,
     ToAccountInfo, ToAccountInfos, ToAccountMetas,
@@ -52,7 +53,7 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T>
         disc_bytes.copy_from_slice(&data[..8]);
         let discriminator = u64::from_le_bytes(disc_bytes);
         if discriminator != 0 {
-            return Err(ProgramError::InvalidAccountData);
+            return Err(ErrorCode::AccountDiscriminatorAlreadySet.into());
         }
 
         Ok(ProgramAccount::new(
@@ -72,13 +73,13 @@ where
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];
         let pa = ProgramAccount::try_from(account)?;
         if pa.inner.info.owner != program_id {
-            return Err(ProgramError::Custom(1)); // todo: proper error
+            return Err(ErrorCode::AccountNotProgramOwned.into());
         }
         Ok(pa)
     }
@@ -94,13 +95,13 @@ where
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];
         let pa = ProgramAccount::try_from_init(account)?;
         if pa.inner.info.owner != program_id {
-            return Err(ProgramError::Custom(1)); // todo: proper error
+            return Err(ErrorCode::AccountNotProgramOwned.into());
         }
         Ok(pa)
     }

+ 4 - 3
lang/src/state.rs

@@ -1,3 +1,4 @@
+use crate::error::ErrorCode;
 use crate::{
     AccountDeserialize, AccountSerialize, Accounts, AccountsExit, CpiAccount, ToAccountInfo,
     ToAccountInfos, ToAccountMetas,
@@ -59,20 +60,20 @@ where
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];
 
         if account.key != &Self::address(program_id) {
             solana_program::msg!("Invalid state address");
-            return Err(ProgramError::Custom(1)); // todo: proper error.
+            return Err(ErrorCode::StateInvalidAddress.into());
         }
 
         let pa = ProgramState::try_from(account)?;
         if pa.inner.info.owner != program_id {
             solana_program::msg!("Invalid state owner");
-            return Err(ProgramError::Custom(1)); // todo: proper error.
+            return Err(ErrorCode::AccountNotProgramOwned.into());
         }
         Ok(pa)
     }

+ 2 - 1
lang/src/sysvar.rs

@@ -1,3 +1,4 @@
+use crate::error::ErrorCode;
 use crate::{Accounts, AccountsExit, ToAccountInfo, ToAccountInfos, ToAccountMetas};
 use solana_program::account_info::AccountInfo;
 use solana_program::entrypoint::ProgramResult;
@@ -38,7 +39,7 @@ impl<'info, T: solana_program::sysvar::Sysvar> Accounts<'info> for Sysvar<'info,
         accounts: &mut &[AccountInfo<'info>],
     ) -> Result<Self, ProgramError> {
         if accounts.is_empty() {
-            return Err(ProgramError::NotEnoughAccountKeys);
+            return Err(ErrorCode::AccountNotEnoughKeys.into());
         }
         let account = &accounts[0];
         *accounts = &accounts[1..];

+ 14 - 15
lang/syn/src/codegen/accounts/constraints.rs

@@ -130,7 +130,7 @@ pub fn generate_constraint_mut(f: &Field, _c: &ConstraintMut) -> proc_macro2::To
     let ident = &f.ident;
     quote! {
         if !#ident.to_account_info().is_writable {
-            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(36)); // todo: error codes
+            return Err(anchor_lang::__private::ErrorCode::ConstraintMut.into());
         }
     }
 }
@@ -147,7 +147,7 @@ pub fn generate_constraint_belongs_to(
     };
     quote! {
         if &#field.#target != #target.to_account_info().key {
-            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes
+            return Err(anchor_lang::__private::ErrorCode::ConstraintBelongsTo.into());
         }
     }
 }
@@ -167,7 +167,7 @@ pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macr
         // This check will be performed on the other end of the invocation.
         if cfg!(not(feature = "cpi")) {
             if !#info.to_account_info().is_signer {
-                return Err(anchor_lang::solana_program::program_error::ProgramError::MissingRequiredSignature);
+                return Err(anchor_lang::__private::ErrorCode::ConstraintSigner.into());
             }
         }
     }
@@ -181,7 +181,7 @@ pub fn generate_constraint_literal(c: &ConstraintLiteral) -> proc_macro2::TokenS
     };
     quote! {
         if !(#lit) {
-            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes
+            return Err(anchor_lang::__private::ErrorCode::Deprecated.into());
         }
     }
 }
@@ -190,7 +190,7 @@ pub fn generate_constraint_raw(c: &ConstraintRaw) -> proc_macro2::TokenStream {
     let raw = &c.raw;
     quote! {
         if !(#raw) {
-            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(14)); // todo: error codes
+            return Err(anchor_lang::__private::ErrorCode::ConstraintRaw.into());
         }
     }
 }
@@ -200,7 +200,7 @@ pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2:
     let owner_target = c.owner_target.clone();
     quote! {
         if #ident.to_account_info().owner != #owner_target.to_account_info().key {
-            return Err(ProgramError::Custom(76)); // todo: proper error.
+            return Err(anchor_lang::__private::ErrorCode::ConstraintOwner.into());
         }
     }
 }
@@ -220,7 +220,7 @@ pub fn generate_constraint_rent_exempt(
         ConstraintRentExempt::Skip => quote! {},
         ConstraintRentExempt::Enforce => quote! {
             if !rent.is_exempt(#info.lamports(), #info.try_data_len()?) {
-                return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(2)); // todo: error codes
+                return Err(anchor_lang::__private::ErrorCode::ConstraintRentExempt.into());
             }
         },
     }
@@ -233,9 +233,9 @@ pub fn generate_constraint_seeds(f: &Field, c: &ConstraintSeeds) -> proc_macro2:
         let program_signer = Pubkey::create_program_address(
             &[#seeds],
             program_id,
-        ).map_err(|_| anchor_lang::solana_program::program_error::ProgramError::Custom(1))?; // todo
+        ).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?;
         if #name.to_account_info().key != &program_signer {
-            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo
+            return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
         }
     }
 }
@@ -247,7 +247,7 @@ pub fn generate_constraint_executable(
     let name = &f.ident;
     quote! {
         if !#name.to_account_info().executable {
-            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(5)) // todo
+            return Err(anchor_lang::__private::ErrorCode::ConstraintExecutable.into());
         }
     }
 }
@@ -263,10 +263,10 @@ pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2:
         // Checks the given state account is the canonical state account for
         // the target program.
         if #ident.to_account_info().key != &anchor_lang::CpiState::<#account_ty>::address(#program_target.to_account_info().key) {
-            return Err(ProgramError::Custom(1)); // todo: proper error.
+            return Err(anchor_lang::__private::ErrorCode::ConstraintState.into());
         }
         if #ident.to_account_info().owner != #program_target.to_account_info().key {
-            return Err(ProgramError::Custom(1)); // todo: proper error.
+            return Err(anchor_lang::__private::ErrorCode::ConstraintState.into());
         }
     }
 }
@@ -371,7 +371,7 @@ pub fn generate_constraint_associated_init(
             #associated_pubkey_and_nonce
 
             if &__associated_field != #field.key {
-                return Err(ProgramError::Custom(45)); // todo: proper error.
+                return Err(anchor_lang::__private::ErrorCode::ConstraintAssociatedInit.into());
             }
             let lamports = rent.minimum_balance(space);
             let ix = anchor_lang::solana_program::system_instruction::create_account(
@@ -417,8 +417,7 @@ pub fn generate_constraint_associated_seeds(
     quote! {
         #generated_associated_pubkey_and_nonce
         if #name.to_account_info().key != &__associated_field {
-            // TODO: proper error.
-            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(45));
+            return Err(anchor_lang::__private::ErrorCode::ConstraintAssociated.into());
         }
     }
 }

+ 16 - 9
lang/syn/src/codegen/error.rs

@@ -32,6 +32,14 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream {
         })
         .collect();
 
+    let offset = match error.args {
+        None => quote! { anchor_lang::__private::ERROR_CODE_OFFSET},
+        Some(args) => {
+            let offset = &args.offset;
+            quote! { #offset }
+        }
+    };
+
     quote! {
         /// Anchor generated Result to be used as the return type for the
         /// program.
@@ -40,15 +48,16 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream {
         /// Anchor generated error allowing one to easily return a
         /// `ProgramError` or a custom, user defined error code by utilizing
         /// its `From` implementation.
+        #[doc(hidden)]
         #[derive(thiserror::Error, Debug)]
         pub enum Error {
             #[error(transparent)]
-            ProgramError(#[from] ProgramError),
+            ProgramError(#[from] anchor_lang::solana_program::program_error::ProgramError),
             #[error(transparent)]
             ErrorCode(#[from] #enum_name),
         }
 
-        #[derive(Debug, Clone, Copy)]
+        #[derive(std::fmt::Debug, Clone, Copy)]
         #[repr(u32)]
         #error_enum
 
@@ -62,19 +71,17 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream {
 
         impl std::error::Error for #enum_name {}
 
-        impl std::convert::From<Error> for ProgramError {
-            fn from(e: Error) -> ProgramError {
-            // Errors 0-100 are reserved for the framework.
-            let error_offset = 100u32;
+        impl std::convert::From<Error> for anchor_lang::solana_program::program_error::ProgramError {
+            fn from(e: Error) -> anchor_lang::solana_program::program_error::ProgramError {
                 match e {
                     Error::ProgramError(e) => e,
-                    Error::ErrorCode(c) => ProgramError::Custom(c as u32 + error_offset),
+                    Error::ErrorCode(c) => anchor_lang::solana_program::program_error::ProgramError::Custom(c as u32 + #offset),
                 }
             }
         }
 
-        impl std::convert::From<#enum_name> for ProgramError {
-            fn from(e: #enum_name) -> ProgramError {
+        impl std::convert::From<#enum_name> for anchor_lang::solana_program::program_error::ProgramError {
+            fn from(e: #enum_name) -> anchor_lang::solana_program::program_error::ProgramError {
                 let err: Error = e.into();
                 err.into()
             }

+ 1 - 1
lang/syn/src/codegen/program/cpi.rs

@@ -77,7 +77,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                         let ix = {
                             let ix = instruction::#ix_variant;
                             let mut ix_data = AnchorSerialize::try_to_vec(&ix)
-                                .map_err(|_| ProgramError::InvalidInstructionData)?;
+                                .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotSerialize)?;
                             let mut data = #sighash_tts.to_vec();
                             data.append(&mut ix_data);
                             let accounts = ctx.to_account_metas(None);

+ 5 - 5
lang/syn/src/codegen/program/dispatch.rs

@@ -20,7 +20,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                 quote! {
                     #sighash_tts => {
                         let ix = instruction::state::#ix_name::deserialize(&mut ix_data)
-                            .map_err(|_| ProgramError::Custom(1))?; // todo: error code
+                            .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
                         let instruction::state::#variant_arm = ix;
                         __private::__state::__ctor(program_id, accounts, #(#ctor_args),*)
                     }
@@ -53,7 +53,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                         quote! {
                             #sighash_tts => {
                                 let ix = instruction::state::#ix_name::deserialize(&mut ix_data)
-                                    .map_err(|_| ProgramError::Custom(1))?; // todo: error code
+                                    .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
                                 let instruction::state::#variant_arm = ix;
                                 __private::__state::#ix_method_name(program_id, accounts, #(#ix_arg_names),*)
                             }
@@ -109,7 +109,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                                     #sighash_tts => {
                                         #args_struct
                                         let ix = Args::deserialize(&mut ix_data)
-                                            .map_err(|_| ProgramError::Custom(1))?; // todo: error code
+                                            .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
                                         let Args {
                                             #(#ix_arg_names),*
                                         } = ix;
@@ -139,7 +139,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
             quote! {
                 #sighash_tts => {
                     let ix = instruction::#ix_name::deserialize(&mut ix_data)
-                        .map_err(|_| ProgramError::Custom(1))?; // todo: error code
+                        .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
                     let instruction::#variant_arm = ix;
                     __private::__global::#ix_method_name(program_id, accounts, #(#ix_arg_names),*)
                 }
@@ -182,7 +182,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                 #(#global_dispatch_arms)*
                 _ => {
                     msg!("Fallback functions are not supported. If you have a use case, please file an issue.");
-                    Err(ProgramError::Custom(99))
+                    Err(anchor_lang::__private::ErrorCode::InstructionFallbackNotFound.into())
                 }
             }
         }

+ 1 - 1
lang/syn/src/codegen/program/entry.rs

@@ -52,7 +52,7 @@ pub fn generate(_program: &Program) -> proc_macro2::TokenStream {
                 msg!("anchor-debug is active");
             }
             if ix_data.len() < 8 {
-                return Err(ProgramError::Custom(99));
+                return Err(anchor_lang::__private::ErrorCode::InstructionMissing.into());
             }
 
             // Split the instruction data into the first 8 byte method

+ 6 - 6
lang/syn/src/codegen/program/handlers.rs

@@ -20,7 +20,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                 let mut data: &[u8] = idl_ix_data;
 
                 let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data)
-                    .map_err(|_| ProgramError::Custom(2))?; // todo
+                    .map_err(|_| anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
 
                 match ix {
                     anchor_lang::idl::IdlInstruction::Create { data_len } => {
@@ -55,7 +55,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
             #[inline(never)]
             #[cfg(feature = "no-idl")]
             pub fn __idl_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult {
-                Err(anchor_lang::solana_program::program_error::ProgramError::Custom(99))
+                Err(anchor_lang::__private::ErrorCode::IdlInstructionStub.into())
             }
 
             // One time IDL account initializer. Will faill on subsequent
@@ -67,7 +67,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                 data_len: u64,
             ) -> ProgramResult {
                 if program_id != accounts.program.key {
-                    return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(98)); // todo proper error
+                    return Err(anchor_lang::__private::ErrorCode::IdlInstructionInvalidProgram.into());
                 }
                 // Create the IDL's account.
                 let from = accounts.from.key;
@@ -336,7 +336,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                                 ) -> ProgramResult {
                                     let mut remaining_accounts: &[AccountInfo] = accounts;
                                     if remaining_accounts.is_empty() {
-                                        return Err(ProgramError::Custom(1)); // todo
+                                        return Err(anchor_lang::__private::ErrorCode::AccountNotEnoughKeys.into());
                                     }
 
                                     let state_account = &remaining_accounts[0];
@@ -374,7 +374,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
                                 ) -> ProgramResult {
                                     let mut remaining_accounts: &[AccountInfo] = accounts;
                                     if remaining_accounts.is_empty() {
-                                        return Err(ProgramError::Custom(1)); // todo
+                                        return Err(anchor_lang::__private::ErrorCode::AccountNotEnoughKeys.into());
                                     }
 
                                     // Deserialize the program state account.
@@ -459,7 +459,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
 
                                             let mut remaining_accounts: &[AccountInfo] = accounts;
                                             if remaining_accounts.is_empty() {
-                                                return Err(ProgramError::Custom(1)); // todo
+                                                return Err(anchor_lang::__private::ErrorCode::AccountNotEnoughKeys.into());
                                             }
 
                                             // Deserialize the program state account.

+ 4 - 2
lang/syn/src/idl/file.rs

@@ -11,6 +11,8 @@ use std::iter::FromIterator;
 use std::path::Path;
 
 const DERIVE_NAME: &str = "Accounts";
+// TODO: sharee this with `anchor_lang` crate.
+const ERROR_CODE_OFFSET: u32 = 300;
 
 // Parse an entire interface file.
 pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
@@ -128,12 +130,12 @@ pub fn parse(filename: impl AsRef<Path>) -> Result<Idl> {
             }
         },
     };
-    let error = parse_error_enum(&f).map(|mut e| error::parse(&mut e));
+    let error = parse_error_enum(&f).map(|mut e| error::parse(&mut e, None));
     let error_codes = error.as_ref().map(|e| {
         e.codes
             .iter()
             .map(|code| IdlErrorCode {
-                code: 100 + code.id,
+                code: ERROR_CODE_OFFSET + code.id,
                 name: code.ident.to_string(),
                 msg: code.msg.clone(),
             })

+ 22 - 1
lang/syn/src/lib.rs

@@ -5,7 +5,8 @@ use parser::program as program_parser;
 use proc_macro2::{Span, TokenStream};
 use quote::ToTokens;
 use std::ops::Deref;
-use syn::parse::{Parse, ParseStream, Result as ParseResult};
+use syn::ext::IdentExt;
+use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
 use syn::punctuated::Punctuated;
 use syn::spanned::Spanned;
 use syn::{
@@ -212,6 +213,26 @@ pub struct Error {
     pub raw_enum: ItemEnum,
     pub ident: Ident,
     pub codes: Vec<ErrorCode>,
+    pub args: Option<ErrorArgs>,
+}
+
+#[derive(Debug)]
+pub struct ErrorArgs {
+    pub offset: LitInt,
+}
+
+impl Parse for ErrorArgs {
+    fn parse(stream: ParseStream) -> ParseResult<Self> {
+        let offset_span = stream.span();
+        let offset = stream.call(Ident::parse_any)?;
+        if offset.to_string().as_str() != "offset" {
+            return Err(ParseError::new(offset_span, "expected keyword offset"));
+        }
+        stream.parse::<Token![=]>()?;
+        Ok(ErrorArgs {
+            offset: stream.parse()?,
+        })
+    }
 }
 
 #[derive(Debug)]

+ 3 - 3
lang/syn/src/parser/error.rs

@@ -1,7 +1,7 @@
-use crate::{Error, ErrorCode};
+use crate::{Error, ErrorArgs, ErrorCode};
 
 // Removes any internal #[msg] attributes, as they are inert.
-pub fn parse(error_enum: &mut syn::ItemEnum) -> Error {
+pub fn parse(error_enum: &mut syn::ItemEnum, args: Option<ErrorArgs>) -> Error {
     let ident = error_enum.ident.clone();
     let mut last_discriminant = 0;
     let codes: Vec<ErrorCode> = error_enum
@@ -30,12 +30,12 @@ pub fn parse(error_enum: &mut syn::ItemEnum) -> Error {
             ErrorCode { id, ident, msg }
         })
         .collect();
-
     Error {
         name: error_enum.ident.to_string(),
         raw_enum: error_enum.clone(),
         ident,
         codes,
+        args,
     }
 }
 

+ 1 - 1
ts/package.json

@@ -22,7 +22,7 @@
   },
   "dependencies": {
     "@project-serum/borsh": "^0.2.2",
-    "@solana/web3.js": "^1.11.0",
+    "@solana/web3.js": "^1.17.0",
     "base64-js": "^1.5.1",
     "bn.js": "^5.1.2",
     "bs58": "^4.0.1",

+ 158 - 0
ts/src/error.ts

@@ -6,7 +6,165 @@ export class ProgramError extends Error {
     super(...params);
   }
 
+  public static parse(
+    err: any,
+    idlErrors: Map<number, string>
+  ): ProgramError | null {
+    // TODO: don't rely on the error string. web3.js should preserve the error
+    //       code information instead of giving us an untyped string.
+    let components = err.toString().split("custom program error: ");
+    if (components.length !== 2) {
+      return null;
+    }
+
+    let errorCode: number;
+    try {
+      errorCode = parseInt(components[1]);
+    } catch (parseErr) {
+      return null;
+    }
+
+    // Parse user error.
+    let errorMsg = idlErrors.get(errorCode);
+    if (errorMsg !== undefined) {
+      return new ProgramError(errorCode, errorMsg);
+    }
+
+    // Parse framework internal error.
+    errorMsg = LangErrorMessage.get(errorCode);
+    if (errorMsg !== undefined) {
+      return new ProgramError(errorCode, errorMsg);
+    }
+
+    // Unable to parse the error. Just return the untranslated error.
+    return null;
+  }
+
   public toString(): string {
     return this.msg;
   }
 }
+
+const LangErrorCode = {
+  // Instructions.
+  InstructionMissing: 100,
+  InstructionFallbackNotFound: 101,
+  InstructionDidNotDeserialize: 102,
+  InstructionDidNotSerialize: 103,
+
+  // IDL instructions.
+  IdlInstructionStub: 120,
+  IdlInstructionInvalidProgram: 121,
+
+  // Constraints.
+  ConstraintMut: 140,
+  ConstraintBelongsTo: 141,
+  ConstraintSigner: 142,
+  ConstraintRaw: 143,
+  ConstraintOwner: 144,
+  ConstraintRentExempt: 145,
+  ConstraintSeeds: 146,
+  ConstraintExecutable: 147,
+  ConstraintState: 148,
+  ConstraintAssociated: 149,
+  ConstraintAssociatedInit: 150,
+
+  // Accounts.
+  AccountDiscriminatorAlreadySet: 160,
+  AccountDiscriminatorNotFound: 161,
+  AccountDiscriminatorMismatch: 162,
+  AccountDidNotDeserialize: 163,
+  AccountDidNotSerialize: 164,
+  AccountNotEnoughKeys: 165,
+  AccountNotMutable: 166,
+  AccountNotProgramOwned: 167,
+
+  // State.
+  StateInvalidAddress: 180,
+
+  // Used for APIs that shouldn't be used anymore.
+  Deprecated: 299,
+};
+
+const LangErrorMessage = new Map([
+  // Instructions.
+  [
+    LangErrorCode.InstructionMissing,
+    "8 byte instruction identifier not provided",
+  ],
+  [
+    LangErrorCode.InstructionFallbackNotFound,
+    "Fallback functions are not supported",
+  ],
+  [
+    LangErrorCode.InstructionDidNotDeserialize,
+    "The program could not deserialize the given instruction",
+  ],
+  [
+    LangErrorCode.InstructionDidNotSerialize,
+    "The program could not serialize the given instruction",
+  ],
+
+  // Idl instructions.
+  [
+    LangErrorCode.IdlInstructionStub,
+    "The program was compiled without idl instructions",
+  ],
+  [
+    LangErrorCode.IdlInstructionInvalidProgram,
+    "The transaction was given an invalid program for the IDL instruction",
+  ],
+
+  // Constraints.
+  [LangErrorCode.ConstraintMut, "A mut constraint was violated"],
+  [LangErrorCode.ConstraintBelongsTo, "A belongs_to constraint was violated"],
+  [LangErrorCode.ConstraintSigner, "A signer constraint was violated"],
+  [LangErrorCode.ConstraintRaw, "A raw constraint as violated"],
+  [LangErrorCode.ConstraintOwner, "An owner constraint was violated"],
+  [LangErrorCode.ConstraintRentExempt, "A rent exempt constraint was violated"],
+  [LangErrorCode.ConstraintSeeds, "A seeds constraint was violated"],
+  [LangErrorCode.ConstraintExecutable, "An executable constraint was violated"],
+  [LangErrorCode.ConstraintState, "A state constraint was violated"],
+  [LangErrorCode.ConstraintAssociated, "An associated constraint was violated"],
+  [
+    LangErrorCode.ConstraintAssociatedInit,
+    "An associated init constraint was violated",
+  ],
+
+  // Accounts.
+  [
+    LangErrorCode.AccountDiscriminatorAlreadySet,
+    "The account discriminator was already set on this account",
+  ],
+  [
+    LangErrorCode.AccountDiscriminatorNotFound,
+    "No 8 byte discriminator was found on the account",
+  ],
+  [
+    LangErrorCode.AccountDiscriminatorMismatch,
+    "8 byte discriminator did not match what was expected",
+  ],
+  [LangErrorCode.AccountDidNotDeserialize, "Failed to deserialize the account"],
+  [LangErrorCode.AccountDidNotSerialize, "Failed to serialize the account"],
+  [
+    LangErrorCode.AccountNotEnoughKeys,
+    "Not enough account keys given to the instruction",
+  ],
+  [LangErrorCode.AccountNotMutable, "The given account is not mutable"],
+  [
+    LangErrorCode.AccountNotProgramOwned,
+    "The given account is not owned by the executing program",
+  ],
+
+  // State.
+  [
+    LangErrorCode.StateInvalidAddress,
+    "The given state account does not have the correct address",
+  ],
+
+  // Misc.
+  [
+    LangErrorCode.Deprecated,
+    "The API being used is deprecated and should no longer be used",
+  ],
+]);

+ 0 - 24
ts/src/program/common.ts

@@ -1,7 +1,6 @@
 import EventEmitter from "eventemitter3";
 import { PublicKey } from "@solana/web3.js";
 import { Idl, IdlInstruction, IdlAccountItem, IdlStateMethod } from "../idl";
-import { ProgramError } from "../error";
 import { Accounts } from "./context";
 
 export type Subscription = {
@@ -56,29 +55,6 @@ export function validateAccounts(
   });
 }
 
-export function translateError(
-  idlErrors: Map<number, string>,
-  err: any
-): Error | null {
-  // TODO: don't rely on the error string. web3.js should preserve the error
-  //       code information instead of giving us an untyped string.
-  let components = err.toString().split("custom program error: ");
-  if (components.length === 2) {
-    try {
-      const errorCode = parseInt(components[1]);
-      let errorMsg = idlErrors.get(errorCode);
-      if (errorMsg === undefined) {
-        // Unexpected error code so just throw the untranslated error.
-        return null;
-      }
-      return new ProgramError(errorCode, errorMsg);
-    } catch (parseErr) {
-      // Unable to parse the error. Just return the untranslated error.
-      return null;
-    }
-  }
-}
-
 // Translates an address to a Pubkey.
 export function translateAddress(address: Address): PublicKey {
   if (typeof address === "string") {

+ 2 - 2
ts/src/program/namespace/rpc.ts

@@ -1,9 +1,9 @@
 import { TransactionSignature } from "@solana/web3.js";
 import Provider from "../../provider";
 import { IdlInstruction } from "../../idl";
-import { translateError } from "../common";
 import { splitArgsAndCtx } from "../context";
 import { TransactionFn } from "./transaction";
+import { ProgramError } from "../../error";
 
 export default class RpcFactory {
   public static build(
@@ -20,7 +20,7 @@ export default class RpcFactory {
         return txSig;
       } catch (err) {
         console.log("Translating error", err);
-        let translatedErr = translateError(idlErrors, err);
+        let translatedErr = ProgramError.parse(err, idlErrors);
         if (translatedErr === null) {
           throw err;
         }

+ 2 - 2
ts/src/program/namespace/simulate.ts

@@ -1,12 +1,12 @@
 import { PublicKey } from "@solana/web3.js";
 import Provider from "../../provider";
 import { IdlInstruction } from "../../idl";
-import { translateError } from "../common";
 import { splitArgsAndCtx } from "../context";
 import { TransactionFn } from "./transaction";
 import { EventParser } from "../event";
 import Coder from "../../coder";
 import { Idl } from "../../idl";
+import { ProgramError } from "../../error";
 
 export default class SimulateFactory {
   public static build(
@@ -26,7 +26,7 @@ export default class SimulateFactory {
         resp = await provider.simulate(tx, ctx.signers, ctx.options);
       } catch (err) {
         console.log("Translating error", err);
-        let translatedErr = translateError(idlErrors, err);
+        let translatedErr = ProgramError.parse(err, idlErrors);
         if (translatedErr === null) {
           throw err;
         }

+ 22 - 6
ts/yarn.lock

@@ -676,13 +676,14 @@
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
-"@solana/web3.js@^1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.11.0.tgz#1cc9a25381687c82e444ad0633f028e050a06753"
-  integrity sha512-kmngWxntzp0HNhWInd7/3g2uqxdOrahvaHOyjilcRe+WCiC777gERz3+eIAbxIYx2zAZPjy02MZzLgoRHccZoQ==
+"@solana/web3.js@^1.17.0":
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.17.0.tgz#51775bd17af77132450c22ac175870d4a9721b9b"
+  integrity sha512-PBOHY260CudciLwBgwt1U8upwCS1Jq0BbS6EVyX0tz6Tj14Dp4i87dQNyntentNiGQQ+yWBIk4vJEm+PMCSd/A==
   dependencies:
     "@babel/runtime" "^7.12.5"
     bn.js "^5.0.0"
+    borsh "^0.4.0"
     bs58 "^4.0.1"
     buffer "6.0.1"
     buffer-layout "^1.2.0"
@@ -728,7 +729,7 @@
   dependencies:
     "@babel/types" "^7.3.0"
 
-"@types/bn.js@^4.11.6":
+"@types/bn.js@^4.11.5", "@types/bn.js@^4.11.6":
   version "4.11.6"
   resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c"
   integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==
@@ -1261,6 +1262,16 @@ bn.js@^5.1.2:
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
   integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
 
+borsh@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.4.0.tgz#9dd6defe741627f1315eac2a73df61421f6ddb9f"
+  integrity sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==
+  dependencies:
+    "@types/bn.js" "^4.11.5"
+    bn.js "^5.0.0"
+    bs58 "^4.0.0"
+    text-encoding-utf-8 "^1.0.2"
+
 brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -1309,7 +1320,7 @@ bs-logger@0.x:
   dependencies:
     fast-json-stable-stringify "2.x"
 
-bs58@^4.0.1:
+bs58@^4.0.0, bs58@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
   integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo=
@@ -5035,6 +5046,11 @@ test-exclude@^6.0.0:
     glob "^7.1.4"
     minimatch "^3.0.4"
 
+text-encoding-utf-8@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
+  integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
+
 text-extensions@^1.0.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"