From 339da4f64876cf0a877c78f562c843df46411739 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Mon, 31 May 2021 19:30:13 -0700 Subject: [PATCH] lang: Framework defined error codes --- examples/errors/programs/errors/src/lib.rs | 38 +++++ examples/errors/tests/errors.js | 75 ++++++++- lang/attribute/account/src/lib.rs | 20 +-- lang/attribute/error/src/lib.rs | 9 +- lang/attribute/interface/src/lib.rs | 12 +- lang/attribute/state/src/lib.rs | 2 +- lang/src/account_info.rs | 7 +- lang/src/cpi_account.rs | 3 +- lang/src/cpi_state.rs | 3 +- lang/src/error.rs | 93 +++++++---- lang/src/lib.rs | 5 +- lang/src/loader.rs | 23 +-- lang/src/program_account.rs | 11 +- lang/src/state.rs | 7 +- lang/src/sysvar.rs | 3 +- lang/syn/src/codegen/accounts/constraints.rs | 29 ++-- lang/syn/src/codegen/error.rs | 25 +-- lang/syn/src/codegen/program/cpi.rs | 2 +- lang/syn/src/codegen/program/dispatch.rs | 10 +- lang/syn/src/codegen/program/entry.rs | 2 +- lang/syn/src/codegen/program/handlers.rs | 12 +- lang/syn/src/idl/file.rs | 6 +- lang/syn/src/lib.rs | 23 ++- lang/syn/src/parser/error.rs | 6 +- ts/package.json | 2 +- ts/src/error.ts | 158 +++++++++++++++++++ ts/src/program/common.ts | 24 --- ts/src/program/namespace/rpc.ts | 4 +- ts/src/program/namespace/simulate.ts | 4 +- ts/yarn.lock | 28 +++- 30 files changed, 488 insertions(+), 158 deletions(-) diff --git a/examples/errors/programs/errors/src/lib.rs b/examples/errors/programs/errors/src/lib.rs index 3eaa9e1f48..d87fbf4386 100644 --- a/examples/errors/programs/errors/src/lib.rs +++ b/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) -> Result<()> { Err(MyError::Hello.into()) } @@ -17,11 +18,48 @@ mod errors { pub fn hello_next(_ctx: Context) -> Result<()> { Err(MyError::HelloNext.into()) } + + pub fn mut_error(_ctx: Context) -> Result<()> { + Ok(()) + } + + pub fn belongs_to_error(_ctx: Context) -> Result<()> { + Ok(()) + } + + pub fn signer_error(_ctx: Context) -> 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")] diff --git a/examples/errors/tests/errors.js b/examples/errors/tests/errors.js index 5eb906b94b..d447b2cbe7 100644 --- a/examples/errors/tests/errors.js +++ b/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); } }); }); diff --git a/lang/attribute/account/src/lib.rs b/lang/attribute/account/src/lib.rs index 597ea47bf6..5efe883c7b 100644 --- a/lang/attribute/account/src/lib.rs +++ b/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 { 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(&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 { 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 { 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 }) } diff --git a/lang/attribute/error/src/lib.rs b/lang/attribute/error/src/lib.rs index 1d9e72095f..4f1748d249 100644 --- a/lang/attribute/error/src/lib.rs +++ b/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 = Result` 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) } diff --git a/lang/attribute/interface/src/lib.rs b/lang/attribute/interface/src/lib.rs index fab6336138..9380fff0e9 100644 --- a/lang/attribute/interface/src/lib.rs +++ b/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) -> Result { -/// Ok(Self {}) -/// } -/// } +/// pub struct CounterAuth; /// /// impl<'info> Auth<'info, Empty> for CounterAuth { /// fn is_authorized(_ctx: Context, 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); diff --git a/lang/attribute/state/src/lib.rs b/lang/attribute/state/src/lib.rs index c6387affca..4b46671c4c 100644 --- a/lang/attribute/state/src/lib.rs +++ b/lang/attribute/state/src/lib.rs @@ -41,7 +41,7 @@ pub fn state( fn size(&self) -> std::result::Result { Ok(8 + self .try_to_vec() - .map_err(|_| ProgramError::Custom(1))? + .map_err(|_| anchor_lang::__private::ErrorCode::AccountDidNotSerialize)? .len() as u64) } } diff --git a/lang/src/account_info.rs b/lang/src/account_info.rs index 2a4c583dcf..243d264d7a 100644 --- a/lang/src/account_info.rs +++ b/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 { 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 { 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()) diff --git a/lang/src/cpi_account.rs b/lang/src/cpi_account.rs index 9547b4733b..cd60684b7e 100644 --- a/lang/src/cpi_account.rs +++ b/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 { if accounts.is_empty() { - return Err(ProgramError::NotEnoughAccountKeys); + return Err(ErrorCode::AccountNotEnoughKeys.into()); } let account = &accounts[0]; *accounts = &accounts[1..]; diff --git a/lang/src/cpi_state.rs b/lang/src/cpi_state.rs index bc86d3df14..051dae57e2 100644 --- a/lang/src/cpi_state.rs +++ b/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 { if accounts.is_empty() { - return Err(ProgramError::NotEnoughAccountKeys); + return Err(ErrorCode::AccountNotEnoughKeys.into()); } let account = &accounts[0]; *accounts = &accounts[1..]; diff --git a/lang/src/error.rs b/lang/src/error.rs index 66342d588a..3b192c0167 100644 --- a/lang/src/error.rs +++ b/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> { - ::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 violatged")] + 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 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, } diff --git a/lang/src/lib.rs b/lang/src/lib.rs index f6ca642d7c..f1f962b43b 100644 --- a/lang/src/lib.rs +++ b/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)] diff --git a/lang/src/loader.rs b/lang/src/loader.rs index bdee14d687..9e35cb3e25 100644 --- a/lang/src/loader.rs +++ b/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 { 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 { 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) } diff --git a/lang/src/program_account.rs b/lang/src/program_account.rs index a5776dd4c3..e3f9b31d34 100644 --- a/lang/src/program_account.rs +++ b/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 { 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 { 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) } diff --git a/lang/src/state.rs b/lang/src/state.rs index c483472138..c7385e7a3f 100644 --- a/lang/src/state.rs +++ b/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 { 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) } diff --git a/lang/src/sysvar.rs b/lang/src/sysvar.rs index fd2b2f2468..1b09c098e8 100644 --- a/lang/src/sysvar.rs +++ b/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 { if accounts.is_empty() { - return Err(ProgramError::NotEnoughAccountKeys); + return Err(ErrorCode::AccountNotEnoughKeys.into()); } let account = &accounts[0]; *accounts = &accounts[1..]; diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 99023f7d90..6033070183 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/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.into())?; 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()); } } } diff --git a/lang/syn/src/codegen/error.rs b/lang/syn/src/codegen/error.rs index baf2283dbe..abc57885bc 100644 --- a/lang/syn/src/codegen/error.rs +++ b/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 for ProgramError { - fn from(e: Error) -> ProgramError { - // Errors 0-100 are reserved for the framework. - let error_offset = 100u32; + impl std::convert::From 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() } diff --git a/lang/syn/src/codegen/program/cpi.rs b/lang/syn/src/codegen/program/cpi.rs index 413b602887..bf8a3a6a7d 100644 --- a/lang/syn/src/codegen/program/cpi.rs +++ b/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); diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index 4cb205855d..8b7ad1c6fd 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/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()) } } } diff --git a/lang/syn/src/codegen/program/entry.rs b/lang/syn/src/codegen/program/entry.rs index 11323c8465..fbabbd7e15 100644 --- a/lang/syn/src/codegen/program/entry.rs +++ b/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 diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index bbcb398779..0375fc638f 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/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. diff --git a/lang/syn/src/idl/file.rs b/lang/syn/src/idl/file.rs index adb613f2f2..1094872c1b 100644 --- a/lang/syn/src/idl/file.rs +++ b/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) -> Result { @@ -128,12 +130,12 @@ pub fn parse(filename: impl AsRef) -> Result { } }, }; - 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(), }) diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 25d74f8b95..0163cc09d9 100644 --- a/lang/syn/src/lib.rs +++ b/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, + pub args: Option, +} + +#[derive(Debug)] +pub struct ErrorArgs { + pub offset: LitInt, +} + +impl Parse for ErrorArgs { + fn parse(stream: ParseStream) -> ParseResult { + 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::()?; + Ok(ErrorArgs { + offset: stream.parse()?, + }) + } } #[derive(Debug)] diff --git a/lang/syn/src/parser/error.rs b/lang/syn/src/parser/error.rs index b5973945c3..d4b0dfb2f3 100644 --- a/lang/syn/src/parser/error.rs +++ b/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) -> Error { let ident = error_enum.ident.clone(); let mut last_discriminant = 0; let codes: Vec = 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, } } diff --git a/ts/package.json b/ts/package.json index eb554f2e7a..cd7c54fe60 100644 --- a/ts/package.json +++ b/ts/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@project-serum/borsh": "^0.2.2", - "@solana/web3.js": "^1.11.0", + "@solana/web3.js": "^1.15.0", "base64-js": "^1.5.1", "bn.js": "^5.1.2", "bs58": "^4.0.1", diff --git a/ts/src/error.ts b/ts/src/error.ts index c06ac4c020..79e1222f0f 100644 --- a/ts/src/error.ts +++ b/ts/src/error.ts @@ -6,7 +6,165 @@ export class ProgramError extends Error { super(...params); } + public static parse( + err: any, + idlErrors: Map + ): 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", + ], +]); diff --git a/ts/src/program/common.ts b/ts/src/program/common.ts index 74640c72cb..0587a8b9ee 100644 --- a/ts/src/program/common.ts +++ b/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, - 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") { diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index 8dfe805c68..c45b07b76e 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/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; } diff --git a/ts/src/program/namespace/simulate.ts b/ts/src/program/namespace/simulate.ts index 310ea97599..d0e579659c 100644 --- a/ts/src/program/namespace/simulate.ts +++ b/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; } diff --git a/ts/yarn.lock b/ts/yarn.lock index 293926c7e1..bc839a8387 100644 --- a/ts/yarn.lock +++ b/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.15.0": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.15.0.tgz#e90f928f4e270fddbe89e83210d54f757128a306" + integrity sha512-q9GWL6ayHbEljzp2l5bdz46618h28KJ07H0m1pL2p/LRF+Jh9x5X/FME87La4/6lVrsSyymSaKnn+loiTTqB7w== 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"