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