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

transfer hook: add new offchain helper #6099

Merged
merged 2 commits 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.

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

Expand Down Expand Up @@ -47,7 +46,8 @@ where
.ok_or(ProgramError::InvalidAccountData)?;
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
if let Some(program_id) = transfer_hook::get_program_id(&mint) {
resolve_extra_account_metas(
#[allow(deprecated)]
spl_transfer_hook_interface::offchain::resolve_extra_account_metas(
instruction,
fetch_account_data_fn,
mint_address,
Expand Down
3 changes: 3 additions & 0 deletions token/transfer-hook/interface/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ spl-pod = { version = "0.1", path = "../../../libraries/pod" }
[lib]
crate-type = ["cdylib", "lib"]

[dev-dependencies]
tokio = { version = "1.35.1", features = ["full"] }

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
253 changes: 252 additions & 1 deletion token/transfer-hook/interface/src/offchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

pub use spl_tlv_account_resolution::state::{AccountDataResult, AccountFetchError};
use {
crate::{get_extra_account_metas_address, instruction::ExecuteInstruction},
crate::{
error::TransferHookError,
get_extra_account_metas_address,
instruction::{execute, ExecuteInstruction},
},
solana_program::{
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
Expand Down Expand Up @@ -35,6 +39,10 @@ use {
/// &program_id,
/// ).await?;
/// ```
#[deprecated(
since = "0.5.0",
note = "Please use `add_extra_account_metas_for_execute` instead"
)]
pub async fn resolve_extra_account_metas<F, Fut>(
instruction: &mut Instruction,
fetch_account_data_fn: F,
Expand Down Expand Up @@ -68,3 +76,246 @@ where

Ok(())
}

/// Offchain helper to get all additional required account metas for an execute
/// instruction, based on a validation state account.
///
/// The instruction being provided to this function must contain at least the
/// same account keys as the ones being provided, in order. Specifically:
/// 1. source
/// 2. mint
/// 3. destination
/// 4. authority
///
/// The `program_id` should be the program ID of the program that the
/// created `ExecuteInstruction` is for.
///
/// 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
/// add_extra_account_metas_for_execute(
/// &mut instruction,
/// &program_id,
/// &source,
/// &mint,
/// &destination,
/// &authority,
/// amount,
/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)),
/// )
/// .await?;
/// ```
#[allow(clippy::too_many_arguments)]
pub async fn add_extra_account_metas_for_execute<F, Fut>(
instruction: &mut Instruction,
buffalojoec marked this conversation as resolved.
Show resolved Hide resolved
program_id: &Pubkey,
buffalojoec marked this conversation as resolved.
Show resolved Hide resolved
source_pubkey: &Pubkey,
mint_pubkey: &Pubkey,
destination_pubkey: &Pubkey,
authority_pubkey: &Pubkey,
amount: u64,
fetch_account_data_fn: F,
) -> Result<(), AccountFetchError>
where
F: Fn(Pubkey) -> Fut,
Fut: Future<Output = AccountDataResult>,
{
let validate_state_pubkey = get_extra_account_metas_address(mint_pubkey, program_id);
let validate_state_data = fetch_account_data_fn(validate_state_pubkey)
.await?
.ok_or(ProgramError::InvalidAccountData)?;

// Check to make sure the provided keys are in the instruction
if [
source_pubkey,
mint_pubkey,
destination_pubkey,
authority_pubkey,
]
.iter()
.any(|&key| !instruction.accounts.iter().any(|meta| meta.pubkey == *key))
{
Err(TransferHookError::IncorrectAccount)?;
}

let mut execute_instruction = execute(
program_id,
source_pubkey,
mint_pubkey,
destination_pubkey,
authority_pubkey,
&validate_state_pubkey,
amount,
);

ExtraAccountMetaList::add_to_instruction::<ExecuteInstruction, _, _>(
&mut execute_instruction,
fetch_account_data_fn,
&validate_state_data,
)
.await?;

// Add only the extra accounts resolved from the validation state
instruction
.accounts
.extend_from_slice(&execute_instruction.accounts[5..]);

// Add the program id and validation state account
instruction
.accounts
.push(AccountMeta::new_readonly(*program_id, false));
instruction
.accounts
.push(AccountMeta::new_readonly(validate_state_pubkey, false));

Ok(())
}

#[cfg(test)]
mod tests {
use {
super::*,
spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed},
tokio,
};

const PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]);
const EXTRA_META_1: Pubkey = Pubkey::new_from_array([2u8; 32]);
const EXTRA_META_2: Pubkey = Pubkey::new_from_array([3u8; 32]);

// Mock to return the validation state account data
async fn mock_fetch_account_data_fn(_address: Pubkey) -> AccountDataResult {
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)?;
Ok(Some(data))
}

#[tokio::test]
async fn test_add_extra_account_metas_for_execute() {
let source = Pubkey::new_unique();
let mint = 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, &PROGRAM_ID);
let extra_meta_3_pubkey = Pubkey::find_program_address(
&[
source.as_ref(),
destination.as_ref(),
validate_state_pubkey.as_ref(),
],
&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(),
],
&PROGRAM_ID,
)
.0;

// Fail missing key
let mut instruction = Instruction::new_with_bytes(
PROGRAM_ID,
&[],
vec![
// source missing
AccountMeta::new_readonly(mint, false),
AccountMeta::new(destination, false),
AccountMeta::new_readonly(authority, true),
],
);
assert_eq!(
add_extra_account_metas_for_execute(
&mut instruction,
&PROGRAM_ID,
&source,
&mint,
&destination,
&authority,
amount,
mock_fetch_account_data_fn,
)
.await
.unwrap_err()
.downcast::<TransferHookError>()
.unwrap(),
Box::new(TransferHookError::IncorrectAccount)
);

// Success
let mut instruction = Instruction::new_with_bytes(
PROGRAM_ID,
&[],
vec![
AccountMeta::new(source, false),
AccountMeta::new_readonly(mint, false),
AccountMeta::new(destination, false),
AccountMeta::new_readonly(authority, true),
],
);
add_extra_account_metas_for_execute(
&mut instruction,
&PROGRAM_ID,
&source,
&mint,
&destination,
&authority,
amount,
mock_fetch_account_data_fn,
)
.await
.unwrap();

let check_metas = [
AccountMeta::new(source, false),
AccountMeta::new_readonly(mint, 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(PROGRAM_ID, false),
AccountMeta::new_readonly(validate_state_pubkey, false),
];

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