lang: add support for logging expected and actual values and pubkeys (#1572)

This commit is contained in:
Paul 2022-03-15 11:42:17 -04:00 committed by GitHub
parent 905968d5ab
commit 721fe6693c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 520 additions and 171 deletions

View File

@ -33,7 +33,8 @@ incremented for features.
* ts: Mark `transaction`, `instruction`, `simulate` and `rpc` program namespaces as deprecated in favor of `methods` ([#1539](https://github.com/project-serum/anchor/pull/1539)).
* ts: No longer allow manual setting of globally resolvable program public keys in `methods#accounts()`. ([#1548][https://github.com/project-serum/anchor/pull/1548])
* lang: Remove space calculation using [`#[derive(Default)]`] (https://github.com/project-serum/anchor/pull/1519).
* lang: Remove space calculation using `#[derive(Default)]` ([#1519](https://github.com/project-serum/anchor/pull/1519)).
* lang: Add support for logging expected and actual values and pubkeys. Add `require_eq` and `require_keys_eq` macros. Add default error code to `require` macro ([#1572](https://github.com/project-serum/anchor/pull/1572)).
## [0.22.1] - 2022-02-28

View File

@ -3,9 +3,9 @@ extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use anchor_syn::parser::error::{self as error_parser, ErrorWithAccountNameInput};
use anchor_syn::codegen;
use anchor_syn::parser::error::{self as error_parser, ErrorInput};
use anchor_syn::ErrorArgs;
use anchor_syn::{codegen, parser::error::ErrorInput};
use syn::{parse_macro_input, Expr};
/// Generates `Error` and `type Result<T> = Result<T, Error>` types to be
@ -88,39 +88,28 @@ pub fn error(ts: proc_macro::TokenStream) -> TokenStream {
create_error(error_code, true, None)
}
#[proc_macro]
pub fn error_with_account_name(ts: proc_macro::TokenStream) -> TokenStream {
let input = parse_macro_input!(ts as ErrorWithAccountNameInput);
let error_code = input.error_code;
let account_name = input.account_name;
create_error(error_code, false, Some(account_name))
}
fn create_error(error_code: Expr, source: bool, account_name: Option<Expr>) -> TokenStream {
let source = if source {
quote! {
Some(anchor_lang::error::Source {
let error_origin = match (source, account_name) {
(false, None) => quote! { None },
(false, Some(account_name)) => quote! {
Some(anchor_lang::error::ErrorOrigin::AccountName(#account_name.to_string()))
},
(true, _) => quote! {
Some(anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source {
filename: file!(),
line: line!()
})
}
} else {
quote! {
None
}
};
let account_name = match account_name {
Some(_) => quote! { Some(#account_name.to_string()) },
None => quote! { None },
}))
},
};
TokenStream::from(quote! {
anchor_lang::error::Error::from(
anchor_lang::error::AnchorError {
error_name: #error_code.name(),
error_code_number: #error_code.into(),
error_msg: #error_code.to_string(),
source: #source,
account_name: #account_name
error_origin: #error_origin,
compared_values: None
}
)
})

View File

@ -1,7 +1,7 @@
//! Account container that checks ownership on deserialization.
use crate::bpf_writer::BpfWriter;
use crate::error::ErrorCode;
use crate::error::{Error, ErrorCode};
use crate::{
AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, Key, Owner,
Result, ToAccountInfo, ToAccountInfos, ToAccountMetas,
@ -251,7 +251,8 @@ impl<'a, T: AccountSerialize + AccountDeserialize + crate::Owner + Clone> Accoun
return Err(ErrorCode::AccountNotInitialized.into());
}
if info.owner != &T::owner() {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, T::owner())));
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(Account::new(info.clone(), T::try_deserialize(&mut data)?))
@ -266,7 +267,8 @@ impl<'a, T: AccountSerialize + AccountDeserialize + crate::Owner + Clone> Accoun
return Err(ErrorCode::AccountNotInitialized.into());
}
if info.owner != &T::owner() {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, T::owner())));
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(Account::new(

View File

@ -1,7 +1,7 @@
//! Type facilitating on demand zero copy deserialization.
use crate::bpf_writer::BpfWriter;
use crate::error::ErrorCode;
use crate::error::{Error, ErrorCode};
use crate::{
Accounts, AccountsClose, AccountsExit, Key, Owner, Result, ToAccountInfo, ToAccountInfos,
ToAccountMetas, ZeroCopy,
@ -120,7 +120,8 @@ impl<'info, T: ZeroCopy + Owner> AccountLoader<'info, T> {
#[inline(never)]
pub fn try_from(acc_info: &AccountInfo<'info>) -> Result<AccountLoader<'info, T>> {
if acc_info.owner != &T::owner() {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*acc_info.owner, T::owner())));
}
let data: &[u8] = &acc_info.try_borrow_data()?;
// Discriminator must match.
@ -139,7 +140,8 @@ impl<'info, T: ZeroCopy + Owner> AccountLoader<'info, T> {
acc_info: &AccountInfo<'info>,
) -> Result<AccountLoader<'info, T>> {
if acc_info.owner != &T::owner() {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*acc_info.owner, T::owner())));
}
Ok(AccountLoader::new(acc_info.clone()))
}

View File

@ -1,5 +1,5 @@
use crate::bpf_writer::BpfWriter;
use crate::error::ErrorCode;
use crate::error::{Error, ErrorCode};
use crate::{
Accounts, AccountsClose, AccountsExit, Key, Result, ToAccountInfo, ToAccountInfos,
ToAccountMetas, ZeroCopy,
@ -59,7 +59,8 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
acc_info: &AccountInfo<'info>,
) -> Result<Loader<'info, T>> {
if acc_info.owner != program_id {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*acc_info.owner, *program_id)));
}
let data: &[u8] = &acc_info.try_borrow_data()?;
// Discriminator must match.
@ -79,7 +80,8 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
acc_info: &AccountInfo<'info>,
) -> Result<Loader<'info, T>> {
if acc_info.owner != program_id {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*acc_info.owner, *program_id)));
}
Ok(Loader::new(acc_info.clone()))
}

View File

@ -1,5 +1,6 @@
//! Type validating that the account is the given Program
use crate::error::ErrorCode;
use crate::error::{Error, ErrorCode};
use crate::{
AccountDeserialize, Accounts, AccountsExit, Id, Key, Result, ToAccountInfos, ToAccountMetas,
};
@ -102,7 +103,7 @@ impl<'a, T: Id + Clone> Program<'a, T> {
#[inline(never)]
pub fn try_from(info: &AccountInfo<'a>) -> Result<Program<'a, T>> {
if info.key != &T::id() {
return Err(ErrorCode::InvalidProgramId.into());
return Err(Error::from(ErrorCode::InvalidProgramId).with_pubkeys((*info.key, T::id())));
}
if !info.executable {
return Err(ErrorCode::InvalidProgramExecutable.into());

View File

@ -1,7 +1,7 @@
#[allow(deprecated)]
use crate::accounts::cpi_account::CpiAccount;
use crate::bpf_writer::BpfWriter;
use crate::error::ErrorCode;
use crate::error::{Error, ErrorCode};
use crate::{
AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, Key, Result,
ToAccountInfo, ToAccountInfos, ToAccountMetas,
@ -38,7 +38,8 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T>
#[inline(never)]
pub fn try_from(program_id: &Pubkey, info: &AccountInfo<'a>) -> Result<ProgramAccount<'a, T>> {
if info.owner != program_id {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, *program_id)));
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(ProgramAccount::new(
@ -56,7 +57,8 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T>
info: &AccountInfo<'a>,
) -> Result<ProgramAccount<'a, T>> {
if info.owner != program_id {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, *program_id)));
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(ProgramAccount::new(

View File

@ -1,7 +1,7 @@
#[allow(deprecated)]
use crate::accounts::cpi_account::CpiAccount;
use crate::bpf_writer::BpfWriter;
use crate::error::ErrorCode;
use crate::error::{Error, ErrorCode};
use crate::{
AccountDeserialize, AccountSerialize, Accounts, AccountsExit, Key, Result, ToAccountInfo,
ToAccountInfos, ToAccountMetas,
@ -40,7 +40,8 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramState<'a, T> {
#[inline(never)]
pub fn try_from(program_id: &Pubkey, info: &AccountInfo<'a>) -> Result<ProgramState<'a, T>> {
if info.owner != program_id {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, *program_id)));
}
if info.key != &Self::address(program_id) {
solana_program::msg!("Invalid state address");

View File

@ -1,6 +1,6 @@
use anchor_attribute_error::error_code;
use borsh::maybestd::io::Error as BorshIoError;
use solana_program::program_error::ProgramError;
use solana_program::{program_error::ProgramError, pubkey::Pubkey};
use std::fmt::{Debug, Display};
/// The starting point for user defined error codes.
@ -106,6 +106,17 @@ pub enum ErrorCode {
#[msg("A space constraint was violated")]
ConstraintSpace,
// Require
/// 2500 - A require expression was violated
#[msg("A require expression was violated")]
RequireViolated = 2500,
/// 2501 - A require_eq expression was violated
#[msg("A require_eq expression was violated")]
RequireEqViolated,
/// 2502 - A require_keys_eq expression was violated
#[msg("A require_keys_eq expression was violated")]
RequireKeysEqViolated,
// Accounts.
/// 3000 - The account discriminator was already set on this account
#[msg("The account discriminator was already set on this account")]
@ -189,12 +200,6 @@ impl Display for Error {
}
}
impl Display for AnchorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self, f)
}
}
impl From<AnchorError> for Error {
fn from(ae: AnchorError) -> Self {
Self::AnchorError(ae)
@ -228,16 +233,51 @@ impl Error {
pub fn with_account_name(mut self, account_name: impl ToString) -> Self {
match &mut self {
Error::AnchorError(ae) => ae.account_name = Some(account_name.to_string()),
Error::ProgramError(pe) => pe.account_name = Some(account_name.to_string()),
Error::AnchorError(ae) => {
ae.error_origin = Some(ErrorOrigin::AccountName(account_name.to_string()));
}
Error::ProgramError(pe) => {
pe.error_origin = Some(ErrorOrigin::AccountName(account_name.to_string()));
}
};
self
}
pub fn with_source(mut self, source: Source) -> Self {
match &mut self {
Error::AnchorError(ae) => ae.source = Some(source),
Error::ProgramError(pe) => pe.source = Some(source),
Error::AnchorError(ae) => {
ae.error_origin = Some(ErrorOrigin::Source(source));
}
Error::ProgramError(pe) => {
pe.error_origin = Some(ErrorOrigin::Source(source));
}
};
self
}
pub fn with_pubkeys(mut self, pubkeys: (Pubkey, Pubkey)) -> Self {
let pubkeys = Some(ComparedValues::Pubkeys((pubkeys.0, pubkeys.1)));
match &mut self {
Error::AnchorError(ae) => ae.compared_values = pubkeys,
Error::ProgramError(pe) => pe.compared_values = pubkeys,
};
self
}
pub fn with_values(mut self, values: (impl ToString, impl ToString)) -> Self {
match &mut self {
Error::AnchorError(ae) => {
ae.compared_values = Some(ComparedValues::Values((
values.0.to_string(),
values.1.to_string(),
)))
}
Error::ProgramError(pe) => {
pe.compared_values = Some(ComparedValues::Values((
values.0.to_string(),
values.1.to_string(),
)))
}
};
self
}
@ -246,8 +286,8 @@ impl Error {
#[derive(Debug)]
pub struct ProgramErrorWithOrigin {
pub program_error: ProgramError,
pub source: Option<Source>,
pub account_name: Option<String>,
pub error_origin: Option<ErrorOrigin>,
pub compared_values: Option<ComparedValues>,
}
impl Display for ProgramErrorWithOrigin {
@ -258,31 +298,59 @@ impl Display for ProgramErrorWithOrigin {
impl ProgramErrorWithOrigin {
pub fn log(&self) {
if let Some(source) = &self.source {
anchor_lang::solana_program::msg!(
"ProgramError thrown in {}:{}. Error Code: {:?}. Error Number: {}. Error Message: {}.",
source.filename,
source.line,
self.program_error,
u64::from(self.program_error.clone()),
self.program_error
);
} else if let Some(account_name) = &self.account_name {
anchor_lang::solana_program::log::sol_log(&format!(
"ProgramError caused by account: {}. Error Code: {:?}. Error Number: {}. Error Message: {}.",
account_name,
self.program_error,
u64::from(self.program_error.clone()),
self.program_error
));
} else {
anchor_lang::solana_program::log::sol_log(&format!(
"ProgramError occurred. Error Code: {:?}. Error Number: {}. Error Message: {}.",
self.program_error,
u64::from(self.program_error.clone()),
self.program_error
));
match &self.error_origin {
None => {
anchor_lang::solana_program::msg!(
"ProgramError occurred. Error Code: {:?}. Error Number: {}. Error Message: {}.",
self.program_error,
u64::from(self.program_error.clone()),
self.program_error
);
}
Some(ErrorOrigin::Source(source)) => {
anchor_lang::solana_program::msg!(
"ProgramError thrown in {}:{}. Error Code: {:?}. Error Number: {}. Error Message: {}.",
source.filename,
source.line,
self.program_error,
u64::from(self.program_error.clone()),
self.program_error
);
}
Some(ErrorOrigin::AccountName(account_name)) => {
// using sol_log because msg! wrongly interprets 5 inputs as u64
anchor_lang::solana_program::log::sol_log(&format!(
"ProgramError caused by account: {}. Error Code: {:?}. Error Number: {}. Error Message: {}.",
account_name,
self.program_error,
u64::from(self.program_error.clone()),
self.program_error
));
}
}
match &self.compared_values {
Some(ComparedValues::Pubkeys((left, right))) => {
anchor_lang::solana_program::msg!("Left:");
left.log();
anchor_lang::solana_program::msg!("Right:");
right.log();
}
Some(ComparedValues::Values((left, right))) => {
anchor_lang::solana_program::msg!("Left: {}", left);
anchor_lang::solana_program::msg!("Right: {}", right);
}
None => (),
}
}
pub fn with_source(mut self, source: Source) -> Self {
self.error_origin = Some(ErrorOrigin::Source(source));
self
}
pub fn with_account_name(mut self, account_name: impl ToString) -> Self {
self.error_origin = Some(ErrorOrigin::AccountName(account_name.to_string()));
self
}
}
@ -290,46 +358,91 @@ impl From<ProgramError> for ProgramErrorWithOrigin {
fn from(program_error: ProgramError) -> Self {
Self {
program_error,
source: None,
account_name: None,
error_origin: None,
compared_values: None,
}
}
}
#[derive(Debug)]
pub enum ComparedValues {
Values((String, String)),
Pubkeys((Pubkey, Pubkey)),
}
#[derive(Debug)]
pub enum ErrorOrigin {
Source(Source),
AccountName(String),
}
#[derive(Debug)]
pub struct AnchorError {
pub error_name: String,
pub error_code_number: u32,
pub error_msg: String,
pub source: Option<Source>,
pub account_name: Option<String>,
pub error_origin: Option<ErrorOrigin>,
pub compared_values: Option<ComparedValues>,
}
impl AnchorError {
pub fn log(&self) {
if let Some(source) = &self.source {
anchor_lang::solana_program::msg!(
"AnchorError thrown in {}:{}. Error Code: {}. Error Number: {}. Error Message: {}.",
source.filename,
source.line,
self.error_name,
self.error_code_number,
self.error_msg
);
} else if let Some(account_name) = &self.account_name {
anchor_lang::solana_program::log::sol_log(&format!(
"AnchorError caused by account: {}. Error Code: {}. Error Number: {}. Error Message: {}.",
account_name,
self.error_name,
self.error_code_number,
self.error_msg
));
} else {
anchor_lang::solana_program::log::sol_log(&format!(
"AnchorError occurred. Error Code: {}. Error Number: {}. Error Message: {}.",
self.error_name, self.error_code_number, self.error_msg
));
match &self.error_origin {
None => {
anchor_lang::solana_program::log::sol_log(&format!(
"AnchorError occurred. Error Code: {}. Error Number: {}. Error Message: {}.",
self.error_name, self.error_code_number, self.error_msg
));
}
Some(ErrorOrigin::Source(source)) => {
anchor_lang::solana_program::msg!(
"AnchorError thrown in {}:{}. Error Code: {}. Error Number: {}. Error Message: {}.",
source.filename,
source.line,
self.error_name,
self.error_code_number,
self.error_msg
);
}
Some(ErrorOrigin::AccountName(account_name)) => {
anchor_lang::solana_program::log::sol_log(&format!(
"AnchorError caused by account: {}. Error Code: {}. Error Number: {}. Error Message: {}.",
account_name,
self.error_name,
self.error_code_number,
self.error_msg
));
}
}
match &self.compared_values {
Some(ComparedValues::Pubkeys((left, right))) => {
anchor_lang::solana_program::msg!("Left:");
left.log();
anchor_lang::solana_program::msg!("Right:");
right.log();
}
Some(ComparedValues::Values((left, right))) => {
anchor_lang::solana_program::msg!("Left: {}", left);
anchor_lang::solana_program::msg!("Right: {}", right);
}
None => (),
}
}
pub fn with_source(mut self, source: Source) -> Self {
self.error_origin = Some(ErrorOrigin::Source(source));
self
}
pub fn with_account_name(mut self, account_name: impl ToString) -> Self {
self.error_origin = Some(ErrorOrigin::AccountName(account_name.to_string()));
self
}
}
impl Display for AnchorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self, f)
}
}
@ -337,11 +450,7 @@ impl std::convert::From<Error> for anchor_lang::solana_program::program_error::P
fn from(e: Error) -> anchor_lang::solana_program::program_error::ProgramError {
match e {
Error::AnchorError(AnchorError {
error_name: _,
error_code_number,
error_msg: _,
source: _,
account_name: _,
error_code_number, ..
}) => {
anchor_lang::solana_program::program_error::ProgramError::Custom(error_code_number)
}

View File

@ -239,10 +239,11 @@ pub mod prelude {
accounts::signer::Signer, accounts::system_account::SystemAccount,
accounts::sysvar::Sysvar, accounts::unchecked_account::UncheckedAccount, constant,
context::Context, context::CpiContext, declare_id, emit, err, error, event, interface,
program, require, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source,
state, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsExit,
AnchorDeserialize, AnchorSerialize, Id, Key, Owner, ProgramData, Result, System,
ToAccountInfo, ToAccountInfos, ToAccountMetas,
program, require, require_eq, require_keys_eq,
solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, state, zero_copy,
AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AnchorDeserialize,
AnchorSerialize, Id, Key, Owner, ProgramData, Result, System, ToAccountInfo,
ToAccountInfos, ToAccountMetas,
};
pub use anchor_attribute_error::*;
pub use borsh;
@ -318,7 +319,7 @@ pub mod __private {
}
/// Ensures a condition is true, otherwise returns with the given error.
/// Use this with a custom error type.
/// Use this with or without a custom error type.
///
/// # Example
/// ```ignore
@ -366,6 +367,66 @@ macro_rules! require {
};
}
/// Ensures two NON-PUBKEY values are equal.
///
/// Use [require_keys_eq](crate::prelude::require_keys_eq)
/// to compare two pubkeys.
///
/// Can be used with or without a custom error code.
///
/// # Example
/// ```rust,ignore
/// pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
/// require_eq!(ctx.accounts.data.data, 0);
/// ctx.accounts.data.data = data;
/// Ok(())
/// }
/// ```
#[macro_export]
macro_rules! require_eq {
($value1: expr, $value2: expr, $error_code:expr $(,)?) => {
if $value1 != $value2 {
return Err(error!($error_code).with_values(($value1, $value2)));
}
};
($value1: expr, $value2: expr $(,)?) => {
if $value1 != $value2 {
return Err(error!(anchor_lang::error::ErrorCode::RequireEqViolated)
.with_values(($value1, $value2)));
}
};
}
/// Ensures two pubkeys values are equal.
///
/// Use [require_eq](crate::prelude::require_eq)
/// to compare two non-pubkey values.
///
/// Can be used with or without a custom error code.
///
/// # Example
/// ```rust,ignore
/// pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
/// require_keys_eq!(ctx.accounts.data.authority.key(), ctx.accounts.authority.key());
/// ctx.accounts.data.data = data;
/// Ok(())
/// }
/// ```
#[macro_export]
macro_rules! require_keys_eq {
($value1: expr, $value2: expr, $error_code:expr $(,)?) => {
if $value1 != $value2 {
return Err(error!($error_code).with_pubkeys(($value1, $value2)));
}
};
($value1: expr, $value2: expr $(,)?) => {
if $value1 != $value2 {
return Err(error!(anchor_lang::error::ErrorCode::RequireKeysEqViolated)
.with_pubkeys(($value1, $value2)));
}
};
}
/// Returns with the given error.
/// Use this with a custom error type.
///

View File

@ -158,7 +158,7 @@ pub fn generate_constraint_zeroed(f: &Field, _c: &ConstraintZeroed) -> proc_macr
__disc_bytes.copy_from_slice(&__data[..8]);
let __discriminator = u64::from_le_bytes(__disc_bytes);
if __discriminator != 0 {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintZero, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintZero).with_account_name(#name_str));
}
#from_account_info
};
@ -171,7 +171,7 @@ pub fn generate_constraint_close(f: &Field, c: &ConstraintClose) -> proc_macro2:
let target = &c.sol_dest;
quote! {
if #field.key() == #target.key() {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintClose, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintClose).with_account_name(#name_str));
}
}
}
@ -238,7 +238,7 @@ pub fn generate_constraint_literal(
};
quote! {
if !(#lit) {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::Deprecated, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::Deprecated).with_account_name(#name_str));
}
}
}
@ -277,7 +277,7 @@ pub fn generate_constraint_rent_exempt(
ConstraintRentExempt::Skip => quote! {},
ConstraintRentExempt::Enforce => quote! {
if !__anchor_rent.is_exempt(#info.lamports(), #info.try_data_len()?) {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintRentExempt, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintRentExempt).with_account_name(#name_str));
}
},
}
@ -374,10 +374,10 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
let pa: #ty_decl = #from_account_info;
if #if_needed {
if pa.mint != #mint.key() {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintTokenMint, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenMint).with_account_name(#name_str));
}
if pa.owner != #owner.key() {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintTokenOwner, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str));
}
}
pa
@ -409,14 +409,14 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
let pa: #ty_decl = #from_account_info;
if #if_needed {
if pa.mint != #mint.key() {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintTokenMint, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenMint).with_account_name(#name_str));
}
if pa.owner != #owner.key() {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintTokenOwner, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str));
}
if pa.key() != anchor_spl::associated_token::get_associated_token_address(&#owner.key(), &#mint.key()) {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::AccountNotAssociatedTokenAccount, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountNotAssociatedTokenAccount).with_account_name(#name_str));
}
}
pa
@ -462,16 +462,16 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
let pa: #ty_decl = #from_account_info;
if #if_needed {
if pa.mint_authority != anchor_lang::solana_program::program_option::COption::Some(#owner.key()) {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintMintMintAuthority, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintMintAuthority).with_account_name(#name_str));
}
if pa.freeze_authority
.as_ref()
.map(|fa| #freeze_authority.as_ref().map(|expected_fa| fa != *expected_fa).unwrap_or(true))
.unwrap_or(#freeze_authority.is_some()) {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintMintFreezeAuthority, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintFreezeAuthority).with_account_name(#name_str));
}
if pa.decimals != #decimals {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintMintDecimals, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintDecimals).with_account_name(#name_str));
}
}
pa
@ -525,17 +525,17 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
// Assert the account was created correctly.
if #if_needed {
if space != actual_field.data_len() {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintSpace, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSpace).with_account_name(#name_str));
}
if actual_owner != #owner {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintOwner, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintOwner).with_account_name(#name_str));
}
{
let required_lamports = __anchor_rent.minimum_balance(space);
if pa.to_account_info().lamports() < required_lamports {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintRentExempt, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintRentExempt).with_account_name(#name_str));
}
}
}
@ -577,10 +577,10 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
let b = c.bump.as_ref().unwrap();
quote! {
if #name.key() != __pda_address {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintSeeds, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str));
}
if __bump != #b {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintSeeds, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str));
}
}
}
@ -592,7 +592,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
else if c.is_init {
quote! {
if #name.key() != __pda_address {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintSeeds, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str));
}
}
}
@ -616,7 +616,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
let __pda_address = Pubkey::create_program_address(
&[#maybe_seeds_plus_comma &[#b][..]],
&#deriving_program_id,
).map_err(|_| anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintSeeds, #name_str))?;
).map_err(|_| anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str))?;
},
};
quote! {
@ -625,7 +625,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
// Check it.
if #name.key() != __pda_address {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintSeeds, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str));
}
}
}
@ -641,11 +641,11 @@ fn generate_constraint_associated_token(
let spl_token_mint_address = &c.mint;
quote! {
if #name.owner != #wallet_address.key() {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintTokenOwner, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str));
}
let __associated_token_address = anchor_spl::associated_token::get_associated_token_address(&#wallet_address.key(), &#spl_token_mint_address.key());
if #name.key() != __associated_token_address {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintAssociated, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintAssociated).with_account_name(#name_str));
}
}
}
@ -741,7 +741,7 @@ pub fn generate_constraint_executable(
let name_str = name.to_string();
quote! {
if !#name.to_account_info().executable {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintExecutable, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintExecutable).with_account_name(#name_str));
}
}
}
@ -758,10 +758,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.key() != anchor_lang::accounts::cpi_state::CpiState::<#account_ty>::address(&#program_target.key()) {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintState, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintState).with_account_name(#name_str));
}
if #ident.as_ref().owner != &#program_target.key() {
return Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::ConstraintState, #name_str));
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintState).with_account_name(#name_str));
}
}
}
@ -774,10 +774,10 @@ fn generate_custom_error(
let account_name = account_name.to_string();
match custom_error {
Some(error) => {
quote! { Err(anchor_lang::anchor_attribute_error::error_with_account_name!(#error, #account_name)) }
quote! { Err(anchor_lang::error::Error::from(#error).with_account_name(#account_name)) }
}
None => {
quote! { Err(anchor_lang::anchor_attribute_error::error_with_account_name!(anchor_lang::error::ErrorCode::#error, #account_name)) }
quote! { Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::#error).with_account_name(#account_name)) }
}
}
}

View File

@ -82,8 +82,8 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream {
error_name: error_code.name(),
error_code_number: error_code.into(),
error_msg: error_code.to_string(),
source: None,
account_name: None
error_origin: None,
compared_values: None
}
)
}

View File

@ -1,6 +1,6 @@
use crate::{Error, ErrorArgs, ErrorCode};
use syn::parse::{Parse, Result as ParseResult};
use syn::{Expr, Token};
use syn::Expr;
// Removes any internal #[msg] attributes, as they are inert.
pub fn parse(error_enum: &mut syn::ItemEnum, args: Option<ErrorArgs>) -> Error {
@ -88,20 +88,3 @@ impl Parse for ErrorInput {
Ok(Self { error_code })
}
}
pub struct ErrorWithAccountNameInput {
pub error_code: Expr,
pub account_name: Expr,
}
impl Parse for ErrorWithAccountNameInput {
fn parse(stream: syn::parse::ParseStream) -> ParseResult<Self> {
let error_code = stream.call(Expr::parse)?;
let _ = stream.parse::<Token!(,)>();
let account_name = stream.call(Expr::parse)?;
Ok(Self {
error_code,
account_name,
})
}
}

View File

@ -57,6 +57,32 @@ mod errors {
pub fn account_not_initialized_error(_ctx: Context<AccountNotInitializedError>) -> Result<()> {
Ok(())
}
pub fn account_owned_by_wrong_program_error(
_ctx: Context<AccountOwnedByWrongProgramError>,
) -> Result<()> {
Ok(())
}
pub fn require_eq(_ctx: Context<RequireEq>) -> Result<()> {
require_eq!(5241, 124124124, MyError::ValueMismatch);
Ok(())
}
pub fn require_eq_default_error(_ctx: Context<RequireEq>) -> Result<()> {
require_eq!(5241, 124124124);
Ok(())
}
pub fn require_keys_eq(ctx: Context<RequireKeysEq>) -> Result<()> {
require_keys_eq!(ctx.accounts.some_account.key(), *ctx.program_id, MyError::ValueMismatch);
Ok(())
}
pub fn require_keys_eq_default_error(ctx: Context<RequireKeysEq>) -> Result<()> {
require_keys_eq!(ctx.accounts.some_account.key(), *ctx.program_id);
Ok(())
}
}
#[derive(Accounts)]
@ -99,6 +125,19 @@ pub struct AccountNotInitializedError<'info> {
not_initialized_account: Account<'info, AnyAccount>,
}
#[derive(Accounts)]
pub struct AccountOwnedByWrongProgramError<'info> {
pub wrong_account: Account<'info, AnyAccount>,
}
#[derive(Accounts)]
pub struct RequireEq {}
#[derive(Accounts)]
pub struct RequireKeysEq<'info> {
pub some_account: UncheckedAccount<'info>
}
#[error_code]
pub enum MyError {
#[msg("This is an error message clients will automatically display")]
@ -106,4 +145,5 @@ pub enum MyError {
HelloNoMsg = 123,
HelloNext,
HelloCustom,
ValueMismatch,
}

View File

@ -1,6 +1,7 @@
const assert = require("assert");
const anchor = require("@project-serum/anchor");
const { Account, Transaction, TransactionInstruction } = anchor.web3;
const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
// sleep to allow logs to come in
const sleep = (ms) =>
@ -8,15 +9,31 @@ const sleep = (ms) =>
setTimeout(() => resolve(), ms);
});
const withLogTest = async (callback, expectedLog) => {
const withLogTest = async (callback, expectedLogs) => {
let logTestOk = false;
const listener = anchor.getProvider().connection.onLogs(
"all",
(logs) => {
if (logs.logs.some((logLine) => logLine === expectedLog)) {
logTestOk = true;
} else {
const index = logs.logs.findIndex(
(logLine) => logLine === expectedLogs[0]
);
if (index === -1) {
console.log("Expected: ");
console.log(expectedLogs);
console.log("Actual: ");
console.log(logs);
} else {
const actualLogs = logs.logs.slice(index, index + expectedLogs.length);
for (let i = 0; i < expectedLogs.length; i++) {
if (actualLogs[i] !== expectedLogs[i]) {
console.log("Expected: ");
console.log(expectedLogs);
console.log("Actual: ");
console.log(logs);
return;
}
}
logTestOk = true;
}
},
"recent"
@ -52,7 +69,9 @@ describe("errors", () => {
assert.equal(err.msg, errMsg);
assert.equal(err.code, 6000);
}
}, "Program log: AnchorError thrown in programs/errors/src/lib.rs:13. Error Code: Hello. Error Number: 6000. Error Message: This is an error message clients will automatically display.");
}, [
"Program log: AnchorError thrown in programs/errors/src/lib.rs:13. Error Code: Hello. Error Number: 6000. Error Message: This is an error message clients will automatically display.",
]);
});
it("Emits a Hello error via require!", async () => {
@ -89,7 +108,9 @@ describe("errors", () => {
} catch (err) {
// No-op (withLogTest expects the callback to catch the initial tx error)
}
}, "Program log: ProgramError occurred. Error Code: InvalidAccountData. Error Number: 17179869184. Error Message: An account's data contents was invalid.");
}, [
"Program log: ProgramError occurred. Error Code: InvalidAccountData. Error Number: 17179869184. Error Message: An account's data contents was invalid.",
]);
});
it("Logs a ProgramError with source", async () => {
@ -100,7 +121,9 @@ describe("errors", () => {
} catch (err) {
// No-op (withLogTest expects the callback to catch the initial tx error)
}
}, "Program log: ProgramError thrown in programs/errors/src/lib.rs:38. Error Code: InvalidAccountData. Error Number: 17179869184. Error Message: An account's data contents was invalid.");
}, [
"Program log: ProgramError thrown in programs/errors/src/lib.rs:38. Error Code: InvalidAccountData. Error Number: 17179869184. Error Message: An account's data contents was invalid.",
]);
});
it("Emits a HelloNoMsg error", async () => {
@ -142,7 +165,9 @@ describe("errors", () => {
assert.equal(err.msg, errMsg);
assert.equal(err.code, 2000);
}
}, "Program log: AnchorError caused by account: my_account. Error Code: ConstraintMut. Error Number: 2000. Error Message: A mut constraint was violated.");
}, [
"Program log: AnchorError caused by account: my_account. Error Code: ConstraintMut. Error Number: 2000. Error Message: A mut constraint was violated.",
]);
});
it("Emits a has one error", async () => {
@ -235,6 +260,124 @@ describe("errors", () => {
"The program expected this account to be already initialized";
assert.equal(err.toString(), errMsg);
}
}, "Program log: AnchorError caused by account: not_initialized_account. Error Code: AccountNotInitialized. Error Number: 3012. Error Message: The program expected this account to be already initialized.");
}, [
"Program log: AnchorError caused by account: not_initialized_account. Error Code: AccountNotInitialized. Error Number: 3012. Error Message: The program expected this account to be already initialized.",
]);
});
it("Emits an AccountOwnedByWrongProgram error", async () => {
let client = await Token.createMint(
program.provider.connection,
program.provider.wallet.payer,
program.provider.wallet.publicKey,
program.provider.wallet.publicKey,
9,
TOKEN_PROGRAM_ID
);
await withLogTest(async () => {
try {
const tx = await program.rpc.accountOwnedByWrongProgramError({
accounts: {
wrongAccount: client.publicKey,
},
});
assert.fail(
"Unexpected success in creating a transaction that should have failed with `AccountOwnedByWrongProgram` error"
);
} catch (err) {
const errMsg =
"The given account is owned by a different program than expected";
assert.equal(err.toString(), errMsg);
}
}, [
"Program log: AnchorError caused by account: wrong_account. Error Code: AccountOwnedByWrongProgram. Error Number: 3007. Error Message: The given account is owned by a different program than expected.",
"Program log: Left:",
"Program log: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"Program log: Right:",
"Program log: Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
]);
});
it("Emits a ValueMismatch error via require_eq", async () => {
await withLogTest(async () => {
try {
const tx = await program.rpc.requireEq();
assert.fail(
"Unexpected success in creating a transaction that should have failed with `ValueMismatch` error"
);
} catch (err) {
assert.equal(err.code, 6126);
}
}, [
"Program log: AnchorError thrown in programs/errors/src/lib.rs:68. Error Code: ValueMismatch. Error Number: 6126. Error Message: ValueMismatch.",
"Program log: Left: 5241",
"Program log: Right: 124124124",
]);
});
it("Emits a RequireEqViolated error via require_eq", async () => {
await withLogTest(async () => {
try {
const tx = await program.rpc.requireEqDefaultError();
assert.fail(
"Unexpected success in creating a transaction that should have failed with `ValueMismatch` error"
);
} catch (err) {
assert.equal(err.code, 2501);
}
}, [
"Program log: AnchorError thrown in programs/errors/src/lib.rs:73. Error Code: RequireEqViolated. Error Number: 2501. Error Message: A require_eq expression was violated.",
"Program log: Left: 5241",
"Program log: Right: 124124124",
]);
});
it("Emits a ValueMismatch error via require_keys_eq", async () => {
const someAccount = anchor.web3.Keypair.generate().publicKey;
await withLogTest(async () => {
try {
const tx = await program.rpc.requireKeysEq({
accounts: {
someAccount,
},
});
assert.fail(
"Unexpected success in creating a transaction that should have failed with `ValueMismatch` error"
);
} catch (err) {
assert.equal(err.code, 6126);
}
}, [
"Program log: AnchorError thrown in programs/errors/src/lib.rs:78. Error Code: ValueMismatch. Error Number: 6126. Error Message: ValueMismatch.",
"Program log: Left:",
`Program log: ${someAccount}`,
"Program log: Right:",
`Program log: ${program.programId}`,
]);
});
it("Emits a RequireKeysEqViolated error via require_keys_eq", async () => {
const someAccount = anchor.web3.Keypair.generate().publicKey;
await withLogTest(async () => {
try {
const tx = await program.rpc.requireKeysEqDefaultError({
accounts: {
someAccount,
},
});
assert.fail(
"Unexpected success in creating a transaction that should have failed with `ValueMismatch` error"
);
} catch (err) {
assert.equal(err.code, 2502);
}
}, [
"Program log: AnchorError thrown in programs/errors/src/lib.rs:83. Error Code: RequireKeysEqViolated. Error Number: 2502. Error Message: A require_keys_eq expression was violated.",
"Program log: Left:",
`Program log: ${someAccount}`,
"Program log: Right:",
`Program log: ${program.programId}`,
]);
});
});

View File

@ -95,6 +95,11 @@ const LangErrorCode = {
ConstraintMintDecimals: 2018,
ConstraintSpace: 2019,
// Require.
RequireViolated: 2500,
RequireEqViolated: 2501,
RequireKeysEqViolated: 2502,
// Accounts.
AccountDiscriminatorAlreadySet: 3000,
AccountDiscriminatorNotFound: 3001,
@ -185,6 +190,14 @@ const LangErrorMessage = new Map([
],
[LangErrorCode.ConstraintSpace, "A space constraint was violated"],
// Require.
[LangErrorCode.RequireViolated, "A require expression was violated"],
[LangErrorCode.RequireEqViolated, "A require_eq expression was violated"],
[
LangErrorCode.RequireKeysEqViolated,
"A require_keys_eq expression was violated",
],
// Accounts.
[
LangErrorCode.AccountDiscriminatorAlreadySet,