diff --git a/.github/workflows/reusable-tests.yaml b/.github/workflows/reusable-tests.yaml index acfa54ce0e..f4aeedd909 100644 --- a/.github/workflows/reusable-tests.yaml +++ b/.github/workflows/reusable-tests.yaml @@ -388,6 +388,8 @@ jobs: path: spl/token-wrapper - cmd: cd tests/spl/transfer-hook && anchor test --skip-lint path: spl/transfer-hook + - cmd: cd tests/spl/token-extensions && anchor test --skip-lint + path: spl/token-extensions - cmd: cd tests/multisig && anchor test --skip-lint path: tests/multisig # - cmd: cd tests/lockup && anchor test --skip-lint diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b24e5722..4647440fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ The minor version will be incremented upon a breaking change and the patch versi - idl: Add `docs` field for constants ([#2887](https://github.com/coral-xyz/anchor/pull/2887)). - idl: Store deployment addresses for other clusters ([#2892](https://github.com/coral-xyz/anchor/pull/2892)). - lang: Add `Event` utility type to get events from bytes ([#2897](https://github.com/coral-xyz/anchor/pull/2897)). +- spl: Add support for [token extensions](https://solana.com/solutions/token-extensions) ([#2789](https://github.com/coral-xyz/anchor/pull/2789)). ### Fixes diff --git a/Cargo.lock b/Cargo.lock index 549a0608c8..a3688edb97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,8 +309,11 @@ dependencies = [ "solana-program", "spl-associated-token-account 3.0.2", "spl-memo", + "spl-pod 0.2.2", "spl-token 4.0.0", "spl-token-2022 3.0.2", + "spl-token-group-interface 0.2.3", + "spl-token-metadata-interface 0.3.3", ] [[package]] @@ -4779,9 +4782,9 @@ dependencies = [ [[package]] name = "spl-pod" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2881dddfca792737c0706fa0175345ab282b1b0879c7d877bad129645737c079" +checksum = "85a5db7e4efb1107b0b8e52a13f035437cdcb36ef99c58f6d467f089d9b2915a" dependencies = [ "borsh 0.10.3", "bytemuck", @@ -4862,7 +4865,7 @@ dependencies = [ "bytemuck", "solana-program", "spl-discriminator 0.1.0", - "spl-pod 0.1.0", + "spl-pod 0.1.1", "spl-program-error 0.3.0", "spl-type-length-value 0.3.0", ] @@ -4926,7 +4929,7 @@ dependencies = [ "solana-security-txt", "solana-zk-token-sdk", "spl-memo", - "spl-pod 0.1.0", + "spl-pod 0.1.1", "spl-token 4.0.0", "spl-token-group-interface 0.1.0", "spl-token-metadata-interface 0.2.0", @@ -4968,7 +4971,7 @@ dependencies = [ "bytemuck", "solana-program", "spl-discriminator 0.1.0", - "spl-pod 0.1.0", + "spl-pod 0.1.1", "spl-program-error 0.3.0", ] @@ -4994,7 +4997,7 @@ dependencies = [ "borsh 0.10.3", "solana-program", "spl-discriminator 0.1.0", - "spl-pod 0.1.0", + "spl-pod 0.1.1", "spl-program-error 0.3.0", "spl-type-length-value 0.3.0", ] @@ -5023,7 +5026,7 @@ dependencies = [ "bytemuck", "solana-program", "spl-discriminator 0.1.0", - "spl-pod 0.1.0", + "spl-pod 0.1.1", "spl-program-error 0.3.0", "spl-tlv-account-resolution 0.5.1", "spl-type-length-value 0.3.0", @@ -5054,7 +5057,7 @@ dependencies = [ "bytemuck", "solana-program", "spl-discriminator 0.1.0", - "spl-pod 0.1.0", + "spl-pod 0.1.1", "spl-program-error 0.3.0", ] diff --git a/lang/Cargo.toml b/lang/Cargo.toml index c3f7d7b931..ef4cc18fb0 100644 --- a/lang/Cargo.toml +++ b/lang/Cargo.toml @@ -59,6 +59,5 @@ borsh = ">=0.9, <0.11" bytemuck = "1" solana-program = "1.16" thiserror = "1" - # TODO: Remove. This crate has been added to fix a build error with the 1.16.0 release. -getrandom = { version = "0.2", features = ["custom"] } +getrandom = { version = "0.2", features = ["custom"] } \ No newline at end of file diff --git a/lang/src/error.rs b/lang/src/error.rs index 09ea04366c..75ef035223 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -126,6 +126,56 @@ pub enum ErrorCode { /// 2023 - A mint token program constraint was violated #[msg("An associated token account token program constraint was violated")] ConstraintAssociatedTokenTokenProgram, + /// Extension constraints + /// + /// 2024 - A group pointer extension constraint was violated + #[msg("A group pointer extension constraint was violated")] + ConstraintMintGroupPointerExtension, + /// 2025 - A group pointer extension authority constraint was violated + #[msg("A group pointer extension authority constraint was violated")] + ConstraintMintGroupPointerExtensionAuthority, + /// 2026 - A group pointer extension group address constraint was violated + #[msg("A group pointer extension group address constraint was violated")] + ConstraintMintGroupPointerExtensionGroupAddress, + /// 2027 - A group member pointer extension constraint was violated + #[msg("A group member pointer extension constraint was violated")] + ConstraintMintGroupMemberPointerExtension, + /// 2028 - A group member pointer extension authority constraint was violated + #[msg("A group member pointer extension authority constraint was violated")] + ConstraintMintGroupMemberPointerExtensionAuthority, + /// 2029 - A group member pointer extension member address constraint was violated + #[msg("A group member pointer extension group address constraint was violated")] + ConstraintMintGroupMemberPointerExtensionMemberAddress, + /// 2030 - A metadata pointer extension constraint was violated + #[msg("A metadata pointer extension constraint was violated")] + ConstraintMintMetadataPointerExtension, + /// 2031 - A metadata pointer extension authority constraint was violated + #[msg("A metadata pointer extension authority constraint was violated")] + ConstraintMintMetadataPointerExtensionAuthority, + /// 2032 - A metadata pointer extension metadata address constraint was violated + #[msg("A metadata pointer extension metadata address constraint was violated")] + ConstraintMintMetadataPointerExtensionMetadataAddress, + /// 2033 - A close authority extension constraint was violated + #[msg("A close authority constraint was violated")] + ConstraintMintCloseAuthorityExtension, + /// 2034 - A close authority extension authority constraint was violated + #[msg("A close authority extension authority constraint was violated")] + ConstraintMintCloseAuthorityExtensionAuthority, + /// 2035 - A permanent delegate extension constraint was violated + #[msg("A permanent delegate extension constraint was violated")] + ConstraintMintPermanentDelegateExtension, + /// 2036 - A permanent delegate extension authority constraint was violated + #[msg("A permanent delegate extension delegate constraint was violated")] + ConstraintMintPermanentDelegateExtensionDelegate, + /// 2037 - A transfer hook extension constraint was violated + #[msg("A transfer hook extension constraint was violated")] + ConstraintMintTransferHookExtension, + /// 2038 - A transfer hook extension authority constraint was violated + #[msg("A transfer hook extension authority constraint was violated")] + ConstraintMintTransferHookExtensionAuthority, + /// 2039 - A transfer hook extension transfer hook program id constraint was violated + #[msg("A transfer hook extension transfer hook program id constraint was violated")] + ConstraintMintTransferHookExtensionProgramId, // Require /// 2500 - A require expression was violated diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 4c1063b969..1f757a6f4b 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -661,6 +661,16 @@ fn generate_constraint_init_group( decimals, freeze_authority, token_program, + group_pointer_authority, + group_pointer_group_address, + group_member_pointer_authority, + group_member_pointer_member_address, + metadata_pointer_authority, + metadata_pointer_metadata_address, + close_authority, + permanent_delegate, + transfer_hook_authority, + transfer_hook_program_id, } => { let token_program = match token_program { Some(t) => t.to_token_stream(), @@ -672,6 +682,59 @@ fn generate_constraint_init_group( None => quote! {}, }; + // extension checks + + let group_pointer_authority_check = match group_pointer_authority { + Some(gpa) => check_scope.generate_check(gpa), + None => quote! {}, + }; + + let group_pointer_group_address_check = match group_pointer_group_address { + Some(gpga) => check_scope.generate_check(gpga), + None => quote! {}, + }; + + let group_member_pointer_authority_check = match group_member_pointer_authority { + Some(gmpa) => check_scope.generate_check(gmpa), + None => quote! {}, + }; + + let group_member_pointer_member_address_check = + match group_member_pointer_member_address { + Some(gmpm) => check_scope.generate_check(gmpm), + None => quote! {}, + }; + + let metadata_pointer_authority_check = match metadata_pointer_authority { + Some(mpa) => check_scope.generate_check(mpa), + None => quote! {}, + }; + + let metadata_pointer_metadata_address_check = match metadata_pointer_metadata_address { + Some(mpma) => check_scope.generate_check(mpma), + None => quote! {}, + }; + + let close_authority_check = match close_authority { + Some(ca) => check_scope.generate_check(ca), + None => quote! {}, + }; + + let transfer_hook_authority_check = match transfer_hook_authority { + Some(tha) => check_scope.generate_check(tha), + None => quote! {}, + }; + + let transfer_hook_program_id_check = match transfer_hook_program_id { + Some(thpid) => check_scope.generate_check(thpid), + None => quote! {}, + }; + + let permanent_delegate_check = match permanent_delegate { + Some(pd) => check_scope.generate_check(pd), + 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); @@ -682,23 +745,126 @@ fn generate_constraint_init_group( #rent_optional_check #owner_optional_check #freeze_authority_optional_check + #group_pointer_authority_check + #group_pointer_group_address_check + #group_member_pointer_authority_check + #group_member_pointer_member_address_check + #metadata_pointer_authority_check + #metadata_pointer_metadata_address_check + #close_authority_check + #transfer_hook_authority_check + #transfer_hook_program_id_check + #permanent_delegate_check }; let payer_optional_check = check_scope.generate_check(payer); + let mut extensions = vec![]; + if group_pointer_authority.is_some() || group_pointer_group_address.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::GroupPointer}); + } + + if group_member_pointer_authority.is_some() + || group_member_pointer_member_address.is_some() + { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::GroupMemberPointer}); + } + + if metadata_pointer_authority.is_some() || metadata_pointer_metadata_address.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::MetadataPointer}); + } + + if close_authority.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::MintCloseAuthority}); + } + + if transfer_hook_authority.is_some() || transfer_hook_program_id.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::TransferHook}); + } + + if permanent_delegate.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::PermanentDelegate}); + } + + let mint_space = if extensions.is_empty() { + quote! { ::anchor_spl::token::Mint::LEN } + } else { + quote! { ::anchor_spl::token_interface::find_mint_account_size(Some(&vec![#(#extensions),*]))? } + }; + + let extensions = if extensions.is_empty() { + quote! {Option::<&::anchor_spl::token_interface::ExtensionsVec>::None} + } else { + quote! {Option::<&::anchor_spl::token_interface::ExtensionsVec>::Some(&vec![#(#extensions),*])} + }; + + let freeze_authority = match freeze_authority { + Some(fa) => quote! { Option::<&anchor_lang::prelude::Pubkey>::Some(&#fa.key()) }, + None => quote! { Option::<&anchor_lang::prelude::Pubkey>::None }, + }; + + let group_pointer_authority = match group_pointer_authority { + Some(gpa) => quote! { Option::::Some(#gpa.key()) }, + None => quote! { Option::::None }, + }; + + let group_pointer_group_address = match group_pointer_group_address { + Some(gpga) => quote! { Option::::Some(#gpga.key()) }, + None => quote! { Option::::None }, + }; + + let group_member_pointer_authority = match group_member_pointer_authority { + Some(gmpa) => quote! { Option::::Some(#gmpa.key()) }, + None => quote! { Option::::None }, + }; + + let group_member_pointer_member_address = match group_member_pointer_member_address { + Some(gmpma) => { + quote! { Option::::Some(#gmpma.key()) } + } + None => quote! { Option::::None }, + }; + + let metadata_pointer_authority = match metadata_pointer_authority { + Some(mpa) => quote! { Option::::Some(#mpa.key()) }, + None => quote! { Option::::None }, + }; + + let metadata_pointer_metadata_address = match metadata_pointer_metadata_address { + Some(mpma) => quote! { Option::::Some(#mpma.key()) }, + None => quote! { Option::::None }, + }; + + let close_authority = match close_authority { + Some(ca) => quote! { Option::<&anchor_lang::prelude::Pubkey>::Some(&#ca.key()) }, + None => quote! { Option::<&anchor_lang::prelude::Pubkey>::None }, + }; + + let permanent_delegate = match permanent_delegate { + Some(pd) => quote! { Option::<&anchor_lang::prelude::Pubkey>::Some(&#pd.key()) }, + None => quote! { Option::<&anchor_lang::prelude::Pubkey>::None }, + }; + + let transfer_hook_authority = match transfer_hook_authority { + Some(tha) => quote! { Option::::Some(#tha.key()) }, + None => quote! { Option::::None }, + }; + + let transfer_hook_program_id = match transfer_hook_program_id { + Some(thpid) => { + quote! { Option::::Some(#thpid.key()) } + } + None => quote! { Option::::None }, + }; + let create_account = generate_create_account( field, - quote! {::anchor_spl::token::Mint::LEN}, + mint_space, quote! {&#token_program.key()}, quote! {#payer}, seeds_with_bump, ); - let freeze_authority = match freeze_authority { - Some(fa) => quote! { Option::<&anchor_lang::prelude::Pubkey>::Some(&#fa.key()) }, - None => quote! { Option::<&anchor_lang::prelude::Pubkey>::None }, - }; - quote! { // Define the bump and pda variable. #find_pda @@ -715,6 +881,78 @@ fn generate_constraint_init_group( // Create the account with the system program. #create_account + // Initialize extensions. + if let Some(extensions) = #extensions { + for e in extensions { + match e { + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::GroupPointer => { + let cpi_program = #token_program.to_account_info(); + let accounts = ::anchor_spl::token_interface::GroupPointerInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts); + ::anchor_spl::token_interface::group_pointer_initialize(cpi_ctx, #group_pointer_authority, #group_pointer_group_address)?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::GroupMemberPointer => { + let cpi_program = #token_program.to_account_info(); + let accounts = ::anchor_spl::token_interface::GroupMemberPointerInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts); + ::anchor_spl::token_interface::group_member_pointer_initialize(cpi_ctx, #group_member_pointer_authority, #group_member_pointer_member_address)?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::MetadataPointer => { + let cpi_program = #token_program.to_account_info(); + let accounts = ::anchor_spl::token_interface::MetadataPointerInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts); + ::anchor_spl::token_interface::metadata_pointer_initialize(cpi_ctx, #metadata_pointer_authority, #metadata_pointer_metadata_address)?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::MintCloseAuthority => { + let cpi_program = #token_program.to_account_info(); + let accounts = ::anchor_spl::token_interface::MintCloseAuthorityInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts); + ::anchor_spl::token_interface::mint_close_authority_initialize(cpi_ctx, #close_authority)?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::TransferHook => { + let cpi_program = #token_program.to_account_info(); + let accounts = ::anchor_spl::token_interface::TransferHookInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts); + ::anchor_spl::token_interface::transfer_hook_initialize(cpi_ctx, #transfer_hook_authority, #transfer_hook_program_id)?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::NonTransferable => { + let cpi_program = #token_program.to_account_info(); + let accounts = ::anchor_spl::token_interface::NonTransferableMintInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts); + ::anchor_spl::token_interface::non_transferable_mint_initialize(cpi_ctx)?; + }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::PermanentDelegate => { + let cpi_program = #token_program.to_account_info(); + let accounts = ::anchor_spl::token_interface::PermanentDelegateInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts); + ::anchor_spl::token_interface::permanent_delegate_initialize(cpi_ctx, #permanent_delegate.unwrap())?; + }, + _ => {} // do nothing + } + }; + } + // Initialize the mint account. let cpi_program = #token_program.to_account_info(); let accounts = ::anchor_spl::token_interface::InitializeMint2 { @@ -723,6 +961,7 @@ fn generate_constraint_init_group( let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts); ::anchor_spl::token_interface::initialize_mint2(cpi_ctx, #decimals, &#owner.key(), #freeze_authority)?; } + let pa: #ty_decl = #from_account_info_unchecked; if #if_needed { if pa.mint_authority != anchor_lang::solana_program::program_option::COption::Some(#owner.key()) { @@ -1068,12 +1307,203 @@ fn generate_constraint_mint( } None => quote! {}, }; + + let group_pointer_authority_check = match &c.group_pointer_authority { + Some(group_pointer_authority) => { + let group_pointer_authority_optional_check = + optional_check_scope.generate_check(group_pointer_authority); + quote! { + let group_pointer = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::group_pointer::GroupPointer>(#account_ref); + if group_pointer.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintGroupPointerExtension.into()); + } + #group_pointer_authority_optional_check + if group_pointer.unwrap().authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#group_pointer_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintGroupPointerExtensionAuthority.into()); + } + } + } + None => quote! {}, + }; + + let group_pointer_group_address_check = match &c.group_pointer_group_address { + Some(group_pointer_group_address) => { + let group_pointer_group_address_optional_check = + optional_check_scope.generate_check(group_pointer_group_address); + quote! { + let group_pointer = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::group_pointer::GroupPointer>(#account_ref); + if group_pointer.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintGroupPointerExtension.into()); + } + #group_pointer_group_address_optional_check + if group_pointer.unwrap().group_address != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#group_pointer_group_address.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintGroupPointerExtensionGroupAddress.into()); + } + } + } + None => quote! {}, + }; + + let group_member_pointer_authority_check = match &c.group_member_pointer_authority { + Some(group_member_pointer_authority) => { + let group_member_pointer_authority_optional_check = + optional_check_scope.generate_check(group_member_pointer_authority); + quote! { + let group_member_pointer = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::group_member_pointer::GroupMemberPointer>(#account_ref); + if group_member_pointer.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintGroupMemberPointerExtension.into()); + } + #group_member_pointer_authority_optional_check + if group_member_pointer.unwrap().authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#group_member_pointer_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintGroupMemberPointerExtensionAuthority.into()); + } + } + } + None => quote! {}, + }; + + let group_member_pointer_member_address_check = match &c.group_member_pointer_member_address { + Some(group_member_pointer_member_address) => { + let group_member_pointer_member_address_optional_check = + optional_check_scope.generate_check(group_member_pointer_member_address); + quote! { + let group_member_pointer = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::group_member_pointer::GroupMemberPointer>(#account_ref); + if group_member_pointer.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintGroupMemberPointerExtension.into()); + } + #group_member_pointer_member_address_optional_check + if group_member_pointer.unwrap().member_address != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#group_member_pointer_member_address.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintGroupMemberPointerExtensionMemberAddress.into()); + } + } + } + None => quote! {}, + }; + + let metadata_pointer_authority_check = match &c.metadata_pointer_authority { + Some(metadata_pointer_authority) => { + let metadata_pointer_authority_optional_check = + optional_check_scope.generate_check(metadata_pointer_authority); + quote! { + let metadata_pointer = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::metadata_pointer::MetadataPointer>(#account_ref); + if metadata_pointer.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintMetadataPointerExtension.into()); + } + #metadata_pointer_authority_optional_check + if metadata_pointer.unwrap().authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#metadata_pointer_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintMetadataPointerExtensionAuthority.into()); + } + } + } + None => quote! {}, + }; + + let metadata_pointer_metadata_address_check = match &c.metadata_pointer_metadata_address { + Some(metadata_pointer_metadata_address) => { + let metadata_pointer_metadata_address_optional_check = + optional_check_scope.generate_check(metadata_pointer_metadata_address); + quote! { + let metadata_pointer = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::metadata_pointer::MetadataPointer>(#account_ref); + if metadata_pointer.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintMetadataPointerExtension.into()); + } + #metadata_pointer_metadata_address_optional_check + if metadata_pointer.unwrap().metadata_address != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#metadata_pointer_metadata_address.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintMetadataPointerExtensionMetadataAddress.into()); + } + } + } + None => quote! {}, + }; + + let close_authority_check = match &c.close_authority { + Some(close_authority) => { + let close_authority_optional_check = + optional_check_scope.generate_check(close_authority); + quote! { + let close_authority = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::mint_close_authority::MintCloseAuthority>(#account_ref); + if close_authority.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintCloseAuthorityExtension.into()); + } + #close_authority_optional_check + if close_authority.unwrap().close_authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#close_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintCloseAuthorityExtensionAuthority.into()); + } + } + } + None => quote! {}, + }; + + let permanent_delegate_check = match &c.permanent_delegate { + Some(permanent_delegate) => { + let permanent_delegate_optional_check = + optional_check_scope.generate_check(permanent_delegate); + quote! { + let permanent_delegate = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::permanent_delegate::PermanentDelegate>(#account_ref); + if permanent_delegate.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintPermanentDelegateExtension.into()); + } + #permanent_delegate_optional_check + if permanent_delegate.unwrap().delegate != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#permanent_delegate.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintPermanentDelegateExtensionDelegate.into()); + } + } + } + None => quote! {}, + }; + + let transfer_hook_authority_check = match &c.transfer_hook_authority { + Some(transfer_hook_authority) => { + let transfer_hook_authority_optional_check = + optional_check_scope.generate_check(transfer_hook_authority); + quote! { + let transfer_hook = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::transfer_hook::TransferHook>(#account_ref); + if transfer_hook.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferHookExtension.into()); + } + #transfer_hook_authority_optional_check + if transfer_hook.unwrap().authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#transfer_hook_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferHookExtensionAuthority.into()); + } + } + } + None => quote! {}, + }; + + let transfer_hook_program_id_check = match &c.transfer_hook_program_id { + Some(transfer_hook_program_id) => { + let transfer_hook_program_id_optional_check = + optional_check_scope.generate_check(transfer_hook_program_id); + quote! { + let transfer_hook = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::transfer_hook::TransferHook>(#account_ref); + if transfer_hook.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferHookExtension.into()); + } + #transfer_hook_program_id_optional_check + if transfer_hook.unwrap().program_id != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#transfer_hook_program_id.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferHookExtensionProgramId.into()); + } + } + } + None => quote! {}, + }; + quote! { { #decimal_check #mint_authority_check #freeze_authority_check #token_program_check + #group_pointer_authority_check + #group_pointer_group_address_check + #group_member_pointer_authority_check + #group_member_pointer_member_address_check + #metadata_pointer_authority_check + #metadata_pointer_metadata_address_check + #close_authority_check + #permanent_delegate_check + #transfer_hook_authority_check + #transfer_hook_program_id_check } } } diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 618c43494f..ad4a18399d 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -680,6 +680,21 @@ pub enum ConstraintToken { Realloc(Context), ReallocPayer(Context), ReallocZero(Context), + // extensions + ExtensionGroupPointerAuthority(Context), + ExtensionGroupPointerGroupAddress(Context), + ExtensionGroupMemberPointerAuthority(Context), + ExtensionGroupMemberPointerMemberAddress( + Context, + ), + ExtensionMetadataPointerAuthority(Context), + ExtensionMetadataPointerMetadataAddress( + Context, + ), + ExtensionCloseAuthority(Context), + ExtensionTokenHookAuthority(Context), + ExtensionTokenHookProgramId(Context), + ExtensionPermanentDelegate(Context), } impl Parse for ConstraintToken { @@ -796,6 +811,37 @@ pub struct ConstraintSpace { pub space: Expr, } +// extension constraints +#[derive(Debug, Clone)] +pub struct ConstraintExtensionAuthority { + pub authority: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionGroupPointerGroupAddress { + pub group_address: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionGroupMemberPointerMemberAddress { + pub member_address: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionMetadataPointerMetadataAddress { + pub metadata_address: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionTokenHookProgramId { + pub program_id: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionPermanentDelegate { + pub permanent_delegate: Expr, +} + #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum InitKind { @@ -822,6 +868,17 @@ pub enum InitKind { freeze_authority: Option, decimals: Expr, token_program: Option, + // extensions + group_pointer_authority: Option, + group_pointer_group_address: Option, + group_member_pointer_authority: Option, + group_member_pointer_member_address: Option, + metadata_pointer_authority: Option, + metadata_pointer_metadata_address: Option, + close_authority: Option, + permanent_delegate: Option, + transfer_hook_authority: Option, + transfer_hook_program_id: Option, }, } @@ -835,6 +892,46 @@ pub struct ConstraintTokenMint { pub mint: Expr, } +#[derive(Debug, Clone)] +pub struct ConstraintMintConfidentialTransferData { + pub confidential_transfer_data: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintMintMetadata { + pub token_metadata: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintMintTokenGroupData { + pub token_group_data: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintMintTokenGroupMemberData { + pub token_group_member_data: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintMintMetadataPointerData { + pub metadata_pointer_data: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintMintGroupPointerData { + pub group_pointer_data: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintMintGroupMemberPointerData { + pub group_member_pointer_data: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintMintCloseAuthority { + pub close_authority: Expr, +} + #[derive(Debug, Clone)] pub struct ConstraintTokenAuthority { pub auth: Expr, @@ -890,6 +987,16 @@ pub struct ConstraintTokenMintGroup { pub mint_authority: Option, pub freeze_authority: Option, pub token_program: Option, + pub group_pointer_authority: Option, + pub group_pointer_group_address: Option, + pub group_member_pointer_authority: Option, + pub group_member_pointer_member_address: Option, + pub metadata_pointer_authority: Option, + pub metadata_pointer_metadata_address: Option, + pub close_authority: Option, + pub permanent_delegate: Option, + pub transfer_hook_authority: Option, + pub transfer_hook_program_id: Option, } // Syntaxt 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 da023f6c12..692eecbbbf 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -89,6 +89,177 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } + "extensions" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + + match kw.as_str() { + "group_pointer" => { + 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::ExtensionGroupPointerAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + "group_address" => { + ConstraintToken::ExtensionGroupPointerGroupAddress(Context::new( + span, + ConstraintExtensionGroupPointerGroupAddress { + group_address: stream.parse()?, + }, + )) + } + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + "group_member_pointer" => { + 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::ExtensionGroupMemberPointerAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + "member_address" => { + ConstraintToken::ExtensionGroupMemberPointerMemberAddress(Context::new( + span, + ConstraintExtensionGroupMemberPointerMemberAddress { + member_address: stream.parse()?, + }, + )) + } + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + "metadata_pointer" => { + 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::ExtensionMetadataPointerAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + "metadata_address" => { + ConstraintToken::ExtensionMetadataPointerMetadataAddress(Context::new( + span, + ConstraintExtensionMetadataPointerMetadataAddress { + metadata_address: stream.parse()?, + }, + )) + } + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + "close_authority" => { + 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::ExtensionCloseAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + "permanent_delegate" => { + 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() { + "delegate" => ConstraintToken::ExtensionPermanentDelegate(Context::new( + span, + ConstraintExtensionPermanentDelegate { + permanent_delegate: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + "transfer_hook" => { + 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::ExtensionTokenHookAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )), + "program_id" => ConstraintToken::ExtensionTokenHookProgramId(Context::new( + span, + ConstraintExtensionTokenHookProgramId { + program_id: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } "token" => { stream.parse::()?; stream.parse::()?; @@ -354,6 +525,19 @@ pub struct ConstraintGroupBuilder<'ty> { pub mint_freeze_authority: Option>, pub mint_decimals: Option>, pub mint_token_program: Option>, + pub extension_group_pointer_authority: Option>, + pub extension_group_pointer_group_address: + Option>, + pub extension_group_member_pointer_authority: Option>, + pub extension_group_member_pointer_member_address: + Option>, + pub extension_metadata_pointer_authority: Option>, + pub extension_metadata_pointer_metadata_address: + Option>, + pub extension_close_authority: Option>, + pub extension_transfer_hook_authority: Option>, + pub extension_transfer_hook_program_id: Option>, + pub extension_permanent_delegate: Option>, pub bump: Option>, pub program_seed: Option>, pub realloc: Option>, @@ -389,6 +573,16 @@ impl<'ty> ConstraintGroupBuilder<'ty> { mint_freeze_authority: None, mint_decimals: None, mint_token_program: None, + extension_group_pointer_authority: None, + extension_group_pointer_group_address: None, + extension_group_member_pointer_authority: None, + extension_group_member_pointer_member_address: None, + extension_metadata_pointer_authority: None, + extension_metadata_pointer_metadata_address: None, + extension_close_authority: None, + extension_transfer_hook_authority: None, + extension_transfer_hook_program_id: None, + extension_permanent_delegate: None, bump: None, program_seed: None, realloc: None, @@ -591,6 +785,16 @@ impl<'ty> ConstraintGroupBuilder<'ty> { mint_freeze_authority, mint_decimals, mint_token_program, + extension_group_pointer_authority, + extension_group_pointer_group_address, + extension_group_member_pointer_authority, + extension_group_member_pointer_member_address, + extension_metadata_pointer_authority, + extension_metadata_pointer_metadata_address, + extension_close_authority, + extension_transfer_hook_authority, + extension_transfer_hook_program_id, + extension_permanent_delegate, bump, program_seed, realloc, @@ -680,8 +884,33 @@ impl<'ty> ConstraintGroupBuilder<'ty> { &mint_authority, &mint_freeze_authority, &mint_token_program, + &extension_group_pointer_authority, + &extension_group_pointer_group_address, + &extension_group_member_pointer_authority, + &extension_group_member_pointer_member_address, + &extension_metadata_pointer_authority, + &extension_metadata_pointer_metadata_address, + &extension_close_authority, + &extension_transfer_hook_authority, + &extension_transfer_hook_program_id, + &extension_permanent_delegate, ) { - (None, None, None, None) => None, + ( + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ) => None, _ => Some(ConstraintTokenMintGroup { decimals: mint_decimals .as_ref() @@ -695,6 +924,37 @@ impl<'ty> ConstraintGroupBuilder<'ty> { token_program: mint_token_program .as_ref() .map(|a| a.clone().into_inner().token_program), + // extensions + group_pointer_authority: extension_group_pointer_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + group_pointer_group_address: extension_group_pointer_group_address + .as_ref() + .map(|a| a.clone().into_inner().group_address), + group_member_pointer_authority: extension_group_member_pointer_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + group_member_pointer_member_address: extension_group_member_pointer_member_address + .as_ref() + .map(|a| a.clone().into_inner().member_address), + metadata_pointer_authority: extension_metadata_pointer_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + metadata_pointer_metadata_address: extension_metadata_pointer_metadata_address + .as_ref() + .map(|a| a.clone().into_inner().metadata_address), + close_authority: extension_close_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + permanent_delegate: extension_permanent_delegate + .as_ref() + .map(|a| a.clone().into_inner().permanent_delegate), + transfer_hook_authority: extension_transfer_hook_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + transfer_hook_program_id: extension_transfer_hook_program_id + .as_ref() + .map(|a| a.clone().into_inner().program_id), }), }; @@ -734,6 +994,17 @@ impl<'ty> ConstraintGroupBuilder<'ty> { }, freeze_authority: mint_freeze_authority.map(|fa| fa.into_inner().mint_freeze_auth), token_program: mint_token_program.map(|tp| tp.into_inner().token_program), + // extensions + group_pointer_authority: extension_group_pointer_authority.map(|gpa| gpa.into_inner().authority), + group_pointer_group_address: extension_group_pointer_group_address.map(|gpga| gpga.into_inner().group_address), + group_member_pointer_authority: extension_group_member_pointer_authority.map(|gmpa| gmpa.into_inner().authority), + group_member_pointer_member_address: extension_group_member_pointer_member_address.map(|gmpma| gmpma.into_inner().member_address), + metadata_pointer_authority: extension_metadata_pointer_authority.map(|mpa| mpa.into_inner().authority), + metadata_pointer_metadata_address: extension_metadata_pointer_metadata_address.map(|mpma| mpma.into_inner().metadata_address), + close_authority: extension_close_authority.map(|ca| ca.into_inner().authority), + permanent_delegate: extension_permanent_delegate.map(|pd| pd.into_inner().permanent_delegate), + transfer_hook_authority: extension_transfer_hook_authority.map(|tha| tha.into_inner().authority), + transfer_hook_program_id: extension_transfer_hook_program_id.map(|thpid| thpid.into_inner().program_id), } } else { InitKind::Program { @@ -796,6 +1067,32 @@ impl<'ty> ConstraintGroupBuilder<'ty> { ConstraintToken::Realloc(c) => self.add_realloc(c), ConstraintToken::ReallocPayer(c) => self.add_realloc_payer(c), ConstraintToken::ReallocZero(c) => self.add_realloc_zero(c), + ConstraintToken::ExtensionGroupPointerAuthority(c) => { + self.add_extension_group_pointer_authority(c) + } + ConstraintToken::ExtensionGroupPointerGroupAddress(c) => { + self.add_extension_group_pointer_group_address(c) + } + ConstraintToken::ExtensionGroupMemberPointerAuthority(c) => { + self.add_extension_group_member_pointer_authority(c) + } + ConstraintToken::ExtensionGroupMemberPointerMemberAddress(c) => { + self.add_extension_group_member_pointer_member_address(c) + } + ConstraintToken::ExtensionMetadataPointerAuthority(c) => { + self.add_extension_metadata_pointer_authority(c) + } + ConstraintToken::ExtensionMetadataPointerMetadataAddress(c) => { + self.add_extension_metadata_pointer_metadata_address(c) + } + ConstraintToken::ExtensionCloseAuthority(c) => self.add_extension_close_authority(c), + ConstraintToken::ExtensionTokenHookAuthority(c) => self.add_extension_authority(c), + ConstraintToken::ExtensionTokenHookProgramId(c) => { + self.add_extension_transfer_hook_program_id(c) + } + ConstraintToken::ExtensionPermanentDelegate(c) => { + self.add_extension_permanent_delegate(c) + } } } @@ -1221,4 +1518,147 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.space.replace(c); Ok(()) } + + // extensions + + fn add_extension_group_pointer_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_group_pointer_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension group pointer authority already provided", + )); + } + self.extension_group_pointer_authority.replace(c); + Ok(()) + } + + fn add_extension_group_pointer_group_address( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_group_pointer_group_address.is_some() { + return Err(ParseError::new( + c.span(), + "extension group pointer group address already provided", + )); + } + self.extension_group_pointer_group_address.replace(c); + Ok(()) + } + + fn add_extension_group_member_pointer_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_group_member_pointer_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension group member pointer authority already provided", + )); + } + self.extension_group_member_pointer_authority.replace(c); + Ok(()) + } + + fn add_extension_group_member_pointer_member_address( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_group_member_pointer_member_address.is_some() { + return Err(ParseError::new( + c.span(), + "extension group member pointer member address already provided", + )); + } + self.extension_group_member_pointer_member_address + .replace(c); + Ok(()) + } + + fn add_extension_metadata_pointer_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_metadata_pointer_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension metadata pointer authority already provided", + )); + } + self.extension_metadata_pointer_authority.replace(c); + Ok(()) + } + + fn add_extension_metadata_pointer_metadata_address( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_metadata_pointer_metadata_address.is_some() { + return Err(ParseError::new( + c.span(), + "extension metadata pointer metadata address already provided", + )); + } + self.extension_metadata_pointer_metadata_address.replace(c); + Ok(()) + } + + fn add_extension_close_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_close_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension close authority already provided", + )); + } + self.extension_close_authority.replace(c); + Ok(()) + } + + fn add_extension_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_hook_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer hook authority already provided", + )); + } + self.extension_transfer_hook_authority.replace(c); + Ok(()) + } + + fn add_extension_transfer_hook_program_id( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_hook_program_id.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer hook program id already provided", + )); + } + self.extension_transfer_hook_program_id.replace(c); + Ok(()) + } + + fn add_extension_permanent_delegate( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_permanent_delegate.is_some() { + return Err(ParseError::new( + c.span(), + "extension permanent delegate already provided", + )); + } + self.extension_permanent_delegate.replace(c); + Ok(()) + } } diff --git a/spl/Cargo.toml b/spl/Cargo.toml index de0206e238..41b69244d0 100644 --- a/spl/Cargo.toml +++ b/spl/Cargo.toml @@ -12,7 +12,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["associated_token", "mint", "token", "token_2022"] +default = ["associated_token", "mint", "token", "token_2022", "token_2022_extensions"] associated_token = ["spl-associated-token-account"] dex = ["serum_dex"] devnet = [] @@ -24,6 +24,7 @@ mint = [] stake = ["borsh"] token = ["spl-token"] token_2022 = ["spl-token-2022"] +token_2022_extensions = ["spl-token-2022", "spl-token-group-interface", "spl-token-metadata-interface", "spl-pod"] [dependencies] anchor-lang = { path = "../lang", version = "0.29.0", features = ["derive"] } @@ -35,3 +36,6 @@ spl-associated-token-account = { version = "3", features = ["no-entrypoint"], op spl-memo = { version = "4", features = ["no-entrypoint"], optional = true } spl-token = { version = "4", features = ["no-entrypoint"], optional = true } spl-token-2022 = { version = "3", features = ["no-entrypoint"], optional = true } +spl-token-group-interface = { version = "0.2.3", optional = true } +spl-token-metadata-interface = { version = "0.3.3", optional = true } +spl-pod = { version = "0.2.2", optional = true } \ No newline at end of file diff --git a/spl/src/lib.rs b/spl/src/lib.rs index a4e2afec23..12cca5d128 100644 --- a/spl/src/lib.rs +++ b/spl/src/lib.rs @@ -14,6 +14,9 @@ pub mod token; #[cfg(feature = "token_2022")] pub mod token_2022; +#[cfg(feature = "token_2022_extensions")] +pub mod token_2022_extensions; + #[cfg(feature = "token_2022")] pub mod token_interface; diff --git a/spl/src/token_2022.rs b/spl/src/token_2022.rs index 5acb90e0d2..8a1fc1121a 100644 --- a/spl/src/token_2022.rs +++ b/spl/src/token_2022.rs @@ -522,7 +522,3 @@ impl anchor_lang::Id for Token2022 { ID } } - -// Field parsers to save compute. All account validation is assumed to be done -// outside of these methods. -pub use crate::token::accessor; diff --git a/spl/src/token_2022_extensions/confidential_transfer.rs b/spl/src/token_2022_extensions/confidential_transfer.rs new file mode 100644 index 0000000000..665ccd2cfc --- /dev/null +++ b/spl/src/token_2022_extensions/confidential_transfer.rs @@ -0,0 +1 @@ +// waiting for labs to merge diff --git a/spl/src/token_2022_extensions/confidential_transfer_fee.rs b/spl/src/token_2022_extensions/confidential_transfer_fee.rs new file mode 100644 index 0000000000..665ccd2cfc --- /dev/null +++ b/spl/src/token_2022_extensions/confidential_transfer_fee.rs @@ -0,0 +1 @@ +// waiting for labs to merge diff --git a/spl/src/token_2022_extensions/cpi_guard.rs b/spl/src/token_2022_extensions/cpi_guard.rs new file mode 100644 index 0000000000..858c91a661 --- /dev/null +++ b/spl/src/token_2022_extensions/cpi_guard.rs @@ -0,0 +1,50 @@ +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; +use solana_program::account_info::AccountInfo; + +pub fn cpi_guard_enable<'info>(ctx: CpiContext<'_, '_, '_, 'info, CpiGuard<'info>>) -> Result<()> { + let ix = spl_token_2022::extension::cpi_guard::instruction::enable_cpi_guard( + ctx.accounts.token_program_id.key, + ctx.accounts.account.key, + ctx.accounts.account.owner, + &[], + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.account, + ctx.accounts.owner, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +pub fn cpi_guard_disable<'info>(ctx: CpiContext<'_, '_, '_, 'info, CpiGuard<'info>>) -> Result<()> { + let ix = spl_token_2022::extension::cpi_guard::instruction::disable_cpi_guard( + ctx.accounts.token_program_id.key, + ctx.accounts.account.key, + ctx.accounts.account.owner, + &[], + )?; + + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.account, + ctx.accounts.owner, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct CpiGuard<'info> { + pub token_program_id: AccountInfo<'info>, + pub account: AccountInfo<'info>, + pub owner: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/default_account_state.rs b/spl/src/token_2022_extensions/default_account_state.rs new file mode 100644 index 0000000000..44fa78d70b --- /dev/null +++ b/spl/src/token_2022_extensions/default_account_state.rs @@ -0,0 +1,59 @@ +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; +use solana_program::account_info::AccountInfo; +use spl_token_2022::state::AccountState; + +pub fn default_account_state_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, DefaultAccountStateInitialize<'info>>, + state: &AccountState, +) -> Result<()> { + let ix = spl_token_2022::extension::default_account_state::instruction::initialize_default_account_state( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + state + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct DefaultAccountStateInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} + +pub fn default_account_state_update<'info>( + ctx: CpiContext<'_, '_, '_, 'info, DefaultAccountStateUpdate<'info>>, + state: &AccountState, +) -> Result<()> { + let ix = spl_token_2022::extension::default_account_state::instruction::update_default_account_state( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + ctx.accounts.freeze_authority.key, + &[], + state + )?; + + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.mint, + ctx.accounts.freeze_authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct DefaultAccountStateUpdate<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub freeze_authority: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/group_member_pointer.rs b/spl/src/token_2022_extensions/group_member_pointer.rs new file mode 100644 index 0000000000..e9a166a32f --- /dev/null +++ b/spl/src/token_2022_extensions/group_member_pointer.rs @@ -0,0 +1,59 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn group_member_pointer_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, GroupMemberPointerInitialize<'info>>, + authority: Option, + member_address: Option, +) -> Result<()> { + let ix = spl_token_2022::extension::group_member_pointer::instruction::initialize( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + authority, + member_address, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct GroupMemberPointerInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} + +pub fn group_member_pointer_update<'info>( + ctx: CpiContext<'_, '_, '_, 'info, GroupMemberPointerUpdate<'info>>, + member_address: Option, +) -> Result<()> { + let ix = spl_token_2022::extension::group_member_pointer::instruction::update( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + ctx.accounts.authority.key, + &[], + member_address, + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.mint, + ctx.accounts.authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct GroupMemberPointerUpdate<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub authority: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/group_pointer.rs b/spl/src/token_2022_extensions/group_pointer.rs new file mode 100644 index 0000000000..6e573fbbf9 --- /dev/null +++ b/spl/src/token_2022_extensions/group_pointer.rs @@ -0,0 +1,55 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn group_pointer_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, GroupPointerInitialize<'info>>, + authority: Option, + group_address: Option, +) -> Result<()> { + let ix = spl_token_2022::extension::group_pointer::instruction::initialize( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + authority, + group_address, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct GroupPointerInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} + +pub fn group_pointer_update<'info>( + ctx: CpiContext<'_, '_, '_, 'info, GroupPointerUpdate<'info>>, + group_address: Option, +) -> Result<()> { + let ix = spl_token_2022::extension::group_pointer::instruction::update( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + ctx.accounts.authority.key, + &[&ctx.accounts.authority.key], + group_address, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct GroupPointerUpdate<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub authority: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/immutable_owner.rs b/spl/src/token_2022_extensions/immutable_owner.rs new file mode 100644 index 0000000000..5d37488138 --- /dev/null +++ b/spl/src/token_2022_extensions/immutable_owner.rs @@ -0,0 +1,25 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn immutable_owner_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, ImmutableOwnerInitialize<'info>>, +) -> Result<()> { + let ix = spl_token_2022::instruction::initialize_immutable_owner( + ctx.accounts.token_program_id.key, + ctx.accounts.token_account.key, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.token_account], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct ImmutableOwnerInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub token_account: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/interest_bearing_mint.rs b/spl/src/token_2022_extensions/interest_bearing_mint.rs new file mode 100644 index 0000000000..155ffe5911 --- /dev/null +++ b/spl/src/token_2022_extensions/interest_bearing_mint.rs @@ -0,0 +1,59 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn interest_bearing_mint_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, InterestBearingMintInitialize<'info>>, + rate_authority: Option, + rate: i16, +) -> Result<()> { + let ix = spl_token_2022::extension::interest_bearing_mint::instruction::initialize( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + rate_authority, + rate, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct InterestBearingMintInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} + +pub fn interest_bearing_mint_update_rate<'info>( + ctx: CpiContext<'_, '_, '_, 'info, InterestBearingMintUpdateRate<'info>>, + rate: i16, +) -> Result<()> { + let ix = spl_token_2022::extension::interest_bearing_mint::instruction::update_rate( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + ctx.accounts.rate_authority.key, + &[], + rate, + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.mint, + ctx.accounts.rate_authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct InterestBearingMintUpdateRate<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub rate_authority: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/memo_transfer.rs b/spl/src/token_2022_extensions/memo_transfer.rs new file mode 100644 index 0000000000..c1a9e59fe2 --- /dev/null +++ b/spl/src/token_2022_extensions/memo_transfer.rs @@ -0,0 +1,54 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn memo_transfer_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, MemoTransfer<'info>>, +) -> Result<()> { + let ix = spl_token_2022::extension::memo_transfer::instruction::enable_required_transfer_memos( + ctx.accounts.token_program_id.key, + ctx.accounts.account.key, + ctx.accounts.owner.key, + &[], + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.account, + ctx.accounts.owner, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +pub fn memo_transfer_disable<'info>( + ctx: CpiContext<'_, '_, '_, 'info, MemoTransfer<'info>>, +) -> Result<()> { + let ix = + spl_token_2022::extension::memo_transfer::instruction::disable_required_transfer_memos( + ctx.accounts.token_program_id.key, + ctx.accounts.account.key, + ctx.accounts.owner.key, + &[], + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.account, + ctx.accounts.owner, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct MemoTransfer<'info> { + pub token_program_id: AccountInfo<'info>, + pub account: AccountInfo<'info>, + pub owner: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/metadata_pointer.rs b/spl/src/token_2022_extensions/metadata_pointer.rs new file mode 100644 index 0000000000..f459644471 --- /dev/null +++ b/spl/src/token_2022_extensions/metadata_pointer.rs @@ -0,0 +1,29 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn metadata_pointer_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, MetadataPointerInitialize<'info>>, + authority: Option, + metadata_address: Option, +) -> Result<()> { + let ix = spl_token_2022::extension::metadata_pointer::instruction::initialize( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + authority, + metadata_address, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct MetadataPointerInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/mint_close_authority.rs b/spl/src/token_2022_extensions/mint_close_authority.rs new file mode 100644 index 0000000000..3734747dc4 --- /dev/null +++ b/spl/src/token_2022_extensions/mint_close_authority.rs @@ -0,0 +1,27 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn mint_close_authority_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, MintCloseAuthorityInitialize<'info>>, + authority: Option<&Pubkey>, +) -> Result<()> { + let ix = spl_token_2022::instruction::initialize_mint_close_authority( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + authority, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct MintCloseAuthorityInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/mod.rs b/spl/src/token_2022_extensions/mod.rs new file mode 100644 index 0000000000..e0941f9fc6 --- /dev/null +++ b/spl/src/token_2022_extensions/mod.rs @@ -0,0 +1,36 @@ +pub mod confidential_transfer; +pub mod confidential_transfer_fee; +pub mod cpi_guard; +pub mod default_account_state; +pub mod group_member_pointer; +pub mod group_pointer; +pub mod immutable_owner; +pub mod interest_bearing_mint; +pub mod memo_transfer; +pub mod metadata_pointer; +pub mod mint_close_authority; +pub mod non_transferable; +pub mod permanent_delegate; +pub mod token_group; +pub mod token_metadata; +pub mod transfer_fee; +pub mod transfer_hook; + +pub use cpi_guard::*; +pub use default_account_state::*; +pub use group_member_pointer::*; +pub use group_pointer::*; +pub use immutable_owner::*; +pub use interest_bearing_mint::*; +pub use memo_transfer::*; +pub use metadata_pointer::*; +pub use mint_close_authority::*; +pub use non_transferable::*; +pub use permanent_delegate::*; +pub use token_group::*; +pub use token_metadata::*; +pub use transfer_fee::*; +pub use transfer_hook::*; + +pub use spl_pod; +pub use spl_token_metadata_interface; diff --git a/spl/src/token_2022_extensions/non_transferable.rs b/spl/src/token_2022_extensions/non_transferable.rs new file mode 100644 index 0000000000..cfa67cc596 --- /dev/null +++ b/spl/src/token_2022_extensions/non_transferable.rs @@ -0,0 +1,25 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn non_transferable_mint_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, NonTransferableMintInitialize<'info>>, +) -> Result<()> { + let ix = spl_token_2022::instruction::initialize_non_transferable_mint( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct NonTransferableMintInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/permanent_delegate.rs b/spl/src/token_2022_extensions/permanent_delegate.rs new file mode 100644 index 0000000000..c75a0eae32 --- /dev/null +++ b/spl/src/token_2022_extensions/permanent_delegate.rs @@ -0,0 +1,27 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn permanent_delegate_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, PermanentDelegateInitialize<'info>>, + permanent_delegate: &Pubkey, +) -> Result<()> { + let ix = spl_token_2022::instruction::initialize_permanent_delegate( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + permanent_delegate, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct PermanentDelegateInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/token_group.rs b/spl/src/token_2022_extensions/token_group.rs new file mode 100644 index 0000000000..0ef86d2f4e --- /dev/null +++ b/spl/src/token_2022_extensions/token_group.rs @@ -0,0 +1,74 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn token_group_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TokenGroupInitialize<'info>>, + update_authority: Option, + max_size: u32, +) -> Result<()> { + let ix = spl_token_group_interface::instruction::initialize_group( + ctx.accounts.token_program_id.key, + ctx.accounts.group.key, + ctx.accounts.mint.key, + ctx.accounts.mint_authority.key, + update_authority, + max_size, + ); + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.group, + ctx.accounts.mint, + ctx.accounts.mint_authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TokenGroupInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub group: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub mint_authority: AccountInfo<'info>, +} + +pub fn token_member_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TokenMemberInitialize<'info>>, +) -> Result<()> { + let ix = spl_token_group_interface::instruction::initialize_member( + ctx.accounts.token_program_id.key, + ctx.accounts.member.key, + ctx.accounts.member_mint.key, + ctx.accounts.member_mint_authority.key, + ctx.accounts.group.key, + ctx.accounts.group_update_authority.key, + ); + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.member, + ctx.accounts.member_mint, + ctx.accounts.member_mint_authority, + ctx.accounts.group, + ctx.accounts.group_update_authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TokenMemberInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub member: AccountInfo<'info>, + pub member_mint: AccountInfo<'info>, + pub member_mint_authority: AccountInfo<'info>, + pub group: AccountInfo<'info>, + pub group_update_authority: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/token_metadata.rs b/spl/src/token_2022_extensions/token_metadata.rs new file mode 100644 index 0000000000..fa4ce47346 --- /dev/null +++ b/spl/src/token_2022_extensions/token_metadata.rs @@ -0,0 +1,107 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +use spl_pod::optional_keys::OptionalNonZeroPubkey; +use spl_token_metadata_interface::state::Field; + +pub fn token_metadata_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TokenMetadataInitialize<'info>>, + name: String, + symbol: String, + uri: String, +) -> Result<()> { + let ix = spl_token_metadata_interface::instruction::initialize( + ctx.accounts.token_program_id.key, + ctx.accounts.metadata.key, + ctx.accounts.update_authority.key, + ctx.accounts.mint.key, + ctx.accounts.mint_authority.key, + name, + symbol, + uri, + ); + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.metadata, + ctx.accounts.update_authority, + ctx.accounts.mint, + ctx.accounts.mint_authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TokenMetadataInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub metadata: AccountInfo<'info>, + pub update_authority: AccountInfo<'info>, + pub mint_authority: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} + +pub fn token_metadata_update_authority<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TokenMetadataUpdateAuthority<'info>>, + new_authority: OptionalNonZeroPubkey, +) -> Result<()> { + let ix = spl_token_metadata_interface::instruction::update_authority( + ctx.accounts.token_program_id.key, + ctx.accounts.metadata.key, + ctx.accounts.current_authority.key, + new_authority, + ); + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.metadata, + ctx.accounts.current_authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TokenMetadataUpdateAuthority<'info> { + pub token_program_id: AccountInfo<'info>, + pub metadata: AccountInfo<'info>, + pub current_authority: AccountInfo<'info>, + pub new_authority: AccountInfo<'info>, +} + +pub fn token_metadata_update_field<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TokenMetadataUpdateField<'info>>, + field: Field, + value: String, +) -> Result<()> { + let ix = spl_token_metadata_interface::instruction::update_field( + ctx.accounts.token_program_id.key, + ctx.accounts.metadata.key, + ctx.accounts.update_authority.key, + field, + value, + ); + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.metadata, + ctx.accounts.update_authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TokenMetadataUpdateField<'info> { + pub token_program_id: AccountInfo<'info>, + pub metadata: AccountInfo<'info>, + pub update_authority: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/transfer_fee.rs b/spl/src/token_2022_extensions/transfer_fee.rs new file mode 100644 index 0000000000..ad64d4336e --- /dev/null +++ b/spl/src/token_2022_extensions/transfer_fee.rs @@ -0,0 +1,160 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn transfer_fee_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TransferFeeInitialize<'info>>, + transfer_fee_config_authority: Option<&Pubkey>, + withdraw_withheld_authority: Option<&Pubkey>, + transfer_fee_basis_points: u16, + maximum_fee: u64, +) -> Result<()> { + let ix = spl_token_2022::extension::transfer_fee::instruction::initialize_transfer_fee_config( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + transfer_fee_config_authority, + withdraw_withheld_authority, + transfer_fee_basis_points, + maximum_fee, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TransferFeeInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} + +pub fn transfer_fee_set<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TransferFeeSetTransferFee<'info>>, + transfer_fee_basis_points: u16, + maximum_fee: u64, +) -> Result<()> { + let ix = spl_token_2022::extension::transfer_fee::instruction::set_transfer_fee( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + ctx.accounts.authority.key, + &[], + transfer_fee_basis_points, + maximum_fee, + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.mint, + ctx.accounts.authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TransferFeeSetTransferFee<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub authority: AccountInfo<'info>, +} + +pub fn transfer_checked_with_fee<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TransferCheckedWithFee<'info>>, + amount: u64, + decimals: u8, + fee: u64, +) -> Result<()> { + let ix = spl_token_2022::extension::transfer_fee::instruction::transfer_checked_with_fee( + ctx.accounts.token_program_id.key, + ctx.accounts.source.key, + ctx.accounts.mint.key, + ctx.accounts.destination.key, + ctx.accounts.authority.key, + &[], + amount, + decimals, + fee, + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.source, + ctx.accounts.mint, + ctx.accounts.destination, + ctx.accounts.authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TransferCheckedWithFee<'info> { + pub token_program_id: AccountInfo<'info>, + pub source: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub destination: AccountInfo<'info>, + pub authority: AccountInfo<'info>, +} + +pub fn harvest_withheld_tokens_to_mint<'info>( + ctx: CpiContext<'_, '_, '_, 'info, HarvestWithheldTokensToMint<'info>>, + sources: Vec>, +) -> Result<()> { + let ix = spl_token_2022::extension::transfer_fee::instruction::harvest_withheld_tokens_to_mint( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + sources.iter().map(|a| a.key).collect::>().as_slice(), + )?; + + let mut account_infos = vec![ctx.accounts.token_program_id, ctx.accounts.mint]; + account_infos.extend_from_slice(&sources); + + solana_program::program::invoke_signed(&ix, &account_infos, ctx.signer_seeds) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct HarvestWithheldTokensToMint<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} + +pub fn withdraw_withheld_tokens_from_mint<'info>( + ctx: CpiContext<'_, '_, '_, 'info, WithdrawWithheldTokensFromMint<'info>>, +) -> Result<()> { + let ix = + spl_token_2022::extension::transfer_fee::instruction::withdraw_withheld_tokens_from_mint( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + ctx.accounts.destination.key, + ctx.accounts.authority.key, + &[], + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.mint, + ctx.accounts.destination, + ctx.accounts.authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct WithdrawWithheldTokensFromMint<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub destination: AccountInfo<'info>, + pub authority: AccountInfo<'info>, +} diff --git a/spl/src/token_2022_extensions/transfer_hook.rs b/spl/src/token_2022_extensions/transfer_hook.rs new file mode 100644 index 0000000000..11a3fd211b --- /dev/null +++ b/spl/src/token_2022_extensions/transfer_hook.rs @@ -0,0 +1,59 @@ +use anchor_lang::solana_program::account_info::AccountInfo; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_lang::Result; +use anchor_lang::{context::CpiContext, Accounts}; + +pub fn transfer_hook_initialize<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TransferHookInitialize<'info>>, + authority: Option, + transfer_hook_program_id: Option, +) -> Result<()> { + let ix = spl_token_2022::extension::transfer_hook::instruction::initialize( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + authority, + transfer_hook_program_id, + )?; + solana_program::program::invoke_signed( + &ix, + &[ctx.accounts.token_program_id, ctx.accounts.mint], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TransferHookInitialize<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, +} + +pub fn transfer_hook_update<'info>( + ctx: CpiContext<'_, '_, '_, 'info, TransferHookUpdate<'info>>, + transfer_hook_program_id: Option, +) -> Result<()> { + let ix = spl_token_2022::extension::transfer_hook::instruction::update( + ctx.accounts.token_program_id.key, + ctx.accounts.mint.key, + ctx.accounts.authority.key, + &[], + transfer_hook_program_id, + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program_id, + ctx.accounts.mint, + ctx.accounts.authority, + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct TransferHookUpdate<'info> { + pub token_program_id: AccountInfo<'info>, + pub mint: AccountInfo<'info>, + pub authority: AccountInfo<'info>, +} diff --git a/spl/src/token_interface.rs b/spl/src/token_interface.rs index 977f0c68e0..d4fff9996a 100644 --- a/spl/src/token_interface.rs +++ b/spl/src/token_interface.rs @@ -1,7 +1,15 @@ use anchor_lang::solana_program::pubkey::Pubkey; +use solana_program::program_pack::Pack; +use spl_token_2022::extension::ExtensionType; +use spl_token_2022::{ + extension::{BaseStateWithExtensions, Extension, StateWithExtensions}, + solana_zk_token_sdk::instruction::Pod, +}; use std::ops::Deref; pub use crate::token_2022::*; +#[cfg(feature = "token_2022_extensions")] +pub use crate::token_2022_extensions::*; static IDS: [Pubkey; 2] = [spl_token::ID, spl_token_2022::ID]; @@ -69,3 +77,25 @@ impl anchor_lang::Ids for TokenInterface { &IDS } } + +pub type ExtensionsVec = Vec; + +pub fn find_mint_account_size(extensions: Option<&ExtensionsVec>) -> anchor_lang::Result { + if let Some(extensions) = extensions { + Ok(ExtensionType::try_calculate_account_len::< + spl_token_2022::state::Mint, + >(extensions)?) + } else { + Ok(spl_token_2022::state::Mint::LEN) + } +} + +pub fn get_mint_extension_data( + account: &solana_program::account_info::AccountInfo, +) -> anchor_lang::Result { + let mint_data = account.data.borrow(); + let mint_with_extension = + StateWithExtensions::::unpack(&mint_data)?; + let extension_data = *mint_with_extension.get_extension::()?; + Ok(extension_data) +} diff --git a/tests/package.json b/tests/package.json index 49d90f9e4e..471a60385b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -34,6 +34,7 @@ "pyth", "realloc", "spl/metadata", + "spl/token-extensions", "spl/token-proxy", "spl/token-wrapper", "spl/transfer-hook", diff --git a/tests/spl/token-extensions/Anchor.toml b/tests/spl/token-extensions/Anchor.toml new file mode 100644 index 0000000000..92a46b8d97 --- /dev/null +++ b/tests/spl/token-extensions/Anchor.toml @@ -0,0 +1,14 @@ +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[programs.localnet] +token_extensions = "tKEkkQtgMXhdaz5NMTR3XbdUu215sZyHSj6Menvous1" + +[scripts] +test = "yarn run ts-mocha -t 1000000 tests/*.ts" + +[features] + +[test.validator] +url = "https://api.mainnet-beta.solana.com" \ No newline at end of file diff --git a/tests/spl/token-extensions/Cargo.toml b/tests/spl/token-extensions/Cargo.toml new file mode 100644 index 0000000000..97d6280542 --- /dev/null +++ b/tests/spl/token-extensions/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = [ + "programs/*" +] +resolver = "2" + +[profile.release] +overflow-checks = true diff --git a/tests/spl/token-extensions/package.json b/tests/spl/token-extensions/package.json new file mode 100644 index 0000000000..0c71110804 --- /dev/null +++ b/tests/spl/token-extensions/package.json @@ -0,0 +1,22 @@ +{ + "name": "token-extensions", + "version": "0.29.0", + "license": "(MIT OR Apache-2.0)", + "homepage": "https://github.com/coral-xyz/anchor#readme", + "bugs": { + "url": "https://github.com/coral-xyz/anchor/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/coral-xyz/anchor.git" + }, + "engines": { + "node": ">=11" + }, + "scripts": { + "test": "anchor test" + }, + "dependencies": { + "@solana/spl-token": "^0.3.9" + } +} diff --git a/tests/spl/token-extensions/programs/token-extensions/Cargo.toml b/tests/spl/token-extensions/programs/token-extensions/Cargo.toml new file mode 100644 index 0000000000..b061d462b0 --- /dev/null +++ b/tests/spl/token-extensions/programs/token-extensions/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "token-extensions" +version = "0.1.0" +description = "Created with Anchor" +rust-version = "1.60" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "token_extensions" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] + +[dependencies] +anchor-lang = { path = "../../../../../lang", features = ["init-if-needed"] } +anchor-spl = { path = "../../../../../spl" } +spl-tlv-account-resolution = "0.6.3" +spl-transfer-hook-interface = "0.6.3" +spl-type-length-value = "0.4.3" +spl-pod = "0.2.2" diff --git a/tests/spl/token-extensions/programs/token-extensions/Xargo.toml b/tests/spl/token-extensions/programs/token-extensions/Xargo.toml new file mode 100644 index 0000000000..1744f098ae --- /dev/null +++ b/tests/spl/token-extensions/programs/token-extensions/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs new file mode 100644 index 0000000000..4cad833973 --- /dev/null +++ b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs @@ -0,0 +1,180 @@ +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_interface::{ + spl_token_metadata_interface::state::TokenMetadata, token_metadata_initialize, Mint, + Token2022, TokenAccount, TokenMetadataInitialize, + }, +}; +use spl_pod::optional_keys::OptionalNonZeroPubkey; + +use crate::{ + get_meta_list_size, get_mint_extensible_extension_data, get_mint_extension_data, + update_account_lamports_to_minimum_balance, META_LIST_ACCOUNT_SEED, +}; + +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct CreateMintAccountArgs { + pub name: String, + pub symbol: String, + pub uri: String, +} + +#[derive(Accounts)] +#[instruction(args: CreateMintAccountArgs)] +pub struct CreateMintAccount<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account(mut)] + /// CHECK: can be any account + pub authority: Signer<'info>, + #[account()] + /// CHECK: can be any account + pub receiver: UncheckedAccount<'info>, + #[account( + init, + signer, + payer = payer, + mint::token_program = token_program, + mint::decimals = 0, + mint::authority = authority, + mint::freeze_authority = authority, + extensions::metadata_pointer::authority = authority, + extensions::metadata_pointer::metadata_address = mint, + extensions::group_member_pointer::authority = authority, + extensions::group_member_pointer::member_address = mint, + extensions::transfer_hook::authority = authority, + extensions::transfer_hook::program_id = crate::ID, + extensions::close_authority::authority = authority, + extensions::permanent_delegate::delegate = authority, + )] + pub mint: Box>, + #[account( + init, + payer = payer, + associated_token::token_program = token_program, + associated_token::mint = mint, + associated_token::authority = receiver, + )] + pub mint_token_account: Box>, + /// CHECK: This account's data is a buffer of TLV data + #[account( + init, + space = get_meta_list_size(None), + seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()], + bump, + payer = payer, + )] + pub extra_metas_account: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub token_program: Program<'info, Token2022>, +} + +impl<'info> CreateMintAccount<'info> { + fn initialize_token_metadata( + &self, + name: String, + symbol: String, + uri: String, + ) -> ProgramResult { + let cpi_accounts = TokenMetadataInitialize { + token_program_id: self.token_program.to_account_info(), + mint: self.mint.to_account_info(), + metadata: self.mint.to_account_info(), // metadata account is the mint, since data is stored in mint + mint_authority: self.authority.to_account_info(), + update_authority: self.authority.to_account_info(), + }; + let cpi_ctx = CpiContext::new(self.token_program.to_account_info(), cpi_accounts); + token_metadata_initialize(cpi_ctx, name, symbol, uri)?; + Ok(()) + } +} + +pub fn handler(ctx: Context, args: CreateMintAccountArgs) -> Result<()> { + ctx.accounts.initialize_token_metadata( + args.name.clone(), + args.symbol.clone(), + args.uri.clone(), + )?; + ctx.accounts.mint.reload()?; + let mint_data = &mut ctx.accounts.mint.to_account_info(); + let metadata = get_mint_extensible_extension_data::(mint_data)?; + assert_eq!(metadata.mint, ctx.accounts.mint.key()); + assert_eq!(metadata.name, args.name); + assert_eq!(metadata.symbol, args.symbol); + assert_eq!(metadata.uri, args.uri); + let metadata_pointer = get_mint_extension_data::(mint_data)?; + let mint_key: Option = Some(ctx.accounts.mint.key()); + let authority_key: Option = Some(ctx.accounts.authority.key()); + assert_eq!( + metadata_pointer.metadata_address, + OptionalNonZeroPubkey::try_from(mint_key)? + ); + assert_eq!( + metadata_pointer.authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + let permanent_delegate = get_mint_extension_data::(mint_data)?; + assert_eq!( + permanent_delegate.delegate, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + let close_authority = get_mint_extension_data::(mint_data)?; + assert_eq!( + close_authority.close_authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + let transfer_hook = get_mint_extension_data::(mint_data)?; + let program_id: Option = Some(ctx.program_id.key()); + assert_eq!( + transfer_hook.authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!( + transfer_hook.program_id, + OptionalNonZeroPubkey::try_from(program_id)? + ); + let group_member_pointer = get_mint_extension_data::(mint_data)?; + assert_eq!( + group_member_pointer.authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!( + group_member_pointer.member_address, + OptionalNonZeroPubkey::try_from(mint_key)? + ); + // transfer minimum rent to mint account + update_account_lamports_to_minimum_balance( + ctx.accounts.mint.to_account_info(), + ctx.accounts.payer.to_account_info(), + ctx.accounts.system_program.to_account_info(), + )?; + + Ok(()) +} + +#[derive(Accounts)] +#[instruction()] +pub struct CheckMintExtensionConstraints<'info> { + #[account(mut)] + /// CHECK: can be any account + pub authority: Signer<'info>, + #[account( + extensions::metadata_pointer::authority = authority, + extensions::metadata_pointer::metadata_address = mint, + extensions::group_member_pointer::authority = authority, + extensions::group_member_pointer::member_address = mint, + extensions::transfer_hook::authority = authority, + extensions::transfer_hook::program_id = crate::ID, + extensions::close_authority::authority = authority, + extensions::permanent_delegate::delegate = authority, + )] + pub mint: Box>, +} diff --git a/tests/spl/token-extensions/programs/token-extensions/src/lib.rs b/tests/spl/token-extensions/programs/token-extensions/src/lib.rs new file mode 100644 index 0000000000..303be8240b --- /dev/null +++ b/tests/spl/token-extensions/programs/token-extensions/src/lib.rs @@ -0,0 +1,32 @@ +//! An example of a program with token extensions enabled +//! +//! This program is intended to implement various token2022 extensions +//! +//! + +use anchor_lang::prelude::*; + +pub mod instructions; +pub mod utils; +pub use instructions::*; +pub use utils::*; + +declare_id!("tKEkkQtgMXhdaz5NMTR3XbdUu215sZyHSj6Menvous1"); + +#[program] +pub mod token_extensions { + use super::*; + + pub fn create_mint_account( + ctx: Context, + args: CreateMintAccountArgs, + ) -> Result<()> { + instructions::handler(ctx, args) + } + + pub fn check_mint_extensions_constraints( + _ctx: Context, + ) -> Result<()> { + Ok(()) + } +} diff --git a/tests/spl/token-extensions/programs/token-extensions/src/utils.rs b/tests/spl/token-extensions/programs/token-extensions/src/utils.rs new file mode 100644 index 0000000000..9a65280554 --- /dev/null +++ b/tests/spl/token-extensions/programs/token-extensions/src/utils.rs @@ -0,0 +1,88 @@ +use anchor_lang::{ + prelude::Result, + solana_program::{ + account_info::AccountInfo, + instruction::{get_stack_height, TRANSACTION_LEVEL_STACK_HEIGHT}, + program::invoke, + pubkey::Pubkey, + rent::Rent, + system_instruction::transfer, + sysvar::Sysvar, + }, + Lamports, +}; +use anchor_spl::token_interface::spl_token_2022::{ + extension::{BaseStateWithExtensions, Extension, StateWithExtensions}, + solana_zk_token_sdk::zk_token_proof_instruction::Pod, + state::Mint, +}; +use spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList}; +use spl_type_length_value::variable_len_pack::VariableLenPack; + +pub const APPROVE_ACCOUNT_SEED: &[u8] = b"approve-account"; +pub const META_LIST_ACCOUNT_SEED: &[u8] = b"extra-account-metas"; + +pub fn update_account_lamports_to_minimum_balance<'info>( + account: AccountInfo<'info>, + payer: AccountInfo<'info>, + system_program: AccountInfo<'info>, +) -> Result<()> { + let extra_lamports = Rent::get()?.minimum_balance(account.data_len()) - account.get_lamports(); + if extra_lamports > 0 { + invoke( + &transfer(payer.key, account.key, extra_lamports), + &[payer, account, system_program], + )?; + } + Ok(()) +} + +pub fn get_mint_extensible_extension_data( + account: &mut AccountInfo, +) -> Result { + let mint_data = account.data.borrow(); + let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; + let extension_data = mint_with_extension.get_variable_len_extension::()?; + Ok(extension_data) +} + +pub fn get_mint_extension_data(account: &mut AccountInfo) -> Result { + let mint_data = account.data.borrow(); + let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; + let extension_data = *mint_with_extension.get_extension::()?; + Ok(extension_data) +} + +pub fn get_extra_meta_list_account_pda(mint: Pubkey) -> Pubkey { + Pubkey::find_program_address(&[META_LIST_ACCOUNT_SEED, mint.as_ref()], &crate::id()).0 +} + +pub fn get_approve_account_pda(mint: Pubkey) -> Pubkey { + Pubkey::find_program_address(&[APPROVE_ACCOUNT_SEED, mint.as_ref()], &crate::id()).0 +} + +/// Determine if we are in CPI +pub fn hook_in_cpi() -> bool { + let stack_height = get_stack_height(); + let tx_height = TRANSACTION_LEVEL_STACK_HEIGHT; + let hook_height: usize = tx_height + 1; + + stack_height > hook_height +} + +pub fn get_meta_list(approve_account: Option) -> Vec { + if let Some(approve_account) = approve_account { + return vec![ExtraAccountMeta { + discriminator: 0, + address_config: approve_account.to_bytes(), + is_signer: false.into(), + is_writable: true.into(), + }]; + } + vec![] +} + +pub fn get_meta_list_size(approve_account: Option) -> usize { + // safe because it's either 0 or 1 + ExtraAccountMetaList::size_of(get_meta_list(approve_account).len()).unwrap() +} diff --git a/tests/spl/token-extensions/tests/token-extensions.ts b/tests/spl/token-extensions/tests/token-extensions.ts new file mode 100644 index 0000000000..f2b498d953 --- /dev/null +++ b/tests/spl/token-extensions/tests/token-extensions.ts @@ -0,0 +1,84 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { PublicKey, Keypair } from "@solana/web3.js"; +import { TokenExtensions } from "../target/types/token_extensions"; +import { ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token"; +import { it } from "node:test"; + +const TOKEN_2022_PROGRAM_ID = new anchor.web3.PublicKey( + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" +); + +export function associatedAddress({ + mint, + owner, +}: { + mint: PublicKey; + owner: PublicKey; +}): PublicKey { + return PublicKey.findProgramAddressSync( + [owner.toBuffer(), TOKEN_2022_PROGRAM_ID.toBuffer(), mint.toBuffer()], + ASSOCIATED_PROGRAM_ID + )[0]; +} + +describe("token extensions", () => { + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const program = anchor.workspace.TokenExtensions as Program; + + const payer = Keypair.generate(); + + it("airdrop payer", async () => { + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(payer.publicKey, 10000000000), + "confirmed" + ); + }); + + let mint = new Keypair(); + + it("Create mint account test passes", async () => { + const [extraMetasAccount] = PublicKey.findProgramAddressSync( + [ + anchor.utils.bytes.utf8.encode("extra-account-metas"), + mint.publicKey.toBuffer(), + ], + program.programId + ); + await program.methods + .createMintAccount({ + name: "hello", + symbol: "hi", + uri: "https://hi.com", + }) + .accountsStrict({ + payer: payer.publicKey, + authority: payer.publicKey, + receiver: payer.publicKey, + mint: mint.publicKey, + mintTokenAccount: associatedAddress({ + mint: mint.publicKey, + owner: payer.publicKey, + }), + extraMetasAccount: extraMetasAccount, + systemProgram: anchor.web3.SystemProgram.programId, + associatedTokenProgram: ASSOCIATED_PROGRAM_ID, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }) + .signers([mint, payer]) + .rpc(); + }); + + it("mint extension constraints test passes", async () => { + await program.methods + .checkMintExtensionsConstraints() + .accountsStrict({ + authority: payer.publicKey, + mint: mint.publicKey, + }) + .signers([payer]) + .rpc(); + }); +}); diff --git a/tests/spl/token-extensions/tsconfig.json b/tests/spl/token-extensions/tsconfig.json new file mode 100644 index 0000000000..c7f23d9eaf --- /dev/null +++ b/tests/spl/token-extensions/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai", "node"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/tests/spl/token-proxy/programs/token-proxy/src/lib.rs b/tests/spl/token-proxy/programs/token-proxy/src/lib.rs index 8f8d6710ed..9f3ff8d6b6 100644 --- a/tests/spl/token-proxy/programs/token-proxy/src/lib.rs +++ b/tests/spl/token-proxy/programs/token-proxy/src/lib.rs @@ -191,7 +191,8 @@ pub struct ProxyCreateAssociatedTokenAccount<'info> { pub struct ProxyCreateMint<'info> { #[account(mut)] pub authority: Signer<'info>, - #[account(init, + #[account( + init, mint::decimals = 9, mint::authority = authority, seeds = [authority.key().as_ref(), name.as_bytes(), b"token-proxy-mint"],