surfpool_core/rpc/
accounts_data.rs

1use jsonrpc_core::{BoxFuture, Result};
2use jsonrpc_derive::rpc;
3use solana_account_decoder::{
4    parse_account_data::SplTokenAdditionalDataV2,
5    parse_token::{parse_token_v3, TokenAccountType, UiTokenAmount},
6    UiAccount,
7};
8use solana_client::{
9    rpc_config::RpcAccountInfoConfig,
10    rpc_response::{RpcBlockCommitment, RpcResponseContext},
11};
12use solana_clock::Slot;
13use solana_commitment_config::CommitmentConfig;
14use solana_rpc_client_api::response::Response as RpcResponse;
15use solana_runtime::commitment::BlockCommitmentArray;
16use solana_sdk::program_pack::Pack;
17use spl_token::state::{Account as TokenAccount, Mint};
18
19use super::{not_implemented_err, RunloopContext, SurfnetRpcContext};
20use crate::{
21    error::{SurfpoolError, SurfpoolResult},
22    rpc::{utils::verify_pubkey, State},
23    surfnet::locker::SvmAccessContext,
24};
25
26#[rpc]
27pub trait AccountsData {
28    type Metadata;
29
30    /// Returns detailed information about an account given its public key.
31    ///
32    /// This method queries the blockchain for the account associated with the provided
33    /// public key string. It can be used to inspect balances, ownership, and program-related metadata.
34    ///
35    /// ## Parameters
36    /// - `pubkey_str`: A base-58 encoded string representing the account's public key.
37    /// - `config`: Optional configuration that controls encoding, commitment level,
38    ///   data slicing, and other response details.
39    ///
40    /// ## Returns
41    /// A [`RpcResponse`] containing an optional [`UiAccount`] object if the account exists.
42    /// If the account does not exist, the response will contain `null`.
43    ///
44    /// ## Example Request (JSON-RPC)
45    /// ```json
46    /// {
47    ///   "jsonrpc": "2.0",
48    ///   "id": 1,
49    ///   "method": "getAccountInfo",
50    ///   "params": [
51    ///     "9XQeWMPMPXwW1fzLEQeTTrfF5Eb9dj8Qs3tCPoMw3GiE",
52    ///     {
53    ///       "encoding": "jsonParsed",
54    ///       "commitment": "finalized"
55    ///     }
56    ///   ]
57    /// }
58    /// ```
59    ///
60    /// ## Example Response
61    /// ```json
62    /// {
63    ///   "jsonrpc": "2.0",
64    ///   "result": {
65    ///     "context": {
66    ///       "slot": 12345678
67    ///     },
68    ///     "value": {
69    ///       "lamports": 10000000,
70    ///       "data": {
71    ///         "program": "spl-token",
72    ///         "parsed": { ... },
73    ///         "space": 165
74    ///       },
75    ///       "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
76    ///       "executable": false,
77    ///       "rentEpoch": 203,
78    ///       "space": 165
79    ///     }
80    ///   },
81    ///   "id": 1
82    /// }
83    /// ```
84    ///
85    /// ## Errors
86    /// - Returns an error if the public key is malformed or invalid
87    /// - Returns an internal error if the ledger cannot be accessed
88    ///
89    /// ## See also
90    /// - [`UiAccount`]: A readable structure representing on-chain accounts
91    #[rpc(meta, name = "getAccountInfo")]
92    fn get_account_info(
93        &self,
94        meta: Self::Metadata,
95        pubkey_str: String,
96        config: Option<RpcAccountInfoConfig>,
97    ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>>;
98
99    /// Returns commitment levels for a given block (slot).
100    ///
101    /// This method provides insight into how many validators have voted for a specific block
102    /// and with what level of lockout. This can be used to analyze consensus progress and
103    /// determine finality confidence.
104    ///
105    /// ## Parameters
106    /// - `block`: The target slot (block) to query.
107    ///
108    /// ## Returns
109    /// A [`RpcBlockCommitment`] containing a [`BlockCommitmentArray`], which is an array of 32
110    /// integers representing the number of votes at each lockout level for that block. Each index
111    /// corresponds to a lockout level (i.e., confidence in finality).
112    ///
113    /// ## Example Request (JSON-RPC)
114    /// ```json
115    /// {
116    ///   "jsonrpc": "2.0",
117    ///   "id": 1,
118    ///   "method": "getBlockCommitment",
119    ///   "params": [150000000]
120    /// }
121    /// ```
122    ///
123    /// ## Example Response
124    /// ```json
125    /// {
126    ///   "jsonrpc": "2.0",
127    ///   "result": {
128    ///     "commitment": [0, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
129    ///     "totalStake": 100000000
130    ///   },
131    ///   "id": 1
132    /// }
133    /// ```
134    ///
135    /// ## Errors
136    /// - If the slot is not found in the current bank or has been purged, this call may return an error.
137    /// - May fail if the RPC node is lagging behind or doesn't have voting history for the slot.
138    ///
139    /// ## See also
140    /// - [`BlockCommitmentArray`]: An array representing votes by lockout level
141    /// - [`RpcBlockCommitment`]: Wrapper struct for the full response
142    #[rpc(meta, name = "getBlockCommitment")]
143    fn get_block_commitment(
144        &self,
145        meta: Self::Metadata,
146        block: Slot,
147    ) -> Result<RpcBlockCommitment<BlockCommitmentArray>>;
148
149    /// Returns account information for multiple public keys in a single call.
150    ///
151    /// This method allows batching of account lookups for improved performance and fewer
152    /// network roundtrips. It returns a list of `UiAccount` values in the same order as
153    /// the provided public keys.
154    ///
155    /// ## Parameters
156    /// - `pubkey_strs`: A list of base-58 encoded public key strings representing accounts to query.
157    /// - `config`: Optional configuration to control encoding, commitment level, data slicing, etc.
158    ///
159    /// ## Returns
160    /// A [`RpcResponse`] wrapping a vector of optional [`UiAccount`] objects.  
161    /// Each element in the response corresponds to the public key at the same index in the request.
162    /// If an account is not found, the corresponding entry will be `null`.
163    ///
164    /// ## Example Request (JSON-RPC)
165    /// ```json
166    /// {
167    ///   "jsonrpc": "2.0",
168    ///   "id": 1,
169    ///   "method": "getMultipleAccounts",
170    ///   "params": [
171    ///     [
172    ///       "9XQeWMPMPXwW1fzLEQeTTrfF5Eb9dj8Qs3tCPoMw3GiE",
173    ///       "3nN8SBQ2HqTDNnaCzryrSv4YHd4d6GpVCEyDhKMPxN4o"
174    ///     ],
175    ///     {
176    ///       "encoding": "jsonParsed",
177    ///       "commitment": "confirmed"
178    ///     }
179    ///   ]
180    /// }
181    /// ```
182    ///
183    /// ## Example Response
184    /// ```json
185    /// {
186    ///   "jsonrpc": "2.0",
187    ///   "result": {
188    ///     "context": { "slot": 12345678 },
189    ///     "value": [
190    ///       {
191    ///         "lamports": 10000000,
192    ///         "data": {
193    ///           "program": "spl-token",
194    ///           "parsed": { ... },
195    ///           "space": 165
196    ///         },
197    ///         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
198    ///         "executable": false,
199    ///         "rentEpoch": 203,
200    ///         "space": 165
201    ///       },
202    ///       null
203    ///     ]
204    ///   },
205    ///   "id": 1
206    /// }
207    /// ```
208    ///
209    /// ## Errors
210    /// - If any public key is malformed or invalid, the entire call may fail.
211    /// - Returns an internal error if the ledger cannot be accessed or some accounts are purged.
212    ///
213    /// ## See also
214    /// - [`UiAccount`]: Human-readable representation of an account
215    /// - [`get_account_info`]: Use when querying a single account
216    #[rpc(meta, name = "getMultipleAccounts")]
217    fn get_multiple_accounts(
218        &self,
219        meta: Self::Metadata,
220        pubkey_strs: Vec<String>,
221        config: Option<RpcAccountInfoConfig>,
222    ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>>;
223
224    /// Returns the balance of a token account, given its public key.
225    ///
226    /// This method fetches the token balance of an account, including its amount and
227    /// user-friendly information (like the UI amount in human-readable format). It is useful
228    /// for token-related applications, such as checking balances in wallets or exchanges.
229    ///
230    /// ## Parameters
231    /// - `pubkey_str`: The base-58 encoded string of the public key of the token account.
232    /// - `commitment`: Optional commitment configuration to specify the desired confirmation level of the query.
233    ///
234    /// ## Returns
235    /// A [`RpcResponse`] containing the token balance in a [`UiTokenAmount`] struct.
236    /// If the account doesn't hold any tokens or is invalid, the response will contain `null`.
237    ///
238    /// ## Example Request (JSON-RPC)
239    /// ```json
240    /// {
241    ///   "jsonrpc": "2.0",
242    ///   "id": 1,
243    ///   "method": "getTokenAccountBalance",
244    ///   "params": [
245    ///     "3nN8SBQ2HqTDNnaCzryrSv4YHd4d6GpVCEyDhKMPxN4o",
246    ///     {
247    ///       "commitment": "confirmed"
248    ///     }
249    ///   ]
250    /// }
251    /// ```
252    ///
253    /// ## Example Response
254    /// ```json
255    /// {
256    ///   "jsonrpc": "2.0",
257    ///   "result": {
258    ///     "context": {
259    ///       "slot": 12345678
260    ///     },
261    ///     "value": {
262    ///       "uiAmount": 100.0,
263    ///       "decimals": 6,
264    ///       "amount": "100000000",
265    ///       "uiAmountString": "100.000000"
266    ///     }
267    ///   },
268    ///   "id": 1
269    /// }
270    /// ```
271    ///
272    /// ## Errors
273    /// - If the provided public key is invalid or does not exist.
274    /// - If the account is not a valid token account or does not hold any tokens.
275    ///
276    /// ## See also
277    /// - [`UiTokenAmount`]: Represents the token balance in user-friendly format.
278    #[rpc(meta, name = "getTokenAccountBalance")]
279    fn get_token_account_balance(
280        &self,
281        meta: Self::Metadata,
282        pubkey_str: String,
283        commitment: Option<CommitmentConfig>,
284    ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>>;
285
286    /// Returns the total supply of a token, given its mint address.
287    ///
288    /// This method provides the total circulating supply of a specific token, including the raw
289    /// amount and human-readable UI-formatted values. It can be useful for tracking token issuance
290    /// and verifying the supply of a token on-chain.
291    ///
292    /// ## Parameters
293    /// - `mint_str`: The base-58 encoded string of the mint address for the token.
294    /// - `commitment`: Optional commitment configuration to specify the desired confirmation level of the query.
295    ///
296    /// ## Returns
297    /// A [`RpcResponse`] containing the total token supply in a [`UiTokenAmount`] struct.
298    /// If the token does not exist or is invalid, the response will return an error.
299    ///
300    /// ## Example Request (JSON-RPC)
301    /// ```json
302    /// {
303    ///   "jsonrpc": "2.0",
304    ///   "id": 1,
305    ///   "method": "getTokenSupply",
306    ///   "params": [
307    ///     "So11111111111111111111111111111111111111112",
308    ///     {
309    ///       "commitment": "confirmed"
310    ///     }
311    ///   ]
312    /// }
313    /// ```
314    ///
315    /// ## Example Response
316    /// ```json
317    /// {
318    ///   "jsonrpc": "2.0",
319    ///   "result": {
320    ///     "context": {
321    ///       "slot": 12345678
322    ///     },
323    ///     "value": {
324    ///       "uiAmount": 1000000000.0,
325    ///       "decimals": 6,
326    ///       "amount": "1000000000000000",
327    ///       "uiAmountString": "1000000000.000000"
328    ///     }
329    ///   },
330    ///   "id": 1
331    /// }
332    /// ```
333    ///
334    /// ## Errors
335    /// - If the mint address is invalid or does not correspond to a token.
336    /// - If the token supply cannot be fetched due to network issues or node synchronization problems.
337    ///
338    /// ## See also
339    /// - [`UiTokenAmount`]: Represents the token balance or supply in a user-friendly format.
340    #[rpc(meta, name = "getTokenSupply")]
341    fn get_token_supply(
342        &self,
343        meta: Self::Metadata,
344        mint_str: String,
345        commitment: Option<CommitmentConfig>,
346    ) -> Result<RpcResponse<UiTokenAmount>>;
347}
348
349#[derive(Clone)]
350pub struct SurfpoolAccountsDataRpc;
351impl AccountsData for SurfpoolAccountsDataRpc {
352    type Metadata = Option<RunloopContext>;
353
354    fn get_account_info(
355        &self,
356        meta: Self::Metadata,
357        pubkey_str: String,
358        config: Option<RpcAccountInfoConfig>,
359    ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>> {
360        let config = config.unwrap_or_default();
361        let pubkey = match verify_pubkey(&pubkey_str) {
362            Ok(res) => res,
363            Err(e) => return e.into(),
364        };
365
366        let SurfnetRpcContext {
367            svm_locker,
368            remote_ctx,
369        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
370            Ok(res) => res,
371            Err(e) => return e.into(),
372        };
373
374        Box::pin(async move {
375            let SvmAccessContext {
376                slot,
377                inner: account_update,
378                ..
379            } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
380
381            svm_locker.write_account_update(account_update.clone());
382
383            Ok(RpcResponse {
384                context: RpcResponseContext::new(slot),
385                value: account_update.try_into_ui_account(config.encoding, config.data_slice),
386            })
387        })
388    }
389
390    fn get_multiple_accounts(
391        &self,
392        meta: Self::Metadata,
393        pubkeys_str: Vec<String>,
394        config: Option<RpcAccountInfoConfig>,
395    ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>> {
396        let config = config.unwrap_or_default();
397        let pubkeys = match pubkeys_str
398            .iter()
399            .map(|s| verify_pubkey(s))
400            .collect::<SurfpoolResult<Vec<_>>>()
401        {
402            Ok(p) => p,
403            Err(e) => return e.into(),
404        };
405
406        let SurfnetRpcContext {
407            svm_locker,
408            remote_ctx,
409        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
410            Ok(res) => res,
411            Err(e) => return e.into(),
412        };
413
414        Box::pin(async move {
415            let SvmAccessContext {
416                slot,
417                inner: account_updates,
418                ..
419            } = svm_locker
420                .get_multiple_accounts(&remote_ctx, &pubkeys, None)
421                .await?;
422
423            svm_locker.write_multiple_account_updates(&account_updates);
424
425            let mut ui_accounts = vec![];
426            {
427                for account_update in account_updates.into_iter() {
428                    ui_accounts.push(
429                        account_update.try_into_ui_account(config.encoding, config.data_slice),
430                    );
431                }
432            }
433
434            Ok(RpcResponse {
435                context: RpcResponseContext::new(slot),
436                value: ui_accounts,
437            })
438        })
439    }
440
441    fn get_block_commitment(
442        &self,
443        _meta: Self::Metadata,
444        _block: Slot,
445    ) -> Result<RpcBlockCommitment<BlockCommitmentArray>> {
446        not_implemented_err("get_block_commitment")
447    }
448
449    // SPL Token-specific RPC endpoints
450    // See https://github.com/solana-labs/solana-program-library/releases/tag/token-v2.0.0 for
451    // program details
452
453    fn get_token_account_balance(
454        &self,
455        meta: Self::Metadata,
456        pubkey_str: String,
457        commitment: Option<CommitmentConfig>,
458    ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>> {
459        let pubkey = match verify_pubkey(&pubkey_str) {
460            Ok(res) => res,
461            Err(e) => return e.into(),
462        };
463
464        let SurfnetRpcContext {
465            svm_locker,
466            remote_ctx,
467        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
468            Ok(res) => res,
469            Err(e) => return e.into(),
470        };
471
472        Box::pin(async move {
473            let token_account_result = svm_locker
474                .get_account(&remote_ctx, &pubkey, None)
475                .await?
476                .inner;
477
478            svm_locker.write_account_update(token_account_result.clone());
479
480            let token_account = token_account_result.map_account()?;
481
482            let unpacked_token_account =
483                TokenAccount::unpack(&token_account.data).map_err(|e| {
484                    SurfpoolError::invalid_account_data(
485                        pubkey,
486                        "Invalid token account data",
487                        Some(e.to_string()),
488                    )
489                })?;
490
491            let SvmAccessContext {
492                slot,
493                inner: mint_account_result,
494                ..
495            } = svm_locker
496                .get_account(&remote_ctx, &unpacked_token_account.mint, None)
497                .await?;
498
499            svm_locker.write_account_update(mint_account_result.clone());
500
501            let mint_account = mint_account_result.map_account()?;
502            let unpacked_mint_account = Mint::unpack(&mint_account.data).map_err(|e| {
503                SurfpoolError::invalid_account_data(
504                    unpacked_token_account.mint,
505                    "Invalid token mint account data",
506                    Some(e.to_string()),
507                )
508            })?;
509
510            let token_decimals = unpacked_mint_account.decimals;
511
512            Ok(RpcResponse {
513                context: RpcResponseContext::new(slot),
514                value: {
515                    parse_token_v3(
516                        &token_account.data,
517                        Some(&SplTokenAdditionalDataV2 {
518                            decimals: token_decimals,
519                            ..Default::default()
520                        }),
521                    )
522                    .ok()
523                    .and_then(|t| match t {
524                        TokenAccountType::Account(account) => Some(account.token_amount),
525                        _ => None,
526                    })
527                },
528            })
529        })
530    }
531
532    fn get_token_supply(
533        &self,
534        _meta: Self::Metadata,
535        _mint_str: String,
536        _commitment: Option<CommitmentConfig>,
537    ) -> Result<RpcResponse<UiTokenAmount>> {
538        not_implemented_err("get_token_supply")
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use solana_account::Account;
545    use solana_pubkey::Pubkey;
546    use solana_sdk::program_pack::Pack;
547    use spl_token::state::{Account as TokenAccount, AccountState, Mint};
548
549    use super::*;
550    use crate::{surfnet::GetAccountResult, tests::helpers::TestSetup};
551
552    #[ignore = "connection-required"]
553    #[tokio::test(flavor = "multi_thread")]
554    async fn test_get_token_account_balance() {
555        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
556
557        let mint_pk = Pubkey::new_unique();
558
559        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
560            svm_reader
561                .inner
562                .minimum_balance_for_rent_exemption(Mint::LEN)
563        });
564
565        let mut data = [0; Mint::LEN];
566
567        let default = Mint {
568            decimals: 6,
569            supply: 1000000000000000,
570            is_initialized: true,
571            ..Default::default()
572        };
573        default.pack_into_slice(&mut data);
574
575        let mint_account = Account {
576            lamports: minimum_rent,
577            owner: spl_token::ID,
578            executable: false,
579            rent_epoch: 0,
580            data: data.to_vec(),
581        };
582
583        setup
584            .context
585            .svm_locker
586            .write_account_update(GetAccountResult::FoundAccount(mint_pk, mint_account, true));
587
588        let token_account_pk = Pubkey::new_unique();
589
590        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
591            svm_reader
592                .inner
593                .minimum_balance_for_rent_exemption(TokenAccount::LEN)
594        });
595
596        let mut data = [0; TokenAccount::LEN];
597
598        let default = TokenAccount {
599            mint: mint_pk,
600            owner: spl_token::ID,
601            state: AccountState::Initialized,
602            amount: 100 * 1000000,
603            ..Default::default()
604        };
605        default.pack_into_slice(&mut data);
606
607        let token_account = Account {
608            lamports: minimum_rent,
609            owner: spl_token::ID,
610            executable: false,
611            rent_epoch: 0,
612            data: data.to_vec(),
613        };
614
615        setup
616            .context
617            .svm_locker
618            .write_account_update(GetAccountResult::FoundAccount(
619                token_account_pk,
620                token_account,
621                true,
622            ));
623
624        let res = setup
625            .rpc
626            .get_token_account_balance(Some(setup.context), token_account_pk.to_string(), None)
627            .await
628            .unwrap();
629
630        assert_eq!(
631            res.value.unwrap(),
632            UiTokenAmount {
633                amount: String::from("100000000"),
634                decimals: 6,
635                ui_amount: Some(100.0),
636                ui_amount_string: String::from("100")
637            }
638        );
639    }
640}