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

Commit 9842992

Browse files
authored
[token-cli] Add support for transfer-hook account resolution for transfers with the transfer-fee extension. (#7171)
* Added transfer_fee extension * added create_transfer_checked_with_fee_instruction_with_extra_metas * add support for transfer-hook account resolution in transfer_with_fee * add offchain helper `invoke_transfer_checked_with_fee` * Nit: Added better description to function * add test for offchain helper `create_transfer_checked_with_fee_instruction_with_extra_metas` * add `success_transfer_with_fee` test * add test `success_transfers_with_fee_using_onchain_helper` * Add cli test `transfer_hook_with_transfer_fee` * fix: correctly use the new onchain helper in test * Remove unneeded helpers
1 parent 3522730 commit 9842992

File tree

5 files changed

+699
-9
lines changed

5 files changed

+699
-9
lines changed

token/cli/tests/command.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ async fn main() {
139139
async_trial!(group_pointer, test_validator, payer),
140140
async_trial!(group_member_pointer, test_validator, payer),
141141
async_trial!(transfer_hook, test_validator, payer),
142+
async_trial!(transfer_hook_with_transfer_fee, test_validator, payer),
142143
async_trial!(metadata, test_validator, payer),
143144
async_trial!(group, test_validator, payer),
144145
async_trial!(confidential_transfer_with_fee, test_validator, payer),
@@ -3872,6 +3873,107 @@ async fn transfer_hook(test_validator: &TestValidator, payer: &Keypair) {
38723873
assert_eq!(extension.program_id, None.try_into().unwrap());
38733874
}
38743875

3876+
async fn transfer_hook_with_transfer_fee(test_validator: &TestValidator, payer: &Keypair) {
3877+
let program_id = spl_token_2022::id();
3878+
let mut config = test_config_with_default_signer(test_validator, payer, &program_id);
3879+
let transfer_hook_program_id = Pubkey::new_unique();
3880+
3881+
let transfer_fee_basis_points = 100;
3882+
let maximum_fee: u64 = 10_000_000_000;
3883+
3884+
let result = process_test_command(
3885+
&config,
3886+
payer,
3887+
&[
3888+
"spl-token",
3889+
CommandName::CreateToken.into(),
3890+
"--program-id",
3891+
&program_id.to_string(),
3892+
"--transfer-hook",
3893+
&transfer_hook_program_id.to_string(),
3894+
"--transfer-fee",
3895+
&transfer_fee_basis_points.to_string(),
3896+
&maximum_fee.to_string(),
3897+
],
3898+
)
3899+
.await;
3900+
3901+
// Check that the transfer-hook extension is correctly configured
3902+
let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
3903+
let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap();
3904+
let account = config.rpc_client.get_account(&mint).await.unwrap();
3905+
let mint_state = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
3906+
let extension = mint_state.get_extension::<TransferHook>().unwrap();
3907+
assert_eq!(
3908+
extension.program_id,
3909+
Some(transfer_hook_program_id).try_into().unwrap()
3910+
);
3911+
3912+
// Check that the transfer-fee extension is correctly configured
3913+
let extension = mint_state.get_extension::<TransferFeeConfig>().unwrap();
3914+
assert_eq!(
3915+
u16::from(extension.older_transfer_fee.transfer_fee_basis_points),
3916+
transfer_fee_basis_points
3917+
);
3918+
assert_eq!(
3919+
u64::from(extension.older_transfer_fee.maximum_fee),
3920+
maximum_fee
3921+
);
3922+
assert_eq!(
3923+
u16::from(extension.newer_transfer_fee.transfer_fee_basis_points),
3924+
transfer_fee_basis_points
3925+
);
3926+
assert_eq!(
3927+
u64::from(extension.newer_transfer_fee.maximum_fee),
3928+
maximum_fee
3929+
);
3930+
3931+
// Make sure that parsing transfer hook accounts and expected-fee works
3932+
let blockhash = Hash::default();
3933+
let program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>> = Arc::new(
3934+
ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction),
3935+
);
3936+
config.program_client = program_client;
3937+
3938+
let _result = exec_test_cmd(
3939+
&config,
3940+
&[
3941+
"spl-token",
3942+
CommandName::Transfer.into(),
3943+
&mint.to_string(),
3944+
"100",
3945+
&Pubkey::new_unique().to_string(),
3946+
"--blockhash",
3947+
&blockhash.to_string(),
3948+
"--nonce",
3949+
&Pubkey::new_unique().to_string(),
3950+
"--nonce-authority",
3951+
&Pubkey::new_unique().to_string(),
3952+
"--sign-only",
3953+
"--mint-decimals",
3954+
&format!("{}", TEST_DECIMALS),
3955+
"--from",
3956+
&Pubkey::new_unique().to_string(),
3957+
"--owner",
3958+
&Pubkey::new_unique().to_string(),
3959+
"--transfer-hook-account",
3960+
&format!("{}:readonly", Pubkey::new_unique()),
3961+
"--transfer-hook-account",
3962+
&format!("{}:writable", Pubkey::new_unique()),
3963+
"--transfer-hook-account",
3964+
&format!("{}:readonly-signer", Pubkey::new_unique()),
3965+
"--transfer-hook-account",
3966+
&format!("{}:writable-signer", Pubkey::new_unique()),
3967+
"--expected-fee",
3968+
"1",
3969+
"--program-id",
3970+
&program_id.to_string(),
3971+
],
3972+
)
3973+
.await
3974+
.unwrap();
3975+
}
3976+
38753977
async fn metadata(test_validator: &TestValidator, payer: &Keypair) {
38763978
let program_id = spl_token_2022::id();
38773979
let config = test_config_with_default_signer(test_validator, payer, &program_id);

token/client/src/token.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,21 +1166,44 @@ where
11661166
let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys);
11671167
let decimals = self.decimals.ok_or(TokenError::MissingDecimals)?;
11681168

1169-
self.process_ixs(
1170-
&[transfer_fee::instruction::transfer_checked_with_fee(
1169+
let fetch_account_data_fn = |address| {
1170+
self.client
1171+
.get_account(address)
1172+
.map_ok(|opt| opt.map(|acc| acc.data))
1173+
};
1174+
1175+
let instruction = if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts {
1176+
let mut instruction = transfer_fee::instruction::transfer_checked_with_fee(
11711177
&self.program_id,
11721178
source,
1173-
&self.pubkey,
1179+
self.get_address(),
11741180
destination,
11751181
authority,
11761182
&multisig_signers,
11771183
amount,
11781184
decimals,
11791185
fee,
1180-
)?],
1181-
signing_keypairs,
1182-
)
1183-
.await
1186+
)?;
1187+
instruction.accounts.extend(transfer_hook_accounts.clone());
1188+
instruction
1189+
} else {
1190+
offchain::create_transfer_checked_with_fee_instruction_with_extra_metas(
1191+
&self.program_id,
1192+
source,
1193+
self.get_address(),
1194+
destination,
1195+
authority,
1196+
&multisig_signers,
1197+
amount,
1198+
decimals,
1199+
fee,
1200+
fetch_account_data_fn,
1201+
)
1202+
.await
1203+
.map_err(|_| TokenError::AccountNotFound)?
1204+
};
1205+
1206+
self.process_ixs(&[instruction], signing_keypairs).await
11841207
}
11851208

11861209
/// Burn tokens from account

0 commit comments

Comments
 (0)