diff --git a/src/agent/index.ts b/src/agent/index.ts index 4acf694..74aa052 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -50,6 +50,7 @@ import { create_TipLink, listNFTForSale, cancelListing, + closeEmptyTokenAccounts, } from "../tools"; import { @@ -83,24 +84,30 @@ export class SolanaAgentKit { * @deprecated Using openai_api_key directly in constructor is deprecated. * Please use the new constructor with Config object instead: * @example - * const agent = new SolanaAgentKit(privateKey, rpcUrl, { + * const agent = new SolanaAgentKit(privateKey, rpcUrl, { * OPENAI_API_KEY: 'your-key' * }); */ - constructor(private_key: string, rpc_url: string, openai_api_key: string | null); + constructor( + private_key: string, + rpc_url: string, + openai_api_key: string | null, + ); constructor(private_key: string, rpc_url: string, config: Config); constructor( private_key: string, rpc_url: string, configOrKey: Config | string | null, ) { - this.connection = new Connection(rpc_url || "https://api.mainnet-beta.solana.com"); + this.connection = new Connection( + rpc_url || "https://api.mainnet-beta.solana.com", + ); this.wallet = Keypair.fromSecretKey(bs58.decode(private_key)); this.wallet_address = this.wallet.publicKey; // Handle both old and new patterns - if (typeof configOrKey === 'string' || configOrKey === null) { - this.config = { OPENAI_API_KEY: configOrKey || '' }; + if (typeof configOrKey === "string" || configOrKey === null) { + this.config = { OPENAI_API_KEY: configOrKey || "" }; } else { this.config = configOrKey; } @@ -463,8 +470,15 @@ export class SolanaAgentKit { async tensorListNFT(nftMint: PublicKey, price: number): Promise { return listNFTForSale(this, nftMint, price); } - + 1; async tensorCancelListing(nftMint: PublicKey): Promise { return cancelListing(this, nftMint); } + + async closeEmptyTokenAccounts(): Promise<{ + signature: string; + size: number; + }> { + return closeEmptyTokenAccounts(this); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index 4605fd3..4d08e0a 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -1825,6 +1825,34 @@ export class SolanaCancelNFTListingTool extends Tool { } } +export class CloseEmptyTokenAccounts extends Tool { + name = "close_empty_token_accounts"; + description = `Close all empty spl-token accounts and reclaim the rent`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(): Promise { + try { + const { signature, size } = + await this.solanaKit.closeEmptyTokenAccounts(); + + return JSON.stringify({ + status: "success", + message: `${size} accounts closed successfully. ${size === 48 ? "48 accounts can be closed in a single transaction try again to close more accounts" : ""}`, + signature, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaBalanceTool(solanaKit), @@ -1873,5 +1901,6 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaTipLinkTool(solanaKit), new SolanaListNFTForSaleTool(solanaKit), new SolanaCancelNFTListingTool(solanaKit), + new CloseEmptyTokenAccounts(solanaKit), ]; } diff --git a/src/tools/close_empty_token_accounts.ts b/src/tools/close_empty_token_accounts.ts new file mode 100644 index 0000000..a327261 --- /dev/null +++ b/src/tools/close_empty_token_accounts.ts @@ -0,0 +1,92 @@ +import { + PublicKey, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; +import { SolanaAgentKit } from "../agent"; +import { + AccountLayout, + createCloseAccountInstruction, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; + +/** + * Close Empty SPL Token accounts of the agent + * @param agent SolanaAgentKit instance + * @returns transaction signature and total number of accounts closed + */ +export async function closeEmptyTokenAccounts( + agent: SolanaAgentKit, +): Promise<{ signature: string; size: number }> { + try { + const spl_token = await create_close_instruction(agent, TOKEN_PROGRAM_ID); + const token_2022 = await create_close_instruction( + agent, + TOKEN_2022_PROGRAM_ID, + ); + const transaction = new Transaction(); + + spl_token.forEach((instruction) => transaction.add(instruction)); + token_2022.forEach((instruction) => transaction.add(instruction)); + + const signature = await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ]); + const size = spl_token.length + token_2022.length; + return { + signature, + size, + }; + } catch (error) { + throw new Error(`Error closing empty token accounts: ${error}`); + } +} + +/** + * creates the close instuctions of a spl token account + * @param agnet SolanaAgentKit instance + * @param token_program Token Program Id + * @returns close instuction array + */ + +async function create_close_instruction( + agent: SolanaAgentKit, + token_program: PublicKey, +): Promise { + const instructions = []; + + const ata_accounts = await agent.connection.getTokenAccountsByOwner( + agent.wallet_address, + { programId: token_program }, + "confirmed", + ); + + const tokens = ata_accounts.value; + const size = tokens.length > 25 ? 24 : tokens.length; // closing 24 accounts in a single transaction + + const accountExceptions = [ + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC + "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT + ]; + + for (let i = 0; i < size; i++) { + const token_data = AccountLayout.decode(tokens[i].account.data); + if ( + token_data.amount === BigInt(0) && + !accountExceptions.includes(token_data.mint.toString()) + ) { + const closeInstruction = createCloseAccountInstruction( + ata_accounts.value[i].pubkey, + agent.wallet_address, + agent.wallet_address, + [], + token_program, + ); + + instructions.push(closeInstruction); + } + } + + return instructions; +} diff --git a/src/tools/index.ts b/src/tools/index.ts index b9f7542..8273430 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -52,3 +52,4 @@ export * from "./rock_paper_scissor"; export * from "./create_tiplinks"; export * from "./tensor_trade"; +export * from "./close_empty_token_accounts";