tlv-account-resolution: Add state interface library for additional accounts (#4146)

This commit is contained in:
Jon Cinque 2023-05-02 16:52:38 +02:00 committed by GitHub
parent ff8710e4db
commit de05760e78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 915 additions and 0 deletions

15
Cargo.lock generated
View File

@ -6354,6 +6354,21 @@ dependencies = [
"spl-token 3.5.0",
]
[[package]]
name = "spl-tlv-account-resolution"
version = "0.1.0"
dependencies = [
"bytemuck",
"num-derive",
"num-traits",
"num_enum",
"solana-program",
"solana-program-test",
"solana-sdk",
"spl-type-length-value",
"thiserror",
]
[[package]]
name = "spl-token"
version = "3.5.0"

View File

@ -25,6 +25,7 @@ members = [
"libraries/math",
"libraries/concurrent-merkle-tree",
"libraries/merkle-tree-reference",
"libraries/tlv-account-resolution",
"libraries/type-length-value",
"memo/program",
"name-service/program",

View File

@ -0,0 +1,30 @@
[package]
name = "spl-tlv-account-resolution"
version = "0.1.0"
description = "Solana Program Library TLV Account Resolution Interface"
authors = ["Solana Labs Maintainers <maintainers@solanalabs.com>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2021"
[features]
test-sbf = []
[dependencies]
bytemuck = { version = "1.13.0", features = ["derive"] }
num-derive = "0.3"
num-traits = "0.2"
num_enum = "0.5.9"
solana-program = "1.14.12"
spl-type-length-value = { version = "0.1", path = "../type-length-value" }
thiserror = "1.0"
[dev-dependencies]
solana-program-test = "1.14.12"
solana-sdk = "1.14.12"
[lib]
crate-type = ["cdylib", "lib"]
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@ -0,0 +1,139 @@
# TLV Account Resolution
Library defining a generic state interface to encode additional required accounts
for an instruction, using Type-Length-Value structures.
## Example usage
If you want to encode the additional required accounts for your instruction
into a TLV entry in an account, you can do the following:
```rust
use {
solana_program::{account_info::AccountInfo, instruction::{AccountMeta, Instruction}, pubkey::Pubkey},
spl_type_length_value::discriminator::{Discriminator, TlvDiscriminator},
spl_tlv_account_resolution::state::ExtraAccountMetas,
};
struct MyInstruction;
impl TlvDiscriminator for MyInstruction {
// For ease of use, give it the same discriminator as its instruction definition
const TLV_DISCRIMINATOR: Discriminator = Discriminator::new([1; Discriminator::LENGTH]);
}
// Actually put it in the additional required account keys and signer / writable
let extra_metas = [
AccountMeta::new(Pubkey::new_unique(), false),
AccountMeta::new(Pubkey::new_unique(), true),
AccountMeta::new_readonly(Pubkey::new_unique(), true),
AccountMeta::new_readonly(Pubkey::new_unique(), false),
];
// Assume that this buffer is actually account data, already allocated to `account_size`
let account_size = ExtraAccountMetas::size_of(extra_metas.len()).unwrap();
let mut buffer = vec![0; account_size];
// Initialize the structure for your instruction
ExtraAccountMetas::init_with_account_metas::<MyInstruction>(&mut buffer, &extra_metas).unwrap();
// Off-chain, you can add the additional accounts directly from the account data
let program_id = Pubkey::new_unique();
let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
ExtraAccountMetas::add_to_instruction::<MyInstruction>(&mut instruction, &buffer).unwrap();
// On-chain, you can add the additional accounts *and* account infos
let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
// Include all of the well-known required account infos here first
let mut cpi_account_infos = vec![];
// Provide all "remaining_account_infos" that are *not* part of any other known interface
let remaining_account_infos = &[];
ExtraAccountMetas::add_to_cpi_instruction::<MyInstruction>(
&mut cpi_instruction,
&mut cpi_account_infos,
&buffer,
&remaining_account_infos,
).unwrap();
```
For ease of use on-chain, `ExtraAccountMetas::init_with_account_infos` is also
provided to initialize directly from a set of given accounts.
## Motivation
The Solana account model presents unique challeneges for program interfaces.
Since it's impossible to load additional accounts on-chain, if a program requires
additional accounts to properly implement an instruction, there's no clear way
for clients to fetch these accounts.
There are two main ways to fetch additional accounts, dynamically through program
simulation, or statically by fetching account data. This library implements
additional account resolution statically. You can find more information about
dynamic account resolution in the Appendix.
### Static Account Resolution
It's possible for programs to write the additional required account infos
into account data, so that on-chain and off-chain clients simply need to read
the data to figure out the additional required accounts.
Rather than exposing this data dynamically through program execution, this method
uses static account data.
For example, let's imagine there's a `Transferable` interface, along with a
`transfer` instruction. Some programs that implement `transfer` may need more
accounts than just the ones defined in the interface. How does a an on-chain or
off-chain client figure out the additional required accounts?
The "static" approach requires programs to write the extra required accounts to
an account defined at a given address. This could be directly in the `mint`, or
some address derivable from the mint address.
Off-chain, a client must fetch this additional account and read its data to find
out the additional required accounts, and then include them in the instruction.
On-chain, a program must have access to "remaining account infos" containing the
special account and all other required accounts to properly create the CPI
instruction and give the correct account infos.
This approach could also be called a "state interface".
## How it works
This library uses `spl-type-length-value` to read and write required instruction
accounts from account data.
Interface instructions must have an 8-byte discriminator, so that the exposed
`ExtraAccountMetas` type can use the instruction discriminator as a `TlvDiscriminator`.
This can be confusing. Typically, a type implements `TlvDiscriminator`, so that
the type can be written into TLV data. In this case, `ExtraAccountMetas` is
generic over `TlvDiscriminator`, meaning that a program can write many different instances of
`ExtraAccountMetas` into one account, using different `TlvDiscriminator`s.
Also, it's reusing an instruction discriminator as a TLV discriminator. For example,
if the `transfer` instruction has a discriminator of `[1, 2, 3, 4, 5, 6, 7, 8]`,
then the account uses a TLV discriminator of `[1, 2, 3, 4, 5, 6, 7, 8]` to denote
where the additional account metas are stored.
This isn't required, but makes it easier for clients to find the additional
required accounts for an instruction.
## Appendix
### Dynamic Account Resolution
To expose the additional accounts required, instruction interfaces can include
supplemental instructions to return the required accounts.
For example, in the `Transferable` interface example, along with a `transfer`
instruction, also requires implementations to expose a
`get_additional_accounts_for_transfer` instruction.
In the program implementation, this instruction writes the additional accounts
into return data, making it easy for on-chain and off-chain clients to consume.
See the
[relevant sRFC](https://forum.solana.com/t/srfc-00010-additional-accounts-request-transfer-spec/122)
for more information about the dynamic approach.

View File

@ -0,0 +1,72 @@
//! Error types
use {
num_derive::FromPrimitive,
solana_program::{
decode_error::DecodeError,
msg,
program_error::{PrintProgramError, ProgramError},
},
thiserror::Error,
};
/// Errors that may be returned by the Account Resolution library.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum AccountResolutionError {
/// Incorrect account provided
#[error("Incorrect account provided")]
IncorrectAccount,
/// Not enough accounts provided
#[error("Not enough accounts provided")]
NotEnoughAccounts,
/// No value initialized in TLV data
#[error("No value initialized in TLV data")]
TlvUninitialized,
/// Some value initialized in TLV data
#[error("Some value initialized in TLV data")]
TlvInitialized,
/// Provided byte buffer too small for validation pubkeys
#[error("Provided byte buffer too small for validation pubkeys")]
BufferTooSmall,
/// Error in checked math operation
#[error("Error in checked math operation")]
CalculationFailure,
/// Too many pubkeys provided
#[error("Too many pubkeys provided")]
TooManyPubkeys,
/// Provided byte buffer too large for expected type
#[error("Provided byte buffer too large for expected type")]
BufferTooLarge,
}
impl From<AccountResolutionError> for ProgramError {
fn from(e: AccountResolutionError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for AccountResolutionError {
fn type_of() -> &'static str {
"AccountResolutionError"
}
}
impl PrintProgramError for AccountResolutionError {
fn print<E>(&self)
where
E: 'static
+ std::error::Error
+ DecodeError<E>
+ PrintProgramError
+ num_traits::FromPrimitive,
{
match self {
Self::IncorrectAccount => msg!("Incorrect account provided"),
Self::NotEnoughAccounts => msg!("Not enough accounts provided"),
Self::TlvUninitialized => msg!("No value initialized in TLV data"),
Self::TlvInitialized => msg!("Some value initialized in TLV data"),
Self::BufferTooSmall => msg!("Provided byte buffer too small for validation pubkeys"),
Self::CalculationFailure => msg!("Error in checked math operation"),
Self::TooManyPubkeys => msg!("Too many pubkeys provided"),
Self::BufferTooLarge => msg!("Provided byte buffer too large for expected type"),
}
}
}

View File

@ -0,0 +1,14 @@
//! Crate defining a state interface for offchain account resolution. If a program
//! writes the proper state information into one of their accounts, any offchain
//! and onchain client can fetch any additional required accounts for an instruction.
#![allow(clippy::integer_arithmetic)]
#![deny(missing_docs)]
#![cfg_attr(not(test), forbid(unsafe_code))]
pub mod error;
pub mod pod;
pub mod state;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;

View File

@ -0,0 +1,199 @@
//! Pod types to be used with bytemuck for zero-copy serde
use {
crate::error::AccountResolutionError,
bytemuck::{Pod, Zeroable},
solana_program::{
account_info::AccountInfo, instruction::AccountMeta, program_error::ProgramError,
pubkey::Pubkey,
},
spl_type_length_value::pod::{pod_from_bytes, pod_from_bytes_mut, PodU32},
};
/// Convert a slice into a mutable `Pod` slice (zero copy)
pub fn pod_slice_from_bytes<T: Pod>(bytes: &[u8]) -> Result<&[T], ProgramError> {
bytemuck::try_cast_slice(bytes).map_err(|_| ProgramError::InvalidArgument)
}
/// Convert a slice into a mutable `Pod` slice (zero copy)
pub fn pod_slice_from_bytes_mut<T: Pod>(bytes: &mut [u8]) -> Result<&mut [T], ProgramError> {
bytemuck::try_cast_slice_mut(bytes).map_err(|_| ProgramError::InvalidArgument)
}
/// The standard `bool` is not a `Pod`, define a replacement that is
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
#[repr(transparent)]
pub struct PodBool(u8);
impl From<bool> for PodBool {
fn from(b: bool) -> Self {
Self(if b { 1 } else { 0 })
}
}
impl From<&PodBool> for bool {
fn from(b: &PodBool) -> Self {
b.0 != 0
}
}
impl From<PodBool> for bool {
fn from(b: PodBool) -> Self {
b.0 != 0
}
}
/// The standard `AccountMeta` is not a `Pod`, define a replacement that is
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct PodAccountMeta {
/// The pubkey of the account
pub pubkey: Pubkey,
/// Whether the account should sign
pub is_signer: PodBool,
/// Whether the account should be writable
pub is_writable: PodBool,
}
impl PartialEq<AccountInfo<'_>> for PodAccountMeta {
fn eq(&self, other: &AccountInfo) -> bool {
self.pubkey == *other.key
&& self.is_signer == other.is_signer.into()
&& self.is_writable == other.is_writable.into()
}
}
impl From<&AccountInfo<'_>> for PodAccountMeta {
fn from(account_info: &AccountInfo) -> Self {
Self {
pubkey: *account_info.key,
is_signer: account_info.is_signer.into(),
is_writable: account_info.is_writable.into(),
}
}
}
impl From<&AccountMeta> for PodAccountMeta {
fn from(meta: &AccountMeta) -> Self {
Self {
pubkey: meta.pubkey,
is_signer: meta.is_signer.into(),
is_writable: meta.is_writable.into(),
}
}
}
impl From<&PodAccountMeta> for AccountMeta {
fn from(meta: &PodAccountMeta) -> Self {
Self {
pubkey: meta.pubkey,
is_signer: meta.is_signer.into(),
is_writable: meta.is_writable.into(),
}
}
}
const LENGTH_SIZE: usize = std::mem::size_of::<PodU32>();
/// Special type for using a slice of `Pod`s in a zero-copy way
pub struct PodSlice<'data, T: Pod> {
length: &'data PodU32,
data: &'data [T],
}
impl<'data, T: Pod> PodSlice<'data, T> {
/// Unpack the buffer into a slice
pub fn unpack<'a>(data: &'a [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
if data.len() < LENGTH_SIZE {
return Err(AccountResolutionError::BufferTooSmall.into());
}
let (length, data) = data.split_at(LENGTH_SIZE);
let length = pod_from_bytes::<PodU32>(length)?;
let _max_length = max_len_for_type::<T>(data.len())?;
let data = pod_slice_from_bytes(data)?;
Ok(Self { length, data })
}
/// Get the slice data
pub fn data(&self) -> &[T] {
let length = u32::from(*self.length) as usize;
&self.data[..length]
}
/// Get the amount of bytes used by `num_items`
pub fn size_of(num_items: usize) -> Result<usize, ProgramError> {
std::mem::size_of::<T>()
.checked_mul(num_items)
.and_then(|len| len.checked_add(LENGTH_SIZE))
.ok_or_else(|| AccountResolutionError::CalculationFailure.into())
}
}
/// Special type for using a slice of mutable `Pod`s in a zero-copy way
pub struct PodSliceMut<'data, T: Pod> {
length: &'data mut PodU32,
data: &'data mut [T],
max_length: usize,
}
impl<'data, T: Pod> PodSliceMut<'data, T> {
/// Unpack the mutable buffer into a mutable slice, with the option to
/// initialize the data
fn unpack_internal<'a>(data: &'a mut [u8], init: bool) -> Result<Self, ProgramError>
where
'a: 'data,
{
if data.len() < LENGTH_SIZE {
return Err(AccountResolutionError::BufferTooSmall.into());
}
let (length, data) = data.split_at_mut(LENGTH_SIZE);
let length = pod_from_bytes_mut::<PodU32>(length)?;
if init {
*length = 0.into();
}
let max_length = max_len_for_type::<T>(data.len())?;
let data = pod_slice_from_bytes_mut(data)?;
Ok(Self {
length,
data,
max_length,
})
}
/// Unpack the mutable buffer into a mutable slice
pub fn unpack<'a>(data: &'a mut [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
Self::unpack_internal(data, /* init */ false)
}
/// Unpack the mutable buffer into a mutable slice, and initialize the
/// slice to 0-length
pub fn init<'a>(data: &'a mut [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
Self::unpack_internal(data, /* init */ true)
}
/// Add another item to the slice
pub fn push(&mut self, t: T) -> Result<(), ProgramError> {
let length = u32::from(*self.length);
if length as usize == self.max_length {
Err(AccountResolutionError::BufferTooSmall.into())
} else {
self.data[length as usize] = t;
*self.length = length.saturating_add(1).into();
Ok(())
}
}
}
fn max_len_for_type<T>(data_len: usize) -> Result<usize, ProgramError> {
let size: usize = std::mem::size_of::<T>();
let max_len = data_len
.checked_div(size)
.ok_or(AccountResolutionError::CalculationFailure)?;
// check that it isn't overallocated
if max_len.saturating_mul(size) != data_len {
Err(AccountResolutionError::BufferTooLarge.into())
} else {
Ok(max_len)
}
}

View File

@ -0,0 +1,445 @@
//! State transition types
use {
crate::{
error::AccountResolutionError,
pod::{PodAccountMeta, PodSlice, PodSliceMut},
},
solana_program::{
account_info::AccountInfo,
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
},
spl_type_length_value::{
discriminator::TlvDiscriminator,
state::{TlvState, TlvStateBorrowed, TlvStateMut},
},
};
/// Stateless helper for storing additional accounts required for an instruction.
///
/// This struct works with any `TlvDiscriminator`, and stores the extra accounts
/// needed for that specific instruction, using the given `Discriminator` as the
/// type-length-value `Discriminator`, and then storing all of the given
/// `AccountMeta`s as a zero-copy slice.
///
/// Sample usage:
///
/// ```
/// use {
/// solana_program::{
/// account_info::AccountInfo, instruction::{AccountMeta, Instruction},
/// pubkey::Pubkey
/// },
/// spl_type_length_value::discriminator::{Discriminator, TlvDiscriminator},
/// spl_tlv_account_resolution::state::ExtraAccountMetas,
/// };
///
/// struct MyInstruction;
/// impl TlvDiscriminator for MyInstruction {
/// // Give it a unique discriminator, can also be generated using a hash function
/// const TLV_DISCRIMINATOR: Discriminator = Discriminator::new([1; Discriminator::LENGTH]);
/// }
///
/// // actually put it in the additional required account keys and signer / writable
/// let extra_metas = [
/// AccountMeta::new(Pubkey::new_unique(), false),
/// AccountMeta::new(Pubkey::new_unique(), true),
/// AccountMeta::new_readonly(Pubkey::new_unique(), true),
/// AccountMeta::new_readonly(Pubkey::new_unique(), false),
/// ];
///
/// // assume that this buffer is actually account data, already allocated to `account_size`
/// let account_size = ExtraAccountMetas::size_of(extra_metas.len()).unwrap();
/// let mut buffer = vec![0; account_size];
///
/// // Initialize the structure for your instruction
/// ExtraAccountMetas::init_with_account_metas::<MyInstruction>(&mut buffer, &extra_metas).unwrap();
///
/// // Off-chain, you can add the additional accounts directly from the account data
/// let program_id = Pubkey::new_unique();
/// let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
/// ExtraAccountMetas::add_to_instruction::<MyInstruction>(&mut instruction, &buffer).unwrap();
///
/// // On-chain, you can add the additional accounts *and* account infos
/// let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
/// let mut cpi_account_infos = vec![]; // assume the other required account infos are already included
/// let remaining_account_infos: &[AccountInfo<'_>] = &[]; // these are the account infos provided to the instruction that are *not* part of any other known interface
/// ExtraAccountMetas::add_to_cpi_instruction::<MyInstruction>(
/// &mut cpi_instruction,
/// &mut cpi_account_infos,
/// &buffer,
/// &remaining_account_infos,
/// );
/// ```
pub struct ExtraAccountMetas;
impl ExtraAccountMetas {
/// Initialize pod slice data for the given instruction and any type
/// convertible to account metas
pub fn init<'a, T: TlvDiscriminator, M>(
data: &mut [u8],
convertible_account_metas: &'a [M],
) -> Result<(), ProgramError>
where
PodAccountMeta: From<&'a M>,
{
let mut state = TlvStateMut::unpack(data).unwrap();
let tlv_size = PodSlice::<PodAccountMeta>::size_of(convertible_account_metas.len())?;
let bytes = state.alloc::<T>(tlv_size)?;
let mut extra_account_metas = PodSliceMut::init(bytes)?;
for account_metas in convertible_account_metas {
extra_account_metas.push(PodAccountMeta::from(account_metas))?;
}
Ok(())
}
/// Initialize a TLV entry for the given discriminator, populating the data
/// with the given account infos
pub fn init_with_account_infos<T: TlvDiscriminator>(
data: &mut [u8],
account_infos: &[AccountInfo<'_>],
) -> Result<(), ProgramError> {
Self::init::<T, AccountInfo>(data, account_infos)
}
/// Initialize a TLV entry for the given discriminator, populating the data
/// with the given account metas
pub fn init_with_account_metas<T: TlvDiscriminator>(
data: &mut [u8],
account_metas: &[AccountMeta],
) -> Result<(), ProgramError> {
Self::init::<T, AccountMeta>(data, account_metas)
}
/// Get the byte size required to hold `num_items` items
pub fn size_of(num_items: usize) -> Result<usize, ProgramError> {
Ok(TlvStateBorrowed::get_base_len()
.saturating_add(PodSlice::<PodAccountMeta>::size_of(num_items)?))
}
/// Add the additional account metas to an existing instruction
pub fn add_to_instruction<T: TlvDiscriminator>(
instruction: &mut Instruction,
data: &[u8],
) -> Result<(), ProgramError> {
let state = TlvStateBorrowed::unpack(data)?;
let bytes = state.get_bytes::<T>()?;
let extra_account_metas = PodSlice::<PodAccountMeta>::unpack(bytes)?;
instruction
.accounts
.extend(extra_account_metas.data().iter().map(|m| m.into()));
Ok(())
}
/// Add the additional account metas and account infos for a CPI
pub fn add_to_cpi_instruction<'a, T: TlvDiscriminator>(
cpi_instruction: &mut Instruction,
cpi_account_infos: &mut Vec<AccountInfo<'a>>,
data: &[u8],
account_infos: &[AccountInfo<'a>],
) -> Result<(), ProgramError> {
let state = TlvStateBorrowed::unpack(data)?;
let bytes = state.get_bytes::<T>()?;
let extra_account_metas = PodSlice::<PodAccountMeta>::unpack(bytes)?;
for account_meta in extra_account_metas.data().iter().map(AccountMeta::from) {
let account_info = account_infos
.iter()
.find(|&x| *x.key == account_meta.pubkey)
.ok_or(AccountResolutionError::IncorrectAccount)?
.clone();
cpi_account_infos.push(account_info);
cpi_instruction.accounts.push(account_meta);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use {
super::*,
solana_program::{clock::Epoch, instruction::AccountMeta, pubkey::Pubkey},
spl_type_length_value::discriminator::Discriminator,
};
pub struct TestInstruction;
impl TlvDiscriminator for TestInstruction {
const TLV_DISCRIMINATOR: Discriminator = Discriminator::new([1; Discriminator::LENGTH]);
}
pub struct TestOtherInstruction;
impl TlvDiscriminator for TestOtherInstruction {
const TLV_DISCRIMINATOR: Discriminator = Discriminator::new([2; Discriminator::LENGTH]);
}
#[test]
fn init_with_metas() {
let metas = [
AccountMeta::new(Pubkey::new_unique(), false),
AccountMeta::new(Pubkey::new_unique(), true),
AccountMeta::new_readonly(Pubkey::new_unique(), true),
AccountMeta::new_readonly(Pubkey::new_unique(), false),
];
let account_size = ExtraAccountMetas::size_of(metas.len()).unwrap();
let mut buffer = vec![0; account_size];
ExtraAccountMetas::init_with_account_metas::<TestInstruction>(&mut buffer, &metas).unwrap();
let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]);
ExtraAccountMetas::add_to_instruction::<TestInstruction>(&mut instruction, &buffer)
.unwrap();
assert_eq!(
instruction
.accounts
.iter()
.map(PodAccountMeta::from)
.collect::<Vec<_>>(),
metas.iter().map(PodAccountMeta::from).collect::<Vec<_>>()
);
}
#[test]
fn init_multiple() {
let metas = [
AccountMeta::new(Pubkey::new_unique(), false),
AccountMeta::new(Pubkey::new_unique(), true),
AccountMeta::new_readonly(Pubkey::new_unique(), true),
AccountMeta::new_readonly(Pubkey::new_unique(), false),
];
let other_metas = [AccountMeta::new(Pubkey::new_unique(), false)];
let account_size = ExtraAccountMetas::size_of(metas.len()).unwrap()
+ ExtraAccountMetas::size_of(other_metas.len()).unwrap();
let mut buffer = vec![0; account_size];
ExtraAccountMetas::init_with_account_metas::<TestInstruction>(&mut buffer, &metas).unwrap();
ExtraAccountMetas::init_with_account_metas::<TestOtherInstruction>(
&mut buffer,
&other_metas,
)
.unwrap();
let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]);
ExtraAccountMetas::add_to_instruction::<TestInstruction>(&mut instruction, &buffer)
.unwrap();
assert_eq!(
instruction
.accounts
.iter()
.map(PodAccountMeta::from)
.collect::<Vec<_>>(),
metas.iter().map(PodAccountMeta::from).collect::<Vec<_>>()
);
let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]);
ExtraAccountMetas::add_to_instruction::<TestOtherInstruction>(&mut instruction, &buffer)
.unwrap();
assert_eq!(
instruction
.accounts
.iter()
.map(PodAccountMeta::from)
.collect::<Vec<_>>(),
other_metas
.iter()
.map(PodAccountMeta::from)
.collect::<Vec<_>>()
);
}
#[test]
fn init_mixed() {
// annoying to setup, but need to test this!
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 account_infos = [
AccountInfo::new(
&pubkey1,
false,
true,
&mut lamports1,
&mut data1,
&owner,
false,
Epoch::default(),
),
AccountInfo::new(
&pubkey2,
true,
false,
&mut lamports2,
&mut data2,
&owner,
false,
Epoch::default(),
),
AccountInfo::new(
&pubkey3,
false,
false,
&mut lamports3,
&mut data3,
&owner,
false,
Epoch::default(),
),
];
let metas = [
AccountMeta::new(Pubkey::new_unique(), false),
AccountMeta::new(Pubkey::new_unique(), true),
AccountMeta::new_readonly(Pubkey::new_unique(), true),
AccountMeta::new_readonly(Pubkey::new_unique(), false),
AccountMeta::new_readonly(Pubkey::new_unique(), false),
AccountMeta::new_readonly(Pubkey::new_unique(), false),
];
let account_size = ExtraAccountMetas::size_of(account_infos.len()).unwrap()
+ ExtraAccountMetas::size_of(metas.len()).unwrap();
let mut buffer = vec![0; account_size];
ExtraAccountMetas::init_with_account_infos::<TestInstruction>(&mut buffer, &account_infos)
.unwrap();
ExtraAccountMetas::init_with_account_metas::<TestOtherInstruction>(&mut buffer, &metas)
.unwrap();
let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]);
ExtraAccountMetas::add_to_instruction::<TestInstruction>(&mut instruction, &buffer)
.unwrap();
assert_eq!(
instruction
.accounts
.iter()
.map(PodAccountMeta::from)
.collect::<Vec<_>>(),
account_infos
.iter()
.map(PodAccountMeta::from)
.collect::<Vec<_>>()
);
let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]);
ExtraAccountMetas::add_to_instruction::<TestOtherInstruction>(&mut instruction, &buffer)
.unwrap();
assert_eq!(
instruction
.accounts
.iter()
.map(PodAccountMeta::from)
.collect::<Vec<_>>(),
metas.iter().map(PodAccountMeta::from).collect::<Vec<_>>()
);
}
#[test]
fn cpi_instruction() {
// annoying to setup, but need to test this!
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 account_infos = [
AccountInfo::new(
&pubkey1,
false,
true,
&mut lamports1,
&mut data1,
&owner,
false,
Epoch::default(),
),
AccountInfo::new(
&pubkey2,
true,
false,
&mut lamports2,
&mut data2,
&owner,
false,
Epoch::default(),
),
AccountInfo::new(
&pubkey3,
false,
false,
&mut lamports3,
&mut data3,
&owner,
false,
Epoch::default(),
),
];
let account_size = ExtraAccountMetas::size_of(account_infos.len()).unwrap();
let mut buffer = vec![0; account_size];
ExtraAccountMetas::init_with_account_infos::<TestInstruction>(&mut buffer, &account_infos)
.unwrap();
// make an instruction to check later
let program_id = Pubkey::new_unique();
let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
ExtraAccountMetas::add_to_instruction::<TestInstruction>(&mut instruction, &buffer)
.unwrap();
// mess around with the account infos to make it harder
let mut messed_account_infos = account_infos.to_vec();
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(),
));
messed_account_infos.swap(0, 4);
messed_account_infos.swap(1, 2);
let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
let mut cpi_account_infos = vec![];
ExtraAccountMetas::add_to_cpi_instruction::<TestInstruction>(
&mut cpi_instruction,
&mut cpi_account_infos,
&buffer,
&messed_account_infos,
)
.unwrap();
assert_eq!(cpi_instruction, instruction);
assert_eq!(cpi_account_infos.len(), account_infos.len());
for (a, b) in std::iter::zip(cpi_account_infos, account_infos) {
assert_eq!(a.key, b.key);
assert_eq!(a.is_signer, b.is_signer);
assert_eq!(a.is_writable, b.is_writable);
}
}
}