UNPKG

solana-token-extension-boost

Version:

SDK for Solana Token Extensions with wallet adapter support

269 lines (268 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CloseAccountExtension = void 0; const web3_js_1 = require("@solana/web3.js"); const spl_token_1 = require("@solana/spl-token"); const token_1 = require("../../core/token"); class CloseAccountExtension extends token_1.Token { constructor(connection, mint, decimals) { super(connection, mint, decimals); } /** * Find token accounts with zero balance for an address * * @param owner - Address of the token accounts owner * @param excludeTokens - List of token mints to exclude (optional) * @returns List of token accounts with zero balance */ async findZeroBalanceAccounts(owner, excludeTokens) { try { // Get all token accounts for the owner const tokenAccounts = await this.connection.getTokenAccountsByOwner(owner, { programId: spl_token_1.TOKEN_2022_PROGRAM_ID }); const zeroBalanceAccounts = []; const excludeTokenSet = new Set(excludeTokens?.map(token => token.toBase58())); // Check each account for (const { pubkey, account } of tokenAccounts.value) { const accountInfo = spl_token_1.AccountLayout.decode(account.data); const mintAddress = new web3_js_1.PublicKey(accountInfo.mint); // Skip native SOL and tokens in the exclude list if (mintAddress.equals(spl_token_1.NATIVE_MINT) || (excludeTokenSet && excludeTokenSet.has(mintAddress.toBase58()))) { continue; } // Check if balance = 0 if (accountInfo.amount === BigInt(0)) { zeroBalanceAccounts.push({ address: pubkey, mint: mintAddress }); } } return zeroBalanceAccounts; } catch (error) { console.error("Error finding zero balance accounts:", error); return []; } } /** * Create instruction to close a token account * * @param account - Address of the token account to close * @param owner - Address of the account owner * @param destination - Address to receive SOL from closing the account (defaults to owner) * @returns Transaction instruction to close the account */ createCloseAccountInstruction(account, owner, destination) { return (0, spl_token_1.createCloseAccountInstruction)(account, destination || owner, owner, [], spl_token_1.TOKEN_2022_PROGRAM_ID); } /** * Create instructions to close multiple token accounts at once * * @param accounts - List of addresses of token accounts to close * @param owner - Address of the account owner * @param destination - Address to receive SOL from closing the account (defaults to owner) * @param adminFeeReceiver - Admin fee receiver address (if applicable) * @param adminFeeBasisPoints - Admin fee in basis points (1% = 100) * @returns List of transaction instructions to close accounts */ createBulkCloseAccountInstructions(accounts, owner, options) { const instructions = []; const destination = options?.destination || owner; // Add instructions to close each account for (const account of accounts) { instructions.push(this.createCloseAccountInstruction(account, owner, destination)); } // Add instruction to transfer admin fee if specified if (options?.adminFeeReceiver && options.adminFeeBasisPoints && options.adminFeeBasisPoints > 0) { // Will add fee transfer logic in complete version // Fee will be calculated based on total rent reclaimed } return instructions; } /** * Verify if a token account can be closed (zero balance) * * @param account - Address of the token account to check * @returns Result of checking if account can be closed */ async canCloseAccount(account) { try { const accountInfo = await (0, spl_token_1.getAccount)(this.connection, account, "confirmed", spl_token_1.TOKEN_2022_PROGRAM_ID); if (accountInfo.isFrozen) { return { closeable: false, account: accountInfo, reason: "Account is frozen" }; } if (accountInfo.amount > BigInt(0)) { return { closeable: false, account: accountInfo, reason: "Account has balance greater than 0" }; } return { closeable: true, account: accountInfo }; } catch (error) { return { closeable: false, account: null, reason: `Error checking account: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Get estimated SOL to be reclaimed when closing accounts * * @param accounts - List of token accounts to close * @param adminFeeBasisPoints - Admin fee in basis points (1% = 100) * @returns Estimated SOL to be reclaimed (in lamports) */ async estimateReclaimableRent(accounts, adminFeeBasisPoints = 0) { let totalRent = 0; const accountsInfo = []; for (const account of accounts) { try { const accountInfo = await this.connection.getAccountInfo(account); const canClose = await this.canCloseAccount(account); if (accountInfo) { accountsInfo.push({ address: account, lamports: accountInfo.lamports, closeable: canClose.closeable, reason: canClose.reason }); if (canClose.closeable) { totalRent += accountInfo.lamports; } } else { accountsInfo.push({ address: account, lamports: 0, closeable: false, reason: "Account does not exist" }); } } catch (error) { accountsInfo.push({ address: account, lamports: 0, closeable: false, reason: `Error: ${error instanceof Error ? error.message : String(error)}` }); } } const adminRent = Math.floor(totalRent * (adminFeeBasisPoints / 10000)); const userRent = totalRent - adminRent; return { totalRent, userRent, adminRent, accountsInfo }; } /** * Close token accounts and reclaim SOL * * @param owner - Signer who owns the accounts * @param accounts - List of token accounts to close * @param options - Additional options * @returns Information about the transaction */ async closeAccounts(owner, accounts, options) { // Check each account if it can be closed const closeableAccounts = []; const rentEstimate = await this.estimateReclaimableRent(accounts, options?.adminFeeBasisPoints || 0); for (const accountInfo of rentEstimate.accountsInfo) { if (accountInfo.closeable) { closeableAccounts.push(accountInfo.address); } } if (closeableAccounts.length === 0) { throw new Error("No accounts can be closed"); } const destination = options?.destination || owner.publicKey; const instructions = this.createBulkCloseAccountInstructions(closeableAccounts, owner.publicKey, options); // Add instruction to transfer admin fee if specified if (options?.adminFeeReceiver && options.adminFeeBasisPoints && options.adminFeeBasisPoints > 0) { const adminFee = Math.floor(rentEstimate.totalRent * (options.adminFeeBasisPoints / 10000)); if (adminFee > 0) { instructions.push(web3_js_1.SystemProgram.transfer({ fromPubkey: destination, toPubkey: options.adminFeeReceiver, lamports: adminFee })); } } const transaction = new web3_js_1.Transaction().add(...instructions); const signature = await (0, web3_js_1.sendAndConfirmTransaction)(this.connection, transaction, [owner], { commitment: 'confirmed' }); return { signature, closedAccounts: closeableAccounts, totalRentReclaimed: rentEstimate.userRent }; } /** * Prepare transaction to close accounts (supports wallet-adapter) * * @param accounts - List of addresses of token accounts to close * @param owner - Address of the account owner * @param options - Additional options * @returns Transaction prepared for signing and sending through wallet-adapter */ async prepareCloseAccountsTransaction(accounts, owner, options) { // Check each account if it can be closed const closeableAccounts = []; const rentEstimate = await this.estimateReclaimableRent(accounts, options?.adminFeeBasisPoints || 0); for (const accountInfo of rentEstimate.accountsInfo) { if (accountInfo.closeable) { closeableAccounts.push(accountInfo.address); } } if (closeableAccounts.length === 0) { throw new Error("No accounts can be closed"); } const destination = options?.destination || owner; const instructions = this.createBulkCloseAccountInstructions(closeableAccounts, owner, options); // Add instruction to transfer admin fee if specified if (options?.adminFeeReceiver && options.adminFeeBasisPoints && options.adminFeeBasisPoints > 0) { const adminFee = Math.floor(rentEstimate.totalRent * (options.adminFeeBasisPoints / 10000)); if (adminFee > 0) { instructions.push(web3_js_1.SystemProgram.transfer({ fromPubkey: destination, toPubkey: options.adminFeeReceiver, lamports: adminFee })); } } const transaction = new web3_js_1.Transaction().add(...instructions); transaction.feePayer = owner; return { transaction, closeableAccounts, estimatedRent: { totalRent: rentEstimate.totalRent, userRent: rentEstimate.userRent, adminRent: rentEstimate.adminRent } }; } /** * Create instructions to burn tokens * * @param account - Address of the token account to burn from * @param owner - Address of the account owner * @param amount - Amount of tokens to burn * @param decimals - Decimals of the token * @returns Object containing transaction instructions to burn tokens */ createBurnInstructions(account, owner, amount, decimals) { // Call method from parent class return super.createBurnInstructions(account, owner, amount, decimals); } } exports.CloseAccountExtension = CloseAccountExtension;