Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

[token-cli] Add support for transfer-hook account resolution for transfers with the transfer-fee extension. #7171

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions token/client/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1166,21 +1166,44 @@ where
let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys);
let decimals = self.decimals.ok_or(TokenError::MissingDecimals)?;

self.process_ixs(
&[transfer_fee::instruction::transfer_checked_with_fee(
let fetch_account_data_fn = |address| {
self.client
.get_account(address)
.map_ok(|opt| opt.map(|acc| acc.data))
};

let instruction = if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts {
let mut instruction = transfer_fee::instruction::transfer_checked_with_fee(
&self.program_id,
source,
&self.pubkey,
self.get_address(),
destination,
authority,
&multisig_signers,
amount,
decimals,
fee,
)?],
signing_keypairs,
)
.await
)?;
instruction.accounts.extend(transfer_hook_accounts.clone());
instruction
} else {
offchain::create_transfer_checked_with_fee_instruction_with_extra_metas(
&self.program_id,
source,
self.get_address(),
destination,
authority,
&multisig_signers,
amount,
decimals,
fee,
fetch_account_data_fn,
)
.await
.map_err(|_| TokenError::AccountNotFound)?
};

self.process_ixs(&[instruction], signing_keypairs).await
}

/// Burn tokens from account
Expand Down
68 changes: 67 additions & 1 deletion token/program-2022/src/offchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pub use spl_transfer_hook_interface::offchain::{AccountDataResult, AccountFetchError};
use {
crate::{
extension::{transfer_hook, StateWithExtensions},
extension::{transfer_fee, transfer_hook, StateWithExtensions},
state::Mint,
},
solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey},
Expand Down Expand Up @@ -74,6 +74,72 @@ where
Ok(transfer_instruction)
}

/// Offchain helper to create a `TransferCheckedWithFee` instruction with all
/// additional required account metas for a transfer, including the ones
/// required by the transfer hook.
///
/// To be client-agnostic and to avoid pulling in the full solana-sdk, this
/// simply takes a function that will return its data as `Future<Vec<u8>>` for
/// the given address. Can be called in the following way:
///
/// ```rust,ignore
/// let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas(
/// &spl_token_2022::id(),
/// &source,
/// &mint,
/// &destination,
/// &authority,
/// &[],
/// amount,
/// decimals,
/// fee,
/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)),
/// )
/// .await?
/// ```
#[allow(clippy::too_many_arguments)]
pub async fn create_transfer_checked_with_fee_instruction_with_extra_metas<F, Fut>(
token_program_id: &Pubkey,
source_pubkey: &Pubkey,
mint_pubkey: &Pubkey,
destination_pubkey: &Pubkey,
authority_pubkey: &Pubkey,
signer_pubkeys: &[&Pubkey],
amount: u64,
decimals: u8,
fee: u64,
fetch_account_data_fn: F,
) -> Result<Instruction, AccountFetchError>
where
F: Fn(Pubkey) -> Fut,
Fut: Future<Output = AccountDataResult>,
{
let mut transfer_instruction = transfer_fee::instruction::transfer_checked_with_fee(
token_program_id,
source_pubkey,
mint_pubkey,
destination_pubkey,
authority_pubkey,
signer_pubkeys,
amount,
decimals,
fee,
)?;

add_extra_account_metas(
&mut transfer_instruction,
source_pubkey,
mint_pubkey,
destination_pubkey,
authority_pubkey,
amount,
fetch_account_data_fn,
)
.await?;

Ok(transfer_instruction)
}

/// Offchain helper to add required account metas to an instruction, including
/// the ones required by the transfer hook.
///
Expand Down
69 changes: 68 additions & 1 deletion token/program-2022/src/onchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use {
crate::{
extension::{transfer_hook, StateWithExtensions},
extension::{transfer_fee, transfer_hook, StateWithExtensions},
instruction,
state::Mint,
},
Expand Down Expand Up @@ -78,3 +78,70 @@ pub fn invoke_transfer_checked<'a>(

invoke_signed(&cpi_instruction, &cpi_account_infos, seeds)
}

/// Helper to CPI into token-2022 on-chain, looking through the additional
/// account infos to create the proper instruction with the proper account infos
#[allow(clippy::too_many_arguments)]
pub fn invoke_transfer_checked_with_fee<'a>(
token_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,
decimals: u8,
fee: u64,
seeds: &[&[&[u8]]],
) -> ProgramResult {
let mut cpi_instruction = transfer_fee::instruction::transfer_checked_with_fee(
token_program_id,
source_info.key,
mint_info.key,
destination_info.key,
authority_info.key,
&[], // add them later, to avoid unnecessary clones
amount,
decimals,
fee,
)?;

let mut cpi_account_infos = vec![
source_info.clone(),
mint_info.clone(),
destination_info.clone(),
authority_info.clone(),
];

// if it's a signer, it might be a multisig signer, throw it in!
additional_accounts
.iter()
.filter(|ai| ai.is_signer)
.for_each(|ai| {
cpi_account_infos.push(ai.clone());
cpi_instruction
.accounts
.push(AccountMeta::new_readonly(*ai.key, ai.is_signer));
});

// scope the borrowing to avoid a double-borrow during CPI
{
let mint_data = mint_info.try_borrow_data()?;
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
if let Some(program_id) = transfer_hook::get_program_id(&mint) {
add_extra_accounts_for_execute_cpi(
&mut cpi_instruction,
&mut cpi_account_infos,
&program_id,
source_info,
mint_info.clone(),
destination_info,
authority_info,
amount,
additional_accounts,
)?;
}
}

invoke_signed(&cpi_instruction, &cpi_account_infos, seeds)
}
Loading