diff --git a/Cargo.lock b/Cargo.lock index 001e4180577..eb9c4d1c93a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6785,6 +6785,7 @@ version = "0.2.0" dependencies = [ "proc-macro2 1.0.63", "quote 1.0.29", + "solana-program", "syn 2.0.28", ] @@ -7249,14 +7250,11 @@ version = "0.1.0" dependencies = [ "arrayref", "bytemuck", - "num-derive 0.4.0", - "num-traits", - "num_enum 0.7.0", "solana-program", "spl-discriminator", + "spl-program-error", "spl-tlv-account-resolution", "spl-type-length-value", - "thiserror", ] [[package]] diff --git a/libraries/program-error/derive/Cargo.toml b/libraries/program-error/derive/Cargo.toml index 9eba3c1b637..4a3aaea5048 100644 --- a/libraries/program-error/derive/Cargo.toml +++ b/libraries/program-error/derive/Cargo.toml @@ -13,4 +13,5 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" +solana-program = "1.16.3" syn = { version = "2.0", features = ["full"] } diff --git a/libraries/program-error/derive/src/lib.rs b/libraries/program-error/derive/src/lib.rs index af9e53dadad..f689f5029f3 100644 --- a/libraries/program-error/derive/src/lib.rs +++ b/libraries/program-error/derive/src/lib.rs @@ -14,39 +14,67 @@ extern crate proc_macro; mod macro_impl; +mod parser; -use macro_impl::MacroType; -use proc_macro::TokenStream; -use syn::{parse_macro_input, ItemEnum}; +use { + crate::parser::SplProgramErrorArgs, + macro_impl::MacroType, + proc_macro::TokenStream, + syn::{parse_macro_input, ItemEnum}, +}; -/// Derive macro to add `Into` traits +/// Derive macro to add `Into` +/// trait #[proc_macro_derive(IntoProgramError)] pub fn into_program_error(input: TokenStream) -> TokenStream { - MacroType::IntoProgramError - .generate_tokens(parse_macro_input!(input as ItemEnum)) + let ItemEnum { ident, .. } = parse_macro_input!(input as ItemEnum); + MacroType::IntoProgramError { ident } + .generate_tokens() .into() } /// Derive macro to add `solana_program::decode_error::DecodeError` trait #[proc_macro_derive(DecodeError)] pub fn decode_error(input: TokenStream) -> TokenStream { - MacroType::DecodeError - .generate_tokens(parse_macro_input!(input as ItemEnum)) - .into() + let ItemEnum { ident, .. } = parse_macro_input!(input as ItemEnum); + MacroType::DecodeError { ident }.generate_tokens().into() } /// Derive macro to add `solana_program::program_error::PrintProgramError` trait #[proc_macro_derive(PrintProgramError)] pub fn print_program_error(input: TokenStream) -> TokenStream { - MacroType::PrintProgramError - .generate_tokens(parse_macro_input!(input as ItemEnum)) + let ItemEnum { + ident, variants, .. + } = parse_macro_input!(input as ItemEnum); + MacroType::PrintProgramError { ident, variants } + .generate_tokens() .into() } /// Proc macro attribute to turn your enum into a Solana Program Error +/// +/// Adds: +/// - `Clone` +/// - `Debug` +/// - `Eq` +/// - `PartialEq` +/// - `thiserror::Error` +/// - `num_derive::FromPrimitive` +/// - `Into` +/// - `solana_program::decode_error::DecodeError` +/// - `solana_program::program_error::PrintProgramError` +/// +/// Optionally, you can add `hash_error_codes: bool` argument to create unique +/// `u32` error codes from the names of the enum variants. +/// +/// Syntax: `#[spl_program_error(hash_error_codes = true)]` +/// Hash Input: `spl_program_error::` +/// Value: `u32::from_le_bytes([8..12])` #[proc_macro_attribute] -pub fn spl_program_error(_: TokenStream, input: TokenStream) -> TokenStream { - MacroType::SplProgramError - .generate_tokens(parse_macro_input!(input as ItemEnum)) +pub fn spl_program_error(attr: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as SplProgramErrorArgs); + let item_enum = parse_macro_input!(input as ItemEnum); + MacroType::SplProgramError { args, item_enum } + .generate_tokens() .into() } diff --git a/libraries/program-error/derive/src/macro_impl.rs b/libraries/program-error/derive/src/macro_impl.rs index f017728432e..00fef9ecd3d 100644 --- a/libraries/program-error/derive/src/macro_impl.rs +++ b/libraries/program-error/derive/src/macro_impl.rs @@ -1,32 +1,51 @@ //! The actual token generator for the macro -use quote::quote; -use syn::{punctuated::Punctuated, token::Comma, Ident, ItemEnum, LitStr, Variant}; + +use { + crate::parser::SplProgramErrorArgs, + proc_macro2::Span, + quote::quote, + syn::{ + punctuated::Punctuated, token::Comma, Expr, ExprLit, Ident, ItemEnum, Lit, LitInt, LitStr, + Token, Variant, + }, +}; + +const SPL_ERROR_HASH_NAMESPACE: &str = "spl_program_error"; /// The type of macro being called, thus directing which tokens to generate #[allow(clippy::enum_variant_names)] pub enum MacroType { - IntoProgramError, - DecodeError, - PrintProgramError, - SplProgramError, + IntoProgramError { + ident: Ident, + }, + DecodeError { + ident: Ident, + }, + PrintProgramError { + ident: Ident, + variants: Punctuated, + }, + SplProgramError { + args: SplProgramErrorArgs, + item_enum: ItemEnum, + }, } impl MacroType { /// Generates the corresponding tokens based on variant selection - pub fn generate_tokens(&self, item_enum: ItemEnum) -> proc_macro2::TokenStream { + pub fn generate_tokens(&mut self) -> proc_macro2::TokenStream { match self { - MacroType::IntoProgramError => into_program_error(&item_enum.ident), - MacroType::DecodeError => decode_error(&item_enum.ident), - MacroType::PrintProgramError => { - print_program_error(&item_enum.ident, &item_enum.variants) - } - MacroType::SplProgramError => spl_program_error(item_enum), + Self::IntoProgramError { ident } => into_program_error(ident), + Self::DecodeError { ident } => decode_error(ident), + Self::PrintProgramError { ident, variants } => print_program_error(ident, variants), + Self::SplProgramError { args, item_enum } => spl_program_error(args, item_enum), } } } -/// Builds the implementation of `Into` -/// More specifically, implements `From for solana_program::program_error::ProgramError` +/// Builds the implementation of +/// `Into` More specifically, +/// implements `From for solana_program::program_error::ProgramError` pub fn into_program_error(ident: &Ident) -> proc_macro2::TokenStream { quote! { impl From<#ident> for solana_program::program_error::ProgramError { @@ -48,7 +67,8 @@ pub fn decode_error(ident: &Ident) -> proc_macro2::TokenStream { } } -/// Builds the implementation of `solana_program::program_error::PrintProgramError` +/// Builds the implementation of +/// `solana_program::program_error::PrintProgramError` pub fn print_program_error( ident: &Ident, variants: &Punctuated, @@ -96,16 +116,25 @@ fn get_error_message(variant: &Variant) -> Option { /// The main function that produces the tokens required to turn your /// error enum into a Solana Program Error -pub fn spl_program_error(input: ItemEnum) -> proc_macro2::TokenStream { - let ident = &input.ident; - let variants = &input.variants; +pub fn spl_program_error( + args: &SplProgramErrorArgs, + item_enum: &mut ItemEnum, +) -> proc_macro2::TokenStream { + if args.hash_error_codes { + build_discriminants(item_enum); + } + + let ident = &item_enum.ident; + let variants = &item_enum.variants; let into_program_error = into_program_error(ident); let decode_error = decode_error(ident); let print_program_error = print_program_error(ident, variants); + quote! { + #[repr(u32)] #[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] #[num_traits = "num_traits"] - #input + #item_enum #into_program_error @@ -114,3 +143,37 @@ pub fn spl_program_error(input: ItemEnum) -> proc_macro2::TokenStream { #print_program_error } } + +/// This function adds discriminants to the enum variants based on the +/// hash of the `SPL_ERROR_HASH_NAMESPACE` constant, the enum name and variant +/// name. +/// +/// See https://docs.rs/syn/latest/syn/struct.Variant.html +fn build_discriminants(item_enum: &mut ItemEnum) { + let enum_ident = &item_enum.ident; + for variant in item_enum.variants.iter_mut() { + let variant_ident = &variant.ident; + let discriminant = u32_from_hash(enum_ident, variant_ident); + let eq = Token![=](Span::call_site()); + let expr = Expr::Lit(ExprLit { + attrs: Vec::new(), + lit: Lit::Int(LitInt::new(&discriminant.to_string(), Span::call_site())), + }); + variant.discriminant = Some((eq, expr)); + } +} + +/// Hashes the `SPL_ERROR_HASH_NAMESPACE` constant, the enum name and variant +/// name and returns four middle bytes (8 through 12) as a u32. +fn u32_from_hash(enum_ident: &Ident, variant_ident: &Ident) -> u32 { + let hash_input = format!( + "{}:{}:{}", + SPL_ERROR_HASH_NAMESPACE, enum_ident, variant_ident + ); + let hash = solana_program::hash::hash(hash_input.as_bytes()); + u32::from_le_bytes( + hash.to_bytes()[8..12] + .try_into() + .expect("Unable to convert hash to u32"), + ) +} diff --git a/libraries/program-error/derive/src/parser.rs b/libraries/program-error/derive/src/parser.rs new file mode 100644 index 00000000000..97b59f0c3aa --- /dev/null +++ b/libraries/program-error/derive/src/parser.rs @@ -0,0 +1,56 @@ +use { + proc_macro2::Ident, + syn::{ + parse::{Parse, ParseStream}, + token::Comma, + LitBool, Token, + }, +}; + +/// Possible arguments to the `#[spl_program_error]` attribute +pub struct SplProgramErrorArgs { + /// Whether to hash the error codes using `solana_program::hash` + /// or to use the default error code assigned by `num_traits`. + pub hash_error_codes: bool, +} + +impl Parse for SplProgramErrorArgs { + fn parse(input: ParseStream) -> syn::Result { + if input.is_empty() { + return Ok(Self { + hash_error_codes: false, + }); + } + match SplProgramErrorArgParser::parse(input)? { + SplProgramErrorArgParser::HashErrorCodes { value, .. } => Ok(Self { + hash_error_codes: value.value, + }), + } + } +} + +/// Parser for args to the `#[spl_program_error]` attribute +/// ie. `#[spl_program_error(hash_error_codes = true)]` +enum SplProgramErrorArgParser { + HashErrorCodes { + _ident: Ident, + _equals_sign: Token![=], + value: LitBool, + _comma: Option, + }, +} + +impl Parse for SplProgramErrorArgParser { + fn parse(input: ParseStream) -> syn::Result { + let _ident = input.parse::()?; + let _equals_sign = input.parse::()?; + let value = input.parse::()?; + let _comma: Option = input.parse().unwrap_or(None); + Ok(Self::HashErrorCodes { + _ident, + _equals_sign, + value, + _comma, + }) + } +} diff --git a/libraries/program-error/src/lib.rs b/libraries/program-error/src/lib.rs index 8c1fb7c0b7f..739995dd197 100644 --- a/libraries/program-error/src/lib.rs +++ b/libraries/program-error/src/lib.rs @@ -8,10 +8,10 @@ extern crate self as spl_program_error; // Make these available downstream for the macro to work without // additional imports -pub use num_derive; -pub use num_traits; -pub use solana_program; -pub use spl_program_error_derive::{ - spl_program_error, DecodeError, IntoProgramError, PrintProgramError, +pub use { + num_derive, num_traits, solana_program, + spl_program_error_derive::{ + spl_program_error, DecodeError, IntoProgramError, PrintProgramError, + }, + thiserror, }; -pub use thiserror; diff --git a/libraries/program-error/tests/decode.rs b/libraries/program-error/tests/decode.rs index 28aeac56b61..0c7c209bc14 100644 --- a/libraries/program-error/tests/decode.rs +++ b/libraries/program-error/tests/decode.rs @@ -1,5 +1,5 @@ //! Tests `#[derive(DecodeError)]` -//! + use spl_program_error::*; /// Example error diff --git a/libraries/program-error/tests/into.rs b/libraries/program-error/tests/into.rs index 97e8868a9be..0f32b8f40d3 100644 --- a/libraries/program-error/tests/into.rs +++ b/libraries/program-error/tests/into.rs @@ -1,5 +1,5 @@ //! Tests `#[derive(IntoProgramError)]` -//! + use spl_program_error::*; /// Example error diff --git a/libraries/program-error/tests/mod.rs b/libraries/program-error/tests/mod.rs index ae50368fd35..413587758e9 100644 --- a/libraries/program-error/tests/mod.rs +++ b/libraries/program-error/tests/mod.rs @@ -6,13 +6,15 @@ pub mod spl; #[cfg(test)] mod tests { - use super::*; - use serial_test::serial; - use solana_program::{ - decode_error::DecodeError, - program_error::{PrintProgramError, ProgramError}, + use { + super::*, + serial_test::serial, + solana_program::{ + decode_error::DecodeError, + program_error::{PrintProgramError, ProgramError}, + }, + std::sync::{Arc, RwLock}, }; - use std::sync::{Arc, RwLock}; // Used to capture output for `PrintProgramError` for testing lazy_static::lazy_static! { diff --git a/libraries/program-error/tests/print.rs b/libraries/program-error/tests/print.rs index ddb62cf60b6..8b68f66a581 100644 --- a/libraries/program-error/tests/print.rs +++ b/libraries/program-error/tests/print.rs @@ -1,5 +1,5 @@ //! Tests `#[derive(PrintProgramError)]` -//! + use spl_program_error::*; /// Example error diff --git a/libraries/program-error/tests/spl.rs b/libraries/program-error/tests/spl.rs index 8cd557b0f36..d9b7140d741 100644 --- a/libraries/program-error/tests/spl.rs +++ b/libraries/program-error/tests/spl.rs @@ -1,5 +1,5 @@ //! Tests `#[spl_program_error]` -//! + use spl_program_error::*; /// Example error @@ -18,3 +18,47 @@ pub enum ExampleError { fn test_macros_compile() { let _ = ExampleError::MintHasNoMintAuthority; } + +/// Example library error with namespace +#[spl_program_error(hash_error_codes = true)] +enum ExampleLibraryError { + /// This is a very informative error + #[error("This is a very informative error")] + VeryInformativeError, + /// This is a super important error + #[error("This is a super important error")] + SuperImportantError, + /// This is a mega serious error + #[error("This is a mega serious error")] + MegaSeriousError, + /// You are toast + #[error("You are toast")] + YouAreToast, +} + +/// Tests hashing of error codes into unique `u32` values +#[test] +fn test_library_error_codes() { + fn get_error_code_check(hash_input: &str) -> u32 { + let preimage = solana_program::hash::hashv(&[hash_input.as_bytes()]); + let mut bytes = [0u8; 4]; + bytes.copy_from_slice(&preimage.to_bytes()[8..12]); + u32::from_le_bytes(bytes) + } + assert_eq!( + ExampleLibraryError::VeryInformativeError as u32, + get_error_code_check("spl_program_error:ExampleLibraryError:VeryInformativeError"), + ); + assert_eq!( + ExampleLibraryError::SuperImportantError as u32, + get_error_code_check("spl_program_error:ExampleLibraryError:SuperImportantError"), + ); + assert_eq!( + ExampleLibraryError::MegaSeriousError as u32, + get_error_code_check("spl_program_error:ExampleLibraryError:MegaSeriousError"), + ); + assert_eq!( + ExampleLibraryError::YouAreToast as u32, + get_error_code_check("spl_program_error:ExampleLibraryError:YouAreToast"), + ); +} diff --git a/libraries/tlv-account-resolution/src/error.rs b/libraries/tlv-account-resolution/src/error.rs index 3fab843e07c..ae301df545d 100644 --- a/libraries/tlv-account-resolution/src/error.rs +++ b/libraries/tlv-account-resolution/src/error.rs @@ -3,7 +3,7 @@ use spl_program_error::*; /// Errors that may be returned by the Account Resolution library. -#[spl_program_error] +#[spl_program_error(hash_error_codes = true)] pub enum AccountResolutionError { /// Incorrect account provided #[error("Incorrect account provided")] diff --git a/libraries/type-length-value/src/error.rs b/libraries/type-length-value/src/error.rs index a24d63de90b..b2fe0847eae 100644 --- a/libraries/type-length-value/src/error.rs +++ b/libraries/type-length-value/src/error.rs @@ -3,7 +3,7 @@ use spl_program_error::*; /// Errors that may be returned by the Token program. -#[spl_program_error] +#[spl_program_error(hash_error_codes = true)] pub enum TlvError { /// Type not found in TLV data #[error("Type not found in TLV data")] diff --git a/token-metadata/interface/src/error.rs b/token-metadata/interface/src/error.rs index a0ebd0b64f3..22be631f526 100644 --- a/token-metadata/interface/src/error.rs +++ b/token-metadata/interface/src/error.rs @@ -3,7 +3,7 @@ use spl_program_error::*; /// Errors that may be returned by the interface. -#[spl_program_error] +#[spl_program_error(hash_error_codes = true)] pub enum TokenMetadataError { /// Incorrect account provided #[error("Incorrect account provided")] diff --git a/token/transfer-hook-example/tests/functional.rs b/token/transfer-hook-example/tests/functional.rs index f60ce1927ff..bdef0cdb80e 100644 --- a/token/transfer-hook-example/tests/functional.rs +++ b/token/transfer-hook-example/tests/functional.rs @@ -18,7 +18,8 @@ use { transaction::{Transaction, TransactionError}, }, spl_tlv_account_resolution::{ - account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, + account::ExtraAccountMeta, error::AccountResolutionError, seeds::Seed, + state::ExtraAccountMetaList, }, spl_token_2022::{ extension::{transfer_hook::TransferHookAccount, ExtensionType, StateWithExtensionsMut}, @@ -270,7 +271,7 @@ async fn success_execute() { error, TransactionError::InstructionError( 0, - InstructionError::Custom(TransferHookError::IncorrectAccount as u32), + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), ) ); } @@ -309,7 +310,7 @@ async fn success_execute() { error, TransactionError::InstructionError( 0, - InstructionError::Custom(TransferHookError::IncorrectAccount as u32), + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), ) ); } @@ -356,7 +357,7 @@ async fn success_execute() { error, TransactionError::InstructionError( 0, - InstructionError::Custom(TransferHookError::IncorrectAccount as u32), + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), ) ); } @@ -395,7 +396,7 @@ async fn success_execute() { error, TransactionError::InstructionError( 0, - InstructionError::Custom(TransferHookError::IncorrectAccount as u32), + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), ) ); } diff --git a/token/transfer-hook-interface/Cargo.toml b/token/transfer-hook-interface/Cargo.toml index e2b751c5039..b60f6918c64 100644 --- a/token/transfer-hook-interface/Cargo.toml +++ b/token/transfer-hook-interface/Cargo.toml @@ -10,14 +10,11 @@ edition = "2021" [dependencies] arrayref = "0.3.7" bytemuck = { version = "1.13.1", features = ["derive"] } -num-derive = "0.4" -num-traits = "0.2" -num_enum = "0.7.0" solana-program = "1.16.3" spl-discriminator = { version = "0.1.0" , path = "../../libraries/discriminator" } +spl-program-error = { version = "0.2.0" , path = "../../libraries/program-error" } spl-tlv-account-resolution = { version = "0.2.0" , path = "../../libraries/tlv-account-resolution" } spl-type-length-value = { version = "0.2.0" , path = "../../libraries/type-length-value" } -thiserror = "1.0" [lib] crate-type = ["cdylib", "lib"] diff --git a/token/transfer-hook-interface/src/error.rs b/token/transfer-hook-interface/src/error.rs index c387b8b15b3..e3b50b0a5c9 100644 --- a/token/transfer-hook-interface/src/error.rs +++ b/token/transfer-hook-interface/src/error.rs @@ -1,19 +1,11 @@ //! Error types -use { - num_derive::FromPrimitive, - solana_program::{ - decode_error::DecodeError, - msg, - program_error::{PrintProgramError, ProgramError}, - }, - thiserror::Error, -}; +use spl_program_error::*; /// Errors that may be returned by the interface. -#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +#[spl_program_error(hash_error_codes = true)] pub enum TransferHookError { - /// Incorrect account provided +/// Incorrect account provided #[error("Incorrect account provided")] IncorrectAccount, /// Mint has no mint authority @@ -26,35 +18,3 @@ pub enum TransferHookError { #[error("Program called outside of a token transfer")] ProgramCalledOutsideOfTransfer, } -impl From for ProgramError { - fn from(e: TransferHookError) -> Self { - ProgramError::Custom(e as u32) - } -} -impl DecodeError for TransferHookError { - fn type_of() -> &'static str { - "TransferHookError" - } -} - -impl PrintProgramError for TransferHookError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - Self::IncorrectAccount => msg!("Incorrect account provided"), - Self::MintHasNoMintAuthority => msg!("Mint has no mint authority"), - Self::IncorrectMintAuthority => { - msg!("Incorrect mint authority has signed the instruction") - } - Self::ProgramCalledOutsideOfTransfer => { - msg!("Program called outside of a token transfer") - } - } - } -}