Skip to content

Commit b57eb29

Browse files
authored
feat: add close empty spl-token accounts transaction (#118)
# Pull Request Description ## Changes Made This PR adds the following changes: - all the close instruction for empty token accounts - this instruction closes the token account and reclaim's the rent ## Implementation Details - createCloseAccountInstruction from @solana/spl-token library to close the spl-token account ## Transaction executed by agent <img width="1467" alt="Screenshot 2025-01-04 at 11 22 20 PM" src="https://github.com/user-attachments/assets/1a48bb54-b76d-49f9-b425-b76b84e924e8" /> Example transaction: [transaction](https://explorer.solana.com/tx/3KmPyiZvJQk8CfBVVaz8nf3c2crb6iqjQVDqNxknnusyb1FTFpXqD8zVSCBAd1X3rUcD8WiG1bdSjFbeHsmcYGXY) ## Prompt Used close my empty token accounts ## Checklist - [x] I have tested these changes locally - [x] I have updated the documentation - [x] I have added a transaction link - [x] I have added the prompt used to test it
2 parents 1073b67 + afcf0ad commit b57eb29

File tree

7 files changed

+994
-616
lines changed

7 files changed

+994
-616
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
![Solana Agent Kit Cover 1 (3)](https://github.com/user-attachments/assets/cfa380f6-79d9-474d-9852-3e1976c6de70)
66

7-
87
![NPM Downloads](https://img.shields.io/npm/dm/solana-agent-kit?style=for-the-badge)
98
![GitHub forks](https://img.shields.io/github/forks/sendaifun/solana-agent-kit?style=for-the-badge)
109
![GitHub License](https://img.shields.io/github/license/sendaifun/solana-agent-kit?style=for-the-badge)
@@ -23,7 +22,6 @@ An open-source toolkit for connecting AI agents to Solana protocols. Now, any ag
2322

2423
Anyone - whether an SF-based AI researcher or a crypto-native builder - can bring their AI agents trained with any model and seamlessly integrate with Solana.
2524

26-
2725
[![Run on Repl.it](https://replit.com/badge/github/sendaifun/solana-agent-kit)](https://replit.com/@sendaifun/Solana-Agent-Kit)
2826
> Replit template created by [Arpit Singh](https://github.com/The-x-35)
2927
@@ -301,6 +299,13 @@ const signature = await agent.closePerpTradeLong({
301299
});
302300
```
303301

302+
### Close Empty Token Accounts
303+
304+
``` typescript
305+
306+
const { signature } = await agent.closeEmptyTokenAccounts();
307+
```
308+
304309
## Examples
305310

306311
### LangGraph Multi-Agent System
@@ -341,7 +346,6 @@ Refer to [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines on how to co
341346
<img src="https://contrib.rocks/image?repo=sendaifun/solana-agent-kit" />
342347
</a>
343348

344-
345349
## Star History
346350

347351
[![Star History Chart](https://api.star-history.com/svg?repos=sendaifun/solana-agent-kit&type=Date)](https://star-history.com/#sendaifun/solana-agent-kit&Date)

pnpm-lock.yaml

Lines changed: 772 additions & 612 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Action } from "../types/action";
2+
import { SolanaAgentKit } from "../agent";
3+
import { z } from "zod";
4+
import { closeEmptyTokenAccounts } from "../tools";
5+
6+
const closeEmptyTokenAccountsAction: Action = {
7+
name: "CLOSE_EMPTY_TOKEN_ACCOUNTS",
8+
similes: [
9+
"close token accounts",
10+
"remove empty accounts",
11+
"clean up token accounts",
12+
"close SPL token accounts",
13+
"clean wallet",
14+
],
15+
description: `Close empty SPL Token accounts associated with your wallet to reclaim rent.
16+
This action will close both regular SPL Token accounts and Token-2022 accounts that have zero balance. `,
17+
examples: [
18+
[
19+
{
20+
input: {},
21+
output: {
22+
status: "success",
23+
signature:
24+
"3KmPyiZvJQk8CfBVVaz8nf3c2crb6iqjQVDqNxknnusyb1FTFpXqD8zVSCBAd1X3rUcD8WiG1bdSjFbeHsmcYGXY",
25+
accountsClosed: 10,
26+
},
27+
explanation: "Closed 10 empty token accounts successfully.",
28+
},
29+
],
30+
[
31+
{
32+
input: {},
33+
output: {
34+
status: "success",
35+
signature: "",
36+
accountsClosed: 0,
37+
},
38+
explanation: "No empty token accounts were found to close.",
39+
},
40+
],
41+
],
42+
schema: z.object({}),
43+
handler: async (agent: SolanaAgentKit) => {
44+
try {
45+
const result = await closeEmptyTokenAccounts(agent);
46+
47+
if (result.size === 0) {
48+
return {
49+
status: "success",
50+
signature: "",
51+
accountsClosed: 0,
52+
message: "No empty token accounts found to close",
53+
};
54+
}
55+
56+
return {
57+
status: "success",
58+
signature: result.signature,
59+
accountsClosed: result.size,
60+
message: `Successfully closed ${result.size} empty token accounts`,
61+
};
62+
} catch (error: any) {
63+
return {
64+
status: "error",
65+
message: `Failed to close empty token accounts: ${error.message}`,
66+
};
67+
}
68+
},
69+
};
70+
71+
export default closeEmptyTokenAccountsAction;

src/agent/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
create_TipLink,
5757
listNFTForSale,
5858
cancelListing,
59+
closeEmptyTokenAccounts,
5960
fetchTokenReportSummary,
6061
fetchTokenDetailedReport,
6162
fetchPythPrice,
@@ -547,6 +548,13 @@ export class SolanaAgentKit {
547548
return cancelListing(this, nftMint);
548549
}
549550

551+
async closeEmptyTokenAccounts(): Promise<{
552+
signature: string;
553+
size: number;
554+
}> {
555+
return closeEmptyTokenAccounts(this);
556+
}
557+
550558
async fetchTokenReportSummary(mint: string): Promise<TokenCheck> {
551559
return fetchTokenReportSummary(mint);
552560
}

src/langchain/index.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2294,7 +2294,7 @@ export class Solana3LandCreateSingle extends Tool {
22942294
...(isMainnet && { isMainnet }),
22952295
};
22962296

2297-
let collectionAccount = inputFormat.collectionAccount;
2297+
const collectionAccount = inputFormat.collectionAccount;
22982298

22992299
const itemName = inputFormat?.itemName;
23002300
const sellerFee = inputFormat?.sellerFee;
@@ -2407,6 +2407,34 @@ export class Solana3LandCreateCollection extends Tool {
24072407
}
24082408
}
24092409

2410+
export class SolanaCloseEmptyTokenAccounts extends Tool {
2411+
name = "close_empty_token_accounts";
2412+
description = `Close all empty spl-token accounts and reclaim the rent`;
2413+
2414+
constructor(private solanaKit: SolanaAgentKit) {
2415+
super();
2416+
}
2417+
2418+
protected async _call(): Promise<string> {
2419+
try {
2420+
const { signature, size } =
2421+
await this.solanaKit.closeEmptyTokenAccounts();
2422+
2423+
return JSON.stringify({
2424+
status: "success",
2425+
message: `${size} accounts closed successfully. ${size === 48 ? "48 accounts can be closed in a single transaction try again to close more accounts" : ""}`,
2426+
signature,
2427+
});
2428+
} catch (error: any) {
2429+
return JSON.stringify({
2430+
status: "error",
2431+
message: error.message,
2432+
code: error.code || "UNKNOWN_ERROR",
2433+
});
2434+
}
2435+
}
2436+
}
2437+
24102438
export function createSolanaTools(solanaKit: SolanaAgentKit) {
24112439
return [
24122440
new SolanaBalanceTool(solanaKit),
@@ -2457,6 +2485,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
24572485
new SolanaTipLinkTool(solanaKit),
24582486
new SolanaListNFTForSaleTool(solanaKit),
24592487
new SolanaCancelNFTListingTool(solanaKit),
2488+
new SolanaCloseEmptyTokenAccounts(solanaKit),
24602489
new SolanaFetchTokenReportSummaryTool(solanaKit),
24612490
new SolanaFetchTokenDetailedReportTool(solanaKit),
24622491
new Solana3LandCreateSingle(solanaKit),
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
PublicKey,
3+
Transaction,
4+
TransactionInstruction,
5+
} from "@solana/web3.js";
6+
import { SolanaAgentKit } from "../agent";
7+
import {
8+
AccountLayout,
9+
createCloseAccountInstruction,
10+
TOKEN_2022_PROGRAM_ID,
11+
TOKEN_PROGRAM_ID,
12+
} from "@solana/spl-token";
13+
14+
/**
15+
* Close Empty SPL Token accounts of the agent
16+
* @param agent SolanaAgentKit instance
17+
* @returns transaction signature and total number of accounts closed
18+
*/
19+
export async function closeEmptyTokenAccounts(
20+
agent: SolanaAgentKit,
21+
): Promise<{ signature: string; size: number }> {
22+
try {
23+
const spl_token = await create_close_instruction(agent, TOKEN_PROGRAM_ID);
24+
const token_2022 = await create_close_instruction(
25+
agent,
26+
TOKEN_2022_PROGRAM_ID,
27+
);
28+
const transaction = new Transaction();
29+
30+
const MAX_INSTRUCTIONS = 40; // 40 instructions can be processed in a single transaction without failing
31+
32+
spl_token
33+
.slice(0, Math.min(MAX_INSTRUCTIONS, spl_token.length))
34+
.forEach((instruction) => transaction.add(instruction));
35+
36+
token_2022
37+
.slice(0, Math.max(0, MAX_INSTRUCTIONS - spl_token.length))
38+
.forEach((instruction) => transaction.add(instruction));
39+
40+
const size = spl_token.length + token_2022.length;
41+
42+
if (size === 0) {
43+
return {
44+
signature: "",
45+
size: 0,
46+
};
47+
}
48+
49+
const signature = await agent.connection.sendTransaction(transaction, [
50+
agent.wallet,
51+
]);
52+
53+
return { signature, size };
54+
} catch (error) {
55+
throw new Error(`Error closing empty token accounts: ${error}`);
56+
}
57+
}
58+
59+
/**
60+
* creates the close instuctions of a spl token account
61+
* @param agnet SolanaAgentKit instance
62+
* @param token_program Token Program Id
63+
* @returns close instuction array
64+
*/
65+
66+
async function create_close_instruction(
67+
agent: SolanaAgentKit,
68+
token_program: PublicKey,
69+
): Promise<TransactionInstruction[]> {
70+
const instructions = [];
71+
72+
const ata_accounts = await agent.connection.getTokenAccountsByOwner(
73+
agent.wallet_address,
74+
{ programId: token_program },
75+
"confirmed",
76+
);
77+
78+
const tokens = ata_accounts.value;
79+
80+
const accountExceptions = [
81+
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
82+
];
83+
84+
for (let i = 0; i < tokens.length; i++) {
85+
const token_data = AccountLayout.decode(tokens[i].account.data);
86+
if (
87+
token_data.amount === BigInt(0) &&
88+
!accountExceptions.includes(token_data.mint.toString())
89+
) {
90+
const closeInstruction = createCloseAccountInstruction(
91+
ata_accounts.value[i].pubkey,
92+
agent.wallet_address,
93+
agent.wallet_address,
94+
[],
95+
token_program,
96+
);
97+
98+
instructions.push(closeInstruction);
99+
}
100+
}
101+
102+
return instructions;
103+
}

src/tools/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export * from "./send_compressed_airdrop";
4141
export * from "./stake_with_jup";
4242
export * from "./stake_with_solayer";
4343
export * from "./tensor_trade";
44+
45+
export * from "./close_empty_token_accounts";
46+
4447
export * from "./trade";
4548
export * from "./transfer";
4649
export * from "./flash_open_trade";

0 commit comments

Comments
 (0)