Skip to main content

surfpool_core/rpc/
accounts_data.rs

1use jsonrpc_core::{BoxFuture, Result};
2use jsonrpc_derive::rpc;
3use solana_account_decoder::{
4    UiAccount,
5    parse_account_data::SplTokenAdditionalDataV2,
6    parse_token::{TokenAccountType, UiTokenAmount, parse_token_v3},
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;
16
17use super::{RunloopContext, SurfnetRpcContext};
18use crate::{
19    error::{SurfpoolError, SurfpoolResult},
20    rpc::{State, utils::verify_pubkey},
21    surfnet::locker::{SvmAccessContext, is_supported_token_program},
22    types::{MintAccount, TokenAccount},
23};
24
25#[rpc]
26pub trait AccountsData {
27    type Metadata;
28
29    /// Returns detailed information about an account given its public key.
30    ///
31    /// This method queries the blockchain for the account associated with the provided
32    /// public key string. It can be used to inspect balances, ownership, and program-related metadata.
33    ///
34    /// ## Parameters
35    /// - `pubkey_str`: A base-58 encoded string representing the account's public key.
36    /// - `config`: Optional configuration that controls encoding, commitment level,
37    ///   data slicing, and other response details.
38    ///
39    /// ## Returns
40    /// A [`RpcResponse`] containing an optional [`UiAccount`] object if the account exists.
41    /// If the account does not exist, the response will contain `null`.
42    ///
43    /// ## Example Request (JSON-RPC)
44    /// ```json
45    /// {
46    ///   "jsonrpc": "2.0",
47    ///   "id": 1,
48    ///   "method": "getAccountInfo",
49    ///   "params": [
50    ///     "9XQeWMPMPXwW1fzLEQeTTrfF5Eb9dj8Qs3tCPoMw3GiE",
51    ///     {
52    ///       "encoding": "jsonParsed",
53    ///       "commitment": "finalized"
54    ///     }
55    ///   ]
56    /// }
57    /// ```
58    ///
59    /// ## Example Response
60    /// ```json
61    /// {
62    ///   "jsonrpc": "2.0",
63    ///   "result": {
64    ///     "context": {
65    ///       "slot": 12345678
66    ///     },
67    ///     "value": {
68    ///       "lamports": 10000000,
69    ///       "data": {
70    ///         "program": "spl-token",
71    ///         "parsed": { ... },
72    ///         "space": 165
73    ///       },
74    ///       "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
75    ///       "executable": false,
76    ///       "rentEpoch": 203,
77    ///       "space": 165
78    ///     }
79    ///   },
80    ///   "id": 1
81    /// }
82    /// ```
83    ///
84    /// ## Errors
85    /// - Returns an error if the public key is malformed or invalid
86    /// - Returns an internal error if the ledger cannot be accessed
87    ///
88    /// ## See also
89    /// - [`UiAccount`]: A readable structure representing on-chain accounts
90    #[rpc(meta, name = "getAccountInfo")]
91    fn get_account_info(
92        &self,
93        meta: Self::Metadata,
94        pubkey_str: String,
95        config: Option<RpcAccountInfoConfig>,
96    ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>>;
97
98    /// Returns commitment levels for a given block (slot).
99    ///
100    /// This method provides insight into how many validators have voted for a specific block
101    /// and with what level of lockout. This can be used to analyze consensus progress and
102    /// determine finality confidence.
103    ///
104    /// ## Parameters
105    /// - `block`: The target slot (block) to query.
106    ///
107    /// ## Returns
108    /// A [`RpcBlockCommitment`] containing a [`BlockCommitmentArray`], which is an array of 32
109    /// integers representing the number of votes at each lockout level for that block. Each index
110    /// corresponds to a lockout level (i.e., confidence in finality).
111    ///
112    /// ## Example Request (JSON-RPC)
113    /// ```json
114    /// {
115    ///   "jsonrpc": "2.0",
116    ///   "id": 1,
117    ///   "method": "getBlockCommitment",
118    ///   "params": [150000000]
119    /// }
120    /// ```
121    ///
122    /// ## Example Response
123    /// ```json
124    /// {
125    ///   "jsonrpc": "2.0",
126    ///   "result": {
127    ///     "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],
128    ///     "totalStake": 100000000
129    ///   },
130    ///   "id": 1
131    /// }
132    /// ```
133    ///
134    /// ## Errors
135    /// - If the slot is not found in the current bank or has been purged, this call may return an error.
136    /// - May fail if the RPC node is lagging behind or doesn't have voting history for the slot.
137    ///
138    /// ## See also
139    /// - [`BlockCommitmentArray`]: An array representing votes by lockout level
140    /// - [`RpcBlockCommitment`]: Wrapper struct for the full response
141    #[rpc(meta, name = "getBlockCommitment")]
142    fn get_block_commitment(
143        &self,
144        meta: Self::Metadata,
145        block: Slot,
146    ) -> Result<RpcBlockCommitment<BlockCommitmentArray>>;
147
148    /// Returns account information for multiple public keys in a single call.
149    ///
150    /// This method allows batching of account lookups for improved performance and fewer
151    /// network roundtrips. It returns a list of `UiAccount` values in the same order as
152    /// the provided public keys.
153    ///
154    /// ## Parameters
155    /// - `pubkey_strs`: A list of base-58 encoded public key strings representing accounts to query.
156    /// - `config`: Optional configuration to control encoding, commitment level, data slicing, etc.
157    ///
158    /// ## Returns
159    /// A [`RpcResponse`] wrapping a vector of optional [`UiAccount`] objects.
160    /// Each element in the response corresponds to the public key at the same index in the request.
161    /// If an account is not found, the corresponding entry will be `null`.
162    ///
163    /// ## Example Request (JSON-RPC)
164    /// ```json
165    /// {
166    ///   "jsonrpc": "2.0",
167    ///   "id": 1,
168    ///   "method": "getMultipleAccounts",
169    ///   "params": [
170    ///     [
171    ///       "9XQeWMPMPXwW1fzLEQeTTrfF5Eb9dj8Qs3tCPoMw3GiE",
172    ///       "3nN8SBQ2HqTDNnaCzryrSv4YHd4d6GpVCEyDhKMPxN4o"
173    ///     ],
174    ///     {
175    ///       "encoding": "jsonParsed",
176    ///       "commitment": "confirmed"
177    ///     }
178    ///   ]
179    /// }
180    /// ```
181    ///
182    /// ## Example Response
183    /// ```json
184    /// {
185    ///   "jsonrpc": "2.0",
186    ///   "result": {
187    ///     "context": { "slot": 12345678 },
188    ///     "value": [
189    ///       {
190    ///         "lamports": 10000000,
191    ///         "data": {
192    ///           "program": "spl-token",
193    ///           "parsed": { ... },
194    ///           "space": 165
195    ///         },
196    ///         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
197    ///         "executable": false,
198    ///         "rentEpoch": 203,
199    ///         "space": 165
200    ///       },
201    ///       null
202    ///     ]
203    ///   },
204    ///   "id": 1
205    /// }
206    /// ```
207    ///
208    /// ## Errors
209    /// - If any public key is malformed or invalid, the entire call may fail.
210    /// - Returns an internal error if the ledger cannot be accessed or some accounts are purged.
211    ///
212    /// ## See also
213    /// - [`UiAccount`]: Human-readable representation of an account
214    /// - [`get_account_info`]: Use when querying a single account
215    #[rpc(meta, name = "getMultipleAccounts")]
216    fn get_multiple_accounts(
217        &self,
218        meta: Self::Metadata,
219        pubkey_strs: Vec<String>,
220        config: Option<RpcAccountInfoConfig>,
221    ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>>;
222
223    /// Returns the balance of a token account, given its public key.
224    ///
225    /// This method fetches the token balance of an account, including its amount and
226    /// user-friendly information (like the UI amount in human-readable format). It is useful
227    /// for token-related applications, such as checking balances in wallets or exchanges.
228    ///
229    /// ## Parameters
230    /// - `pubkey_str`: The base-58 encoded string of the public key of the token account.
231    /// - `commitment`: Optional commitment configuration to specify the desired confirmation level of the query.
232    ///
233    /// ## Returns
234    /// A [`RpcResponse`] containing the token balance in a [`UiTokenAmount`] struct.
235    /// If the account doesn't hold any tokens or is invalid, the response will contain `null`.
236    ///
237    /// ## Example Request (JSON-RPC)
238    /// ```json
239    /// {
240    ///   "jsonrpc": "2.0",
241    ///   "id": 1,
242    ///   "method": "getTokenAccountBalance",
243    ///   "params": [
244    ///     "3nN8SBQ2HqTDNnaCzryrSv4YHd4d6GpVCEyDhKMPxN4o",
245    ///     {
246    ///       "commitment": "confirmed"
247    ///     }
248    ///   ]
249    /// }
250    /// ```
251    ///
252    /// ## Example Response
253    /// ```json
254    /// {
255    ///   "jsonrpc": "2.0",
256    ///   "result": {
257    ///     "context": {
258    ///       "slot": 12345678
259    ///     },
260    ///     "value": {
261    ///       "uiAmount": 100.0,
262    ///       "decimals": 6,
263    ///       "amount": "100000000",
264    ///       "uiAmountString": "100.000000"
265    ///     }
266    ///   },
267    ///   "id": 1
268    /// }
269    /// ```
270    ///
271    /// ## Errors
272    /// - If the provided public key is invalid or does not exist.
273    /// - If the account is not a valid token account or does not hold any tokens.
274    ///
275    /// ## See also
276    /// - [`UiTokenAmount`]: Represents the token balance in user-friendly format.
277    #[rpc(meta, name = "getTokenAccountBalance")]
278    fn get_token_account_balance(
279        &self,
280        meta: Self::Metadata,
281        pubkey_str: String,
282        commitment: Option<CommitmentConfig>,
283    ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>>;
284
285    /// Returns the total supply of a token, given its mint address.
286    ///
287    /// This method provides the total circulating supply of a specific token, including the raw
288    /// amount and human-readable UI-formatted values. It can be useful for tracking token issuance
289    /// and verifying the supply of a token on-chain.
290    ///
291    /// ## Parameters
292    /// - `mint_str`: The base-58 encoded string of the mint address for the token.
293    /// - `commitment`: Optional commitment configuration to specify the desired confirmation level of the query.
294    ///
295    /// ## Returns
296    /// A [`RpcResponse`] containing the total token supply in a [`UiTokenAmount`] struct.
297    /// If the token does not exist or is invalid, the response will return an error.
298    ///
299    /// ## Example Request (JSON-RPC)
300    /// ```json
301    /// {
302    ///   "jsonrpc": "2.0",
303    ///   "id": 1,
304    ///   "method": "getTokenSupply",
305    ///   "params": [
306    ///     "So11111111111111111111111111111111111111112",
307    ///     {
308    ///       "commitment": "confirmed"
309    ///     }
310    ///   ]
311    /// }
312    /// ```
313    ///
314    /// ## Example Response
315    /// ```json
316    /// {
317    ///   "jsonrpc": "2.0",
318    ///   "result": {
319    ///     "context": {
320    ///       "slot": 12345678
321    ///     },
322    ///     "value": {
323    ///       "uiAmount": 1000000000.0,
324    ///       "decimals": 6,
325    ///       "amount": "1000000000000000",
326    ///       "uiAmountString": "1000000000.000000"
327    ///     }
328    ///   },
329    ///   "id": 1
330    /// }
331    /// ```
332    ///
333    /// ## Errors
334    /// - If the mint address is invalid or does not correspond to a token.
335    /// - If the token supply cannot be fetched due to network issues or node synchronization problems.
336    ///
337    /// ## See also
338    /// - [`UiTokenAmount`]: Represents the token balance or supply in a user-friendly format.
339    #[rpc(meta, name = "getTokenSupply")]
340    fn get_token_supply(
341        &self,
342        meta: Self::Metadata,
343        mint_str: String,
344        commitment: Option<CommitmentConfig>,
345    ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>>;
346}
347
348#[derive(Clone)]
349pub struct SurfpoolAccountsDataRpc;
350impl AccountsData for SurfpoolAccountsDataRpc {
351    type Metadata = Option<RunloopContext>;
352
353    fn get_account_info(
354        &self,
355        meta: Self::Metadata,
356        pubkey_str: String,
357        config: Option<RpcAccountInfoConfig>,
358    ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>> {
359        let config = config.unwrap_or_default();
360        let pubkey = match verify_pubkey(&pubkey_str) {
361            Ok(res) => res,
362            Err(e) => return e.into(),
363        };
364        #[cfg(feature = "prometheus")]
365        let rpc_start = std::time::Instant::now();
366
367        let SurfnetRpcContext {
368            svm_locker,
369            remote_ctx,
370        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
371            Ok(res) => res,
372            Err(e) => return e.into(),
373        };
374
375        Box::pin(async move {
376            let SvmAccessContext {
377                slot,
378                inner: account_update,
379                ..
380            } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
381
382            #[cfg(feature = "prometheus")]
383            if let Some(m) = crate::telemetry::metrics() {
384                m.record_rpc_request("getAccountInfo", rpc_start.elapsed().as_millis() as u64);
385            }
386            svm_locker.write_account_update(account_update.clone());
387
388            let ui_account = if let Some(((pubkey, account), token_data)) =
389                account_update.map_account_with_token_data()
390            {
391                Some(
392                    svm_locker
393                        .account_to_rpc_keyed_account(
394                            &pubkey,
395                            &account,
396                            &config,
397                            token_data.map(|(mint, _)| mint),
398                        )
399                        .account,
400                )
401            } else {
402                None
403            };
404
405            Ok(RpcResponse {
406                context: RpcResponseContext::new(slot),
407                value: ui_account,
408            })
409        })
410    }
411
412    fn get_multiple_accounts(
413        &self,
414        meta: Self::Metadata,
415        pubkeys_str: Vec<String>,
416        config: Option<RpcAccountInfoConfig>,
417    ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>> {
418        let config = config.unwrap_or_default();
419        let pubkeys = match pubkeys_str
420            .iter()
421            .map(|s| verify_pubkey(s))
422            .collect::<SurfpoolResult<Vec<_>>>()
423        {
424            Ok(p) => p,
425            Err(e) => return e.into(),
426        };
427
428        let SurfnetRpcContext {
429            svm_locker,
430            remote_ctx,
431        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
432            Ok(res) => res,
433            Err(e) => return e.into(),
434        };
435
436        #[cfg(feature = "prometheus")]
437        let rpc_start = std::time::Instant::now();
438
439        Box::pin(async move {
440            let SvmAccessContext {
441                slot,
442                inner: account_updates,
443                ..
444            } = svm_locker
445                .get_multiple_accounts(&remote_ctx, &pubkeys, None)
446                .await?;
447
448            #[cfg(feature = "prometheus")]
449            if let Some(m) = crate::telemetry::metrics() {
450                m.record_rpc_request(
451                    "getMultipleAccounts",
452                    rpc_start.elapsed().as_millis() as u64,
453                );
454            }
455
456            svm_locker.write_multiple_account_updates(&account_updates);
457
458            // Convert account updates to UI accounts, order is already preserved by get_multiple_accounts
459            let mut ui_accounts = vec![];
460            for account_update in account_updates.into_iter() {
461                if let Some(((pubkey, account), token_data)) =
462                    account_update.map_account_with_token_data()
463                {
464                    ui_accounts.push(Some(
465                        svm_locker
466                            .account_to_rpc_keyed_account(
467                                &pubkey,
468                                &account,
469                                &config,
470                                token_data.map(|(mint, _)| mint),
471                            )
472                            .account,
473                    ));
474                } else {
475                    ui_accounts.push(None);
476                }
477            }
478
479            Ok(RpcResponse {
480                context: RpcResponseContext::new(slot),
481                value: ui_accounts,
482            })
483        })
484    }
485
486    fn get_block_commitment(
487        &self,
488        meta: Self::Metadata,
489        block: Slot,
490    ) -> Result<RpcBlockCommitment<BlockCommitmentArray>> {
491        // get the info we need and free up lock before validation
492        let (current_slot, block_exists) = meta
493            .with_svm_reader(|svm_reader| {
494                svm_reader
495                    .blocks
496                    .contains_key(&block)
497                    .map_err(SurfpoolError::from)
498                    .map(|exists| (svm_reader.get_latest_absolute_slot(), exists))
499            })
500            .map_err(Into::<jsonrpc_core::Error>::into)??;
501
502        // block is valid if it exists in our block history or it's not too far in the future
503        if !block_exists && block > current_slot {
504            return Err(jsonrpc_core::Error::invalid_params(format!(
505                "Block {} not found",
506                block
507            )));
508        }
509
510        let commitment_array = [0u64; 32];
511
512        Ok(RpcBlockCommitment {
513            commitment: Some(commitment_array),
514            total_stake: 0,
515        })
516    }
517
518    // SPL Token-specific RPC endpoints
519    // See https://github.com/solana-labs/solana-program-library/releases/tag/token-v2.0.0 for
520    // program details
521
522    fn get_token_account_balance(
523        &self,
524        meta: Self::Metadata,
525        pubkey_str: String,
526        commitment: Option<CommitmentConfig>,
527    ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>> {
528        let pubkey = match verify_pubkey(&pubkey_str) {
529            Ok(res) => res,
530            Err(e) => return e.into(),
531        };
532
533        let SurfnetRpcContext {
534            svm_locker,
535            remote_ctx,
536        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
537            Ok(res) => res,
538            Err(e) => return e.into(),
539        };
540
541        Box::pin(async move {
542            let token_account_result = svm_locker
543                .get_account(&remote_ctx, &pubkey, None)
544                .await?
545                .inner;
546
547            svm_locker.write_account_update(token_account_result.clone());
548
549            let token_account = token_account_result.map_account()?;
550
551            let (mint_pubkey, _amount) = if is_supported_token_program(&token_account.owner) {
552                let unpacked_token_account = TokenAccount::unpack(&token_account.data)?;
553                (
554                    unpacked_token_account.mint(),
555                    unpacked_token_account.amount(),
556                )
557            } else {
558                return Err(SurfpoolError::invalid_account_data(
559                    pubkey,
560                    "Account is not owned by Token or Token-2022 program",
561                    None::<String>,
562                )
563                .into());
564            };
565
566            let SvmAccessContext {
567                slot,
568                inner: mint_account_result,
569                ..
570            } = svm_locker
571                .get_account(&remote_ctx, &mint_pubkey, None)
572                .await?;
573
574            svm_locker.write_account_update(mint_account_result.clone());
575
576            let mint_account = mint_account_result.map_account()?;
577
578            let token_decimals = if is_supported_token_program(&mint_account.owner) {
579                let unpacked_mint_account = MintAccount::unpack(&mint_account.data)?;
580                unpacked_mint_account.decimals()
581            } else {
582                return Err(SurfpoolError::invalid_account_data(
583                    mint_pubkey,
584                    "Mint account is not owned by Token or Token-2022 program",
585                    None::<String>,
586                )
587                .into());
588            };
589
590            Ok(RpcResponse {
591                context: RpcResponseContext::new(slot),
592                value: {
593                    parse_token_v3(
594                        &token_account.data,
595                        Some(&SplTokenAdditionalDataV2 {
596                            decimals: token_decimals,
597                            ..Default::default()
598                        }),
599                    )
600                    .ok()
601                    .and_then(|t| match t {
602                        TokenAccountType::Account(account) => Some(account.token_amount),
603                        _ => None,
604                    })
605                },
606            })
607        })
608    }
609
610    fn get_token_supply(
611        &self,
612        meta: Self::Metadata,
613        mint_str: String,
614        commitment: Option<CommitmentConfig>,
615    ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>> {
616        let mint_pubkey = match verify_pubkey(&mint_str) {
617            Ok(pubkey) => pubkey,
618            Err(e) => return e.into(),
619        };
620
621        let SurfnetRpcContext {
622            svm_locker,
623            remote_ctx,
624        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
625            Ok(res) => res,
626            Err(e) => return e.into(),
627        };
628
629        Box::pin(async move {
630            let SvmAccessContext {
631                slot,
632                inner: mint_account_result,
633                ..
634            } = svm_locker
635                .get_account(&remote_ctx, &mint_pubkey, None)
636                .await?;
637
638            svm_locker.write_account_update(mint_account_result.clone());
639
640            let mint_account = mint_account_result.map_account()?;
641
642            if !is_supported_token_program(&mint_account.owner) {
643                return Err(SurfpoolError::invalid_account_data(
644                    mint_pubkey,
645                    "Account is not a token mint account",
646                    None::<String>,
647                )
648                .into());
649            }
650
651            let mint_data = MintAccount::unpack(&mint_account.data)?;
652
653            Ok(RpcResponse {
654                context: RpcResponseContext::new(slot),
655                value: {
656                    parse_token_v3(
657                        &mint_account.data,
658                        Some(&SplTokenAdditionalDataV2 {
659                            decimals: mint_data.decimals(),
660                            ..Default::default()
661                        }),
662                    )
663                    .ok()
664                    .and_then(|t| match t {
665                        TokenAccountType::Mint(mint) => {
666                            let supply_u64 = mint.supply.parse::<u64>().unwrap_or(0);
667                            let ui_amount = if supply_u64 == 0 {
668                                Some(0.0)
669                            } else {
670                                let divisor = 10_u64.pow(mint.decimals as u32);
671                                Some(supply_u64 as f64 / divisor as f64)
672                            };
673
674                            Some(UiTokenAmount {
675                                amount: mint.supply.clone(),
676                                decimals: mint.decimals,
677                                ui_amount,
678                                ui_amount_string: mint.supply,
679                            })
680                        }
681                        _ => None,
682                    })
683                    .ok_or_else(|| {
684                        SurfpoolError::invalid_account_data(
685                            mint_pubkey,
686                            "Failed to parse token mint account",
687                            None::<String>,
688                        )
689                    })?
690                },
691            })
692        })
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use solana_account::Account;
699    use solana_keypair::Keypair;
700    use solana_program_option::COption;
701    use solana_program_pack::Pack;
702    use solana_pubkey::Pubkey;
703    use solana_signer::Signer;
704    use solana_system_interface::instruction::create_account;
705    use solana_transaction::Transaction;
706    use spl_associated_token_account_interface::{
707        address::get_associated_token_address_with_program_id,
708        instruction::create_associated_token_account,
709    };
710    use spl_token_2022_interface::instruction::{initialize_mint2, mint_to, transfer_checked};
711    use spl_token_interface::state::{Account as TokenAccount, AccountState, Mint};
712
713    use super::*;
714    use crate::{
715        surfnet::{GetAccountResult, remote::SurfnetRemoteClient},
716        tests::helpers::TestSetup,
717        types::SyntheticBlockhash,
718    };
719
720    #[ignore = "connection-required"]
721    #[tokio::test(flavor = "multi_thread")]
722    async fn test_get_token_account_balance() {
723        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
724
725        let mint_pk = Pubkey::new_unique();
726
727        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
728            svm_reader
729                .inner
730                .minimum_balance_for_rent_exemption(Mint::LEN)
731        });
732
733        let mut data = [0; Mint::LEN];
734
735        let default = Mint {
736            decimals: 6,
737            supply: 1000000000000000,
738            is_initialized: true,
739            ..Default::default()
740        };
741        default.pack_into_slice(&mut data);
742
743        let mint_account = Account {
744            lamports: minimum_rent,
745            owner: spl_token_interface::ID,
746            executable: false,
747            rent_epoch: 0,
748            data: data.to_vec(),
749        };
750
751        setup
752            .context
753            .svm_locker
754            .write_account_update(GetAccountResult::FoundAccount(mint_pk, mint_account, true));
755
756        let token_account_pk = Pubkey::new_unique();
757
758        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
759            svm_reader
760                .inner
761                .minimum_balance_for_rent_exemption(TokenAccount::LEN)
762        });
763
764        let mut data = [0; TokenAccount::LEN];
765
766        let default = TokenAccount {
767            mint: mint_pk,
768            owner: spl_token_interface::ID,
769            state: AccountState::Initialized,
770            amount: 100 * 1000000,
771            ..Default::default()
772        };
773        default.pack_into_slice(&mut data);
774
775        let token_account = Account {
776            lamports: minimum_rent,
777            owner: spl_token_interface::ID,
778            executable: false,
779            rent_epoch: 0,
780            data: data.to_vec(),
781        };
782
783        setup
784            .context
785            .svm_locker
786            .write_account_update(GetAccountResult::FoundAccount(
787                token_account_pk,
788                token_account,
789                true,
790            ));
791
792        let res = setup
793            .rpc
794            .get_token_account_balance(Some(setup.context), token_account_pk.to_string(), None)
795            .await
796            .unwrap();
797
798        assert_eq!(
799            res.value.unwrap(),
800            UiTokenAmount {
801                amount: String::from("100000000"),
802                decimals: 6,
803                ui_amount: Some(100.0),
804                ui_amount_string: String::from("100")
805            }
806        );
807    }
808
809    #[test]
810    fn test_get_block_commitment_past_slot() {
811        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
812        let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
813        let past_slot = if current_slot > 10 {
814            current_slot - 10
815        } else {
816            0
817        };
818
819        let result = setup
820            .rpc
821            .get_block_commitment(Some(setup.context), past_slot)
822            .unwrap();
823
824        // Should return commitment data for past slot
825        assert!(result.commitment.is_some());
826        assert_eq!(result.total_stake, 0);
827    }
828
829    #[test]
830    fn test_get_block_commitment_with_actual_block() {
831        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
832
833        // create a block in the SVM's block history
834        let test_slot = 12345;
835        setup.context.svm_locker.with_svm_writer(|svm_writer| {
836            use crate::surfnet::BlockHeader;
837
838            svm_writer
839                .blocks
840                .store(
841                    test_slot,
842                    BlockHeader {
843                        hash: SyntheticBlockhash::new(test_slot).to_string(),
844                        previous_blockhash: SyntheticBlockhash::new(test_slot - 1).to_string(),
845                        parent_slot: test_slot - 1,
846                        block_time: chrono::Utc::now().timestamp_millis(),
847                        block_height: test_slot,
848                        signatures: vec![],
849                    },
850                )
851                .unwrap();
852        });
853
854        let result = setup
855            .rpc
856            .get_block_commitment(Some(setup.context), test_slot)
857            .unwrap();
858
859        // should return commitment data for the existing block
860        assert!(result.commitment.is_some());
861        assert_eq!(result.total_stake, 0);
862    }
863
864    #[test]
865    fn test_get_block_commitment_no_metadata() {
866        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
867
868        let result = setup.rpc.get_block_commitment(None, 123);
869
870        assert!(result.is_err());
871        // This should fail because meta is None, triggering the SurfpoolError::missing_context() path
872    }
873
874    #[test]
875    fn test_get_block_commitment_future_slot_error() {
876        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
877        let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
878        let future_slot = current_slot + 1000;
879
880        let result = setup
881            .rpc
882            .get_block_commitment(Some(setup.context), future_slot);
883
884        // Should return an error for future slots
885        assert!(result.is_err());
886
887        let error = result.unwrap_err();
888        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
889        assert!(error.message.contains("Block") && error.message.contains("not found"));
890    }
891
892    #[tokio::test(flavor = "multi_thread")]
893    async fn test_get_token_supply_with_real_mint() {
894        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
895
896        let mint_pubkey = Pubkey::new_unique();
897
898        // Create mint account data
899        let mut mint_data = [0u8; Mint::LEN];
900        let mint = Mint {
901            mint_authority: COption::Some(Pubkey::new_unique()),
902            supply: 1_000_000_000_000,
903            decimals: 6,
904            is_initialized: true,
905            freeze_authority: COption::None,
906        };
907        Mint::pack(mint, &mut mint_data).unwrap();
908
909        let mint_account = Account {
910            lamports: setup.context.svm_locker.with_svm_reader(|svm_reader| {
911                svm_reader
912                    .inner
913                    .minimum_balance_for_rent_exemption(Mint::LEN)
914            }),
915            data: mint_data.to_vec(),
916            owner: spl_token_interface::id(),
917            executable: false,
918            rent_epoch: 0,
919        };
920
921        setup.context.svm_locker.with_svm_writer(|svm_writer| {
922            svm_writer
923                .set_account(&mint_pubkey, mint_account.clone())
924                .unwrap();
925        });
926
927        let res = setup
928            .rpc
929            .get_token_supply(
930                Some(setup.context),
931                mint_pubkey.to_string(),
932                Some(CommitmentConfig::confirmed()),
933            )
934            .await
935            .unwrap();
936
937        assert_eq!(res.value.amount, "1000000000000");
938        assert_eq!(res.value.decimals, 6);
939        assert_eq!(res.value.ui_amount_string, "1000000000000");
940    }
941
942    #[tokio::test(flavor = "multi_thread")]
943    async fn test_invalid_pubkey_format() {
944        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
945
946        // test various invalid pubkey formats
947        let invalid_pubkeys = vec![
948            "",
949            "invalid",
950            "123",
951            "not-a-valid-base58-string!@#$",
952            "11111111111111111111111111111111111111111111111111111111111111111",
953            "invalid-base58-characters-ö",
954        ];
955
956        for invalid_pubkey in invalid_pubkeys {
957            let res = setup
958                .rpc
959                .get_token_supply(
960                    Some(setup.context.clone()),
961                    invalid_pubkey.to_string(),
962                    Some(CommitmentConfig::confirmed()),
963                )
964                .await;
965
966            assert!(
967                res.is_err(),
968                "Should fail for invalid pubkey: '{}'",
969                invalid_pubkey
970            );
971
972            let error_msg = res.unwrap_err().to_string();
973            assert!(
974                error_msg.contains("Invalid") || error_msg.contains("invalid"),
975                "Error should mention invalidity for '{}': {}",
976                invalid_pubkey,
977                error_msg
978            );
979        }
980
981        println!("✅ All invalid pubkey formats correctly rejected");
982    }
983
984    #[tokio::test(flavor = "multi_thread")]
985    async fn test_nonexistent_account() {
986        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
987
988        // valid pubkey format but nonexistent account
989        let nonexistent_mint = Pubkey::new_unique();
990
991        let res = setup
992            .rpc
993            .get_token_supply(
994                Some(setup.context),
995                nonexistent_mint.to_string(),
996                Some(CommitmentConfig::confirmed()),
997            )
998            .await;
999
1000        assert!(res.is_err(), "Should fail for nonexistent account");
1001
1002        let error_msg = res.unwrap_err().to_string();
1003        assert!(
1004            error_msg.contains("not found") || error_msg.contains("account"),
1005            "Error should mention account not found: {}",
1006            error_msg
1007        );
1008
1009        println!("✅ Nonexistent account correctly rejected: {}", error_msg);
1010    }
1011
1012    #[tokio::test(flavor = "multi_thread")]
1013    async fn test_invalid_mint_data() {
1014        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
1015
1016        let fake_mint = Pubkey::new_unique();
1017
1018        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1019            // create an account owned by SPL Token but with invalid data
1020            let invalid_mint_account = Account {
1021                lamports: 1000000,
1022                data: vec![0xFF; 50], // invalid mint data (random bytes)
1023                owner: spl_token_interface::id(),
1024                executable: false,
1025                rent_epoch: 0,
1026            };
1027
1028            svm_writer
1029                .set_account(&fake_mint, invalid_mint_account)
1030                .unwrap();
1031        });
1032
1033        let res = setup
1034            .rpc
1035            .get_token_supply(
1036                Some(setup.context),
1037                fake_mint.to_string(),
1038                Some(CommitmentConfig::confirmed()),
1039            )
1040            .await;
1041
1042        assert!(
1043            res.is_err(),
1044            "Should fail for account with invalid mint data"
1045        );
1046
1047        let error_msg = res.unwrap_err().to_string();
1048        assert!(
1049            error_msg.eq("Parse error: Failed to unpack mint account"),
1050            "Incorrect error received: {}",
1051            error_msg
1052        );
1053
1054        println!("✅ Invalid mint data correctly rejected: {}", error_msg);
1055    }
1056
1057    #[ignore = "requires-network"]
1058    #[tokio::test(flavor = "multi_thread")]
1059    async fn test_remote_rpc_failure() {
1060        // test with invalid remote RPC URL
1061        let bad_remote_client =
1062            SurfnetRemoteClient::new("https://invalid-url-that-doesnt-exist.com");
1063        let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1064        setup.context.remote_rpc_client = Some(bad_remote_client);
1065
1066        let nonexistent_mint = Pubkey::new_unique();
1067
1068        let res = setup
1069            .rpc
1070            .get_token_supply(
1071                Some(setup.context),
1072                nonexistent_mint.to_string(),
1073                Some(CommitmentConfig::confirmed()),
1074            )
1075            .await;
1076
1077        assert!(res.is_err(), "Should fail when remote RPC is unreachable");
1078
1079        let error_msg = res.unwrap_err().to_string();
1080        println!("✅ Remote RPC failure handled: {}", error_msg);
1081    }
1082
1083    #[tokio::test(flavor = "multi_thread")]
1084    async fn test_transfer_token() {
1085        // Create connection to local validator
1086        let client = TestSetup::new(SurfpoolAccountsDataRpc);
1087        let recent_blockhash = client
1088            .context
1089            .svm_locker
1090            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1091
1092        // Generate a new keypair for the fee payer
1093        let fee_payer = Keypair::new();
1094
1095        // Generate a second keypair for the token recipient
1096        let recipient = Keypair::new();
1097
1098        // Airdrop 1 SOL to fee payer
1099        client
1100            .context
1101            .svm_locker
1102            .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1103            .unwrap()
1104            .unwrap();
1105
1106        // Airdrop 1 SOL to recipient for rent exemption
1107        client
1108            .context
1109            .svm_locker
1110            .airdrop(&recipient.pubkey(), 1_000_000_000)
1111            .unwrap()
1112            .unwrap();
1113
1114        // Generate keypair to use as address of mint
1115        let mint = Keypair::new();
1116
1117        // Get default mint account size (in bytes), no extensions enabled
1118        let mint_space = Mint::LEN;
1119        let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1120            svm_reader
1121                .inner
1122                .minimum_balance_for_rent_exemption(mint_space)
1123        });
1124
1125        // Instruction to create new account for mint (token 2022 program)
1126        let create_account_instruction = create_account(
1127            &fee_payer.pubkey(),             // payer
1128            &mint.pubkey(),                  // new account (mint)
1129            mint_rent,                       // lamports
1130            mint_space as u64,               // space
1131            &spl_token_2022_interface::id(), // program id
1132        );
1133
1134        // Instruction to initialize mint account data
1135        let initialize_mint_instruction = initialize_mint2(
1136            &spl_token_2022_interface::id(),
1137            &mint.pubkey(),            // mint
1138            &fee_payer.pubkey(),       // mint authority
1139            Some(&fee_payer.pubkey()), // freeze authority
1140            2,                         // decimals
1141        )
1142        .unwrap();
1143
1144        // Calculate the associated token account address for fee_payer
1145        let source_token_address = get_associated_token_address_with_program_id(
1146            &fee_payer.pubkey(),             // owner
1147            &mint.pubkey(),                  // mint
1148            &spl_token_2022_interface::id(), // program_id
1149        );
1150
1151        // Instruction to create associated token account for fee_payer
1152        let create_source_ata_instruction = create_associated_token_account(
1153            &fee_payer.pubkey(),             // funding address
1154            &fee_payer.pubkey(),             // wallet address
1155            &mint.pubkey(),                  // mint address
1156            &spl_token_2022_interface::id(), // program id
1157        );
1158
1159        // Calculate the associated token account address for recipient
1160        let destination_token_address = get_associated_token_address_with_program_id(
1161            &recipient.pubkey(),             // owner
1162            &mint.pubkey(),                  // mint
1163            &spl_token_2022_interface::id(), // program_id
1164        );
1165
1166        // Instruction to create associated token account for recipient
1167        let create_destination_ata_instruction = create_associated_token_account(
1168            &fee_payer.pubkey(),             // funding address
1169            &recipient.pubkey(),             // wallet address
1170            &mint.pubkey(),                  // mint address
1171            &spl_token_2022_interface::id(), // program id
1172        );
1173
1174        // Amount of tokens to mint (100 tokens with 2 decimal places)
1175        let amount = 100_00;
1176
1177        // Create mint_to instruction to mint tokens to the source token account
1178        let mint_to_instruction = mint_to(
1179            &spl_token_2022_interface::id(),
1180            &mint.pubkey(),         // mint
1181            &source_token_address,  // destination
1182            &fee_payer.pubkey(),    // authority
1183            &[&fee_payer.pubkey()], // signer
1184            amount,                 // amount
1185        )
1186        .unwrap();
1187
1188        // Create transaction and add instructions
1189        let transaction = Transaction::new_signed_with_payer(
1190            &[
1191                create_account_instruction,
1192                initialize_mint_instruction,
1193                create_source_ata_instruction,
1194                create_destination_ata_instruction,
1195                mint_to_instruction,
1196            ],
1197            Some(&fee_payer.pubkey()),
1198            &[&fee_payer, &mint],
1199            recent_blockhash,
1200        );
1201
1202        let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1203        client
1204            .context
1205            .svm_locker
1206            .process_transaction(&None, transaction.into(), status_tx.clone(), false, true)
1207            .await
1208            .unwrap();
1209
1210        println!("Mint Address: {}", mint.pubkey());
1211        println!("Recipient Address: {}", recipient.pubkey());
1212        println!("Source Token Account Address: {}", source_token_address);
1213        println!(
1214            "Destination Token Account Address: {}",
1215            destination_token_address
1216        );
1217        println!("Minted {} tokens to the source token account", amount);
1218
1219        // Get the latest blockhash for the transfer transaction
1220        let recent_blockhash = client
1221            .context
1222            .svm_locker
1223            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1224
1225        // Amount of tokens to transfer (0.50 tokens with 2 decimals)
1226        let transfer_amount = 50;
1227
1228        // Create transfer_checked instruction to send tokens from source to destination
1229        let transfer_instruction = transfer_checked(
1230            &spl_token_2022_interface::id(), // program id
1231            &source_token_address,           // source
1232            &mint.pubkey(),                  // mint
1233            &destination_token_address,      // destination
1234            &fee_payer.pubkey(),             // owner of source
1235            &[&fee_payer.pubkey()],          // signers
1236            transfer_amount,                 // amount
1237            2,                               // decimals
1238        )
1239        .unwrap();
1240
1241        // Create transaction for transferring tokens
1242        let transaction = Transaction::new_signed_with_payer(
1243            &[transfer_instruction],
1244            Some(&fee_payer.pubkey()),
1245            &[&fee_payer],
1246            recent_blockhash,
1247        );
1248
1249        client
1250            .context
1251            .svm_locker
1252            .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1253            .await
1254            .unwrap();
1255
1256        println!("Successfully transferred 0.50 tokens from sender to recipient");
1257
1258        let source_balance = client
1259            .rpc
1260            .get_token_account_balance(
1261                Some(client.context.clone()),
1262                source_token_address.to_string(),
1263                Some(CommitmentConfig::confirmed()),
1264            )
1265            .await
1266            .unwrap();
1267
1268        let destination_balance = client
1269            .rpc
1270            .get_token_account_balance(
1271                Some(client.context.clone()),
1272                destination_token_address.to_string(),
1273                Some(CommitmentConfig::confirmed()),
1274            )
1275            .await
1276            .unwrap();
1277
1278        println!(
1279            "Source Token Account Balance: {} tokens ({})",
1280            source_balance.value.as_ref().unwrap().ui_amount.unwrap(),
1281            source_balance.value.as_ref().unwrap().amount
1282        );
1283        println!(
1284            "Destination Token Account Balance: {} tokens ({})",
1285            destination_balance
1286                .value
1287                .as_ref()
1288                .unwrap()
1289                .ui_amount
1290                .unwrap(),
1291            destination_balance.value.as_ref().unwrap().amount
1292        );
1293
1294        assert_eq!(source_balance.value.unwrap().amount, "9950");
1295        assert_eq!(destination_balance.value.unwrap().amount, "50");
1296    }
1297
1298    #[tokio::test(flavor = "multi_thread")]
1299    async fn test_get_account_info() {
1300        // Create connection to local validator
1301        let client = TestSetup::new(SurfpoolAccountsDataRpc);
1302        let recent_blockhash = client
1303            .context
1304            .svm_locker
1305            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1306
1307        // Generate a new keypair for the fee payer
1308        let fee_payer = Keypair::new();
1309
1310        // Generate a second keypair for the token recipient
1311        let recipient = Keypair::new();
1312
1313        // Airdrop 1 SOL to fee payer
1314        client
1315            .context
1316            .svm_locker
1317            .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1318            .unwrap()
1319            .unwrap();
1320
1321        // Airdrop 1 SOL to recipient for rent exemption
1322        client
1323            .context
1324            .svm_locker
1325            .airdrop(&recipient.pubkey(), 1_000_000_000)
1326            .unwrap()
1327            .unwrap();
1328
1329        // Generate keypair to use as address of mint
1330        let mint = Keypair::new();
1331
1332        // Get default mint account size (in bytes), no extensions enabled
1333        let mint_space = Mint::LEN;
1334        let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1335            svm_reader
1336                .inner
1337                .minimum_balance_for_rent_exemption(mint_space)
1338        });
1339
1340        // Instruction to create new account for mint (token 2022 program)
1341        let create_account_instruction = create_account(
1342            &fee_payer.pubkey(),             // payer
1343            &mint.pubkey(),                  // new account (mint)
1344            mint_rent,                       // lamports
1345            mint_space as u64,               // space
1346            &spl_token_2022_interface::id(), // program id
1347        );
1348
1349        // Instruction to initialize mint account data
1350        let initialize_mint_instruction = initialize_mint2(
1351            &spl_token_2022_interface::id(),
1352            &mint.pubkey(),            // mint
1353            &fee_payer.pubkey(),       // mint authority
1354            Some(&fee_payer.pubkey()), // freeze authority
1355            2,                         // decimals
1356        )
1357        .unwrap();
1358
1359        // Calculate the associated token account address for fee_payer
1360        let source_token_address = get_associated_token_address_with_program_id(
1361            &fee_payer.pubkey(),             // owner
1362            &mint.pubkey(),                  // mint
1363            &spl_token_2022_interface::id(), // program_id
1364        );
1365
1366        // Instruction to create associated token account for fee_payer
1367        let create_source_ata_instruction = create_associated_token_account(
1368            &fee_payer.pubkey(),             // funding address
1369            &fee_payer.pubkey(),             // wallet address
1370            &mint.pubkey(),                  // mint address
1371            &spl_token_2022_interface::id(), // program id
1372        );
1373
1374        // Calculate the associated token account address for recipient
1375        let destination_token_address = get_associated_token_address_with_program_id(
1376            &recipient.pubkey(),             // owner
1377            &mint.pubkey(),                  // mint
1378            &spl_token_2022_interface::id(), // program_id
1379        );
1380
1381        // Instruction to create associated token account for recipient
1382        let create_destination_ata_instruction = create_associated_token_account(
1383            &fee_payer.pubkey(),             // funding address
1384            &recipient.pubkey(),             // wallet address
1385            &mint.pubkey(),                  // mint address
1386            &spl_token_2022_interface::id(), // program id
1387        );
1388
1389        // Amount of tokens to mint (100 tokens with 2 decimal places)
1390        let amount = 100_00;
1391
1392        // Create mint_to instruction to mint tokens to the source token account
1393        let mint_to_instruction = mint_to(
1394            &spl_token_2022_interface::id(),
1395            &mint.pubkey(),         // mint
1396            &source_token_address,  // destination
1397            &fee_payer.pubkey(),    // authority
1398            &[&fee_payer.pubkey()], // signer
1399            amount,                 // amount
1400        )
1401        .unwrap();
1402
1403        // Create transaction and add instructions
1404        let transaction = Transaction::new_signed_with_payer(
1405            &[
1406                create_account_instruction,
1407                initialize_mint_instruction,
1408                create_source_ata_instruction,
1409                create_destination_ata_instruction,
1410                mint_to_instruction,
1411            ],
1412            Some(&fee_payer.pubkey()),
1413            &[&fee_payer, &mint],
1414            recent_blockhash,
1415        );
1416
1417        let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1418        // Send and confirm transaction
1419        client
1420            .context
1421            .svm_locker
1422            .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1423            .await
1424            .unwrap();
1425
1426        println!("Mint Address: {}", mint.pubkey());
1427        println!("Recipient Address: {}", recipient.pubkey());
1428        println!("Source Token Account Address: {}", source_token_address);
1429        println!(
1430            "Destination Token Account Address: {}",
1431            destination_token_address
1432        );
1433        println!("Minted {} tokens to the source token account", amount);
1434
1435        // Get the latest blockhash for the transfer transaction
1436        let recent_blockhash = client
1437            .context
1438            .svm_locker
1439            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1440
1441        // Amount of tokens to transfer (0.50 tokens with 2 decimals)
1442        let transfer_amount = 50;
1443
1444        // Create transfer_checked instruction to send tokens from source to destination
1445        let transfer_instruction = transfer_checked(
1446            &spl_token_2022_interface::id(), // program id
1447            &source_token_address,           // source
1448            &mint.pubkey(),                  // mint
1449            &destination_token_address,      // destination
1450            &fee_payer.pubkey(),             // owner of source
1451            &[&fee_payer.pubkey()],          // signers
1452            transfer_amount,                 // amount
1453            2,                               // decimals
1454        )
1455        .unwrap();
1456
1457        // Create transaction for transferring tokens
1458        let transaction = Transaction::new_signed_with_payer(
1459            &[transfer_instruction],
1460            Some(&fee_payer.pubkey()),
1461            &[&fee_payer],
1462            recent_blockhash,
1463        );
1464        let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1465        // Send and confirm transaction
1466        client
1467            .context
1468            .svm_locker
1469            .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1470            .await
1471            .unwrap();
1472
1473        println!(
1474            "Successfully transferred 0.50 tokens from {} to {}",
1475            source_token_address, destination_token_address
1476        );
1477
1478        let source_account_info = client
1479            .rpc
1480            .get_account_info(
1481                Some(client.context.clone()),
1482                source_token_address.to_string(),
1483                Some(RpcAccountInfoConfig {
1484                    encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1485                    ..Default::default()
1486                }),
1487            )
1488            .await
1489            .unwrap();
1490
1491        let destination_account_info = client
1492            .rpc
1493            .get_account_info(
1494                Some(client.context.clone()),
1495                destination_token_address.to_string(),
1496                Some(RpcAccountInfoConfig {
1497                    encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1498                    ..Default::default()
1499                }),
1500            )
1501            .await
1502            .unwrap();
1503
1504        println!("Source Account Info: {:?}", source_account_info);
1505        println!("Destination Account Info: {:?}", destination_account_info);
1506
1507        let source_account = source_account_info.value.unwrap();
1508        if let solana_account_decoder::UiAccountData::Json(parsed) = source_account.data {
1509            let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1510                .as_str()
1511                .unwrap();
1512            assert_eq!(amount, "9950");
1513        } else {
1514            panic!("source account data was not in json parsed format");
1515        }
1516
1517        let destination_account = destination_account_info.value.unwrap();
1518        if let solana_account_decoder::UiAccountData::Json(parsed) = destination_account.data {
1519            let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1520                .as_str()
1521                .unwrap();
1522            assert_eq!(amount, "50");
1523        } else {
1524            panic!("destination account data was not in json parsed format");
1525        }
1526    }
1527
1528    #[ignore = "requires-network"]
1529    #[tokio::test(flavor = "multi_thread")]
1530    async fn test_get_multiple_accounts_with_remote_preserves_order() {
1531        // This test checks that order is preserved when mixing local and remote accounts
1532        let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1533
1534        // Add a remote client to trigger get_multiple_accounts_with_remote_fallback path
1535        let remote_client = SurfnetRemoteClient::new("https://api.mainnet-beta.solana.com");
1536        setup.context.remote_rpc_client = Some(remote_client);
1537
1538        // Create three accounts with different lamport amounts
1539        let pk1 = Pubkey::new_unique();
1540        let pk2 = Pubkey::new_unique();
1541        let pk3 = Pubkey::new_unique();
1542
1543        println!("{}", pk1);
1544        println!("{}", pk2);
1545        println!("{}", pk3);
1546
1547        let account1 = Account {
1548            lamports: 1_000_000,
1549            data: vec![],
1550            owner: solana_pubkey::Pubkey::default(),
1551            executable: false,
1552            rent_epoch: 0,
1553        };
1554
1555        let account3 = Account {
1556            lamports: 3_000_000,
1557            data: vec![],
1558            owner: solana_pubkey::Pubkey::default(),
1559            executable: false,
1560            rent_epoch: 0,
1561        };
1562
1563        // Store only account1 and account3 locally (account2 will need remote fetch)
1564        setup
1565            .context
1566            .svm_locker
1567            .write_account_update(GetAccountResult::FoundAccount(pk1, account1, true));
1568        setup
1569            .context
1570            .svm_locker
1571            .write_account_update(GetAccountResult::FoundAccount(pk3, account3, true));
1572
1573        // Request accounts in order: [pk1, pk2, pk3]
1574        // pk1 and pk3 are local, pk2 is missing (will try remote fetch and fail)
1575        let pubkeys_str = vec![pk1.to_string(), pk2.to_string(), pk3.to_string()];
1576
1577        let response = setup
1578            .rpc
1579            .get_multiple_accounts(
1580                Some(setup.context),
1581                pubkeys_str,
1582                Some(RpcAccountInfoConfig::default()),
1583            )
1584            .await
1585            .unwrap();
1586
1587        // Verify we got 3 results
1588        assert_eq!(response.value.len(), 3);
1589
1590        println!("{:?}", response);
1591
1592        // First account should be account1 with 1M lamports
1593        assert!(response.value[0].is_some());
1594        assert_eq!(
1595            response.value[0].as_ref().unwrap().lamports,
1596            1_000_000,
1597            "First element should be account1"
1598        );
1599
1600        // Second account should be None (pk2 doesn't exist locally or remotely)
1601        assert!(
1602            response.value[1].is_none(),
1603            "Second element should be None for missing pk2"
1604        );
1605
1606        // Third account should be account3 with 3M lamports
1607        assert!(response.value[2].is_some());
1608        assert_eq!(
1609            response.value[2].as_ref().unwrap().lamports,
1610            3_000_000,
1611            "Third element should be account3"
1612        );
1613
1614        println!("✅ Account order preserved with remote: [1M lamports, None, 3M lamports]");
1615    }
1616}