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
365        let SurfnetRpcContext {
366            svm_locker,
367            remote_ctx,
368        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
369            Ok(res) => res,
370            Err(e) => return e.into(),
371        };
372
373        Box::pin(async move {
374            let SvmAccessContext {
375                slot,
376                inner: account_update,
377                ..
378            } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
379            svm_locker.write_account_update(account_update.clone());
380
381            let ui_account = if let Some(((pubkey, account), token_data)) =
382                account_update.map_account_with_token_data()
383            {
384                Some(
385                    svm_locker
386                        .account_to_rpc_keyed_account(
387                            &pubkey,
388                            &account,
389                            &config,
390                            token_data.map(|(mint, _)| mint),
391                        )
392                        .account,
393                )
394            } else {
395                None
396            };
397
398            Ok(RpcResponse {
399                context: RpcResponseContext::new(slot),
400                value: ui_account,
401            })
402        })
403    }
404
405    fn get_multiple_accounts(
406        &self,
407        meta: Self::Metadata,
408        pubkeys_str: Vec<String>,
409        config: Option<RpcAccountInfoConfig>,
410    ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>> {
411        let config = config.unwrap_or_default();
412        let pubkeys = match pubkeys_str
413            .iter()
414            .map(|s| verify_pubkey(s))
415            .collect::<SurfpoolResult<Vec<_>>>()
416        {
417            Ok(p) => p,
418            Err(e) => return e.into(),
419        };
420
421        let SurfnetRpcContext {
422            svm_locker,
423            remote_ctx,
424        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
425            Ok(res) => res,
426            Err(e) => return e.into(),
427        };
428
429        Box::pin(async move {
430            let SvmAccessContext {
431                slot,
432                inner: account_updates,
433                ..
434            } = svm_locker
435                .get_multiple_accounts(&remote_ctx, &pubkeys, None)
436                .await?;
437
438            svm_locker.write_multiple_account_updates(&account_updates);
439
440            // Convert account updates to UI accounts, order is already preserved by get_multiple_accounts
441            let mut ui_accounts = vec![];
442            for account_update in account_updates.into_iter() {
443                if let Some(((pubkey, account), token_data)) =
444                    account_update.map_account_with_token_data()
445                {
446                    ui_accounts.push(Some(
447                        svm_locker
448                            .account_to_rpc_keyed_account(
449                                &pubkey,
450                                &account,
451                                &config,
452                                token_data.map(|(mint, _)| mint),
453                            )
454                            .account,
455                    ));
456                } else {
457                    ui_accounts.push(None);
458                }
459            }
460
461            Ok(RpcResponse {
462                context: RpcResponseContext::new(slot),
463                value: ui_accounts,
464            })
465        })
466    }
467
468    fn get_block_commitment(
469        &self,
470        meta: Self::Metadata,
471        block: Slot,
472    ) -> Result<RpcBlockCommitment<BlockCommitmentArray>> {
473        // get the info we need and free up lock before validation
474        let (current_slot, block_exists) = meta
475            .with_svm_reader(|svm_reader| {
476                svm_reader
477                    .blocks
478                    .contains_key(&block)
479                    .map_err(SurfpoolError::from)
480                    .map(|exists| (svm_reader.get_latest_absolute_slot(), exists))
481            })
482            .map_err(Into::<jsonrpc_core::Error>::into)??;
483
484        // block is valid if it exists in our block history or it's not too far in the future
485        if !block_exists && block > current_slot {
486            return Err(jsonrpc_core::Error::invalid_params(format!(
487                "Block {} not found",
488                block
489            )));
490        }
491
492        let commitment_array = [0u64; 32];
493
494        Ok(RpcBlockCommitment {
495            commitment: Some(commitment_array),
496            total_stake: 0,
497        })
498    }
499
500    // SPL Token-specific RPC endpoints
501    // See https://github.com/solana-labs/solana-program-library/releases/tag/token-v2.0.0 for
502    // program details
503
504    fn get_token_account_balance(
505        &self,
506        meta: Self::Metadata,
507        pubkey_str: String,
508        commitment: Option<CommitmentConfig>,
509    ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>> {
510        let pubkey = match verify_pubkey(&pubkey_str) {
511            Ok(res) => res,
512            Err(e) => return e.into(),
513        };
514
515        let SurfnetRpcContext {
516            svm_locker,
517            remote_ctx,
518        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
519            Ok(res) => res,
520            Err(e) => return e.into(),
521        };
522
523        Box::pin(async move {
524            let token_account_result = svm_locker
525                .get_account(&remote_ctx, &pubkey, None)
526                .await?
527                .inner;
528
529            svm_locker.write_account_update(token_account_result.clone());
530
531            let token_account = token_account_result.map_account()?;
532
533            let (mint_pubkey, _amount) = if is_supported_token_program(&token_account.owner) {
534                let unpacked_token_account = TokenAccount::unpack(&token_account.data)?;
535                (
536                    unpacked_token_account.mint(),
537                    unpacked_token_account.amount(),
538                )
539            } else {
540                return Err(SurfpoolError::invalid_account_data(
541                    pubkey,
542                    "Account is not owned by Token or Token-2022 program",
543                    None::<String>,
544                )
545                .into());
546            };
547
548            let SvmAccessContext {
549                slot,
550                inner: mint_account_result,
551                ..
552            } = svm_locker
553                .get_account(&remote_ctx, &mint_pubkey, None)
554                .await?;
555
556            svm_locker.write_account_update(mint_account_result.clone());
557
558            let mint_account = mint_account_result.map_account()?;
559
560            let token_decimals = if is_supported_token_program(&mint_account.owner) {
561                let unpacked_mint_account = MintAccount::unpack(&mint_account.data)?;
562                unpacked_mint_account.decimals()
563            } else {
564                return Err(SurfpoolError::invalid_account_data(
565                    mint_pubkey,
566                    "Mint account is not owned by Token or Token-2022 program",
567                    None::<String>,
568                )
569                .into());
570            };
571
572            Ok(RpcResponse {
573                context: RpcResponseContext::new(slot),
574                value: {
575                    parse_token_v3(
576                        &token_account.data,
577                        Some(&SplTokenAdditionalDataV2 {
578                            decimals: token_decimals,
579                            ..Default::default()
580                        }),
581                    )
582                    .ok()
583                    .and_then(|t| match t {
584                        TokenAccountType::Account(account) => Some(account.token_amount),
585                        _ => None,
586                    })
587                },
588            })
589        })
590    }
591
592    fn get_token_supply(
593        &self,
594        meta: Self::Metadata,
595        mint_str: String,
596        commitment: Option<CommitmentConfig>,
597    ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>> {
598        let mint_pubkey = match verify_pubkey(&mint_str) {
599            Ok(pubkey) => pubkey,
600            Err(e) => return e.into(),
601        };
602
603        let SurfnetRpcContext {
604            svm_locker,
605            remote_ctx,
606        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
607            Ok(res) => res,
608            Err(e) => return e.into(),
609        };
610
611        Box::pin(async move {
612            let SvmAccessContext {
613                slot,
614                inner: mint_account_result,
615                ..
616            } = svm_locker
617                .get_account(&remote_ctx, &mint_pubkey, None)
618                .await?;
619
620            svm_locker.write_account_update(mint_account_result.clone());
621
622            let mint_account = mint_account_result.map_account()?;
623
624            if !is_supported_token_program(&mint_account.owner) {
625                return Err(SurfpoolError::invalid_account_data(
626                    mint_pubkey,
627                    "Account is not a token mint account",
628                    None::<String>,
629                )
630                .into());
631            }
632
633            let mint_data = MintAccount::unpack(&mint_account.data)?;
634
635            Ok(RpcResponse {
636                context: RpcResponseContext::new(slot),
637                value: {
638                    parse_token_v3(
639                        &mint_account.data,
640                        Some(&SplTokenAdditionalDataV2 {
641                            decimals: mint_data.decimals(),
642                            ..Default::default()
643                        }),
644                    )
645                    .ok()
646                    .and_then(|t| match t {
647                        TokenAccountType::Mint(mint) => {
648                            let supply_u64 = mint.supply.parse::<u64>().unwrap_or(0);
649                            let ui_amount = if supply_u64 == 0 {
650                                Some(0.0)
651                            } else {
652                                let divisor = 10_u64.pow(mint.decimals as u32);
653                                Some(supply_u64 as f64 / divisor as f64)
654                            };
655
656                            Some(UiTokenAmount {
657                                amount: mint.supply.clone(),
658                                decimals: mint.decimals,
659                                ui_amount,
660                                ui_amount_string: mint.supply,
661                            })
662                        }
663                        _ => None,
664                    })
665                    .ok_or_else(|| {
666                        SurfpoolError::invalid_account_data(
667                            mint_pubkey,
668                            "Failed to parse token mint account",
669                            None::<String>,
670                        )
671                    })?
672                },
673            })
674        })
675    }
676}
677
678#[cfg(test)]
679mod tests {
680    use solana_account::Account;
681    use solana_keypair::Keypair;
682    use solana_program_option::COption;
683    use solana_program_pack::Pack;
684    use solana_pubkey::{Pubkey, new_rand};
685    use solana_signer::Signer;
686    use solana_system_interface::instruction::create_account;
687    use solana_transaction::Transaction;
688    use spl_associated_token_account_interface::{
689        address::get_associated_token_address_with_program_id,
690        instruction::create_associated_token_account,
691    };
692    use spl_token_2022_interface::instruction::{initialize_mint2, mint_to, transfer_checked};
693    use spl_token_interface::state::{Account as TokenAccount, AccountState, Mint};
694
695    use super::*;
696    use crate::{
697        surfnet::{GetAccountResult, remote::SurfnetRemoteClient},
698        tests::helpers::TestSetup,
699        types::SyntheticBlockhash,
700    };
701
702    #[ignore = "connection-required"]
703    #[tokio::test(flavor = "multi_thread")]
704    async fn test_get_token_account_balance() {
705        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
706
707        let mint_pk = Pubkey::new_unique();
708
709        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
710            svm_reader
711                .inner
712                .minimum_balance_for_rent_exemption(Mint::LEN)
713        });
714
715        let mut data = [0; Mint::LEN];
716
717        let default = Mint {
718            decimals: 6,
719            supply: 1000000000000000,
720            is_initialized: true,
721            ..Default::default()
722        };
723        default.pack_into_slice(&mut data);
724
725        let mint_account = Account {
726            lamports: minimum_rent,
727            owner: spl_token_interface::ID,
728            executable: false,
729            rent_epoch: 0,
730            data: data.to_vec(),
731        };
732
733        setup
734            .context
735            .svm_locker
736            .write_account_update(GetAccountResult::FoundAccount(mint_pk, mint_account, true));
737
738        let token_account_pk = Pubkey::new_unique();
739
740        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
741            svm_reader
742                .inner
743                .minimum_balance_for_rent_exemption(TokenAccount::LEN)
744        });
745
746        let mut data = [0; TokenAccount::LEN];
747
748        let default = TokenAccount {
749            mint: mint_pk,
750            owner: spl_token_interface::ID,
751            state: AccountState::Initialized,
752            amount: 100 * 1000000,
753            ..Default::default()
754        };
755        default.pack_into_slice(&mut data);
756
757        let token_account = Account {
758            lamports: minimum_rent,
759            owner: spl_token_interface::ID,
760            executable: false,
761            rent_epoch: 0,
762            data: data.to_vec(),
763        };
764
765        setup
766            .context
767            .svm_locker
768            .write_account_update(GetAccountResult::FoundAccount(
769                token_account_pk,
770                token_account,
771                true,
772            ));
773
774        let res = setup
775            .rpc
776            .get_token_account_balance(Some(setup.context), token_account_pk.to_string(), None)
777            .await
778            .unwrap();
779
780        assert_eq!(
781            res.value.unwrap(),
782            UiTokenAmount {
783                amount: String::from("100000000"),
784                decimals: 6,
785                ui_amount: Some(100.0),
786                ui_amount_string: String::from("100")
787            }
788        );
789    }
790
791    #[test]
792    fn test_get_block_commitment_past_slot() {
793        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
794        let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
795        let past_slot = if current_slot > 10 {
796            current_slot - 10
797        } else {
798            0
799        };
800
801        let result = setup
802            .rpc
803            .get_block_commitment(Some(setup.context), past_slot)
804            .unwrap();
805
806        // Should return commitment data for past slot
807        assert!(result.commitment.is_some());
808        assert_eq!(result.total_stake, 0);
809    }
810
811    #[test]
812    fn test_get_block_commitment_with_actual_block() {
813        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
814
815        // create a block in the SVM's block history
816        let test_slot = 12345;
817        setup.context.svm_locker.with_svm_writer(|svm_writer| {
818            use crate::surfnet::BlockHeader;
819
820            svm_writer
821                .blocks
822                .store(
823                    test_slot,
824                    BlockHeader {
825                        hash: SyntheticBlockhash::new(test_slot).to_string(),
826                        previous_blockhash: SyntheticBlockhash::new(test_slot - 1).to_string(),
827                        parent_slot: test_slot - 1,
828                        block_time: chrono::Utc::now().timestamp_millis(),
829                        block_height: test_slot,
830                        signatures: vec![],
831                    },
832                )
833                .unwrap();
834        });
835
836        let result = setup
837            .rpc
838            .get_block_commitment(Some(setup.context), test_slot)
839            .unwrap();
840
841        // should return commitment data for the existing block
842        assert!(result.commitment.is_some());
843        assert_eq!(result.total_stake, 0);
844    }
845
846    #[test]
847    fn test_get_block_commitment_no_metadata() {
848        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
849
850        let result = setup.rpc.get_block_commitment(None, 123);
851
852        assert!(result.is_err());
853        // This should fail because meta is None, triggering the SurfpoolError::missing_context() path
854    }
855
856    #[test]
857    fn test_get_block_commitment_future_slot_error() {
858        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
859        let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
860        let future_slot = current_slot + 1000;
861
862        let result = setup
863            .rpc
864            .get_block_commitment(Some(setup.context), future_slot);
865
866        // Should return an error for future slots
867        assert!(result.is_err());
868
869        let error = result.unwrap_err();
870        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
871        assert!(error.message.contains("Block") && error.message.contains("not found"));
872    }
873
874    #[tokio::test(flavor = "multi_thread")]
875    async fn test_get_token_supply_with_real_mint() {
876        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
877
878        let mint_pubkey = Pubkey::new_unique();
879
880        // Create mint account data
881        let mut mint_data = [0u8; Mint::LEN];
882        let mint = Mint {
883            mint_authority: COption::Some(Pubkey::new_unique()),
884            supply: 1_000_000_000_000,
885            decimals: 6,
886            is_initialized: true,
887            freeze_authority: COption::None,
888        };
889        Mint::pack(mint, &mut mint_data).unwrap();
890
891        let mint_account = Account {
892            lamports: setup.context.svm_locker.with_svm_reader(|svm_reader| {
893                svm_reader
894                    .inner
895                    .minimum_balance_for_rent_exemption(Mint::LEN)
896            }),
897            data: mint_data.to_vec(),
898            owner: spl_token_interface::id(),
899            executable: false,
900            rent_epoch: 0,
901        };
902
903        setup.context.svm_locker.with_svm_writer(|svm_writer| {
904            svm_writer
905                .set_account(&mint_pubkey, mint_account.clone())
906                .unwrap();
907        });
908
909        let res = setup
910            .rpc
911            .get_token_supply(
912                Some(setup.context),
913                mint_pubkey.to_string(),
914                Some(CommitmentConfig::confirmed()),
915            )
916            .await
917            .unwrap();
918
919        assert_eq!(res.value.amount, "1000000000000");
920        assert_eq!(res.value.decimals, 6);
921        assert_eq!(res.value.ui_amount_string, "1000000000000");
922    }
923
924    #[tokio::test(flavor = "multi_thread")]
925    async fn test_invalid_pubkey_format() {
926        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
927
928        // test various invalid pubkey formats
929        let invalid_pubkeys = vec![
930            "",
931            "invalid",
932            "123",
933            "not-a-valid-base58-string!@#$",
934            "11111111111111111111111111111111111111111111111111111111111111111",
935            "invalid-base58-characters-ö",
936        ];
937
938        for invalid_pubkey in invalid_pubkeys {
939            let res = setup
940                .rpc
941                .get_token_supply(
942                    Some(setup.context.clone()),
943                    invalid_pubkey.to_string(),
944                    Some(CommitmentConfig::confirmed()),
945                )
946                .await;
947
948            assert!(
949                res.is_err(),
950                "Should fail for invalid pubkey: '{}'",
951                invalid_pubkey
952            );
953
954            let error_msg = res.unwrap_err().to_string();
955            assert!(
956                error_msg.contains("Invalid") || error_msg.contains("invalid"),
957                "Error should mention invalidity for '{}': {}",
958                invalid_pubkey,
959                error_msg
960            );
961        }
962
963        println!("✅ All invalid pubkey formats correctly rejected");
964    }
965
966    #[tokio::test(flavor = "multi_thread")]
967    async fn test_nonexistent_account() {
968        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
969
970        // valid pubkey format but nonexistent account
971        let nonexistent_mint = Pubkey::new_unique();
972
973        let res = setup
974            .rpc
975            .get_token_supply(
976                Some(setup.context),
977                nonexistent_mint.to_string(),
978                Some(CommitmentConfig::confirmed()),
979            )
980            .await;
981
982        assert!(res.is_err(), "Should fail for nonexistent account");
983
984        let error_msg = res.unwrap_err().to_string();
985        assert!(
986            error_msg.contains("not found") || error_msg.contains("account"),
987            "Error should mention account not found: {}",
988            error_msg
989        );
990
991        println!("✅ Nonexistent account correctly rejected: {}", error_msg);
992    }
993
994    #[tokio::test(flavor = "multi_thread")]
995    async fn test_invalid_mint_data() {
996        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
997
998        let fake_mint = Pubkey::new_unique();
999
1000        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1001            // create an account owned by SPL Token but with invalid data
1002            let invalid_mint_account = Account {
1003                lamports: 1000000,
1004                data: vec![0xFF; 50], // invalid mint data (random bytes)
1005                owner: spl_token_interface::id(),
1006                executable: false,
1007                rent_epoch: 0,
1008            };
1009
1010            svm_writer
1011                .set_account(&fake_mint, invalid_mint_account)
1012                .unwrap();
1013        });
1014
1015        let res = setup
1016            .rpc
1017            .get_token_supply(
1018                Some(setup.context),
1019                fake_mint.to_string(),
1020                Some(CommitmentConfig::confirmed()),
1021            )
1022            .await;
1023
1024        assert!(
1025            res.is_err(),
1026            "Should fail for account with invalid mint data"
1027        );
1028
1029        let error_msg = res.unwrap_err().to_string();
1030        assert!(
1031            error_msg.eq("Parse error: Failed to unpack mint account"),
1032            "Incorrect error received: {}",
1033            error_msg
1034        );
1035
1036        println!("✅ Invalid mint data correctly rejected: {}", error_msg);
1037    }
1038
1039    #[ignore = "requires-network"]
1040    #[tokio::test(flavor = "multi_thread")]
1041    async fn test_remote_rpc_failure() {
1042        // test with invalid remote RPC URL
1043        let bad_remote_client =
1044            SurfnetRemoteClient::new("https://invalid-url-that-doesnt-exist.com");
1045        let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1046        setup.context.remote_rpc_client = Some(bad_remote_client);
1047
1048        let nonexistent_mint = Pubkey::new_unique();
1049
1050        let res = setup
1051            .rpc
1052            .get_token_supply(
1053                Some(setup.context),
1054                nonexistent_mint.to_string(),
1055                Some(CommitmentConfig::confirmed()),
1056            )
1057            .await;
1058
1059        assert!(res.is_err(), "Should fail when remote RPC is unreachable");
1060
1061        let error_msg = res.unwrap_err().to_string();
1062        println!("✅ Remote RPC failure handled: {}", error_msg);
1063    }
1064
1065    #[tokio::test(flavor = "multi_thread")]
1066    async fn test_transfer_token() {
1067        // Create connection to local validator
1068        let client = TestSetup::new(SurfpoolAccountsDataRpc);
1069        let recent_blockhash = client
1070            .context
1071            .svm_locker
1072            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1073
1074        // Generate a new keypair for the fee payer
1075        let fee_payer = Keypair::new();
1076
1077        // Generate a second keypair for the token recipient
1078        let recipient = Keypair::new();
1079
1080        // Airdrop 1 SOL to fee payer
1081        client
1082            .context
1083            .svm_locker
1084            .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1085            .unwrap()
1086            .unwrap();
1087
1088        // Airdrop 1 SOL to recipient for rent exemption
1089        client
1090            .context
1091            .svm_locker
1092            .airdrop(&recipient.pubkey(), 1_000_000_000)
1093            .unwrap()
1094            .unwrap();
1095
1096        // Generate keypair to use as address of mint
1097        let mint = Keypair::new();
1098
1099        // Get default mint account size (in bytes), no extensions enabled
1100        let mint_space = Mint::LEN;
1101        let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1102            svm_reader
1103                .inner
1104                .minimum_balance_for_rent_exemption(mint_space)
1105        });
1106
1107        // Instruction to create new account for mint (token 2022 program)
1108        let create_account_instruction = create_account(
1109            &fee_payer.pubkey(),             // payer
1110            &mint.pubkey(),                  // new account (mint)
1111            mint_rent,                       // lamports
1112            mint_space as u64,               // space
1113            &spl_token_2022_interface::id(), // program id
1114        );
1115
1116        // Instruction to initialize mint account data
1117        let initialize_mint_instruction = initialize_mint2(
1118            &spl_token_2022_interface::id(),
1119            &mint.pubkey(),            // mint
1120            &fee_payer.pubkey(),       // mint authority
1121            Some(&fee_payer.pubkey()), // freeze authority
1122            2,                         // decimals
1123        )
1124        .unwrap();
1125
1126        // Calculate the associated token account address for fee_payer
1127        let source_token_address = get_associated_token_address_with_program_id(
1128            &fee_payer.pubkey(),             // owner
1129            &mint.pubkey(),                  // mint
1130            &spl_token_2022_interface::id(), // program_id
1131        );
1132
1133        // Instruction to create associated token account for fee_payer
1134        let create_source_ata_instruction = create_associated_token_account(
1135            &fee_payer.pubkey(),             // funding address
1136            &fee_payer.pubkey(),             // wallet address
1137            &mint.pubkey(),                  // mint address
1138            &spl_token_2022_interface::id(), // program id
1139        );
1140
1141        // Calculate the associated token account address for recipient
1142        let destination_token_address = get_associated_token_address_with_program_id(
1143            &recipient.pubkey(),             // owner
1144            &mint.pubkey(),                  // mint
1145            &spl_token_2022_interface::id(), // program_id
1146        );
1147
1148        // Instruction to create associated token account for recipient
1149        let create_destination_ata_instruction = create_associated_token_account(
1150            &fee_payer.pubkey(),             // funding address
1151            &recipient.pubkey(),             // wallet address
1152            &mint.pubkey(),                  // mint address
1153            &spl_token_2022_interface::id(), // program id
1154        );
1155
1156        // Amount of tokens to mint (100 tokens with 2 decimal places)
1157        let amount = 100_00;
1158
1159        // Create mint_to instruction to mint tokens to the source token account
1160        let mint_to_instruction = mint_to(
1161            &spl_token_2022_interface::id(),
1162            &mint.pubkey(),         // mint
1163            &source_token_address,  // destination
1164            &fee_payer.pubkey(),    // authority
1165            &[&fee_payer.pubkey()], // signer
1166            amount,                 // amount
1167        )
1168        .unwrap();
1169
1170        // Create transaction and add instructions
1171        let transaction = Transaction::new_signed_with_payer(
1172            &[
1173                create_account_instruction,
1174                initialize_mint_instruction,
1175                create_source_ata_instruction,
1176                create_destination_ata_instruction,
1177                mint_to_instruction,
1178            ],
1179            Some(&fee_payer.pubkey()),
1180            &[&fee_payer, &mint],
1181            recent_blockhash,
1182        );
1183
1184        let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1185        client
1186            .context
1187            .svm_locker
1188            .process_transaction(&None, transaction.into(), status_tx.clone(), false, true)
1189            .await
1190            .unwrap();
1191
1192        println!("Mint Address: {}", mint.pubkey());
1193        println!("Recipient Address: {}", recipient.pubkey());
1194        println!("Source Token Account Address: {}", source_token_address);
1195        println!(
1196            "Destination Token Account Address: {}",
1197            destination_token_address
1198        );
1199        println!("Minted {} tokens to the source token account", amount);
1200
1201        // Get the latest blockhash for the transfer transaction
1202        let recent_blockhash = client
1203            .context
1204            .svm_locker
1205            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1206
1207        // Amount of tokens to transfer (0.50 tokens with 2 decimals)
1208        let transfer_amount = 50;
1209
1210        // Create transfer_checked instruction to send tokens from source to destination
1211        let transfer_instruction = transfer_checked(
1212            &spl_token_2022_interface::id(), // program id
1213            &source_token_address,           // source
1214            &mint.pubkey(),                  // mint
1215            &destination_token_address,      // destination
1216            &fee_payer.pubkey(),             // owner of source
1217            &[&fee_payer.pubkey()],          // signers
1218            transfer_amount,                 // amount
1219            2,                               // decimals
1220        )
1221        .unwrap();
1222
1223        // Create transaction for transferring tokens
1224        let transaction = Transaction::new_signed_with_payer(
1225            &[transfer_instruction],
1226            Some(&fee_payer.pubkey()),
1227            &[&fee_payer],
1228            recent_blockhash,
1229        );
1230
1231        client
1232            .context
1233            .svm_locker
1234            .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1235            .await
1236            .unwrap();
1237
1238        println!("Successfully transferred 0.50 tokens from sender to recipient");
1239
1240        let source_balance = client
1241            .rpc
1242            .get_token_account_balance(
1243                Some(client.context.clone()),
1244                source_token_address.to_string(),
1245                Some(CommitmentConfig::confirmed()),
1246            )
1247            .await
1248            .unwrap();
1249
1250        let destination_balance = client
1251            .rpc
1252            .get_token_account_balance(
1253                Some(client.context.clone()),
1254                destination_token_address.to_string(),
1255                Some(CommitmentConfig::confirmed()),
1256            )
1257            .await
1258            .unwrap();
1259
1260        println!(
1261            "Source Token Account Balance: {} tokens ({})",
1262            source_balance.value.as_ref().unwrap().ui_amount.unwrap(),
1263            source_balance.value.as_ref().unwrap().amount
1264        );
1265        println!(
1266            "Destination Token Account Balance: {} tokens ({})",
1267            destination_balance
1268                .value
1269                .as_ref()
1270                .unwrap()
1271                .ui_amount
1272                .unwrap(),
1273            destination_balance.value.as_ref().unwrap().amount
1274        );
1275
1276        assert_eq!(source_balance.value.unwrap().amount, "9950");
1277        assert_eq!(destination_balance.value.unwrap().amount, "50");
1278    }
1279
1280    #[tokio::test(flavor = "multi_thread")]
1281    async fn test_get_account_info() {
1282        // Create connection to local validator
1283        let client = TestSetup::new(SurfpoolAccountsDataRpc);
1284        let recent_blockhash = client
1285            .context
1286            .svm_locker
1287            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1288
1289        // Generate a new keypair for the fee payer
1290        let fee_payer = Keypair::new();
1291
1292        // Generate a second keypair for the token recipient
1293        let recipient = Keypair::new();
1294
1295        // Airdrop 1 SOL to fee payer
1296        client
1297            .context
1298            .svm_locker
1299            .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1300            .unwrap()
1301            .unwrap();
1302
1303        // Airdrop 1 SOL to recipient for rent exemption
1304        client
1305            .context
1306            .svm_locker
1307            .airdrop(&recipient.pubkey(), 1_000_000_000)
1308            .unwrap()
1309            .unwrap();
1310
1311        // Generate keypair to use as address of mint
1312        let mint = Keypair::new();
1313
1314        // Get default mint account size (in bytes), no extensions enabled
1315        let mint_space = Mint::LEN;
1316        let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1317            svm_reader
1318                .inner
1319                .minimum_balance_for_rent_exemption(mint_space)
1320        });
1321
1322        // Instruction to create new account for mint (token 2022 program)
1323        let create_account_instruction = create_account(
1324            &fee_payer.pubkey(),             // payer
1325            &mint.pubkey(),                  // new account (mint)
1326            mint_rent,                       // lamports
1327            mint_space as u64,               // space
1328            &spl_token_2022_interface::id(), // program id
1329        );
1330
1331        // Instruction to initialize mint account data
1332        let initialize_mint_instruction = initialize_mint2(
1333            &spl_token_2022_interface::id(),
1334            &mint.pubkey(),            // mint
1335            &fee_payer.pubkey(),       // mint authority
1336            Some(&fee_payer.pubkey()), // freeze authority
1337            2,                         // decimals
1338        )
1339        .unwrap();
1340
1341        // Calculate the associated token account address for fee_payer
1342        let source_token_address = get_associated_token_address_with_program_id(
1343            &fee_payer.pubkey(),             // owner
1344            &mint.pubkey(),                  // mint
1345            &spl_token_2022_interface::id(), // program_id
1346        );
1347
1348        // Instruction to create associated token account for fee_payer
1349        let create_source_ata_instruction = create_associated_token_account(
1350            &fee_payer.pubkey(),             // funding address
1351            &fee_payer.pubkey(),             // wallet address
1352            &mint.pubkey(),                  // mint address
1353            &spl_token_2022_interface::id(), // program id
1354        );
1355
1356        // Calculate the associated token account address for recipient
1357        let destination_token_address = get_associated_token_address_with_program_id(
1358            &recipient.pubkey(),             // owner
1359            &mint.pubkey(),                  // mint
1360            &spl_token_2022_interface::id(), // program_id
1361        );
1362
1363        // Instruction to create associated token account for recipient
1364        let create_destination_ata_instruction = create_associated_token_account(
1365            &fee_payer.pubkey(),             // funding address
1366            &recipient.pubkey(),             // wallet address
1367            &mint.pubkey(),                  // mint address
1368            &spl_token_2022_interface::id(), // program id
1369        );
1370
1371        // Amount of tokens to mint (100 tokens with 2 decimal places)
1372        let amount = 100_00;
1373
1374        // Create mint_to instruction to mint tokens to the source token account
1375        let mint_to_instruction = mint_to(
1376            &spl_token_2022_interface::id(),
1377            &mint.pubkey(),         // mint
1378            &source_token_address,  // destination
1379            &fee_payer.pubkey(),    // authority
1380            &[&fee_payer.pubkey()], // signer
1381            amount,                 // amount
1382        )
1383        .unwrap();
1384
1385        // Create transaction and add instructions
1386        let transaction = Transaction::new_signed_with_payer(
1387            &[
1388                create_account_instruction,
1389                initialize_mint_instruction,
1390                create_source_ata_instruction,
1391                create_destination_ata_instruction,
1392                mint_to_instruction,
1393            ],
1394            Some(&fee_payer.pubkey()),
1395            &[&fee_payer, &mint],
1396            recent_blockhash,
1397        );
1398
1399        let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1400        // Send and confirm transaction
1401        client
1402            .context
1403            .svm_locker
1404            .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1405            .await
1406            .unwrap();
1407
1408        println!("Mint Address: {}", mint.pubkey());
1409        println!("Recipient Address: {}", recipient.pubkey());
1410        println!("Source Token Account Address: {}", source_token_address);
1411        println!(
1412            "Destination Token Account Address: {}",
1413            destination_token_address
1414        );
1415        println!("Minted {} tokens to the source token account", amount);
1416
1417        // Get the latest blockhash for the transfer transaction
1418        let recent_blockhash = client
1419            .context
1420            .svm_locker
1421            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1422
1423        // Amount of tokens to transfer (0.50 tokens with 2 decimals)
1424        let transfer_amount = 50;
1425
1426        // Create transfer_checked instruction to send tokens from source to destination
1427        let transfer_instruction = transfer_checked(
1428            &spl_token_2022_interface::id(), // program id
1429            &source_token_address,           // source
1430            &mint.pubkey(),                  // mint
1431            &destination_token_address,      // destination
1432            &fee_payer.pubkey(),             // owner of source
1433            &[&fee_payer.pubkey()],          // signers
1434            transfer_amount,                 // amount
1435            2,                               // decimals
1436        )
1437        .unwrap();
1438
1439        // Create transaction for transferring tokens
1440        let transaction = Transaction::new_signed_with_payer(
1441            &[transfer_instruction],
1442            Some(&fee_payer.pubkey()),
1443            &[&fee_payer],
1444            recent_blockhash,
1445        );
1446        let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1447        // Send and confirm transaction
1448        client
1449            .context
1450            .svm_locker
1451            .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1452            .await
1453            .unwrap();
1454
1455        println!(
1456            "Successfully transferred 0.50 tokens from {} to {}",
1457            source_token_address, destination_token_address
1458        );
1459
1460        let source_account_info = client
1461            .rpc
1462            .get_account_info(
1463                Some(client.context.clone()),
1464                source_token_address.to_string(),
1465                Some(RpcAccountInfoConfig {
1466                    encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1467                    ..Default::default()
1468                }),
1469            )
1470            .await
1471            .unwrap();
1472
1473        let destination_account_info = client
1474            .rpc
1475            .get_account_info(
1476                Some(client.context.clone()),
1477                destination_token_address.to_string(),
1478                Some(RpcAccountInfoConfig {
1479                    encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1480                    ..Default::default()
1481                }),
1482            )
1483            .await
1484            .unwrap();
1485
1486        println!("Source Account Info: {:?}", source_account_info);
1487        println!("Destination Account Info: {:?}", destination_account_info);
1488
1489        let source_account = source_account_info.value.unwrap();
1490        if let solana_account_decoder::UiAccountData::Json(parsed) = source_account.data {
1491            let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1492                .as_str()
1493                .unwrap();
1494            assert_eq!(amount, "9950");
1495        } else {
1496            panic!("source account data was not in json parsed format");
1497        }
1498
1499        let destination_account = destination_account_info.value.unwrap();
1500        if let solana_account_decoder::UiAccountData::Json(parsed) = destination_account.data {
1501            let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1502                .as_str()
1503                .unwrap();
1504            assert_eq!(amount, "50");
1505        } else {
1506            panic!("destination account data was not in json parsed format");
1507        }
1508    }
1509
1510    #[ignore = "requires-network"]
1511    #[tokio::test(flavor = "multi_thread")]
1512    async fn test_get_multiple_accounts_with_remote_preserves_order() {
1513        // This test checks that order is preserved when mixing local and remote accounts
1514        let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1515
1516        // Add a remote client to trigger get_multiple_accounts_with_remote_fallback path
1517        let remote_client = SurfnetRemoteClient::new("https://api.mainnet-beta.solana.com");
1518        setup.context.remote_rpc_client = Some(remote_client);
1519
1520        // Create three accounts with different lamport amounts
1521        let pk1 = new_rand();
1522        let pk2 = new_rand();
1523        let pk3 = new_rand();
1524
1525        println!("{}", pk1);
1526        println!("{}", pk2);
1527        println!("{}", pk3);
1528
1529        let account1 = Account {
1530            lamports: 1_000_000,
1531            data: vec![],
1532            owner: solana_pubkey::Pubkey::default(),
1533            executable: false,
1534            rent_epoch: 0,
1535        };
1536
1537        let account3 = Account {
1538            lamports: 3_000_000,
1539            data: vec![],
1540            owner: solana_pubkey::Pubkey::default(),
1541            executable: false,
1542            rent_epoch: 0,
1543        };
1544
1545        // Store only account1 and account3 locally (account2 will need remote fetch)
1546        setup
1547            .context
1548            .svm_locker
1549            .write_account_update(GetAccountResult::FoundAccount(pk1, account1, true));
1550        setup
1551            .context
1552            .svm_locker
1553            .write_account_update(GetAccountResult::FoundAccount(pk3, account3, true));
1554
1555        // Request accounts in order: [pk1, pk2, pk3]
1556        // pk1 and pk3 are local, pk2 is missing (will try remote fetch and fail)
1557        let pubkeys_str = vec![pk1.to_string(), pk2.to_string(), pk3.to_string()];
1558
1559        let response = setup
1560            .rpc
1561            .get_multiple_accounts(
1562                Some(setup.context),
1563                pubkeys_str,
1564                Some(RpcAccountInfoConfig::default()),
1565            )
1566            .await
1567            .unwrap();
1568
1569        // Verify we got 3 results
1570        assert_eq!(response.value.len(), 3);
1571
1572        println!("{:?}", response);
1573
1574        // First account should be account1 with 1M lamports
1575        assert!(response.value[0].is_some());
1576        assert_eq!(
1577            response.value[0].as_ref().unwrap().lamports,
1578            1_000_000,
1579            "First element should be account1"
1580        );
1581
1582        // Second account should be None (pk2 doesn't exist locally or remotely)
1583        assert!(
1584            response.value[1].is_none(),
1585            "Second element should be None for missing pk2"
1586        );
1587
1588        // Third account should be account3 with 3M lamports
1589        assert!(response.value[2].is_some());
1590        assert_eq!(
1591            response.value[2].as_ref().unwrap().lamports,
1592            3_000_000,
1593            "Third element should be account3"
1594        );
1595
1596        println!("✅ Account order preserved with remote: [1M lamports, None, 3M lamports]");
1597    }
1598}