token-cli: Support transfer fee extension (#3792)

* token-cli: Add transfer fee to create-token

* Add `expected-fee` arg to transfer

* Add withdraw-withheld-tokens implementation

* Add optional `Harvest` call while closing account

* Add `set-transfer-fee` command

* Fix rebase issues
This commit is contained in:
Jon Cinque 2022-11-11 19:16:51 +01:00 committed by GitHub
parent de73b277f3
commit d50f2745f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 622 additions and 19 deletions

View File

@ -37,9 +37,13 @@ use solana_sdk::{
use spl_associated_token_account::get_associated_token_address_with_program_id;
use spl_token_2022::{
extension::{
cpi_guard::CpiGuard, default_account_state::DefaultAccountState,
interest_bearing_mint::InterestBearingConfig, memo_transfer::MemoTransfer,
mint_close_authority::MintCloseAuthority, ExtensionType, StateWithExtensionsOwned,
cpi_guard::CpiGuard,
default_account_state::DefaultAccountState,
interest_bearing_mint::InterestBearingConfig,
memo_transfer::MemoTransfer,
mint_close_authority::MintCloseAuthority,
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
ExtensionType, StateWithExtensionsOwned,
},
instruction::*,
state::{Account, AccountState, Mint},
@ -135,6 +139,8 @@ pub enum CommandName {
EnableCpiGuard,
DisableCpiGuard,
UpdateDefaultAccountState,
WithdrawWithheldTokens,
SetTransferFee,
}
impl fmt::Display for CommandName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@ -253,6 +259,10 @@ where
}
pub(crate) type Error = Box<dyn std::error::Error + Send + Sync>;
fn print_error_and_exit<T, E: Display>(e: E) -> T {
eprintln!("error: {}", e);
exit(1)
}
type BulkSigners = Vec<Arc<dyn Signer>>;
pub(crate) type CommandResult = Result<String, Error>;
@ -269,11 +279,8 @@ fn get_signer(
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Option<(Arc<dyn Signer>, Pubkey)> {
matches.value_of(keypair_name).map(|path| {
let signer =
signer_from_path(matches, path, keypair_name, wallet_manager).unwrap_or_else(|e| {
eprintln!("error: {}", e);
exit(1);
});
let signer = signer_from_path(matches, path, keypair_name, wallet_manager)
.unwrap_or_else(print_error_and_exit);
let signer_pubkey = signer.pubkey();
(Arc::from(signer), signer_pubkey)
})
@ -391,6 +398,7 @@ async fn command_create_token(
memo: Option<String>,
rate_bps: Option<i16>,
default_account_state: Option<AccountState>,
transfer_fee: Option<(u16, u64)>,
bulk_signers: Vec<Arc<dyn Signer>>,
) -> CommandResult {
println_display(
@ -432,6 +440,15 @@ async fn command_create_token(
extensions.push(ExtensionInitializationParams::DefaultAccountState { state })
}
if let Some((transfer_fee_basis_points, maximum_fee)) = transfer_fee {
extensions.push(ExtensionInitializationParams::TransferFeeConfig {
transfer_fee_config_authority: Some(authority),
withdraw_withheld_authority: Some(authority),
transfer_fee_basis_points,
maximum_fee,
});
}
if let Some(text) = memo {
token.with_memo(text, vec![config.default_signer()?.pubkey()]);
}
@ -520,6 +537,83 @@ async fn command_set_interest_rate(
})
}
async fn command_set_transfer_fee(
config: &Config<'_>,
token_pubkey: Pubkey,
transfer_fee_authority: Pubkey,
transfer_fee_basis_points: u16,
maximum_fee: f64,
mint_decimals: Option<u8>,
bulk_signers: Vec<Arc<dyn Signer>>,
) -> CommandResult {
let decimals = if !config.sign_only {
let mint_account = config.get_account_checked(&token_pubkey).await?;
let mint_state = StateWithExtensionsOwned::<Mint>::unpack(mint_account.data)
.map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?;
if mint_decimals.is_some() && mint_decimals != Some(mint_state.base.decimals) {
return Err(format!(
"Decimals {} was provided, but actual value is {}",
mint_decimals.unwrap(),
mint_state.base.decimals
)
.into());
}
if let Ok(transfer_fee_config) = mint_state.get_extension::<TransferFeeConfig>() {
let mint_fee_authority_pubkey =
Option::<Pubkey>::from(transfer_fee_config.transfer_fee_config_authority);
if mint_fee_authority_pubkey != Some(transfer_fee_authority) {
return Err(format!(
"Mint {} has transfer fee authority {}, but {} was provided",
token_pubkey,
mint_fee_authority_pubkey
.map(|pubkey| pubkey.to_string())
.unwrap_or_else(|| "disabled".to_string()),
transfer_fee_authority
)
.into());
}
} else {
return Err(format!("Mint {} does not have a transfer fee", token_pubkey).into());
}
mint_state.base.decimals
} else {
mint_decimals.unwrap()
};
println_display(
config,
format!(
"Setting transfer fee for {} to {} bps, {} maximum",
token_pubkey, transfer_fee_basis_points, maximum_fee
),
);
let token = token_client_from_config(config, &token_pubkey, Some(decimals))?;
let maximum_fee = spl_token::ui_amount_to_amount(maximum_fee, decimals);
let res = token
.set_transfer_fee(
&transfer_fee_authority,
transfer_fee_basis_points,
maximum_fee,
&bulk_signers,
)
.await?;
let tx_return = finish_tx(config, &res, false).await?;
Ok(match tx_return {
TransactionReturnData::CliSignature(signature) => {
config.output_format.formatted_string(&signature)
}
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
config.output_format.formatted_string(&sign_only_data)
}
})
}
async fn command_create_account(
config: &Config<'_>,
token_pubkey: Pubkey,
@ -805,6 +899,7 @@ async fn command_transfer(
mint_decimals: Option<u8>,
recipient_is_ata_owner: bool,
use_unchecked_instruction: bool,
ui_fee: Option<f64>,
memo: Option<String>,
bulk_signers: BulkSigners,
no_wait: bool,
@ -880,6 +975,9 @@ async fn command_transfer(
maybe_transfer_balance.unwrap()
};
let maybe_fee =
ui_fee.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals));
// determine whether recipient is a token account or an expected owner of one
let recipient_is_token_account = if !config.sign_only {
// in online mode we can fetch it and see
@ -1037,6 +1135,18 @@ async fn command_transfer(
&recipient_owner,
&sender_owner,
transfer_balance,
maybe_fee,
&bulk_signers,
)
.await?
} else if let Some(fee) = maybe_fee {
token
.transfer_with_fee(
&sender,
&recipient_token_account,
&sender_owner,
transfer_balance,
fee,
&bulk_signers,
)
.await?
@ -1448,7 +1558,8 @@ async fn command_close(
recipient: Pubkey,
bulk_signers: BulkSigners,
) -> CommandResult {
let mint_pubkey = if !config.sign_only {
let mut results = vec![];
let token = if !config.sign_only {
let source_account = config.get_account_checked(&account).await?;
let source_state = StateWithExtensionsOwned::<Account>::unpack(source_account.data)
@ -1463,26 +1574,42 @@ async fn command_close(
.into());
}
source_state.base.mint
let token = token_client_from_config(config, &source_state.base.mint, None)?;
if let Ok(extension) = source_state.get_extension::<TransferFeeAmount>() {
if u64::from(extension.withheld_amount) != 0 {
let res = token.harvest_withheld_tokens_to_mint(&[&account]).await?;
let tx_return = finish_tx(config, &res, false).await?;
results.push(match tx_return {
TransactionReturnData::CliSignature(signature) => {
config.output_format.formatted_string(&signature)
}
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
config.output_format.formatted_string(&sign_only_data)
}
});
}
}
token
} else {
// default is safe here because close doesnt use it
Pubkey::default()
token_client_from_config(config, &Pubkey::default(), None)?
};
let token = token_client_from_config(config, &mint_pubkey, None)?;
let res = token
.close_account(&account, &recipient, &close_authority, &bulk_signers)
.await?;
let tx_return = finish_tx(config, &res, false).await?;
Ok(match tx_return {
results.push(match tx_return {
TransactionReturnData::CliSignature(signature) => {
config.output_format.formatted_string(&signature)
}
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
config.output_format.formatted_string(&sign_only_data)
}
})
});
Ok(results.join(""))
}
async fn command_close_mint(
@ -2069,6 +2196,79 @@ async fn command_update_default_account_state(
})
}
async fn command_withdraw_withheld_tokens(
config: &Config<'_>,
destination_token_account: Pubkey,
source_token_accounts: Vec<Pubkey>,
authority: Pubkey,
include_mint: bool,
bulk_signers: BulkSigners,
) -> CommandResult {
if config.sign_only {
panic!("Config can not be sign-only for withdrawing withheld tokens.");
}
let destination_account = config
.get_account_checked(&destination_token_account)
.await?;
let destination_state = StateWithExtensionsOwned::<Account>::unpack(destination_account.data)
.map_err(|_| {
format!(
"Could not deserialize token account {}",
destination_token_account
)
})?;
let token_pubkey = destination_state.base.mint;
destination_state
.get_extension::<TransferFeeAmount>()
.map_err(|_| format!("Token mint {} has no transfer fee configured", token_pubkey))?;
let token = token_client_from_config(config, &token_pubkey, None)?;
let mut results = vec![];
if include_mint {
let res = token
.withdraw_withheld_tokens_from_mint(
&destination_token_account,
&authority,
&bulk_signers,
)
.await;
let tx_return = finish_tx(config, &res?, false).await?;
results.push(match tx_return {
TransactionReturnData::CliSignature(signature) => {
config.output_format.formatted_string(&signature)
}
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
config.output_format.formatted_string(&sign_only_data)
}
});
}
let source_refs = source_token_accounts.iter().collect::<Vec<_>>();
// this can be tweaked better, but keep it simple for now
const MAX_WITHDRAWAL_ACCOUNTS: usize = 25;
for sources in source_refs.chunks(MAX_WITHDRAWAL_ACCOUNTS) {
let res = token
.withdraw_withheld_tokens_from_accounts(
&destination_token_account,
&authority,
sources,
&bulk_signers,
)
.await;
let tx_return = finish_tx(config, &res?, false).await?;
results.push(match tx_return {
TransactionReturnData::CliSignature(signature) => {
config.output_format.formatted_string(&signature)
}
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
config.output_format.formatted_string(&sign_only_data)
}
});
}
Ok(results.join(""))
}
struct SignOnlyNeedsFullMintSpec {}
impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec {
fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> {
@ -2261,6 +2461,17 @@ fn app<'a, 'b>(
This behavior is not the same as the default, which makes it \
impossible to specify a default account state in the future."),
)
.arg(
Arg::with_name("transfer_fee")
.long("transfer-fee")
.value_names(&["FEE_IN_BASIS_POINTS", "MAXIMUM_FEE"])
.takes_value(true)
.number_of_values(2)
.help(
"Add a transfer fee to the mint. \
The mint authority can set the fee and withdraw collected fees.",
),
)
.nonce_args(true)
.arg(memo_arg())
)
@ -2516,6 +2727,14 @@ fn app<'a, 'b>(
.requires("sign_only")
.help("In sign-only mode, specifies that the recipient is the owner of the associated token account rather than an actual token account"),
)
.arg(
Arg::with_name("expected_fee")
.long("expected-fee")
.validator(is_amount)
.value_name("TOKEN_AMOUNT")
.takes_value(true)
.help("Expected fee amount collected during the transfer"),
)
.arg(multisig_signer_arg())
.arg(mint_decimals_arg())
.nonce_args(true)
@ -3178,6 +3397,88 @@ fn app<'a, 'b>(
.nonce_args(true)
.offline_args(),
)
.subcommand(
SubCommand::with_name(CommandName::WithdrawWithheldTokens.into())
.about("Withdraw withheld transfer fee tokens from mint and / or account(s)")
.arg(
Arg::with_name("account")
.validator(is_valid_pubkey)
.value_name("TOKEN_ACCOUNT_ADDRESS")
.takes_value(true)
.index(1)
.required(true)
.help("The address of the token account to receive withdrawn tokens"),
)
.arg(
Arg::with_name("source")
.validator(is_valid_pubkey)
.value_name("ACCOUNT_ADDRESS")
.takes_value(true)
.multiple(true)
.min_values(0u64)
.help("The token accounts to withdraw from")
)
.arg(
Arg::with_name("include_mint")
.long("include-mint")
.takes_value(false)
.help("Also withdraw withheld tokens from the mint"),
)
.arg(
Arg::with_name("withdraw_withheld_authority")
.long("withdraw-withheld-authority")
.alias("owner")
.value_name("KEYPAIR")
.validator(is_valid_signer)
.takes_value(true)
.help(
"Specify the withdraw withheld authority keypair. \
This may be a keypair file or the ASK keyword. \
Defaults to the client keypair."
),
)
.arg(owner_address_arg())
.arg(multisig_signer_arg())
)
.subcommand(
SubCommand::with_name(CommandName::SetTransferFee.into())
.about("Set the transfer fee for a token with a configured transfer fee")
.arg(
Arg::with_name("token")
.validator(is_valid_pubkey)
.value_name("TOKEN_MINT_ADDRESS")
.takes_value(true)
.required(true)
.help("The interest-bearing token address"),
)
.arg(
Arg::with_name("transfer_fee_basis_points")
.value_name("FEE_IN_BASIS_POINTS")
.takes_value(true)
.required(true)
.help("The new transfer fee in basis points"),
)
.arg(
Arg::with_name("maximum_fee")
.value_name("TOKEN_AMOUNT")
.validator(is_amount)
.takes_value(true)
.required(true)
.help("The new maximum transfer fee in UI amount"),
)
.arg(
Arg::with_name("transfer_fee_authority")
.long("transfer-fee-authority")
.validator(is_valid_signer)
.value_name("SIGNER")
.takes_value(true)
.help(
"Specify the rate authority keypair. \
Defaults to the client keypair address."
)
)
.arg(mint_decimals_arg())
)
}
#[tokio::main]
@ -3239,6 +3540,19 @@ async fn process_command<'a>(
let memo = value_t!(arg_matches, "memo", String).ok();
let rate_bps = value_t!(arg_matches, "interest_rate", i16).ok();
let transfer_fee = arg_matches.values_of("transfer_fee").map(|mut v| {
(
v.next()
.unwrap()
.parse::<u16>()
.unwrap_or_else(print_error_and_exit),
v.next()
.unwrap()
.parse::<u64>()
.unwrap_or_else(print_error_and_exit),
)
});
let (token_signer, token) =
get_signer(arg_matches, "token_keypair", &mut wallet_manager)
.unwrap_or_else(new_throwaway_signer);
@ -3266,6 +3580,7 @@ async fn process_command<'a>(
memo,
rate_bps,
default_account_state,
transfer_fee,
bulk_signers,
)
.await
@ -3318,10 +3633,7 @@ async fn process_command<'a>(
let minimum_signers = value_of::<u8>(arg_matches, "minimum_signers").unwrap();
let multisig_members =
pubkeys_of_multiple_signers(arg_matches, "multisig_member", &mut wallet_manager)
.unwrap_or_else(|e| {
eprintln!("error: {}", e);
exit(1);
})
.unwrap_or_else(print_error_and_exit)
.unwrap();
if minimum_signers as usize > multisig_members.len() {
eprintln!(
@ -3399,6 +3711,7 @@ async fn process_command<'a>(
let recipient_is_ata_owner = arg_matches.is_present("recipient_is_ata_owner");
let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction");
let expected_fee = value_of::<f64>(arg_matches, "expected_fee");
let memo = value_t!(arg_matches, "memo", String).ok();
command_transfer(
@ -3413,6 +3726,7 @@ async fn process_command<'a>(
mint_decimals,
recipient_is_ata_owner,
use_unchecked_instruction,
expected_fee,
memo,
bulk_signers,
arg_matches.is_present("no_wait"),
@ -3817,6 +4131,60 @@ async fn process_command<'a>(
)
.await
}
(CommandName::WithdrawWithheldTokens, arg_matches) => {
let (authority_signer, authority) = config.signer_or_default(
arg_matches,
"withdraw_withheld_authority",
&mut wallet_manager,
);
if !bulk_signers.contains(&authority_signer) {
bulk_signers.push(authority_signer);
}
// Since destination is required it will always be present
let destination_token_account =
pubkey_of_signer(arg_matches, "account", &mut wallet_manager)
.unwrap()
.unwrap();
let include_mint = arg_matches.is_present("include_mint");
let source_accounts = arg_matches
.values_of("source")
.unwrap_or_default()
.into_iter()
.map(|s| Pubkey::from_str(s).unwrap_or_else(print_error_and_exit))
.collect::<Vec<_>>();
command_withdraw_withheld_tokens(
config,
destination_token_account,
source_accounts,
authority,
include_mint,
bulk_signers,
)
.await
}
(CommandName::SetTransferFee, arg_matches) => {
let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
.unwrap()
.unwrap();
let transfer_fee_basis_points =
value_t_or_exit!(arg_matches, "transfer_fee_basis_points", u16);
let maximum_fee = value_t_or_exit!(arg_matches, "maximum_fee", f64);
let (transfer_fee_authority_signer, transfer_fee_authority_pubkey) = config
.signer_or_default(arg_matches, "transfer_fee_authority", &mut wallet_manager);
let mint_decimals = value_of::<u8>(arg_matches, MINT_DECIMALS_ARG.name);
let bulk_signers = vec![transfer_fee_authority_signer];
command_set_transfer_fee(
config,
token_pubkey,
transfer_fee_authority_pubkey,
transfer_fee_basis_points,
maximum_fee,
mint_decimals,
bulk_signers,
)
.await
}
}
}
@ -4008,6 +4376,7 @@ mod tests {
None,
None,
None,
None,
bulk_signers,
)
.await
@ -4036,6 +4405,7 @@ mod tests {
None,
Some(rate_bps),
None,
None,
bulk_signers,
)
.await
@ -5284,6 +5654,7 @@ mod tests {
None,
None,
None,
None,
bulk_signers,
)
.await
@ -5589,6 +5960,7 @@ mod tests {
None,
None,
None,
None,
bulk_signers,
)
.await
@ -5642,6 +6014,7 @@ mod tests {
None,
None,
Some(AccountState::Frozen),
None,
bulk_signers,
)
.await
@ -5682,4 +6055,220 @@ mod tests {
let account = StateWithExtensionsOwned::<Account>::unpack(token_account.data).unwrap();
assert_eq!(account.base.state, AccountState::Initialized);
}
#[tokio::test]
#[serial]
async fn transfer_fee() {
let (test_validator, payer) = new_validator_for_test().await;
let config =
test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id());
let token_keypair = Keypair::new();
let token_pubkey = token_keypair.pubkey();
let bulk_signers: Vec<Arc<dyn Signer>> =
vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)];
let transfer_fee_basis_points = 100;
let maximum_fee = 2_000_000;
command_create_token(
&config,
TEST_DECIMALS,
token_pubkey,
payer.pubkey(),
false,
false,
false,
None,
None,
None,
Some((transfer_fee_basis_points, maximum_fee)),
bulk_signers,
)
.await
.unwrap();
let account = config.rpc_client.get_account(&token_pubkey).await.unwrap();
let test_mint = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
let extension = test_mint.get_extension::<TransferFeeConfig>().unwrap();
assert_eq!(
u16::from(extension.older_transfer_fee.transfer_fee_basis_points),
transfer_fee_basis_points
);
assert_eq!(
u64::from(extension.older_transfer_fee.maximum_fee),
maximum_fee
);
assert_eq!(
u16::from(extension.newer_transfer_fee.transfer_fee_basis_points),
transfer_fee_basis_points
);
assert_eq!(
u64::from(extension.newer_transfer_fee.maximum_fee),
maximum_fee
);
let total_amount = 1000.0;
let transfer_amount = 100.0;
let token_account = create_associated_account(&config, &payer, token_pubkey).await;
let source_account = create_auxiliary_account(&config, &payer, token_pubkey).await;
mint_tokens(&config, &payer, token_pubkey, total_amount, source_account).await;
// withdraw from account directly
process_test_command(
&config,
&payer,
&[
"spl-token",
CommandName::Transfer.into(),
"--from",
&source_account.to_string(),
&token_pubkey.to_string(),
&transfer_amount.to_string(),
&token_account.to_string(),
"--expected-fee",
"1",
],
)
.await
.unwrap();
let account = config.rpc_client.get_account(&token_account).await.unwrap();
let account_state = StateWithExtensionsOwned::<Account>::unpack(account.data).unwrap();
let extension = account_state.get_extension::<TransferFeeAmount>().unwrap();
let withheld_amount =
spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS);
assert_eq!(withheld_amount, 1.0);
process_test_command(
&config,
&payer,
&[
"spl-token",
CommandName::WithdrawWithheldTokens.into(),
&token_account.to_string(),
&token_account.to_string(),
],
)
.await
.unwrap();
let account = config.rpc_client.get_account(&token_account).await.unwrap();
let account_state = StateWithExtensionsOwned::<Account>::unpack(account.data).unwrap();
let extension = account_state.get_extension::<TransferFeeAmount>().unwrap();
assert_eq!(u64::from(extension.withheld_amount), 0);
// withdraw from mint after account closure
// gather fees
process_test_command(
&config,
&payer,
&[
"spl-token",
CommandName::Transfer.into(),
"--from",
&source_account.to_string(),
&token_pubkey.to_string(),
&(total_amount - transfer_amount).to_string(),
&token_account.to_string(),
"--expected-fee",
"9",
],
)
.await
.unwrap();
// burn tokens
let account = config.rpc_client.get_account(&token_account).await.unwrap();
let account_state = StateWithExtensionsOwned::<Account>::unpack(account.data).unwrap();
let burn_amount = spl_token::amount_to_ui_amount(account_state.base.amount, TEST_DECIMALS);
process_test_command(
&config,
&payer,
&[
"spl-token",
CommandName::Burn.into(),
&token_account.to_string(),
&burn_amount.to_string(),
],
)
.await
.unwrap();
let account = config.rpc_client.get_account(&token_account).await.unwrap();
let account_state = StateWithExtensionsOwned::<Account>::unpack(account.data).unwrap();
let extension = account_state.get_extension::<TransferFeeAmount>().unwrap();
let withheld_amount =
spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS);
assert_eq!(withheld_amount, 9.0);
process_test_command(
&config,
&payer,
&[
"spl-token",
CommandName::Close.into(),
"--address",
&token_account.to_string(),
"--recipient",
&payer.pubkey().to_string(),
],
)
.await
.unwrap();
let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap();
let mint_state = StateWithExtensionsOwned::<Mint>::unpack(mint.data).unwrap();
let extension = mint_state.get_extension::<TransferFeeConfig>().unwrap();
let withheld_amount =
spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS);
assert_eq!(withheld_amount, 9.0);
process_test_command(
&config,
&payer,
&[
"spl-token",
CommandName::WithdrawWithheldTokens.into(),
&source_account.to_string(),
"--include-mint",
],
)
.await
.unwrap();
let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap();
let mint_state = StateWithExtensionsOwned::<Mint>::unpack(mint.data).unwrap();
let extension = mint_state.get_extension::<TransferFeeConfig>().unwrap();
assert_eq!(u64::from(extension.withheld_amount), 0);
// set the transfer fee
let new_transfer_fee_basis_points = 800;
let new_maximum_fee = 5_000_000.0;
process_test_command(
&config,
&payer,
&[
"spl-token",
CommandName::SetTransferFee.into(),
&token_pubkey.to_string(),
&new_transfer_fee_basis_points.to_string(),
&new_maximum_fee.to_string(),
],
)
.await
.unwrap();
let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap();
let mint_state = StateWithExtensionsOwned::<Mint>::unpack(mint.data).unwrap();
let extension = mint_state.get_extension::<TransferFeeConfig>().unwrap();
assert_eq!(
u16::from(extension.newer_transfer_fee.transfer_fee_basis_points),
new_transfer_fee_basis_points
);
let new_maximum_fee = spl_token::ui_amount_to_amount(new_maximum_fee, TEST_DECIMALS);
assert_eq!(
u64::from(extension.newer_transfer_fee.maximum_fee),
new_maximum_fee
);
}
}

View File

@ -790,6 +790,7 @@ where
destination_owner: &Pubkey,
authority: &Pubkey,
amount: u64,
fee: Option<u64>,
signing_keypairs: &S,
) -> TokenResult<T::Output> {
let signing_pubkeys = signing_keypairs.pubkeys();
@ -808,7 +809,20 @@ where
)),
];
if let Some(decimals) = self.decimals {
if let Some(fee) = fee {
let decimals = self.decimals.ok_or(TokenError::MissingDecimals)?;
instructions.push(transfer_fee::instruction::transfer_checked_with_fee(
&self.program_id,
source,
&self.pubkey,
destination,
authority,
&multisig_signers,
amount,
decimals,
fee,
)?);
} else if let Some(decimals) = self.decimals {
instructions.push(instruction::transfer_checked(
&self.program_id,
source,