UNPKG

close-token-accounts

Version:

Plugin for closing empty token accounts in API City

212 lines (180 loc) 7.02 kB
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;