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::{RunloopContext, State, SurfnetRpcContext, utils::verify_pubkey};
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 SvmAccessContext {
537                slot,
538                inner: largest_accounts,
539                ..
540            } = svm_locker.get_largest_accounts(&remote_ctx, config).await?;
541
542            Ok(RpcResponse {
543                context: RpcResponseContext::new(slot),
544                value: largest_accounts,
545            })
546        })
547    }
548
549    fn get_supply(
550        &self,
551        meta: Self::Metadata,
552        config: Option<RpcSupplyConfig>,
553    ) -> BoxFuture<Result<RpcResponse<RpcSupply>>> {
554        let svm_locker = match meta.get_svm_locker() {
555            Ok(locker) => locker,
556            Err(e) => return e.into(),
557        };
558
559        Box::pin(async move {
560            svm_locker.with_svm_reader(|svm_reader| {
561                let slot = svm_reader.get_latest_absolute_slot();
562
563                // Check if we should exclude non-circulating accounts list
564                let exclude_accounts = config
565                    .as_ref()
566                    .map(|c| c.exclude_non_circulating_accounts_list)
567                    .unwrap_or(false);
568
569                Ok(RpcResponse {
570                    context: RpcResponseContext::new(slot),
571                    value: RpcSupply {
572                        total: svm_reader.total_supply,
573                        circulating: svm_reader.circulating_supply,
574                        non_circulating: svm_reader.non_circulating_supply,
575                        non_circulating_accounts: if exclude_accounts {
576                            vec![]
577                        } else {
578                            svm_reader.non_circulating_accounts.clone()
579                        },
580                    },
581                })
582            })
583        })
584    }
585
586    fn get_token_largest_accounts(
587        &self,
588        meta: Self::Metadata,
589        mint_str: String,
590        commitment: Option<CommitmentConfig>,
591    ) -> BoxFuture<Result<RpcResponse<Vec<RpcTokenAccountBalance>>>> {
592        let mint = match verify_pubkey(&mint_str) {
593            Ok(res) => res,
594            Err(e) => return e.into(),
595        };
596
597        let SurfnetRpcContext {
598            svm_locker,
599            remote_ctx,
600        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
601            Ok(res) => res,
602            Err(e) => return e.into(),
603        };
604
605        Box::pin(async move {
606            let SvmAccessContext {
607                slot,
608                inner: largest_accounts,
609                ..
610            } = svm_locker
611                .get_token_largest_accounts(&remote_ctx, &mint)
612                .await?;
613
614            Ok(RpcResponse {
615                context: RpcResponseContext::new(slot),
616                value: largest_accounts,
617            })
618        })
619    }
620
621    fn get_token_accounts_by_owner(
622        &self,
623        meta: Self::Metadata,
624        owner_str: String,
625        token_account_filter: RpcTokenAccountsFilter,
626        config: Option<RpcAccountInfoConfig>,
627    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>> {
628        let config = config.unwrap_or_default();
629        let owner = match verify_pubkey(&owner_str) {
630            Ok(res) => res,
631            Err(e) => return e.into(),
632        };
633
634        let filter = match token_account_filter {
635            RpcTokenAccountsFilter::Mint(mint_str) => {
636                let mint = match verify_pubkey(&mint_str) {
637                    Ok(res) => res,
638                    Err(e) => return e.into(),
639                };
640                TokenAccountsFilter::Mint(mint)
641            }
642            RpcTokenAccountsFilter::ProgramId(program_id_str) => {
643                let program_id = match verify_pubkey(&program_id_str) {
644                    Ok(res) => res,
645                    Err(e) => return e.into(),
646                };
647                TokenAccountsFilter::ProgramId(program_id)
648            }
649        };
650
651        let SurfnetRpcContext {
652            svm_locker,
653            remote_ctx,
654        } = match meta.get_rpc_context(()) {
655            Ok(res) => res,
656            Err(e) => return e.into(),
657        };
658
659        Box::pin(async move {
660            let SvmAccessContext {
661                slot,
662                inner: token_accounts,
663                ..
664            } = svm_locker
665                .get_token_accounts_by_owner(
666                    &remote_ctx.map(|(client, _)| client),
667                    owner,
668                    &filter,
669                    &config,
670                )
671                .await?;
672
673            Ok(RpcResponse {
674                context: RpcResponseContext::new(slot),
675                value: token_accounts,
676            })
677        })
678    }
679
680    fn get_token_accounts_by_delegate(
681        &self,
682        meta: Self::Metadata,
683        delegate_str: String,
684        token_account_filter: RpcTokenAccountsFilter,
685        config: Option<RpcAccountInfoConfig>,
686    ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>> {
687        let config = config.unwrap_or_default();
688        let delegate = match verify_pubkey(&delegate_str) {
689            Ok(res) => res,
690            Err(e) => return e.into(),
691        };
692
693        let SurfnetRpcContext {
694            svm_locker,
695            remote_ctx,
696        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
697            Ok(res) => res,
698            Err(e) => return e.into(),
699        };
700
701        Box::pin(async move {
702            let filter = match token_account_filter {
703                RpcTokenAccountsFilter::Mint(mint_str) => {
704                    TokenAccountsFilter::Mint(verify_pubkey(&mint_str)?)
705                }
706                RpcTokenAccountsFilter::ProgramId(program_id_str) => {
707                    TokenAccountsFilter::ProgramId(verify_pubkey(&program_id_str)?)
708                }
709            };
710
711            let remote_ctx = remote_ctx.map(|(r, _)| r);
712            let SvmAccessContext {
713                slot,
714                inner: keyed_accounts,
715                ..
716            } = svm_locker
717                .get_token_accounts_by_delegate(&remote_ctx, delegate, &filter, &config)
718                .await?;
719
720            Ok(RpcResponse {
721                context: RpcResponseContext::new(slot),
722                value: keyed_accounts,
723            })
724        })
725    }
726}
727
728#[cfg(test)]
729mod tests {
730
731    use core::panic;
732    use std::str::FromStr;
733
734    use solana_account::Account;
735    use solana_client::{
736        rpc_config::{
737            RpcLargestAccountsConfig, RpcLargestAccountsFilter, RpcProgramAccountsConfig,
738            RpcSupplyConfig, RpcTokenAccountsFilter,
739        },
740        rpc_filter::{Memcmp, RpcFilterType},
741        rpc_response::OptionalContext,
742    };
743    use solana_pubkey::Pubkey;
744    use solana_sdk::program_pack::Pack;
745    use spl_token::state::Account as TokenAccount;
746    use surfpool_types::SupplyUpdate;
747
748    use super::{AccountsScan, SurfpoolAccountsScanRpc};
749    use crate::{
750        rpc::surfnet_cheatcodes::{SurfnetCheatcodesRpc, SvmTricksRpc},
751        tests::helpers::TestSetup,
752    };
753
754    const VALID_PUBKEY_1: &str = "11111111111111111111111111111112";
755
756    #[tokio::test(flavor = "multi_thread")]
757    async fn test_get_program_accounts() {
758        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
759
760        // The owner program id that owns the accounts we will query
761        let owner_pubkey = Pubkey::new_unique();
762        // The owned accounts with different data sizes to filter by
763        let owned_pubkey_short_data = Pubkey::new_unique();
764        let owner_pubkey_long_data = Pubkey::new_unique();
765        // Another account that is not owned by the owner program
766        let other_pubkey = Pubkey::new_unique();
767
768        setup.context.svm_locker.with_svm_writer(|svm_writer| {
769            svm_writer
770                .set_account(
771                    &owned_pubkey_short_data,
772                    Account {
773                        lamports: 1000,
774                        data: vec![4, 5, 6],
775                        owner: owner_pubkey,
776                        executable: false,
777                        rent_epoch: 0,
778                    },
779                )
780                .unwrap();
781
782            svm_writer
783                .set_account(
784                    &owner_pubkey_long_data,
785                    Account {
786                        lamports: 2000,
787                        data: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
788                        owner: owner_pubkey,
789                        executable: false,
790                        rent_epoch: 0,
791                    },
792                )
793                .unwrap();
794
795            svm_writer
796                .set_account(
797                    &other_pubkey,
798                    Account {
799                        lamports: 500,
800                        data: vec![4, 5, 6],
801                        owner: Pubkey::new_unique(),
802                        executable: false,
803                        rent_epoch: 0,
804                    },
805                )
806                .unwrap();
807        });
808
809        // Test with no filters
810        {
811            let res = setup
812                .rpc
813                .get_program_accounts(Some(setup.context.clone()), owner_pubkey.to_string(), None)
814                .await
815                .expect("Failed to get program accounts");
816            match res {
817                OptionalContext::Context(_) => {
818                    panic!("Expected no context");
819                }
820                OptionalContext::NoContext(value) => {
821                    assert_eq!(value.len(), 2);
822
823                    let short_data_account = value
824                        .iter()
825                        .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
826                        .expect("Short data account not found");
827                    assert_eq!(short_data_account.account.lamports, 1000);
828
829                    let long_data_account = value
830                        .iter()
831                        .find(|acc| acc.pubkey == owner_pubkey_long_data.to_string())
832                        .expect("Long data account not found");
833                    assert_eq!(long_data_account.account.lamports, 2000);
834                }
835            }
836        }
837
838        // Test with data size filter
839        {
840            let res = setup
841                .rpc
842                .get_program_accounts(
843                    Some(setup.context.clone()),
844                    owner_pubkey.to_string(),
845                    Some(RpcProgramAccountsConfig {
846                        filters: Some(vec![RpcFilterType::DataSize(3)]),
847                        with_context: Some(true),
848                        ..Default::default()
849                    }),
850                )
851                .await
852                .expect("Failed to get program accounts with data size filter");
853
854            match res {
855                OptionalContext::Context(response) => {
856                    assert_eq!(response.value.len(), 1);
857
858                    let short_data_account = response
859                        .value
860                        .iter()
861                        .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
862                        .expect("Short data account not found");
863                    assert_eq!(short_data_account.account.lamports, 1000);
864                }
865                OptionalContext::NoContext(_) => {
866                    panic!("Expected context");
867                }
868            }
869        }
870
871        // Test with memcmp filter
872        {
873            let res = setup
874                .rpc
875                .get_program_accounts(
876                    Some(setup.context.clone()),
877                    owner_pubkey.to_string(),
878                    Some(RpcProgramAccountsConfig {
879                        filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
880                            1,
881                            vec![5, 6],
882                        ))]),
883                        with_context: Some(false),
884                        ..Default::default()
885                    }),
886                )
887                .await
888                .expect("Failed to get program accounts with memcmp filter");
889
890            match res {
891                OptionalContext::Context(_) => {
892                    panic!("Expected no context");
893                }
894                OptionalContext::NoContext(value) => {
895                    assert_eq!(value.len(), 1);
896
897                    let short_data_account = value
898                        .iter()
899                        .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
900                        .expect("Short data account not found");
901                    assert_eq!(short_data_account.account.lamports, 1000);
902                }
903            }
904        }
905    }
906
907    #[tokio::test(flavor = "multi_thread")]
908    async fn test_set_and_get_supply() {
909        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
910        let cheatcodes_rpc = SurfnetCheatcodesRpc;
911
912        // test initial default values
913        let initial_supply = setup
914            .rpc
915            .get_supply(Some(setup.context.clone()), None)
916            .await
917            .unwrap();
918
919        assert_eq!(initial_supply.value.total, 0);
920        assert_eq!(initial_supply.value.circulating, 0);
921        assert_eq!(initial_supply.value.non_circulating, 0);
922        assert_eq!(initial_supply.value.non_circulating_accounts.len(), 0);
923
924        // set supply values using cheatcode
925        let supply_update = SupplyUpdate {
926            total: Some(1_000_000_000_000_000),
927            circulating: Some(800_000_000_000_000),
928            non_circulating: Some(200_000_000_000_000),
929            non_circulating_accounts: Some(vec![
930                VALID_PUBKEY_1.to_string(),
931                VALID_PUBKEY_1.to_string(),
932            ]),
933        };
934
935        let set_result = cheatcodes_rpc
936            .set_supply(Some(setup.context.clone()), supply_update)
937            .await
938            .unwrap();
939
940        assert_eq!(set_result.value, ());
941
942        // verify the values are returned by getSupply
943        let supply = setup
944            .rpc
945            .get_supply(Some(setup.context.clone()), None)
946            .await
947            .unwrap();
948
949        assert_eq!(supply.value.total, 1_000_000_000_000_000);
950        assert_eq!(supply.value.circulating, 800_000_000_000_000);
951        assert_eq!(supply.value.non_circulating, 200_000_000_000_000);
952        assert_eq!(supply.value.non_circulating_accounts.len(), 2);
953        assert_eq!(supply.value.non_circulating_accounts[0], VALID_PUBKEY_1);
954    }
955
956    #[tokio::test(flavor = "multi_thread")]
957    async fn test_get_supply_exclude_accounts() {
958        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
959        let cheatcodes_rpc = SurfnetCheatcodesRpc;
960
961        // set supply with non-circulating accounts
962        let supply_update = SupplyUpdate {
963            total: Some(1_000_000_000_000_000),
964            circulating: Some(800_000_000_000_000),
965            non_circulating: Some(200_000_000_000_000),
966            non_circulating_accounts: Some(vec![
967                VALID_PUBKEY_1.to_string(),
968                VALID_PUBKEY_1.to_string(),
969            ]),
970        };
971
972        cheatcodes_rpc
973            .set_supply(Some(setup.context.clone()), supply_update)
974            .await
975            .unwrap();
976
977        // test with exclude_non_circulating_accounts_list = true
978        let config_exclude = RpcSupplyConfig {
979            commitment: None,
980            exclude_non_circulating_accounts_list: true,
981        };
982
983        let supply_excluded = setup
984            .rpc
985            .get_supply(Some(setup.context.clone()), Some(config_exclude))
986            .await
987            .unwrap();
988
989        assert_eq!(supply_excluded.value.total, 1_000_000_000_000_000);
990        assert_eq!(supply_excluded.value.circulating, 800_000_000_000_000);
991        assert_eq!(supply_excluded.value.non_circulating, 200_000_000_000_000);
992        assert_eq!(supply_excluded.value.non_circulating_accounts.len(), 0); // should be empty
993
994        // test with exclude_non_circulating_accounts_list = false (default)
995        let supply_included = setup
996            .rpc
997            .get_supply(Some(setup.context.clone()), None)
998            .await
999            .unwrap();
1000
1001        assert_eq!(supply_included.value.non_circulating_accounts.len(), 2);
1002    }
1003
1004    #[tokio::test(flavor = "multi_thread")]
1005    async fn test_partial_supply_update() {
1006        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1007        let cheatcodes_rpc = SurfnetCheatcodesRpc;
1008
1009        // set initial values
1010        let initial_update = SupplyUpdate {
1011            total: Some(1_000_000_000_000_000),
1012            circulating: Some(800_000_000_000_000),
1013            non_circulating: Some(200_000_000_000_000),
1014            non_circulating_accounts: Some(vec![VALID_PUBKEY_1.to_string()]),
1015        };
1016
1017        cheatcodes_rpc
1018            .set_supply(Some(setup.context.clone()), initial_update)
1019            .await
1020            .unwrap();
1021
1022        // update only the total supply
1023        let partial_update = SupplyUpdate {
1024            total: Some(2_000_000_000_000_000),
1025            circulating: None,
1026            non_circulating: None,
1027            non_circulating_accounts: None,
1028        };
1029
1030        cheatcodes_rpc
1031            .set_supply(Some(setup.context.clone()), partial_update)
1032            .await
1033            .unwrap();
1034
1035        // verify only total was updated, others remain the same
1036        let supply = setup
1037            .rpc
1038            .get_supply(Some(setup.context), None)
1039            .await
1040            .unwrap();
1041
1042        assert_eq!(supply.value.total, 2_000_000_000_000_000); // updated
1043        assert_eq!(supply.value.circulating, 800_000_000_000_000);
1044        assert_eq!(supply.value.non_circulating, 200_000_000_000_000);
1045        assert_eq!(supply.value.non_circulating_accounts.len(), 1);
1046    }
1047
1048    #[tokio::test(flavor = "multi_thread")]
1049    async fn test_set_supply_with_multiple_invalid_pubkeys() {
1050        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1051        let cheatcodes_rpc = SurfnetCheatcodesRpc;
1052        let invalid_pubkey = "invalid_pubkey";
1053
1054        // test with multiple invalid pubkeys - should fail on the first one
1055        let supply_update = SupplyUpdate {
1056            total: Some(1_000_000_000_000_000),
1057            circulating: Some(800_000_000_000_000),
1058            non_circulating: Some(200_000_000_000_000),
1059            non_circulating_accounts: Some(vec![
1060                VALID_PUBKEY_1.to_string(), // Valid
1061                invalid_pubkey.to_string(), // Invalid - should fail here
1062                "also_invalid".to_string(), // Also invalid but won't reach here
1063            ]),
1064        };
1065
1066        let result = cheatcodes_rpc
1067            .set_supply(Some(setup.context), supply_update)
1068            .await;
1069
1070        assert!(result.is_err());
1071        let error = result.unwrap_err();
1072        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
1073        assert_eq!(
1074            error.message,
1075            format!("Invalid pubkey '{}' at index 1", invalid_pubkey)
1076        );
1077    }
1078
1079    #[tokio::test(flavor = "multi_thread")]
1080    async fn test_set_supply_with_max_values() {
1081        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1082        let cheatcodes_rpc = SurfnetCheatcodesRpc;
1083
1084        let supply_update = SupplyUpdate {
1085            total: Some(u64::MAX),
1086            circulating: Some(u64::MAX - 1),
1087            non_circulating: Some(1),
1088            non_circulating_accounts: Some(vec![VALID_PUBKEY_1.to_string()]),
1089        };
1090
1091        let result = cheatcodes_rpc
1092            .set_supply(Some(setup.context.clone()), supply_update)
1093            .await;
1094
1095        assert!(result.is_ok());
1096
1097        let supply = setup
1098            .rpc
1099            .get_supply(Some(setup.context), None)
1100            .await
1101            .unwrap();
1102
1103        assert_eq!(supply.value.total, u64::MAX);
1104        assert_eq!(supply.value.circulating, u64::MAX - 1);
1105        assert_eq!(supply.value.non_circulating, 1);
1106        assert_eq!(supply.value.non_circulating_accounts[0], VALID_PUBKEY_1);
1107    }
1108
1109    #[tokio::test(flavor = "multi_thread")]
1110    async fn test_set_supply_large_valid_account_list() {
1111        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1112        let cheatcodes_rpc = SurfnetCheatcodesRpc;
1113
1114        let large_account_list: Vec<String> = (0..100)
1115            .map(|i| match i % 10 {
1116                0 => "3rSZJHysEk2ueFVovRLtZ8LGnQBMZGg96H2Q4jErspAF".to_string(),
1117                1 => "11111111111111111111111111111111".to_string(),
1118                2 => "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(),
1119                3 => "So11111111111111111111111111111111111111112".to_string(),
1120                4 => "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1121                5 => "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB".to_string(),
1122                6 => "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R".to_string(),
1123                7 => "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E".to_string(),
1124                8 => "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk".to_string(),
1125                _ => "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263".to_string(),
1126            })
1127            .collect();
1128
1129        let supply_update = SupplyUpdate {
1130            total: Some(1_000_000_000_000_000),
1131            circulating: Some(800_000_000_000_000),
1132            non_circulating: Some(200_000_000_000_000),
1133            non_circulating_accounts: Some(large_account_list.clone()),
1134        };
1135
1136        let result = cheatcodes_rpc
1137            .set_supply(Some(setup.context.clone()), supply_update)
1138            .await;
1139
1140        assert!(result.is_ok());
1141
1142        let supply = setup
1143            .rpc
1144            .get_supply(Some(setup.context), None)
1145            .await
1146            .unwrap();
1147
1148        assert_eq!(supply.value.non_circulating_accounts.len(), 100);
1149        assert_eq!(
1150            supply.value.non_circulating_accounts[0],
1151            "3rSZJHysEk2ueFVovRLtZ8LGnQBMZGg96H2Q4jErspAF"
1152        );
1153        assert_eq!(
1154            supply.value.non_circulating_accounts[1],
1155            "11111111111111111111111111111111"
1156        );
1157    }
1158
1159    #[tokio::test(flavor = "multi_thread")]
1160    async fn test_get_largest_accounts() {
1161        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1162
1163        let large_circulating_pubkey = Pubkey::new_unique();
1164        let large_circulating_amount = 1_000_000_000_000_000u64;
1165
1166        setup
1167            .context
1168            .svm_locker
1169            .with_svm_writer(|svm_writer| {
1170                svm_writer.set_account(
1171                    &large_circulating_pubkey,
1172                    Account {
1173                        lamports: large_circulating_amount,
1174                        ..Default::default()
1175                    },
1176                )
1177            })
1178            .unwrap();
1179
1180        let large_non_circulating_pubkey = Pubkey::new_unique();
1181        let large_non_circulating_amount = 2_000_000_000_000_000u64;
1182
1183        setup
1184            .context
1185            .svm_locker
1186            .with_svm_writer(|svm_writer| {
1187                svm_writer
1188                    .non_circulating_accounts
1189                    .push(large_non_circulating_pubkey.to_string());
1190                svm_writer.set_account(
1191                    &large_non_circulating_pubkey,
1192                    Account {
1193                        lamports: large_non_circulating_amount,
1194                        ..Default::default()
1195                    },
1196                )
1197            })
1198            .unwrap();
1199
1200        // Test with filter for circulating accounts
1201        {
1202            let result = setup
1203                .rpc
1204                .get_largest_accounts(
1205                    Some(setup.context.clone()),
1206                    Some(RpcLargestAccountsConfig {
1207                        filter: Some(RpcLargestAccountsFilter::Circulating),
1208                        ..Default::default()
1209                    }),
1210                )
1211                .await
1212                .unwrap();
1213
1214            assert_eq!(result.value.len(), 1);
1215            assert_eq!(
1216                large_circulating_pubkey.to_string(),
1217                result.value[0].address
1218            );
1219            assert_eq!(large_circulating_amount, result.value[0].lamports);
1220        }
1221
1222        // Test with filter for non-circulating accounts
1223        {
1224            let result = setup
1225                .rpc
1226                .get_largest_accounts(
1227                    Some(setup.context.clone()),
1228                    Some(RpcLargestAccountsConfig {
1229                        filter: Some(RpcLargestAccountsFilter::NonCirculating),
1230                        ..Default::default()
1231                    }),
1232                )
1233                .await
1234                .unwrap();
1235
1236            assert_eq!(result.value.len(), 1);
1237            assert_eq!(
1238                large_non_circulating_pubkey.to_string(),
1239                result.value[0].address
1240            );
1241            assert_eq!(large_non_circulating_amount, result.value[0].lamports);
1242        }
1243
1244        // Test without filter - should return both accounts
1245        {
1246            let result = setup
1247                .rpc
1248                .get_largest_accounts(Some(setup.context), None)
1249                .await
1250                .unwrap();
1251
1252            assert_eq!(result.value.len(), 2);
1253            assert_eq!(
1254                large_non_circulating_pubkey.to_string(),
1255                result.value[0].address
1256            );
1257            assert_eq!(large_non_circulating_amount, result.value[0].lamports);
1258            assert_eq!(
1259                large_circulating_pubkey.to_string(),
1260                result.value[1].address
1261            );
1262            assert_eq!(large_circulating_amount, result.value[1].lamports);
1263        }
1264    }
1265
1266    #[tokio::test(flavor = "multi_thread")]
1267    async fn test_get_supply_with_invalid_config() {
1268        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1269
1270        let config = RpcSupplyConfig {
1271            commitment: Some(solana_commitment_config::CommitmentConfig {
1272                commitment: solana_commitment_config::CommitmentLevel::Processed,
1273            }),
1274            exclude_non_circulating_accounts_list: false,
1275        };
1276
1277        let result = setup
1278            .rpc
1279            .get_supply(Some(setup.context), Some(config))
1280            .await;
1281
1282        assert!(result.is_ok());
1283        let supply = result.unwrap();
1284        assert_eq!(supply.value.total, 0);
1285        assert_eq!(supply.value.circulating, 0);
1286        assert_eq!(supply.value.non_circulating, 0);
1287    }
1288
1289    #[tokio::test(flavor = "multi_thread")]
1290    async fn test_get_token_largest_accounts_local_svm() {
1291        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1292
1293        // create a mint
1294        let mint_pk = Pubkey::new_unique();
1295        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1296            svm_reader
1297                .inner
1298                .minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)
1299        });
1300
1301        // create mint account
1302        let mut mint_data = [0; spl_token::state::Mint::LEN];
1303        let mint = spl_token::state::Mint {
1304            decimals: 9,
1305            supply: 1000000000000000,
1306            is_initialized: true,
1307            ..Default::default()
1308        };
1309        mint.pack_into_slice(&mut mint_data);
1310
1311        let mint_account = Account {
1312            lamports: minimum_rent,
1313            owner: spl_token::ID,
1314            executable: false,
1315            rent_epoch: 0,
1316            data: mint_data.to_vec(),
1317        };
1318
1319        // create multiple token accounts with different balances
1320        let token_accounts = vec![
1321            (Pubkey::new_unique(), 1000000000), // 1 SOL worth
1322            (Pubkey::new_unique(), 5000000000), // 5 SOL worth (should be first)
1323            (Pubkey::new_unique(), 500000000),  // 0.5 SOL worth
1324            (Pubkey::new_unique(), 2000000000), // 2 SOL worth (should be second)
1325            (Pubkey::new_unique(), 100000000),  // 0.1 SOL worth
1326        ];
1327
1328        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1329            // set the mint account
1330            svm_writer
1331                .set_account(&mint_pk, mint_account.clone())
1332                .unwrap();
1333
1334            // set token accounts
1335            for (token_account_pk, amount) in &token_accounts {
1336                let mut token_account_data = [0; TokenAccount::LEN];
1337                let token_account = TokenAccount {
1338                    mint: mint_pk,
1339                    owner: Pubkey::new_unique(),
1340                    amount: *amount,
1341                    delegate: solana_sdk::program_option::COption::None,
1342                    state: spl_token::state::AccountState::Initialized,
1343                    is_native: solana_sdk::program_option::COption::None,
1344                    delegated_amount: 0,
1345                    close_authority: solana_sdk::program_option::COption::None,
1346                };
1347                token_account.pack_into_slice(&mut token_account_data);
1348
1349                let account = Account {
1350                    lamports: minimum_rent,
1351                    owner: spl_token::ID,
1352                    executable: false,
1353                    rent_epoch: 0,
1354                    data: token_account_data.to_vec(),
1355                };
1356
1357                svm_writer.set_account(token_account_pk, account).unwrap();
1358            }
1359        });
1360
1361        let result = setup
1362            .rpc
1363            .get_token_largest_accounts(Some(setup.context), mint_pk.to_string(), None)
1364            .await
1365            .unwrap();
1366
1367        assert_eq!(result.value.len(), 5);
1368
1369        // should be sorted by balance descending
1370        assert_eq!(result.value[0].amount.amount, "5000000000"); // 5 SOL
1371        assert_eq!(result.value[1].amount.amount, "2000000000"); // 2 SOL
1372        assert_eq!(result.value[2].amount.amount, "1000000000"); // 1 SOL
1373        assert_eq!(result.value[3].amount.amount, "500000000"); // 0.5 SOL
1374        assert_eq!(result.value[4].amount.amount, "100000000"); // 0.1 SOL
1375
1376        // verify decimals and UI amounts
1377        for balance in &result.value {
1378            assert_eq!(balance.amount.decimals, 9);
1379            assert!(balance.amount.ui_amount.is_some());
1380            assert!(!balance.amount.ui_amount_string.is_empty());
1381        }
1382
1383        // Verify addresses are valid pubkeys
1384        for balance in &result.value {
1385            assert!(Pubkey::from_str(&balance.address).is_ok());
1386        }
1387    }
1388
1389    #[tokio::test(flavor = "multi_thread")]
1390    async fn test_get_token_largest_accounts_limit_to_20() {
1391        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1392
1393        // Create a mint
1394        let mint_pk = Pubkey::new_unique();
1395        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1396            svm_reader
1397                .inner
1398                .minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)
1399        });
1400
1401        // Create mint account
1402        let mut mint_data = [0; spl_token::state::Mint::LEN];
1403        let mint = spl_token::state::Mint {
1404            decimals: 6,
1405            supply: 1000000000000000,
1406            is_initialized: true,
1407            ..Default::default()
1408        };
1409        mint.pack_into_slice(&mut mint_data);
1410
1411        let mint_account = Account {
1412            lamports: minimum_rent,
1413            owner: spl_token::ID,
1414            executable: false,
1415            rent_epoch: 0,
1416            data: mint_data.to_vec(),
1417        };
1418
1419        // Create 25 token accounts (more than the 20 limit)
1420        let mut token_accounts = Vec::new();
1421        for i in 0..25 {
1422            token_accounts.push((Pubkey::new_unique(), (i + 1) * 1000000)); // Varying amounts
1423        }
1424
1425        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1426            // Set the mint account
1427            svm_writer
1428                .set_account(&mint_pk, mint_account.clone())
1429                .unwrap();
1430
1431            // Set token accounts
1432            for (token_account_pk, amount) in &token_accounts {
1433                let mut token_account_data = [0; TokenAccount::LEN];
1434                let token_account = TokenAccount {
1435                    mint: mint_pk,
1436                    owner: Pubkey::new_unique(),
1437                    amount: *amount,
1438                    delegate: solana_sdk::program_option::COption::None,
1439                    state: spl_token::state::AccountState::Initialized,
1440                    is_native: solana_sdk::program_option::COption::None,
1441                    delegated_amount: 0,
1442                    close_authority: solana_sdk::program_option::COption::None,
1443                };
1444                token_account.pack_into_slice(&mut token_account_data);
1445
1446                let account = Account {
1447                    lamports: minimum_rent,
1448                    owner: spl_token::ID,
1449                    executable: false,
1450                    rent_epoch: 0,
1451                    data: token_account_data.to_vec(),
1452                };
1453
1454                svm_writer.set_account(token_account_pk, account).unwrap();
1455            }
1456        });
1457
1458        // Call get_token_largest_accounts
1459        let result = setup
1460            .rpc
1461            .get_token_largest_accounts(Some(setup.context), mint_pk.to_string(), None)
1462            .await
1463            .unwrap();
1464
1465        // Should be limited to 20 accounts
1466        assert_eq!(result.value.len(), 20);
1467
1468        // Should be sorted by balance descending (highest amounts first)
1469        assert_eq!(result.value[0].amount.amount, "25000000"); // Highest amount
1470        assert_eq!(result.value[1].amount.amount, "24000000"); // Second highest
1471        assert_eq!(result.value[19].amount.amount, "6000000"); // 20th highest
1472
1473        // Verify all are properly formatted
1474        for balance in &result.value {
1475            assert_eq!(balance.amount.decimals, 6);
1476            assert!(balance.amount.ui_amount.is_some());
1477            assert!(!balance.amount.ui_amount_string.is_empty());
1478            assert!(Pubkey::from_str(&balance.address).is_ok());
1479        }
1480    }
1481
1482    #[tokio::test(flavor = "multi_thread")]
1483    async fn test_get_token_largest_accounts_edge_cases() {
1484        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1485
1486        // Test 1: Invalid mint pubkey
1487        let invalid_result = setup
1488            .rpc
1489            .get_token_largest_accounts(
1490                Some(setup.context.clone()),
1491                "invalid_pubkey".to_string(),
1492                None,
1493            )
1494            .await;
1495        assert!(invalid_result.is_err());
1496        let error = invalid_result.unwrap_err();
1497        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
1498
1499        // Test 2: Valid mint but no token accounts
1500        let empty_mint_pk = Pubkey::new_unique();
1501        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1502            svm_reader
1503                .inner
1504                .minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)
1505        });
1506
1507        // Create mint account with no associated token accounts
1508        let mut mint_data = [0; spl_token::state::Mint::LEN];
1509        let mint = spl_token::state::Mint {
1510            decimals: 9,
1511            supply: 0,
1512            is_initialized: true,
1513            ..Default::default()
1514        };
1515        mint.pack_into_slice(&mut mint_data);
1516
1517        let mint_account = Account {
1518            lamports: minimum_rent,
1519            owner: spl_token::ID,
1520            executable: false,
1521            rent_epoch: 0,
1522            data: mint_data.to_vec(),
1523        };
1524
1525        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1526            svm_writer
1527                .set_account(&empty_mint_pk, mint_account.clone())
1528                .unwrap();
1529        });
1530
1531        let empty_result = setup
1532            .rpc
1533            .get_token_largest_accounts(
1534                Some(setup.context.clone()),
1535                empty_mint_pk.to_string(),
1536                None,
1537            )
1538            .await
1539            .unwrap();
1540
1541        // Should return empty array
1542        assert_eq!(empty_result.value.len(), 0);
1543
1544        // Test 3: Mint that doesn't exist at all
1545        let nonexistent_mint_pk = Pubkey::new_unique();
1546        let nonexistent_result = setup
1547            .rpc
1548            .get_token_largest_accounts(Some(setup.context), nonexistent_mint_pk.to_string(), None)
1549            .await
1550            .unwrap();
1551
1552        // Should return empty array (no token accounts for nonexistent mint)
1553        assert_eq!(nonexistent_result.value.len(), 0);
1554    }
1555
1556    #[tokio::test(flavor = "multi_thread")]
1557    async fn test_get_token_accounts_by_delegate() {
1558        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1559
1560        let delegate = Pubkey::new_unique();
1561        let owner = Pubkey::new_unique();
1562        let mint = Pubkey::new_unique();
1563        let token_account_pubkey = Pubkey::new_unique();
1564        let token_program = spl_token::id();
1565
1566        // create a token account with delegate
1567        let mut token_account_data = [0u8; spl_token::state::Account::LEN];
1568        let token_account = spl_token::state::Account {
1569            mint,
1570            owner,
1571            amount: 1000,
1572            delegate: solana_sdk::program_option::COption::Some(delegate),
1573            state: spl_token::state::AccountState::Initialized,
1574            is_native: solana_sdk::program_option::COption::None,
1575            delegated_amount: 500,
1576            close_authority: solana_sdk::program_option::COption::None,
1577        };
1578        solana_sdk::program_pack::Pack::pack_into_slice(&token_account, &mut token_account_data);
1579
1580        let account = Account {
1581            lamports: 1000000,
1582            data: token_account_data.to_vec(),
1583            owner: token_program,
1584            executable: false,
1585            rent_epoch: 0,
1586        };
1587
1588        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1589            svm_writer
1590                .set_account(&token_account_pubkey, account.clone())
1591                .unwrap();
1592        });
1593
1594        // programId filter - should find the account
1595        let result = setup
1596            .rpc
1597            .get_token_accounts_by_delegate(
1598                Some(setup.context.clone()),
1599                delegate.to_string(),
1600                RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1601                None,
1602            )
1603            .await;
1604
1605        assert!(result.is_ok(), "ProgramId filter should succeed");
1606        let response = result.unwrap();
1607        assert_eq!(response.value.len(), 1, "Should find 1 token account");
1608        assert_eq!(response.value[0].pubkey, token_account_pubkey.to_string());
1609
1610        // mint filter - should find the account
1611        let result = setup
1612            .rpc
1613            .get_token_accounts_by_delegate(
1614                Some(setup.context.clone()),
1615                delegate.to_string(),
1616                RpcTokenAccountsFilter::Mint(mint.to_string()),
1617                None,
1618            )
1619            .await;
1620
1621        assert!(result.is_ok(), "Mint filter should succeed");
1622        let response = result.unwrap();
1623        assert_eq!(response.value.len(), 1, "Should find 1 token account");
1624        assert_eq!(response.value[0].pubkey, token_account_pubkey.to_string());
1625
1626        // non-existent delegate - should return empty
1627        let non_existent_delegate = Pubkey::new_unique();
1628        let result = setup
1629            .rpc
1630            .get_token_accounts_by_delegate(
1631                Some(setup.context.clone()),
1632                non_existent_delegate.to_string(),
1633                RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1634                None,
1635            )
1636            .await;
1637
1638        assert!(result.is_ok(), "Non-existent delegate should succeed");
1639        let response = result.unwrap();
1640        assert_eq!(response.value.len(), 0, "Should find 0 token accounts");
1641
1642        // wrong mint - should return empty
1643        let wrong_mint = Pubkey::new_unique();
1644        let result = setup
1645            .rpc
1646            .get_token_accounts_by_delegate(
1647                Some(setup.context.clone()),
1648                delegate.to_string(),
1649                RpcTokenAccountsFilter::Mint(wrong_mint.to_string()),
1650                None,
1651            )
1652            .await;
1653
1654        assert!(result.is_ok(), "Wrong mint should succeed");
1655        let response = result.unwrap();
1656        assert_eq!(response.value.len(), 0, "Should find 0 token accounts");
1657
1658        // invalid delegate pubkey - should fail
1659        let result = setup
1660            .rpc
1661            .get_token_accounts_by_delegate(
1662                Some(setup.context.clone()),
1663                "invalid_pubkey".to_string(),
1664                RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1665                None,
1666            )
1667            .await;
1668
1669        assert!(result.is_err(), "Invalid pubkey should fail");
1670    }
1671
1672    #[tokio::test(flavor = "multi_thread")]
1673    async fn test_get_token_accounts_by_delegate_multiple_accounts() {
1674        let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1675
1676        let delegate = Pubkey::new_unique();
1677        let owner1 = Pubkey::new_unique();
1678        let owner2 = Pubkey::new_unique();
1679        let mint1 = Pubkey::new_unique();
1680        let mint2 = Pubkey::new_unique();
1681        let token_account1 = Pubkey::new_unique();
1682        let token_account2 = Pubkey::new_unique();
1683        let token_program = spl_token::id();
1684
1685        // create first token account with delegate
1686        let mut token_account_data1 = [0u8; spl_token::state::Account::LEN];
1687        let token_account_struct1 = spl_token::state::Account {
1688            mint: mint1,
1689            owner: owner1,
1690            amount: 1000,
1691            delegate: solana_sdk::program_option::COption::Some(delegate),
1692            state: spl_token::state::AccountState::Initialized,
1693            is_native: solana_sdk::program_option::COption::None,
1694            delegated_amount: 500,
1695            close_authority: solana_sdk::program_option::COption::None,
1696        };
1697        solana_sdk::program_pack::Pack::pack_into_slice(
1698            &token_account_struct1,
1699            &mut token_account_data1,
1700        );
1701
1702        // create second token account with same delegate
1703        let mut token_account_data2 = [0u8; spl_token::state::Account::LEN];
1704        let token_account_struct2 = spl_token::state::Account {
1705            mint: mint2,
1706            owner: owner2,
1707            amount: 2000,
1708            delegate: solana_sdk::program_option::COption::Some(delegate),
1709            state: spl_token::state::AccountState::Initialized,
1710            is_native: solana_sdk::program_option::COption::None,
1711            delegated_amount: 1000,
1712            close_authority: solana_sdk::program_option::COption::None,
1713        };
1714        solana_sdk::program_pack::Pack::pack_into_slice(
1715            &token_account_struct2,
1716            &mut token_account_data2,
1717        );
1718
1719        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1720            svm_writer
1721                .set_account(
1722                    &token_account1,
1723                    Account {
1724                        lamports: 1000000,
1725                        data: token_account_data1.to_vec(),
1726                        owner: token_program,
1727                        executable: false,
1728                        rent_epoch: 0,
1729                    },
1730                )
1731                .unwrap();
1732
1733            svm_writer
1734                .set_account(
1735                    &token_account2,
1736                    Account {
1737                        lamports: 1000000,
1738                        data: token_account_data2.to_vec(),
1739                        owner: token_program,
1740                        executable: false,
1741                        rent_epoch: 0,
1742                    },
1743                )
1744                .unwrap();
1745        });
1746
1747        let result = setup
1748            .rpc
1749            .get_token_accounts_by_delegate(
1750                Some(setup.context.clone()),
1751                delegate.to_string(),
1752                RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1753                None,
1754            )
1755            .await;
1756
1757        assert!(result.is_ok(), "ProgramId filter should succeed");
1758        let response = result.unwrap();
1759        assert_eq!(response.value.len(), 2, "Should find 2 token accounts");
1760
1761        let returned_pubkeys: std::collections::HashSet<String> = response
1762            .value
1763            .iter()
1764            .map(|acc| acc.pubkey.clone())
1765            .collect();
1766        assert!(returned_pubkeys.contains(&token_account1.to_string()));
1767        assert!(returned_pubkeys.contains(&token_account2.to_string()));
1768
1769        // Test: Mint filter for mint1 - should find only first account
1770        let result = setup
1771            .rpc
1772            .get_token_accounts_by_delegate(
1773                Some(setup.context.clone()),
1774                delegate.to_string(),
1775                RpcTokenAccountsFilter::Mint(mint1.to_string()),
1776                None,
1777            )
1778            .await;
1779
1780        assert!(result.is_ok(), "Mint filter should succeed");
1781        let response = result.unwrap();
1782        assert_eq!(
1783            response.value.len(),
1784            1,
1785            "Should find 1 token account for mint1"
1786        );
1787        assert_eq!(response.value[0].pubkey, token_account1.to_string());
1788    }
1789}