solagent_plugin_solana/
close_empty_token_accounts.rs

1// Copyright 2025 zTgx
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use serde::{Deserialize, Serialize};
16use solagent_core::{
17    solana_client::rpc_request::TokenAccountsFilter,
18    solana_sdk::{instruction::Instruction, pubkey::Pubkey, transaction::Transaction},
19    SolanaAgentKit,
20};
21use spl_token::instruction::close_account;
22
23pub const USDC: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
24
25#[derive(serde::Deserialize)]
26pub struct Parsed {
27    pub info: SplToken,
28}
29
30#[derive(serde::Deserialize)]
31pub struct SplToken {
32    pub mint: String,
33    #[serde(rename(deserialize = "tokenAmount"))]
34    pub token_amount: Amount,
35}
36
37#[allow(dead_code)]
38#[derive(serde::Deserialize)]
39pub struct Amount {
40    pub amount: String,
41    #[serde(rename(deserialize = "uiAmountString"))]
42    ui_amount_string: String,
43    #[serde(rename(deserialize = "uiAmount"))]
44    pub ui_amount: f64,
45    pub decimals: u8,
46}
47
48#[derive(Serialize, Deserialize, Debug, Default)]
49pub struct CloseEmptyTokenAccountsData {
50    pub signature: String,
51    pub closed_size: usize,
52}
53
54impl CloseEmptyTokenAccountsData {
55    pub fn new(signature: String, closed_size: usize) -> Self {
56        CloseEmptyTokenAccountsData {
57            signature,
58            closed_size,
59        }
60    }
61}
62
63/// Close Empty SPL Token accounts of the agent.
64///
65/// # Parameters
66///
67/// - `agent`: An instance of `SolanaAgentKit`.
68///
69/// # Returns
70///
71/// Transaction signature and total number of accounts closed or an error if the account doesn't exist.
72pub async fn close_empty_token_accounts(
73    agent: &SolanaAgentKit,
74) -> Result<CloseEmptyTokenAccountsData, Box<dyn std::error::Error>> {
75    let max_instructions = 40_u32;
76    let mut transaction: Vec<Instruction> = vec![];
77    let mut closed_size = 0;
78    let token_programs = vec![spl_token::ID, spl_token_2022::ID];
79
80    for token_program in token_programs {
81        let accounts = agent
82            .connection
83            .get_token_accounts_by_owner(
84                &agent.wallet.pubkey,
85                TokenAccountsFilter::ProgramId(token_program.to_owned()),
86            )
87            .expect("get_token_accounts_by_owner");
88
89        closed_size += accounts.len();
90
91        for account in accounts {
92            if transaction.len() >= max_instructions as usize {
93                break;
94            }
95
96            if let solana_account_decoder::UiAccountData::Json(d) = &account.account.data {
97                if let Ok(parsed) = serde_json::from_value::<Parsed>(d.parsed.clone()) {
98                    if parsed
99                        .info
100                        .token_amount
101                        .amount
102                        .parse::<u32>()
103                        .unwrap_or_default()
104                        == 0_u32
105                        && parsed.info.mint != USDC
106                    {
107                        let account_pubkey = Pubkey::from_str_const(&account.pubkey);
108                        if let Ok(instruct) = close_account(
109                            &token_program,
110                            &account_pubkey,
111                            &agent.wallet.pubkey,
112                            &agent.wallet.pubkey,
113                            &[&agent.wallet.pubkey],
114                        ) {
115                            transaction.push(instruct);
116                        }
117                    }
118                }
119            }
120        }
121    }
122
123    if transaction.is_empty() {
124        return Ok(CloseEmptyTokenAccountsData::default());
125    }
126
127    // Create and send transaction
128    let recent_blockhash = agent.connection.get_latest_blockhash()?;
129    let transaction = Transaction::new_signed_with_payer(
130        &transaction,
131        Some(&agent.wallet.pubkey),
132        &[&agent.wallet.keypair],
133        recent_blockhash,
134    );
135
136    let signature = agent
137        .connection
138        .send_and_confirm_transaction(&transaction)?;
139    let data = CloseEmptyTokenAccountsData::new(signature.to_string(), closed_size);
140    Ok(data)
141}