surfpool_core/rpc/
accounts_scan.rs

1use jsonrpc_core::{BoxFuture, Error as JsonRpcCoreError, ErrorCode, Result};
2use jsonrpc_derive::rpc;
3use solana_client::{
4    rpc_config::{
5        RpcAccountInfoConfig, RpcLargestAccountsConfig, RpcProgramAccountsConfig, RpcSupplyConfig,
6        RpcTokenAccountsFilter,
7    },
8    rpc_request::TokenAccountsFilter,
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;
16
17use super::{utils::verify_pubkey, RunloopContext, State, SurfnetRpcContext};
18use crate::surfnet::locker::SvmAccessContext;
19
20#[rpc]
21pub trait AccountsScan {
22    type Metadata;
23
24    /// Returns all accounts owned by the specified program ID, optionally filtered and configured.
25    ///
26    /// This RPC method retrieves all accounts whose owner is the given program. It is commonly used
27    /// to scan on-chain program state, such as finding all token accounts, order books, or PDAs
28    /// owned by a given program. The results can be filtered using data size, memory comparisons, and
29    /// token-specific criteria.
30    ///
31    /// ## Parameters
32    /// - `program_id_str`: Base-58 encoded program ID to scan for owned accounts.
33    /// - `config`: Optional configuration object allowing filters, encoding options, context inclusion,
34    ///   and sorting of results.
35    ///
36    /// ## Returns
37    /// A future resolving to a vector of [`RpcKeyedAccount`]s wrapped in an [`OptionalContext`].
38    /// Each result includes the account's public key and full account data.
39    ///
40    /// ## Example Request (JSON-RPC)
41    /// ```json
42    /// {
43    ///   "jsonrpc": "2.0",
44    ///   "id": 1,
45    ///   "method": "getProgramAccounts",
46    ///   "params": [
47    ///     "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
48    ///     {
49    ///       "filters": [
50    ///         {
51    ///           "dataSize": 165
52    ///         },
53    ///         {
54    ///           "memcmp": {
55    ///             "offset": 0,
56    ///             "bytes": "3N5kaPhfUGuTQZPQ3mnDZZGkUZ97rS1NVSC94QkgUzKN"
57    ///           }
58    ///         }
59    ///       ],
60    ///       "encoding": "jsonParsed",
61    ///       "commitment": "finalized",
62    ///       "withContext": true
63    ///     }
64    ///   ]
65    /// }
66    /// ```
67    ///
68    /// ## Example Response
69    /// ```json
70    /// {
71    ///   "jsonrpc": "2.0",
72    ///   "result": {
73    ///     "context": {
74    ///       "slot": 12345678
75    ///     },
76    ///     "value": [
77    ///       {
78    ///         "pubkey": "BvckZ2XDJmJLho7LnFnV7zM19fRZqnvfs8Qy3fLo6EEk",
79    ///         "account": {
80    ///           "lamports": 2039280,
81    ///           "data": {...},
82    ///           "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
83    ///           "executable": false,
84    ///           "rentEpoch": 255,
85    ///           "space": 165
86    ///         }
87    ///       },
88    ///       ...
89    ///     ]
90    ///   },
91    ///   "id": 1
92    /// }
93    /// ```
94    ///
95    /// # Filters
96    /// - `DataSize(u64)`: Only include accounts with a matching data length.
97    /// - `Memcmp`: Match byte patterns at specified offsets in account data.
98    /// - `TokenAccountState`: Match on internal token account state (e.g. initialized).
99    ///
100    /// ## See also
101    /// - [`RpcProgramAccountsConfig`]: Main config for filtering and encoding.
102    /// - [`UiAccount`]: Returned data representation.
103    /// - [`RpcKeyedAccount`]: Wrapper struct with both pubkey and account fields.
104    #[rpc(meta, name = "getProgramAccounts")]
105    fn get_program_accounts(
106        &self,
107        meta: Self::Metadata,
108        program_id_str: String,
109        config: Option<RpcProgramAccountsConfig>,
110    ) -> BoxFuture<Result<OptionalContext<Vec<RpcKeyedAccount>>>>;
111
112    /// Returns the 20 largest accounts by lamport balance, optionally filtered by account type.
113    ///
114    /// This RPC endpoint is useful for analytics, network monitoring, or understanding
115    /// the distribution of large token holders. It can also be used for sanity checks on
116    /// protocol activity or whale tracking.
117    ///
118    /// ## Parameters
119    /// - `config`: Optional configuration allowing for filtering on specific account types
120    ///   such as circulating or non-circulating accounts.
121    ///
122    /// ## Returns
123    /// A future resolving to a [`RpcResponse`] containing a list of the 20 largest accounts
124    /// by lamports, each represented as an [`RpcAccountBalance`].
125    ///
126    /// ## Example Request (JSON-RPC)
127    /// ```json
128    /// {
129    ///   "jsonrpc": "2.0",
130    ///   "id": 1,
131    ///   "method": "getLargestAccounts",
132    ///   "params": [
133    ///     {
134    ///       "filter": "circulating"
135    ///     }
136    ///   ]
137    /// }
138    /// ```
139    ///
140    /// ## Example Response
141    /// ```json
142    /// {
143    ///   "jsonrpc": "2.0",
144    ///   "result": {
145    ///     "context": {
146    ///       "slot": 15039284
147    ///     },
148    ///     "value": [
149    ///       {
150    ///         "lamports": 999999999999,
151    ///         "address": "9xQeWvG816bUx9EPaZzdd5eUjuJcN3TBDZcd8DM33zDf"
152    ///       },
153    ///       ...
154    ///     ]
155    ///   },
156    ///   "id": 1
157    /// }
158    /// ```
159    ///
160    /// ## See also
161    /// - [`RpcLargestAccountsConfig`] *(defined elsewhere)*: Config struct that may specify a `filter`.
162    /// - [`RpcAccountBalance`]: Struct representing account address and lamport amount.
163    ///
164    /// # Notes
165    /// This method only returns up to 20 accounts and is primarily intended for inspection or diagnostics.
166    #[rpc(meta, name = "getLargestAccounts")]
167    fn get_largest_accounts(
168        &self,
169        meta: Self::Metadata,
170        config: Option<RpcLargestAccountsConfig>,
171    ) -> BoxFuture<Result<RpcResponse<Vec<RpcAccountBalance>>>>;
172
173    /// Returns information about the current token supply on the network, including
174    /// circulating and non-circulating amounts.
175    ///
176    /// This method provides visibility into the economic state of the chain by exposing
177    /// the total amount of tokens issued, how much is in circulation, and what is held in
178    /// non-circulating accounts.
179    ///
180    /// ## Parameters
181    /// - `config`: Optional [`RpcSupplyConfig`] that allows specifying commitment level and
182    ///   whether to exclude the list of non-circulating accounts from the response.
183    ///
184    /// ## Returns
185    /// A future resolving to a [`RpcResponse`] containing a [`RpcSupply`] struct with
186    /// supply metrics in lamports.
187    ///
188    /// ## Example Request (JSON-RPC)
189    /// ```json
190    /// {
191    ///   "jsonrpc": "2.0",
192    ///   "id": 1,
193    ///   "method": "getSupply",
194    ///   "params": [
195    ///     {
196    ///       "excludeNonCirculatingAccountsList": true
197    ///     }
198    ///   ]
199    /// }
200    /// ```
201    ///
202    /// ## Example Response
203    /// ```json
204    /// {
205    ///   "jsonrpc": "2.0",
206    ///   "result": {
207    ///     "context": {
208    ///       "slot": 18000345
209    ///     },
210    ///     "value": {
211    ///       "total": 510000000000000000,
212    ///       "circulating": 420000000000000000,
213    ///       "nonCirculating": 90000000000000000,
214    ///       "nonCirculatingAccounts": []
215    ///     }
216    ///   },
217    ///   "id": 1
218    /// }
219    /// ```
220    ///
221    /// ## See also
222    /// - [`RpcSupplyConfig`]: Configuration struct for optional parameters.
223    /// - [`RpcSupply`]: Response struct with total, circulating, and non-circulating amounts.
224    ///
225    /// # Notes
226    /// - All values are returned in lamports.
227    /// - Use this method to monitor token inflation, distribution, and locked supply dynamics.
228    #[rpc(meta, name = "getSupply")]
229    fn get_supply(
230        &self,
231        meta: Self::Metadata,
232        config: Option<RpcSupplyConfig>,
233    ) -> BoxFuture<Result<RpcResponse<RpcSupply>>>;
234
235    /// Returns the addresses and balances of the largest accounts for a given SPL token mint.
236    ///
237    /// This method is useful for analyzing token distribution and concentration, especially
238    /// to assess decentralization or identify whales.
239    ///
240    /// ## Parameters
241    /// - `mint_str`: The base-58 encoded public key of the mint account of the SPL token.
242    /// - `commitment`: Optional commitment level to query the state of the ledger at different levels
243    ///   of finality (e.g., `Processed`, `Confirmed`, `Finalized`).
244    ///
245    /// ## Returns
246    /// A [`BoxFuture`] resolving to a [`RpcResponse`] with a vector of [`RpcTokenAccountBalance`]s,
247    /// representing the largest accounts holding the token.
248    ///
249    /// ## Example Request (JSON-RPC)
250    /// ```json
251    /// {
252    ///   "jsonrpc": "2.0",
253    ///   "id": 1,
254    ///   "method": "getTokenLargestAccounts",
255    ///   "params": [
256    ///     "So11111111111111111111111111111111111111112"
257    ///   ]
258    /// }
259    /// ```
260    ///
261    /// ## Example Response
262    /// ```json
263    /// {
264    ///   "jsonrpc": "2.0",
265    ///   "result": {
266    ///     "context": {
267    ///       "slot": 18300000
268    ///     },
269    ///     "value": [
270    ///       {
271    ///         "address": "5xy34...Abcd1",
272    ///         "amount": "100000000000",
273    ///         "decimals": 9,
274    ///         "uiAmount": 100.0,
275    ///         "uiAmountString": "100.0"
276    ///       },
277    ///       {
278    ///         "address": "2aXyZ...Efgh2",
279    ///         "amount": "50000000000",
280    ///         "decimals": 9,
281    ///         "uiAmount": 50.0,
282    ///         "uiAmountString": "50.0"
283    ///       }
284    ///     ]
285    ///   },
286    ///   "id": 1
287    /// }
288    /// ```
289    ///
290    /// ## See also
291    /// - [`UiTokenAmount`]: Describes the token amount in different representations.
292    /// - [`RpcTokenAccountBalance`]: Includes token holder address and amount.
293    ///
294    /// # Notes
295    /// - Balances are sorted in descending order.
296    /// - Token decimals are used to format the raw amount into a user-friendly float string.
297    #[rpc(meta, name = "getTokenLargestAccounts")]
298    fn get_token_largest_accounts(
299        &self,
300        meta: Self::Metadata,
301        mint_str: String,
302        commitment: Option<CommitmentConfig>,
303    ) -> BoxFuture<Result<RpcResponse<Vec<RpcTokenAccountBalance>>>>;
304
305    /// Returns all SPL Token accounts owned by a specific wallet address, optionally filtered by mint or program.
306    ///
307    /// This endpoint is commonly used by wallets and explorers to retrieve all token balances
308    /// associated with a user, and optionally narrow results to a specific token mint or program.
309    ///
310    /// ## Parameters
311    /// - `owner_str`: The base-58 encoded public key of the wallet owner.
312    /// - `token_account_filter`: A [`RpcTokenAccountsFilter`] enum that allows filtering results by:
313    ///   - Mint address
314    ///   - Program ID (usually the SPL Token program)
315    /// - `config`: Optional configuration for encoding, data slicing, and commitment.
316    ///
317    /// ## Returns
318    /// A [`BoxFuture`] resolving to a [`RpcResponse`] containing a vector of [`RpcKeyedAccount`]s.
319    /// Each entry contains the public key of a token account and its deserialized account data.
320    ///
321    /// ## Example Request (JSON-RPC)
322    /// ```json
323    /// {
324    ///   "jsonrpc": "2.0",
325    ///   "id": 1,
326    ///   "method": "getTokenAccountsByOwner",
327    ///   "params": [
328    ///     "4Nd1mKxQmZj...Aa123",
329    ///     {
330    ///       "mint": "So11111111111111111111111111111111111111112"
331    ///     },
332    ///     {
333    ///       "encoding": "jsonParsed"
334    ///     }
335    ///   ]
336    /// }
337    /// ```
338    ///
339    /// ## Example Response
340    /// ```json
341    /// {
342    ///   "jsonrpc": "2.0",
343    ///   "result": {
344    ///     "context": { "slot": 19281234 },
345    ///     "value": [
346    ///       {
347    ///         "pubkey": "2sZp...xyz",
348    ///         "account": {
349    ///           "lamports": 2039280,
350    ///           "data": { /* token info */ },
351    ///           "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
352    ///           "executable": false,
353    ///           "rentEpoch": 123
354    ///         }
355    ///       }
356    ///     ]
357    ///   },
358    ///   "id": 1
359    /// }
360    /// ```
361    ///
362    /// # Filter Enum
363    /// [`RpcTokenAccountsFilter`] can be:
364    /// - `Mint(String)` — return only token accounts associated with the specified mint.
365    /// - `ProgramId(String)` — return only token accounts owned by the specified program (e.g. SPL Token program).
366    ///
367    /// ## See also
368    /// - [`RpcKeyedAccount`]: Contains the pubkey and the associated account data.
369    /// - [`RpcAccountInfoConfig`]: Allows tweaking how account data is returned (encoding, commitment, etc.).
370    /// - [`UiAccountEncoding`], [`CommitmentConfig`]
371    ///
372    /// # Notes
373    /// - The response may contain `Option::None` for accounts that couldn't be fetched or decoded.
374    /// - Encoding `jsonParsed` is recommended when integrating with frontend UIs.
375    #[rpc(meta, name = "getTokenAccountsByOwner")]
376    fn get_token_accounts_by_owner(
377        &self,
378        meta: Self::Metadata,
379        owner_str: String,
380        token_account_filter: RpcTokenAccountsFilter,
381        config: Option<RpcAccountInfoConfig>,
382    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>>;
383
384    /// Returns all SPL Token accounts that have delegated authority to a specific address, with optional filters.
385    ///
386    /// This RPC method is useful for identifying which token accounts have granted delegate rights
387    /// to a particular wallet or program (commonly used in DeFi apps or custodial flows).
388    ///
389    /// ## Parameters
390    /// - `delegate_str`: The base-58 encoded public key of the delegate authority.
391    /// - `token_account_filter`: A [`RpcTokenAccountsFilter`] enum to filter results by mint or program.
392    /// - `config`: Optional [`RpcAccountInfoConfig`] for controlling account encoding, commitment level, etc.
393    ///
394    /// ## Returns
395    /// A [`BoxFuture`] resolving to a [`RpcResponse`] containing a vector of [`RpcKeyedAccount`]s,
396    /// each pairing a token account public key with its associated on-chain data.
397    ///
398    /// ## Example Request (JSON-RPC)
399    /// ```json
400    /// {
401    ///   "jsonrpc": "2.0",
402    ///   "id": 1,
403    ///   "method": "getTokenAccountsByDelegate",
404    ///   "params": [
405    ///     "3qTwHcdK1j...XYZ",
406    ///     { "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" },
407    ///     { "encoding": "jsonParsed" }
408    ///   ]
409    /// }
410    /// ```
411    ///
412    /// ## Example Response
413    /// ```json
414    /// {
415    ///   "jsonrpc": "2.0",
416    ///   "result": {
417    ///     "context": { "slot": 19301523 },
418    ///     "value": [
419    ///       {
420    ///         "pubkey": "8H5k...abc",
421    ///         "account": {
422    ///           "lamports": 2039280,
423    ///           "data": { /* token info */ },
424    ///           "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
425    ///           "executable": false,
426    ///           "rentEpoch": 131
427    ///         }
428    ///       }
429    ///     ]
430    ///   },
431    ///   "id": 1
432    /// }
433    /// ```
434    ///
435    /// # Filters
436    /// Use [`RpcTokenAccountsFilter`] to limit the query scope:
437    /// - `Mint(String)` – return accounts associated with a given token.
438    /// - `ProgramId(String)` – return accounts under a specific program (e.g., SPL Token program).
439    ///
440    /// # Notes
441    /// - Useful for monitoring delegated token activity in governance or trading protocols.
442    /// - If a token account doesn't have a delegate, it won't be included in results.
443    ///
444    /// ## See also
445    /// - [`RpcKeyedAccount`], [`RpcAccountInfoConfig`], [`CommitmentConfig`], [`UiAccountEncoding`]
446    #[rpc(meta, name = "getTokenAccountsByDelegate")]
447    fn get_token_accounts_by_delegate(
448        &self,
449        meta: Self::Metadata,
450        delegate_str: String,
451        token_account_filter: RpcTokenAccountsFilter,
452        config: Option<RpcAccountInfoConfig>,
453    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>>;
454}
455
456#[derive(Clone)]
457pub struct SurfpoolAccountsScanRpc;
458impl AccountsScan for SurfpoolAccountsScanRpc {
459    type Metadata = Option<RunloopContext>;
460
461    fn get_program_accounts(
462        &self,
463        meta: Self::Metadata,
464        program_id_str: String,
465        config: Option<RpcProgramAccountsConfig>,
466    ) -> BoxFuture<Result<OptionalContext<Vec<RpcKeyedAccount>>>> {
467        let config = config.unwrap_or_default();
468        let program_id = match verify_pubkey(&program_id_str) {
469            Ok(res) => res,
470            Err(e) => return e.into(),
471        };
472
473        let SurfnetRpcContext {
474            svm_locker,
475            remote_ctx,
476        } = match meta.get_rpc_context(()) {
477            Ok(res) => res,
478            Err(e) => return e.into(),
479        };
480
481        Box::pin(async move {
482            let current_slot = svm_locker.get_latest_absolute_slot();
483
484            let account_config = config.account_config;
485
486            if let Some(min_context_slot_val) = account_config.min_context_slot.as_ref() {
487                if current_slot < *min_context_slot_val {
488                    return Err(JsonRpcCoreError {
489                        code: ErrorCode::InternalError,
490                        message: format!(
491                            "Node's current slot {} is less than requested minContextSlot {}",
492                            current_slot, min_context_slot_val
493                        ),
494                        data: None,
495                    });
496                }
497            }
498
499            // Get program-owned accounts from the account registry
500            let program_accounts = svm_locker
501                .get_program_accounts(
502                    &remote_ctx.map(|(client, _)| client),
503                    &program_id,
504                    account_config,
505                    config.filters,
506                )
507                .await?
508                .inner;
509
510            if config.with_context.unwrap_or(false) {
511                Ok(OptionalContext::Context(RpcResponse {
512                    context: RpcResponseContext::new(current_slot),
513                    value: program_accounts,
514                }))
515            } else {
516                Ok(OptionalContext::NoContext(program_accounts))
517            }
518        })
519    }
520
521    fn get_largest_accounts(
522        &self,
523        meta: Self::Metadata,
524        config: Option<RpcLargestAccountsConfig>,
525    ) -> BoxFuture<Result<RpcResponse<Vec<RpcAccountBalance>>>> {
526        let config = config.unwrap_or_default();
527        let SurfnetRpcContext {
528            svm_locker,
529            remote_ctx,
530        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
531            Ok(res) => res,
532            Err(e) => return e.into(),
533        };
534
535        Box::pin(async move {
536            let current_slot = svm_locker.get_latest_absolute_slot();
537
538            let largest_accounts = svm_locker
539                .get_largest_accounts(&remote_ctx, config)
540                .await?
541                .inner;
542
543            Ok(RpcResponse {
544                context: RpcResponseContext::new(current_slot),
545                value: largest_accounts,
546            })
547        })
548    }
549
550    fn get_supply(
551        &self,
552        meta: Self::Metadata,
553        config: Option<RpcSupplyConfig>,
554    ) -> BoxFuture<Result<RpcResponse<RpcSupply>>> {
555        let svm_locker = match meta.get_svm_locker() {
556            Ok(locker) => locker,
557            Err(e) => return e.into(),
558        };
559
560        Box::pin(async move {
561            svm_locker.with_svm_reader(|svm_reader| {
562                let slot = svm_reader.get_latest_absolute_slot();
563
564                // Check if we should exclude non-circulating accounts list
565                let exclude_accounts = config
566                    .as_ref()
567                    .map(|c| c.exclude_non_circulating_accounts_list)
568                    .unwrap_or(false);
569
570                Ok(RpcResponse {
571                    context: RpcResponseContext::new(slot),
572                    value: RpcSupply {
573                        total: svm_reader.total_supply,
574                        circulating: svm_reader.circulating_supply,
575                        non_circulating: svm_reader.non_circulating_supply,
576                        non_circulating_accounts: if exclude_accounts {
577                            vec![]
578                        } else {
579                            svm_reader.non_circulating_accounts.clone()
580                        },
581                    },
582                })
583            })
584        })
585    }
586
587    fn get_token_largest_accounts(
588        &self,
589        meta: Self::Metadata,
590        mint_str: String,
591        commitment: Option<CommitmentConfig>,
592    ) -> BoxFuture<Result<RpcResponse<Vec<RpcTokenAccountBalance>>>> {
593        let mint = match verify_pubkey(&mint_str) {
594            Ok(res) => res,
595            Err(e) => return e.into(),
596        };
597
598        let SurfnetRpcContext {
599            svm_locker,
600            remote_ctx,
601        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
602            Ok(res) => res,
603            Err(e) => return e.into(),
604        };
605
606        Box::pin(async move {
607            let SvmAccessContext {
608                slot,
609                inner: largest_accounts,
610                ..
611            } = svm_locker
612                .get_token_largest_accounts(&remote_ctx, &mint)
613                .await?;
614
615            Ok(RpcResponse {
616                context: RpcResponseContext::new(slot),
617                value: largest_accounts,
618            })
619        })
620    }
621
622    fn get_token_accounts_by_owner(
623        &self,
624        meta: Self::Metadata,
625        owner_str: String,
626        token_account_filter: RpcTokenAccountsFilter,
627        config: Option<RpcAccountInfoConfig>,
628    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>> {
629        let config = config.unwrap_or_default();
630        let owner = match verify_pubkey(&owner_str) {
631            Ok(res) => res,
632            Err(e) => return e.into(),
633        };
634
635        let filter = match token_account_filter {
636            RpcTokenAccountsFilter::Mint(mint_str) => {
637                let mint = match verify_pubkey(&mint_str) {
638                    Ok(res) => res,
639                    Err(e) => return e.into(),
640                };
641                TokenAccountsFilter::Mint(mint)
642            }
643            RpcTokenAccountsFilter::ProgramId(program_id_str) => {
644                let program_id = match verify_pubkey(&program_id_str) {
645                    Ok(res) => res,
646                    Err(e) => return e.into(),
647                };
648                TokenAccountsFilter::ProgramId(program_id)
649            }
650        };
651
652        let SurfnetRpcContext {
653            svm_locker,
654            remote_ctx,
655        } = match meta.get_rpc_context(()) {
656            Ok(res) => res,
657            Err(e) => return e.into(),
658        };
659
660        Box::pin(async move {
661            let SvmAccessContext {
662                slot,
663                inner: token_accounts,
664                ..
665            } = svm_locker
666                .get_token_accounts_by_owner(
667                    &remote_ctx.map(|(client, _)| client),
668                    owner,
669                    &filter,
670                    &config,
671                )
672                .await?;
673
674            Ok(RpcResponse {
675                context: RpcResponseContext::new(slot),
676                value: token_accounts,
677            })
678        })
679    }
680
681    fn get_token_accounts_by_delegate(
682        &self,
683        meta: Self::Metadata,
684        delegate_str: String,
685        token_account_filter: RpcTokenAccountsFilter,
686        config: Option<RpcAccountInfoConfig>,
687    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>> {
688        let config = config.unwrap_or_default();
689        let delegate = match verify_pubkey(&delegate_str) {
690            Ok(res) => res,
691            Err(e) => return e.into(),
692        };
693
694        let SurfnetRpcContext {
695            svm_locker,
696            remote_ctx,
697        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
698            Ok(res) => res,
699            Err(e) => return e.into(),
700        };
701
702        Box::pin(async move {
703            let filter = match token_account_filter {
704                RpcTokenAccountsFilter::Mint(mint_str) => {
705                    TokenAccountsFilter::Mint(verify_pubkey(&mint_str)?)
706                }
707                RpcTokenAccountsFilter::ProgramId(program_id_str) => {
708                    TokenAccountsFilter::ProgramId(verify_pubkey(&program_id_str)?)
709                }
710            };
711
712            let remote_ctx = remote_ctx.map(|(r, _)| r);
713            let SvmAccessContext {
714                slot,
715                inner: keyed_accounts,
716                ..
717            } = svm_locker
718                .get_token_accounts_by_delegate(&remote_ctx, delegate, &filter, &config)
719                .await?;
720
721            Ok(RpcResponse {
722                context: RpcResponseContext::new(slot),
723                value: keyed_accounts,
724            })
725        })
726    }
727}
728
729#[cfg(test)]
730mod tests {
731
732    use core::panic;
733    use std::str::FromStr;
734
735    use solana_account::Account;
736    use solana_client::{
737        rpc_config::{
738            RpcLargestAccountsConfig, RpcLargestAccountsFilter, RpcProgramAccountsConfig,
739            RpcSupplyConfig, RpcTokenAccountsFilter,
740        },
741        rpc_filter::{Memcmp, RpcFilterType},
742        rpc_response::OptionalContext,
743    };
744    use solana_pubkey::Pubkey;
745    use solana_sdk::program_pack::Pack;
746    use spl_token::state::Account as TokenAccount;
747    use surfpool_types::SupplyUpdate;
748
749    use super::{AccountsScan, SurfpoolAccountsScanRpc};
750    use crate::{
751        rpc::surfnet_cheatcodes::{SurfnetCheatcodesRpc, SvmTricksRpc},
752        tests::helpers::TestSetup,
753    };
754
755    const VALID_PUBKEY_1: &str = "11111111111111111111111111111112";
756
757    #[tokio::test(flavor = "multi_thread")]
758    async fn test_get_program_accounts() {
759        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
760
761        // The owner program id that owns the accounts we will query
762        let owner_pubkey = Pubkey::new_unique();
763        // The owned accounts with different data sizes to filter by
764        let owned_pubkey_short_data = Pubkey::new_unique();
765        let owner_pubkey_long_data = Pubkey::new_unique();
766        // Another account that is not owned by the owner program
767        let other_pubkey = Pubkey::new_unique();
768
769        setup.context.svm_locker.with_svm_writer(|svm_writer| {
770            svm_writer
771                .set_account(
772                    &owned_pubkey_short_data,
773                    Account {
774                        lamports: 1000,
775                        data: vec![4, 5, 6],
776                        owner: owner_pubkey,
777                        executable: false,
778                        rent_epoch: 0,
779                    },
780                )
781                .unwrap();
782
783            svm_writer
784                .set_account(
785                    &owner_pubkey_long_data,
786                    Account {
787                        lamports: 2000,
788                        data: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
789                        owner: owner_pubkey,
790                        executable: false,
791                        rent_epoch: 0,
792                    },
793                )
794                .unwrap();
795
796            svm_writer
797                .set_account(
798                    &other_pubkey,
799                    Account {
800                        lamports: 500,
801                        data: vec![4, 5, 6],
802                        owner: Pubkey::new_unique(),
803                        executable: false,
804                        rent_epoch: 0,
805                    },
806                )
807                .unwrap();
808        });
809
810        // Test with no filters
811        {
812            let res = setup
813                .rpc
814                .get_program_accounts(Some(setup.context.clone()), owner_pubkey.to_string(), None)
815                .await
816                .expect("Failed to get program accounts");
817            match res {
818                OptionalContext::Context(_) => {
819                    panic!("Expected no context");
820                }
821                OptionalContext::NoContext(value) => {
822                    assert_eq!(value.len(), 2);
823
824                    let short_data_account = value
825                        .iter()
826                        .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
827                        .expect("Short data account not found");
828                    assert_eq!(short_data_account.account.lamports, 1000);
829
830                    let long_data_account = value
831                        .iter()
832                        .find(|acc| acc.pubkey == owner_pubkey_long_data.to_string())
833                        .expect("Long data account not found");
834                    assert_eq!(long_data_account.account.lamports, 2000);
835                }
836            }
837        }
838
839        // Test with data size filter
840        {
841            let res = setup
842                .rpc
843                .get_program_accounts(
844                    Some(setup.context.clone()),
845                    owner_pubkey.to_string(),
846                    Some(RpcProgramAccountsConfig {
847                        filters: Some(vec![RpcFilterType::DataSize(3)]),
848                        with_context: Some(true),
849                        ..Default::default()
850                    }),
851                )
852                .await
853                .expect("Failed to get program accounts with data size filter");
854
855            match res {
856                OptionalContext::Context(response) => {
857                    assert_eq!(response.value.len(), 1);
858
859                    let short_data_account = response
860                        .value
861                        .iter()
862                        .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
863                        .expect("Short data account not found");
864                    assert_eq!(short_data_account.account.lamports, 1000);
865                }
866                OptionalContext::NoContext(_) => {
867                    panic!("Expected context");
868                }
869            }
870        }
871
872        // Test with memcmp filter
873        {
874            let res = setup
875                .rpc
876                .get_program_accounts(
877                    Some(setup.context.clone()),
878                    owner_pubkey.to_string(),
879                    Some(RpcProgramAccountsConfig {
880                        filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
881                            1,
882                            vec![5, 6],
883                        ))]),
884                        with_context: Some(false),
885                        ..Default::default()
886                    }),
887                )
888                .await
889                .expect("Failed to get program accounts with memcmp filter");
890
891            match res {
892                OptionalContext::Context(_) => {
893                    panic!("Expected no context");
894                }
895                OptionalContext::NoContext(value) => {
896                    assert_eq!(value.len(), 1);
897
898                    let short_data_account = value
899                        .iter()
900                        .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
901                        .expect("Short data account not found");
902                    assert_eq!(short_data_account.account.lamports, 1000);
903                }
904            }
905        }
906    }
907
908    #[tokio::test(flavor = "multi_thread")]
909    async fn test_set_and_get_supply() {
910        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
911        let cheatcodes_rpc = SurfnetCheatcodesRpc;
912
913        // test initial default values
914        let initial_supply = setup
915            .rpc
916            .get_supply(Some(setup.context.clone()), None)
917            .await
918            .unwrap();
919
920        assert_eq!(initial_supply.value.total, 0);
921        assert_eq!(initial_supply.value.circulating, 0);
922        assert_eq!(initial_supply.value.non_circulating, 0);
923        assert_eq!(initial_supply.value.non_circulating_accounts.len(), 0);
924
925        // set supply values using cheatcode
926        let supply_update = SupplyUpdate {
927            total: Some(1_000_000_000_000_000),
928            circulating: Some(800_000_000_000_000),
929            non_circulating: Some(200_000_000_000_000),
930            non_circulating_accounts: Some(vec![
931                VALID_PUBKEY_1.to_string(),
932                VALID_PUBKEY_1.to_string(),
933            ]),
934        };
935
936        let set_result = cheatcodes_rpc
937            .set_supply(Some(setup.context.clone()), supply_update)
938            .await
939            .unwrap();
940
941        assert_eq!(set_result.value, ());
942
943        // verify the values are returned by getSupply
944        let supply = setup
945            .rpc
946            .get_supply(Some(setup.context.clone()), None)
947            .await
948            .unwrap();
949
950        assert_eq!(supply.value.total, 1_000_000_000_000_000);
951        assert_eq!(supply.value.circulating, 800_000_000_000_000);
952        assert_eq!(supply.value.non_circulating, 200_000_000_000_000);
953        assert_eq!(supply.value.non_circulating_accounts.len(), 2);
954        assert_eq!(supply.value.non_circulating_accounts[0], VALID_PUBKEY_1);
955    }
956
957    #[tokio::test(flavor = "multi_thread")]
958    async fn test_get_supply_exclude_accounts() {
959        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
960        let cheatcodes_rpc = SurfnetCheatcodesRpc;
961
962        // set supply with non-circulating accounts
963        let supply_update = SupplyUpdate {
964            total: Some(1_000_000_000_000_000),
965            circulating: Some(800_000_000_000_000),
966            non_circulating: Some(200_000_000_000_000),
967            non_circulating_accounts: Some(vec![
968                VALID_PUBKEY_1.to_string(),
969                VALID_PUBKEY_1.to_string(),
970            ]),
971        };
972
973        cheatcodes_rpc
974            .set_supply(Some(setup.context.clone()), supply_update)
975            .await
976            .unwrap();
977
978        // test with exclude_non_circulating_accounts_list = true
979        let config_exclude = RpcSupplyConfig {
980            commitment: None,
981            exclude_non_circulating_accounts_list: true,
982        };
983
984        let supply_excluded = setup
985            .rpc
986            .get_supply(Some(setup.context.clone()), Some(config_exclude))
987            .await
988            .unwrap();
989
990        assert_eq!(supply_excluded.value.total, 1_000_000_000_000_000);
991        assert_eq!(supply_excluded.value.circulating, 800_000_000_000_000);
992        assert_eq!(supply_excluded.value.non_circulating, 200_000_000_000_000);
993        assert_eq!(supply_excluded.value.non_circulating_accounts.len(), 0); // should be empty
994
995        // test with exclude_non_circulating_accounts_list = false (default)
996        let supply_included = setup
997            .rpc
998            .get_supply(Some(setup.context.clone()), None)
999            .await
1000            .unwrap();
1001
1002        assert_eq!(supply_included.value.non_circulating_accounts.len(), 2);
1003    }
1004
1005    #[tokio::test(flavor = "multi_thread")]
1006    async fn test_partial_supply_update() {
1007        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1008        let cheatcodes_rpc = SurfnetCheatcodesRpc;
1009
1010        // set initial values
1011        let initial_update = SupplyUpdate {
1012            total: Some(1_000_000_000_000_000),
1013            circulating: Some(800_000_000_000_000),
1014            non_circulating: Some(200_000_000_000_000),
1015            non_circulating_accounts: Some(vec![VALID_PUBKEY_1.to_string()]),
1016        };
1017
1018        cheatcodes_rpc
1019            .set_supply(Some(setup.context.clone()), initial_update)
1020            .await
1021            .unwrap();
1022
1023        // update only the total supply
1024        let partial_update = SupplyUpdate {
1025            total: Some(2_000_000_000_000_000),
1026            circulating: None,
1027            non_circulating: None,
1028            non_circulating_accounts: None,
1029        };
1030
1031        cheatcodes_rpc
1032            .set_supply(Some(setup.context.clone()), partial_update)
1033            .await
1034            .unwrap();
1035
1036        // verify only total was updated, others remain the same
1037        let supply = setup
1038            .rpc
1039            .get_supply(Some(setup.context), None)
1040            .await
1041            .unwrap();
1042
1043        assert_eq!(supply.value.total, 2_000_000_000_000_000); // updated
1044        assert_eq!(supply.value.circulating, 800_000_000_000_000);
1045        assert_eq!(supply.value.non_circulating, 200_000_000_000_000);
1046        assert_eq!(supply.value.non_circulating_accounts.len(), 1);
1047    }
1048
1049    #[tokio::test(flavor = "multi_thread")]
1050    async fn test_set_supply_with_multiple_invalid_pubkeys() {
1051        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1052        let cheatcodes_rpc = SurfnetCheatcodesRpc;
1053        let invalid_pubkey = "invalid_pubkey";
1054
1055        // test with multiple invalid pubkeys - should fail on the first one
1056        let supply_update = SupplyUpdate {
1057            total: Some(1_000_000_000_000_000),
1058            circulating: Some(800_000_000_000_000),
1059            non_circulating: Some(200_000_000_000_000),
1060            non_circulating_accounts: Some(vec![
1061                VALID_PUBKEY_1.to_string(), // Valid
1062                invalid_pubkey.to_string(), // Invalid - should fail here
1063                "also_invalid".to_string(), // Also invalid but won't reach here
1064            ]),
1065        };
1066
1067        let result = cheatcodes_rpc
1068            .set_supply(Some(setup.context), supply_update)
1069            .await;
1070
1071        assert!(result.is_err());
1072        let error = result.unwrap_err();
1073        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
1074        assert_eq!(
1075            error.message,
1076            format!("Invalid pubkey '{}' at index 1", invalid_pubkey)
1077        );
1078    }
1079
1080    #[tokio::test(flavor = "multi_thread")]
1081    async fn test_set_supply_with_max_values() {
1082        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1083        let cheatcodes_rpc = SurfnetCheatcodesRpc;
1084
1085        let supply_update = SupplyUpdate {
1086            total: Some(u64::MAX),
1087            circulating: Some(u64::MAX - 1),
1088            non_circulating: Some(1),
1089            non_circulating_accounts: Some(vec![VALID_PUBKEY_1.to_string()]),
1090        };
1091
1092        let result = cheatcodes_rpc
1093            .set_supply(Some(setup.context.clone()), supply_update)
1094            .await;
1095
1096        assert!(result.is_ok());
1097
1098        let supply = setup
1099            .rpc
1100            .get_supply(Some(setup.context), None)
1101            .await
1102            .unwrap();
1103
1104        assert_eq!(supply.value.total, u64::MAX);
1105        assert_eq!(supply.value.circulating, u64::MAX - 1);
1106        assert_eq!(supply.value.non_circulating, 1);
1107        assert_eq!(supply.value.non_circulating_accounts[0], VALID_PUBKEY_1);
1108    }
1109
1110    #[tokio::test(flavor = "multi_thread")]
1111    async fn test_set_supply_large_valid_account_list() {
1112        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1113        let cheatcodes_rpc = SurfnetCheatcodesRpc;
1114
1115        let large_account_list: Vec<String> = (0..100)
1116            .map(|i| match i % 10 {
1117                0 => "3rSZJHysEk2ueFVovRLtZ8LGnQBMZGg96H2Q4jErspAF".to_string(),
1118                1 => "11111111111111111111111111111111".to_string(),
1119                2 => "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(),
1120                3 => "So11111111111111111111111111111111111111112".to_string(),
1121                4 => "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1122                5 => "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB".to_string(),
1123                6 => "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R".to_string(),
1124                7 => "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E".to_string(),
1125                8 => "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk".to_string(),
1126                _ => "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263".to_string(),
1127            })
1128            .collect();
1129
1130        let supply_update = SupplyUpdate {
1131            total: Some(1_000_000_000_000_000),
1132            circulating: Some(800_000_000_000_000),
1133            non_circulating: Some(200_000_000_000_000),
1134            non_circulating_accounts: Some(large_account_list.clone()),
1135        };
1136
1137        let result = cheatcodes_rpc
1138            .set_supply(Some(setup.context.clone()), supply_update)
1139            .await;
1140
1141        assert!(result.is_ok());
1142
1143        let supply = setup
1144            .rpc
1145            .get_supply(Some(setup.context), None)
1146            .await
1147            .unwrap();
1148
1149        assert_eq!(supply.value.non_circulating_accounts.len(), 100);
1150        assert_eq!(
1151            supply.value.non_circulating_accounts[0],
1152            "3rSZJHysEk2ueFVovRLtZ8LGnQBMZGg96H2Q4jErspAF"
1153        );
1154        assert_eq!(
1155            supply.value.non_circulating_accounts[1],
1156            "11111111111111111111111111111111"
1157        );
1158    }
1159
1160    #[tokio::test(flavor = "multi_thread")]
1161    async fn test_get_largest_accounts() {
1162        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1163
1164        let large_circulating_pubkey = Pubkey::new_unique();
1165        let large_circulating_amount = 1_000_000_000_000_000u64;
1166
1167        setup
1168            .context
1169            .svm_locker
1170            .with_svm_writer(|svm_writer| {
1171                svm_writer.set_account(
1172                    &large_circulating_pubkey,
1173                    Account {
1174                        lamports: large_circulating_amount,
1175                        ..Default::default()
1176                    },
1177                )
1178            })
1179            .unwrap();
1180
1181        let large_non_circulating_pubkey = Pubkey::new_unique();
1182        let large_non_circulating_amount = 2_000_000_000_000_000u64;
1183
1184        setup
1185            .context
1186            .svm_locker
1187            .with_svm_writer(|svm_writer| {
1188                svm_writer
1189                    .non_circulating_accounts
1190                    .push(large_non_circulating_pubkey.to_string());
1191                svm_writer.set_account(
1192                    &large_non_circulating_pubkey,
1193                    Account {
1194                        lamports: large_non_circulating_amount,
1195                        ..Default::default()
1196                    },
1197                )
1198            })
1199            .unwrap();
1200
1201        // Test with filter for circulating accounts
1202        {
1203            let result = setup
1204                .rpc
1205                .get_largest_accounts(
1206                    Some(setup.context.clone()),
1207                    Some(RpcLargestAccountsConfig {
1208                        filter: Some(RpcLargestAccountsFilter::Circulating),
1209                        ..Default::default()
1210                    }),
1211                )
1212                .await
1213                .unwrap();
1214
1215            assert_eq!(result.value.len(), 1);
1216            assert_eq!(
1217                large_circulating_pubkey.to_string(),
1218                result.value[0].address
1219            );
1220            assert_eq!(large_circulating_amount, result.value[0].lamports);
1221        }
1222
1223        // Test with filter for non-circulating accounts
1224        {
1225            let result = setup
1226                .rpc
1227                .get_largest_accounts(
1228                    Some(setup.context.clone()),
1229                    Some(RpcLargestAccountsConfig {
1230                        filter: Some(RpcLargestAccountsFilter::NonCirculating),
1231                        ..Default::default()
1232                    }),
1233                )
1234                .await
1235                .unwrap();
1236
1237            assert_eq!(result.value.len(), 1);
1238            assert_eq!(
1239                large_non_circulating_pubkey.to_string(),
1240                result.value[0].address
1241            );
1242            assert_eq!(large_non_circulating_amount, result.value[0].lamports);
1243        }
1244
1245        // Test without filter - should return both accounts
1246        {
1247            let result = setup
1248                .rpc
1249                .get_largest_accounts(Some(setup.context), None)
1250                .await
1251                .unwrap();
1252
1253            assert_eq!(result.value.len(), 2);
1254            assert_eq!(
1255                large_non_circulating_pubkey.to_string(),
1256                result.value[0].address
1257            );
1258            assert_eq!(large_non_circulating_amount, result.value[0].lamports);
1259            assert_eq!(
1260                large_circulating_pubkey.to_string(),
1261                result.value[1].address
1262            );
1263            assert_eq!(large_circulating_amount, result.value[1].lamports);
1264        }
1265    }
1266
1267    #[tokio::test(flavor = "multi_thread")]
1268    async fn test_get_supply_with_invalid_config() {
1269        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1270
1271        let config = RpcSupplyConfig {
1272            commitment: Some(solana_commitment_config::CommitmentConfig {
1273                commitment: solana_commitment_config::CommitmentLevel::Processed,
1274            }),
1275            exclude_non_circulating_accounts_list: false,
1276        };
1277
1278        let result = setup
1279            .rpc
1280            .get_supply(Some(setup.context), Some(config))
1281            .await;
1282
1283        assert!(result.is_ok());
1284        let supply = result.unwrap();
1285        assert_eq!(supply.value.total, 0);
1286        assert_eq!(supply.value.circulating, 0);
1287        assert_eq!(supply.value.non_circulating, 0);
1288    }
1289
1290    #[tokio::test(flavor = "multi_thread")]
1291    async fn test_get_token_largest_accounts_local_svm() {
1292        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1293
1294        // create a mint
1295        let mint_pk = Pubkey::new_unique();
1296        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1297            svm_reader
1298                .inner
1299                .minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)
1300        });
1301
1302        // create mint account
1303        let mut mint_data = [0; spl_token::state::Mint::LEN];
1304        let mint = spl_token::state::Mint {
1305            decimals: 9,
1306            supply: 1000000000000000,
1307            is_initialized: true,
1308            ..Default::default()
1309        };
1310        mint.pack_into_slice(&mut mint_data);
1311
1312        let mint_account = Account {
1313            lamports: minimum_rent,
1314            owner: spl_token::ID,
1315            executable: false,
1316            rent_epoch: 0,
1317            data: mint_data.to_vec(),
1318        };
1319
1320        // create multiple token accounts with different balances
1321        let token_accounts = vec![
1322            (Pubkey::new_unique(), 1000000000), // 1 SOL worth
1323            (Pubkey::new_unique(), 5000000000), // 5 SOL worth (should be first)
1324            (Pubkey::new_unique(), 500000000),  // 0.5 SOL worth
1325            (Pubkey::new_unique(), 2000000000), // 2 SOL worth (should be second)
1326            (Pubkey::new_unique(), 100000000),  // 0.1 SOL worth
1327        ];
1328
1329        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1330            // set the mint account
1331            svm_writer
1332                .set_account(&mint_pk, mint_account.clone())
1333                .unwrap();
1334
1335            // set token accounts
1336            for (token_account_pk, amount) in &token_accounts {
1337                let mut token_account_data = [0; TokenAccount::LEN];
1338                let token_account = TokenAccount {
1339                    mint: mint_pk,
1340                    owner: Pubkey::new_unique(),
1341                    amount: *amount,
1342                    delegate: solana_sdk::program_option::COption::None,
1343                    state: spl_token::state::AccountState::Initialized,
1344                    is_native: solana_sdk::program_option::COption::None,
1345                    delegated_amount: 0,
1346                    close_authority: solana_sdk::program_option::COption::None,
1347                };
1348                token_account.pack_into_slice(&mut token_account_data);
1349
1350                let account = Account {
1351                    lamports: minimum_rent,
1352                    owner: spl_token::ID,
1353                    executable: false,
1354                    rent_epoch: 0,
1355                    data: token_account_data.to_vec(),
1356                };
1357
1358                svm_writer.set_account(token_account_pk, account).unwrap();
1359            }
1360        });
1361
1362        let result = setup
1363            .rpc
1364            .get_token_largest_accounts(Some(setup.context), mint_pk.to_string(), None)
1365            .await
1366            .unwrap();
1367
1368        assert_eq!(result.value.len(), 5);
1369
1370        // should be sorted by balance descending
1371        assert_eq!(result.value[0].amount.amount, "5000000000"); // 5 SOL
1372        assert_eq!(result.value[1].amount.amount, "2000000000"); // 2 SOL
1373        assert_eq!(result.value[2].amount.amount, "1000000000"); // 1 SOL
1374        assert_eq!(result.value[3].amount.amount, "500000000"); // 0.5 SOL
1375        assert_eq!(result.value[4].amount.amount, "100000000"); // 0.1 SOL
1376
1377        // verify decimals and UI amounts
1378        for balance in &result.value {
1379            assert_eq!(balance.amount.decimals, 9);
1380            assert!(balance.amount.ui_amount.is_some());
1381            assert!(!balance.amount.ui_amount_string.is_empty());
1382        }
1383
1384        // Verify addresses are valid pubkeys
1385        for balance in &result.value {
1386            assert!(Pubkey::from_str(&balance.address).is_ok());
1387        }
1388    }
1389
1390    #[tokio::test(flavor = "multi_thread")]
1391    async fn test_get_token_largest_accounts_limit_to_20() {
1392        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1393
1394        // Create a mint
1395        let mint_pk = Pubkey::new_unique();
1396        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1397            svm_reader
1398                .inner
1399                .minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)
1400        });
1401
1402        // Create mint account
1403        let mut mint_data = [0; spl_token::state::Mint::LEN];
1404        let mint = spl_token::state::Mint {
1405            decimals: 6,
1406            supply: 1000000000000000,
1407            is_initialized: true,
1408            ..Default::default()
1409        };
1410        mint.pack_into_slice(&mut mint_data);
1411
1412        let mint_account = Account {
1413            lamports: minimum_rent,
1414            owner: spl_token::ID,
1415            executable: false,
1416            rent_epoch: 0,
1417            data: mint_data.to_vec(),
1418        };
1419
1420        // Create 25 token accounts (more than the 20 limit)
1421        let mut token_accounts = Vec::new();
1422        for i in 0..25 {
1423            token_accounts.push((Pubkey::new_unique(), (i + 1) * 1000000)); // Varying amounts
1424        }
1425
1426        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1427            // Set the mint account
1428            svm_writer
1429                .set_account(&mint_pk, mint_account.clone())
1430                .unwrap();
1431
1432            // Set token accounts
1433            for (token_account_pk, amount) in &token_accounts {
1434                let mut token_account_data = [0; TokenAccount::LEN];
1435                let token_account = TokenAccount {
1436                    mint: mint_pk,
1437                    owner: Pubkey::new_unique(),
1438                    amount: *amount,
1439                    delegate: solana_sdk::program_option::COption::None,
1440                    state: spl_token::state::AccountState::Initialized,
1441                    is_native: solana_sdk::program_option::COption::None,
1442                    delegated_amount: 0,
1443                    close_authority: solana_sdk::program_option::COption::None,
1444                };
1445                token_account.pack_into_slice(&mut token_account_data);
1446
1447                let account = Account {
1448                    lamports: minimum_rent,
1449                    owner: spl_token::ID,
1450                    executable: false,
1451                    rent_epoch: 0,
1452                    data: token_account_data.to_vec(),
1453                };
1454
1455                svm_writer.set_account(token_account_pk, account).unwrap();
1456            }
1457        });
1458
1459        // Call get_token_largest_accounts
1460        let result = setup
1461            .rpc
1462            .get_token_largest_accounts(Some(setup.context), mint_pk.to_string(), None)
1463            .await
1464            .unwrap();
1465
1466        // Should be limited to 20 accounts
1467        assert_eq!(result.value.len(), 20);
1468
1469        // Should be sorted by balance descending (highest amounts first)
1470        assert_eq!(result.value[0].amount.amount, "25000000"); // Highest amount
1471        assert_eq!(result.value[1].amount.amount, "24000000"); // Second highest
1472        assert_eq!(result.value[19].amount.amount, "6000000"); // 20th highest
1473
1474        // Verify all are properly formatted
1475        for balance in &result.value {
1476            assert_eq!(balance.amount.decimals, 6);
1477            assert!(balance.amount.ui_amount.is_some());
1478            assert!(!balance.amount.ui_amount_string.is_empty());
1479            assert!(Pubkey::from_str(&balance.address).is_ok());
1480        }
1481    }
1482
1483    #[tokio::test(flavor = "multi_thread")]
1484    async fn test_get_token_largest_accounts_edge_cases() {
1485        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1486
1487        // Test 1: Invalid mint pubkey
1488        let invalid_result = setup
1489            .rpc
1490            .get_token_largest_accounts(
1491                Some(setup.context.clone()),
1492                "invalid_pubkey".to_string(),
1493                None,
1494            )
1495            .await;
1496        assert!(invalid_result.is_err());
1497        let error = invalid_result.unwrap_err();
1498        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
1499
1500        // Test 2: Valid mint but no token accounts
1501        let empty_mint_pk = Pubkey::new_unique();
1502        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1503            svm_reader
1504                .inner
1505                .minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)
1506        });
1507
1508        // Create mint account with no associated token accounts
1509        let mut mint_data = [0; spl_token::state::Mint::LEN];
1510        let mint = spl_token::state::Mint {
1511            decimals: 9,
1512            supply: 0,
1513            is_initialized: true,
1514            ..Default::default()
1515        };
1516        mint.pack_into_slice(&mut mint_data);
1517
1518        let mint_account = Account {
1519            lamports: minimum_rent,
1520            owner: spl_token::ID,
1521            executable: false,
1522            rent_epoch: 0,
1523            data: mint_data.to_vec(),
1524        };
1525
1526        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1527            svm_writer
1528                .set_account(&empty_mint_pk, mint_account.clone())
1529                .unwrap();
1530        });
1531
1532        let empty_result = setup
1533            .rpc
1534            .get_token_largest_accounts(
1535                Some(setup.context.clone()),
1536                empty_mint_pk.to_string(),
1537                None,
1538            )
1539            .await
1540            .unwrap();
1541
1542        // Should return empty array
1543        assert_eq!(empty_result.value.len(), 0);
1544
1545        // Test 3: Mint that doesn't exist at all
1546        let nonexistent_mint_pk = Pubkey::new_unique();
1547        let nonexistent_result = setup
1548            .rpc
1549            .get_token_largest_accounts(Some(setup.context), nonexistent_mint_pk.to_string(), None)
1550            .await
1551            .unwrap();
1552
1553        // Should return empty array (no token accounts for nonexistent mint)
1554        assert_eq!(nonexistent_result.value.len(), 0);
1555    }
1556
1557    #[tokio::test(flavor = "multi_thread")]
1558    async fn test_get_token_accounts_by_delegate() {
1559        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1560
1561        let delegate = Pubkey::new_unique();
1562        let owner = Pubkey::new_unique();
1563        let mint = Pubkey::new_unique();
1564        let token_account_pubkey = Pubkey::new_unique();
1565        let token_program = spl_token::id();
1566
1567        // create a token account with delegate
1568        let mut token_account_data = [0u8; spl_token::state::Account::LEN];
1569        let token_account = spl_token::state::Account {
1570            mint,
1571            owner,
1572            amount: 1000,
1573            delegate: solana_sdk::program_option::COption::Some(delegate),
1574            state: spl_token::state::AccountState::Initialized,
1575            is_native: solana_sdk::program_option::COption::None,
1576            delegated_amount: 500,
1577            close_authority: solana_sdk::program_option::COption::None,
1578        };
1579        solana_sdk::program_pack::Pack::pack_into_slice(&token_account, &mut token_account_data);
1580
1581        let account = Account {
1582            lamports: 1000000,
1583            data: token_account_data.to_vec(),
1584            owner: token_program,
1585            executable: false,
1586            rent_epoch: 0,
1587        };
1588
1589        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1590            svm_writer
1591                .set_account(&token_account_pubkey, account.clone())
1592                .unwrap();
1593        });
1594
1595        // programId filter - should find the account
1596        let result = setup
1597            .rpc
1598            .get_token_accounts_by_delegate(
1599                Some(setup.context.clone()),
1600                delegate.to_string(),
1601                RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1602                None,
1603            )
1604            .await;
1605
1606        assert!(result.is_ok(), "ProgramId filter should succeed");
1607        let response = result.unwrap();
1608        assert_eq!(response.value.len(), 1, "Should find 1 token account");
1609        assert_eq!(response.value[0].pubkey, token_account_pubkey.to_string());
1610
1611        // mint filter - should find the account
1612        let result = setup
1613            .rpc
1614            .get_token_accounts_by_delegate(
1615                Some(setup.context.clone()),
1616                delegate.to_string(),
1617                RpcTokenAccountsFilter::Mint(mint.to_string()),
1618                None,
1619            )
1620            .await;
1621
1622        assert!(result.is_ok(), "Mint filter should succeed");
1623        let response = result.unwrap();
1624        assert_eq!(response.value.len(), 1, "Should find 1 token account");
1625        assert_eq!(response.value[0].pubkey, token_account_pubkey.to_string());
1626
1627        // non-existent delegate - should return empty
1628        let non_existent_delegate = Pubkey::new_unique();
1629        let result = setup
1630            .rpc
1631            .get_token_accounts_by_delegate(
1632                Some(setup.context.clone()),
1633                non_existent_delegate.to_string(),
1634                RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1635                None,
1636            )
1637            .await;
1638
1639        assert!(result.is_ok(), "Non-existent delegate should succeed");
1640        let response = result.unwrap();
1641        assert_eq!(response.value.len(), 0, "Should find 0 token accounts");
1642
1643        // wrong mint - should return empty
1644        let wrong_mint = Pubkey::new_unique();
1645        let result = setup
1646            .rpc
1647            .get_token_accounts_by_delegate(
1648                Some(setup.context.clone()),
1649                delegate.to_string(),
1650                RpcTokenAccountsFilter::Mint(wrong_mint.to_string()),
1651                None,
1652            )
1653            .await;
1654
1655        assert!(result.is_ok(), "Wrong mint should succeed");
1656        let response = result.unwrap();
1657        assert_eq!(response.value.len(), 0, "Should find 0 token accounts");
1658
1659        // invalid delegate pubkey - should fail
1660        let result = setup
1661            .rpc
1662            .get_token_accounts_by_delegate(
1663                Some(setup.context.clone()),
1664                "invalid_pubkey".to_string(),
1665                RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1666                None,
1667            )
1668            .await;
1669
1670        assert!(result.is_err(), "Invalid pubkey should fail");
1671    }
1672
1673    #[tokio::test(flavor = "multi_thread")]
1674    async fn test_get_token_accounts_by_delegate_multiple_accounts() {
1675        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1676
1677        let delegate = Pubkey::new_unique();
1678        let owner1 = Pubkey::new_unique();
1679        let owner2 = Pubkey::new_unique();
1680        let mint1 = Pubkey::new_unique();
1681        let mint2 = Pubkey::new_unique();
1682        let token_account1 = Pubkey::new_unique();
1683        let token_account2 = Pubkey::new_unique();
1684        let token_program = spl_token::id();
1685
1686        // create first token account with delegate
1687        let mut token_account_data1 = [0u8; spl_token::state::Account::LEN];
1688        let token_account_struct1 = spl_token::state::Account {
1689            mint: mint1,
1690            owner: owner1,
1691            amount: 1000,
1692            delegate: solana_sdk::program_option::COption::Some(delegate),
1693            state: spl_token::state::AccountState::Initialized,
1694            is_native: solana_sdk::program_option::COption::None,
1695            delegated_amount: 500,
1696            close_authority: solana_sdk::program_option::COption::None,
1697        };
1698        solana_sdk::program_pack::Pack::pack_into_slice(
1699            &token_account_struct1,
1700            &mut token_account_data1,
1701        );
1702
1703        // create second token account with same delegate
1704        let mut token_account_data2 = [0u8; spl_token::state::Account::LEN];
1705        let token_account_struct2 = spl_token::state::Account {
1706            mint: mint2,
1707            owner: owner2,
1708            amount: 2000,
1709            delegate: solana_sdk::program_option::COption::Some(delegate),
1710            state: spl_token::state::AccountState::Initialized,
1711            is_native: solana_sdk::program_option::COption::None,
1712            delegated_amount: 1000,
1713            close_authority: solana_sdk::program_option::COption::None,
1714        };
1715        solana_sdk::program_pack::Pack::pack_into_slice(
1716            &token_account_struct2,
1717            &mut token_account_data2,
1718        );
1719
1720        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1721            svm_writer
1722                .set_account(
1723                    &token_account1,
1724                    Account {
1725                        lamports: 1000000,
1726                        data: token_account_data1.to_vec(),
1727                        owner: token_program,
1728                        executable: false,
1729                        rent_epoch: 0,
1730                    },
1731                )
1732                .unwrap();
1733
1734            svm_writer
1735                .set_account(
1736                    &token_account2,
1737                    Account {
1738                        lamports: 1000000,
1739                        data: token_account_data2.to_vec(),
1740                        owner: token_program,
1741                        executable: false,
1742                        rent_epoch: 0,
1743                    },
1744                )
1745                .unwrap();
1746        });
1747
1748        let result = setup
1749            .rpc
1750            .get_token_accounts_by_delegate(
1751                Some(setup.context.clone()),
1752                delegate.to_string(),
1753                RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1754                None,
1755            )
1756            .await;
1757
1758        assert!(result.is_ok(), "ProgramId filter should succeed");
1759        let response = result.unwrap();
1760        assert_eq!(response.value.len(), 2, "Should find 2 token accounts");
1761
1762        let returned_pubkeys: std::collections::HashSet<String> = response
1763            .value
1764            .iter()
1765            .map(|acc| acc.pubkey.clone())
1766            .collect();
1767        assert!(returned_pubkeys.contains(&token_account1.to_string()));
1768        assert!(returned_pubkeys.contains(&token_account2.to_string()));
1769
1770        // Test: Mint filter for mint1 - should find only first account
1771        let result = setup
1772            .rpc
1773            .get_token_accounts_by_delegate(
1774                Some(setup.context.clone()),
1775                delegate.to_string(),
1776                RpcTokenAccountsFilter::Mint(mint1.to_string()),
1777                None,
1778            )
1779            .await;
1780
1781        assert!(result.is_ok(), "Mint filter should succeed");
1782        let response = result.unwrap();
1783        assert_eq!(
1784            response.value.len(),
1785            1,
1786            "Should find 1 token account for mint1"
1787        );
1788        assert_eq!(response.value[0].pubkey, token_account1.to_string());
1789    }
1790}