Skip to content

Commit

Permalink
feat: add close empty spl-token accounts transaction (#118)
Browse files Browse the repository at this point in the history
# 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
  • Loading branch information
thearyanag authored Jan 7, 2025
2 parents 1073b67 + afcf0ad commit b57eb29
Show file tree
Hide file tree
Showing 7 changed files with 994 additions and 616 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

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


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

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.


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

### Close Empty Token Accounts

``` typescript

const { signature } = await agent.closeEmptyTokenAccounts();
```

## Examples

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


## Star History

[![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)
Expand Down
1,384 changes: 772 additions & 612 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions src/actions/closeEmptyTokenAccounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
import { closeEmptyTokenAccounts } from "../tools";

const closeEmptyTokenAccountsAction: Action = {
name: "CLOSE_EMPTY_TOKEN_ACCOUNTS",
similes: [
"close token accounts",
"remove empty accounts",
"clean up token accounts",
"close SPL token accounts",
"clean wallet",
],
description: `Close empty SPL Token accounts associated with your wallet to reclaim rent.
This action will close both regular SPL Token accounts and Token-2022 accounts that have zero balance. `,
examples: [
[
{
input: {},
output: {
status: "success",
signature:
"3KmPyiZvJQk8CfBVVaz8nf3c2crb6iqjQVDqNxknnusyb1FTFpXqD8zVSCBAd1X3rUcD8WiG1bdSjFbeHsmcYGXY",
accountsClosed: 10,
},
explanation: "Closed 10 empty token accounts successfully.",
},
],
[
{
input: {},
output: {
status: "success",
signature: "",
accountsClosed: 0,
},
explanation: "No empty token accounts were found to close.",
},
],
],
schema: z.object({}),
handler: async (agent: SolanaAgentKit) => {
try {
const result = await closeEmptyTokenAccounts(agent);

if (result.size === 0) {
return {
status: "success",
signature: "",
accountsClosed: 0,
message: "No empty token accounts found to close",
};
}

return {
status: "success",
signature: result.signature,
accountsClosed: result.size,
message: `Successfully closed ${result.size} empty token accounts`,
};
} catch (error: any) {
return {
status: "error",
message: `Failed to close empty token accounts: ${error.message}`,
};
}
},
};

export default closeEmptyTokenAccountsAction;
8 changes: 8 additions & 0 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
create_TipLink,
listNFTForSale,
cancelListing,
closeEmptyTokenAccounts,
fetchTokenReportSummary,
fetchTokenDetailedReport,
fetchPythPrice,
Expand Down Expand Up @@ -547,6 +548,13 @@ export class SolanaAgentKit {
return cancelListing(this, nftMint);
}

async closeEmptyTokenAccounts(): Promise<{
signature: string;
size: number;
}> {
return closeEmptyTokenAccounts(this);
}

async fetchTokenReportSummary(mint: string): Promise<TokenCheck> {
return fetchTokenReportSummary(mint);
}
Expand Down
31 changes: 30 additions & 1 deletion src/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2294,7 +2294,7 @@ export class Solana3LandCreateSingle extends Tool {
...(isMainnet && { isMainnet }),
};

let collectionAccount = inputFormat.collectionAccount;
const collectionAccount = inputFormat.collectionAccount;

const itemName = inputFormat?.itemName;
const sellerFee = inputFormat?.sellerFee;
Expand Down Expand Up @@ -2407,6 +2407,34 @@ export class Solana3LandCreateCollection extends Tool {
}
}

export class SolanaCloseEmptyTokenAccounts 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),
Expand Down Expand Up @@ -2457,6 +2485,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
new SolanaTipLinkTool(solanaKit),
new SolanaListNFTForSaleTool(solanaKit),
new SolanaCancelNFTListingTool(solanaKit),
new SolanaCloseEmptyTokenAccounts(solanaKit),
new SolanaFetchTokenReportSummaryTool(solanaKit),
new SolanaFetchTokenDetailedReportTool(solanaKit),
new Solana3LandCreateSingle(solanaKit),
Expand Down
103 changes: 103 additions & 0 deletions src/tools/close_empty_token_accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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();

const MAX_INSTRUCTIONS = 40; // 40 instructions can be processed in a single transaction without failing

spl_token
.slice(0, Math.min(MAX_INSTRUCTIONS, spl_token.length))
.forEach((instruction) => transaction.add(instruction));

token_2022
.slice(0, Math.max(0, MAX_INSTRUCTIONS - spl_token.length))
.forEach((instruction) => transaction.add(instruction));

const size = spl_token.length + token_2022.length;

if (size === 0) {
return {
signature: "",
size: 0,
};
}

const signature = await agent.connection.sendTransaction(transaction, [
agent.wallet,
]);

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 accountExceptions = [
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
];

for (let i = 0; i < tokens.length; 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;
}
3 changes: 3 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export * from "./send_compressed_airdrop";
export * from "./stake_with_jup";
export * from "./stake_with_solayer";
export * from "./tensor_trade";

export * from "./close_empty_token_accounts";

export * from "./trade";
export * from "./transfer";
export * from "./flash_open_trade";
Expand Down

0 comments on commit b57eb29

Please sign in to comment.