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}