Skip to main content

surfpool_core/rpc/
accounts_scan.rs

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