diff --git a/Cargo.lock b/Cargo.lock index cd9df08c65b..d62b90e1969 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7716,6 +7716,7 @@ dependencies = [ "spl-program-error 0.3.0", "spl-tlv-account-resolution 0.5.1", "spl-type-length-value 0.3.0", + "tokio", ] [[package]] diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index e0e6fbf82c0..921f23a466e 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -7,7 +7,6 @@ use { state::Mint, }, solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey}, - spl_transfer_hook_interface::offchain::resolve_extra_account_metas, std::future::Future, }; @@ -47,7 +46,8 @@ where .ok_or(ProgramError::InvalidAccountData)?; let mint = StateWithExtensions::::unpack(&mint_data)?; if let Some(program_id) = transfer_hook::get_program_id(&mint) { - resolve_extra_account_metas( + #[allow(deprecated)] + spl_transfer_hook_interface::offchain::resolve_extra_account_metas( instruction, fetch_account_data_fn, mint_address, diff --git a/token/transfer-hook/interface/Cargo.toml b/token/transfer-hook/interface/Cargo.toml index 0db28e9d48f..79d4a7dfb2e 100644 --- a/token/transfer-hook/interface/Cargo.toml +++ b/token/transfer-hook/interface/Cargo.toml @@ -20,5 +20,8 @@ spl-pod = { version = "0.1", path = "../../../libraries/pod" } [lib] crate-type = ["cdylib", "lib"] +[dev-dependencies] +tokio = { version = "1.35.1", features = ["full"] } + [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/transfer-hook/interface/src/offchain.rs b/token/transfer-hook/interface/src/offchain.rs index 60b7ea53593..692f5dd53f3 100644 --- a/token/transfer-hook/interface/src/offchain.rs +++ b/token/transfer-hook/interface/src/offchain.rs @@ -2,7 +2,11 @@ pub use spl_tlv_account_resolution::state::{AccountDataResult, AccountFetchError}; use { - crate::{get_extra_account_metas_address, instruction::ExecuteInstruction}, + crate::{ + error::TransferHookError, + get_extra_account_metas_address, + instruction::{execute, ExecuteInstruction}, + }, solana_program::{ instruction::{AccountMeta, Instruction}, program_error::ProgramError, @@ -35,6 +39,10 @@ use { /// &program_id, /// ).await?; /// ``` +#[deprecated( + since = "0.5.0", + note = "Please use `add_extra_account_metas_for_execute` instead" +)] pub async fn resolve_extra_account_metas( instruction: &mut Instruction, fetch_account_data_fn: F, @@ -68,3 +76,246 @@ where Ok(()) } + +/// Offchain helper to get all additional required account metas for an execute +/// instruction, based on a validation state account. +/// +/// The instruction being provided to this function must contain at least the +/// same account keys as the ones being provided, in order. Specifically: +/// 1. source +/// 2. mint +/// 3. destination +/// 4. authority +/// +/// The `program_id` should be the program ID of the program that the +/// created `ExecuteInstruction` is for. +/// +/// 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 +/// add_extra_account_metas_for_execute( +/// &mut instruction, +/// &program_id, +/// &source, +/// &mint, +/// &destination, +/// &authority, +/// amount, +/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)), +/// ) +/// .await?; +/// ``` +#[allow(clippy::too_many_arguments)] +pub async fn add_extra_account_metas_for_execute( + instruction: &mut Instruction, + program_id: &Pubkey, + source_pubkey: &Pubkey, + mint_pubkey: &Pubkey, + destination_pubkey: &Pubkey, + authority_pubkey: &Pubkey, + amount: u64, + fetch_account_data_fn: F, +) -> Result<(), AccountFetchError> +where + F: Fn(Pubkey) -> Fut, + Fut: Future, +{ + let validate_state_pubkey = get_extra_account_metas_address(mint_pubkey, program_id); + let validate_state_data = fetch_account_data_fn(validate_state_pubkey) + .await? + .ok_or(ProgramError::InvalidAccountData)?; + + // Check to make sure the provided keys are in the instruction + if [ + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + ] + .iter() + .any(|&key| !instruction.accounts.iter().any(|meta| meta.pubkey == *key)) + { + Err(TransferHookError::IncorrectAccount)?; + } + + let mut execute_instruction = execute( + program_id, + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + &validate_state_pubkey, + amount, + ); + + ExtraAccountMetaList::add_to_instruction::( + &mut execute_instruction, + fetch_account_data_fn, + &validate_state_data, + ) + .await?; + + // Add only the extra accounts resolved from the validation state + instruction + .accounts + .extend_from_slice(&execute_instruction.accounts[5..]); + + // Add the program id and validation state account + instruction + .accounts + .push(AccountMeta::new_readonly(*program_id, false)); + instruction + .accounts + .push(AccountMeta::new_readonly(validate_state_pubkey, false)); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use { + super::*, + spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}, + tokio, + }; + + const PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]); + const EXTRA_META_1: Pubkey = Pubkey::new_from_array([2u8; 32]); + const EXTRA_META_2: Pubkey = Pubkey::new_from_array([3u8; 32]); + + // Mock to return the validation state account data + async fn mock_fetch_account_data_fn(_address: Pubkey) -> AccountDataResult { + 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)) + } + + #[tokio::test] + async fn test_add_extra_account_metas_for_execute() { + let source = Pubkey::new_unique(); + let mint = 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, &PROGRAM_ID); + let extra_meta_3_pubkey = Pubkey::find_program_address( + &[ + source.as_ref(), + destination.as_ref(), + validate_state_pubkey.as_ref(), + ], + &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(), + ], + &PROGRAM_ID, + ) + .0; + + // Fail missing key + let mut instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &[], + vec![ + // source missing + AccountMeta::new_readonly(mint, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + ], + ); + assert_eq!( + add_extra_account_metas_for_execute( + &mut instruction, + &PROGRAM_ID, + &source, + &mint, + &destination, + &authority, + amount, + mock_fetch_account_data_fn, + ) + .await + .unwrap_err() + .downcast::() + .unwrap(), + Box::new(TransferHookError::IncorrectAccount) + ); + + // Success + let mut instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &[], + vec![ + AccountMeta::new(source, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + ], + ); + add_extra_account_metas_for_execute( + &mut instruction, + &PROGRAM_ID, + &source, + &mint, + &destination, + &authority, + amount, + mock_fetch_account_data_fn, + ) + .await + .unwrap(); + + let check_metas = [ + AccountMeta::new(source, false), + AccountMeta::new_readonly(mint, 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(PROGRAM_ID, false), + AccountMeta::new_readonly(validate_state_pubkey, false), + ]; + + assert_eq!(instruction.accounts, check_metas); + } +}