gmsol_sdk/client/
accounts.rs

1use std::ops::Deref;
2
3use gmsol_programs::anchor_lang::{AccountDeserialize, Discriminator};
4use gmsol_solana_utils::{program::Program, utils::WithSlot};
5use serde_json::json;
6use solana_account_decoder::{UiAccount, UiAccountEncoding};
7use solana_client::{
8    client_error::ClientError,
9    nonblocking::rpc_client::RpcClient,
10    rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig, RpcTokenAccountsFilter},
11    rpc_filter::{Memcmp, RpcFilterType},
12    rpc_request::{RpcError, RpcRequest, TokenAccountsFilter},
13    rpc_response::{Response, RpcKeyedAccount},
14};
15use solana_sdk::{
16    account::Account, commitment_config::CommitmentConfig, pubkey::Pubkey, signer::Signer,
17};
18
19/// Program Accounts Config.
20#[derive(Debug, Default)]
21pub struct ProgramAccountsConfigForRpc {
22    /// Filters.
23    pub filters: Option<Vec<RpcFilterType>>,
24    /// Account Config.
25    pub account_config: RpcAccountInfoConfig,
26}
27
28/// Get program accounts with context.
29///
30/// # Note
31/// This function only supports RPC Node versions `>= 1.17`.
32pub async fn get_program_accounts_with_context(
33    client: &RpcClient,
34    program: &Pubkey,
35    mut config: ProgramAccountsConfigForRpc,
36) -> crate::Result<WithSlot<Vec<(Pubkey, Account)>>> {
37    let commitment = config
38        .account_config
39        .commitment
40        .unwrap_or_else(|| client.commitment());
41    config.account_config.commitment = Some(commitment);
42    let config = RpcProgramAccountsConfig {
43        filters: config.filters,
44        account_config: config.account_config,
45        with_context: Some(true),
46        sort_results: None,
47    };
48    tracing::debug!(%program, ?config, "fetching program accounts");
49    let res = client
50        .send::<Response<Vec<RpcKeyedAccount>>>(
51            RpcRequest::GetProgramAccounts,
52            json!([program.to_string(), config]),
53        )
54        .await
55        .map_err(crate::Error::custom)?;
56    WithSlot::new(res.context.slot, res.value)
57        .map(|accounts| parse_keyed_accounts(accounts, RpcRequest::GetProgramAccounts))
58        .transpose()
59}
60
61/// Get account with context.
62///
63/// The value inside the context will be `None` if the account does not exist.
64pub async fn get_account_with_context(
65    client: &RpcClient,
66    address: &Pubkey,
67    mut config: RpcAccountInfoConfig,
68) -> crate::Result<WithSlot<Option<Account>>> {
69    config.encoding = Some(config.encoding.unwrap_or(UiAccountEncoding::Base64));
70    let commitment = config.commitment.unwrap_or_else(|| client.commitment());
71    config.commitment = Some(commitment);
72    tracing::debug!(%address, ?config, "fetching account");
73    let res = client
74        .send::<Response<Option<UiAccount>>>(
75            RpcRequest::GetAccountInfo,
76            json!([address.to_string(), config]),
77        )
78        .await
79        .map_err(crate::Error::custom)?;
80    Ok(WithSlot::new(res.context.slot, res.value).map(|value| value.and_then(|a| a.decode())))
81}
82
83/// Program Accounts Config.
84#[derive(Debug, Default)]
85pub struct ProgramAccountsConfig {
86    /// Whether to skip the account type filter.
87    pub skip_account_type_filter: bool,
88    /// Commitment.
89    pub commitment: Option<CommitmentConfig>,
90    /// Min context slot.
91    pub min_context_slot: Option<u64>,
92}
93
94/// Returns all program accounts of the given type matching the specified filters as
95/// an iterator, along with context. Deserialization is executed lazily.
96pub async fn accounts_lazy_with_context<
97    T: AccountDeserialize + Discriminator,
98    C: Deref<Target = impl Signer> + Clone,
99>(
100    program: &Program<C>,
101    filters: impl IntoIterator<Item = RpcFilterType>,
102    config: ProgramAccountsConfig,
103) -> crate::Result<WithSlot<impl Iterator<Item = crate::Result<(Pubkey, T)>>>> {
104    let ProgramAccountsConfig {
105        skip_account_type_filter,
106        commitment,
107        min_context_slot,
108    } = config;
109    let filters = (!skip_account_type_filter)
110        .then(|| RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, T::DISCRIMINATOR)))
111        .into_iter()
112        .chain(filters)
113        .collect::<Vec<_>>();
114    let config = ProgramAccountsConfigForRpc {
115        filters: (!filters.is_empty()).then_some(filters),
116        account_config: RpcAccountInfoConfig {
117            encoding: Some(UiAccountEncoding::Base64),
118            commitment,
119            min_context_slot,
120            ..Default::default()
121        },
122    };
123    let client = program.rpc();
124    let res = get_program_accounts_with_context(&client, program.id(), config).await?;
125    Ok(res.map(|accounts| {
126        accounts
127            .into_iter()
128            .map(|(key, account)| Ok((key, T::try_deserialize(&mut (&account.data as &[u8]))?)))
129    }))
130}
131
132/// Return the decoded account at the given address, along with context.
133///
134/// The value inside the context will be `None` if the account does not exist.
135pub async fn account_with_context<T: AccountDeserialize>(
136    client: &RpcClient,
137    address: &Pubkey,
138    config: RpcAccountInfoConfig,
139) -> crate::Result<WithSlot<Option<T>>> {
140    let res = get_account_with_context(client, address, config).await?;
141    Ok(res
142        .map(|a| {
143            a.map(|account| T::try_deserialize(&mut (&account.data as &[u8])))
144                .transpose()
145        })
146        .transpose()?)
147}
148
149fn parse_keyed_accounts(
150    accounts: Vec<RpcKeyedAccount>,
151    request: RpcRequest,
152) -> crate::Result<Vec<(Pubkey, Account)>> {
153    let mut pubkey_accounts: Vec<(Pubkey, Account)> = Vec::with_capacity(accounts.len());
154    for RpcKeyedAccount { pubkey, account } in accounts.into_iter() {
155        let pubkey = pubkey
156            .parse()
157            .map_err(|_| {
158                ClientError::new_with_request(
159                    RpcError::ParseError("Pubkey".to_string()).into(),
160                    request,
161                )
162            })
163            .map_err(crate::Error::custom)?;
164        pubkey_accounts.push((
165            pubkey,
166            account
167                .decode()
168                .ok_or_else(|| {
169                    ClientError::new_with_request(
170                        RpcError::ParseError("Account from rpc".to_string()).into(),
171                        request,
172                    )
173                })
174                .map_err(crate::Error::custom)?,
175        ));
176    }
177    Ok(pubkey_accounts)
178}
179
180/// Get token accounts by owner and return with the context.
181pub async fn get_token_accounts_by_owner_with_context(
182    client: &RpcClient,
183    owner: &Pubkey,
184    token_account_filter: TokenAccountsFilter,
185    mut config: RpcAccountInfoConfig,
186) -> crate::Result<WithSlot<Vec<RpcKeyedAccount>>> {
187    let token_account_filter = match token_account_filter {
188        TokenAccountsFilter::Mint(mint) => RpcTokenAccountsFilter::Mint(mint.to_string()),
189        TokenAccountsFilter::ProgramId(program_id) => {
190            RpcTokenAccountsFilter::ProgramId(program_id.to_string())
191        }
192    };
193
194    if config.commitment.is_none() {
195        config.commitment = Some(client.commitment());
196    }
197
198    let res = client
199        .send::<Response<Vec<RpcKeyedAccount>>>(
200            RpcRequest::GetTokenAccountsByOwner,
201            json!([owner.to_string(), token_account_filter, config]),
202        )
203        .await
204        .map_err(crate::Error::custom)?;
205
206    Ok(WithSlot::new(res.context.slot, res.value))
207}
208
209#[cfg(test)]
210mod tests {
211    use std::sync::Arc;
212
213    use anchor_spl::token::ID;
214    use gmsol_solana_utils::cluster::Cluster;
215    use solana_sdk::signature::Keypair;
216
217    use crate::client::Client;
218
219    use super::*;
220
221    #[tokio::test]
222    async fn get_token_accounts_by_owner() -> crate::Result<()> {
223        let client = Client::new(Cluster::Devnet, Arc::new(Keypair::new()))?;
224        let rpc = client.rpc();
225        let owner = "A1TMhSGzQxMr1TboBKtgixKz1sS6REASMxPo1qsyTSJd"
226            .parse()
227            .unwrap();
228        let accounts = get_token_accounts_by_owner_with_context(
229            rpc,
230            &owner,
231            TokenAccountsFilter::ProgramId(ID),
232            RpcAccountInfoConfig {
233                encoding: Some(UiAccountEncoding::Base64),
234                data_slice: None,
235                ..Default::default()
236            },
237        )
238        .await?;
239
240        assert!(!accounts.value().is_empty());
241
242        Ok(())
243    }
244}