close-token-accounts
Version:
Plugin for closing empty token accounts in API City
212 lines (180 loc) • 7.02 kB
text/typescript
import { Connection, PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID, getAccount, createCloseAccountInstruction } from '@solana/spl-token';
import { BasePlugin, PluginContext, PluginManifest } from 'api-city-sdk';
export class CloseTokenAccountsPlugin extends BasePlugin {
protected context!: PluginContext;
private connection: Connection;
private readonly BASE_COMMAND = 'empty';
constructor() {
super({
name: "Empty Token Accounts Manager",
description: "Manage and close empty Solana token accounts to reclaim rent",
version: "1.1.2",
author: "API City",
permissions: [],
entryPoints: {
main: "empty"
}
});
// Initialize connection
this.connection = new Connection(
process.env.NEXT_PUBLIC_SOLANA_RPC || 'https://api.mainnet-beta.solana.com',
{ commitment: 'confirmed' }
);
}
async initialize(context: PluginContext): Promise<void> {
this.context = context;
}
render(): React.ReactNode {
return null;
}
private async getEmptyTokenAccounts(publicKey: PublicKey): Promise<{ pubkey: PublicKey; account: any }[]> {
const accounts = await this.connection.getParsedTokenAccountsByOwner(publicKey, {
programId: TOKEN_PROGRAM_ID,
});
return accounts.value.filter(({ account }) => {
const info = account.data.parsed.info;
return Number(info.tokenAmount.amount) === 0;
});
}
private async closeTokenAccounts(publicKey: PublicKey, accountsToClose: PublicKey[]): Promise<void> {
if (accountsToClose.length === 0) {
this.emit('message', { content: "No accounts to close" });
return;
}
const latestBlockhash = await this.connection.getLatestBlockhash();
const transaction = new Transaction(latestBlockhash);
transaction.feePayer = publicKey;
for (const account of accountsToClose) {
transaction.add(
createCloseAccountInstruction(
account,
publicKey,
publicKey,
[],
TOKEN_PROGRAM_ID
)
);
}
if (!this.context.wallet.signTransaction) {
throw new Error("Wallet does not support transaction signing");
}
this.emit('message', { content: "Sending transaction for approval..." });
const signedTransaction = await this.context.wallet.signTransaction(transaction);
const signature = await this.connection.sendRawTransaction(
signedTransaction.serialize(),
{ preflightCommitment: 'confirmed', maxRetries: 10 }
);
await this.connection.confirmTransaction({
signature,
...latestBlockhash
});
this.emit('message', {
content: `Successfully closed ${accountsToClose.length} empty token accounts.\nTransaction: https://solscan.io/tx/${signature}`
});
}
async handleCommand(command: string, args: string[]): Promise<void> {
if (!command.startsWith("empty")) {
this.emit('message', { content: "Unknown command" });
return;
}
try {
const publicKey = this.context.wallet.publicKey;
if (!publicKey) {
this.emit('message', { content: "Wallet not connected" });
return;
}
const subCommand = args[0] || 'help';
switch (subCommand) {
case 'help':
this.emit('message', { content: `Available commands:
- /empty help: Show this help message
- /empty list: Show list of empty token accounts
- /empty all: Close all empty token accounts
- /empty [address1], [address2], ...: Close specific empty token accounts` });
break;
case 'list':
const emptyAccounts = await this.getEmptyTokenAccounts(publicKey);
if (emptyAccounts.length === 0) {
this.emit('message', { content: "No empty token accounts found" });
} else {
const accountList = emptyAccounts.map(({ pubkey }, index) =>
`${index + 1}. ${pubkey.toString()}`
).join('\n');
this.emit('message', {
content: `Found ${emptyAccounts.length} empty token accounts:\n${accountList}`
});
}
break;
case 'all':
const allEmptyAccounts = await this.getEmptyTokenAccounts(publicKey);
await this.closeTokenAccounts(
publicKey,
allEmptyAccounts.map(({ pubkey }) => pubkey)
);
break;
default:
// Handle specific addresses
if (args.length === 0) {
this.emit('message', { content: "Please provide at least one address to close" });
return;
}
const addresses = args.join(' ').split(',').map(addr => addr.trim());
const validAddresses: PublicKey[] = [];
for (const addr of addresses) {
try {
validAddresses.push(new PublicKey(addr));
} catch (err) {
this.emit('message', { content: `Invalid address: ${addr}` });
return;
}
}
// Verify these are actually empty token accounts owned by the user
const userEmptyAccounts = await this.getEmptyTokenAccounts(publicKey);
const userEmptyAddresses = new Set(userEmptyAccounts.map(({ pubkey }) => pubkey.toString()));
const validClosingAddresses = validAddresses.filter(addr =>
userEmptyAddresses.has(addr.toString())
);
if (validClosingAddresses.length === 0) {
this.emit('message', { content: "None of the provided addresses are valid empty token accounts owned by you" });
return;
}
await this.closeTokenAccounts(publicKey, validClosingAddresses);
break;
}
} catch (err: any) {
console.error("Error in command execution:", err);
this.emit('message', { content: `Error: ${err.message}` });
}
}
getCommands(): { command: string; description: string; usage: string; }[] {
return [
{
command: this.BASE_COMMAND,
description: "Manage and close empty token accounts",
usage: `${this.BASE_COMMAND} <help|list|all|addresses>`
},
{
command: `${this.BASE_COMMAND} help`,
description: "Show available commands for managing empty token accounts",
usage: `${this.BASE_COMMAND} help`
},
{
command: `${this.BASE_COMMAND} list`,
description: "List all empty token accounts that can be closed",
usage: `${this.BASE_COMMAND} list`
},
{
command: `${this.BASE_COMMAND} all`,
description: "Close all empty token accounts to reclaim rent",
usage: `${this.BASE_COMMAND} all`
},
{
command: `${this.BASE_COMMAND} [addresses]`,
description: "Close specific empty token accounts (comma-separated addresses)",
usage: `${this.BASE_COMMAND} address1, address2, address3`
}
];
}
}
export default CloseTokenAccountsPlugin;