transfer-hook-interface: Add interface defining a hook called during token-2022 transfer (#4147)
This commit is contained in:
parent
de05760e78
commit
d92664f6df
|
@ -64,6 +64,9 @@ jobs:
|
||||||
- name: Build and test ATA
|
- name: Build and test ATA
|
||||||
run: ./ci/cargo-test-sbf.sh associated-token-account
|
run: ./ci/cargo-test-sbf.sh associated-token-account
|
||||||
|
|
||||||
|
- name: Build and test transfer hook example
|
||||||
|
run: ./ci/cargo-test-sbf.sh token/transfer-hook-example
|
||||||
|
|
||||||
- name: Build and test token-2022 with "serde" activated
|
- name: Build and test token-2022 with "serde" activated
|
||||||
run: |
|
run: |
|
||||||
cargo +"${{ env.RUST_STABLE }}" test \
|
cargo +"${{ env.RUST_STABLE }}" test \
|
||||||
|
|
|
@ -6618,6 +6618,37 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spl-transfer-hook-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"solana-program",
|
||||||
|
"solana-program-test",
|
||||||
|
"solana-sdk",
|
||||||
|
"spl-tlv-account-resolution",
|
||||||
|
"spl-token-2022 0.6.1",
|
||||||
|
"spl-token-client",
|
||||||
|
"spl-transfer-hook-interface",
|
||||||
|
"spl-type-length-value",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spl-transfer-hook-interface"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"bytemuck",
|
||||||
|
"num-derive",
|
||||||
|
"num-traits",
|
||||||
|
"num_enum",
|
||||||
|
"solana-program",
|
||||||
|
"solana-sdk",
|
||||||
|
"spl-tlv-account-resolution",
|
||||||
|
"spl-type-length-value",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spl-type-length-value"
|
name = "spl-type-length-value"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -46,6 +46,8 @@ members = [
|
||||||
"token/program",
|
"token/program",
|
||||||
"token/program-2022",
|
"token/program-2022",
|
||||||
"token/program-2022-test",
|
"token/program-2022-test",
|
||||||
|
"token/transfer-hook-example",
|
||||||
|
"token/transfer-hook-interface",
|
||||||
"token/client",
|
"token/client",
|
||||||
"utils/cgen",
|
"utils/cgen",
|
||||||
"utils/test-client",
|
"utils/test-client",
|
||||||
|
|
|
@ -102,6 +102,18 @@ impl ExtraAccountMetas {
|
||||||
Self::init::<T, AccountInfo>(data, account_infos)
|
Self::init::<T, AccountInfo>(data, account_infos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the underlying `PodSlice<PodAccountMeta>` from an unpacked TLV
|
||||||
|
///
|
||||||
|
/// Due to lifetime annoyances, this function can't just take in the bytes,
|
||||||
|
/// since then we would be returning a reference to a locally created
|
||||||
|
/// `TlvStateBorrowed`. I hope there's a better way to do this!
|
||||||
|
pub fn unpack_with_tlv_state<'a, T: TlvDiscriminator>(
|
||||||
|
tlv_state: &'a TlvStateBorrowed,
|
||||||
|
) -> Result<PodSlice<'a, PodAccountMeta>, ProgramError> {
|
||||||
|
let bytes = tlv_state.get_bytes::<T>()?;
|
||||||
|
PodSlice::<PodAccountMeta>::unpack(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize a TLV entry for the given discriminator, populating the data
|
/// Initialize a TLV entry for the given discriminator, populating the data
|
||||||
/// with the given account metas
|
/// with the given account metas
|
||||||
pub fn init_with_account_metas<T: TlvDiscriminator>(
|
pub fn init_with_account_metas<T: TlvDiscriminator>(
|
||||||
|
@ -118,19 +130,25 @@ impl ExtraAccountMetas {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add the additional account metas to an existing instruction
|
/// Add the additional account metas to an existing instruction
|
||||||
pub fn add_to_instruction<T: TlvDiscriminator>(
|
pub fn add_to_vec<T: TlvDiscriminator>(
|
||||||
instruction: &mut Instruction,
|
account_metas: &mut Vec<AccountMeta>,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> Result<(), ProgramError> {
|
) -> Result<(), ProgramError> {
|
||||||
let state = TlvStateBorrowed::unpack(data)?;
|
let state = TlvStateBorrowed::unpack(data)?;
|
||||||
let bytes = state.get_bytes::<T>()?;
|
let bytes = state.get_bytes::<T>()?;
|
||||||
let extra_account_metas = PodSlice::<PodAccountMeta>::unpack(bytes)?;
|
let extra_account_metas = PodSlice::<PodAccountMeta>::unpack(bytes)?;
|
||||||
instruction
|
account_metas.extend(extra_account_metas.data().iter().map(|m| m.into()));
|
||||||
.accounts
|
|
||||||
.extend(extra_account_metas.data().iter().map(|m| m.into()));
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add the additional account metas to an existing instruction
|
||||||
|
pub fn add_to_instruction<T: TlvDiscriminator>(
|
||||||
|
instruction: &mut Instruction,
|
||||||
|
data: &[u8],
|
||||||
|
) -> Result<(), ProgramError> {
|
||||||
|
Self::add_to_vec::<T>(&mut instruction.accounts, data)
|
||||||
|
}
|
||||||
|
|
||||||
/// Add the additional account metas and account infos for a CPI
|
/// Add the additional account metas and account infos for a CPI
|
||||||
pub fn add_to_cpi_instruction<'a, T: TlvDiscriminator>(
|
pub fn add_to_cpi_instruction<'a, T: TlvDiscriminator>(
|
||||||
cpi_instruction: &mut Instruction,
|
cpi_instruction: &mut Instruction,
|
||||||
|
|
|
@ -34,6 +34,11 @@ impl AsRef<[u8]> for Discriminator {
|
||||||
&self.0[..]
|
&self.0[..]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl AsRef<[u8; Discriminator::LENGTH]> for Discriminator {
|
||||||
|
fn as_ref(&self) -> &[u8; Discriminator::LENGTH] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
impl From<u64> for Discriminator {
|
impl From<u64> for Discriminator {
|
||||||
fn from(from: u64) -> Self {
|
fn from(from: u64) -> Self {
|
||||||
Self(from.to_le_bytes())
|
Self(from.to_le_bytes())
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
[package]
|
||||||
|
name = "spl-transfer-hook-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Solana Program Library Transfer Hook Example Program"
|
||||||
|
authors = ["Solana Labs Maintainers <maintainers@solanalabs.com>"]
|
||||||
|
repository = "https://github.com/solana-labs/solana-program-library"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
no-entrypoint = []
|
||||||
|
test-sbf = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
arrayref = "0.3.6"
|
||||||
|
solana-program = "1.14.12"
|
||||||
|
spl-tlv-account-resolution = { version = "0.1.0" , path = "../../libraries/tlv-account-resolution" }
|
||||||
|
spl-transfer-hook-interface = { version = "0.1.0" , path = "../transfer-hook-interface" }
|
||||||
|
spl-type-length-value = { version = "0.1.0" , path = "../../libraries/type-length-value" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
solana-program-test = "1.14.12"
|
||||||
|
solana-sdk = "1.14.12"
|
||||||
|
spl-token-2022 = { version = "0.6", path = "../program-2022", features = ["no-entrypoint"] }
|
||||||
|
spl-token-client = { version = "0.5", path = "../client" }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
targets = ["x86_64-unknown-linux-gnu"]
|
|
@ -0,0 +1,54 @@
|
||||||
|
## Transfer-Hook Example
|
||||||
|
|
||||||
|
Full example program and tests implementing the `spl-transfer-hook-interface`,
|
||||||
|
to be used for testing a program that calls into the `spl-transfer-hook-interface`.
|
||||||
|
|
||||||
|
See the
|
||||||
|
[SPL Transfer Hook Interface](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook-interface)
|
||||||
|
code for more information.
|
||||||
|
|
||||||
|
### Example usage of example
|
||||||
|
|
||||||
|
When testing your program that uses `spl-transfer-hook-interface`, you can also
|
||||||
|
import this crate, and then use it with `solana-program-test`, ie:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use {
|
||||||
|
solana_program_test::{processor, ProgramTest},
|
||||||
|
solana_sdk::account::Account,
|
||||||
|
spl_transfer_hook_example::state::example_data,
|
||||||
|
spl_transfer_hook_interface::get_extra_account_metas_address,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn my_program_test() {
|
||||||
|
let mut program_test = ProgramTest::new(
|
||||||
|
"my_program",
|
||||||
|
my_program_id,
|
||||||
|
processor!(my_program_processor),
|
||||||
|
);
|
||||||
|
|
||||||
|
let transfer_hook_program_id = Pubkey::new_unique();
|
||||||
|
program_test.prefer_bpf(false); // BPF won't work, unless you've built this from scratch!
|
||||||
|
program_test.add_program(
|
||||||
|
"spl_transfer_hook_example",
|
||||||
|
transfer_hook_program_id,
|
||||||
|
processor!(spl_transfer_hook_example::processor::process),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mint = Pubkey::new_unique();
|
||||||
|
let extra_accounts_address = get_extra_account_metas_address(&mint, &transfer_hook_program_id);
|
||||||
|
let data = example_data();
|
||||||
|
program_test.add_account(
|
||||||
|
extra_accounts_address,
|
||||||
|
Account {
|
||||||
|
lamports: 1_000_000_000, // a lot, just to be safe
|
||||||
|
data,
|
||||||
|
owner: transfer_hook_program_id,
|
||||||
|
..Account::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// run your test logic!
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,24 @@
|
||||||
|
//! Program entrypoint
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::processor,
|
||||||
|
solana_program::{
|
||||||
|
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
|
||||||
|
program_error::PrintProgramError, pubkey::Pubkey,
|
||||||
|
},
|
||||||
|
spl_transfer_hook_interface::error::TransferHookError,
|
||||||
|
};
|
||||||
|
|
||||||
|
entrypoint!(process_instruction);
|
||||||
|
fn process_instruction(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
instruction_data: &[u8],
|
||||||
|
) -> ProgramResult {
|
||||||
|
if let Err(error) = processor::process(program_id, accounts, instruction_data) {
|
||||||
|
// catch the error so we can print it
|
||||||
|
error.print::<TransferHookError>();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
//! Structs required to verify spl-token-2022 mints.
|
||||||
|
//!
|
||||||
|
//! By copying the required functions here, we avoid a circular dependency
|
||||||
|
//! between spl-token-2022 and this crate.
|
||||||
|
|
||||||
|
use {
|
||||||
|
arrayref::{array_ref, array_refs},
|
||||||
|
solana_program::{program_error::ProgramError, program_option::COption, pubkey::Pubkey},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn unpack_coption_key(src: &[u8; 36]) -> Result<COption<Pubkey>, ProgramError> {
|
||||||
|
let (tag, body) = array_refs![src, 4, 32];
|
||||||
|
match *tag {
|
||||||
|
[0, 0, 0, 0] => Ok(COption::None),
|
||||||
|
[1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))),
|
||||||
|
_ => Err(ProgramError::InvalidAccountData),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the mint authority from the account bytes
|
||||||
|
pub fn get_mint_authority(account_data: &[u8]) -> Result<COption<Pubkey>, ProgramError> {
|
||||||
|
const MINT_SIZE: usize = 82;
|
||||||
|
if account_data.len() < MINT_SIZE {
|
||||||
|
Err(ProgramError::InvalidAccountData)
|
||||||
|
} else {
|
||||||
|
let mint_authority = array_ref![account_data, 0, 36];
|
||||||
|
unpack_coption_key(mint_authority)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
//! Crate defining an example program for performing a hook on transfer, where the
|
||||||
|
//! token program calls into a separate program with additional accounts after
|
||||||
|
//! all other logic, to be sure that a transfer has accomplished all required
|
||||||
|
//! preconditions.
|
||||||
|
|
||||||
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
#![cfg_attr(not(test), forbid(unsafe_code))]
|
||||||
|
|
||||||
|
pub mod inline_spl_token;
|
||||||
|
pub mod processor;
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "no-entrypoint"))]
|
||||||
|
mod entrypoint;
|
||||||
|
|
||||||
|
// Export current sdk types for downstream users building with a different sdk version
|
||||||
|
pub use solana_program;
|
|
@ -0,0 +1,139 @@
|
||||||
|
//! Program state processor
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::inline_spl_token,
|
||||||
|
solana_program::{
|
||||||
|
account_info::{next_account_info, AccountInfo},
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
msg,
|
||||||
|
program::invoke_signed,
|
||||||
|
program_error::ProgramError,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
system_instruction,
|
||||||
|
},
|
||||||
|
spl_tlv_account_resolution::state::ExtraAccountMetas,
|
||||||
|
spl_transfer_hook_interface::{
|
||||||
|
collect_extra_account_metas_signer_seeds,
|
||||||
|
error::TransferHookError,
|
||||||
|
get_extra_account_metas_address, get_extra_account_metas_address_and_bump_seed,
|
||||||
|
instruction::{ExecuteInstruction, TransferHookInstruction},
|
||||||
|
},
|
||||||
|
spl_type_length_value::state::TlvStateBorrowed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Processes an [Execute](enum.TransferHookInstruction.html) instruction.
|
||||||
|
pub fn process_execute(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
_amount: u64,
|
||||||
|
) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
let _source_account_info = next_account_info(account_info_iter)?;
|
||||||
|
let mint_info = next_account_info(account_info_iter)?;
|
||||||
|
let _destination_account_info = next_account_info(account_info_iter)?;
|
||||||
|
let _authority_info = next_account_info(account_info_iter)?;
|
||||||
|
let extra_account_metas_info = next_account_info(account_info_iter)?;
|
||||||
|
|
||||||
|
// For the example program, we just check that the correct pda and validation
|
||||||
|
// pubkeys are provided
|
||||||
|
let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id);
|
||||||
|
if expected_validation_address != *extra_account_metas_info.key {
|
||||||
|
return Err(ProgramError::InvalidSeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = extra_account_metas_info.try_borrow_data()?;
|
||||||
|
let state = TlvStateBorrowed::unpack(&data).unwrap();
|
||||||
|
let extra_account_metas =
|
||||||
|
ExtraAccountMetas::unpack_with_tlv_state::<ExecuteInstruction>(&state)?;
|
||||||
|
|
||||||
|
// if incorrect number of are provided, error
|
||||||
|
let extra_account_infos = account_info_iter.as_slice();
|
||||||
|
let account_metas = extra_account_metas.data();
|
||||||
|
if extra_account_infos.len() != account_metas.len() {
|
||||||
|
return Err(TransferHookError::IncorrectAccount.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's assume that they're provided in the correct order
|
||||||
|
for (i, account_info) in extra_account_infos.iter().enumerate() {
|
||||||
|
if &account_metas[i] != account_info {
|
||||||
|
return Err(TransferHookError::IncorrectAccount.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes a [InitializeExtraAccountMetas](enum.TransferHookInstruction.html) instruction.
|
||||||
|
pub fn process_initialize_extra_account_metas(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
let extra_account_metas_info = next_account_info(account_info_iter)?;
|
||||||
|
let mint_info = next_account_info(account_info_iter)?;
|
||||||
|
let authority_info = next_account_info(account_info_iter)?;
|
||||||
|
let _system_program_info = next_account_info(account_info_iter)?;
|
||||||
|
|
||||||
|
// check that the mint authority is valid without fully deserializing
|
||||||
|
let mint_authority = inline_spl_token::get_mint_authority(&mint_info.try_borrow_data()?)?;
|
||||||
|
let mint_authority = mint_authority.ok_or(TransferHookError::MintHasNoMintAuthority)?;
|
||||||
|
|
||||||
|
// Check signers
|
||||||
|
if !authority_info.is_signer {
|
||||||
|
return Err(ProgramError::MissingRequiredSignature);
|
||||||
|
}
|
||||||
|
if *authority_info.key != mint_authority {
|
||||||
|
return Err(TransferHookError::IncorrectMintAuthority.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check validation account
|
||||||
|
let (expected_validation_address, bump_seed) =
|
||||||
|
get_extra_account_metas_address_and_bump_seed(mint_info.key, program_id);
|
||||||
|
if expected_validation_address != *extra_account_metas_info.key {
|
||||||
|
return Err(ProgramError::InvalidSeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the account
|
||||||
|
let bump_seed = [bump_seed];
|
||||||
|
let signer_seeds = collect_extra_account_metas_signer_seeds(mint_info.key, &bump_seed);
|
||||||
|
let extra_account_infos = account_info_iter.as_slice();
|
||||||
|
let length = extra_account_infos.len();
|
||||||
|
let account_size = ExtraAccountMetas::size_of(length)?;
|
||||||
|
invoke_signed(
|
||||||
|
&system_instruction::allocate(extra_account_metas_info.key, account_size as u64),
|
||||||
|
&[extra_account_metas_info.clone()],
|
||||||
|
&[&signer_seeds],
|
||||||
|
)?;
|
||||||
|
invoke_signed(
|
||||||
|
&system_instruction::assign(extra_account_metas_info.key, program_id),
|
||||||
|
&[extra_account_metas_info.clone()],
|
||||||
|
&[&signer_seeds],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Write the data
|
||||||
|
let mut data = extra_account_metas_info.try_borrow_mut_data()?;
|
||||||
|
ExtraAccountMetas::init_with_account_infos::<ExecuteInstruction>(
|
||||||
|
&mut data,
|
||||||
|
extra_account_infos,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes an [Instruction](enum.Instruction.html).
|
||||||
|
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
|
||||||
|
let instruction = TransferHookInstruction::unpack(input)?;
|
||||||
|
|
||||||
|
match instruction {
|
||||||
|
TransferHookInstruction::Execute { amount } => {
|
||||||
|
msg!("Instruction: Execute");
|
||||||
|
process_execute(program_id, accounts, amount)
|
||||||
|
}
|
||||||
|
TransferHookInstruction::InitializeExtraAccountMetas => {
|
||||||
|
msg!("Instruction: InitializeExtraAccountMetas");
|
||||||
|
process_initialize_extra_account_metas(program_id, accounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
//! State helpers for working with the example program
|
||||||
|
|
||||||
|
use {
|
||||||
|
solana_program::{instruction::AccountMeta, program_error::ProgramError, pubkey::Pubkey},
|
||||||
|
spl_tlv_account_resolution::state::ExtraAccountMetas,
|
||||||
|
spl_transfer_hook_interface::instruction::ExecuteInstruction,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generate example data to be used directly in an account for testing
|
||||||
|
pub fn example_data() -> Result<Vec<u8>, ProgramError> {
|
||||||
|
let account_metas = vec![
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: Pubkey::new_unique(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
AccountMeta {
|
||||||
|
pubkey: Pubkey::new_unique(),
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let account_size = ExtraAccountMetas::size_of(account_metas.len())?;
|
||||||
|
let mut data = vec![0; account_size];
|
||||||
|
ExtraAccountMetas::init_with_account_metas::<ExecuteInstruction>(&mut data, &account_metas)?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
|
@ -0,0 +1,495 @@
|
||||||
|
// Mark this test as SBF-only due to current `ProgramTest` limitations when CPIing into the system program
|
||||||
|
#![cfg(feature = "test-sbf")]
|
||||||
|
|
||||||
|
use {
|
||||||
|
solana_program_test::{
|
||||||
|
processor,
|
||||||
|
tokio::{self, sync::Mutex},
|
||||||
|
ProgramTest, ProgramTestContext,
|
||||||
|
},
|
||||||
|
solana_sdk::{
|
||||||
|
account_info::AccountInfo,
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
instruction::{AccountMeta, InstructionError},
|
||||||
|
pubkey::Pubkey,
|
||||||
|
signature::Signer,
|
||||||
|
signer::keypair::Keypair,
|
||||||
|
system_instruction, sysvar,
|
||||||
|
transaction::{Transaction, TransactionError},
|
||||||
|
},
|
||||||
|
spl_tlv_account_resolution::state::ExtraAccountMetas,
|
||||||
|
spl_token_client::{
|
||||||
|
client::{
|
||||||
|
ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient,
|
||||||
|
SendTransaction,
|
||||||
|
},
|
||||||
|
token::Token,
|
||||||
|
},
|
||||||
|
spl_transfer_hook_interface::{
|
||||||
|
error::TransferHookError,
|
||||||
|
get_extra_account_metas_address,
|
||||||
|
instruction::{execute_with_extra_account_metas, initialize_extra_account_metas},
|
||||||
|
invoke,
|
||||||
|
},
|
||||||
|
std::sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn keypair_clone(kp: &Keypair) -> Keypair {
|
||||||
|
Keypair::from_bytes(&kp.to_bytes()).expect("failed to copy keypair")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
) -> (
|
||||||
|
Arc<Mutex<ProgramTestContext>>,
|
||||||
|
Arc<dyn ProgramClient<ProgramBanksClientProcessTransaction>>,
|
||||||
|
Arc<Keypair>,
|
||||||
|
) {
|
||||||
|
let mut program_test = ProgramTest::new(
|
||||||
|
"spl_transfer_hook_example",
|
||||||
|
*program_id,
|
||||||
|
processor!(spl_transfer_hook_example::processor::process),
|
||||||
|
);
|
||||||
|
|
||||||
|
program_test.prefer_bpf(false); // simplicity in the build
|
||||||
|
|
||||||
|
program_test.add_program(
|
||||||
|
"spl_token_2022",
|
||||||
|
spl_token_2022::id(),
|
||||||
|
processor!(spl_token_2022::processor::Processor::process),
|
||||||
|
);
|
||||||
|
|
||||||
|
let context = program_test.start_with_context().await;
|
||||||
|
let payer = Arc::new(keypair_clone(&context.payer));
|
||||||
|
let context = Arc::new(Mutex::new(context));
|
||||||
|
|
||||||
|
let client: Arc<dyn ProgramClient<ProgramBanksClientProcessTransaction>> =
|
||||||
|
Arc::new(ProgramBanksClient::new_from_context(
|
||||||
|
Arc::clone(&context),
|
||||||
|
ProgramBanksClientProcessTransaction,
|
||||||
|
));
|
||||||
|
(context, client, payer)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_mint<T: SendTransaction>(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
mint_authority: &Pubkey,
|
||||||
|
decimals: u8,
|
||||||
|
payer: Arc<Keypair>,
|
||||||
|
client: Arc<dyn ProgramClient<T>>,
|
||||||
|
) -> Token<T> {
|
||||||
|
let mint_account = Keypair::new();
|
||||||
|
let token = Token::new(
|
||||||
|
client,
|
||||||
|
program_id,
|
||||||
|
&mint_account.pubkey(),
|
||||||
|
Some(decimals),
|
||||||
|
payer,
|
||||||
|
);
|
||||||
|
token
|
||||||
|
.create_mint(mint_authority, None, vec![], &[&mint_account])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
token
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn success() {
|
||||||
|
let program_id = Pubkey::new_unique();
|
||||||
|
let (context, client, payer) = setup(&program_id).await;
|
||||||
|
|
||||||
|
let token_program_id = spl_token_2022::id();
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let mint_authority = Keypair::new();
|
||||||
|
let mint_authority_pubkey = mint_authority.pubkey();
|
||||||
|
|
||||||
|
let decimals = 2;
|
||||||
|
let token = setup_mint(
|
||||||
|
&token_program_id,
|
||||||
|
&mint_authority_pubkey,
|
||||||
|
decimals,
|
||||||
|
payer.clone(),
|
||||||
|
client.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let extra_account_metas = get_extra_account_metas_address(token.get_address(), &program_id);
|
||||||
|
|
||||||
|
token
|
||||||
|
.create_associated_token_account(&wallet.pubkey())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let source = token.get_associated_token_address(&wallet.pubkey());
|
||||||
|
let token_amount = 1_000_000_000_000;
|
||||||
|
token
|
||||||
|
.mint_to(
|
||||||
|
&source,
|
||||||
|
&mint_authority_pubkey,
|
||||||
|
token_amount,
|
||||||
|
&[&mint_authority],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let destination = Keypair::new();
|
||||||
|
token
|
||||||
|
.create_auxiliary_token_account(&destination, &wallet.pubkey())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let destination = destination.pubkey();
|
||||||
|
|
||||||
|
let extra_account_pubkeys = [
|
||||||
|
AccountMeta::new_readonly(sysvar::instructions::id(), false),
|
||||||
|
AccountMeta::new_readonly(mint_authority_pubkey, true),
|
||||||
|
AccountMeta::new(extra_account_metas, false),
|
||||||
|
];
|
||||||
|
let mut context = context.lock().await;
|
||||||
|
let rent = context.banks_client.get_rent().await.unwrap();
|
||||||
|
let rent_lamports =
|
||||||
|
rent.minimum_balance(ExtraAccountMetas::size_of(extra_account_pubkeys.len()).unwrap());
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[
|
||||||
|
system_instruction::transfer(
|
||||||
|
&context.payer.pubkey(),
|
||||||
|
&extra_account_metas,
|
||||||
|
rent_lamports,
|
||||||
|
),
|
||||||
|
initialize_extra_account_metas(
|
||||||
|
&program_id,
|
||||||
|
&extra_account_metas,
|
||||||
|
token.get_address(),
|
||||||
|
&mint_authority_pubkey,
|
||||||
|
&extra_account_pubkeys,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &mint_authority],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
|
||||||
|
context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// fail with missing account
|
||||||
|
{
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[execute_with_extra_account_metas(
|
||||||
|
&program_id,
|
||||||
|
&source,
|
||||||
|
token.get_address(),
|
||||||
|
&destination,
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&extra_account_metas,
|
||||||
|
&extra_account_pubkeys[..2],
|
||||||
|
0,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &mint_authority],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
let error = context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
TransactionError::InstructionError(
|
||||||
|
0,
|
||||||
|
InstructionError::Custom(TransferHookError::IncorrectAccount as u32),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail with wrong account
|
||||||
|
{
|
||||||
|
let extra_account_pubkeys = [
|
||||||
|
AccountMeta::new_readonly(sysvar::instructions::id(), false),
|
||||||
|
AccountMeta::new_readonly(mint_authority_pubkey, true),
|
||||||
|
AccountMeta::new(wallet.pubkey(), false),
|
||||||
|
];
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[execute_with_extra_account_metas(
|
||||||
|
&program_id,
|
||||||
|
&source,
|
||||||
|
token.get_address(),
|
||||||
|
&destination,
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&extra_account_metas,
|
||||||
|
&extra_account_pubkeys,
|
||||||
|
0,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &mint_authority],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
let error = context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
TransactionError::InstructionError(
|
||||||
|
0,
|
||||||
|
InstructionError::Custom(TransferHookError::IncorrectAccount as u32),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail with not signer
|
||||||
|
{
|
||||||
|
let extra_account_pubkeys = [
|
||||||
|
AccountMeta::new_readonly(sysvar::instructions::id(), false),
|
||||||
|
AccountMeta::new_readonly(mint_authority_pubkey, false),
|
||||||
|
AccountMeta::new(extra_account_metas, false),
|
||||||
|
];
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[execute_with_extra_account_metas(
|
||||||
|
&program_id,
|
||||||
|
&source,
|
||||||
|
token.get_address(),
|
||||||
|
&destination,
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&extra_account_metas,
|
||||||
|
&extra_account_pubkeys,
|
||||||
|
0,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
let error = context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
TransactionError::InstructionError(
|
||||||
|
0,
|
||||||
|
InstructionError::Custom(TransferHookError::IncorrectAccount as u32),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// success with correct params
|
||||||
|
{
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[execute_with_extra_account_metas(
|
||||||
|
&program_id,
|
||||||
|
&source,
|
||||||
|
token.get_address(),
|
||||||
|
&destination,
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&extra_account_metas,
|
||||||
|
&extra_account_pubkeys,
|
||||||
|
0,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &mint_authority],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_incorrect_derivation() {
|
||||||
|
let program_id = Pubkey::new_unique();
|
||||||
|
let (context, client, payer) = setup(&program_id).await;
|
||||||
|
|
||||||
|
let token_program_id = spl_token_2022::id();
|
||||||
|
let mint_authority = Keypair::new();
|
||||||
|
let mint_authority_pubkey = mint_authority.pubkey();
|
||||||
|
|
||||||
|
let decimals = 2;
|
||||||
|
let token = setup_mint(
|
||||||
|
&token_program_id,
|
||||||
|
&mint_authority_pubkey,
|
||||||
|
decimals,
|
||||||
|
payer.clone(),
|
||||||
|
client.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// wrong derivation
|
||||||
|
let extra_account_metas = get_extra_account_metas_address(&program_id, token.get_address());
|
||||||
|
|
||||||
|
let mut context = context.lock().await;
|
||||||
|
let rent = context.banks_client.get_rent().await.unwrap();
|
||||||
|
let rent_lamports = rent.minimum_balance(ExtraAccountMetas::size_of(0).unwrap());
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[
|
||||||
|
system_instruction::transfer(
|
||||||
|
&context.payer.pubkey(),
|
||||||
|
&extra_account_metas,
|
||||||
|
rent_lamports,
|
||||||
|
),
|
||||||
|
initialize_extra_account_metas(
|
||||||
|
&program_id,
|
||||||
|
&extra_account_metas,
|
||||||
|
token.get_address(),
|
||||||
|
&mint_authority_pubkey,
|
||||||
|
&[],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &mint_authority],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
let error = context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
TransactionError::InstructionError(1, InstructionError::InvalidSeeds)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test program to CPI into default transfer-hook-interface program
|
||||||
|
pub fn process_instruction(
|
||||||
|
_program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
_input: &[u8],
|
||||||
|
) -> ProgramResult {
|
||||||
|
invoke::execute(
|
||||||
|
accounts[0].key,
|
||||||
|
accounts[1].clone(),
|
||||||
|
accounts[2].clone(),
|
||||||
|
accounts[3].clone(),
|
||||||
|
accounts[4].clone(),
|
||||||
|
&accounts[4..],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn success_on_chain_invoke() {
|
||||||
|
let hook_program_id = Pubkey::new_unique();
|
||||||
|
let mut program_test = ProgramTest::new(
|
||||||
|
"spl_transfer_hook_example",
|
||||||
|
hook_program_id,
|
||||||
|
processor!(spl_transfer_hook_example::processor::process),
|
||||||
|
);
|
||||||
|
program_test.prefer_bpf(false);
|
||||||
|
program_test.add_program(
|
||||||
|
"spl_token_2022",
|
||||||
|
spl_token_2022::id(),
|
||||||
|
processor!(spl_token_2022::processor::Processor::process),
|
||||||
|
);
|
||||||
|
|
||||||
|
let program_id = Pubkey::new_unique();
|
||||||
|
program_test.add_program(
|
||||||
|
"test_cpi_program",
|
||||||
|
program_id,
|
||||||
|
processor!(process_instruction),
|
||||||
|
);
|
||||||
|
|
||||||
|
let context = program_test.start_with_context().await;
|
||||||
|
let payer = Arc::new(keypair_clone(&context.payer));
|
||||||
|
let context = Arc::new(Mutex::new(context));
|
||||||
|
|
||||||
|
let client: Arc<dyn ProgramClient<ProgramBanksClientProcessTransaction>> =
|
||||||
|
Arc::new(ProgramBanksClient::new_from_context(
|
||||||
|
Arc::clone(&context),
|
||||||
|
ProgramBanksClientProcessTransaction,
|
||||||
|
));
|
||||||
|
|
||||||
|
let token_program_id = spl_token_2022::id();
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let mint_authority = Keypair::new();
|
||||||
|
let mint_authority_pubkey = mint_authority.pubkey();
|
||||||
|
|
||||||
|
let decimals = 2;
|
||||||
|
let token = setup_mint(
|
||||||
|
&token_program_id,
|
||||||
|
&mint_authority_pubkey,
|
||||||
|
decimals,
|
||||||
|
payer.clone(),
|
||||||
|
client.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let extra_account_metas =
|
||||||
|
get_extra_account_metas_address(token.get_address(), &hook_program_id);
|
||||||
|
|
||||||
|
let source = Pubkey::new_unique();
|
||||||
|
let destination = Pubkey::new_unique();
|
||||||
|
|
||||||
|
let extra_account_pubkeys = [
|
||||||
|
AccountMeta::new_readonly(sysvar::instructions::id(), false),
|
||||||
|
AccountMeta::new_readonly(mint_authority_pubkey, true),
|
||||||
|
AccountMeta::new(extra_account_metas, false),
|
||||||
|
];
|
||||||
|
let mut context = context.lock().await;
|
||||||
|
let rent = context.banks_client.get_rent().await.unwrap();
|
||||||
|
let rent_lamports =
|
||||||
|
rent.minimum_balance(ExtraAccountMetas::size_of(extra_account_pubkeys.len()).unwrap());
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[
|
||||||
|
system_instruction::transfer(
|
||||||
|
&context.payer.pubkey(),
|
||||||
|
&extra_account_metas,
|
||||||
|
rent_lamports,
|
||||||
|
),
|
||||||
|
initialize_extra_account_metas(
|
||||||
|
&hook_program_id,
|
||||||
|
&extra_account_metas,
|
||||||
|
token.get_address(),
|
||||||
|
&mint_authority_pubkey,
|
||||||
|
&extra_account_pubkeys,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &mint_authority],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
|
||||||
|
context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// easier to hack this up!
|
||||||
|
let mut test_instruction = execute_with_extra_account_metas(
|
||||||
|
&program_id,
|
||||||
|
&source,
|
||||||
|
token.get_address(),
|
||||||
|
&destination,
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&extra_account_metas,
|
||||||
|
&extra_account_pubkeys,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
test_instruction
|
||||||
|
.accounts
|
||||||
|
.insert(0, AccountMeta::new_readonly(hook_program_id, false));
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[test_instruction],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &mint_authority],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
|
||||||
|
context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
[package]
|
||||||
|
name = "spl-transfer-hook-interface"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Solana Program Library Transfer Hook Interface"
|
||||||
|
authors = ["Solana Labs Maintainers <maintainers@solanalabs.com>"]
|
||||||
|
repository = "https://github.com/solana-labs/solana-program-library"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
offchain-client = ["dep:solana-sdk"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
arrayref = "0.3.6"
|
||||||
|
bytemuck = { version = "1.13.0", features = ["derive"] }
|
||||||
|
num-derive = "0.3"
|
||||||
|
num-traits = "0.2"
|
||||||
|
num_enum = "0.5.9"
|
||||||
|
solana-sdk = { version = "1.14.12", optional = true }
|
||||||
|
solana-program = "1.14.12"
|
||||||
|
spl-tlv-account-resolution = { version = "0.1.0" , path = "../../libraries/tlv-account-resolution" }
|
||||||
|
spl-type-length-value = { version = "0.1.0" , path = "../../libraries/type-length-value" }
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
targets = ["x86_64-unknown-linux-gnu"]
|
|
@ -0,0 +1,106 @@
|
||||||
|
## Transfer-Hook Interface
|
||||||
|
|
||||||
|
### Example program
|
||||||
|
|
||||||
|
Here is an example program that only implements the required "execute" instruction,
|
||||||
|
assuming that the proper account data is already written to the appropriate
|
||||||
|
program-derived address defined by the interface.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use {
|
||||||
|
solana_program::{entrypoint::ProgramResult, program_error::ProgramError},
|
||||||
|
spl_tlv_account_resolution::state::ExtraAccountMetas,
|
||||||
|
spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction},
|
||||||
|
spl_type_length_value::state::TlvStateBorrowed,
|
||||||
|
};
|
||||||
|
pub fn process_instruction(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
input: &[u8],
|
||||||
|
) -> ProgramResult {
|
||||||
|
let instruction = TransferHookInstruction::unpack(input)?;
|
||||||
|
let _amount = match instruction {
|
||||||
|
TransferHookInstruction::Execute { amount } => amount,
|
||||||
|
_ => return Err(ProgramError::InvalidInstructionData),
|
||||||
|
};
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
// Pull out the accounts in order, none are validated in this test program
|
||||||
|
let _source_account_info = next_account_info(account_info_iter)?;
|
||||||
|
let mint_info = next_account_info(account_info_iter)?;
|
||||||
|
let _destination_account_info = next_account_info(account_info_iter)?;
|
||||||
|
let _authority_info = next_account_info(account_info_iter)?;
|
||||||
|
let extra_account_metas_info = next_account_info(account_info_iter)?;
|
||||||
|
|
||||||
|
// Only check that the correct pda and account are provided
|
||||||
|
let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id);
|
||||||
|
if expected_validation_address != *extra_account_metas_info.key {
|
||||||
|
return Err(ProgramError::InvalidSeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the extra account metas from the account data
|
||||||
|
let data = extra_account_metas_info.try_borrow_data()?;
|
||||||
|
let state = TlvStateBorrowed::unpack(&data).unwrap();
|
||||||
|
let extra_account_metas =
|
||||||
|
ExtraAccountMetas::unpack_with_tlv_state::<ExecuteInstruction>(&state)?;
|
||||||
|
|
||||||
|
// If incorrect number of accounts is provided, error
|
||||||
|
let extra_account_infos = account_info_iter.as_slice();
|
||||||
|
let account_metas = extra_account_metas.data();
|
||||||
|
if extra_account_infos.len() != account_metas.len() {
|
||||||
|
return Err(ProgramError::InvalidInstructionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's require that they're provided in the correct order
|
||||||
|
for (i, account_info) in extra_account_infos.iter().enumerate() {
|
||||||
|
if &account_metas[i] != account_info {
|
||||||
|
return Err(ProgramError::InvalidInstructionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
Token creators may need more control over transfers of their token. The most
|
||||||
|
prominent use case revolves around NFT royalties. Whenever a token is moved,
|
||||||
|
the creator should be entitled to royalties, but due to the design of the current
|
||||||
|
token program, it's impossible to stop a transfer at the protocol level.
|
||||||
|
|
||||||
|
Current solutions typically resort to perpetually freezing tokens, which requires
|
||||||
|
a whole proxy layer to interact with the token. Wallets and marketplaces need
|
||||||
|
to be aware of the proxy layer in order to properly use the token.
|
||||||
|
|
||||||
|
Worse still, different royalty systems have different proxy layers for using
|
||||||
|
their token. All in all, these systems harm composability and make development
|
||||||
|
harder.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
|
||||||
|
To give more flexibility to token creators and improve the situation for everyone,
|
||||||
|
`spl-transfer-hook-interface` introduces the concept of an interface integrated
|
||||||
|
with `spl-token-2022`. A token creator must develop and deploy a program that
|
||||||
|
implements the interface and then configure their token mint to use their program.
|
||||||
|
|
||||||
|
During transfer, token-2022 calls into the program with the accounts specified
|
||||||
|
at a well-defined program-derived address for that mint and program id. This
|
||||||
|
call happens after all other transfer logic, so the accounts reflect the *end*
|
||||||
|
state of the transfer.
|
||||||
|
|
||||||
|
A developer must implement the `Execute` instruction, and the
|
||||||
|
`InitializeExtraAccountMetas` instruction to write the required additional account
|
||||||
|
pubkeys into the program-derived address defined by the mint and program id.
|
||||||
|
|
||||||
|
Side note: it's technically not required to implement `InitializeExtraAccountMetas`
|
||||||
|
at that instruction descriminator. Your program may implement multiple interfaces,
|
||||||
|
so any other instruction in your program can create the account at the program-derived
|
||||||
|
address!
|
||||||
|
|
||||||
|
This library provides offchain and onchain helpers for resolving the additional
|
||||||
|
accounts required. See
|
||||||
|
[invoke.rs](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook-interface/src/invoke.rs)
|
||||||
|
for usage on-chain, and
|
||||||
|
[offchain.rs](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook-interface/src/offchain.rs)
|
||||||
|
for fetching the additional required account metas.
|
|
@ -0,0 +1,54 @@
|
||||||
|
//! 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 interface.
|
||||||
|
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
|
||||||
|
pub enum TransferHookError {
|
||||||
|
/// Incorrect account provided
|
||||||
|
#[error("Incorrect account provided")]
|
||||||
|
IncorrectAccount,
|
||||||
|
/// Mint has no mint authority
|
||||||
|
#[error("Mint has no mint authority")]
|
||||||
|
MintHasNoMintAuthority,
|
||||||
|
/// Incorrect mint authority has signed the instruction
|
||||||
|
#[error("Incorrect mint authority has signed the instruction")]
|
||||||
|
IncorrectMintAuthority,
|
||||||
|
}
|
||||||
|
impl From<TransferHookError> for ProgramError {
|
||||||
|
fn from(e: TransferHookError) -> Self {
|
||||||
|
ProgramError::Custom(e as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> DecodeError<T> for TransferHookError {
|
||||||
|
fn type_of() -> &'static str {
|
||||||
|
"TransferHookError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrintProgramError for TransferHookError {
|
||||||
|
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::MintHasNoMintAuthority => msg!("Mint has no mint authority"),
|
||||||
|
Self::IncorrectMintAuthority => {
|
||||||
|
msg!("Incorrect mint authority has signed the instruction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,211 @@
|
||||||
|
//! Instruction types
|
||||||
|
|
||||||
|
use {
|
||||||
|
solana_program::{
|
||||||
|
instruction::{AccountMeta, Instruction},
|
||||||
|
program_error::ProgramError,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
system_program,
|
||||||
|
},
|
||||||
|
spl_type_length_value::discriminator::{Discriminator, TlvDiscriminator},
|
||||||
|
std::convert::TryInto,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Instructions supported by the transfer hook interface.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum TransferHookInstruction {
|
||||||
|
/// Runs additional transfer logic.
|
||||||
|
///
|
||||||
|
/// Accounts expected by this instruction:
|
||||||
|
///
|
||||||
|
/// 0. `[]` Source account
|
||||||
|
/// 1. `[]` Token mint
|
||||||
|
/// 2. `[]` Destination account
|
||||||
|
/// 3. `[]` Source account's owner/delegate
|
||||||
|
/// 4. `[]` Validation account
|
||||||
|
/// 5..5+M `[]` `M` additional accounts, written in validation account data
|
||||||
|
///
|
||||||
|
Execute {
|
||||||
|
/// Amount of tokens to transfer
|
||||||
|
amount: u64,
|
||||||
|
},
|
||||||
|
/// Initializes the extra account metas on an account, writing into
|
||||||
|
/// the first open TLV space.
|
||||||
|
///
|
||||||
|
/// Accounts expected by this instruction:
|
||||||
|
///
|
||||||
|
/// 0. `[w]` Account with extra account metas
|
||||||
|
/// 1. `[]` Mint
|
||||||
|
/// 2. `[s]` Mint authority
|
||||||
|
/// 3. `[]` System program
|
||||||
|
/// 4..4+M `[]` `M` additional accounts, to be written to validation data
|
||||||
|
///
|
||||||
|
InitializeExtraAccountMetas,
|
||||||
|
}
|
||||||
|
/// TLV instruction type only used to define the discriminator. The actual data
|
||||||
|
/// is entirely managed by `ExtraAccountMetas`, and it is the only data contained
|
||||||
|
/// by this type.
|
||||||
|
pub struct ExecuteInstruction;
|
||||||
|
impl TlvDiscriminator for ExecuteInstruction {
|
||||||
|
/// Please use this discriminator in your program when matching
|
||||||
|
const TLV_DISCRIMINATOR: Discriminator = Discriminator::new(EXECUTE_DISCRIMINATOR);
|
||||||
|
}
|
||||||
|
/// First 8 bytes of `hash::hashv(&["spl-transfer-hook-interface:execute"])`
|
||||||
|
const EXECUTE_DISCRIMINATOR: [u8; Discriminator::LENGTH] = [105, 37, 101, 197, 75, 251, 102, 26];
|
||||||
|
// annoying, but needed to perform a match on the value
|
||||||
|
const EXECUTE_DISCRIMINATOR_SLICE: &[u8] = &EXECUTE_DISCRIMINATOR;
|
||||||
|
/// First 8 bytes of `hash::hashv(&["spl-transfer-hook-interface:initialize-extra-account-metas"])`
|
||||||
|
const INITIALIZE_EXTRA_ACCOUNT_METAS_DISCRIMINATOR: &[u8] = &[43, 34, 13, 49, 167, 88, 235, 235];
|
||||||
|
|
||||||
|
impl TransferHookInstruction {
|
||||||
|
/// Unpacks a byte buffer into a [TransferHookInstruction](enum.TransferHookInstruction.html).
|
||||||
|
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
|
||||||
|
if input.len() < Discriminator::LENGTH {
|
||||||
|
return Err(ProgramError::InvalidInstructionData);
|
||||||
|
}
|
||||||
|
let (discriminator, rest) = input.split_at(Discriminator::LENGTH);
|
||||||
|
Ok(match discriminator {
|
||||||
|
EXECUTE_DISCRIMINATOR_SLICE => {
|
||||||
|
let amount = rest
|
||||||
|
.get(..8)
|
||||||
|
.and_then(|slice| slice.try_into().ok())
|
||||||
|
.map(u64::from_le_bytes)
|
||||||
|
.ok_or(ProgramError::InvalidInstructionData)?;
|
||||||
|
Self::Execute { amount }
|
||||||
|
}
|
||||||
|
INITIALIZE_EXTRA_ACCOUNT_METAS_DISCRIMINATOR => Self::InitializeExtraAccountMetas,
|
||||||
|
_ => return Err(ProgramError::InvalidInstructionData),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packs a [TokenInstruction](enum.TokenInstruction.html) into a byte buffer.
|
||||||
|
pub fn pack(&self) -> Vec<u8> {
|
||||||
|
let mut buf = vec![];
|
||||||
|
match self {
|
||||||
|
Self::Execute { amount } => {
|
||||||
|
buf.extend_from_slice(EXECUTE_DISCRIMINATOR_SLICE);
|
||||||
|
buf.extend_from_slice(&amount.to_le_bytes());
|
||||||
|
}
|
||||||
|
Self::InitializeExtraAccountMetas => {
|
||||||
|
buf.extend_from_slice(INITIALIZE_EXTRA_ACCOUNT_METAS_DISCRIMINATOR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an `Execute` instruction, provided all of the additional required
|
||||||
|
/// account metas
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn execute_with_extra_account_metas(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
source_pubkey: &Pubkey,
|
||||||
|
mint_pubkey: &Pubkey,
|
||||||
|
destination_pubkey: &Pubkey,
|
||||||
|
authority_pubkey: &Pubkey,
|
||||||
|
validate_state_pubkey: &Pubkey,
|
||||||
|
additional_accounts: &[AccountMeta],
|
||||||
|
amount: u64,
|
||||||
|
) -> Instruction {
|
||||||
|
let mut instruction = execute(
|
||||||
|
program_id,
|
||||||
|
source_pubkey,
|
||||||
|
mint_pubkey,
|
||||||
|
destination_pubkey,
|
||||||
|
authority_pubkey,
|
||||||
|
validate_state_pubkey,
|
||||||
|
amount,
|
||||||
|
);
|
||||||
|
instruction.accounts.extend_from_slice(additional_accounts);
|
||||||
|
instruction
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an `Execute` instruction, without the additional accounts
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn execute(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
source_pubkey: &Pubkey,
|
||||||
|
mint_pubkey: &Pubkey,
|
||||||
|
destination_pubkey: &Pubkey,
|
||||||
|
authority_pubkey: &Pubkey,
|
||||||
|
validate_state_pubkey: &Pubkey,
|
||||||
|
amount: u64,
|
||||||
|
) -> Instruction {
|
||||||
|
let data = TransferHookInstruction::Execute { amount }.pack();
|
||||||
|
let accounts = vec![
|
||||||
|
AccountMeta::new_readonly(*source_pubkey, false),
|
||||||
|
AccountMeta::new_readonly(*mint_pubkey, false),
|
||||||
|
AccountMeta::new_readonly(*destination_pubkey, false),
|
||||||
|
AccountMeta::new_readonly(*authority_pubkey, false),
|
||||||
|
AccountMeta::new_readonly(*validate_state_pubkey, false),
|
||||||
|
];
|
||||||
|
Instruction {
|
||||||
|
program_id: *program_id,
|
||||||
|
accounts,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a `InitializeExtraAccountMetas` instruction.
|
||||||
|
pub fn initialize_extra_account_metas(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
extra_account_metas_pubkey: &Pubkey,
|
||||||
|
mint_pubkey: &Pubkey,
|
||||||
|
authority_pubkey: &Pubkey,
|
||||||
|
additional_accounts: &[AccountMeta],
|
||||||
|
) -> Instruction {
|
||||||
|
let data = TransferHookInstruction::InitializeExtraAccountMetas.pack();
|
||||||
|
|
||||||
|
let mut accounts = vec![
|
||||||
|
AccountMeta::new(*extra_account_metas_pubkey, false),
|
||||||
|
AccountMeta::new_readonly(*mint_pubkey, false),
|
||||||
|
AccountMeta::new_readonly(*authority_pubkey, true),
|
||||||
|
AccountMeta::new_readonly(system_program::id(), false),
|
||||||
|
];
|
||||||
|
accounts.extend_from_slice(additional_accounts);
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: *program_id,
|
||||||
|
accounts,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use {super::*, crate::NAMESPACE, solana_program::hash};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_packing() {
|
||||||
|
let amount = 111_111_111;
|
||||||
|
let check = TransferHookInstruction::Execute { amount };
|
||||||
|
let packed = check.pack();
|
||||||
|
// Please use ExecuteInstruction::TLV_DISCRIMINATOR in your program, the
|
||||||
|
// following is just for test purposes
|
||||||
|
let preimage = hash::hashv(&[format!("{NAMESPACE}:execute").as_bytes()]);
|
||||||
|
let discriminator = &preimage.as_ref()[..Discriminator::LENGTH];
|
||||||
|
let mut expect = vec![];
|
||||||
|
expect.extend_from_slice(discriminator.as_ref());
|
||||||
|
expect.extend_from_slice(&amount.to_le_bytes());
|
||||||
|
assert_eq!(packed, expect);
|
||||||
|
let unpacked = TransferHookInstruction::unpack(&expect).unwrap();
|
||||||
|
assert_eq!(unpacked, check);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initialize_validation_pubkeys_packing() {
|
||||||
|
let check = TransferHookInstruction::InitializeExtraAccountMetas;
|
||||||
|
let packed = check.pack();
|
||||||
|
// Please use INITIALIZE_EXTRA_ACCOUNT_METAS_DISCRIMINATOR in your program,
|
||||||
|
// the following is just for test purposes
|
||||||
|
let preimage =
|
||||||
|
hash::hashv(&[format!("{NAMESPACE}:initialize-extra-account-metas").as_bytes()]);
|
||||||
|
let discriminator = &preimage.as_ref()[..Discriminator::LENGTH];
|
||||||
|
let mut expect = vec![];
|
||||||
|
expect.extend_from_slice(discriminator.as_ref());
|
||||||
|
assert_eq!(packed, expect);
|
||||||
|
let unpacked = TransferHookInstruction::unpack(&expect).unwrap();
|
||||||
|
assert_eq!(unpacked, check);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
//! On-chain program invoke helper to perform on-chain `execute` with correct accounts
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::{error::TransferHookError, get_extra_account_metas_address, instruction},
|
||||||
|
solana_program::{
|
||||||
|
account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, pubkey::Pubkey,
|
||||||
|
},
|
||||||
|
spl_tlv_account_resolution::state::ExtraAccountMetas,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Helper to CPI into a transfer-hook program on-chain, looking through the
|
||||||
|
/// additional account infos to create the proper instruction
|
||||||
|
pub fn execute<'a>(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
source_info: AccountInfo<'a>,
|
||||||
|
mint_info: AccountInfo<'a>,
|
||||||
|
destination_info: AccountInfo<'a>,
|
||||||
|
authority_info: AccountInfo<'a>,
|
||||||
|
additional_accounts: &[AccountInfo<'a>],
|
||||||
|
amount: u64,
|
||||||
|
) -> ProgramResult {
|
||||||
|
let validation_pubkey = get_extra_account_metas_address(mint_info.key, program_id);
|
||||||
|
let validation_info = additional_accounts
|
||||||
|
.iter()
|
||||||
|
.find(|&x| *x.key == validation_pubkey)
|
||||||
|
.ok_or(TransferHookError::IncorrectAccount)?;
|
||||||
|
let mut cpi_instruction = instruction::execute(
|
||||||
|
program_id,
|
||||||
|
source_info.key,
|
||||||
|
mint_info.key,
|
||||||
|
destination_info.key,
|
||||||
|
authority_info.key,
|
||||||
|
&validation_pubkey,
|
||||||
|
amount,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cpi_account_infos = vec![
|
||||||
|
source_info,
|
||||||
|
mint_info,
|
||||||
|
destination_info,
|
||||||
|
authority_info,
|
||||||
|
validation_info.clone(),
|
||||||
|
];
|
||||||
|
ExtraAccountMetas::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
|
||||||
|
&mut cpi_instruction,
|
||||||
|
&mut cpi_account_infos,
|
||||||
|
&validation_info.try_borrow_data()?,
|
||||||
|
additional_accounts,
|
||||||
|
)?;
|
||||||
|
invoke(&cpi_instruction, &cpi_account_infos)
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
//! Crate defining an interface for performing a hook on transfer, where the
|
||||||
|
//! token program calls into a separate program with additional accounts after
|
||||||
|
//! all other logic, to be sure that a transfer has accomplished all required
|
||||||
|
//! preconditions.
|
||||||
|
|
||||||
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
#![cfg_attr(not(test), forbid(unsafe_code))]
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod instruction;
|
||||||
|
pub mod invoke;
|
||||||
|
#[cfg(feature = "offchain-client")]
|
||||||
|
pub mod offchain;
|
||||||
|
|
||||||
|
// Export current sdk types for downstream users building with a different sdk version
|
||||||
|
pub use solana_program;
|
||||||
|
use solana_program::pubkey::Pubkey;
|
||||||
|
|
||||||
|
/// Namespace for all programs implementing transfer-hook
|
||||||
|
pub const NAMESPACE: &str = "spl-transfer-hook-interface";
|
||||||
|
|
||||||
|
/// Seed for the state
|
||||||
|
const EXTRA_ACCOUNT_METAS_SEED: &[u8] = b"extra-account-metas";
|
||||||
|
|
||||||
|
/// Get the state address PDA
|
||||||
|
pub fn get_extra_account_metas_address(mint: &Pubkey, program_id: &Pubkey) -> Pubkey {
|
||||||
|
get_extra_account_metas_address_and_bump_seed(mint, program_id).0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function used by programs implementing the interface, when creating the PDA,
|
||||||
|
/// to also get the bump seed
|
||||||
|
pub fn get_extra_account_metas_address_and_bump_seed(
|
||||||
|
mint: &Pubkey,
|
||||||
|
program_id: &Pubkey,
|
||||||
|
) -> (Pubkey, u8) {
|
||||||
|
Pubkey::find_program_address(&collect_extra_account_metas_seeds(mint), program_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function used by programs implementing the interface, when creating the PDA,
|
||||||
|
/// to get all of the PDA seeds
|
||||||
|
pub fn collect_extra_account_metas_seeds(mint: &Pubkey) -> [&[u8]; 2] {
|
||||||
|
[EXTRA_ACCOUNT_METAS_SEED, mint.as_ref()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function used by programs implementing the interface, when creating the PDA,
|
||||||
|
/// to sign for the PDA
|
||||||
|
pub fn collect_extra_account_metas_signer_seeds<'a>(
|
||||||
|
mint: &'a Pubkey,
|
||||||
|
bump_seed: &'a [u8],
|
||||||
|
) -> [&'a [u8]; 3] {
|
||||||
|
[EXTRA_ACCOUNT_METAS_SEED, mint.as_ref(), bump_seed]
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
//! Offchain helper for fetching required accounts to build instructions
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::{get_extra_account_metas_address, instruction::ExecuteInstruction},
|
||||||
|
solana_sdk::{
|
||||||
|
account::Account, instruction::AccountMeta, program_error::ProgramError, pubkey::Pubkey,
|
||||||
|
},
|
||||||
|
spl_tlv_account_resolution::state::ExtraAccountMetas,
|
||||||
|
std::future::Future,
|
||||||
|
};
|
||||||
|
|
||||||
|
type AccountResult = Result<Option<Account>, AccountFetchError>;
|
||||||
|
type AccountFetchError = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
|
||||||
|
/// Offchain helper to get all additional required account metas for a mint
|
||||||
|
///
|
||||||
|
/// To be client-agnostic, this simply takes a function that will return a
|
||||||
|
/// `Future<Account>` for the given address. Can be called in the following way:
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// use solana_client::nonblocking::rpc_client::RpcClient;
|
||||||
|
/// use solana_sdk::pubkey::Pubkey;
|
||||||
|
///
|
||||||
|
/// let program_id = Pubkey::new_unique();
|
||||||
|
/// let mint = Pubkey::new_unique();
|
||||||
|
/// let client = RpcClient::new_mock("succeeds".to_string());
|
||||||
|
///
|
||||||
|
/// let extra_account_metas = get_extra_account_metas(
|
||||||
|
/// |address| self.client.get_account(&address),
|
||||||
|
/// &program_id,
|
||||||
|
/// &mint,
|
||||||
|
/// ).await?;
|
||||||
|
/// ```
|
||||||
|
pub async fn get_extra_account_metas<F, Fut>(
|
||||||
|
get_account_fn: F,
|
||||||
|
permissioned_transfer_program_id: &Pubkey,
|
||||||
|
mint: &Pubkey,
|
||||||
|
) -> Result<Vec<AccountMeta>, AccountFetchError>
|
||||||
|
where
|
||||||
|
F: Fn(Pubkey) -> Fut,
|
||||||
|
Fut: Future<Output = AccountResult>,
|
||||||
|
{
|
||||||
|
let mut instruction_metas = vec![];
|
||||||
|
let validation_address =
|
||||||
|
get_extra_account_metas_address(mint, permissioned_transfer_program_id);
|
||||||
|
let validation_account = get_account_fn(validation_address)
|
||||||
|
.await?
|
||||||
|
.ok_or(ProgramError::InvalidAccountData)?;
|
||||||
|
ExtraAccountMetas::add_to_vec::<ExecuteInstruction>(
|
||||||
|
&mut instruction_metas,
|
||||||
|
&validation_account.data,
|
||||||
|
)?;
|
||||||
|
instruction_metas.push(AccountMeta {
|
||||||
|
pubkey: *permissioned_transfer_program_id,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
});
|
||||||
|
instruction_metas.push(AccountMeta {
|
||||||
|
pubkey: validation_address,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: false,
|
||||||
|
});
|
||||||
|
Ok(instruction_metas)
|
||||||
|
}
|
Loading…
Reference in New Issue