From 7d3ecc85f7ebc8fd8ce020be78006385e194789b Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 5 Jan 2024 01:31:52 -0600 Subject: [PATCH 1/7] token-2022: modify offchain extra metas helper --- token/client/src/token.rs | 1 + .../program-2022-test/tests/transfer_hook.rs | 2 + token/program-2022/src/offchain.rs | 41 +++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 51232ac9593..51ddfa8e9e9 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -946,6 +946,7 @@ where .map_ok(|opt| opt.map(|acc| acc.data)) }, self.get_address(), + amount, ) .await .map_err(|_| TokenError::AccountNotFound)?; diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 4727f97c091..06db5ed0a45 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -715,6 +715,7 @@ async fn success_transfers_using_onchain_helper() { ) }, &mint_a, + amount, ) .await .unwrap(); @@ -730,6 +731,7 @@ async fn success_transfers_using_onchain_helper() { ) }, &mint_b, + amount, ) .await .unwrap(); diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index e0e6fbf82c0..501fcfb0850 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -3,11 +3,14 @@ pub use spl_transfer_hook_interface::offchain::{AccountDataResult, AccountFetchError}; use { crate::{ + error::TokenError, extension::{transfer_hook, StateWithExtensions}, state::Mint, }, - solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey}, - spl_transfer_hook_interface::offchain::resolve_extra_account_metas, + solana_program::{instruction::Instruction, msg, program_error::ProgramError, pubkey::Pubkey}, + spl_transfer_hook_interface::{ + get_extra_account_metas_address, offchain::resolve_extra_account_metas, + }, std::future::Future, }; @@ -33,10 +36,18 @@ use { /// &mint, /// ).await?; /// ``` +/// Note that this offchain helper will build a new `Execute` instruction, +/// resolve the extra account metas, and then add them to the transfer +/// instruction. This is because the extra account metas are configured +/// specifically for the `Execute` instruction, which requires five accounts +/// (source, mint, destination, authority, and validation state), wheras the +/// transfer instruction only requires four (source, mint, destination, and +/// authority) in addition to `n` number of multisig authorities. pub async fn resolve_extra_transfer_account_metas( instruction: &mut Instruction, fetch_account_data_fn: F, mint_address: &Pubkey, + amount: u64, ) -> Result<(), AccountFetchError> where F: Fn(Pubkey) -> Fut, @@ -46,14 +57,38 @@ where .await? .ok_or(ProgramError::InvalidAccountData)?; let mint = StateWithExtensions::::unpack(&mint_data)?; + if let Some(program_id) = transfer_hook::get_program_id(&mint) { + // Convert the transfer instruction into an `Execute` instruction, + // then resolve the extra account metas as configured in the validation + // account data, then finally add the extra account metas to the original + // transfer instruction. + if instruction.accounts.len() < 4 { + msg!("Not a valid transfer instruction"); + Err(TokenError::InvalidInstruction)?; + } + + let mut execute_ix = spl_transfer_hook_interface::instruction::execute( + &program_id, + &instruction.accounts[0].pubkey, + &instruction.accounts[1].pubkey, + &instruction.accounts[2].pubkey, + &instruction.accounts[3].pubkey, + &get_extra_account_metas_address(mint_address, &program_id), + amount, + ); + resolve_extra_account_metas( - instruction, + &mut execute_ix, fetch_account_data_fn, mint_address, &program_id, ) .await?; + + instruction + .accounts + .extend_from_slice(&execute_ix.accounts[5..]); } Ok(()) } From e015464561fce354528e284c3daf2bf63be14da2 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 5 Jan 2024 01:32:27 -0600 Subject: [PATCH 2/7] token-2022: add test for the modified offchain extra metas helper --- Cargo.lock | 1 + token/program-2022/Cargo.toml | 1 + token/program-2022/src/offchain.rs | 487 +++++++++++++++++++++++++++++ 3 files changed, 489 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 603e65fdfb2..4318fff5bcb 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.0", "spl-token 4.0.0", "spl-token-group-interface", "spl-token-metadata-interface 0.2.0", diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index 8db3d2fe3a9..863e1c443cc 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -45,6 +45,7 @@ proptest = "1.4" serial_test = "2.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 501fcfb0850..5d75707b712 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -92,3 +92,490 @@ where } Ok(()) } + +#[cfg(test)] +mod tests { + use { + super::*, + solana_program::{account_info::AccountInfo, system_program}, + solana_program_test::tokio, + spl_tlv_account_resolution::state::ExtraAccountMetaList, + spl_transfer_hook_interface::instruction::ExecuteInstruction, + }; + + const TRANSFER_HOOK_PROGRAM_ID: Pubkey = Pubkey::new_from_array([1; 32]); + + const MINT_PUBKEY: Pubkey = Pubkey::new_from_array([2; 32]); + + const MOCK_MINT_STATE: [u8; 234] = [ + 0, 0, 0, 0, // COption (4): None = 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Mint authority (32) + 0, 0, 0, 0, 0, 0, 0, 0, // Supply (8) + 0, // Decimals (1) + 1, // Is initialized (1) + 0, 0, 0, 0, // COption (4): None = 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Freeze authority (32) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Padding (83) + 1, // Account type (1): Mint = 1 + 14, 0, // Extension type (2): Transfer hook = 14 + 64, 0, // Extension length (2): 64 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Authority (32) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, // Transfer hook program ID (32) + ]; + + const MOCK_EXTRA_METAS_STATE: [u8; 226] = [ + 105, 37, 101, 197, 75, 251, 102, 26, // Discriminator for `ExecuteInstruction` (8) + 214, 0, 0, 0, // Length of pod slice (4): 214 + 6, 0, 0, 0, // Count of account metas (4): 6 + 1, // First account meta discriminator (1): PDA = 1 + 3, 0, // First seed: Account key at index 0 (2) + 3, 1, // Second seed: Account key at index 1 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // No more seeds (28) + 0, // First account meta is signer (1): false = 0 + 0, // First account meta is writable (1): false = 0 + 1, // Second account meta discriminator (1): PDA = 1 + 3, 4, // First seed: Account key at index 4 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // No more seeds (30) + 0, // Second account meta is signer (1): false = 0 + 0, // Second account meta is writable (1): false = 0 + 1, // Third account meta discriminator (1): PDA = 1 + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (21) + 0, // Third account meta is signer (1): false = 0 + 0, // Third account meta is writable (1): false = 0 + 0, // Fourth account meta discriminator (1): Pubkey = 0 + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, // Pubkey (32) + 0, // Fourth account meta is signer (1): false = 0 + 0, // Fourth account meta is writable (1): false = 0 + 136, // Fifth account meta discriminator (1): External PDA = 128 + index 8 = 136 + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (19) + 0, // Fifth account meta is signer (1): false = 0 + 0, // Fifth account meta is writable (1): false = 0 + 136, // Sixth account meta discriminator (1): External PDA = 128 + index 8 = 136 + 1, 14, 97, 110, 111, 116, 104, 101, 114, 95, 112, 114, 101, 102, 105, + 120, // First seed: Literal "another_prefix" (16) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + 3, 9, // Fourth seed: Account key at index 9 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (9) + 0, // Sixth account meta is signer (1): false = 0 + 0, // Sixth account meta is writable (1): false = 0 + ]; + + async fn mock_fetch_account_data_fn(address: Pubkey) -> AccountDataResult { + if address == MINT_PUBKEY { + Ok(Some(MOCK_MINT_STATE.to_vec())) + } else if address + == get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID) + { + Ok(Some(MOCK_EXTRA_METAS_STATE.to_vec())) + } else { + Ok(None) + } + } + + #[tokio::test] + async fn test_resolve_extra_transfer_account_metas() { + let spl_token_2022_program_id = crate::id(); + let transfer_hook_program_id = TRANSFER_HOOK_PROGRAM_ID; + let amount = 2u64; + + let source_pubkey = Pubkey::new_unique(); + let mut source_data = vec![0; 165]; // Mock + let mut source_lamports = 0; // Mock + let source_account_info = AccountInfo::new( + &source_pubkey, + false, + true, + &mut source_lamports, + &mut source_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let mint_pubkey = MINT_PUBKEY; + let mut mint_data = MOCK_MINT_STATE.to_vec(); + let mut mint_lamports = 0; // Mock + let mint_account_info = AccountInfo::new( + &mint_pubkey, + false, + true, + &mut mint_lamports, + &mut mint_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let destination_pubkey = Pubkey::new_unique(); + let mut destination_data = vec![0; 165]; // Mock + let mut destination_lamports = 0; // Mock + let destination_account_info = AccountInfo::new( + &destination_pubkey, + false, + true, + &mut destination_lamports, + &mut destination_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let authority_pubkey = Pubkey::new_unique(); + let mut authority_data = vec![]; // Mock + let mut authority_lamports = 0; // Mock + let authority_account_info = AccountInfo::new( + &authority_pubkey, + false, + true, + &mut authority_lamports, + &mut authority_data, + &system_program::ID, + false, + 0, + ); + + let validate_state_pubkey = + get_extra_account_metas_address(&mint_pubkey, &transfer_hook_program_id); + + let extra_meta_1_pubkey = Pubkey::find_program_address( + &[ + &source_pubkey.to_bytes(), // Account key at index 0 + &mint_pubkey.to_bytes(), // Account key at index 1 + ], + &transfer_hook_program_id, + ) + .0; + let mut extra_meta_1_data = vec![]; // Mock + let mut extra_meta_1_lamports = 0; // Mock + let extra_meta_1_account_info = AccountInfo::new( + &extra_meta_1_pubkey, + false, + true, + &mut extra_meta_1_lamports, + &mut extra_meta_1_data, + &transfer_hook_program_id, + false, + 0, + ); + + let extra_meta_2_pubkey = Pubkey::find_program_address( + &[ + &validate_state_pubkey.to_bytes(), // Account key at index 4 + ], + &transfer_hook_program_id, + ) + .0; + let mut extra_meta_2_data = vec![]; // Mock + let mut extra_meta_2_lamports = 0; // Mock + let extra_meta_2_account_info = AccountInfo::new( + &extra_meta_2_pubkey, + false, + true, + &mut extra_meta_2_lamports, + &mut extra_meta_2_data, + &transfer_hook_program_id, + false, + 0, + ); + + let extra_meta_3_pubkey = Pubkey::find_program_address( + &[ + b"prefix", + amount.to_le_bytes().as_ref(), // Instruction data 8..16 + ], + &transfer_hook_program_id, + ) + .0; + let mut extra_meta_3_data = vec![]; // Mock + let mut extra_meta_3_lamports = 0; // Mock + let extra_meta_3_account_info = AccountInfo::new( + &extra_meta_3_pubkey, + false, + true, + &mut extra_meta_3_lamports, + &mut extra_meta_3_data, + &transfer_hook_program_id, + false, + 0, + ); + + let extra_meta_4_pubkey = Pubkey::new_from_array([7; 32]); // Some arbitrary program ID + let mut extra_meta_4_data = vec![]; // Mock + let mut extra_meta_4_lamports = 0; // Mock + let extra_meta_4_account_info = AccountInfo::new( + &extra_meta_4_pubkey, + false, + true, + &mut extra_meta_4_lamports, + &mut extra_meta_4_data, + &transfer_hook_program_id, + true, // Executable program + 0, + ); + + let extra_meta_5_pubkey = Pubkey::find_program_address( + &[ + b"prefix", + amount.to_le_bytes().as_ref(), // Instruction data 8..16 + extra_meta_2_pubkey.as_ref(), + ], + &extra_meta_4_pubkey, // PDA off of the arbitrary program ID + ) + .0; + let mut extra_meta_5_data = vec![]; // Mock + let mut extra_meta_5_lamports = 0; // Mock + let extra_meta_5_account_info = AccountInfo::new( + &extra_meta_5_pubkey, + false, + true, + &mut extra_meta_5_lamports, + &mut extra_meta_5_data, + &extra_meta_4_pubkey, + false, + 0, + ); + + let extra_meta_6_pubkey = Pubkey::find_program_address( + &[ + b"another_prefix", + amount.to_le_bytes().as_ref(), // Instruction data 8..16 + extra_meta_2_pubkey.as_ref(), + extra_meta_5_pubkey.as_ref(), + ], + &extra_meta_4_pubkey, // PDA off of the arbitrary program ID + ) + .0; + let mut extra_meta_6_data = vec![]; // Mock + let mut extra_meta_6_lamports = 0; // Mock + let extra_meta_6_account_info = AccountInfo::new( + &extra_meta_6_pubkey, + false, + true, + &mut extra_meta_6_lamports, + &mut extra_meta_6_data, + &extra_meta_4_pubkey, + false, + 0, + ); + + let mut validate_state_data = MOCK_EXTRA_METAS_STATE.to_vec(); + let mut validate_state_lamports = 0; // Mock + let validate_state_account_info = AccountInfo::new( + &validate_state_pubkey, + false, + true, + &mut validate_state_lamports, + &mut validate_state_data, + &transfer_hook_program_id, + false, + 0, + ); + + // First use the resolve function to add the extra account metas to the + // transfer instruction from offchain + let mut offchain_transfer_instruction = crate::instruction::transfer_checked( + &spl_token_2022_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + &[], + amount, + 9, + ) + .unwrap(); + + resolve_extra_transfer_account_metas( + &mut offchain_transfer_instruction, + mock_fetch_account_data_fn, + &mint_pubkey, + amount, + ) + .await + .unwrap(); + + // Then use the offchain function to add the extra account metas to the + // _execute_ instruction from offchain + let mut offchain_execute_instruction = spl_transfer_hook_interface::instruction::execute( + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + &validate_state_pubkey, + amount, + ); + + ExtraAccountMetaList::add_to_instruction::( + &mut offchain_execute_instruction, + mock_fetch_account_data_fn, + &MOCK_EXTRA_METAS_STATE, + ) + .await + .unwrap(); + + // Finally, use the onchain function to add the extra account metas to + // the _execute_ CPI instruction from onchain + let mut onchain_execute_cpi_instruction = spl_transfer_hook_interface::instruction::execute( + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + &validate_state_pubkey, + amount, + ); + let mut onchain_execute_cpi_account_infos = vec![ + source_account_info.clone(), + mint_account_info.clone(), + destination_account_info.clone(), + authority_account_info.clone(), + validate_state_account_info.clone(), + ]; + let all_account_infos = &[ + source_account_info.clone(), + mint_account_info.clone(), + destination_account_info.clone(), + authority_account_info.clone(), + validate_state_account_info.clone(), + extra_meta_1_account_info.clone(), + extra_meta_2_account_info.clone(), + extra_meta_3_account_info.clone(), + extra_meta_4_account_info.clone(), + extra_meta_5_account_info.clone(), + extra_meta_6_account_info.clone(), + ]; + + ExtraAccountMetaList::add_to_cpi_instruction::( + &mut onchain_execute_cpi_instruction, + &mut onchain_execute_cpi_account_infos, + &MOCK_EXTRA_METAS_STATE, + all_account_infos, + ) + .unwrap(); + + // The two `Execute` instructions should have the same accounts + assert_eq!( + offchain_execute_instruction.accounts, + onchain_execute_cpi_instruction.accounts, + ); + + // Still, the transfer instruction is going to be missing the + // the validation account at index 4 + assert_ne!( + offchain_transfer_instruction.accounts, + offchain_execute_instruction.accounts, + ); + assert_ne!( + offchain_transfer_instruction.accounts[4].pubkey, + validate_state_pubkey, + ); + + // Even though both execute instructions have the validation account + // at index 4 + assert_eq!( + offchain_execute_instruction.accounts[4].pubkey, + validate_state_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[4].pubkey, + validate_state_pubkey, + ); + + // The most important thing is verifying all PDAs are correct across + // all lists + // PDA 1 + assert_eq!( + offchain_transfer_instruction.accounts[4].pubkey, + extra_meta_1_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[5].pubkey, + extra_meta_1_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[5].pubkey, + extra_meta_1_pubkey, + ); + // PDA 2 + assert_eq!( + offchain_transfer_instruction.accounts[5].pubkey, + extra_meta_2_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[6].pubkey, + extra_meta_2_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[6].pubkey, + extra_meta_2_pubkey, + ); + // PDA 3 + assert_eq!( + offchain_transfer_instruction.accounts[6].pubkey, + extra_meta_3_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[7].pubkey, + extra_meta_3_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[7].pubkey, + extra_meta_3_pubkey, + ); + // PDA 4 + assert_eq!( + offchain_transfer_instruction.accounts[7].pubkey, + extra_meta_4_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[8].pubkey, + extra_meta_4_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[8].pubkey, + extra_meta_4_pubkey, + ); + // PDA 5 + assert_eq!( + offchain_transfer_instruction.accounts[8].pubkey, + extra_meta_5_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[9].pubkey, + extra_meta_5_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[9].pubkey, + extra_meta_5_pubkey, + ); + // PDA 6 + assert_eq!( + offchain_transfer_instruction.accounts[9].pubkey, + extra_meta_6_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[10].pubkey, + extra_meta_6_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[10].pubkey, + extra_meta_6_pubkey, + ); + } +} From 6b915a70e48e2a0ca11fa87be8b93e2df78c8bb2 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 8 Jan 2024 02:22:35 -0600 Subject: [PATCH 3/7] token-2022: refactor transfer hook program test --- .../program-2022-test/tests/transfer_hook.rs | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 06db5ed0a45..3ce1250c437 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -688,23 +688,20 @@ async fn success_transfers_using_onchain_helper() { let (source_b_account, destination_b_account) = setup_accounts(&token_b_context, Keypair::new(), Keypair::new(), amount).await; let authority_b = token_b_context.alice; - let account_metas = vec![ + + // Since we need to add extra account metas for our swap, which is a + // combination of two transfers, we need to resolve the extra metas + // for each transfer instruction. + let transfer_1_metas = vec![ AccountMeta::new(source_a_account, false), AccountMeta::new_readonly(mint_a, false), AccountMeta::new(destination_a_account, false), AccountMeta::new_readonly(authority_a.pubkey(), true), - AccountMeta::new_readonly(spl_token_2022::id(), false), - AccountMeta::new(source_b_account, false), - AccountMeta::new_readonly(mint_b, false), - AccountMeta::new(destination_b_account, false), - AccountMeta::new_readonly(authority_b.pubkey(), true), - AccountMeta::new_readonly(spl_token_2022::id(), false), ]; - - let mut instruction = Instruction::new_with_bytes(swap_program_id, &[], account_metas); - + let mut transfer_1_instruction = + Instruction::new_with_bytes(swap_program_id, &[], transfer_1_metas.clone()); offchain::resolve_extra_transfer_account_metas( - &mut instruction, + &mut transfer_1_instruction, |address| { token_a.get_account(address).map_ok_or_else( |e| match e { @@ -719,8 +716,17 @@ async fn success_transfers_using_onchain_helper() { ) .await .unwrap(); + + let transfer_2_metas = vec![ + AccountMeta::new(source_b_account, false), + AccountMeta::new_readonly(mint_b, false), + AccountMeta::new(destination_b_account, false), + AccountMeta::new_readonly(authority_b.pubkey(), true), + ]; + let mut transfer_2_instruction = + Instruction::new_with_bytes(swap_program_id, &[], transfer_2_metas.clone()); offchain::resolve_extra_transfer_account_metas( - &mut instruction, + &mut transfer_2_instruction, |address| { token_a.get_account(address).map_ok_or_else( |e| match e { @@ -736,8 +742,23 @@ async fn success_transfers_using_onchain_helper() { .await .unwrap(); + let mut swap_metas = vec![ + AccountMeta::new(source_a_account, false), + AccountMeta::new_readonly(mint_a, false), + AccountMeta::new(destination_a_account, false), + AccountMeta::new_readonly(authority_a.pubkey(), true), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(source_b_account, false), + AccountMeta::new_readonly(mint_b, false), + AccountMeta::new(destination_b_account, false), + AccountMeta::new_readonly(authority_b.pubkey(), true), + AccountMeta::new_readonly(spl_token_2022::id(), false), + ]; + swap_metas.extend_from_slice(&transfer_1_instruction.accounts[4..]); // Remaining accounts from transfer 1 + swap_metas.extend_from_slice(&transfer_2_instruction.accounts[4..]); // Remaining accounts from transfer 2 + let swap_instruction = Instruction::new_with_bytes(swap_program_id, &[], swap_metas); token_a - .process_ixs(&[instruction], &[&authority_a, &authority_b]) + .process_ixs(&[swap_instruction], &[&authority_a, &authority_b]) .await .unwrap(); } From ff029c396943171bca694ed85b6088a727e391c5 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 8 Jan 2024 12:40:12 -0600 Subject: [PATCH 4/7] token-2022: modify onchain extra metas helper --- token/program-2022/src/onchain.rs | 101 ++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/token/program-2022/src/onchain.rs b/token/program-2022/src/onchain.rs index a174b499aa8..3be93a15be2 100644 --- a/token/program-2022/src/onchain.rs +++ b/token/program-2022/src/onchain.rs @@ -3,19 +3,91 @@ use { crate::{ + error::TokenError, extension::{transfer_hook, StateWithExtensions}, instruction, state::Mint, }, solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, instruction::AccountMeta, - program::invoke_signed, pubkey::Pubkey, + account_info::AccountInfo, + entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction}, + msg, + program::invoke_signed, + program_error::ProgramError, + pubkey::Pubkey, + }, + spl_transfer_hook_interface::{ + error::TransferHookError, get_extra_account_metas_address, + onchain::add_cpi_accounts_for_execute, }, - spl_transfer_hook_interface::onchain::add_cpi_accounts_for_execute, }; +/// Onchain helper to get all additional required account metas for a checked +/// transfer +/// +/// Note that this onchain helper will build a new `Execute` instruction, +/// resolve the extra account metas, and then add them to the transfer +/// instruction. This is because the extra account metas are configured +/// specifically for the `Execute` instruction, which requires five accounts +/// (source, mint, destination, authority, and validation state), wheras the +/// transfer instruction only requires four (source, mint, destination, and +/// authority) in addition to `n` number of multisig authorities. +pub fn resolve_extra_transfer_account_metas_for_cpi<'a>( + cpi_instruction: &mut Instruction, + cpi_account_infos: &mut Vec>, + mint_info: &AccountInfo<'a>, + additional_accounts: &[AccountInfo<'a>], + amount: u64, +) -> Result<(), ProgramError> { + let mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + if let Some(program_id) = transfer_hook::get_program_id(&mint) { + // Convert the transfer instruction into an `Execute` instruction, + // then resolve the extra account metas as configured in the validation + // account data, then finally add the extra account metas to the original + // transfer instruction. + if cpi_instruction.accounts.len() < 4 { + msg!("Not a valid transfer instruction"); + Err(TokenError::InvalidInstruction)?; + } + + let validation_pubkey = get_extra_account_metas_address(mint_info.key, &program_id); + let validation_info = additional_accounts + .iter() + .find(|&x| *x.key == validation_pubkey) + .ok_or(TransferHookError::IncorrectAccount)?; + + let mut execute_ix = spl_transfer_hook_interface::instruction::execute( + &program_id, + &cpi_instruction.accounts[0].pubkey, + &cpi_instruction.accounts[1].pubkey, + &cpi_instruction.accounts[2].pubkey, + &cpi_instruction.accounts[3].pubkey, + &validation_pubkey, + amount, + ); + + cpi_account_infos.push(validation_info.clone()); + + add_cpi_accounts_for_execute( + &mut execute_ix, + cpi_account_infos, + mint_info.key, + &program_id, + additional_accounts, + )?; + + cpi_instruction + .accounts + .extend_from_slice(&execute_ix.accounts[5..]); + } + Ok(()) +} + /// Helper to CPI into token-2022 on-chain, looking through the additional -/// account infos to create the proper instruction with the proper account infos +/// account infos to create the proper instruction with the proper account +/// infos. #[allow(clippy::too_many_arguments)] pub fn invoke_transfer_checked<'a>( token_program_id: &Pubkey, @@ -57,20 +129,13 @@ pub fn invoke_transfer_checked<'a>( .push(AccountMeta::new_readonly(*ai.key, ai.is_signer)); }); - // scope the borrowing to avoid a double-borrow during CPI - { - let mint_data = mint_info.try_borrow_data()?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - if let Some(program_id) = transfer_hook::get_program_id(&mint) { - add_cpi_accounts_for_execute( - &mut cpi_instruction, - &mut cpi_account_infos, - mint_info.key, - &program_id, - additional_accounts, - )?; - } - } + resolve_extra_transfer_account_metas_for_cpi( + &mut cpi_instruction, + &mut cpi_account_infos, + &mint_info, + additional_accounts, + amount, + )?; invoke_signed(&cpi_instruction, &cpi_account_infos, seeds) } From 85359e1a56c60a5c8113d47120d442bd137490be Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 8 Jan 2024 12:41:35 -0600 Subject: [PATCH 5/7] token-2022: add test for the modified onchain extra metas helper --- token/program-2022/src/onchain.rs | 516 ++++++++++++++++++++++++++++++ 1 file changed, 516 insertions(+) diff --git a/token/program-2022/src/onchain.rs b/token/program-2022/src/onchain.rs index 3be93a15be2..f995dbed584 100644 --- a/token/program-2022/src/onchain.rs +++ b/token/program-2022/src/onchain.rs @@ -139,3 +139,519 @@ pub fn invoke_transfer_checked<'a>( invoke_signed(&cpi_instruction, &cpi_account_infos, seeds) } + +#[cfg(test)] +mod tests { + use { + super::*, + solana_program::system_program, + solana_program_test::tokio, + spl_tlv_account_resolution::state::{AccountDataResult, ExtraAccountMetaList}, + spl_transfer_hook_interface::instruction::ExecuteInstruction, + }; + + const TRANSFER_HOOK_PROGRAM_ID: Pubkey = Pubkey::new_from_array([1; 32]); + + const MINT_PUBKEY: Pubkey = Pubkey::new_from_array([2; 32]); + + const MOCK_MINT_STATE: [u8; 234] = [ + 0, 0, 0, 0, // COption (4): None = 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Mint authority (32) + 0, 0, 0, 0, 0, 0, 0, 0, // Supply (8) + 0, // Decimals (1) + 1, // Is initialized (1) + 0, 0, 0, 0, // COption (4): None = 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Freeze authority (32) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Padding (83) + 1, // Account type (1): Mint = 1 + 14, 0, // Extension type (2): Transfer hook = 14 + 64, 0, // Extension length (2): 64 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Authority (32) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, // Transfer hook program ID (32) + ]; + + const MOCK_EXTRA_METAS_STATE: [u8; 226] = [ + 105, 37, 101, 197, 75, 251, 102, 26, // Discriminator for `ExecuteInstruction` (8) + 214, 0, 0, 0, // Length of pod slice (4): 214 + 6, 0, 0, 0, // Count of account metas (4): 6 + 1, // First account meta discriminator (1): PDA = 1 + 3, 0, // First seed: Account key at index 0 (2) + 3, 1, // Second seed: Account key at index 1 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // No more seeds (28) + 0, // First account meta is signer (1): false = 0 + 0, // First account meta is writable (1): false = 0 + 1, // Second account meta discriminator (1): PDA = 1 + 3, 4, // First seed: Account key at index 4 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // No more seeds (30) + 0, // Second account meta is signer (1): false = 0 + 0, // Second account meta is writable (1): false = 0 + 1, // Third account meta discriminator (1): PDA = 1 + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (21) + 0, // Third account meta is signer (1): false = 0 + 0, // Third account meta is writable (1): false = 0 + 0, // Fourth account meta discriminator (1): Pubkey = 0 + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, // Pubkey (32) + 0, // Fourth account meta is signer (1): false = 0 + 0, // Fourth account meta is writable (1): false = 0 + 136, // Fifth account meta discriminator (1): External PDA = 128 + index 8 = 136 + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (19) + 0, // Fifth account meta is signer (1): false = 0 + 0, // Fifth account meta is writable (1): false = 0 + 136, // Sixth account meta discriminator (1): External PDA = 128 + index 8 = 136 + 1, 14, 97, 110, 111, 116, 104, 101, 114, 95, 112, 114, 101, 102, 105, + 120, // First seed: Literal "another_prefix" (16) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + 3, 9, // Fourth seed: Account key at index 9 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (9) + 0, // Sixth account meta is signer (1): false = 0 + 0, // Sixth account meta is writable (1): false = 0 + ]; + + async fn mock_fetch_account_data_fn(address: Pubkey) -> AccountDataResult { + if address == MINT_PUBKEY { + Ok(Some(MOCK_MINT_STATE.to_vec())) + } else if address + == get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID) + { + Ok(Some(MOCK_EXTRA_METAS_STATE.to_vec())) + } else { + Ok(None) + } + } + + #[tokio::test] + async fn test_resolve_extra_transfer_account_metas_for_cpi() { + let spl_token_2022_program_id = crate::id(); + let transfer_hook_program_id = TRANSFER_HOOK_PROGRAM_ID; + let amount = 2u64; + + let source_pubkey = Pubkey::new_unique(); + let mut source_data = vec![0; 165]; // Mock + let mut source_lamports = 0; // Mock + let source_account_info = AccountInfo::new( + &source_pubkey, + false, + true, + &mut source_lamports, + &mut source_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let mint_pubkey = MINT_PUBKEY; + let mut mint_data = MOCK_MINT_STATE.to_vec(); + let mut mint_lamports = 0; // Mock + let mint_account_info = AccountInfo::new( + &mint_pubkey, + false, + true, + &mut mint_lamports, + &mut mint_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let destination_pubkey = Pubkey::new_unique(); + let mut destination_data = vec![0; 165]; // Mock + let mut destination_lamports = 0; // Mock + let destination_account_info = AccountInfo::new( + &destination_pubkey, + false, + true, + &mut destination_lamports, + &mut destination_data, + &spl_token_2022_program_id, + false, + 0, + ); + + let authority_pubkey = Pubkey::new_unique(); + let mut authority_data = vec![]; // Mock + let mut authority_lamports = 0; // Mock + let authority_account_info = AccountInfo::new( + &authority_pubkey, + false, + true, + &mut authority_lamports, + &mut authority_data, + &system_program::ID, + false, + 0, + ); + + let validate_state_pubkey = + get_extra_account_metas_address(&mint_pubkey, &transfer_hook_program_id); + + let extra_meta_1_pubkey = Pubkey::find_program_address( + &[ + &source_pubkey.to_bytes(), // Account key at index 0 + &mint_pubkey.to_bytes(), // Account key at index 1 + ], + &transfer_hook_program_id, + ) + .0; + let mut extra_meta_1_data = vec![]; // Mock + let mut extra_meta_1_lamports = 0; // Mock + let extra_meta_1_account_info = AccountInfo::new( + &extra_meta_1_pubkey, + false, + true, + &mut extra_meta_1_lamports, + &mut extra_meta_1_data, + &transfer_hook_program_id, + false, + 0, + ); + + let extra_meta_2_pubkey = Pubkey::find_program_address( + &[ + &validate_state_pubkey.to_bytes(), // Account key at index 4 + ], + &transfer_hook_program_id, + ) + .0; + let mut extra_meta_2_data = vec![]; // Mock + let mut extra_meta_2_lamports = 0; // Mock + let extra_meta_2_account_info = AccountInfo::new( + &extra_meta_2_pubkey, + false, + true, + &mut extra_meta_2_lamports, + &mut extra_meta_2_data, + &transfer_hook_program_id, + false, + 0, + ); + + let extra_meta_3_pubkey = Pubkey::find_program_address( + &[ + b"prefix", + amount.to_le_bytes().as_ref(), // Instruction data 8..16 + ], + &transfer_hook_program_id, + ) + .0; + let mut extra_meta_3_data = vec![]; // Mock + let mut extra_meta_3_lamports = 0; // Mock + let extra_meta_3_account_info = AccountInfo::new( + &extra_meta_3_pubkey, + false, + true, + &mut extra_meta_3_lamports, + &mut extra_meta_3_data, + &transfer_hook_program_id, + false, + 0, + ); + + let extra_meta_4_pubkey = Pubkey::new_from_array([7; 32]); // Some arbitrary program ID + let mut extra_meta_4_data = vec![]; // Mock + let mut extra_meta_4_lamports = 0; // Mock + let extra_meta_4_account_info = AccountInfo::new( + &extra_meta_4_pubkey, + false, + true, + &mut extra_meta_4_lamports, + &mut extra_meta_4_data, + &transfer_hook_program_id, + true, // Executable program + 0, + ); + + let extra_meta_5_pubkey = Pubkey::find_program_address( + &[ + b"prefix", + amount.to_le_bytes().as_ref(), // Instruction data 8..16 + extra_meta_2_pubkey.as_ref(), + ], + &extra_meta_4_pubkey, // PDA off of the arbitrary program ID + ) + .0; + let mut extra_meta_5_data = vec![]; // Mock + let mut extra_meta_5_lamports = 0; // Mock + let extra_meta_5_account_info = AccountInfo::new( + &extra_meta_5_pubkey, + false, + true, + &mut extra_meta_5_lamports, + &mut extra_meta_5_data, + &extra_meta_4_pubkey, + false, + 0, + ); + + let extra_meta_6_pubkey = Pubkey::find_program_address( + &[ + b"another_prefix", + amount.to_le_bytes().as_ref(), // Instruction data 8..16 + extra_meta_2_pubkey.as_ref(), + extra_meta_5_pubkey.as_ref(), + ], + &extra_meta_4_pubkey, // PDA off of the arbitrary program ID + ) + .0; + let mut extra_meta_6_data = vec![]; // Mock + let mut extra_meta_6_lamports = 0; // Mock + let extra_meta_6_account_info = AccountInfo::new( + &extra_meta_6_pubkey, + false, + true, + &mut extra_meta_6_lamports, + &mut extra_meta_6_data, + &extra_meta_4_pubkey, + false, + 0, + ); + + let mut validate_state_data = MOCK_EXTRA_METAS_STATE.to_vec(); + let mut validate_state_lamports = 0; // Mock + let validate_state_account_info = AccountInfo::new( + &validate_state_pubkey, + false, + true, + &mut validate_state_lamports, + &mut validate_state_data, + &transfer_hook_program_id, + false, + 0, + ); + + let mut transfer_hook_program_data = vec![]; // Mock + let mut transfer_hook_program_lamports = 0; // Mock + let transfer_hook_program_info = AccountInfo::new( + &transfer_hook_program_id, + false, + true, + &mut transfer_hook_program_lamports, + &mut transfer_hook_program_data, + &system_program::ID, + true, // Executable program + 0, + ); + + // First use the resolve function to add the extra account metas to the + // transfer instruction from onchain + let mut onchain_transfer_cpi_instruction = crate::instruction::transfer_checked( + &spl_token_2022_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + &[], + amount, + 9, + ) + .unwrap(); + let mut onchain_transfer_cpi_account_infos = vec![ + source_account_info.clone(), + mint_account_info.clone(), + destination_account_info.clone(), + authority_account_info.clone(), + ]; + let onchain_transfer_additional_account_infos = vec![ + extra_meta_1_account_info.clone(), + extra_meta_2_account_info.clone(), + extra_meta_3_account_info.clone(), + extra_meta_4_account_info.clone(), + extra_meta_5_account_info.clone(), + extra_meta_6_account_info.clone(), + validate_state_account_info.clone(), + transfer_hook_program_info.clone(), + ]; + + resolve_extra_transfer_account_metas_for_cpi( + &mut onchain_transfer_cpi_instruction, + &mut onchain_transfer_cpi_account_infos, + &mint_account_info, + &onchain_transfer_additional_account_infos, + amount, + ) + .unwrap(); + + // Then use the offchain function to add the extra account metas to the + // _execute_ instruction from offchain + let mut offchain_execute_instruction = spl_transfer_hook_interface::instruction::execute( + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + &validate_state_pubkey, + amount, + ); + + ExtraAccountMetaList::add_to_instruction::( + &mut offchain_execute_instruction, + mock_fetch_account_data_fn, + &MOCK_EXTRA_METAS_STATE, + ) + .await + .unwrap(); + + // Finally, use the onchain function to add the extra account metas to + // the _execute_ CPI instruction from onchain + let mut onchain_execute_cpi_instruction = spl_transfer_hook_interface::instruction::execute( + &transfer_hook_program_id, + &source_pubkey, + &mint_pubkey, + &destination_pubkey, + &authority_pubkey, + &validate_state_pubkey, + amount, + ); + let mut onchain_execute_cpi_account_infos = vec![ + source_account_info.clone(), + mint_account_info.clone(), + destination_account_info.clone(), + authority_account_info.clone(), + validate_state_account_info.clone(), + ]; + let all_account_infos = &[ + source_account_info.clone(), + mint_account_info.clone(), + destination_account_info.clone(), + authority_account_info.clone(), + validate_state_account_info.clone(), + extra_meta_1_account_info.clone(), + extra_meta_2_account_info.clone(), + extra_meta_3_account_info.clone(), + extra_meta_4_account_info.clone(), + extra_meta_5_account_info.clone(), + extra_meta_6_account_info.clone(), + ]; + + ExtraAccountMetaList::add_to_cpi_instruction::( + &mut onchain_execute_cpi_instruction, + &mut onchain_execute_cpi_account_infos, + &MOCK_EXTRA_METAS_STATE, + all_account_infos, + ) + .unwrap(); + + // The two `Execute` instructions should have the same accounts + assert_eq!( + offchain_execute_instruction.accounts, + onchain_execute_cpi_instruction.accounts, + ); + + // Still, the transfer instruction is going to be missing the + // the validation account at index 4 + assert_ne!( + onchain_transfer_cpi_instruction.accounts, + offchain_execute_instruction.accounts, + ); + assert_ne!( + onchain_transfer_cpi_instruction.accounts[4].pubkey, + validate_state_pubkey, + ); + + // Even though both execute instructions have the validation account + // at index 4 + assert_eq!( + offchain_execute_instruction.accounts[4].pubkey, + validate_state_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[4].pubkey, + validate_state_pubkey, + ); + + // The most important thing is verifying all PDAs are correct across + // all lists + // PDA 1 + assert_eq!( + onchain_transfer_cpi_instruction.accounts[4].pubkey, + extra_meta_1_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[5].pubkey, + extra_meta_1_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[5].pubkey, + extra_meta_1_pubkey, + ); + // PDA 2 + assert_eq!( + onchain_transfer_cpi_instruction.accounts[5].pubkey, + extra_meta_2_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[6].pubkey, + extra_meta_2_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[6].pubkey, + extra_meta_2_pubkey, + ); + // PDA 3 + assert_eq!( + onchain_transfer_cpi_instruction.accounts[6].pubkey, + extra_meta_3_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[7].pubkey, + extra_meta_3_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[7].pubkey, + extra_meta_3_pubkey, + ); + // PDA 4 + assert_eq!( + onchain_transfer_cpi_instruction.accounts[7].pubkey, + extra_meta_4_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[8].pubkey, + extra_meta_4_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[8].pubkey, + extra_meta_4_pubkey, + ); + // PDA 5 + assert_eq!( + onchain_transfer_cpi_instruction.accounts[8].pubkey, + extra_meta_5_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[9].pubkey, + extra_meta_5_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[9].pubkey, + extra_meta_5_pubkey, + ); + // PDA 6 + assert_eq!( + onchain_transfer_cpi_instruction.accounts[9].pubkey, + extra_meta_6_pubkey, + ); + assert_eq!( + offchain_execute_instruction.accounts[10].pubkey, + extra_meta_6_pubkey, + ); + assert_eq!( + onchain_execute_cpi_instruction.accounts[10].pubkey, + extra_meta_6_pubkey, + ); + } +} From 87e6525875f33455a3246faf9fdaceb657e27134 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 5 Jan 2024 18:25:29 -0600 Subject: [PATCH 6/7] token js: modify offchain extra metas helper --- .../extensions/transferHook/instructions.ts | 102 ++++++++++++++---- token/js/src/extensions/transferHook/state.ts | 2 +- token/js/test/unit/transferHook.test.ts | 6 +- 3 files changed, 84 insertions(+), 26 deletions(-) diff --git a/token/js/src/extensions/transferHook/instructions.ts b/token/js/src/extensions/transferHook/instructions.ts index 9484a3053c0..3738659dd2f 100644 --- a/token/js/src/extensions/transferHook/instructions.ts +++ b/token/js/src/extensions/transferHook/instructions.ts @@ -9,7 +9,12 @@ import { publicKey } from '@solana/buffer-layout-utils'; import { createTransferCheckedInstruction } from '../../instructions/transferChecked.js'; import { createTransferCheckedWithFeeInstruction } from '../transferFee/instructions.js'; import { getMint } from '../../state/mint.js'; -import { getExtraAccountMetaAddress, getExtraAccountMetas, getTransferHook, resolveExtraAccountMeta } from './state.js'; +import { + getExtraAccountMetaAddress, + getExtraAccountMetaList, + getTransferHook, + resolveExtraAccountMeta, +} from './state.js'; export enum TransferHookInstruction { Initialize = 0, @@ -136,58 +141,109 @@ function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMe return accountMeta; } +function createExecuteInstructionFromTransfer( + transferInstruction: TransactionInstruction, + validateStatePubkey: PublicKey, + transferHookProgramId: PublicKey, + amount: bigint +): TransactionInstruction { + if (transferInstruction.keys.length < 4) { + throw new Error('Not a valid transfer instruction'); + } + + const keys = [ + transferInstruction.keys[0].pubkey, + transferInstruction.keys[1].pubkey, + transferInstruction.keys[2].pubkey, + transferInstruction.keys[3].pubkey, + validateStatePubkey, + ].map((pubkey) => ({ + pubkey, + isSigner: false, + isWritable: false, + })); + + const programId = transferHookProgramId; + + const data = Buffer.alloc(16); + data.set(Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]), 0); // `Execute` discriminator + data.writeBigUInt64LE(amount, 8); + + return new TransactionInstruction({ keys, programId, data }); +} + /** * Add extra accounts needed for transfer hook to an instruction * + * Note that this offchain helper will build a new `Execute` instruction, + * resolve the extra account metas, and then add them to the transfer + * instruction. This is because the extra account metas are configured + * specifically for the `Execute` instruction, which requires five accounts + * (source, mint, destination, authority, and validation state), wheras the + * transfer instruction only requires four (source, mint, destination, and + * authority) in addition to `n` number of multisig authorities. + * * @param connection Connection to use * @param instruction The transferChecked instruction to add accounts to + * @param mint Mint being transferred + * @param amount Amount being transferred * @param commitment Commitment to use * @param programId SPL Token program account * * @return Instruction to add to a transaction */ -export async function addExtraAccountsToInstruction( +export async function addExtraAccountsToTransferInstruction( connection: Connection, instruction: TransactionInstruction, mint: PublicKey, + amount: bigint, commitment?: Commitment, - programId = TOKEN_PROGRAM_ID + tokenProgramId = TOKEN_PROGRAM_ID ): Promise { - if (!programSupportsExtensions(programId)) { + if (!programSupportsExtensions(tokenProgramId)) { throw new TokenUnsupportedInstructionError(); } - const mintInfo = await getMint(connection, mint, commitment, programId); + const mintInfo = await getMint(connection, mint, commitment, tokenProgramId); const transferHook = getTransferHook(mintInfo); if (transferHook == null) { return instruction; } - const extraAccountsAccount = getExtraAccountMetaAddress(mint, transferHook.programId); - const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, commitment); - if (extraAccountsInfo == null) { + // Convert the transfer instruction into an `Execute` instruction, + // then resolve the extra account metas as configured in the validation + // account data, then finally add the extra account metas to the original + // transfer instruction. + const validateStatePubkey = getExtraAccountMetaAddress(mint, transferHook.programId); + const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment); + if (validateStateAccount == null) { return instruction; } - const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo); - - const accountMetas = instruction.keys; + const executeIx = createExecuteInstructionFromTransfer( + instruction, + validateStatePubkey, + transferHook.programId, + amount + ); - for (const extraAccountMeta of extraAccountMetas) { + for (const extraAccountMeta of getExtraAccountMetaList(validateStateAccount)) { const accountMetaUnchecked = await resolveExtraAccountMeta( connection, extraAccountMeta, - accountMetas, - instruction.data, - transferHook.programId + executeIx.keys, + executeIx.data, + executeIx.programId ); - const accountMeta = deEscalateAccountMeta(accountMetaUnchecked, accountMetas); - accountMetas.push(accountMeta); + const accountMeta = deEscalateAccountMeta(accountMetaUnchecked, executeIx.keys); + executeIx.keys.push(accountMeta); } - accountMetas.push({ pubkey: transferHook.programId, isSigner: false, isWritable: false }); - accountMetas.push({ pubkey: extraAccountsAccount, isSigner: false, isWritable: false }); + executeIx.keys.push({ pubkey: transferHook.programId, isSigner: false, isWritable: false }); + executeIx.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false }); + + instruction.keys.push(...executeIx.keys.slice(5)); - return new TransactionInstruction({ keys: accountMetas, programId, data: instruction.data }); + return instruction; } /** @@ -229,10 +285,11 @@ export async function createTransferCheckedWithTransferHookInstruction( programId ); - const hydratedInstruction = await addExtraAccountsToInstruction( + const hydratedInstruction = await addExtraAccountsToTransferInstruction( connection, rawInstruction, mint, + amount, commitment, programId ); @@ -282,10 +339,11 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction( programId ); - const hydratedInstruction = await addExtraAccountsToInstruction( + const hydratedInstruction = await addExtraAccountsToTransferInstruction( connection, rawInstruction, mint, + amount, commitment, programId ); diff --git a/token/js/src/extensions/transferHook/state.ts b/token/js/src/extensions/transferHook/state.ts index 748d326c8fe..50cd0401823 100644 --- a/token/js/src/extensions/transferHook/state.ts +++ b/token/js/src/extensions/transferHook/state.ts @@ -100,7 +100,7 @@ export const ExtraAccountMetaAccountDataLayout = struct): ExtraAccountMeta[] { +export function getExtraAccountMetaList(account: AccountInfo): ExtraAccountMeta[] { const extraAccountsList = ExtraAccountMetaAccountDataLayout.decode(account.data).extraAccountsList; return extraAccountsList.extraAccounts.slice(0, extraAccountsList.count); } diff --git a/token/js/test/unit/transferHook.test.ts b/token/js/test/unit/transferHook.test.ts index f24c24747c1..6c91c9375f2 100644 --- a/token/js/test/unit/transferHook.test.ts +++ b/token/js/test/unit/transferHook.test.ts @@ -1,4 +1,4 @@ -import { getExtraAccountMetas, resolveExtraAccountMeta } from '../../src'; +import { getExtraAccountMetaList, resolveExtraAccountMeta } from '../../src'; import { expect } from 'chai'; import type { Connection } from '@solana/web3.js'; import { PublicKey } from '@solana/web3.js'; @@ -100,14 +100,14 @@ describe('transferHookExtraAccounts', () => { }); }); - it('getExtraAccountMetas', () => { + it('getExtraAccountMetaList', () => { const accountInfo = { data: extraAccountList, owner: PublicKey.default, executable: false, lamports: 0, }; - const parsedExtraAccounts = getExtraAccountMetas(accountInfo); + const parsedExtraAccounts = getExtraAccountMetaList(accountInfo); expect(parsedExtraAccounts).to.not.be.null; if (parsedExtraAccounts == null) { return; From c35964e8f5d27b1b09123f45eadfc32e242bfa96 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 5 Jan 2024 18:26:56 -0600 Subject: [PATCH 7/7] token js: add test for the modified offchain extra metas helper --- token/js/test/unit/transferHook.test.ts | 528 +++++++++++++++++------- 1 file changed, 369 insertions(+), 159 deletions(-) diff --git a/token/js/test/unit/transferHook.test.ts b/token/js/test/unit/transferHook.test.ts index 6c91c9375f2..2f055bdb73e 100644 --- a/token/js/test/unit/transferHook.test.ts +++ b/token/js/test/unit/transferHook.test.ts @@ -1,173 +1,383 @@ -import { getExtraAccountMetaList, resolveExtraAccountMeta } from '../../src'; +import { + addExtraAccountsToTransferInstruction, + createTransferCheckedInstruction, + getExtraAccountMetaAddress, + getExtraAccountMetaList, + resolveExtraAccountMeta, + TOKEN_2022_PROGRAM_ID, +} from '../../src'; import { expect } from 'chai'; import type { Connection } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; +import { Keypair, PublicKey } from '@solana/web3.js'; import { getConnection } from '../common'; -describe('transferHookExtraAccounts', () => { - let connection: Connection; - const testProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj'); - const instructionData = Buffer.from(Array.from(Array(32).keys())); - const plainAccount = new PublicKey('6c5q79ccBTWvZTEx3JkdHThtMa2eALba5bfvHGf8kA2c'); - const seeds = [Buffer.from('seed'), Buffer.from([4, 5, 6, 7]), plainAccount.toBuffer(), Buffer.from([2, 2, 2, 2])]; - const pdaPublicKey = PublicKey.findProgramAddressSync(seeds, testProgramId)[0]; - const pdaPublicKeyWithProgramId = PublicKey.findProgramAddressSync(seeds, plainAccount)[0]; - - const plainSeed = Buffer.concat([ - Buffer.from([1]), // u8 discriminator - Buffer.from([4]), // u8 length - Buffer.from('seed'), // 4 bytes seed - ]); - - const instructionDataSeed = Buffer.concat([ - Buffer.from([2]), // u8 discriminator - Buffer.from([4]), // u8 offset - Buffer.from([4]), // u8 length - ]); - - const accountKeySeed = Buffer.concat([ - Buffer.from([3]), // u8 discriminator - Buffer.from([0]), // u8 index - ]); - - const accountDataSeed = Buffer.concat([ - Buffer.from([4]), // u8 discriminator - Buffer.from([0]), // u8 account index - Buffer.from([2]), // u8 account data offset - Buffer.from([4]), // u8 account data length - ]); - - const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed, accountDataSeed], 32); - - const plainExtraAccountMeta = { - discriminator: 0, - addressConfig: plainAccount.toBuffer(), - isSigner: false, - isWritable: false, - }; - const plainExtraAccount = Buffer.concat([ - Buffer.from([0]), // u8 discriminator - plainAccount.toBuffer(), // 32 bytes address - Buffer.from([0]), // bool isSigner - Buffer.from([0]), // bool isWritable - ]); - - const pdaExtraAccountMeta = { - discriminator: 1, - addressConfig, - isSigner: true, - isWritable: false, - }; - const pdaExtraAccount = Buffer.concat([ - Buffer.from([1]), // u8 discriminator - addressConfig, // 32 bytes address config - Buffer.from([1]), // bool isSigner - Buffer.from([0]), // bool isWritable - ]); - - const pdaExtraAccountMetaWithProgramId = { - discriminator: 128, - addressConfig, - isSigner: false, - isWritable: true, - }; - const pdaExtraAccountWithProgramId = Buffer.concat([ - Buffer.from([128]), // u8 discriminator - addressConfig, // 32 bytes address config - Buffer.from([0]), // bool isSigner - Buffer.from([1]), // bool isWritable - ]); - - const extraAccountList = Buffer.concat([ - Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), // u64 accountDiscriminator - Buffer.from([0, 0, 0, 0]), // u32 length - Buffer.from([3, 0, 0, 0]), // u32 count - plainExtraAccount, - pdaExtraAccount, - pdaExtraAccountWithProgramId, - ]); - - before(async () => { - connection = await getConnection(); - connection.getAccountInfo = async ( - _publicKey: PublicKey, - _commitmentOrConfig?: Parameters<(typeof connection)['getAccountInfo']>[1] - ): ReturnType<(typeof connection)['getAccountInfo']> => ({ - data: Buffer.from([0, 0, 2, 2, 2, 2]), - owner: PublicKey.default, - executable: false, - lamports: 0, +describe('transferHook', () => { + describe('validation data', () => { + let connection: Connection; + const testProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj'); + const instructionData = Buffer.from(Array.from(Array(32).keys())); + const plainAccount = new PublicKey('6c5q79ccBTWvZTEx3JkdHThtMa2eALba5bfvHGf8kA2c'); + const seeds = [ + Buffer.from('seed'), + Buffer.from([4, 5, 6, 7]), + plainAccount.toBuffer(), + Buffer.from([2, 2, 2, 2]), + ]; + const pdaPublicKey = PublicKey.findProgramAddressSync(seeds, testProgramId)[0]; + const pdaPublicKeyWithProgramId = PublicKey.findProgramAddressSync(seeds, plainAccount)[0]; + + const plainSeed = Buffer.concat([ + Buffer.from([1]), // u8 discriminator + Buffer.from([4]), // u8 length + Buffer.from('seed'), // 4 bytes seed + ]); + + const instructionDataSeed = Buffer.concat([ + Buffer.from([2]), // u8 discriminator + Buffer.from([4]), // u8 offset + Buffer.from([4]), // u8 length + ]); + + const accountKeySeed = Buffer.concat([ + Buffer.from([3]), // u8 discriminator + Buffer.from([0]), // u8 index + ]); + + const accountDataSeed = Buffer.concat([ + Buffer.from([4]), // u8 discriminator + Buffer.from([0]), // u8 account index + Buffer.from([2]), // u8 account data offset + Buffer.from([4]), // u8 account data length + ]); + + const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed, accountDataSeed], 32); + + const plainExtraAccountMeta = { + discriminator: 0, + addressConfig: plainAccount.toBuffer(), + isSigner: false, + isWritable: false, + }; + const plainExtraAccount = Buffer.concat([ + Buffer.from([0]), // u8 discriminator + plainAccount.toBuffer(), // 32 bytes address + Buffer.from([0]), // bool isSigner + Buffer.from([0]), // bool isWritable + ]); + + const pdaExtraAccountMeta = { + discriminator: 1, + addressConfig, + isSigner: true, + isWritable: false, + }; + const pdaExtraAccount = Buffer.concat([ + Buffer.from([1]), // u8 discriminator + addressConfig, // 32 bytes address config + Buffer.from([1]), // bool isSigner + Buffer.from([0]), // bool isWritable + ]); + + const pdaExtraAccountMetaWithProgramId = { + discriminator: 128, + addressConfig, + isSigner: false, + isWritable: true, + }; + const pdaExtraAccountWithProgramId = Buffer.concat([ + Buffer.from([128]), // u8 discriminator + addressConfig, // 32 bytes address config + Buffer.from([0]), // bool isSigner + Buffer.from([1]), // bool isWritable + ]); + + const extraAccountList = Buffer.concat([ + Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), // u64 accountDiscriminator + Buffer.from([109, 0, 0, 0]), // u32 length (35 * 3 + 4) + Buffer.from([3, 0, 0, 0]), // u32 count + plainExtraAccount, + pdaExtraAccount, + pdaExtraAccountWithProgramId, + ]); + + before(async () => { + connection = await getConnection(); + connection.getAccountInfo = async ( + _publicKey: PublicKey, + _commitmentOrConfig?: Parameters<(typeof connection)['getAccountInfo']>[1] + ): ReturnType<(typeof connection)['getAccountInfo']> => ({ + data: Buffer.from([0, 0, 2, 2, 2, 2]), + owner: PublicKey.default, + executable: false, + lamports: 0, + }); + }); + + it('can parse extra metas', () => { + const accountInfo = { + data: extraAccountList, + owner: PublicKey.default, + executable: false, + lamports: 0, + }; + const parsedExtraAccounts = getExtraAccountMetaList(accountInfo); + expect(parsedExtraAccounts).to.not.be.null; + if (parsedExtraAccounts == null) { + return; + } + + expect(parsedExtraAccounts).to.have.length(3); + if (parsedExtraAccounts.length !== 3) { + return; + } + + expect(parsedExtraAccounts[0].discriminator).to.eql(0); + expect(parsedExtraAccounts[0].addressConfig).to.eql(plainAccount.toBuffer()); + expect(parsedExtraAccounts[0].isSigner).to.be.false; + expect(parsedExtraAccounts[0].isWritable).to.be.false; + + expect(parsedExtraAccounts[1].discriminator).to.eql(1); + expect(parsedExtraAccounts[1].addressConfig).to.eql(addressConfig); + expect(parsedExtraAccounts[1].isSigner).to.be.true; + expect(parsedExtraAccounts[1].isWritable).to.be.false; + + expect(parsedExtraAccounts[2].discriminator).to.eql(128); + expect(parsedExtraAccounts[2].addressConfig).to.eql(addressConfig); + expect(parsedExtraAccounts[2].isSigner).to.be.false; + expect(parsedExtraAccounts[2].isWritable).to.be.true; + }); + + it('can resolve extra metas', async () => { + const resolvedPlainAccount = await resolveExtraAccountMeta( + connection, + plainExtraAccountMeta, + [], + instructionData, + testProgramId + ); + + expect(resolvedPlainAccount.pubkey).to.eql(plainAccount); + expect(resolvedPlainAccount.isSigner).to.be.false; + expect(resolvedPlainAccount.isWritable).to.be.false; + + const resolvedPdaAccount = await resolveExtraAccountMeta( + connection, + pdaExtraAccountMeta, + [resolvedPlainAccount], + instructionData, + testProgramId + ); + + expect(resolvedPdaAccount.pubkey).to.eql(pdaPublicKey); + expect(resolvedPdaAccount.isSigner).to.be.true; + expect(resolvedPdaAccount.isWritable).to.be.false; + + const resolvedPdaAccountWithProgramId = await resolveExtraAccountMeta( + connection, + pdaExtraAccountMetaWithProgramId, + [resolvedPlainAccount], + instructionData, + testProgramId + ); + + expect(resolvedPdaAccountWithProgramId.pubkey).to.eql(pdaPublicKeyWithProgramId); + expect(resolvedPdaAccountWithProgramId.isSigner).to.be.false; + expect(resolvedPdaAccountWithProgramId.isWritable).to.be.true; }); }); - it('getExtraAccountMetaList', () => { - const accountInfo = { - data: extraAccountList, - owner: PublicKey.default, - executable: false, - lamports: 0, - }; - const parsedExtraAccounts = getExtraAccountMetaList(accountInfo); - expect(parsedExtraAccounts).to.not.be.null; - if (parsedExtraAccounts == null) { - return; - } + // prettier-ignore + describe('adding to transfer instructions', () => { + const TRANSFER_HOOK_PROGRAM_ID = new PublicKey(Buffer.from([ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ])) + + const MINT_PUBKEY = new PublicKey(Buffer.from([ + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + ])) + + const MOCK_MINT_STATE = [ + 0, 0, 0, 0, // COption (4): None = 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Mint authority (32) + 0, 0, 0, 0, 0, 0, 0, 0, // Supply (8) + 0, // Decimals (1) + 1, // Is initialized (1) + 0, 0, 0, 0, // COption (4): None = 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Freeze authority (32) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Padding (83) + 1, // Account type (1): Mint = 1 + 14, 0, // Extension type (2): Transfer hook = 14 + 64, 0, // Extension length (2): 64 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, // Authority (32) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, // Transfer hook program ID (32) + ]; - expect(parsedExtraAccounts).to.have.length(3); - if (parsedExtraAccounts.length !== 3) { - return; + const MOCK_EXTRA_METAS_STATE = [ + 105, 37, 101, 197, 75, 251, 102, 26, // Discriminator for `ExecuteInstruction` (8) + 214, 0, 0, 0, // Length of pod slice (4): 214 + 6, 0, 0, 0, // Count of account metas (4): 6 + 1, // First account meta discriminator (1): PDA = 1 + 3, 0, // First seed: Account key at index 0 (2) + 3, 1, // Second seed: Account key at index 1 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // No more seeds (28) + 0, // First account meta is signer (1): false = 0 + 0, // First account meta is writable (1): false = 0 + 1, // Second account meta discriminator (1): PDA = 1 + 3, 4, // First seed: Account key at index 4 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // No more seeds (30) + 0, // Second account meta is signer (1): false = 0 + 0, // Second account meta is writable (1): false = 0 + 1, // Third account meta discriminator (1): PDA = 1 + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (21) + 0, // Third account meta is signer (1): false = 0 + 0, // Third account meta is writable (1): false = 0 + 0, // Fourth account meta discriminator (1): Pubkey = 0 + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, // Pubkey (32) + 0, // Fourth account meta is signer (1): false = 0 + 0, // Fourth account meta is writable (1): false = 0 + 136, // Fifth account meta discriminator (1): External PDA = 128 + index 8 = 136 + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (19) + 0, // Fifth account meta is signer (1): false = 0 + 0, // Fifth account meta is writable (1): false = 0 + 136, // Sixth account meta discriminator (1): External PDA = 128 + index 8 = 136 + 1, 14, 97, 110, 111, 116, 104, 101, 114, 95, 112, 114, 101, 102, 105, + 120, // First seed: Literal "another_prefix" (16) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + 3, 9, // Fourth seed: Account key at index 9 (2) + 0, 0, 0, 0, 0, 0, 0, 0, 0, // No more seeds (9) + 0, // Sixth account meta is signer (1): false = 0 + 0, // Sixth account meta is writable (1): false = 0 + ]; + + async function mockFetchAccountDataFn( + publicKey: PublicKey, + _commitmentOrConfig?: Parameters[1] + ): ReturnType { + if (publicKey.equals(MINT_PUBKEY)) { + return { + data: Buffer.from(MOCK_MINT_STATE), + owner: TOKEN_2022_PROGRAM_ID, + executable: false, + lamports: 0, + }; + }; + if (publicKey.equals(getExtraAccountMetaAddress(MINT_PUBKEY, TRANSFER_HOOK_PROGRAM_ID))) { + return { + data: Buffer.from(MOCK_EXTRA_METAS_STATE), + owner: TRANSFER_HOOK_PROGRAM_ID, + executable: false, + lamports: 0, + }; + }; + return { + data: Buffer.from([]), + owner: PublicKey.default, + executable: false, + lamports: 0, + }; } - expect(parsedExtraAccounts[0].discriminator).to.eql(0); - expect(parsedExtraAccounts[0].addressConfig).to.eql(plainAccount.toBuffer()); - expect(parsedExtraAccounts[0].isSigner).to.be.false; - expect(parsedExtraAccounts[0].isWritable).to.be.false; + it('can add extra accounts to a transfer instruction', async () => { + const amount = 2n; + const sourcePubkey = Keypair.generate().publicKey; + const mintPubkey = MINT_PUBKEY; + const destinationPubkey = Keypair.generate().publicKey; + const authorityPubkey = Keypair.generate().publicKey; + const validateStatePubkey = getExtraAccountMetaAddress(MINT_PUBKEY, TRANSFER_HOOK_PROGRAM_ID); - expect(parsedExtraAccounts[1].discriminator).to.eql(1); - expect(parsedExtraAccounts[1].addressConfig).to.eql(addressConfig); - expect(parsedExtraAccounts[1].isSigner).to.be.true; - expect(parsedExtraAccounts[1].isWritable).to.be.false; + const amountInLeBytes = Buffer.alloc(8); + amountInLeBytes.writeBigUInt64LE(amount); - expect(parsedExtraAccounts[2].discriminator).to.eql(128); - expect(parsedExtraAccounts[2].addressConfig).to.eql(addressConfig); - expect(parsedExtraAccounts[2].isSigner).to.be.false; - expect(parsedExtraAccounts[2].isWritable).to.be.true; - }); - it('resolveExtraAccountMeta', async () => { - const resolvedPlainAccount = await resolveExtraAccountMeta( - connection, - plainExtraAccountMeta, - [], - instructionData, - testProgramId - ); - - expect(resolvedPlainAccount.pubkey).to.eql(plainAccount); - expect(resolvedPlainAccount.isSigner).to.be.false; - expect(resolvedPlainAccount.isWritable).to.be.false; - - const resolvedPdaAccount = await resolveExtraAccountMeta( - connection, - pdaExtraAccountMeta, - [resolvedPlainAccount], - instructionData, - testProgramId - ); - - expect(resolvedPdaAccount.pubkey).to.eql(pdaPublicKey); - expect(resolvedPdaAccount.isSigner).to.be.true; - expect(resolvedPdaAccount.isWritable).to.be.false; - - const resolvedPdaAccountWithProgramId = await resolveExtraAccountMeta( - connection, - pdaExtraAccountMetaWithProgramId, - [resolvedPlainAccount], - instructionData, - testProgramId - ); - - expect(resolvedPdaAccountWithProgramId.pubkey).to.eql(pdaPublicKeyWithProgramId); - expect(resolvedPdaAccountWithProgramId.isSigner).to.be.false; - expect(resolvedPdaAccountWithProgramId.isWritable).to.be.true; + const extraMeta1Pubkey = PublicKey.findProgramAddressSync( + [ + sourcePubkey.toBuffer(), // Account key at index 0 + mintPubkey.toBuffer(), // Account key at index 1 + ], + TRANSFER_HOOK_PROGRAM_ID, + )[0]; + const extraMeta2Pubkey = PublicKey.findProgramAddressSync( + [ + validateStatePubkey.toBuffer(), // Account key at index 4 + ], + TRANSFER_HOOK_PROGRAM_ID, + )[0]; + const extraMeta3Pubkey = PublicKey.findProgramAddressSync( + [ + Buffer.from("prefix"), + amountInLeBytes, // Instruction data 8..16 + ], + TRANSFER_HOOK_PROGRAM_ID, + )[0]; + const extraMeta4Pubkey = new PublicKey(Buffer.from([ + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + ])); // Some arbitrary program ID + const extraMeta5Pubkey = PublicKey.findProgramAddressSync( + [ + Buffer.from("prefix"), + amountInLeBytes, // Instruction data 8..16 + extraMeta2Pubkey.toBuffer(), + ], + extraMeta4Pubkey, // PDA off of the arbitrary program ID + )[0]; + const extraMeta6Pubkey = PublicKey.findProgramAddressSync( + [ + Buffer.from("another_prefix"), + amountInLeBytes, // Instruction data 8..16 + extraMeta2Pubkey.toBuffer(), + extraMeta5Pubkey.toBuffer(), + ], + extraMeta4Pubkey, // PDA off of the arbitrary program ID + )[0]; + + + const connection = await getConnection(); + connection.getAccountInfo = mockFetchAccountDataFn; + + const rawInstruction = createTransferCheckedInstruction( + sourcePubkey, + mintPubkey, + destinationPubkey, + authorityPubkey, + amount, + 9, + undefined, + TOKEN_2022_PROGRAM_ID, + ); + + const hydratedInstruction = await addExtraAccountsToTransferInstruction( + connection, + rawInstruction, + mintPubkey, + amount, + undefined, + TOKEN_2022_PROGRAM_ID, + ); + + // The validation account should not be at index 4 + expect(hydratedInstruction.keys[4].pubkey).to.not.eql(validateStatePubkey); + + // Verify all PDAs are correct + expect(hydratedInstruction.keys[4].pubkey).to.eql(extraMeta1Pubkey); + expect(hydratedInstruction.keys[5].pubkey).to.eql(extraMeta2Pubkey); + expect(hydratedInstruction.keys[6].pubkey).to.eql(extraMeta3Pubkey); + expect(hydratedInstruction.keys[7].pubkey).to.eql(extraMeta4Pubkey); + expect(hydratedInstruction.keys[8].pubkey).to.eql(extraMeta5Pubkey); + expect(hydratedInstruction.keys[9].pubkey).to.eql(extraMeta6Pubkey); + }); }); });