diff --git a/lang/src/error.rs b/lang/src/error.rs index 2e6a04fe23..d8abdfa580 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -183,6 +183,32 @@ pub enum ErrorCode { /// 2042 - Account must be migrated before exiting #[msg("Account must be migrated before exiting")] AccountNotMigrated, + /// Extension constraints - cont. + /// + /// 2043 - A non-transferable extension constraint was violated + #[msg("A non-transferable extension constraint was violated")] + ConstraintMintNonTransferableExtension, + /// 2044 - A transfer fee extension constraint was violated + #[msg("A transfer fee extension constraint was violated")] + ConstraintMintTransferFeeExtension, + /// 2045 - A transfer fee extension config authority constraint was violated + #[msg("A transfer fee extension config authority constraint was violated")] + ConstraintMintTransferFeeConfigAuthority, + /// 2046 - A transfer fee extension withheld authority constraint was violated + #[msg("A transfer fee extension withheld authority constraint was violated")] + ConstraintMintTransferFeeWithheldAuthority, + /// 2047 - An interest bearing extension constraint was violated + #[msg("An interest bearing extension constraint was violated")] + ConstraintMintInterestBearingExtension, + /// 2048 - An interest bearing extension rate authority constraint was violated + #[msg("An interest bearing extension rate authority constraint was violated")] + ConstraintMintInterestBearingRateAuthority, + /// 2049 - A default account state extension constraint was violated + #[msg("A default account state extension constraint was violated")] + ConstraintMintDefaultAccountStateExtension, + /// 2050 - A default account state extension state constraint was violated + #[msg("A default account state extension state constraint was violated")] + ConstraintMintDefaultAccountState, // Require /// 2500 - A require expression was violated diff --git a/lang/src/lib.rs b/lang/src/lib.rs index fb28f0d9f1..0ad3ab66dc 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -62,7 +62,7 @@ pub use { anchor_attribute_event::{emit, event}, anchor_attribute_program::{declare_program, instruction, program}, anchor_derive_accounts::Accounts, - anchor_derive_serde::{__erase, AnchorDeserialize, AnchorSerialize}, + anchor_derive_serde::{AnchorDeserialize, AnchorSerialize, __erase}, anchor_derive_space::InitSpace, borsh::ser::BorshSerialize as AnchorSerialize, const_crypto::ed25519::derive_program_address, diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index f89991d223..d1c010201e 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -769,6 +769,14 @@ fn generate_constraint_init_group( permanent_delegate, transfer_hook_authority, transfer_hook_program_id, + non_transferable, + transfer_fee_config_authority, + transfer_fee_withheld_authority, + transfer_fee_basis_points, + transfer_fee_max_fee, + interest_bearing_authority, + interest_bearing_rate, + default_account_state, } => { let token_program = match token_program { Some(t) => t.to_token_stream(), @@ -833,6 +841,41 @@ fn generate_constraint_init_group( None => quote! {}, }; + let transfer_fee_config_authority_check = match transfer_fee_config_authority { + Some(tfca) => check_scope.generate_check(tfca), + None => quote! {}, + }; + + let transfer_fee_withheld_authority_check = match transfer_fee_withheld_authority { + Some(tfwa) => check_scope.generate_check(tfwa), + None => quote! {}, + }; + + let transfer_fee_basis_points_check = match transfer_fee_basis_points { + Some(tfbp) => check_scope.generate_check(tfbp), + None => quote! {}, + }; + + let transfer_fee_max_fee_check = match transfer_fee_max_fee { + Some(tfmf) => check_scope.generate_check(tfmf), + None => quote! {}, + }; + + let interest_bearing_authority_check = match interest_bearing_authority { + Some(iba) => check_scope.generate_check(iba), + None => quote! {}, + }; + + let interest_bearing_rate_check = match interest_bearing_rate { + Some(ibr) => check_scope.generate_check(ibr), + None => quote! {}, + }; + + let default_account_state_check = match default_account_state { + Some(das) => check_scope.generate_check(das), + None => quote! {}, + }; + let system_program_optional_check = check_scope.generate_check(system_program); let token_program_optional_check = check_scope.generate_check(&token_program); let rent_optional_check = check_scope.generate_check(rent); @@ -853,6 +896,13 @@ fn generate_constraint_init_group( #transfer_hook_authority_check #transfer_hook_program_id_check #permanent_delegate_check + #transfer_fee_config_authority_check + #transfer_fee_withheld_authority_check + #transfer_fee_basis_points_check + #transfer_fee_max_fee_check + #interest_bearing_authority_check + #interest_bearing_rate_check + #default_account_state_check }; let payer_optional_check = check_scope.generate_check(payer); @@ -884,6 +934,26 @@ fn generate_constraint_init_group( extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::PermanentDelegate}); } + if non_transferable.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::NonTransferable}); + } + + if transfer_fee_config_authority.is_some() + || transfer_fee_withheld_authority.is_some() + || transfer_fee_basis_points.is_some() + || transfer_fee_max_fee.is_some() + { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::TransferFeeConfig}); + } + + if interest_bearing_authority.is_some() || interest_bearing_rate.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::InterestBearingConfig}); + } + + if default_account_state.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::DefaultAccountState}); + } + let mint_space = if extensions.is_empty() { quote! { ::anchor_spl::token::Mint::LEN } } else { @@ -955,6 +1025,45 @@ fn generate_constraint_init_group( None => quote! { Option::::None }, }; + let transfer_fee_config_authority = match transfer_fee_config_authority { + Some(tfca) => quote! { Option::::Some(#tfca.key()) }, + None => quote! { Option::::None }, + }; + + let transfer_fee_withheld_authority = match transfer_fee_withheld_authority { + Some(tfwa) => quote! { Option::::Some(#tfwa.key()) }, + None => quote! { Option::::None }, + }; + + let transfer_fee_basis_points = match transfer_fee_basis_points { + Some(tfbp) => quote! { Option::::Some(#tfbp) }, + None => quote! { Option::::None }, + }; + + let transfer_fee_max_fee = match transfer_fee_max_fee { + Some(tfmf) => quote! { Option::::Some(#tfmf) }, + None => quote! { Option::::None }, + }; + + let interest_bearing_authority = match interest_bearing_authority { + Some(iba) => quote! { Option::::Some(#iba.key()) }, + None => quote! { Option::::None }, + }; + + let interest_bearing_rate = match interest_bearing_rate { + Some(ibr) => quote! { Option::::Some(#ibr) }, + None => quote! { Option::::None }, + }; + + let default_account_state = match default_account_state { + Some(das) => { + quote! { Option::::Some(#das) } + } + None => { + quote! { Option::::None } + } + }; + let create_account = generate_create_account( field, mint_space, @@ -1028,6 +1137,38 @@ fn generate_constraint_init_group( mint: #field.to_account_info(), }), #permanent_delegate.unwrap())?; }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::NonTransferable => { + ::anchor_spl::token_interface::non_transferable_mint_initialize(anchor_lang::context::CpiContext::new(cpi_program_id, ::anchor_spl::token_interface::NonTransferableMintInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }))?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::TransferFeeConfig => { + ::anchor_spl::token_interface::transfer_fee_initialize(anchor_lang::context::CpiContext::new(cpi_program_id, ::anchor_spl::token_interface::TransferFeeInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }), + #transfer_fee_config_authority.as_ref(), + #transfer_fee_withheld_authority.as_ref(), + #transfer_fee_basis_points.unwrap(), + #transfer_fee_max_fee.unwrap(), + )?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::InterestBearingConfig => { + ::anchor_spl::token_interface::interest_bearing_mint_initialize(anchor_lang::context::CpiContext::new(cpi_program_id, ::anchor_spl::token_interface::InterestBearingMintInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }), + #interest_bearing_authority, + #interest_bearing_rate.unwrap(), + )?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::DefaultAccountState => { + ::anchor_spl::token_interface::default_account_state_initialize(anchor_lang::context::CpiContext::new(cpi_program_id, ::anchor_spl::token_interface::DefaultAccountStateInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }), #default_account_state.as_ref().unwrap())?; + }, // All extensions specified by the user should be implemented. // If this line runs, it means there is a bug in the codegen. _ => unimplemented!("{e:?}"), @@ -1588,6 +1729,90 @@ fn generate_constraint_mint( None => quote! {}, }; + let non_transferable_check = match &c.non_transferable { + Some(_) => { + quote! { + let non_transferable = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::non_transferable::NonTransferable>(#account_ref); + if non_transferable.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintNonTransferableExtension.into()); + } + } + } + None => quote! {}, + }; + + let transfer_fee_config_authority_check = match &c.transfer_fee_config_authority { + Some(transfer_fee_config_authority) => { + let transfer_fee_config_authority_optional_check = + optional_check_scope.generate_check(transfer_fee_config_authority); + quote! { + let transfer_fee = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::transfer_fee::TransferFeeConfig>(#account_ref); + if transfer_fee.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferFeeExtension.into()); + } + #transfer_fee_config_authority_optional_check + if transfer_fee.unwrap().transfer_fee_config_authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#transfer_fee_config_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferFeeConfigAuthority.into()); + } + } + } + None => quote! {}, + }; + + let transfer_fee_withheld_authority_check = match &c.transfer_fee_withheld_authority { + Some(transfer_fee_withheld_authority) => { + let transfer_fee_withheld_authority_optional_check = + optional_check_scope.generate_check(transfer_fee_withheld_authority); + quote! { + let transfer_fee = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::transfer_fee::TransferFeeConfig>(#account_ref); + if transfer_fee.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferFeeExtension.into()); + } + #transfer_fee_withheld_authority_optional_check + if transfer_fee.unwrap().withdraw_withheld_authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#transfer_fee_withheld_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferFeeWithheldAuthority.into()); + } + } + } + None => quote! {}, + }; + + let interest_bearing_authority_check = match &c.interest_bearing_authority { + Some(interest_bearing_authority) => { + let interest_bearing_authority_optional_check = + optional_check_scope.generate_check(interest_bearing_authority); + quote! { + let interest_bearing = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::interest_bearing_mint::InterestBearingConfig>(#account_ref); + if interest_bearing.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintInterestBearingExtension.into()); + } + #interest_bearing_authority_optional_check + if interest_bearing.unwrap().rate_authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#interest_bearing_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintInterestBearingRateAuthority.into()); + } + } + } + None => quote! {}, + }; + + let default_account_state_check = match &c.default_account_state { + Some(default_account_state) => { + let default_account_state_optional_check = + optional_check_scope.generate_check(default_account_state); + quote! { + let account_state = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::default_account_state::DefaultAccountState>(#account_ref); + if account_state.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintDefaultAccountStateExtension.into()); + } + #default_account_state_optional_check + if account_state.unwrap().state != #default_account_state as u8 { + return Err(anchor_lang::error::ErrorCode::ConstraintMintDefaultAccountState.into()); + } + } + } + None => quote! {}, + }; + quote! { { #decimal_check @@ -1604,6 +1829,11 @@ fn generate_constraint_mint( #permanent_delegate_check #transfer_hook_authority_check #transfer_hook_program_id_check + #non_transferable_check + #transfer_fee_config_authority_check + #transfer_fee_withheld_authority_check + #interest_bearing_authority_check + #default_account_state_check } } } diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 67c265a644..94719a08b9 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -834,6 +834,14 @@ pub enum ConstraintToken { ExtensionTokenHookAuthority(Context), ExtensionTokenHookProgramId(Context), ExtensionPermanentDelegate(Context), + ExtensionNonTransferable(Context), + ExtensionTransferFeeConfigAuthority(Context), + ExtensionTransferFeeWithheldAuthority(Context), + ExtensionTransferFeeBasisPoints(Context), + ExtensionTransferFeeMaxFee(Context), + ExtensionInterestBearingRateAuthority(Context), + ExtensionInterestBearingRate(Context), + ExtensionDefaultAccountState(Context), } impl Parse for ConstraintToken { @@ -1074,6 +1082,29 @@ pub struct ConstraintExtensionPermanentDelegate { pub permanent_delegate: Expr, } +#[derive(Debug, Clone)] +pub struct ConstraintExtensionNonTransferable {} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionTransferFeeBasisPoints { + pub basis_points: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionTransferFeeMaxFee { + pub max_fee: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionInterestBearingRate { + pub rate: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionDefaultAccountState { + pub state: Expr, +} + #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum InitKind { @@ -1111,6 +1142,14 @@ pub enum InitKind { permanent_delegate: Option, transfer_hook_authority: Option, transfer_hook_program_id: Option, + non_transferable: Option<()>, + transfer_fee_config_authority: Option, + transfer_fee_withheld_authority: Option, + transfer_fee_basis_points: Option, + transfer_fee_max_fee: Option, + interest_bearing_authority: Option, + interest_bearing_rate: Option, + default_account_state: Option, }, } @@ -1229,6 +1268,14 @@ pub struct ConstraintTokenMintGroup { pub permanent_delegate: Option, pub transfer_hook_authority: Option, pub transfer_hook_program_id: Option, + pub non_transferable: Option<()>, + pub transfer_fee_config_authority: Option, + pub transfer_fee_withheld_authority: Option, + pub transfer_fee_basis_points: Option, + pub transfer_fee_max_fee: Option, + pub interest_bearing_authority: Option, + pub interest_bearing_rate: Option, + pub default_account_state: Option, } // Syntax context object for preserving metadata about the inner item. diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index fda6d48dae..72c5107ee9 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -262,6 +262,105 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } + "non_transferable" => ConstraintToken::ExtensionNonTransferable(Context::new( + ident.span(), + ConstraintExtensionNonTransferable {}, + )), + "transfer_fee" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "config_authority" => { + ConstraintToken::ExtensionTransferFeeConfigAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + "withheld_authority" => { + ConstraintToken::ExtensionTransferFeeWithheldAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + "basis_points" => { + ConstraintToken::ExtensionTransferFeeBasisPoints(Context::new( + span, + ConstraintExtensionTransferFeeBasisPoints { + basis_points: stream.parse()?, + }, + )) + } + "max_fee" => ConstraintToken::ExtensionTransferFeeMaxFee(Context::new( + span, + ConstraintExtensionTransferFeeMaxFee { + max_fee: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + "interest_bearing" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "authority" => { + ConstraintToken::ExtensionInterestBearingRateAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + "rate" => ConstraintToken::ExtensionInterestBearingRate(Context::new( + span, + ConstraintExtensionInterestBearingRate { + rate: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + "default_account_state" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "state" => ConstraintToken::ExtensionDefaultAccountState(Context::new( + span, + ConstraintExtensionDefaultAccountState { + state: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } @@ -544,6 +643,15 @@ pub struct ConstraintGroupBuilder<'ty> { pub extension_transfer_hook_authority: Option>, pub extension_transfer_hook_program_id: Option>, pub extension_permanent_delegate: Option>, + pub extension_non_transferable: Option>, + pub extension_transfer_fee_config_authority: Option>, + pub extension_transfer_fee_withheld_authority: Option>, + pub extension_transfer_fee_basis_points: + Option>, + pub extension_transfer_fee_max_fee: Option>, + pub extension_interest_bearing_authority: Option>, + pub extension_interest_bearing_rate: Option>, + pub extension_default_account_state: Option>, pub bump: Option>, pub program_seed: Option>, pub realloc: Option>, @@ -590,6 +698,14 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_authority: None, extension_transfer_hook_program_id: None, extension_permanent_delegate: None, + extension_non_transferable: None, + extension_transfer_fee_config_authority: None, + extension_transfer_fee_withheld_authority: None, + extension_transfer_fee_basis_points: None, + extension_transfer_fee_max_fee: None, + extension_interest_bearing_authority: None, + extension_interest_bearing_rate: None, + extension_default_account_state: None, bump: None, program_seed: None, realloc: None, @@ -803,6 +919,14 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_authority, extension_transfer_hook_program_id, extension_permanent_delegate, + extension_non_transferable, + extension_transfer_fee_config_authority, + extension_transfer_fee_withheld_authority, + extension_transfer_fee_basis_points, + extension_transfer_fee_max_fee, + extension_interest_bearing_authority, + extension_interest_bearing_rate, + extension_default_account_state, bump, program_seed, realloc, @@ -907,6 +1031,14 @@ impl<'ty> ConstraintGroupBuilder<'ty> { &extension_transfer_hook_authority, &extension_transfer_hook_program_id, &extension_permanent_delegate, + &extension_non_transferable, + &extension_transfer_fee_config_authority, + &extension_transfer_fee_withheld_authority, + &extension_transfer_fee_basis_points, + &extension_transfer_fee_max_fee, + &extension_interest_bearing_authority, + &extension_interest_bearing_rate, + &extension_default_account_state, ) { ( None, @@ -923,6 +1055,14 @@ impl<'ty> ConstraintGroupBuilder<'ty> { None, None, None, + None, + None, + None, + None, + None, + None, + None, + None, ) => None, _ => Some(ConstraintTokenMintGroup { decimals: mint_decimals @@ -968,6 +1108,28 @@ impl<'ty> ConstraintGroupBuilder<'ty> { transfer_hook_program_id: extension_transfer_hook_program_id .as_ref() .map(|a| a.clone().into_inner().program_id), + non_transferable: extension_non_transferable.as_ref().map(|_| ()), + transfer_fee_config_authority: extension_transfer_fee_config_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + transfer_fee_withheld_authority: extension_transfer_fee_withheld_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + transfer_fee_basis_points: extension_transfer_fee_basis_points + .as_ref() + .map(|a| a.clone().into_inner().basis_points), + transfer_fee_max_fee: extension_transfer_fee_max_fee + .as_ref() + .map(|a| a.clone().into_inner().max_fee), + interest_bearing_authority: extension_interest_bearing_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + interest_bearing_rate: extension_interest_bearing_rate + .as_ref() + .map(|a| a.clone().into_inner().rate), + default_account_state: extension_default_account_state + .as_ref() + .map(|a| a.clone().into_inner().state), }), }; @@ -1004,6 +1166,11 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .map(|tp| tp.into_inner().token_program), } } else if let Some(d) = &mint_decimals { + let init_transfer_fee = extension_transfer_fee_config_authority + .is_some() + || extension_transfer_fee_withheld_authority.is_some() + || extension_transfer_fee_basis_points.is_some() + || extension_transfer_fee_max_fee.is_some(); InitKind::Mint { decimals: d.clone().into_inner().decimals, owner: match &mint_authority { @@ -1044,6 +1211,60 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .map(|tha| tha.into_inner().authority), transfer_hook_program_id: extension_transfer_hook_program_id .map(|thpid| thpid.into_inner().program_id), + non_transferable: extension_non_transferable.map(|_| ()), + transfer_fee_config_authority: + extension_transfer_fee_config_authority + .map(|tfca| tfca.into_inner().authority), + transfer_fee_withheld_authority: + extension_transfer_fee_withheld_authority + .map(|tfwa| tfwa.into_inner().authority), + transfer_fee_basis_points: match ( + &extension_transfer_fee_basis_points, + init_transfer_fee, + ) { + (Some(tfbp), _) => Some(tfbp.clone().into_inner().basis_points), + (None, true) => { + return Err(ParseError::new( + d.span(), + "fee basis points must be provided to initialize a \ + mint with transfer fee extension", + )) + } + _ => None, + }, + transfer_fee_max_fee: match ( + &extension_transfer_fee_max_fee, + init_transfer_fee, + ) { + (Some(tfmf), _) => Some(tfmf.clone().into_inner().max_fee), + (None, true) => { + return Err(ParseError::new( + d.span(), + "maximum fee must be provided to initialize a mint \ + with transfer fee extension", + )) + } + _ => None, + }, + interest_bearing_authority: extension_interest_bearing_authority + .as_ref() + .map(|iba| iba.clone().into_inner().authority), + interest_bearing_rate: match ( + &extension_interest_bearing_rate, + &extension_interest_bearing_authority, + ) { + (Some(ibr), _) => Some(ibr.clone().into_inner().rate), + (None, Some(_)) => { + return Err(ParseError::new( + d.span(), + "rate must be provided to initialize a mint with \ + interest bearing extension", + )) + } + _ => None, + }, + default_account_state: extension_default_account_state + .map(|das| das.into_inner().state), } } else { InitKind::Program { @@ -1136,6 +1357,28 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.add_extension_permanent_delegate(c) } ConstraintToken::Dup(c) => self.add_dup(c), + ConstraintToken::ExtensionNonTransferable(c) => self.add_extension_non_transferable(c), + ConstraintToken::ExtensionTransferFeeConfigAuthority(c) => { + self.add_extension_transfer_fee_config_authority(c) + } + ConstraintToken::ExtensionTransferFeeWithheldAuthority(c) => { + self.add_extension_transfer_fee_withheld_authority(c) + } + ConstraintToken::ExtensionTransferFeeBasisPoints(c) => { + self.add_extension_transfer_fee_basis_points(c) + } + ConstraintToken::ExtensionTransferFeeMaxFee(c) => { + self.add_extension_transfer_fee_max_fee(c) + } + ConstraintToken::ExtensionInterestBearingRateAuthority(c) => { + self.add_extension_interest_bearing_authority(c) + } + ConstraintToken::ExtensionInterestBearingRate(c) => { + self.add_extension_interest_bearing_rate(c) + } + ConstraintToken::ExtensionDefaultAccountState(c) => { + self.add_extension_default_account_state(c) + } } } @@ -1727,4 +1970,116 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.dup.replace(c); Ok(()) } + + fn add_extension_non_transferable( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_non_transferable.is_some() { + return Err(ParseError::new( + c.span(), + "extension non-transferable already provided", + )); + } + self.extension_non_transferable.replace(c); + Ok(()) + } + + fn add_extension_transfer_fee_config_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_fee_config_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer fee config authority already provided", + )); + } + self.extension_transfer_fee_config_authority.replace(c); + Ok(()) + } + + fn add_extension_transfer_fee_withheld_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_fee_withheld_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer fee withheld authority already provided", + )); + } + self.extension_transfer_fee_withheld_authority.replace(c); + Ok(()) + } + + fn add_extension_transfer_fee_basis_points( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_fee_basis_points.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer fee basis points already provided", + )); + } + self.extension_transfer_fee_basis_points.replace(c); + Ok(()) + } + + fn add_extension_transfer_fee_max_fee( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_fee_max_fee.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer fee maximum fee already provided", + )); + } + self.extension_transfer_fee_max_fee.replace(c); + Ok(()) + } + + fn add_extension_interest_bearing_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_interest_bearing_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension interest bearing authority already provided", + )); + } + self.extension_interest_bearing_authority.replace(c); + Ok(()) + } + + fn add_extension_interest_bearing_rate( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_interest_bearing_rate.is_some() { + return Err(ParseError::new( + c.span(), + "extension interest bearing rate already provided", + )); + } + self.extension_interest_bearing_rate.replace(c); + Ok(()) + } + + fn add_extension_default_account_state( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_default_account_state.is_some() { + return Err(ParseError::new( + c.span(), + "extension default account state already provided", + )); + } + self.extension_default_account_state.replace(c); + Ok(()) + } } diff --git a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs index eaf98c5313..b05fbab7a4 100644 --- a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs +++ b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs @@ -2,11 +2,9 @@ use anchor_lang::{prelude::*, solana_program::entrypoint::ProgramResult}; use anchor_spl::{ associated_token::AssociatedToken, - token_2022::spl_token_2022::extension::{ - group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, - mint_close_authority::MintCloseAuthority, permanent_delegate::PermanentDelegate, - transfer_hook::TransferHook, - }, + token_2022::spl_token_2022::{extension::{ + default_account_state::DefaultAccountState, group_member_pointer::GroupMemberPointer, interest_bearing_mint::InterestBearingConfig, metadata_pointer::MetadataPointer, mint_close_authority::MintCloseAuthority, non_transferable::NonTransferable, permanent_delegate::PermanentDelegate, transfer_fee::TransferFeeConfig, transfer_hook::TransferHook + }, state::AccountState}, token_interface::{ get_mint_extension_data, spl_token_metadata_interface::state::TokenMetadata, token_metadata_initialize, Mint, Token2022, TokenAccount, TokenMetadataInitialize, @@ -19,6 +17,10 @@ use crate::{ update_account_lamports_to_minimum_balance, META_LIST_ACCOUNT_SEED, }; +const BASIS_POINTS: u16 = 100; +const MAX_FEE: u64 = 10000; +const RATE: i16 = 100; + #[derive(AnchorDeserialize, AnchorSerialize)] pub struct CreateMintAccountArgs { pub name: String, @@ -53,6 +55,14 @@ pub struct CreateMintAccount<'info> { extensions::transfer_hook::program_id = crate::ID, extensions::close_authority::authority = authority, extensions::permanent_delegate::delegate = authority, + extensions::non_transferable, + extensions::transfer_fee::config_authority = authority, + extensions::transfer_fee::withheld_authority = authority, + extensions::transfer_fee::basis_points = BASIS_POINTS, + extensions::transfer_fee::max_fee = MAX_FEE, + extensions::interest_bearing::authority = authority, + extensions::interest_bearing::rate = RATE, + extensions::default_account_state::state = AccountState::Frozen, )] pub mint: Box>, #[account( @@ -150,6 +160,38 @@ pub fn handler(ctx: Context, args: CreateMintAccountArgs) -> group_member_pointer.member_address, OptionalNonZeroPubkey::try_from(mint_key)? ); + let _ = get_mint_extension_data::(mint_data)?; + let transfer_fee = get_mint_extension_data::(mint_data)?; + assert_eq!( + transfer_fee.transfer_fee_config_authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!( + transfer_fee.withdraw_withheld_authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!( + transfer_fee.newer_transfer_fee.transfer_fee_basis_points, + BASIS_POINTS.into() + ); + assert_eq!( + transfer_fee.newer_transfer_fee.maximum_fee, + MAX_FEE.into() + ); + let interest_bearing = get_mint_extension_data::(mint_data)?; + assert_eq!( + interest_bearing.rate_authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!( + interest_bearing.current_rate, + RATE.into() + ); + let default_account_state = get_mint_extension_data::(mint_data)?; + assert_eq!( + default_account_state.state, + AccountState::Frozen as u8 + ); // transfer minimum rent to mint account update_account_lamports_to_minimum_balance( ctx.accounts.mint.to_account_info(), @@ -175,6 +217,11 @@ pub struct CheckMintExtensionConstraints<'info> { extensions::transfer_hook::program_id = crate::ID, extensions::close_authority::authority = authority, extensions::permanent_delegate::delegate = authority, + extensions::non_transferable, + extensions::transfer_fee::config_authority = authority, + extensions::transfer_fee::withheld_authority = authority, + extensions::interest_bearing::authority = authority, + extensions::default_account_state::state = AccountState::Frozen, )] pub mint: Box>, }