solana-token-extension-boost
Version:
SDK for Solana Token Extensions with wallet adapter support
269 lines (268 loc) • 11.6 kB
JavaScript
;
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;