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#[derive(Debug, Default)]
21pub struct ProgramAccountsConfigForRpc {
22 pub filters: Option<Vec<RpcFilterType>>,
24 pub account_config: RpcAccountInfoConfig,
26}
27
28pub 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
61pub 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#[derive(Debug, Default)]
85pub struct ProgramAccountsConfig {
86 pub skip_account_type_filter: bool,
88 pub commitment: Option<CommitmentConfig>,
90 pub min_context_slot: Option<u64>,
92}
93
94pub 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
132pub 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
180pub 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}