gmsol/utils/rpc/
accounts.rs

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