diff --git a/Cargo.lock b/Cargo.lock index 7747a849498..8a5e7fe1347 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6937,6 +6937,9 @@ name = "spl-tlv-account-resolution" version = "0.3.0" dependencies = [ "bytemuck", + "futures 0.3.28", + "futures-util", + "solana-client", "solana-program", "solana-program-test", "solana-sdk", diff --git a/libraries/tlv-account-resolution/Cargo.toml b/libraries/tlv-account-resolution/Cargo.toml index cb4e15060d8..518560371af 100644 --- a/libraries/tlv-account-resolution/Cargo.toml +++ b/libraries/tlv-account-resolution/Cargo.toml @@ -19,6 +19,9 @@ spl-type-length-value = { version = "0.3", path = "../type-length-value" } spl-pod = { version = "0.1", path = "../pod" } [dev-dependencies] +futures = "0.3.28" +futures-util = "0.3" +solana-client = "1.16.13" solana-program-test = "1.16.13" solana-sdk = "1.16.13" spl-discriminator = { version = "0.1", path = "../discriminator" } diff --git a/libraries/tlv-account-resolution/README.md b/libraries/tlv-account-resolution/README.md index b98ca3294d5..7d7bbb27f4d 100644 --- a/libraries/tlv-account-resolution/README.md +++ b/libraries/tlv-account-resolution/README.md @@ -59,9 +59,21 @@ let mut buffer = vec![0; account_size]; ExtraAccountMetaList::init::(&mut buffer, &extra_metas).unwrap(); // Off-chain, you can add the additional accounts directly from the account data +// You need to provide the resolver a way to fetch account data off-chain +let client = RpcClient::new_mock("succeeds".to_string()); let program_id = Pubkey::new_unique(); let mut instruction = Instruction::new_with_bytes(program_id, &[0, 1, 2], vec![]); -ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer).unwrap(); +ExtraAccountMetaList::add_to_instruction::<_, _, MyInstruction>( + &mut instruction, + |address: &Pubkey| { + client + .get_account(address) + .map_ok(|acct| Some(acct.data)) + }, + &buffer, +) +.await +.unwrap(); // On-chain, you can add the additional accounts *and* account infos let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[0, 1, 2], vec![]); diff --git a/libraries/tlv-account-resolution/src/account.rs b/libraries/tlv-account-resolution/src/account.rs index 15b7085d223..8c9203321a9 100644 --- a/libraries/tlv-account-resolution/src/account.rs +++ b/libraries/tlv-account-resolution/src/account.rs @@ -15,12 +15,15 @@ use { /// Resolve a program-derived address (PDA) from the instruction data /// and the accounts that have already been resolved -fn resolve_pda( +fn resolve_pda<'a, F>( seeds: &[Seed], - accounts: &[AccountMeta], instruction_data: &[u8], program_id: &Pubkey, -) -> Result { + get_account_key_data_fn: F, +) -> Result +where + F: Fn(usize) -> Option<(&'a Pubkey, Option<&'a [u8]>)>, +{ let mut pda_seeds: Vec<&[u8]> = vec![]; for config in seeds { match config { @@ -36,10 +39,27 @@ fn resolve_pda( } Seed::AccountKey { index } => { let account_index = *index as usize; - let account_meta = accounts - .get(account_index) - .ok_or::(AccountResolutionError::AccountNotFound.into())?; - pda_seeds.push(account_meta.pubkey.as_ref()); + let address = get_account_key_data_fn(account_index) + .ok_or::(AccountResolutionError::AccountNotFound.into())? + .0; + pda_seeds.push(address.as_ref()); + } + Seed::AccountData { + account_index, + data_index, + length, + } => { + let account_index = *account_index as usize; + let account_data = get_account_key_data_fn(account_index) + .ok_or::(AccountResolutionError::AccountNotFound.into())? + .1 + .ok_or::(AccountResolutionError::AccountDataNotFound.into())?; + let arg_start = *data_index as usize; + let arg_end = arg_start + *length as usize; + if account_data.len() < arg_end { + return Err(AccountResolutionError::AccountDataTooSmall.into()); + } + pda_seeds.push(&account_data[arg_start..arg_end]); } } } @@ -122,26 +142,33 @@ impl ExtraAccountMeta { /// Resolve an `ExtraAccountMeta` into an `AccountMeta`, potentially /// resolving a program-derived address (PDA) if necessary - pub fn resolve( + pub fn resolve<'a, F>( &self, - accounts: &[AccountMeta], instruction_data: &[u8], program_id: &Pubkey, - ) -> Result { + get_account_key_data_fn: F, + ) -> Result + where + F: Fn(usize) -> Option<(&'a Pubkey, Option<&'a [u8]>)>, + { match self.discriminator { 0 => AccountMeta::try_from(self), x if x == 1 || x >= U8_TOP_BIT => { let program_id = if x == 1 { program_id } else { - &accounts - .get(x.saturating_sub(U8_TOP_BIT) as usize) - .ok_or(AccountResolutionError::AccountNotFound)? - .pubkey + get_account_key_data_fn(x.saturating_sub(U8_TOP_BIT) as usize) + .ok_or::(AccountResolutionError::AccountNotFound.into())? + .0 }; let seeds = Seed::unpack_address_config(&self.address_config)?; Ok(AccountMeta { - pubkey: resolve_pda(&seeds, accounts, instruction_data, program_id)?, + pubkey: resolve_pda( + &seeds, + instruction_data, + program_id, + get_account_key_data_fn, + )?, is_signer: self.is_signer.into(), is_writable: self.is_writable.into(), }) diff --git a/libraries/tlv-account-resolution/src/error.rs b/libraries/tlv-account-resolution/src/error.rs index be152b2dbf1..d86ee6ba00c 100644 --- a/libraries/tlv-account-resolution/src/error.rs +++ b/libraries/tlv-account-resolution/src/error.rs @@ -51,4 +51,13 @@ pub enum AccountResolutionError { /// Error in checked math operation #[error("Error in checked math operation")] CalculationFailure, + /// Could not find account data at specified index + #[error("Could not find account data at specified index")] + AccountDataNotFound, + /// Account data too small for requested seed configuration + #[error("Account data too small for requested seed configuration")] + AccountDataTooSmall, + /// Failed to fetch account + #[error("Failed to fetch account")] + AccountFetchFailed, } diff --git a/libraries/tlv-account-resolution/src/seeds.rs b/libraries/tlv-account-resolution/src/seeds.rs index 243382b2cd7..716d9db5806 100644 --- a/libraries/tlv-account-resolution/src/seeds.rs +++ b/libraries/tlv-account-resolution/src/seeds.rs @@ -13,11 +13,16 @@ //! * N - Literal bytes themselves //! * `Seed::InstructionData`: 1 + 1 + 1 = 3 //! * 1 - Discriminator -//! * 1 - Index of instruction data +//! * 1 - Start index of instruction data //! * 1 - Length of instruction data starting at index //! * `Seed::AccountKey` - 1 + 1 = 2 //! * 1 - Discriminator //! * 1 - Index of account in accounts list +//! * `Seed::AccountData`: 1 + 1 + 1 + 1 = 4 +//! * 1 - Discriminator +//! * 1 - Index of account in accounts list +//! * 1 - Start index of account data +//! * 1 - Length of account data starting at index //! //! No matter which types of seeds you choose, the total size of all seed //! configurations must be less than or equal to 32 bytes. @@ -46,7 +51,7 @@ pub enum Seed { /// data /// Packed as: /// * 1 - Discriminator - /// * 1 - Index of instruction data + /// * 1 - Start index of instruction data /// * 1 - Length of instruction data starting at index InstructionData { /// The index where the bytes of an instruction argument begin @@ -66,6 +71,22 @@ pub enum Seed { /// The index of the account in the entire accounts list index: u8, }, + /// An argument to be resolved from the inner data of some account + /// Packed as: + /// * 1 - Discriminator + /// * 1 - Index of account in accounts list + /// * 1 - Start index of account data + /// * 1 - Length of account data starting at index + AccountData { + /// The index of the account in the entire accounts list + account_index: u8, + /// The index where the bytes of an account data argument begin + data_index: u8, + /// The length of the argument (number of bytes) + /// + /// Note: Max seed length is 32 bytes, so `u8` is appropriate here + length: u8, + }, } impl Seed { /// Get the size of a seed configuration @@ -79,6 +100,9 @@ impl Seed { Self::InstructionData { .. } => 1 + 1 + 1, // 1 byte for the discriminator, 1 byte for the index Self::AccountKey { .. } => 1 + 1, + // 1 byte for the discriminator, 1 byte for the account index, + // 1 byte for the data index 1 byte for the length + Self::AccountData { .. } => 1 + 1 + 1 + 1, } } @@ -106,6 +130,16 @@ impl Seed { dst[0] = 3; dst[1] = *index; } + Self::AccountData { + account_index, + data_index, + length, + } => { + dst[0] = 4; + dst[1] = *account_index; + dst[2] = *data_index; + dst[3] = *length; + } } Ok(()) } @@ -137,6 +171,7 @@ impl Seed { 1 => unpack_seed_literal(rest), 2 => unpack_seed_instruction_arg(rest), 3 => unpack_seed_account_key(rest), + 4 => unpack_seed_account_data(rest), _ => Err(ProgramError::InvalidAccountData), } } @@ -193,6 +228,18 @@ fn unpack_seed_account_key(bytes: &[u8]) -> Result { Ok(Seed::AccountKey { index: bytes[0] }) } +fn unpack_seed_account_data(bytes: &[u8]) -> Result { + if bytes.len() < 3 { + // Should be at least 3 bytes + return Err(AccountResolutionError::InvalidBytesForSeed.into()); + } + Ok(Seed::AccountData { + account_index: bytes[0], + data_index: bytes[1], + length: bytes[2], + }) +} + #[cfg(test)] mod tests { use super::*; @@ -301,7 +348,7 @@ mod tests { 1, // Discrim (Literal) 4, // Length 1, 1, 1, 1, // 4 - 4, // Discrim (Invalid) + 6, // Discrim (Invalid) 2, // Index 1, // Length 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -376,13 +423,12 @@ mod tests { ); } - fn test_pack_unpack_seed(seed: Seed, mixed: &mut Vec) { + fn test_pack_unpack_seed(seed: Seed) { let tlv_size = seed.tlv_size() as usize; let mut packed = vec![0u8; tlv_size]; seed.pack(&mut packed).unwrap(); let unpacked = Seed::unpack(&packed).unwrap(); assert_eq!(seed, unpacked); - mixed.push(seed); } #[test] @@ -395,19 +441,21 @@ mod tests { let seed = Seed::Literal { bytes: bytes.to_vec(), }; - test_pack_unpack_seed(seed, &mut mixed); + test_pack_unpack_seed(seed); let bytes = 8u8.to_le_bytes(); let seed = Seed::Literal { bytes: bytes.to_vec(), }; - test_pack_unpack_seed(seed, &mut mixed); + test_pack_unpack_seed(seed.clone()); + mixed.push(seed); let bytes = 32u32.to_le_bytes(); let seed = Seed::Literal { bytes: bytes.to_vec(), }; - test_pack_unpack_seed(seed, &mut mixed); + test_pack_unpack_seed(seed.clone()); + mixed.push(seed); // Instruction args @@ -415,21 +463,40 @@ mod tests { index: 0, length: 0, }; - test_pack_unpack_seed(seed, &mut mixed); + test_pack_unpack_seed(seed); let seed = Seed::InstructionData { index: 6, length: 9, }; - test_pack_unpack_seed(seed, &mut mixed); + test_pack_unpack_seed(seed.clone()); + mixed.push(seed); // Account keys let seed = Seed::AccountKey { index: 0 }; - test_pack_unpack_seed(seed, &mut mixed); + test_pack_unpack_seed(seed); let seed = Seed::AccountKey { index: 9 }; - test_pack_unpack_seed(seed, &mut mixed); + test_pack_unpack_seed(seed.clone()); + mixed.push(seed); + + // Account data + + let seed = Seed::AccountData { + account_index: 0, + data_index: 0, + length: 0, + }; + test_pack_unpack_seed(seed); + + let seed = Seed::AccountData { + account_index: 0, + data_index: 0, + length: 9, + }; + test_pack_unpack_seed(seed.clone()); + mixed.push(seed); // Arrays @@ -438,9 +505,8 @@ mod tests { assert_eq!(mixed, unpacked_array); let mut shuffled_mixed = mixed.clone(); - shuffled_mixed.swap(0, 5); + shuffled_mixed.swap(0, 1); shuffled_mixed.swap(1, 4); - shuffled_mixed.swap(3, 6); shuffled_mixed.swap(3, 0); let packed_array = Seed::pack_into_address_config(&shuffled_mixed).unwrap(); diff --git a/libraries/tlv-account-resolution/src/state.rs b/libraries/tlv-account-resolution/src/state.rs index 573c14ccd00..5a687bfeddc 100644 --- a/libraries/tlv-account-resolution/src/state.rs +++ b/libraries/tlv-account-resolution/src/state.rs @@ -11,8 +11,24 @@ use { spl_discriminator::SplDiscriminate, spl_pod::slice::{PodSlice, PodSliceMut}, spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut}, + std::future::Future, }; +/// Type representing the output of an account fetching function, for easy +/// chaining between APIs +pub type AccountDataResult = Result>, AccountFetchError>; +/// Generic error type that can come out of any client while fetching account data +pub type AccountFetchError = Box; + +/// Helper to convert an `AccountInfo` to an `AccountMeta` +fn account_info_to_meta(account_info: &AccountInfo) -> AccountMeta { + AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + } +} + /// De-escalate an account meta if necessary fn de_escalate_account_meta(account_meta: &mut AccountMeta, account_metas: &[AccountMeta]) { // This is a little tricky to read, but the idea is to see if @@ -39,15 +55,6 @@ fn de_escalate_account_meta(account_meta: &mut AccountMeta, account_metas: &[Acc } } -/// Helper to convert an `AccountInfo` to an `AccountMeta` -fn account_meta_from_info(account_info: &AccountInfo) -> AccountMeta { - AccountMeta { - pubkey: *account_info.key, - is_signer: account_info.is_signer, - is_writable: account_info.is_writable, - } -} - /// Stateless helper for storing additional accounts required for an /// instruction. /// @@ -58,8 +65,10 @@ fn account_meta_from_info(account_info: &AccountInfo) -> AccountMeta { /// /// Sample usage: /// -/// ``` +/// ```rust /// use { +/// futures_util::TryFutureExt, +/// solana_client::nonblocking::rpc_client::RpcClient, /// solana_program::{ /// account_info::AccountInfo, instruction::{AccountMeta, Instruction}, /// pubkey::Pubkey @@ -68,7 +77,7 @@ fn account_meta_from_info(account_info: &AccountInfo) -> AccountMeta { /// spl_tlv_account_resolution::{ /// account::ExtraAccountMeta, /// seeds::Seed, -/// state::ExtraAccountMetaList +/// state::{AccountDataResult, AccountFetchError, ExtraAccountMetaList} /// }, /// }; /// @@ -112,9 +121,36 @@ fn account_meta_from_info(account_info: &AccountInfo) -> AccountMeta { /// ExtraAccountMetaList::init::(&mut buffer, &extra_metas).unwrap(); /// /// // Off-chain, you can add the additional accounts directly from the account data +/// // You need to provide the resolver a way to fetch account data off-chain +/// struct MyClient { +/// client: RpcClient, +/// } +/// impl MyClient { +/// pub fn new() -> Self { +/// Self { +/// client: RpcClient::new_mock("succeeds".to_string()), +/// } +/// } +/// pub async fn get_account_data(&self, address: Pubkey) -> AccountDataResult { +/// self.client.get_account(&address) +/// .await +/// .map(|acct| Some(acct.data)) +/// .map_err(|e| Box::new(e) as AccountFetchError) +/// } +/// } +/// +/// let client = MyClient::new(); /// let program_id = Pubkey::new_unique(); /// let mut instruction = Instruction::new_with_bytes(program_id, &[0, 1, 2], vec![]); -/// ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer).unwrap(); +/// # futures::executor::block_on(async { +/// // Now use the resolver to add the additional accounts off-chain +/// ExtraAccountMetaList::add_to_instruction::( +/// &mut instruction, +/// |address: Pubkey| client.get_account_data(address), +/// &buffer, +/// ) +/// .await; +/// # }); /// /// // On-chain, you can add the additional accounts *and* account infos /// let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[0, 1, 2], vec![]); @@ -181,13 +217,33 @@ impl ExtraAccountMetaList { let initial_accounts_len = account_infos.len() - extra_account_metas.len(); + // Convert to `AccountMeta` to check resolved metas let provided_metas = account_infos .iter() - .map(account_meta_from_info) + .map(account_info_to_meta) .collect::>(); for (i, config) in extra_account_metas.iter().enumerate() { - let meta = config.resolve(&provided_metas, instruction_data, program_id)?; + let meta = { + // Create a list of `Ref`s so we can reference account data in the + // resolution step + let account_key_data_refs = account_infos + .iter() + .map(|info| { + let key = *info.key; + let data = info.try_borrow_data()?; + Ok((key, data)) + }) + .collect::, ProgramError>>()?; + + config.resolve(instruction_data, program_id, |usize| { + account_key_data_refs + .get(usize) + .map(|(pubkey, opt_data)| (pubkey, Some(opt_data.as_ref()))) + })? + }; + + // Ensure the account is in the correct position let expected_index = i .checked_add(initial_accounts_len) .ok_or::(AccountResolutionError::CalculationFailure.into())?; @@ -200,21 +256,48 @@ impl ExtraAccountMetaList { } /// Add the additional account metas to an existing instruction - pub fn add_to_instruction( + pub async fn add_to_instruction( instruction: &mut Instruction, + fetch_account_data_fn: F, data: &[u8], - ) -> Result<(), ProgramError> { + ) -> Result<(), ProgramError> + where + F: Fn(Pubkey) -> Fut, + Fut: Future, + { let state = TlvStateBorrowed::unpack(data)?; let bytes = state.get_first_bytes::()?; let extra_account_metas = PodSlice::::unpack(bytes)?; + // Fetch account data for each of the instruction accounts + let mut account_key_datas = vec![]; + for meta in instruction.accounts.iter() { + let account_data = fetch_account_data_fn(meta.pubkey) + .await + .map_err::(|_| { + AccountResolutionError::AccountFetchFailed.into() + })?; + account_key_datas.push((meta.pubkey, account_data)); + } + for extra_meta in extra_account_metas.data().iter() { - let mut meta = extra_meta.resolve( - &instruction.accounts, - &instruction.data, - &instruction.program_id, - )?; + let mut meta = + extra_meta.resolve(&instruction.data, &instruction.program_id, |usize| { + account_key_datas + .get(usize) + .map(|(pubkey, opt_data)| (pubkey, opt_data.as_ref().map(|x| x.as_slice()))) + })?; de_escalate_account_meta(&mut meta, &instruction.accounts); + + // Fetch account data for the new account + account_key_datas.push(( + meta.pubkey, + fetch_account_data_fn(meta.pubkey) + .await + .map_err::(|_| { + AccountResolutionError::AccountFetchFailed.into() + })?, + )); instruction.accounts.push(meta); } Ok(()) @@ -227,20 +310,42 @@ impl ExtraAccountMetaList { data: &[u8], account_infos: &[AccountInfo<'a>], ) -> Result<(), ProgramError> { - let initial_instruction_metas_len = cpi_instruction.accounts.len(); + let state = TlvStateBorrowed::unpack(data)?; + let bytes = state.get_first_bytes::()?; + let extra_account_metas = PodSlice::::unpack(bytes)?; - Self::add_to_instruction::(cpi_instruction, data)?; + for extra_meta in extra_account_metas.data().iter() { + let mut meta = { + // Create a list of `Ref`s so we can reference account data in the + // resolution step + let account_key_data_refs = cpi_account_infos + .iter() + .map(|info| { + let key = *info.key; + let data = info.try_borrow_data()?; + Ok((key, data)) + }) + .collect::, ProgramError>>()?; + + extra_meta.resolve( + &cpi_instruction.data, + &cpi_instruction.program_id, + |usize| { + account_key_data_refs + .get(usize) + .map(|(pubkey, opt_data)| (pubkey, Some(opt_data.as_ref()))) + }, + )? + }; + de_escalate_account_meta(&mut meta, &cpi_instruction.accounts); - for account_meta in cpi_instruction - .accounts - .iter() - .skip(initial_instruction_metas_len) - { let account_info = account_infos .iter() - .find(|&x| *x.key == account_meta.pubkey) + .find(|&x| *x.key == meta.pubkey) .ok_or(AccountResolutionError::IncorrectAccount)? .clone(); + + cpi_instruction.accounts.push(meta); cpi_account_infos.push(account_info); } Ok(()) @@ -253,7 +358,9 @@ mod tests { super::*, crate::seeds::Seed, solana_program::{clock::Epoch, instruction::AccountMeta, pubkey::Pubkey}, + solana_program_test::tokio, spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, + std::collections::HashMap, }; pub struct TestInstruction; @@ -268,8 +375,28 @@ mod tests { ArrayDiscriminator::new([2; ArrayDiscriminator::LENGTH]); } - #[test] - fn init_with_metas() { + pub struct MockRpc<'a> { + cache: HashMap>, + } + impl<'a> MockRpc<'a> { + pub fn setup(account_infos: &'a [AccountInfo<'a>]) -> Self { + let mut cache = HashMap::new(); + for info in account_infos { + cache.insert(*info.key, info); + } + Self { cache } + } + + pub async fn get_account_data(&self, pubkey: Pubkey) -> AccountDataResult { + Ok(self + .cache + .get(&pubkey) + .map(|account| account.try_borrow_data().unwrap().to_vec())) + } + } + + #[tokio::test] + async fn init_with_metas() { let metas = [ AccountMeta::new(Pubkey::new_unique(), false).into(), AccountMeta::new(Pubkey::new_unique(), true).into(), @@ -281,28 +408,35 @@ mod tests { ExtraAccountMetaList::init::(&mut buffer, &metas).unwrap(); + let mock_rpc = MockRpc::setup(&[]); + let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) - .unwrap(); + ExtraAccountMetaList::add_to_instruction::( + &mut instruction, + |pubkey| mock_rpc.get_account_data(pubkey), + &buffer, + ) + .await + .unwrap(); - assert_eq!( - instruction - .accounts - .iter() - .map(ExtraAccountMeta::from) - .collect::>(), - metas - ); + let check_metas = metas + .iter() + .map(|e| AccountMeta::try_from(e).unwrap()) + .collect::>(); + + assert_eq!(instruction.accounts, check_metas,); } - #[test] - fn init_with_infos() { + #[tokio::test] + async fn init_with_infos() { + let program_id = Pubkey::new_unique(); + let pubkey1 = Pubkey::new_unique(); let mut lamports1 = 0; let mut data1 = []; let pubkey2 = Pubkey::new_unique(); let mut lamports2 = 0; - let mut data2 = []; + let mut data2 = [4, 4, 4, 6, 6, 6, 8, 8]; let pubkey3 = Pubkey::new_unique(); let mut lamports3 = 0; let mut data3 = []; @@ -317,8 +451,7 @@ mod tests { &owner, false, Epoch::default(), - ) - .into(), + ), AccountInfo::new( &pubkey2, true, @@ -328,8 +461,7 @@ mod tests { &owner, false, Epoch::default(), - ) - .into(), + ), AccountInfo::new( &pubkey3, false, @@ -339,30 +471,74 @@ mod tests { &owner, false, Epoch::default(), - ) - .into(), + ), ]; - let account_size = ExtraAccountMetaList::size_of(account_infos.len()).unwrap(); - let mut buffer = vec![0; account_size]; - ExtraAccountMetaList::init::(&mut buffer, &account_infos).unwrap(); + let required_pda = ExtraAccountMeta::new_with_seeds( + &[ + Seed::AccountKey { index: 0 }, + Seed::AccountData { + account_index: 1, + data_index: 2, + length: 4, + }, + ], + false, + true, + ) + .unwrap(); - let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) + // Convert to `ExtraAccountMeta` + let required_extra_accounts = [ + ExtraAccountMeta::from(&account_infos[0]), + ExtraAccountMeta::from(&account_infos[1]), + ExtraAccountMeta::from(&account_infos[2]), + required_pda, + ]; + + let account_size = ExtraAccountMetaList::size_of(required_extra_accounts.len()).unwrap(); + let mut buffer = vec![0; account_size]; + + ExtraAccountMetaList::init::(&mut buffer, &required_extra_accounts) .unwrap(); + let mock_rpc = MockRpc::setup(&account_infos); + + let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]); + ExtraAccountMetaList::add_to_instruction::( + &mut instruction, + |pubkey| mock_rpc.get_account_data(pubkey), + &buffer, + ) + .await + .unwrap(); + + let (check_required_pda, _) = Pubkey::find_program_address( + &[ + account_infos[0].key.as_ref(), // Account key + &account_infos[1].try_borrow_data().unwrap()[2..6], // Account data + ], + &program_id, + ); + + // Convert to `AccountMeta` to check instruction + let check_metas = [ + account_info_to_meta(&account_infos[0]), + account_info_to_meta(&account_infos[1]), + account_info_to_meta(&account_infos[2]), + AccountMeta::new(check_required_pda, false), + ]; + + assert_eq!(instruction.accounts, check_metas,); + assert_eq!( - instruction - .accounts - .iter() - .map(ExtraAccountMeta::from) - .collect::>(), - account_infos + instruction.accounts.get(3).unwrap().pubkey, + check_required_pda ); } - #[test] - fn init_with_extra_account_metas() { + #[tokio::test] + async fn init_with_extra_account_metas() { let program_id = Pubkey::new_unique(); let extra_meta3_literal_str = "seed_prefix"; @@ -378,8 +554,8 @@ mod tests { bytes: extra_meta3_literal_str.as_bytes().to_vec(), }, Seed::InstructionData { - index: 2, - length: 2, // u16 + index: 1, + length: 1, // u8 }, Seed::AccountKey { index: 0 }, Seed::AccountKey { index: 2 }, @@ -388,86 +564,60 @@ mod tests { true, ) .unwrap(); - let extra_meta4 = ExtraAccountMeta::new_external_pda_with_seeds( - 0, - &[Seed::AccountKey { index: 2 }], - false, - false, - ) - .unwrap(); let metas = [ ExtraAccountMeta::from(&extra_meta1), ExtraAccountMeta::from(&extra_meta2), extra_meta3, - extra_meta4, ]; + let ix_data = vec![1, 2, 3, 4]; + let ix_accounts = vec![ix_account1.clone(), ix_account2.clone()]; + let mut instruction = Instruction::new_with_bytes(program_id, &ix_data, ix_accounts); + let account_size = ExtraAccountMetaList::size_of(metas.len()).unwrap(); let mut buffer = vec![0; account_size]; ExtraAccountMetaList::init::(&mut buffer, &metas).unwrap(); - // Fails with not enough instruction data - let ix_data = vec![1, 2, 3]; - let ix_accounts = vec![ix_account1.clone(), ix_account2.clone()]; - let mut instruction = Instruction::new_with_bytes(program_id, &ix_data, ix_accounts); - assert_eq!( - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) - .unwrap_err(), - AccountResolutionError::InstructionDataTooSmall.into() - ); + let mock_rpc = MockRpc::setup(&[]); - let ix_data = vec![1, 2, 3, 4]; - let ix_accounts = vec![ix_account1.clone(), ix_account2.clone()]; - let mut instruction = Instruction::new_with_bytes(program_id, &ix_data, ix_accounts); - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) - .unwrap(); + ExtraAccountMetaList::add_to_instruction::( + &mut instruction, + |pubkey| mock_rpc.get_account_data(pubkey), + &buffer, + ) + .await + .unwrap(); + let check_extra_meta3_u8_arg = ix_data[1]; let check_extra_meta3_pubkey = Pubkey::find_program_address( &[ extra_meta3_literal_str.as_bytes(), - &ix_data[2..4], + &[check_extra_meta3_u8_arg], ix_account1.pubkey.as_ref(), extra_meta1.pubkey.as_ref(), ], &program_id, ) .0; - let check_extra_meta4_pubkey = - Pubkey::find_program_address(&[extra_meta1.pubkey.as_ref()], &ix_account1.pubkey).0; let check_metas = [ ix_account1, ix_account2, extra_meta1, extra_meta2, AccountMeta::new(check_extra_meta3_pubkey, false), - AccountMeta::new_readonly(check_extra_meta4_pubkey, false), ]; assert_eq!( instruction.accounts.get(4).unwrap().pubkey, check_extra_meta3_pubkey, ); - assert_eq!( - instruction.accounts.get(5).unwrap().pubkey, - check_extra_meta4_pubkey, - ); - assert_eq!( - instruction - .accounts - .iter() - .map(ExtraAccountMeta::from) - .collect::>(), - check_metas - .iter() - .map(ExtraAccountMeta::from) - .collect::>() - ); + assert_eq!(instruction.accounts, check_metas,); } - #[test] - fn init_multiple() { + #[tokio::test] + async fn init_multiple() { let extra_meta5_literal_str = "seed_prefix"; let extra_meta5_literal_u32 = 4u32; let other_meta2_literal_str = "other_seed_prefix"; @@ -511,13 +661,6 @@ mod tests { true, ) .unwrap(); - let other_meta3 = ExtraAccountMeta::new_external_pda_with_seeds( - 1, - &[Seed::AccountKey { index: 3 }], - false, - false, - ) - .unwrap(); let metas = [ ExtraAccountMeta::from(&extra_meta1), @@ -526,11 +669,7 @@ mod tests { ExtraAccountMeta::from(&extra_meta4), extra_meta5, ]; - let other_metas = [ - ExtraAccountMeta::from(&other_meta1), - other_meta2, - other_meta3, - ]; + let other_metas = [ExtraAccountMeta::from(&other_meta1), other_meta2]; let account_size = ExtraAccountMetaList::size_of(metas.len()).unwrap() + ExtraAccountMetaList::size_of(other_metas.len()).unwrap(); @@ -539,12 +678,19 @@ mod tests { ExtraAccountMetaList::init::(&mut buffer, &metas).unwrap(); ExtraAccountMetaList::init::(&mut buffer, &other_metas).unwrap(); + let mock_rpc = MockRpc::setup(&[]); + let program_id = Pubkey::new_unique(); let ix_data = vec![0, 0, 0, 0, 0, 7, 0, 0]; let ix_accounts = vec![]; let mut instruction = Instruction::new_with_bytes(program_id, &ix_data, ix_accounts); - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) - .unwrap(); + ExtraAccountMetaList::add_to_instruction::( + &mut instruction, + |pubkey| mock_rpc.get_account_data(pubkey), + &buffer, + ) + .await + .unwrap(); let check_extra_meta5_u8_arg = ix_data[5]; let check_extra_meta5_pubkey = Pubkey::find_program_address( @@ -569,17 +715,7 @@ mod tests { instruction.accounts.get(4).unwrap().pubkey, check_extra_meta5_pubkey, ); - assert_eq!( - instruction - .accounts - .iter() - .map(ExtraAccountMeta::from) - .collect::>(), - check_metas - .iter() - .map(ExtraAccountMeta::from) - .collect::>() - ); + assert_eq!(instruction.accounts, check_metas,); let program_id = Pubkey::new_unique(); let ix_account1 = AccountMeta::new(Pubkey::new_unique(), false); @@ -587,8 +723,13 @@ mod tests { let ix_accounts = vec![ix_account1.clone(), ix_account2.clone()]; let ix_data = vec![0, 26, 0, 0, 0, 0, 0]; let mut instruction = Instruction::new_with_bytes(program_id, &ix_data, ix_accounts); - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) - .unwrap(); + ExtraAccountMetaList::add_to_instruction::( + &mut instruction, + |pubkey| mock_rpc.get_account_data(pubkey), + &buffer, + ) + .await + .unwrap(); let check_other_meta2_u32_arg = u32::from_le_bytes(ix_data[1..5].try_into().unwrap()); let check_other_meta2_pubkey = Pubkey::find_program_address( @@ -600,40 +741,22 @@ mod tests { &program_id, ) .0; - let check_other_meta3_pubkey = - Pubkey::find_program_address(&[check_other_meta2_pubkey.as_ref()], &ix_account2.pubkey) - .0; let check_other_metas = [ ix_account1, ix_account2, other_meta1, AccountMeta::new(check_other_meta2_pubkey, false), - AccountMeta::new_readonly(check_other_meta3_pubkey, false), ]; assert_eq!( instruction.accounts.get(3).unwrap().pubkey, check_other_meta2_pubkey, ); - assert_eq!( - instruction.accounts.get(4).unwrap().pubkey, - check_other_meta3_pubkey, - ); - assert_eq!( - instruction - .accounts - .iter() - .map(ExtraAccountMeta::from) - .collect::>(), - check_other_metas - .iter() - .map(ExtraAccountMeta::from) - .collect::>() - ); + assert_eq!(instruction.accounts, check_other_metas,); } - #[test] - fn init_mixed() { + #[tokio::test] + async fn init_mixed() { let extra_meta5_literal_str = "seed_prefix"; let extra_meta6_literal_u64 = 28u64; @@ -657,8 +780,7 @@ mod tests { &owner, false, Epoch::default(), - ) - .into(), + ), AccountInfo::new( &pubkey2, true, @@ -668,8 +790,7 @@ mod tests { &owner, false, Epoch::default(), - ) - .into(), + ), AccountInfo::new( &pubkey3, false, @@ -679,8 +800,7 @@ mod tests { &owner, false, Epoch::default(), - ) - .into(), + ), ]; let extra_meta1 = AccountMeta::new(Pubkey::new_unique(), false); @@ -719,7 +839,11 @@ mod tests { ) .unwrap(); - let metas = [ + let test_ix_required_extra_accounts = account_infos + .iter() + .map(ExtraAccountMeta::from) + .collect::>(); + let test_other_ix_required_extra_accounts = [ ExtraAccountMeta::from(&extra_meta1), ExtraAccountMeta::from(&extra_meta2), ExtraAccountMeta::from(&extra_meta3), @@ -728,25 +852,39 @@ mod tests { extra_meta6, ]; - let account_size = ExtraAccountMetaList::size_of(account_infos.len()).unwrap() - + ExtraAccountMetaList::size_of(metas.len()).unwrap(); + let account_size = ExtraAccountMetaList::size_of(test_ix_required_extra_accounts.len()) + .unwrap() + + ExtraAccountMetaList::size_of(test_other_ix_required_extra_accounts.len()).unwrap(); let mut buffer = vec![0; account_size]; - ExtraAccountMetaList::init::(&mut buffer, &account_infos).unwrap(); - ExtraAccountMetaList::init::(&mut buffer, &metas).unwrap(); + ExtraAccountMetaList::init::( + &mut buffer, + &test_ix_required_extra_accounts, + ) + .unwrap(); + ExtraAccountMetaList::init::( + &mut buffer, + &test_other_ix_required_extra_accounts, + ) + .unwrap(); + + let mock_rpc = MockRpc::setup(&account_infos); let program_id = Pubkey::new_unique(); let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]); - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) - .unwrap(); - assert_eq!( - instruction - .accounts - .iter() - .map(ExtraAccountMeta::from) - .collect::>(), - account_infos - ); + ExtraAccountMetaList::add_to_instruction::( + &mut instruction, + |pubkey| mock_rpc.get_account_data(pubkey), + &buffer, + ) + .await + .unwrap(); + + let test_ix_check_metas = account_infos + .iter() + .map(account_info_to_meta) + .collect::>(); + assert_eq!(instruction.accounts, test_ix_check_metas,); let program_id = Pubkey::new_unique(); let instruction_u8array_arg = [1, 2, 3, 4, 5, 6, 7, 8]; @@ -755,8 +893,13 @@ mod tests { instruction_data.extend_from_slice(&instruction_u8array_arg); instruction_data.extend_from_slice(instruction_pubkey_arg.as_ref()); let mut instruction = Instruction::new_with_bytes(program_id, &instruction_data, vec![]); - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) - .unwrap(); + ExtraAccountMetaList::add_to_instruction::( + &mut instruction, + |pubkey| mock_rpc.get_account_data(pubkey), + &buffer, + ) + .await + .unwrap(); let check_extra_meta5_pubkey = Pubkey::find_program_address( &[ @@ -779,7 +922,7 @@ mod tests { ) .0; - let check_metas = vec![ + let test_other_ix_check_metas = vec![ extra_meta1, extra_meta2, extra_meta3, @@ -796,44 +939,186 @@ mod tests { instruction.accounts.get(5).unwrap().pubkey, check_extra_meta6_pubkey, ); - assert_eq!( - instruction - .accounts - .iter() - .map(ExtraAccountMeta::from) - .collect::>(), - check_metas - .iter() - .map(ExtraAccountMeta::from) - .collect::>() - ); + assert_eq!(instruction.accounts, test_other_ix_check_metas,); } - #[test] - fn cpi_instruction() { + #[tokio::test] + async fn cpi_instruction() { // Say we have a program that CPIs to another program. // // Say that _other_ program will need extra account infos. - // This will be our program. Let's ignore the account info - // for the other program in this example. + // This will be our program let program_id = Pubkey::new_unique(); let owner = Pubkey::new_unique(); - // First let's build a list of account infos for the CPI - // instruction itself. - let pubkey_ix_1 = Pubkey::new_unique(); + // Some seeds used by the program for PDAs + let required_pda1_literal_string = "required_pda1"; + let required_pda2_literal_u32 = 4u32; + + // Define instruction data + // - 0: u8 + // - 1-8: [u8; 8] + // - 9-16: u64 + let instruction_u8array_arg = [1, 2, 3, 4, 5, 6, 7, 8]; + let instruction_u64_arg = 208u64; + let mut instruction_data = vec![0]; + instruction_data.extend_from_slice(&instruction_u8array_arg); + instruction_data.extend_from_slice(instruction_u64_arg.to_le_bytes().as_ref()); + + // Define known instruction accounts + let ix_accounts = vec![ + AccountMeta::new(Pubkey::new_unique(), false), + AccountMeta::new(Pubkey::new_unique(), false), + ]; + + // Define extra account metas required by the program we will CPI to + let extra_meta1 = AccountMeta::new(Pubkey::new_unique(), false); + let extra_meta2 = AccountMeta::new(Pubkey::new_unique(), true); + let extra_meta3 = AccountMeta::new_readonly(Pubkey::new_unique(), false); + let required_accounts = [ + ExtraAccountMeta::from(&extra_meta1), + ExtraAccountMeta::from(&extra_meta2), + ExtraAccountMeta::from(&extra_meta3), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: required_pda1_literal_string.as_bytes().to_vec(), + }, + Seed::InstructionData { + index: 1, + length: 8, // [u8; 8] + }, + Seed::AccountKey { index: 1 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: required_pda2_literal_u32.to_le_bytes().to_vec(), + }, + Seed::InstructionData { + index: 9, + length: 8, // u64 + }, + Seed::AccountKey { index: 5 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 0, + length: 1, // u8 + }, + Seed::AccountData { + account_index: 2, + data_index: 0, + length: 8, + }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::AccountData { + account_index: 5, + data_index: 4, + length: 4, + }, // This one is a PDA! + ], + false, + true, + ) + .unwrap(), + ]; + + // Now here we're going to build the list of account infos + // We'll need to include: + // - The instruction account infos for the program to CPI to + // - The extra account infos for the program to CPI to + // - Some other arbitrary account infos our program may use + + // First we need to manually derive each PDA + let check_required_pda1_pubkey = Pubkey::find_program_address( + &[ + required_pda1_literal_string.as_bytes(), + &instruction_u8array_arg, + ix_accounts.get(1).unwrap().pubkey.as_ref(), // The second account + ], + &program_id, + ) + .0; + let check_required_pda2_pubkey = Pubkey::find_program_address( + &[ + required_pda2_literal_u32.to_le_bytes().as_ref(), + instruction_u64_arg.to_le_bytes().as_ref(), + check_required_pda1_pubkey.as_ref(), // The first PDA should be at index 5 + ], + &program_id, + ) + .0; + let check_required_pda3_pubkey = Pubkey::find_program_address( + &[ + &[0], // Instruction "discriminator" (u8) + &[8; 8], // The first 8 bytes of the data for account at index 2 (extra account 1) + ], + &program_id, + ) + .0; + let check_required_pda4_pubkey = Pubkey::find_program_address( + &[ + &[7; 4], /* 4 bytes starting at index 4 of the data for account at index 5 (extra + * pda 1) */ + ], + &program_id, + ) + .0; + + // The instruction account infos for the program to CPI to + let pubkey_ix_1 = ix_accounts.get(0).unwrap().pubkey; let mut lamports_ix_1 = 0; let mut data_ix_1 = []; - let pubkey_ix_2 = Pubkey::new_unique(); + let pubkey_ix_2 = ix_accounts.get(1).unwrap().pubkey; let mut lamports_ix_2 = 0; let mut data_ix_2 = []; - // For the CPI account infos themselves. - let ix_account_infos = [ + + // The extra account infos for the program to CPI to + let mut lamports1 = 0; + let mut data1 = [8; 12]; + let mut lamports2 = 0; + let mut data2 = []; + let mut lamports3 = 0; + let mut data3 = []; + let mut lamports_pda1 = 0; + let mut data_pda1 = [7; 12]; + let mut lamports_pda2 = 0; + let mut data_pda2 = []; + let mut lamports_pda3 = 0; + let mut data_pda3 = []; + let mut lamports_pda4 = 0; + let mut data_pda4 = []; + + // Some other arbitrary account infos our program may use + let pubkey_arb_1 = Pubkey::new_unique(); + let mut lamports_arb_1 = 0; + let mut data_arb_1 = []; + let pubkey_arb_2 = Pubkey::new_unique(); + let mut lamports_arb_2 = 0; + let mut data_arb_2 = []; + + let all_account_infos = [ AccountInfo::new( &pubkey_ix_1, - false, - true, + ix_accounts.get(0).unwrap().is_signer, + ix_accounts.get(0).unwrap().is_writable, &mut lamports_ix_1, &mut data_ix_1, &owner, @@ -842,38 +1127,18 @@ mod tests { ), AccountInfo::new( &pubkey_ix_2, - false, - true, + ix_accounts.get(1).unwrap().is_signer, + ix_accounts.get(1).unwrap().is_writable, &mut lamports_ix_2, &mut data_ix_2, &owner, false, Epoch::default(), ), - ]; - // For the CPI instruction's list of account metas. - let ix_accounts = vec![ - AccountMeta::new(*ix_account_infos[0].key, false), - AccountMeta::new(*ix_account_infos[1].key, false), - ]; - - // Now let's build a list of extra account infos required by - // the program we are going to CPI to. - let pubkey1 = Pubkey::new_unique(); - let mut lamports1 = 0; - let mut data1 = []; - let pubkey2 = Pubkey::new_unique(); - let mut lamports2 = 0; - let mut data2 = []; - let pubkey3 = Pubkey::new_unique(); - let mut lamports3 = 0; - let mut data3 = []; - let owner = Pubkey::new_unique(); - let extra_account_infos = [ AccountInfo::new( - &pubkey1, - false, - true, + &extra_meta1.pubkey, + required_accounts.get(0).unwrap().is_signer.into(), + required_accounts.get(0).unwrap().is_writable.into(), &mut lamports1, &mut data1, &owner, @@ -881,9 +1146,9 @@ mod tests { Epoch::default(), ), AccountInfo::new( - &pubkey2, - true, - false, + &extra_meta2.pubkey, + required_accounts.get(1).unwrap().is_signer.into(), + required_accounts.get(1).unwrap().is_writable.into(), &mut lamports2, &mut data2, &owner, @@ -891,182 +1156,115 @@ mod tests { Epoch::default(), ), AccountInfo::new( - &pubkey3, - false, - false, + &extra_meta3.pubkey, + required_accounts.get(2).unwrap().is_signer.into(), + required_accounts.get(2).unwrap().is_writable.into(), &mut lamports3, &mut data3, &owner, false, Epoch::default(), ), + AccountInfo::new( + &check_required_pda1_pubkey, + required_accounts.get(3).unwrap().is_signer.into(), + required_accounts.get(3).unwrap().is_writable.into(), + &mut lamports_pda1, + &mut data_pda1, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &check_required_pda2_pubkey, + required_accounts.get(4).unwrap().is_signer.into(), + required_accounts.get(4).unwrap().is_writable.into(), + &mut lamports_pda2, + &mut data_pda2, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &check_required_pda3_pubkey, + required_accounts.get(5).unwrap().is_signer.into(), + required_accounts.get(5).unwrap().is_writable.into(), + &mut lamports_pda3, + &mut data_pda3, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &check_required_pda4_pubkey, + required_accounts.get(6).unwrap().is_signer.into(), + required_accounts.get(6).unwrap().is_writable.into(), + &mut lamports_pda4, + &mut data_pda4, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &pubkey_arb_1, + false, + true, + &mut lamports_arb_1, + &mut data_arb_1, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &pubkey_arb_2, + false, + true, + &mut lamports_arb_2, + &mut data_arb_2, + &owner, + false, + Epoch::default(), + ), ]; - // Let's also add 2 required PDAs to the extra required accounts. - - let required_pda1_literal_string = "required_pda1"; - let required_pda2_literal_u32 = 4u32; - - let required_pda1 = ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: required_pda1_literal_string.as_bytes().to_vec(), - }, - Seed::InstructionData { - index: 1, - length: 8, // [u8; 8] - }, - Seed::AccountKey { index: 1 }, - ], - false, - true, - ) - .unwrap(); - let required_pda2 = ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: required_pda2_literal_u32.to_le_bytes().to_vec(), - }, - Seed::InstructionData { - index: 9, - length: 8, // u64 - }, - Seed::AccountKey { index: 5 }, - ], - false, - true, - ) - .unwrap(); - - // The program to CPI to has 2 account metas and - // 5 extra required accounts (3 metas, 2 PDAs). - - // Now we set up the validation account data - - let mut required_accounts = extra_account_infos - .iter() - .map(ExtraAccountMeta::from) - .collect::>(); - required_accounts.push(required_pda1); - required_accounts.push(required_pda2); + // Let's use a mock RPC and set up a test instruction to check the CPI + // instruction against later + let rpc_account_infos = all_account_infos.clone(); + let mock_rpc = MockRpc::setup(&rpc_account_infos); let account_size = ExtraAccountMetaList::size_of(required_accounts.len()).unwrap(); let mut buffer = vec![0; account_size]; - ExtraAccountMetaList::init::(&mut buffer, &required_accounts).unwrap(); - // Make an instruction to check later - // We'll also check the instruction seed components later - let instruction_u8array_arg = [1, 2, 3, 4, 5, 6, 7, 8]; - let instruction_u64_arg = 208u64; - let mut instruction_data = vec![0]; - instruction_data.extend_from_slice(&instruction_u8array_arg); - instruction_data.extend_from_slice(instruction_u64_arg.to_le_bytes().as_ref()); - let mut instruction = Instruction::new_with_bytes(program_id, &instruction_data, ix_accounts.clone()); - ExtraAccountMetaList::add_to_instruction::(&mut instruction, &buffer) - .unwrap(); - - // Now our program is going to use its own set of account infos. - // - // These account infos must contain all required account infos for the CPI. - // - // We'll mess them up a bit to make sure the ordering doesn't matter when - // performing account resolution. - let mut messed_account_infos = Vec::new(); - - // First add the instruction account infos. - messed_account_infos.extend(ix_account_infos.clone()); - - // Next add the extra account infos. - messed_account_infos.extend(extra_account_infos.iter().cloned()); - - // Also add the extra PDAs with their actual addresses. - let check_required_pda1_pubkey = Pubkey::find_program_address( - &[ - required_pda1_literal_string.as_bytes(), - &instruction_u8array_arg, - ix_account_infos.get(1).unwrap().key.as_ref(), // The second account - ], - &program_id, - ) - .0; - let check_required_pda2_pubkey = Pubkey::find_program_address( - &[ - required_pda2_literal_u32.to_le_bytes().as_ref(), - instruction_u64_arg.to_le_bytes().as_ref(), - check_required_pda1_pubkey.as_ref(), // The first PDA should be at index 5 - ], - &program_id, + ExtraAccountMetaList::add_to_instruction::( + &mut instruction, + |pubkey| mock_rpc.get_account_data(pubkey), + &buffer, ) - .0; + .await + .unwrap(); - let mut lamports_pda1 = 0; - let mut data_pda1 = []; - let extra_pda_info1 = AccountInfo::new( - &check_required_pda1_pubkey, - false, - true, - &mut lamports_pda1, - &mut data_pda1, - &owner, - false, - Epoch::default(), - ); - messed_account_infos.push(extra_pda_info1.clone()); + // Perform the account resolution for the CPI instruction - let mut lamports_pda2 = 0; - let mut data_pda2 = []; - let extra_pda_info2 = AccountInfo::new( - &check_required_pda2_pubkey, - false, - true, - &mut lamports_pda2, - &mut data_pda2, - &owner, - false, - Epoch::default(), - ); - messed_account_infos.push(extra_pda_info2.clone()); - - // Now throw in a few extras that might be just for our program. - let pubkey4 = Pubkey::new_unique(); - let mut lamports4 = 0; - let mut data4 = []; - messed_account_infos.push(AccountInfo::new( - &pubkey4, - false, - true, - &mut lamports4, - &mut data4, - &owner, - false, - Epoch::default(), - )); - let pubkey5 = Pubkey::new_unique(); - let mut lamports5 = 0; - let mut data5 = []; - messed_account_infos.push(AccountInfo::new( - &pubkey5, - false, - true, - &mut lamports5, - &mut data5, - &owner, - false, - Epoch::default(), - )); + // Create the instruction itself + let mut cpi_instruction = + Instruction::new_with_bytes(program_id, &instruction_data, ix_accounts); - // Mess 'em up! + // Start with the known account infos + let mut cpi_account_infos = + vec![all_account_infos[0].clone(), all_account_infos[1].clone()]; + + // Mess up the ordering of the account infos to make it harder! + let mut messed_account_infos = all_account_infos.clone(); messed_account_infos.swap(0, 4); messed_account_infos.swap(1, 2); messed_account_infos.swap(3, 4); + messed_account_infos.swap(5, 6); + messed_account_infos.swap(8, 7); - // Perform the account resolution. - let mut cpi_instruction = - Instruction::new_with_bytes(program_id, &instruction_data, ix_accounts); - let mut cpi_account_infos = ix_account_infos.to_vec(); + // Resolve the rest! ExtraAccountMetaList::add_to_cpi_instruction::( &mut cpi_instruction, &mut cpi_account_infos, @@ -1081,13 +1279,12 @@ mod tests { // CPI account infos should have the instruction account infos // and the extra required account infos from the validation account, // and they should be in the correct order. - let mut all_account_infos = ix_account_infos.to_vec(); - all_account_infos.extend(extra_account_infos.iter().cloned()); - all_account_infos.push(extra_pda_info1); - all_account_infos.push(extra_pda_info2); - - assert_eq!(cpi_account_infos.len(), all_account_infos.len()); - for (a, b) in std::iter::zip(cpi_account_infos, all_account_infos) { + // Note: The two additional arbitrary account infos for the currently + // executing program won't be present in the CPI instruction's account + // infos, so we will omit them (hence the `..9`). + let check_account_infos = &all_account_infos[..9]; + assert_eq!(cpi_account_infos.len(), check_account_infos.len()); + for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) { assert_eq!(a.key, b.key); assert_eq!(a.is_signer, b.is_signer); assert_eq!(a.is_writable, b.is_writable); @@ -1120,18 +1317,6 @@ mod tests { true, ) .unwrap(), - ExtraAccountMeta::new_external_pda_with_seeds( - 1, - &[ - Seed::Literal { - bytes: b"external_pda_seed".to_vec(), - }, - Seed::AccountKey { index: 4 }, - ], - false, - false, - ) - .unwrap(), ]; // Create the validation data @@ -1161,10 +1346,6 @@ mod tests { &program_id, ) .0; - let mut lamports4 = 0; - let mut data4 = []; - let external_pda = - Pubkey::find_program_address(&[b"external_pda_seed", pda.as_ref()], &pubkey_ix_2).0; let account_infos = [ // Instruction account 1 AccountInfo::new( @@ -1221,17 +1402,6 @@ mod tests { false, Epoch::default(), ), - // Required account 4 (external PDA) - AccountInfo::new( - &external_pda, - false, - false, - &mut lamports4, - &mut data4, - &owner, - false, - Epoch::default(), - ), ]; // Create another list of account infos to intentionally mess up diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index 0506ca1553e..eb333536793 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -706,10 +706,13 @@ async fn success_transfers_using_onchain_helper() { offchain::resolve_extra_transfer_account_metas( &mut instruction, |address| { - token_a - .get_account(address) - .map_ok(|acc| Some(acc.data)) - .map_err(offchain::AccountFetchError::from) + token_a.get_account(address).map_ok_or_else( + |e| match e { + TokenClientError::AccountNotFound => Ok(None), + _ => Err(offchain::AccountFetchError::from(e)), + }, + |acc| Ok(Some(acc.data)), + ) }, &mint_a, ) @@ -718,10 +721,13 @@ async fn success_transfers_using_onchain_helper() { offchain::resolve_extra_transfer_account_metas( &mut instruction, |address| { - token_a - .get_account(address) - .map_ok(|acc| Some(acc.data)) - .map_err(offchain::AccountFetchError::from) + token_a.get_account(address).map_ok_or_else( + |e| match e { + TokenClientError::AccountNotFound => Ok(None), + _ => Err(offchain::AccountFetchError::from(e)), + }, + |acc| Ok(Some(acc.data)), + ) }, &mint_b, ) diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index 104d6d3e41b..94ca6c5590a 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -34,20 +34,25 @@ use { /// ``` pub async fn resolve_extra_transfer_account_metas( instruction: &mut Instruction, - get_account_data_fn: F, + fetch_account_data_fn: F, mint_address: &Pubkey, ) -> Result<(), AccountFetchError> where F: Fn(Pubkey) -> Fut, Fut: Future, { - let mint_data = get_account_data_fn(*mint_address) + let mint_data = fetch_account_data_fn(*mint_address) .await? .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(instruction, get_account_data_fn, mint_address, &program_id) - .await?; + resolve_extra_account_metas( + instruction, + fetch_account_data_fn, + mint_address, + &program_id, + ) + .await?; } Ok(()) } diff --git a/token/transfer-hook-interface/src/offchain.rs b/token/transfer-hook-interface/src/offchain.rs index e5425e3325e..60b7ea53593 100644 --- a/token/transfer-hook-interface/src/offchain.rs +++ b/token/transfer-hook-interface/src/offchain.rs @@ -1,5 +1,6 @@ //! Offchain helper for fetching required accounts to build instructions +pub use spl_tlv_account_resolution::state::{AccountDataResult, AccountFetchError}; use { crate::{get_extra_account_metas_address, instruction::ExecuteInstruction}, solana_program::{ @@ -11,12 +12,6 @@ use { std::future::Future, }; -/// Type representing the output of an account fetching function, for easy -/// chaining between APIs -pub type AccountDataResult = Result>, AccountFetchError>; -/// Generic error type that can come out of any client while fetching account data -pub type AccountFetchError = Box; - /// Offchain helper to get all additional required account metas for a mint /// /// To be client-agnostic and to avoid pulling in the full solana-sdk, this @@ -42,7 +37,7 @@ pub type AccountFetchError = Box; /// ``` pub async fn resolve_extra_account_metas( instruction: &mut Instruction, - get_account_data_fn: F, + fetch_account_data_fn: F, mint: &Pubkey, permissioned_transfer_program_id: &Pubkey, ) -> Result<(), AccountFetchError> @@ -52,13 +47,15 @@ where { let validation_address = get_extra_account_metas_address(mint, permissioned_transfer_program_id); - let validation_account_data = get_account_data_fn(validation_address) + let validation_account_data = fetch_account_data_fn(validation_address) .await? .ok_or(ProgramError::InvalidAccountData)?; - ExtraAccountMetaList::add_to_instruction::( + ExtraAccountMetaList::add_to_instruction::( instruction, + fetch_account_data_fn, &validation_account_data, - )?; + ) + .await?; // The onchain helpers pull out the required accounts from an opaque // slice by pubkey, so the order doesn't matter here! instruction.accounts.push(AccountMeta::new_readonly(