From 714e869bc267ff6788edf6539953be95df742766 Mon Sep 17 00:00:00 2001 From: steviez Date: Mon, 4 Mar 2024 11:17:28 -0600 Subject: [PATCH] token-cli: Allow burn command to accept ALL keyword for amount (#6057) Several of the token-spl command support using the ALL keyword as the amount. spl-token burn does not, so add support for that command. --- token/cli/src/clap_app.rs | 4 +- token/cli/src/command.rs | 49 ++++++++---- token/cli/tests/command.rs | 159 +++++++++++++++++++++++++++---------- 3 files changed, 153 insertions(+), 59 deletions(-) diff --git a/token/cli/src/clap_app.rs b/token/cli/src/clap_app.rs index 7def6bcbe80..182a4dcd891 100644 --- a/token/cli/src/clap_app.rs +++ b/token/cli/src/clap_app.rs @@ -1371,12 +1371,12 @@ pub fn app<'a, 'b>( ) .arg( Arg::with_name("amount") - .validator(is_amount) + .validator(is_amount_or_all) .value_name("TOKEN_AMOUNT") .takes_value(true) .index(2) .required(true) - .help("Amount to burn, in tokens"), + .help("Amount to burn, in tokens; accepts keyword ALL"), ) .arg(owner_keypair_arg_with_value_name("TOKEN_OWNER_KEYPAIR") .help( diff --git a/token/cli/src/command.rs b/token/cli/src/command.rs index 8862e6a0f67..e25a27c197b 100644 --- a/token/cli/src/command.rs +++ b/token/cli/src/command.rs @@ -107,6 +107,14 @@ fn get_signer( (Arc::from(signer), signer_pubkey) }) } + +fn parse_amount_or_all(matches: &ArgMatches<'_>) -> Option { + match matches.value_of("amount").unwrap() { + "ALL" => None, + amount => Some(amount.parse::().unwrap()), + } +} + async fn check_wallet_balance( config: &Config<'_>, wallet: &Pubkey, @@ -1654,21 +1662,15 @@ async fn command_burn( config: &Config<'_>, account: Pubkey, owner: Pubkey, - ui_amount: f64, + ui_amount: Option, mint_address: Option, mint_decimals: Option, use_unchecked_instruction: bool, memo: Option, bulk_signers: BulkSigners, ) -> CommandResult { - println_display( - config, - format!("Burn {} tokens\n Source: {}", ui_amount, account), - ); - let mint_address = config.check_account(&account, mint_address).await?; let mint_info = config.get_mint_info(&mint_address, mint_decimals).await?; - let amount = spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals); let decimals = if use_unchecked_instruction { None } else { @@ -1676,6 +1678,27 @@ async fn command_burn( }; let token = token_client_from_config(config, &mint_info.address, decimals)?; + + let amount = if let Some(ui_amount) = ui_amount { + spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals) + } else { + if config.sign_only { + return Err("Use of ALL keyword to burn tokens requires online signing" + .to_string() + .into()); + } + token.get_account_info(&account).await?.base.amount + }; + + println_display( + config, + format!( + "Burn {} tokens\n Source: {}", + spl_token::amount_to_ui_amount(amount, mint_info.decimals), + account + ), + ); + if let Some(text) = memo { token.with_memo(text, vec![config.default_signer()?.pubkey()]); } @@ -3701,10 +3724,7 @@ pub async fn process_command<'a>( let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) .unwrap() .unwrap(); - let amount = match arg_matches.value_of("amount").unwrap() { - "ALL" => None, - amount => Some(amount.parse::().unwrap()), - }; + let amount = parse_amount_or_all(arg_matches); let recipient = pubkey_of_signer(arg_matches, "recipient", &mut wallet_manager) .unwrap() .unwrap(); @@ -3792,7 +3812,7 @@ pub async fn process_command<'a>( push_signer_with_dedup(owner_signer, &mut bulk_signers); } - let amount = value_t_or_exit!(arg_matches, "amount", f64); + let amount = parse_amount_or_all(arg_matches); let mint_address = pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); @@ -4424,10 +4444,7 @@ pub async fn process_command<'a>( let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) .unwrap() .unwrap(); - let amount = match arg_matches.value_of("amount").unwrap() { - "ALL" => None, - amount => Some(amount.parse::().unwrap()), - }; + let amount = parse_amount_or_all(arg_matches); let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); let (owner_signer, owner) = diff --git a/token/cli/tests/command.rs b/token/cli/tests/command.rs index bc7d9227643..5111170ed60 100644 --- a/token/cli/tests/command.rs +++ b/token/cli/tests/command.rs @@ -107,10 +107,11 @@ async fn main() { async_trial!(disable_mint_authority, test_validator, payer), async_trial!(set_owner, test_validator, payer), async_trial!(transfer_with_account_delegate, test_validator, payer), + async_trial!(burn, test_validator, payer), async_trial!(burn_with_account_delegate, test_validator, payer), - async_trial!(close_mint, test_validator, payer), async_trial!(burn_with_permanent_delegate, test_validator, payer), async_trial!(transfer_with_permanent_delegate, test_validator, payer), + async_trial!(close_mint, test_validator, payer), async_trial!(required_transfer_memos, test_validator, payer), async_trial!(cpi_guard, test_validator, payer), async_trial!(immutable_accounts, test_validator, payer), @@ -1508,6 +1509,82 @@ async fn transfer_with_account_delegate(test_validator: &TestValidator, payer: & } } +async fn burn(test_validator: &TestValidator, payer: &Keypair) { + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let mut config = test_config_with_default_signer(test_validator, payer, program_id); + let token = create_token(&config, payer).await; + let source = create_associated_account(&config, payer, &token, &payer.pubkey()).await; + let ui_amount = 100.0; + mint_tokens(&config, payer, token, ui_amount, source) + .await + .unwrap(); + + process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::Burn.into(), + &source.to_string(), + "10", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&source).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); + assert_eq!(token_account.base.amount, amount); + + process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::Burn.into(), + &source.to_string(), + "ALL", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&source).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let amount = spl_token::ui_amount_to_amount(0.0, TEST_DECIMALS); + assert_eq!(token_account.base.amount, amount); + + let result = process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::Burn.into(), + &source.to_string(), + "10", + ], + ) + .await; + assert!(result.is_err()); + + // Use of the ALL keyword not supported with offline signing + config.sign_only = true; + let result = process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::Burn.into(), + &source.to_string(), + "ALL", + ], + ) + .await; + assert!(result.is_err_and(|err| err.to_string().contains("ALL"))); + } +} + async fn burn_with_account_delegate(test_validator: &TestValidator, payer: &Keypair) { for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { let config = test_config_with_default_signer(test_validator, payer, program_id); @@ -1600,46 +1677,6 @@ async fn burn_with_account_delegate(test_validator: &TestValidator, payer: &Keyp } } -async fn close_mint(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - let token = Keypair::new(); - let token_pubkey = token.pubkey(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--enable-close", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data); - assert!(test_mint.is_ok()); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CloseMint.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await; - assert!(account.is_err()); -} - async fn burn_with_permanent_delegate(test_validator: &TestValidator, payer: &Keypair) { let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); @@ -1797,6 +1834,46 @@ async fn transfer_with_permanent_delegate(test_validator: &TestValidator, payer: assert_eq!(ui_account.token_amount.amount, format!("{amount}")); } +async fn close_mint(test_validator: &TestValidator, payer: &Keypair) { + let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); + + let token = Keypair::new(); + let token_pubkey = token.pubkey(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--enable-close", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let test_mint = StateWithExtensionsOwned::::unpack(account.data); + assert!(test_mint.is_ok()); + + process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::CloseMint.into(), + &token_pubkey.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await; + assert!(account.is_err()); +} + async fn required_transfer_memos(test_validator: &TestValidator, payer: &Keypair) { let program_id = spl_token_2022::id(); let config = test_config_with_default_signer(test_validator, payer, &program_id);