12

I have a bunch of 0 balance SPL token accounts. How do I close these accounts to redeem the rent lamports?

3
  • Are you looking for a programmatic answer? Or an answer for your particular wallet?
    – Ellie High
    Jul 13, 2022 at 3:55
  • A programmatic one
    – Son Dang
    Jul 13, 2022 at 3:57
  • An easy way is to script it using Python or Ruby. A high level overview: spl-token accounts gives you a list of token accounts If the account is empty, you can close it with spl-token close <address>. Closing empty token accounts is safe and gives you back the rent. If they are not empty, look up the account address with spl-token info <token address> and then issue a burn command to the token account this is displayed to empty it out. Then close the account. If you script it this way, then be careful you don't burn tokens you actually want to keep.
    – Sven
    Jul 13, 2022 at 8:30

5 Answers 5

17

The other answers are Rust implementations. Here's a Typescript one for browsers/Node applications using @solana/web3.js and @solana/spl-token (v0.2.0):

NOTE: Solana enforces transaction size limits, so you'll need to batch the account closures. dApps nowadays usually do 10 at a time.

// Split an array into chunks of length `chunkSize`
const chunks = <T>(array: T[], chunkSize = 10): T[][] => {
  let res: T[][] = [];
  for (let currentChunk = 0; currentChunk < array.length; currentChunk += chunkSize) {
    res.push(array.slice(currentChunk, currentChunk + chunkSize));
  }
  return res;
};

// Get all token accounts of `wallet`
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(wallet.publicKey, { programId: TOKEN_PROGRAM_ID });

// You can only close accounts that have a 0 token balance. Be sure to filter those out!
const filteredAccounts = tokenAccounts.value.filter(account => account.account.data.parsed.info.tokenAmount.uiAmount === 0);

const transactions: Transaction[] = [];

const recentBlockhash = (await connection.getLatestBlockhash()).blockhash;

chunks(filteredAccounts).forEach((chunk) => {
  // New empty transaction
  const txn = new Transaction();
  txn.feePayer = wallet.publicKey;
  txn.recentBlockhash = recentBlockhash;
  for (const account of chunk) {
    // Add a `closeAccount` instruction for every token account in the chunk
    txn.add(createCloseAccountInstruction(account.pubkey, wallet.publicKey, wallet.publicKey));
  }
  tranasctions.push(txn);
});

Afterward you can either iterate through transactions and send each one, but I like to use the following so that users only have to click Approve once:

const signedTransactions = await wallet.signAllTransactions(transactions);

const serializedTransactions = signedTransactions.map(t => t.serialize());

serializedTransactions.forEach(async t => await connection.sendRawTransaction(t));

Note that you should be using a custom RPC if going the above route to avoid rate-limits.

The arguments of createCloseAccountInstruction:

createCloseAccountInstruction(TOKEN_ACCOUNT_ADDRESS, SOL_DESTINATION_ADDRESS, OWNER_OF_TOKEN_ACCOUNT)

so usually you'll just put the wallet's public key in both of those last two arguments to return the SOL to a user in a dApp scenario.

4
  • 2
    One note, if you have a lot of empty accounts, you can get out of transaction size limits, so you may want to split the loop for N batches / txs. If i recall correctly, you cannot close more than 27 accounts within 1 TX, above is out of packet size limits.
    – rxdx
    Jul 13, 2022 at 11:19
  • 1
    Thank you! This is perfect for closing multiple token accounts. It would be great if you add an extra note on the transaction limit as rxdx suggested.
    – Son Dang
    Jul 13, 2022 at 14:31
  • How can I close the token account without the code?
    – Nam Lee
    Jan 20 at 20:22
  • We made a tool for this over at Step Finance app.step.finance/en/account-tools
    – Quellen
    Jan 21 at 21:19
5

I get the feeling this isn't what you mean by programmatic but I'll leave this here anyway in case someone else comes here looking for this.


If you're asking how to do this inside an on-chain Anchor program, then you'd usually add a #[account(mut, ..., close = <target_account>)] constraint.

But this won't work for a token account since your program doesn't own that account state. The token program does.

Here is how I've done it, but again, this might not be the best way.

  1. Make sure your account has no balance in it.

  2. Make sure you're not vulnerable to re-init attack vectors in the PDA (if it is a program owned token account). Change or increment the PDA path if it is owned by the program.

  3. Make a cross program invoke to close the account.

     // We can't close the account though because anchor doesn't know how to sign the CPI for that.
     // Manually close the token account.
    
     let cpi_accounts = anchor_spl::token::CloseAccount {
         account: ctx.accounts.vault.to_account_info().clone(),
         destination: ctx.accounts.authority.to_account_info().clone(),
         authority: ctx.accounts.vault.to_account_info().clone(),
     };
    
     let cpi_program = ctx.accounts.token_program.to_account_info().clone();
     let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
     token::close_account(cpi_ctx)?;
    
4

If the accounts are "ancillary" token accounts, meaning not associated token accounts, you can use the spl-token CLI's gc command, which cleans up extra accounts.

You can simply run:

$ spl-token gc --owner <YOUR_OWNER_KEYPAIR>

More information in the SPL Token docs: https://spl.solana.com/token#garbage-collecting-ancillary-token-accounts

3

SPL has a closeAccount instruction. Here's the anchor_spl docs. I don't think there's any way to mass-close accounts, you will just have to close them one by one (or several per transaction).

1
  • 1
    Stack exchanges tend to copy paste the actual code as well as linking just because links can go dead pretty easy.
    – Henry E
    Jul 13, 2022 at 7:26
1

no code solution to close token accounts https://www.draffle.io/

and one to burn spam nfts https://www.sol-incinerator.com/#/

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.