feat: add close accounts and reclaim rent

This commit is contained in:
thrishank
2025-01-02 16:58:56 +05:30
parent 42b64f2264
commit c08ae5bb04
4 changed files with 142 additions and 6 deletions

View File

@@ -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<string> {
return listNFTForSale(this, nftMint, price);
}
1;
async tensorCancelListing(nftMint: PublicKey): Promise<string> {
return cancelListing(this, nftMint);
}
async closeEmptyTokenAccounts(): Promise<{
signature: string;
size: number;
}> {
return closeEmptyTokenAccounts(this);
}
}

View File

@@ -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<string> {
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),
];
}

View File

@@ -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<TransactionInstruction[]> {
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;
}

View File

@@ -52,3 +52,4 @@ export * from "./rock_paper_scissor";
export * from "./create_tiplinks";
export * from "./tensor_trade";
export * from "./close_empty_token_accounts";