surfpool_core/rpc/
accounts_scan.rs

1use jsonrpc_core::{BoxFuture, Error, Result};
2use jsonrpc_derive::rpc;
3use solana_account_decoder::{encode_ui_account, UiAccountEncoding};
4use solana_client::{
5    rpc_config::{
6        RpcAccountInfoConfig, RpcLargestAccountsConfig, RpcProgramAccountsConfig, RpcSupplyConfig,
7        RpcTokenAccountsFilter,
8    },
9    rpc_response::{
10        OptionalContext, RpcAccountBalance, RpcKeyedAccount, RpcResponseContext, RpcSupply,
11        RpcTokenAccountBalance,
12    },
13};
14use solana_commitment_config::CommitmentConfig;
15use solana_rpc_client_api::response::Response as RpcResponse;
16use solana_sdk::program_pack::Pack;
17use spl_associated_token_account::get_associated_token_address_with_program_id;
18use spl_token::state::Account as TokenAccount;
19
20use super::{
21    not_implemented_err_async, utils::verify_pubkey, RunloopContext, State, SurfnetRpcContext,
22};
23use crate::surfnet::locker::SvmAccessContext;
24
25#[rpc]
26pub trait AccountsScan {
27    type Metadata;
28
29    /// Returns all accounts owned by the specified program ID, optionally filtered and configured.
30    ///
31    /// This RPC method retrieves all accounts whose owner is the given program. It is commonly used
32    /// to scan on-chain program state, such as finding all token accounts, order books, or PDAs
33    /// owned by a given program. The results can be filtered using data size, memory comparisons, and
34    /// token-specific criteria.
35    ///
36    /// ## Parameters
37    /// - `program_id_str`: Base-58 encoded program ID to scan for owned accounts.
38    /// - `config`: Optional configuration object allowing filters, encoding options, context inclusion,
39    ///   and sorting of results.
40    ///
41    /// ## Returns
42    /// A future resolving to a vector of [`RpcKeyedAccount`]s wrapped in an [`OptionalContext`].
43    /// Each result includes the account's public key and full account data.
44    ///
45    /// ## Example Request (JSON-RPC)
46    /// ```json
47    /// {
48    ///   "jsonrpc": "2.0",
49    ///   "id": 1,
50    ///   "method": "getProgramAccounts",
51    ///   "params": [
52    ///     "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
53    ///     {
54    ///       "filters": [
55    ///         {
56    ///           "dataSize": 165
57    ///         },
58    ///         {
59    ///           "memcmp": {
60    ///             "offset": 0,
61    ///             "bytes": "3N5kaPhfUGuTQZPQ3mnDZZGkUZ97rS1NVSC94QkgUzKN"
62    ///           }
63    ///         }
64    ///       ],
65    ///       "encoding": "jsonParsed",
66    ///       "commitment": "finalized",
67    ///       "withContext": true
68    ///     }
69    ///   ]
70    /// }
71    /// ```
72    ///
73    /// ## Example Response
74    /// ```json
75    /// {
76    ///   "jsonrpc": "2.0",
77    ///   "result": {
78    ///     "context": {
79    ///       "slot": 12345678
80    ///     },
81    ///     "value": [
82    ///       {
83    ///         "pubkey": "BvckZ2XDJmJLho7LnFnV7zM19fRZqnvfs8Qy3fLo6EEk",
84    ///         "account": {
85    ///           "lamports": 2039280,
86    ///           "data": {...},
87    ///           "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
88    ///           "executable": false,
89    ///           "rentEpoch": 255,
90    ///           "space": 165
91    ///         }
92    ///       },
93    ///       ...
94    ///     ]
95    ///   },
96    ///   "id": 1
97    /// }
98    /// ```
99    ///
100    /// # Filters
101    /// - `DataSize(u64)`: Only include accounts with a matching data length.
102    /// - `Memcmp`: Match byte patterns at specified offsets in account data.
103    /// - `TokenAccountState`: Match on internal token account state (e.g. initialized).
104    ///
105    /// ## See also
106    /// - [`RpcProgramAccountsConfig`]: Main config for filtering and encoding.
107    /// - [`UiAccount`]: Returned data representation.
108    /// - [`RpcKeyedAccount`]: Wrapper struct with both pubkey and account fields.
109    #[rpc(meta, name = "getProgramAccounts")]
110    fn get_program_accounts(
111        &self,
112        meta: Self::Metadata,
113        program_id_str: String,
114        config: Option<RpcProgramAccountsConfig>,
115    ) -> BoxFuture<Result<OptionalContext<Vec<RpcKeyedAccount>>>>;
116
117    /// Returns the 20 largest accounts by lamport balance, optionally filtered by account type.
118    ///
119    /// This RPC endpoint is useful for analytics, network monitoring, or understanding
120    /// the distribution of large token holders. It can also be used for sanity checks on
121    /// protocol activity or whale tracking.
122    ///
123    /// ## Parameters
124    /// - `config`: Optional configuration allowing for filtering on specific account types
125    ///   such as circulating or non-circulating accounts.
126    ///
127    /// ## Returns
128    /// A future resolving to a [`RpcResponse`] containing a list of the 20 largest accounts
129    /// by lamports, each represented as an [`RpcAccountBalance`].
130    ///
131    /// ## Example Request (JSON-RPC)
132    /// ```json
133    /// {
134    ///   "jsonrpc": "2.0",
135    ///   "id": 1,
136    ///   "method": "getLargestAccounts",
137    ///   "params": [
138    ///     {
139    ///       "filter": "circulating"
140    ///     }
141    ///   ]
142    /// }
143    /// ```
144    ///
145    /// ## Example Response
146    /// ```json
147    /// {
148    ///   "jsonrpc": "2.0",
149    ///   "result": {
150    ///     "context": {
151    ///       "slot": 15039284
152    ///     },
153    ///     "value": [
154    ///       {
155    ///         "lamports": 999999999999,
156    ///         "address": "9xQeWvG816bUx9EPaZzdd5eUjuJcN3TBDZcd8DM33zDf"
157    ///       },
158    ///       ...
159    ///     ]
160    ///   },
161    ///   "id": 1
162    /// }
163    /// ```
164    ///
165    /// ## See also
166    /// - [`RpcLargestAccountsConfig`] *(defined elsewhere)*: Config struct that may specify a `filter`.
167    /// - [`RpcAccountBalance`]: Struct representing account address and lamport amount.
168    ///
169    /// # Notes
170    /// This method only returns up to 20 accounts and is primarily intended for inspection or diagnostics.
171    #[rpc(meta, name = "getLargestAccounts")]
172    fn get_largest_accounts(
173        &self,
174        meta: Self::Metadata,
175        config: Option<RpcLargestAccountsConfig>,
176    ) -> BoxFuture<Result<RpcResponse<Vec<RpcAccountBalance>>>>;
177
178    /// Returns information about the current token supply on the network, including
179    /// circulating and non-circulating amounts.
180    ///
181    /// This method provides visibility into the economic state of the chain by exposing
182    /// the total amount of tokens issued, how much is in circulation, and what is held in
183    /// non-circulating accounts.
184    ///
185    /// ## Parameters
186    /// - `config`: Optional [`RpcSupplyConfig`] that allows specifying commitment level and
187    ///   whether to exclude the list of non-circulating accounts from the response.
188    ///
189    /// ## Returns
190    /// A future resolving to a [`RpcResponse`] containing a [`RpcSupply`] struct with
191    /// supply metrics in lamports.
192    ///
193    /// ## Example Request (JSON-RPC)
194    /// ```json
195    /// {
196    ///   "jsonrpc": "2.0",
197    ///   "id": 1,
198    ///   "method": "getSupply",
199    ///   "params": [
200    ///     {
201    ///       "excludeNonCirculatingAccountsList": true
202    ///     }
203    ///   ]
204    /// }
205    /// ```
206    ///
207    /// ## Example Response
208    /// ```json
209    /// {
210    ///   "jsonrpc": "2.0",
211    ///   "result": {
212    ///     "context": {
213    ///       "slot": 18000345
214    ///     },
215    ///     "value": {
216    ///       "total": 510000000000000000,
217    ///       "circulating": 420000000000000000,
218    ///       "nonCirculating": 90000000000000000,
219    ///       "nonCirculatingAccounts": []
220    ///     }
221    ///   },
222    ///   "id": 1
223    /// }
224    /// ```
225    ///
226    /// ## See also
227    /// - [`RpcSupplyConfig`]: Configuration struct for optional parameters.
228    /// - [`RpcSupply`]: Response struct with total, circulating, and non-circulating amounts.
229    ///
230    /// # Notes
231    /// - All values are returned in lamports.
232    /// - Use this method to monitor token inflation, distribution, and locked supply dynamics.
233    #[rpc(meta, name = "getSupply")]
234    fn get_supply(
235        &self,
236        meta: Self::Metadata,
237        config: Option<RpcSupplyConfig>,
238    ) -> BoxFuture<Result<RpcResponse<RpcSupply>>>;
239
240    /// Returns the addresses and balances of the largest accounts for a given SPL token mint.
241    ///
242    /// This method is useful for analyzing token distribution and concentration, especially
243    /// to assess decentralization or identify whales.
244    ///
245    /// ## Parameters
246    /// - `mint_str`: The base-58 encoded public key of the mint account of the SPL token.
247    /// - `commitment`: Optional commitment level to query the state of the ledger at different levels
248    ///   of finality (e.g., `Processed`, `Confirmed`, `Finalized`).
249    ///
250    /// ## Returns
251    /// A [`BoxFuture`] resolving to a [`RpcResponse`] with a vector of [`RpcTokenAccountBalance`]s,
252    /// representing the largest accounts holding the token.
253    ///
254    /// ## Example Request (JSON-RPC)
255    /// ```json
256    /// {
257    ///   "jsonrpc": "2.0",
258    ///   "id": 1,
259    ///   "method": "getTokenLargestAccounts",
260    ///   "params": [
261    ///     "So11111111111111111111111111111111111111112"
262    ///   ]
263    /// }
264    /// ```
265    ///
266    /// ## Example Response
267    /// ```json
268    /// {
269    ///   "jsonrpc": "2.0",
270    ///   "result": {
271    ///     "context": {
272    ///       "slot": 18300000
273    ///     },
274    ///     "value": [
275    ///       {
276    ///         "address": "5xy34...Abcd1",
277    ///         "amount": "100000000000",
278    ///         "decimals": 9,
279    ///         "uiAmount": 100.0,
280    ///         "uiAmountString": "100.0"
281    ///       },
282    ///       {
283    ///         "address": "2aXyZ...Efgh2",
284    ///         "amount": "50000000000",
285    ///         "decimals": 9,
286    ///         "uiAmount": 50.0,
287    ///         "uiAmountString": "50.0"
288    ///       }
289    ///     ]
290    ///   },
291    ///   "id": 1
292    /// }
293    /// ```
294    ///
295    /// ## See also
296    /// - [`UiTokenAmount`]: Describes the token amount in different representations.
297    /// - [`RpcTokenAccountBalance`]: Includes token holder address and amount.
298    ///
299    /// # Notes
300    /// - Balances are sorted in descending order.
301    /// - Token decimals are used to format the raw amount into a user-friendly float string.
302    #[rpc(meta, name = "getTokenLargestAccounts")]
303    fn get_token_largest_accounts(
304        &self,
305        meta: Self::Metadata,
306        mint_str: String,
307        commitment: Option<CommitmentConfig>,
308    ) -> BoxFuture<Result<RpcResponse<Vec<RpcTokenAccountBalance>>>>;
309
310    /// Returns all SPL Token accounts owned by a specific wallet address, optionally filtered by mint or program.
311    ///
312    /// This endpoint is commonly used by wallets and explorers to retrieve all token balances
313    /// associated with a user, and optionally narrow results to a specific token mint or program.
314    ///
315    /// ## Parameters
316    /// - `owner_str`: The base-58 encoded public key of the wallet owner.
317    /// - `token_account_filter`: A [`RpcTokenAccountsFilter`] enum that allows filtering results by:
318    ///   - Mint address
319    ///   - Program ID (usually the SPL Token program)
320    /// - `config`: Optional configuration for encoding, data slicing, and commitment.
321    ///
322    /// ## Returns
323    /// A [`BoxFuture`] resolving to a [`RpcResponse`] containing a vector of [`RpcKeyedAccount`]s.
324    /// Each entry contains the public key of a token account and its deserialized account data.
325    ///
326    /// ## Example Request (JSON-RPC)
327    /// ```json
328    /// {
329    ///   "jsonrpc": "2.0",
330    ///   "id": 1,
331    ///   "method": "getTokenAccountsByOwner",
332    ///   "params": [
333    ///     "4Nd1mKxQmZj...Aa123",
334    ///     {
335    ///       "mint": "So11111111111111111111111111111111111111112"
336    ///     },
337    ///     {
338    ///       "encoding": "jsonParsed"
339    ///     }
340    ///   ]
341    /// }
342    /// ```
343    ///
344    /// ## Example Response
345    /// ```json
346    /// {
347    ///   "jsonrpc": "2.0",
348    ///   "result": {
349    ///     "context": { "slot": 19281234 },
350    ///     "value": [
351    ///       {
352    ///         "pubkey": "2sZp...xyz",
353    ///         "account": {
354    ///           "lamports": 2039280,
355    ///           "data": { /* token info */ },
356    ///           "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
357    ///           "executable": false,
358    ///           "rentEpoch": 123
359    ///         }
360    ///       }
361    ///     ]
362    ///   },
363    ///   "id": 1
364    /// }
365    /// ```
366    ///
367    /// # Filter Enum
368    /// [`RpcTokenAccountsFilter`] can be:
369    /// - `Mint(String)` — return only token accounts associated with the specified mint.
370    /// - `ProgramId(String)` — return only token accounts owned by the specified program (e.g. SPL Token program).
371    ///
372    /// ## See also
373    /// - [`RpcKeyedAccount`]: Contains the pubkey and the associated account data.
374    /// - [`RpcAccountInfoConfig`]: Allows tweaking how account data is returned (encoding, commitment, etc.).
375    /// - [`UiAccountEncoding`], [`CommitmentConfig`]
376    ///
377    /// # Notes
378    /// - The response may contain `Option::None` for accounts that couldn't be fetched or decoded.
379    /// - Encoding `jsonParsed` is recommended when integrating with frontend UIs.
380    #[rpc(meta, name = "getTokenAccountsByOwner")]
381    fn get_token_accounts_by_owner(
382        &self,
383        meta: Self::Metadata,
384        owner_str: String,
385        token_account_filter: RpcTokenAccountsFilter,
386        config: Option<RpcAccountInfoConfig>,
387    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>>;
388
389    /// Returns all SPL Token accounts that have delegated authority to a specific address, with optional filters.
390    ///
391    /// This RPC method is useful for identifying which token accounts have granted delegate rights
392    /// to a particular wallet or program (commonly used in DeFi apps or custodial flows).
393    ///
394    /// ## Parameters
395    /// - `delegate_str`: The base-58 encoded public key of the delegate authority.
396    /// - `token_account_filter`: A [`RpcTokenAccountsFilter`] enum to filter results by mint or program.
397    /// - `config`: Optional [`RpcAccountInfoConfig`] for controlling account encoding, commitment level, etc.
398    ///
399    /// ## Returns
400    /// A [`BoxFuture`] resolving to a [`RpcResponse`] containing a vector of [`RpcKeyedAccount`]s,
401    /// each pairing a token account public key with its associated on-chain data.
402    ///
403    /// ## Example Request (JSON-RPC)
404    /// ```json
405    /// {
406    ///   "jsonrpc": "2.0",
407    ///   "id": 1,
408    ///   "method": "getTokenAccountsByDelegate",
409    ///   "params": [
410    ///     "3qTwHcdK1j...XYZ",
411    ///     { "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" },
412    ///     { "encoding": "jsonParsed" }
413    ///   ]
414    /// }
415    /// ```
416    ///
417    /// ## Example Response
418    /// ```json
419    /// {
420    ///   "jsonrpc": "2.0",
421    ///   "result": {
422    ///     "context": { "slot": 19301523 },
423    ///     "value": [
424    ///       {
425    ///         "pubkey": "8H5k...abc",
426    ///         "account": {
427    ///           "lamports": 2039280,
428    ///           "data": { /* token info */ },
429    ///           "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
430    ///           "executable": false,
431    ///           "rentEpoch": 131
432    ///         }
433    ///       }
434    ///     ]
435    ///   },
436    ///   "id": 1
437    /// }
438    /// ```
439    ///
440    /// # Filters
441    /// Use [`RpcTokenAccountsFilter`] to limit the query scope:
442    /// - `Mint(String)` – return accounts associated with a given token.
443    /// - `ProgramId(String)` – return accounts under a specific program (e.g., SPL Token program).
444    ///
445    /// # Notes
446    /// - Useful for monitoring delegated token activity in governance or trading protocols.
447    /// - If a token account doesn’t have a delegate, it won’t be included in results.
448    ///
449    /// ## See also
450    /// - [`RpcKeyedAccount`], [`RpcAccountInfoConfig`], [`CommitmentConfig`], [`UiAccountEncoding`]
451    #[rpc(meta, name = "getTokenAccountsByDelegate")]
452    fn get_token_accounts_by_delegate(
453        &self,
454        meta: Self::Metadata,
455        delegate_str: String,
456        token_account_filter: RpcTokenAccountsFilter,
457        config: Option<RpcAccountInfoConfig>,
458    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>>;
459}
460
461pub struct SurfpoolAccountsScanRpc;
462impl AccountsScan for SurfpoolAccountsScanRpc {
463    type Metadata = Option<RunloopContext>;
464
465    fn get_program_accounts(
466        &self,
467        _meta: Self::Metadata,
468        _program_id_str: String,
469        _config: Option<RpcProgramAccountsConfig>,
470    ) -> BoxFuture<Result<OptionalContext<Vec<RpcKeyedAccount>>>> {
471        not_implemented_err_async("get_program_accounts")
472    }
473
474    fn get_largest_accounts(
475        &self,
476        _meta: Self::Metadata,
477        _config: Option<RpcLargestAccountsConfig>,
478    ) -> BoxFuture<Result<RpcResponse<Vec<RpcAccountBalance>>>> {
479        not_implemented_err_async("get_largest_accounts")
480    }
481
482    fn get_supply(
483        &self,
484        meta: Self::Metadata,
485        _config: Option<RpcSupplyConfig>,
486    ) -> BoxFuture<Result<RpcResponse<RpcSupply>>> {
487        let svm_locker = match meta.get_svm_locker() {
488            Ok(locker) => locker,
489            Err(e) => return e.into(),
490        };
491
492        Box::pin(async move {
493            svm_locker.with_svm_reader(|svm_reader| {
494                let slot = svm_reader.get_latest_absolute_slot();
495                Ok(RpcResponse {
496                    context: RpcResponseContext::new(slot),
497                    value: RpcSupply {
498                        total: 1,
499                        circulating: 0,
500                        non_circulating: 0,
501                        non_circulating_accounts: vec![],
502                    },
503                })
504            })
505        })
506    }
507
508    fn get_token_largest_accounts(
509        &self,
510        _meta: Self::Metadata,
511        _mint_str: String,
512        _commitment: Option<CommitmentConfig>,
513    ) -> BoxFuture<Result<RpcResponse<Vec<RpcTokenAccountBalance>>>> {
514        not_implemented_err_async("get_token_largest_accounts")
515    }
516
517    fn get_token_accounts_by_owner(
518        &self,
519        meta: Self::Metadata,
520        owner_str: String,
521        token_account_filter: RpcTokenAccountsFilter,
522        config: Option<RpcAccountInfoConfig>,
523    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>> {
524        let config = config.unwrap_or_default();
525        let owner = match verify_pubkey(&owner_str) {
526            Ok(res) => res,
527            Err(e) => return e.into(),
528        };
529
530        let SurfnetRpcContext {
531            svm_locker,
532            remote_ctx,
533        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
534            Ok(res) => res,
535            Err(e) => return e.into(),
536        };
537
538        Box::pin(async move {
539            match token_account_filter {
540                RpcTokenAccountsFilter::Mint(mint) => {
541                    let mint = verify_pubkey(&mint)?;
542
543                    let associated_token_address = get_associated_token_address_with_program_id(
544                        &owner,
545                        &mint,
546                        &spl_token::id(),
547                    );
548                    let SvmAccessContext {
549                        slot,
550                        inner: account_update,
551                        ..
552                    } = svm_locker
553                        .get_account(&remote_ctx, &associated_token_address, None)
554                        .await?;
555
556                    svm_locker.write_account_update(account_update.clone());
557
558                    let token_account = account_update.map_account()?;
559
560                    let _ = TokenAccount::unpack(&token_account.data).map_err(|e| {
561                        Error::invalid_params(format!("Failed to unpack token account data: {}", e))
562                    })?;
563
564                    Ok(RpcResponse {
565                        context: RpcResponseContext::new(slot),
566                        value: vec![RpcKeyedAccount {
567                            pubkey: associated_token_address.to_string(),
568                            account: encode_ui_account(
569                                &associated_token_address,
570                                &token_account,
571                                config.encoding.unwrap_or(UiAccountEncoding::Base64),
572                                None,
573                                config.data_slice,
574                            ),
575                        }],
576                    })
577                }
578                RpcTokenAccountsFilter::ProgramId(program_id) => {
579                    let program_id = verify_pubkey(&program_id)?;
580
581                    let remote_ctx = remote_ctx.map(|(r, _)| r);
582                    let SvmAccessContext {
583                        slot,
584                        inner: (keyed_accounts, missing_pubkeys),
585                        ..
586                    } = svm_locker
587                        .get_all_token_accounts(&remote_ctx, owner, program_id)
588                        .await?;
589
590                    Ok(RpcResponse {
591                        context: RpcResponseContext::new(slot),
592                        value: keyed_accounts,
593                    })
594                }
595            }
596        })
597    }
598
599    fn get_token_accounts_by_delegate(
600        &self,
601        _meta: Self::Metadata,
602        _delegate_str: String,
603        _token_account_filter: RpcTokenAccountsFilter,
604        _config: Option<RpcAccountInfoConfig>,
605    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>> {
606        not_implemented_err_async("get_token_accounts_by_delegate")
607    }
608}