Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add close empty spl-token accounts transaction #118

Merged
merged 14 commits into from
Jan 7, 2025
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