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

Commit e6739b5

Browse files
committed
token-cli: convert gc to client
add new client function empty_and_close_auxiliary_account which does what it says also remove sign-only mode from command_close to permit client close behave more sensibly
1 parent 186303a commit e6739b5

File tree

2 files changed

+232
-118
lines changed

2 files changed

+232
-118
lines changed

token/cli/src/main.rs

Lines changed: 141 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,28 +1403,21 @@ async fn command_close(
14031403
recipient: Pubkey,
14041404
bulk_signers: BulkSigners,
14051405
) -> CommandResult {
1406-
let mint_pubkey = if !config.sign_only {
1407-
let source_account = config.get_account_checked(&account).await?;
1406+
let source_account = config.get_account_checked(&account).await?;
14081407

1409-
let source_state = StateWithExtensionsOwned::<Account>::unpack(source_account.data)
1410-
.map_err(|_| format!("Could not deserialize token account {}", account))?;
1411-
let source_amount = source_state.base.amount;
1408+
let source_state = StateWithExtensionsOwned::<Account>::unpack(source_account.data)
1409+
.map_err(|_| format!("Could not deserialize token account {}", account))?;
1410+
let source_amount = source_state.base.amount;
14121411

1413-
if !source_state.base.is_native() && source_amount > 0 {
1414-
return Err(format!(
1415-
"Account {} still has {} tokens; empty the account in order to close it.",
1416-
account, source_amount,
1417-
)
1418-
.into());
1419-
}
1420-
1421-
source_state.base.mint
1422-
} else {
1423-
// default is safe here because close doesnt use it
1424-
Pubkey::default()
1425-
};
1412+
if !source_state.base.is_native() && source_amount > 0 {
1413+
return Err(format!(
1414+
"Account {} still has {} tokens; empty the account in order to close it.",
1415+
account, source_amount,
1416+
)
1417+
.into());
1418+
}
14261419

1427-
let token = token_client_from_config(config, &mint_pubkey);
1420+
let token = token_client_from_config(config, &source_state.base.mint);
14281421
let res = token
14291422
.close_account(&account, &recipient, &close_authority, &bulk_signers)
14301423
.await?;
@@ -1649,15 +1642,6 @@ async fn command_gc(
16491642
return Ok("".to_string());
16501643
}
16511644

1652-
let minimum_balance_for_rent_exemption = if !config.sign_only {
1653-
config
1654-
.program_client
1655-
.get_minimum_balance_for_rent_exemption(Account::LEN)
1656-
.await?
1657-
} else {
1658-
0
1659-
};
1660-
16611645
let mut accounts_by_token = HashMap::new();
16621646

16631647
for keyed_account in accounts {
@@ -1702,107 +1686,79 @@ async fn command_gc(
17021686
}
17031687
}
17041688

1705-
let mut instructions = vec![];
1706-
let mut lamports_needed = 0;
1689+
let mut results = vec![];
1690+
for (token_pubkey, accounts) in accounts_by_token.into_iter() {
1691+
println_display(config, format!("Processing token: {}", token_pubkey));
17071692

1708-
for (token, accounts) in accounts_by_token.into_iter() {
1709-
println_display(config, format!("Processing token: {}", token));
1710-
let associated_token_account =
1711-
get_associated_token_address_with_program_id(&owner, &token, &config.program_id);
1693+
let token = token_client_from_config(config, &token_pubkey);
1694+
let associated_token_account = token.get_associated_token_address(&owner);
17121695
let total_balance: u64 = accounts.values().map(|account| account.0).sum();
17131696

1714-
if total_balance > 0 && !accounts.contains_key(&associated_token_account) {
1715-
// Create the associated token account
1716-
instructions.push(vec![create_associated_token_account(
1717-
&config.fee_payer.pubkey(),
1718-
&owner,
1719-
&token,
1720-
&config.program_id,
1721-
)]);
1722-
lamports_needed += minimum_balance_for_rent_exemption;
1723-
}
1724-
17251697
for (address, (amount, decimals, frozen, close_authority)) in accounts {
1726-
match (
1727-
address == associated_token_account,
1728-
close_empty_associated_accounts,
1729-
total_balance > 0,
1730-
) {
1731-
(true, _, true) => continue, // don't ever close associated token account with amount
1732-
(true, false, _) => continue, // don't close associated token account if close_empty_associated_accounts isn't set
1733-
(true, true, false) => println_display(
1734-
config,
1735-
format!("Closing Account {}", associated_token_account),
1736-
),
1737-
_ => {}
1698+
let is_associated = address == associated_token_account;
1699+
1700+
// only close the associated account if --close-empty-associated-accounts is provided
1701+
if is_associated && !close_empty_associated_accounts {
1702+
continue;
17381703
}
17391704

1740-
if frozen {
1741-
// leave frozen accounts alone
1705+
// never close the associated account if *any* account carries a balance
1706+
if is_associated && total_balance > 0 {
17421707
continue;
17431708
}
17441709

1745-
let mut account_instructions = vec![];
1710+
// dont attempt to close frozen accounts
1711+
if frozen {
1712+
continue;
1713+
}
17461714

17471715
// Sanity check!
17481716
// we shouldn't ever be here, but if we are here, abort!
1749-
assert!(amount == 0 || address != associated_token_account);
1750-
1751-
if amount > 0 {
1752-
// Transfer the account balance into the associated token account
1753-
account_instructions.push(transfer_checked(
1754-
&config.program_id,
1755-
&address,
1756-
&token,
1757-
&associated_token_account,
1758-
&owner,
1759-
&config.multisigner_pubkeys,
1760-
amount,
1761-
decimals,
1762-
)?);
1763-
}
1764-
// Close the account if config.owner is able to
1765-
if close_authority == owner {
1766-
account_instructions.push(close_account(
1767-
&config.program_id,
1768-
&address,
1769-
&owner,
1770-
&owner,
1771-
&config.multisigner_pubkeys,
1772-
)?);
1717+
if is_associated && amount > 0 {
1718+
panic!("gc should NEVER attempt to close a nonempty ata");
17731719
}
17741720

1775-
if !account_instructions.is_empty() {
1776-
instructions.push(account_instructions);
1721+
if close_authority == owner {
1722+
let res = if is_associated || amount == 0 {
1723+
token
1724+
.close_account(&address, &owner, &owner, &bulk_signers)
1725+
.await
1726+
} else {
1727+
token
1728+
.empty_and_close_auxiliary_account(
1729+
&address,
1730+
&owner,
1731+
&owner,
1732+
decimals,
1733+
&bulk_signers,
1734+
)
1735+
.await
1736+
}?;
1737+
1738+
let tx_return = finish_tx(config, &res, false).await?;
1739+
1740+
results.push(match tx_return {
1741+
TransactionReturnData::CliSignature(signature) => {
1742+
config.output_format.formatted_string(&signature)
1743+
}
1744+
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
1745+
config.output_format.formatted_string(&sign_only_data)
1746+
}
1747+
});
1748+
} else {
1749+
println_display(
1750+
config,
1751+
format!(
1752+
"Note: skipping {} due to separate close authority {}; \
1753+
revoke authority and rerun gc, or rerun gc with --owner",
1754+
address, close_authority
1755+
),
1756+
);
17771757
}
17781758
}
17791759
}
17801760

1781-
let cli_signer_info = CliSignerInfo {
1782-
signers: bulk_signers,
1783-
};
1784-
1785-
let mut result = String::from("");
1786-
for tx_instructions in instructions {
1787-
let tx_return = handle_tx(
1788-
&cli_signer_info,
1789-
config,
1790-
false,
1791-
lamports_needed,
1792-
tx_instructions,
1793-
)
1794-
.await?;
1795-
result += &match tx_return {
1796-
TransactionReturnData::CliSignature(signature) => {
1797-
config.output_format.formatted_string(&signature)
1798-
}
1799-
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
1800-
config.output_format.formatted_string(&sign_only_data)
1801-
}
1802-
};
1803-
result += "\n";
1804-
}
1805-
Ok(result)
1761+
Ok(results.join(""))
18061762
}
18071763

18081764
async fn command_sync_native(config: &Config<'_>, native_account_address: Pubkey) -> CommandResult {
@@ -2630,7 +2586,8 @@ fn app<'a, 'b>(
26302586
.takes_value(true)
26312587
.index(1)
26322588
.required_unless("address")
2633-
.help("Token to close. To close a specific account, use the `--address` parameter instead"),
2589+
.help("Token of the associated account to close. \
2590+
To close a specific account, use the `--address` parameter instead"),
26342591
)
26352592
.arg(owner_address_arg())
26362593
.arg(
@@ -2667,7 +2624,6 @@ fn app<'a, 'b>(
26672624
)
26682625
.arg(multisig_signer_arg())
26692626
.nonce_args(true)
2670-
.offline_args(),
26712627
)
26722628
.subcommand(
26732629
SubCommand::with_name(CommandName::CloseMint.into())
@@ -4419,6 +4375,78 @@ mod tests {
44194375
.unwrap();
44204376
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
44214377
assert_eq!(value["accounts"].as_array().unwrap().len(), 1);
4378+
4379+
config.output_format = OutputFormat::Display;
4380+
4381+
// test implicit transfer
4382+
let token = create_token(&config, &payer).await;
4383+
let ata = create_associated_account(&config, &payer, token).await;
4384+
let aux = create_auxiliary_account(&config, &payer, token).await;
4385+
mint_tokens(&config, &payer, token, 1.0, ata).await;
4386+
mint_tokens(&config, &payer, token, 1.0, aux).await;
4387+
4388+
process_test_command(&config, &payer, &["spl-token", CommandName::Gc.into()])
4389+
.await
4390+
.unwrap();
4391+
4392+
let ui_ata = config
4393+
.rpc_client
4394+
.get_token_account(&ata)
4395+
.await
4396+
.unwrap()
4397+
.unwrap();
4398+
4399+
// aux is gone and its tokens are in ata
4400+
assert_eq!(ui_ata.token_amount.amount, "2");
4401+
config.rpc_client.get_account(&aux).await.unwrap_err();
4402+
4403+
// test ata closure
4404+
let token = create_token(&config, &payer).await;
4405+
let ata = create_associated_account(&config, &payer, token).await;
4406+
4407+
process_test_command(
4408+
&config,
4409+
&payer,
4410+
&[
4411+
"spl-token",
4412+
CommandName::Gc.into(),
4413+
"--close-empty-associated-accounts",
4414+
],
4415+
)
4416+
.await
4417+
.unwrap();
4418+
4419+
// ata is gone
4420+
config.rpc_client.get_account(&ata).await.unwrap_err();
4421+
4422+
// test a tricky corner case of both
4423+
let token = create_token(&config, &payer).await;
4424+
let ata = create_associated_account(&config, &payer, token).await;
4425+
let aux = create_auxiliary_account(&config, &payer, token).await;
4426+
mint_tokens(&config, &payer, token, 1.0, aux).await;
4427+
4428+
process_test_command(
4429+
&config,
4430+
&payer,
4431+
&[
4432+
"spl-token",
4433+
CommandName::Gc.into(),
4434+
"--close-empty-associated-accounts",
4435+
],
4436+
)
4437+
.await
4438+
.unwrap();
4439+
4440+
let ui_ata = config
4441+
.rpc_client
4442+
.get_token_account(&ata)
4443+
.await
4444+
.unwrap()
4445+
.unwrap();
4446+
4447+
// aux is gone and its tokens are in ata, and ata has not been closed
4448+
assert_eq!(ui_ata.token_amount.amount, "1");
4449+
config.rpc_client.get_account(&aux).await.unwrap_err();
44224450
}
44234451
}
44244452

0 commit comments

Comments
 (0)