tlv-account-resolution: Add state interface library for additional accounts (#4146)
This commit is contained in:
parent
ff8710e4db
commit
de05760e78
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue