Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

token 2022: add new offchain helper #6100

Merged
merged 1 commit into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions token/client/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ where
if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts {
instruction.accounts.extend(transfer_hook_accounts.clone());
} else {
#[allow(deprecated)]
offchain::resolve_extra_transfer_account_metas(
&mut instruction,
|address| {
Expand Down
1 change: 1 addition & 0 deletions token/program-2022-test/tests/transfer_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ async fn success_downgrade_writable_and_signer_accounts() {
.unwrap();
}

#[allow(deprecated)]
#[tokio::test]
async fn success_transfers_using_onchain_helper() {
let authority = Pubkey::new_unique();
Expand Down
1 change: 1 addition & 0 deletions token/program-2022/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ proptest = "1.4"
serial_test = "3.0.0"
solana-program-test = "1.17.6"
solana-sdk = "1.17.6"
spl-tlv-account-resolution = { version = "0.5.0", path = "../../libraries/tlv-account-resolution" }
serde_json = "1.0.111"

[lib]
Expand Down
256 changes: 256 additions & 0 deletions token/program-2022/src/offchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use {
state::Mint,
},
solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey},
spl_transfer_hook_interface::offchain::add_extra_account_metas_for_execute,
std::future::Future,
};

Expand All @@ -32,6 +33,10 @@ use {
/// &mint,
/// ).await?;
/// ```
#[deprecated(
since = "1.1.0",
note = "Please use `create_transfer_checked_instruction_with_extra_metas` instead"
)]
pub async fn resolve_extra_transfer_account_metas<F, Fut>(
instruction: &mut Instruction,
fetch_account_data_fn: F,
Expand All @@ -57,3 +62,254 @@ where
}
Ok(())
}

/// Offchain helper to create a `TransferChecked` 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_instruction_with_extra_metas(
/// &spl_token_2022::id(),
/// &source,
/// &mint,
/// &destination,
/// &authority,
/// &[],
/// amount,
/// decimals,
/// |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_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,
fetch_account_data_fn: F,
) -> Result<Instruction, AccountFetchError>
where
F: Fn(Pubkey) -> Fut,
Fut: Future<Output = AccountDataResult>,
{
let mut transfer_instruction = crate::instruction::transfer_checked(
token_program_id,
source_pubkey,
mint_pubkey,
destination_pubkey,
authority_pubkey,
signer_pubkeys,
amount,
decimals,
)?;

let mint_data = fetch_account_data_fn(*mint_pubkey)
.await?
.ok_or(ProgramError::InvalidAccountData)?;
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;

if let Some(program_id) = transfer_hook::get_program_id(&mint) {
add_extra_account_metas_for_execute(
&mut transfer_instruction,
&program_id,
source_pubkey,
mint_pubkey,
destination_pubkey,
authority_pubkey,
amount,
fetch_account_data_fn,
)
.await?;
}

Ok(transfer_instruction)
}

#[cfg(test)]
mod tests {
use {
super::*,
crate::extension::{transfer_hook::TransferHook, ExtensionType, StateWithExtensionsMut},
solana_program::{instruction::AccountMeta, program_option::COption},
solana_program_test::tokio,
spl_pod::optional_keys::OptionalNonZeroPubkey,
spl_tlv_account_resolution::{
account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
},
spl_transfer_hook_interface::{
get_extra_account_metas_address, instruction::ExecuteInstruction,
},
};

const DECIMALS: u8 = 0;
const MINT_PUBKEY: Pubkey = Pubkey::new_from_array([1u8; 32]);
const TRANSFER_HOOK_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]);
const EXTRA_META_1: Pubkey = Pubkey::new_from_array([3u8; 32]);
const EXTRA_META_2: Pubkey = Pubkey::new_from_array([4u8; 32]);

// Mock to return the mint data or the validation state account data
async fn mock_fetch_account_data_fn(address: Pubkey) -> AccountDataResult {
if address == MINT_PUBKEY {
let mint_len =
ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::TransferHook])
.unwrap();
let mut data = vec![0u8; mint_len];
let mut mint = StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut data).unwrap();

let extension = mint.init_extension::<TransferHook>(true).unwrap();
extension.program_id =
OptionalNonZeroPubkey::try_from(Some(TRANSFER_HOOK_PROGRAM_ID)).unwrap();

mint.base.mint_authority = COption::Some(Pubkey::new_unique());
mint.base.decimals = DECIMALS;
mint.base.is_initialized = true;
mint.base.freeze_authority = COption::None;
mint.pack_base();
mint.init_account_type().unwrap();
buffalojoec marked this conversation as resolved.
Show resolved Hide resolved

Ok(Some(data))
} else if address
== get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID)
{
let extra_metas = vec![
ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(),
ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(),
ExtraAccountMeta::new_with_seeds(
&[
Seed::AccountKey { index: 0 }, // source
Seed::AccountKey { index: 2 }, // destination
Seed::AccountKey { index: 4 }, // validation state
],
false,
true,
)
.unwrap(),
ExtraAccountMeta::new_with_seeds(
&[
Seed::InstructionData {
index: 8,
length: 8,
}, // amount
Seed::AccountKey { index: 2 }, // destination
Seed::AccountKey { index: 5 }, // extra meta 1
Seed::AccountKey { index: 7 }, // extra meta 3 (PDA)
],
false,
true,
)
.unwrap(),
];
let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap();
let mut data = vec![0u8; account_size];
ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &extra_metas)?;
buffalojoec marked this conversation as resolved.
Show resolved Hide resolved
Ok(Some(data))
} else {
Ok(None)
}
}

#[tokio::test]
async fn test_create_transfer_checked_instruction_with_extra_metas() {
let source = Pubkey::new_unique();
let destination = Pubkey::new_unique();
let authority = Pubkey::new_unique();
let amount = 100u64;

let validate_state_pubkey =
get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID);
let extra_meta_3_pubkey = Pubkey::find_program_address(
&[
source.as_ref(),
destination.as_ref(),
validate_state_pubkey.as_ref(),
],
&TRANSFER_HOOK_PROGRAM_ID,
)
.0;
let extra_meta_4_pubkey = Pubkey::find_program_address(
&[
amount.to_le_bytes().as_ref(),
destination.as_ref(),
EXTRA_META_1.as_ref(),
extra_meta_3_pubkey.as_ref(),
],
&TRANSFER_HOOK_PROGRAM_ID,
)
.0;

let instruction = create_transfer_checked_instruction_with_extra_metas(
&crate::id(),
&source,
&MINT_PUBKEY,
&destination,
&authority,
&[],
amount,
DECIMALS,
mock_fetch_account_data_fn,
)
.await
.unwrap();

let check_metas = [
AccountMeta::new(source, false),
AccountMeta::new_readonly(MINT_PUBKEY, false),
AccountMeta::new(destination, false),
AccountMeta::new_readonly(authority, true),
AccountMeta::new_readonly(EXTRA_META_1, true),
AccountMeta::new_readonly(EXTRA_META_2, true),
AccountMeta::new(extra_meta_3_pubkey, false),
AccountMeta::new(extra_meta_4_pubkey, false),
AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false),
AccountMeta::new_readonly(validate_state_pubkey, false),
];

assert_eq!(instruction.accounts, check_metas);

// With additional signers
let signer_1 = Pubkey::new_unique();
let signer_2 = Pubkey::new_unique();
let signer_3 = Pubkey::new_unique();

let instruction = create_transfer_checked_instruction_with_extra_metas(
&crate::id(),
&source,
&MINT_PUBKEY,
&destination,
&authority,
&[&signer_1, &signer_2, &signer_3],
amount,
DECIMALS,
mock_fetch_account_data_fn,
)
.await
.unwrap();

let check_metas = [
AccountMeta::new(source, false),
AccountMeta::new_readonly(MINT_PUBKEY, false),
AccountMeta::new(destination, false),
AccountMeta::new_readonly(authority, false), // False because of additional signers
AccountMeta::new_readonly(signer_1, true),
AccountMeta::new_readonly(signer_2, true),
AccountMeta::new_readonly(signer_3, true),
AccountMeta::new_readonly(EXTRA_META_1, true),
AccountMeta::new_readonly(EXTRA_META_2, true),
AccountMeta::new(extra_meta_3_pubkey, false),
AccountMeta::new(extra_meta_4_pubkey, false),
AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false),
AccountMeta::new_readonly(validate_state_pubkey, false),
];

assert_eq!(instruction.accounts, check_metas);
}
}