diff --git a/Cargo.lock b/Cargo.lock index d62b90e1969..485b5791466 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7350,6 +7350,7 @@ dependencies = [ "solana-zk-token-sdk", "spl-memo 4.0.0", "spl-pod 0.1.0", + "spl-tlv-account-resolution 0.5.1", "spl-token 4.0.0", "spl-token-group-interface", "spl-token-metadata-interface 0.2.0", diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 51232ac9593..d09ca78a8b7 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -938,6 +938,7 @@ where if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts { instruction.accounts.extend(transfer_hook_accounts.clone()); } else { + #[allow(deprecated)] offchain::resolve_extra_transfer_account_metas( &mut instruction, |address| { diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 4727f97c091..4346126c1d9 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -627,6 +627,7 @@ async fn success_downgrade_writable_and_signer_accounts() { .unwrap(); } +#[allow(deprecated)] #[tokio::test] async fn success_transfers_using_onchain_helper() { let authority = Pubkey::new_unique(); diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index e3a1f3d73bf..ac0a8ff8a3a 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -45,6 +45,7 @@ proptest = "1.4" serial_test = "3.0.0" solana-program-test = "1.17.6" solana-sdk = "1.17.6" +spl-tlv-account-resolution = { version = "0.5.0", path = "../../libraries/tlv-account-resolution" } serde_json = "1.0.111" [lib] diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index 921f23a466e..65451772151 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -7,6 +7,7 @@ use { state::Mint, }, solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey}, + spl_transfer_hook_interface::offchain::add_extra_account_metas_for_execute, std::future::Future, }; @@ -32,6 +33,10 @@ use { /// &mint, /// ).await?; /// ``` +#[deprecated( + since = "1.1.0", + note = "Please use `create_transfer_checked_instruction_with_extra_metas` instead" +)] pub async fn resolve_extra_transfer_account_metas( instruction: &mut Instruction, fetch_account_data_fn: F, @@ -57,3 +62,254 @@ where } Ok(()) } + +/// Offchain helper to create a `TransferChecked` instruction with all +/// additional required account metas for a transfer, including the ones +/// required by the transfer hook. +/// +/// To be client-agnostic and to avoid pulling in the full solana-sdk, this +/// simply takes a function that will return its data as `Future>` for +/// the given address. Can be called in the following way: +/// +/// ```rust,ignore +/// let instruction = create_transfer_checked_instruction_with_extra_metas( +/// &spl_token_2022::id(), +/// &source, +/// &mint, +/// &destination, +/// &authority, +/// &[], +/// amount, +/// decimals, +/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)), +/// ) +/// .await? +/// ``` +#[allow(clippy::too_many_arguments)] +pub async fn create_transfer_checked_instruction_with_extra_metas( + token_program_id: &Pubkey, + source_pubkey: &Pubkey, + mint_pubkey: &Pubkey, + destination_pubkey: &Pubkey, + authority_pubkey: &Pubkey, + signer_pubkeys: &[&Pubkey], + amount: u64, + decimals: u8, + fetch_account_data_fn: F, +) -> Result +where + F: Fn(Pubkey) -> Fut, + Fut: Future, +{ + let mut transfer_instruction = crate::instruction::transfer_checked( + token_program_id, + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + signer_pubkeys, + amount, + decimals, + )?; + + let mint_data = fetch_account_data_fn(*mint_pubkey) + .await? + .ok_or(ProgramError::InvalidAccountData)?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + + if let Some(program_id) = transfer_hook::get_program_id(&mint) { + add_extra_account_metas_for_execute( + &mut transfer_instruction, + &program_id, + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + amount, + fetch_account_data_fn, + ) + .await?; + } + + Ok(transfer_instruction) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::extension::{transfer_hook::TransferHook, ExtensionType, StateWithExtensionsMut}, + solana_program::{instruction::AccountMeta, program_option::COption}, + solana_program_test::tokio, + spl_pod::optional_keys::OptionalNonZeroPubkey, + spl_tlv_account_resolution::{ + account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, + }, + spl_transfer_hook_interface::{ + get_extra_account_metas_address, instruction::ExecuteInstruction, + }, + }; + + const DECIMALS: u8 = 0; + const MINT_PUBKEY: Pubkey = Pubkey::new_from_array([1u8; 32]); + const TRANSFER_HOOK_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]); + const EXTRA_META_1: Pubkey = Pubkey::new_from_array([3u8; 32]); + const EXTRA_META_2: Pubkey = Pubkey::new_from_array([4u8; 32]); + + // Mock to return the mint data or the validation state account data + async fn mock_fetch_account_data_fn(address: Pubkey) -> AccountDataResult { + if address == MINT_PUBKEY { + let mint_len = + ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferHook]) + .unwrap(); + let mut data = vec![0u8; mint_len]; + let mut mint = StateWithExtensionsMut::::unpack_uninitialized(&mut data).unwrap(); + + let extension = mint.init_extension::(true).unwrap(); + extension.program_id = + OptionalNonZeroPubkey::try_from(Some(TRANSFER_HOOK_PROGRAM_ID)).unwrap(); + + mint.base.mint_authority = COption::Some(Pubkey::new_unique()); + mint.base.decimals = DECIMALS; + mint.base.is_initialized = true; + mint.base.freeze_authority = COption::None; + mint.pack_base(); + mint.init_account_type().unwrap(); + + Ok(Some(data)) + } else if address + == get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID) + { + let extra_metas = vec![ + ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::AccountKey { index: 0 }, // source + Seed::AccountKey { index: 2 }, // destination + Seed::AccountKey { index: 4 }, // validation state + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, + length: 8, + }, // amount + Seed::AccountKey { index: 2 }, // destination + Seed::AccountKey { index: 5 }, // extra meta 1 + Seed::AccountKey { index: 7 }, // extra meta 3 (PDA) + ], + false, + true, + ) + .unwrap(), + ]; + let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap(); + let mut data = vec![0u8; account_size]; + ExtraAccountMetaList::init::(&mut data, &extra_metas)?; + Ok(Some(data)) + } else { + Ok(None) + } + } + + #[tokio::test] + async fn test_create_transfer_checked_instruction_with_extra_metas() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let amount = 100u64; + + let validate_state_pubkey = + get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID); + let extra_meta_3_pubkey = Pubkey::find_program_address( + &[ + source.as_ref(), + destination.as_ref(), + validate_state_pubkey.as_ref(), + ], + &TRANSFER_HOOK_PROGRAM_ID, + ) + .0; + let extra_meta_4_pubkey = Pubkey::find_program_address( + &[ + amount.to_le_bytes().as_ref(), + destination.as_ref(), + EXTRA_META_1.as_ref(), + extra_meta_3_pubkey.as_ref(), + ], + &TRANSFER_HOOK_PROGRAM_ID, + ) + .0; + + let instruction = create_transfer_checked_instruction_with_extra_metas( + &crate::id(), + &source, + &MINT_PUBKEY, + &destination, + &authority, + &[], + amount, + DECIMALS, + mock_fetch_account_data_fn, + ) + .await + .unwrap(); + + let check_metas = [ + AccountMeta::new(source, false), + AccountMeta::new_readonly(MINT_PUBKEY, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(EXTRA_META_1, true), + AccountMeta::new_readonly(EXTRA_META_2, true), + AccountMeta::new(extra_meta_3_pubkey, false), + AccountMeta::new(extra_meta_4_pubkey, false), + AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), + AccountMeta::new_readonly(validate_state_pubkey, false), + ]; + + assert_eq!(instruction.accounts, check_metas); + + // With additional signers + let signer_1 = Pubkey::new_unique(); + let signer_2 = Pubkey::new_unique(); + let signer_3 = Pubkey::new_unique(); + + let instruction = create_transfer_checked_instruction_with_extra_metas( + &crate::id(), + &source, + &MINT_PUBKEY, + &destination, + &authority, + &[&signer_1, &signer_2, &signer_3], + amount, + DECIMALS, + mock_fetch_account_data_fn, + ) + .await + .unwrap(); + + let check_metas = [ + AccountMeta::new(source, false), + AccountMeta::new_readonly(MINT_PUBKEY, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, false), // False because of additional signers + AccountMeta::new_readonly(signer_1, true), + AccountMeta::new_readonly(signer_2, true), + AccountMeta::new_readonly(signer_3, true), + AccountMeta::new_readonly(EXTRA_META_1, true), + AccountMeta::new_readonly(EXTRA_META_2, true), + AccountMeta::new(extra_meta_3_pubkey, false), + AccountMeta::new(extra_meta_4_pubkey, false), + AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), + AccountMeta::new_readonly(validate_state_pubkey, false), + ]; + + assert_eq!(instruction.accounts, check_metas); + } +}