From 15ddbc8e8e742e418b5897b4b0a103e4bec862f8 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 9 Jan 2024 21:13:35 -0600 Subject: [PATCH] transfer hook: add new offchain helper --- Cargo.lock | 1 + token/program-2022/src/offchain.rs | 2 + token/transfer-hook/interface/Cargo.toml | 3 + token/transfer-hook/interface/src/offchain.rs | 243 +++++++++++++++++- 4 files changed, 248 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b4fca7fd4e0..808c4ad1b10 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..e9efc876c66 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -1,6 +1,7 @@ //! Offchain helper for fetching required accounts to build instructions pub use spl_transfer_hook_interface::offchain::{AccountDataResult, AccountFetchError}; +#[allow(deprecated)] use { crate::{ extension::{transfer_hook, StateWithExtensions}, @@ -33,6 +34,7 @@ use { /// &mint, /// ).await?; /// ``` +#[allow(deprecated)] pub async fn resolve_extra_transfer_account_metas( instruction: &mut Instruction, fetch_account_data_fn: F, 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..422867b7534 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,236 @@ where Ok(()) } + +/// Offchain helper to get all additional required account metas for an execute +/// instruction, based on a validation state account. +/// +/// 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); + } +}