surfpool_core/rpc/
accounts_data.rs

1use jsonrpc_core::{BoxFuture, Result};
2use jsonrpc_derive::rpc;
3use solana_account_decoder::{
4    parse_account_data::SplTokenAdditionalDataV2,
5    parse_token::{parse_token_v3, TokenAccountType, UiTokenAmount},
6    UiAccount,
7};
8use solana_client::{
9    rpc_config::RpcAccountInfoConfig,
10    rpc_response::{RpcBlockCommitment, RpcResponseContext},
11};
12use solana_clock::Slot;
13use solana_commitment_config::CommitmentConfig;
14use solana_rpc_client_api::response::Response as RpcResponse;
15use solana_runtime::commitment::BlockCommitmentArray;
16use solana_sdk::program_pack::Pack;
17use spl_token::state::{Account as TokenAccount, Mint};
18
19use super::{RunloopContext, SurfnetRpcContext};
20use crate::{
21    error::{SurfpoolError, SurfpoolResult},
22    rpc::{utils::verify_pubkey, State},
23    surfnet::locker::SvmAccessContext,
24};
25
26#[rpc]
27pub trait AccountsData {
28    type Metadata;
29
30    /// Returns detailed information about an account given its public key.
31    ///
32    /// This method queries the blockchain for the account associated with the provided
33    /// public key string. It can be used to inspect balances, ownership, and program-related metadata.
34    ///
35    /// ## Parameters
36    /// - `pubkey_str`: A base-58 encoded string representing the account's public key.
37    /// - `config`: Optional configuration that controls encoding, commitment level,
38    ///   data slicing, and other response details.
39    ///
40    /// ## Returns
41    /// A [`RpcResponse`] containing an optional [`UiAccount`] object if the account exists.
42    /// If the account does not exist, the response will contain `null`.
43    ///
44    /// ## Example Request (JSON-RPC)
45    /// ```json
46    /// {
47    ///   "jsonrpc": "2.0",
48    ///   "id": 1,
49    ///   "method": "getAccountInfo",
50    ///   "params": [
51    ///     "9XQeWMPMPXwW1fzLEQeTTrfF5Eb9dj8Qs3tCPoMw3GiE",
52    ///     {
53    ///       "encoding": "jsonParsed",
54    ///       "commitment": "finalized"
55    ///     }
56    ///   ]
57    /// }
58    /// ```
59    ///
60    /// ## Example Response
61    /// ```json
62    /// {
63    ///   "jsonrpc": "2.0",
64    ///   "result": {
65    ///     "context": {
66    ///       "slot": 12345678
67    ///     },
68    ///     "value": {
69    ///       "lamports": 10000000,
70    ///       "data": {
71    ///         "program": "spl-token",
72    ///         "parsed": { ... },
73    ///         "space": 165
74    ///       },
75    ///       "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
76    ///       "executable": false,
77    ///       "rentEpoch": 203,
78    ///       "space": 165
79    ///     }
80    ///   },
81    ///   "id": 1
82    /// }
83    /// ```
84    ///
85    /// ## Errors
86    /// - Returns an error if the public key is malformed or invalid
87    /// - Returns an internal error if the ledger cannot be accessed
88    ///
89    /// ## See also
90    /// - [`UiAccount`]: A readable structure representing on-chain accounts
91    #[rpc(meta, name = "getAccountInfo")]
92    fn get_account_info(
93        &self,
94        meta: Self::Metadata,
95        pubkey_str: String,
96        config: Option<RpcAccountInfoConfig>,
97    ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>>;
98
99    /// Returns commitment levels for a given block (slot).
100    ///
101    /// This method provides insight into how many validators have voted for a specific block
102    /// and with what level of lockout. This can be used to analyze consensus progress and
103    /// determine finality confidence.
104    ///
105    /// ## Parameters
106    /// - `block`: The target slot (block) to query.
107    ///
108    /// ## Returns
109    /// A [`RpcBlockCommitment`] containing a [`BlockCommitmentArray`], which is an array of 32
110    /// integers representing the number of votes at each lockout level for that block. Each index
111    /// corresponds to a lockout level (i.e., confidence in finality).
112    ///
113    /// ## Example Request (JSON-RPC)
114    /// ```json
115    /// {
116    ///   "jsonrpc": "2.0",
117    ///   "id": 1,
118    ///   "method": "getBlockCommitment",
119    ///   "params": [150000000]
120    /// }
121    /// ```
122    ///
123    /// ## Example Response
124    /// ```json
125    /// {
126    ///   "jsonrpc": "2.0",
127    ///   "result": {
128    ///     "commitment": [0, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
129    ///     "totalStake": 100000000
130    ///   },
131    ///   "id": 1
132    /// }
133    /// ```
134    ///
135    /// ## Errors
136    /// - If the slot is not found in the current bank or has been purged, this call may return an error.
137    /// - May fail if the RPC node is lagging behind or doesn't have voting history for the slot.
138    ///
139    /// ## See also
140    /// - [`BlockCommitmentArray`]: An array representing votes by lockout level
141    /// - [`RpcBlockCommitment`]: Wrapper struct for the full response
142    #[rpc(meta, name = "getBlockCommitment")]
143    fn get_block_commitment(
144        &self,
145        meta: Self::Metadata,
146        block: Slot,
147    ) -> Result<RpcBlockCommitment<BlockCommitmentArray>>;
148
149    /// Returns account information for multiple public keys in a single call.
150    ///
151    /// This method allows batching of account lookups for improved performance and fewer
152    /// network roundtrips. It returns a list of `UiAccount` values in the same order as
153    /// the provided public keys.
154    ///
155    /// ## Parameters
156    /// - `pubkey_strs`: A list of base-58 encoded public key strings representing accounts to query.
157    /// - `config`: Optional configuration to control encoding, commitment level, data slicing, etc.
158    ///
159    /// ## Returns
160    /// A [`RpcResponse`] wrapping a vector of optional [`UiAccount`] objects.  
161    /// Each element in the response corresponds to the public key at the same index in the request.
162    /// If an account is not found, the corresponding entry will be `null`.
163    ///
164    /// ## Example Request (JSON-RPC)
165    /// ```json
166    /// {
167    ///   "jsonrpc": "2.0",
168    ///   "id": 1,
169    ///   "method": "getMultipleAccounts",
170    ///   "params": [
171    ///     [
172    ///       "9XQeWMPMPXwW1fzLEQeTTrfF5Eb9dj8Qs3tCPoMw3GiE",
173    ///       "3nN8SBQ2HqTDNnaCzryrSv4YHd4d6GpVCEyDhKMPxN4o"
174    ///     ],
175    ///     {
176    ///       "encoding": "jsonParsed",
177    ///       "commitment": "confirmed"
178    ///     }
179    ///   ]
180    /// }
181    /// ```
182    ///
183    /// ## Example Response
184    /// ```json
185    /// {
186    ///   "jsonrpc": "2.0",
187    ///   "result": {
188    ///     "context": { "slot": 12345678 },
189    ///     "value": [
190    ///       {
191    ///         "lamports": 10000000,
192    ///         "data": {
193    ///           "program": "spl-token",
194    ///           "parsed": { ... },
195    ///           "space": 165
196    ///         },
197    ///         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
198    ///         "executable": false,
199    ///         "rentEpoch": 203,
200    ///         "space": 165
201    ///       },
202    ///       null
203    ///     ]
204    ///   },
205    ///   "id": 1
206    /// }
207    /// ```
208    ///
209    /// ## Errors
210    /// - If any public key is malformed or invalid, the entire call may fail.
211    /// - Returns an internal error if the ledger cannot be accessed or some accounts are purged.
212    ///
213    /// ## See also
214    /// - [`UiAccount`]: Human-readable representation of an account
215    /// - [`get_account_info`]: Use when querying a single account
216    #[rpc(meta, name = "getMultipleAccounts")]
217    fn get_multiple_accounts(
218        &self,
219        meta: Self::Metadata,
220        pubkey_strs: Vec<String>,
221        config: Option<RpcAccountInfoConfig>,
222    ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>>;
223
224    /// Returns the balance of a token account, given its public key.
225    ///
226    /// This method fetches the token balance of an account, including its amount and
227    /// user-friendly information (like the UI amount in human-readable format). It is useful
228    /// for token-related applications, such as checking balances in wallets or exchanges.
229    ///
230    /// ## Parameters
231    /// - `pubkey_str`: The base-58 encoded string of the public key of the token account.
232    /// - `commitment`: Optional commitment configuration to specify the desired confirmation level of the query.
233    ///
234    /// ## Returns
235    /// A [`RpcResponse`] containing the token balance in a [`UiTokenAmount`] struct.
236    /// If the account doesn't hold any tokens or is invalid, the response will contain `null`.
237    ///
238    /// ## Example Request (JSON-RPC)
239    /// ```json
240    /// {
241    ///   "jsonrpc": "2.0",
242    ///   "id": 1,
243    ///   "method": "getTokenAccountBalance",
244    ///   "params": [
245    ///     "3nN8SBQ2HqTDNnaCzryrSv4YHd4d6GpVCEyDhKMPxN4o",
246    ///     {
247    ///       "commitment": "confirmed"
248    ///     }
249    ///   ]
250    /// }
251    /// ```
252    ///
253    /// ## Example Response
254    /// ```json
255    /// {
256    ///   "jsonrpc": "2.0",
257    ///   "result": {
258    ///     "context": {
259    ///       "slot": 12345678
260    ///     },
261    ///     "value": {
262    ///       "uiAmount": 100.0,
263    ///       "decimals": 6,
264    ///       "amount": "100000000",
265    ///       "uiAmountString": "100.000000"
266    ///     }
267    ///   },
268    ///   "id": 1
269    /// }
270    /// ```
271    ///
272    /// ## Errors
273    /// - If the provided public key is invalid or does not exist.
274    /// - If the account is not a valid token account or does not hold any tokens.
275    ///
276    /// ## See also
277    /// - [`UiTokenAmount`]: Represents the token balance in user-friendly format.
278    #[rpc(meta, name = "getTokenAccountBalance")]
279    fn get_token_account_balance(
280        &self,
281        meta: Self::Metadata,
282        pubkey_str: String,
283        commitment: Option<CommitmentConfig>,
284    ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>>;
285
286    /// Returns the total supply of a token, given its mint address.
287    ///
288    /// This method provides the total circulating supply of a specific token, including the raw
289    /// amount and human-readable UI-formatted values. It can be useful for tracking token issuance
290    /// and verifying the supply of a token on-chain.
291    ///
292    /// ## Parameters
293    /// - `mint_str`: The base-58 encoded string of the mint address for the token.
294    /// - `commitment`: Optional commitment configuration to specify the desired confirmation level of the query.
295    ///
296    /// ## Returns
297    /// A [`RpcResponse`] containing the total token supply in a [`UiTokenAmount`] struct.
298    /// If the token does not exist or is invalid, the response will return an error.
299    ///
300    /// ## Example Request (JSON-RPC)
301    /// ```json
302    /// {
303    ///   "jsonrpc": "2.0",
304    ///   "id": 1,
305    ///   "method": "getTokenSupply",
306    ///   "params": [
307    ///     "So11111111111111111111111111111111111111112",
308    ///     {
309    ///       "commitment": "confirmed"
310    ///     }
311    ///   ]
312    /// }
313    /// ```
314    ///
315    /// ## Example Response
316    /// ```json
317    /// {
318    ///   "jsonrpc": "2.0",
319    ///   "result": {
320    ///     "context": {
321    ///       "slot": 12345678
322    ///     },
323    ///     "value": {
324    ///       "uiAmount": 1000000000.0,
325    ///       "decimals": 6,
326    ///       "amount": "1000000000000000",
327    ///       "uiAmountString": "1000000000.000000"
328    ///     }
329    ///   },
330    ///   "id": 1
331    /// }
332    /// ```
333    ///
334    /// ## Errors
335    /// - If the mint address is invalid or does not correspond to a token.
336    /// - If the token supply cannot be fetched due to network issues or node synchronization problems.
337    ///
338    /// ## See also
339    /// - [`UiTokenAmount`]: Represents the token balance or supply in a user-friendly format.
340    #[rpc(meta, name = "getTokenSupply")]
341    fn get_token_supply(
342        &self,
343        meta: Self::Metadata,
344        mint_str: String,
345        commitment: Option<CommitmentConfig>,
346    ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>>;
347}
348
349#[derive(Clone)]
350pub struct SurfpoolAccountsDataRpc;
351impl AccountsData for SurfpoolAccountsDataRpc {
352    type Metadata = Option<RunloopContext>;
353
354    fn get_account_info(
355        &self,
356        meta: Self::Metadata,
357        pubkey_str: String,
358        config: Option<RpcAccountInfoConfig>,
359    ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>> {
360        let config = config.unwrap_or_default();
361        let pubkey = match verify_pubkey(&pubkey_str) {
362            Ok(res) => res,
363            Err(e) => return e.into(),
364        };
365
366        let SurfnetRpcContext {
367            svm_locker,
368            remote_ctx,
369        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
370            Ok(res) => res,
371            Err(e) => return e.into(),
372        };
373
374        Box::pin(async move {
375            let SvmAccessContext {
376                slot,
377                inner: account_update,
378                ..
379            } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
380            svm_locker.write_account_update(account_update.clone());
381
382            let SvmAccessContext {
383                inner: associated_data,
384                ..
385            } = svm_locker.get_local_account_associated_data(&account_update);
386
387            Ok(RpcResponse {
388                context: RpcResponseContext::new(slot),
389                value: account_update.try_into_ui_account(
390                    config.encoding,
391                    config.data_slice,
392                    associated_data,
393                ),
394            })
395        })
396    }
397
398    fn get_multiple_accounts(
399        &self,
400        meta: Self::Metadata,
401        pubkeys_str: Vec<String>,
402        config: Option<RpcAccountInfoConfig>,
403    ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>> {
404        let config = config.unwrap_or_default();
405        let pubkeys = match pubkeys_str
406            .iter()
407            .map(|s| verify_pubkey(s))
408            .collect::<SurfpoolResult<Vec<_>>>()
409        {
410            Ok(p) => p,
411            Err(e) => return e.into(),
412        };
413
414        let SurfnetRpcContext {
415            svm_locker,
416            remote_ctx,
417        } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
418            Ok(res) => res,
419            Err(e) => return e.into(),
420        };
421
422        Box::pin(async move {
423            let SvmAccessContext {
424                slot,
425                inner: account_updates,
426                ..
427            } = svm_locker
428                .get_multiple_accounts(&remote_ctx, &pubkeys, None)
429                .await?;
430
431            svm_locker.write_multiple_account_updates(&account_updates);
432
433            let mut ui_accounts = vec![];
434            {
435                for account_update in account_updates.into_iter() {
436                    ui_accounts.push(account_update.try_into_ui_account(
437                        config.encoding,
438                        config.data_slice,
439                        None,
440                    ));
441                }
442            }
443
444            Ok(RpcResponse {
445                context: RpcResponseContext::new(slot),
446                value: ui_accounts,
447            })
448        })
449    }
450
451    fn get_block_commitment(
452        &self,
453        meta: Self::Metadata,
454        block: Slot,
455    ) -> Result<RpcBlockCommitment<BlockCommitmentArray>> {
456        // get the info we need and free up lock before validation
457        let (current_slot, block_exists) = meta
458            .with_svm_reader(|svm_reader| {
459                (
460                    svm_reader.get_latest_absolute_slot(),
461                    svm_reader.blocks.contains_key(&block),
462                )
463            })
464            .map_err(Into::<jsonrpc_core::Error>::into)?;
465
466        // block is valid if it exists in our block history or it's not too far in the future
467        if !block_exists && block > current_slot {
468            return Err(jsonrpc_core::Error::invalid_params(format!(
469                "Block {} not found",
470                block
471            )));
472        }
473
474        let commitment_array = [0u64; 32];
475
476        Ok(RpcBlockCommitment {
477            commitment: Some(commitment_array),
478            total_stake: 0,
479        })
480    }
481
482    // SPL Token-specific RPC endpoints
483    // See https://github.com/solana-labs/solana-program-library/releases/tag/token-v2.0.0 for
484    // program details
485
486    fn get_token_account_balance(
487        &self,
488        meta: Self::Metadata,
489        pubkey_str: String,
490        commitment: Option<CommitmentConfig>,
491    ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>> {
492        let pubkey = match verify_pubkey(&pubkey_str) {
493            Ok(res) => res,
494            Err(e) => return e.into(),
495        };
496
497        let SurfnetRpcContext {
498            svm_locker,
499            remote_ctx,
500        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
501            Ok(res) => res,
502            Err(e) => return e.into(),
503        };
504
505        Box::pin(async move {
506            let token_account_result = svm_locker
507                .get_account(&remote_ctx, &pubkey, None)
508                .await?
509                .inner;
510
511            svm_locker.write_account_update(token_account_result.clone());
512
513            let token_account = token_account_result.map_account()?;
514
515            let unpacked_token_account =
516                TokenAccount::unpack(&token_account.data).map_err(|e| {
517                    SurfpoolError::invalid_account_data(
518                        pubkey,
519                        "Invalid token account data",
520                        Some(e.to_string()),
521                    )
522                })?;
523
524            let SvmAccessContext {
525                slot,
526                inner: mint_account_result,
527                ..
528            } = svm_locker
529                .get_account(&remote_ctx, &unpacked_token_account.mint, None)
530                .await?;
531
532            svm_locker.write_account_update(mint_account_result.clone());
533
534            let mint_account = mint_account_result.map_account()?;
535            let unpacked_mint_account = Mint::unpack(&mint_account.data).map_err(|e| {
536                SurfpoolError::invalid_account_data(
537                    unpacked_token_account.mint,
538                    "Invalid token mint account data",
539                    Some(e.to_string()),
540                )
541            })?;
542
543            let token_decimals = unpacked_mint_account.decimals;
544
545            Ok(RpcResponse {
546                context: RpcResponseContext::new(slot),
547                value: {
548                    parse_token_v3(
549                        &token_account.data,
550                        Some(&SplTokenAdditionalDataV2 {
551                            decimals: token_decimals,
552                            ..Default::default()
553                        }),
554                    )
555                    .ok()
556                    .and_then(|t| match t {
557                        TokenAccountType::Account(account) => Some(account.token_amount),
558                        _ => None,
559                    })
560                },
561            })
562        })
563    }
564
565    fn get_token_supply(
566        &self,
567        meta: Self::Metadata,
568        mint_str: String,
569        commitment: Option<CommitmentConfig>,
570    ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>> {
571        let mint_pubkey = match verify_pubkey(&mint_str) {
572            Ok(pubkey) => pubkey,
573            Err(e) => return e.into(),
574        };
575
576        let SurfnetRpcContext {
577            svm_locker,
578            remote_ctx,
579        } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
580            Ok(res) => res,
581            Err(e) => return e.into(),
582        };
583
584        Box::pin(async move {
585            let SvmAccessContext {
586                slot,
587                inner: mint_account_result,
588                ..
589            } = svm_locker
590                .get_account(&remote_ctx, &mint_pubkey, None)
591                .await?;
592
593            svm_locker.write_account_update(mint_account_result.clone());
594
595            let mint_account = mint_account_result.map_account()?;
596
597            if !matches!(mint_account.owner, owner if owner == spl_token::id() || owner == spl_token_2022::id())
598            {
599                return Err(SurfpoolError::invalid_account_data(
600                    mint_pubkey,
601                    "Account is not a token mint account",
602                    None::<String>,
603                )
604                .into());
605            }
606
607            let mint_data = Mint::unpack(&mint_account.data).map_err(|e| {
608                SurfpoolError::invalid_account_data(
609                    mint_pubkey,
610                    "Invalid token mint account data",
611                    Some(e.to_string()),
612                )
613            })?;
614
615            Ok(RpcResponse {
616                context: RpcResponseContext::new(slot),
617                value: {
618                    parse_token_v3(
619                        &mint_account.data,
620                        Some(&SplTokenAdditionalDataV2 {
621                            decimals: mint_data.decimals,
622                            ..Default::default()
623                        }),
624                    )
625                    .ok()
626                    .and_then(|t| match t {
627                        TokenAccountType::Mint(mint) => {
628                            let supply_u64 = mint.supply.parse::<u64>().unwrap_or(0);
629                            let ui_amount = if supply_u64 == 0 {
630                                Some(0.0)
631                            } else {
632                                let divisor = 10_u64.pow(mint.decimals as u32);
633                                Some(supply_u64 as f64 / divisor as f64)
634                            };
635
636                            Some(UiTokenAmount {
637                                amount: mint.supply.clone(),
638                                decimals: mint.decimals,
639                                ui_amount,
640                                ui_amount_string: mint.supply,
641                            })
642                        }
643                        _ => None,
644                    })
645                    .ok_or_else(|| {
646                        SurfpoolError::invalid_account_data(
647                            mint_pubkey,
648                            "Failed to parse token mint account",
649                            None::<String>,
650                        )
651                    })?
652                },
653            })
654        })
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use solana_account::Account;
661    use solana_keypair::Keypair;
662    use solana_pubkey::Pubkey;
663    use solana_sdk::{
664        program_option::COption, program_pack::Pack, system_instruction::create_account,
665    };
666    use solana_signer::Signer;
667    use solana_transaction::Transaction;
668    use spl_associated_token_account::{
669        get_associated_token_address_with_program_id, instruction::create_associated_token_account,
670    };
671    use spl_token::state::{Account as TokenAccount, AccountState, Mint};
672    use spl_token_2022::{
673        extension::StateWithExtensions,
674        instruction::{initialize_mint2, mint_to, transfer_checked},
675        state::Account as Token2022Account,
676    };
677
678    use super::*;
679    use crate::{
680        surfnet::{remote::SurfnetRemoteClient, GetAccountResult},
681        tests::helpers::TestSetup,
682    };
683
684    #[ignore = "connection-required"]
685    #[tokio::test(flavor = "multi_thread")]
686    async fn test_get_token_account_balance() {
687        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
688
689        let mint_pk = Pubkey::new_unique();
690
691        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
692            svm_reader
693                .inner
694                .minimum_balance_for_rent_exemption(Mint::LEN)
695        });
696
697        let mut data = [0; Mint::LEN];
698
699        let default = Mint {
700            decimals: 6,
701            supply: 1000000000000000,
702            is_initialized: true,
703            ..Default::default()
704        };
705        default.pack_into_slice(&mut data);
706
707        let mint_account = Account {
708            lamports: minimum_rent,
709            owner: spl_token::ID,
710            executable: false,
711            rent_epoch: 0,
712            data: data.to_vec(),
713        };
714
715        setup
716            .context
717            .svm_locker
718            .write_account_update(GetAccountResult::FoundAccount(mint_pk, mint_account, true));
719
720        let token_account_pk = Pubkey::new_unique();
721
722        let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
723            svm_reader
724                .inner
725                .minimum_balance_for_rent_exemption(TokenAccount::LEN)
726        });
727
728        let mut data = [0; TokenAccount::LEN];
729
730        let default = TokenAccount {
731            mint: mint_pk,
732            owner: spl_token::ID,
733            state: AccountState::Initialized,
734            amount: 100 * 1000000,
735            ..Default::default()
736        };
737        default.pack_into_slice(&mut data);
738
739        let token_account = Account {
740            lamports: minimum_rent,
741            owner: spl_token::ID,
742            executable: false,
743            rent_epoch: 0,
744            data: data.to_vec(),
745        };
746
747        setup
748            .context
749            .svm_locker
750            .write_account_update(GetAccountResult::FoundAccount(
751                token_account_pk,
752                token_account,
753                true,
754            ));
755
756        let res = setup
757            .rpc
758            .get_token_account_balance(Some(setup.context), token_account_pk.to_string(), None)
759            .await
760            .unwrap();
761
762        assert_eq!(
763            res.value.unwrap(),
764            UiTokenAmount {
765                amount: String::from("100000000"),
766                decimals: 6,
767                ui_amount: Some(100.0),
768                ui_amount_string: String::from("100")
769            }
770        );
771    }
772
773    #[test]
774    fn test_get_block_commitment_past_slot() {
775        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
776        let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
777        let past_slot = if current_slot > 10 {
778            current_slot - 10
779        } else {
780            0
781        };
782
783        let result = setup
784            .rpc
785            .get_block_commitment(Some(setup.context), past_slot)
786            .unwrap();
787
788        // Should return commitment data for past slot
789        assert!(result.commitment.is_some());
790        assert_eq!(result.total_stake, 0);
791    }
792
793    #[test]
794    fn test_get_block_commitment_with_actual_block() {
795        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
796
797        // create a block in the SVM's block history
798        let test_slot = 12345;
799        setup.context.svm_locker.with_svm_writer(|svm_writer| {
800            use crate::surfnet::BlockHeader;
801
802            svm_writer.blocks.insert(
803                test_slot,
804                BlockHeader {
805                    hash: "test_hash".to_string(),
806                    previous_blockhash: "prev_hash".to_string(),
807                    parent_slot: test_slot - 1,
808                    block_time: chrono::Utc::now().timestamp_millis(),
809                    block_height: test_slot,
810                    signatures: vec![],
811                },
812            );
813        });
814
815        let result = setup
816            .rpc
817            .get_block_commitment(Some(setup.context), test_slot)
818            .unwrap();
819
820        // should return commitment data for the existing block
821        assert!(result.commitment.is_some());
822        assert_eq!(result.total_stake, 0);
823    }
824
825    #[test]
826    fn test_get_block_commitment_no_metadata() {
827        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
828
829        let result = setup.rpc.get_block_commitment(None, 123);
830
831        assert!(result.is_err());
832        // This should fail because meta is None, triggering the SurfpoolError::no_locker() path
833    }
834
835    #[test]
836    fn test_get_block_commitment_future_slot_error() {
837        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
838        let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
839        let future_slot = current_slot + 1000;
840
841        let result = setup
842            .rpc
843            .get_block_commitment(Some(setup.context), future_slot);
844
845        // Should return an error for future slots
846        assert!(result.is_err());
847
848        let error = result.unwrap_err();
849        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
850        assert!(error.message.contains("Block") && error.message.contains("not found"));
851    }
852
853    #[tokio::test(flavor = "multi_thread")]
854    async fn test_get_token_supply_with_real_mint() {
855        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
856
857        let mint_pubkey = Pubkey::new_unique();
858
859        // Create mint account data
860        let mut mint_data = [0u8; Mint::LEN];
861        let mint = Mint {
862            mint_authority: COption::Some(Pubkey::new_unique()),
863            supply: 1_000_000_000_000,
864            decimals: 6,
865            is_initialized: true,
866            freeze_authority: COption::None,
867        };
868        Mint::pack(mint, &mut mint_data).unwrap();
869
870        let mint_account = Account {
871            lamports: setup.context.svm_locker.with_svm_reader(|svm_reader| {
872                svm_reader
873                    .inner
874                    .minimum_balance_for_rent_exemption(Mint::LEN)
875            }),
876            data: mint_data.to_vec(),
877            owner: spl_token::id(),
878            executable: false,
879            rent_epoch: 0,
880        };
881
882        setup.context.svm_locker.with_svm_writer(|svm_writer| {
883            svm_writer
884                .set_account(&mint_pubkey, mint_account.clone())
885                .unwrap();
886        });
887
888        let res = setup
889            .rpc
890            .get_token_supply(
891                Some(setup.context),
892                mint_pubkey.to_string(),
893                Some(CommitmentConfig::confirmed()),
894            )
895            .await
896            .unwrap();
897
898        assert_eq!(res.value.amount, "1000000000000");
899        assert_eq!(res.value.decimals, 6);
900        assert_eq!(res.value.ui_amount_string, "1000000000000");
901    }
902
903    #[tokio::test(flavor = "multi_thread")]
904    async fn test_invalid_pubkey_format() {
905        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
906
907        // test various invalid pubkey formats
908        let invalid_pubkeys = vec![
909            "",
910            "invalid",
911            "123",
912            "not-a-valid-base58-string!@#$",
913            "11111111111111111111111111111111111111111111111111111111111111111",
914            "invalid-base58-characters-ö",
915        ];
916
917        for invalid_pubkey in invalid_pubkeys {
918            let res = setup
919                .rpc
920                .get_token_supply(
921                    Some(setup.context.clone()),
922                    invalid_pubkey.to_string(),
923                    Some(CommitmentConfig::confirmed()),
924                )
925                .await;
926
927            assert!(
928                res.is_err(),
929                "Should fail for invalid pubkey: '{}'",
930                invalid_pubkey
931            );
932
933            let error_msg = res.unwrap_err().to_string();
934            assert!(
935                error_msg.contains("Invalid") || error_msg.contains("invalid"),
936                "Error should mention invalidity for '{}': {}",
937                invalid_pubkey,
938                error_msg
939            );
940        }
941
942        println!("✅ All invalid pubkey formats correctly rejected");
943    }
944
945    #[tokio::test(flavor = "multi_thread")]
946    async fn test_nonexistent_account() {
947        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
948
949        // valid pubkey format but nonexistent account
950        let nonexistent_mint = Pubkey::new_unique();
951
952        let res = setup
953            .rpc
954            .get_token_supply(
955                Some(setup.context),
956                nonexistent_mint.to_string(),
957                Some(CommitmentConfig::confirmed()),
958            )
959            .await;
960
961        assert!(res.is_err(), "Should fail for nonexistent account");
962
963        let error_msg = res.unwrap_err().to_string();
964        assert!(
965            error_msg.contains("not found") || error_msg.contains("account"),
966            "Error should mention account not found: {}",
967            error_msg
968        );
969
970        println!("✅ Nonexistent account correctly rejected: {}", error_msg);
971    }
972
973    #[tokio::test(flavor = "multi_thread")]
974    async fn test_invalid_mint_data() {
975        let setup = TestSetup::new(SurfpoolAccountsDataRpc);
976
977        let fake_mint = Pubkey::new_unique();
978
979        setup.context.svm_locker.with_svm_writer(|svm_writer| {
980            // create an account owned by SPL Token but with invalid data
981            let invalid_mint_account = Account {
982                lamports: 1000000,
983                data: vec![0xFF; 50], // invalid mint data (random bytes)
984                owner: spl_token::id(),
985                executable: false,
986                rent_epoch: 0,
987            };
988
989            svm_writer
990                .set_account(&fake_mint, invalid_mint_account)
991                .unwrap();
992        });
993
994        let res = setup
995            .rpc
996            .get_token_supply(
997                Some(setup.context),
998                fake_mint.to_string(),
999                Some(CommitmentConfig::confirmed()),
1000            )
1001            .await;
1002
1003        assert!(
1004            res.is_err(),
1005            "Should fail for account with invalid mint data"
1006        );
1007
1008        let error_msg = res.unwrap_err().to_string();
1009        assert!(
1010            error_msg.contains("deserialize")
1011                || error_msg.contains("Invalid")
1012                || error_msg.contains("parse"),
1013            "Error should mention deserialization failure: {}",
1014            error_msg
1015        );
1016
1017        println!("✅ Invalid mint data correctly rejected: {}", error_msg);
1018    }
1019
1020    #[ignore = "requires-network"]
1021    #[tokio::test(flavor = "multi_thread")]
1022    async fn test_remote_rpc_failure() {
1023        // test with invalid remote RPC URL
1024        let bad_remote_client =
1025            SurfnetRemoteClient::new("https://invalid-url-that-doesnt-exist.com");
1026        let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1027        setup.context.remote_rpc_client = Some(bad_remote_client);
1028
1029        let nonexistent_mint = Pubkey::new_unique();
1030
1031        let res = setup
1032            .rpc
1033            .get_token_supply(
1034                Some(setup.context),
1035                nonexistent_mint.to_string(),
1036                Some(CommitmentConfig::confirmed()),
1037            )
1038            .await;
1039
1040        assert!(res.is_err(), "Should fail when remote RPC is unreachable");
1041
1042        let error_msg = res.unwrap_err().to_string();
1043        println!("✅ Remote RPC failure handled: {}", error_msg);
1044    }
1045
1046    #[tokio::test(flavor = "multi_thread")]
1047    async fn test_transfer_token() {
1048        // Create connection to local validator
1049        let client = TestSetup::new(SurfpoolAccountsDataRpc);
1050        let recent_blockhash = client
1051            .context
1052            .svm_locker
1053            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1054
1055        // Generate a new keypair for the fee payer
1056        let fee_payer = Keypair::new();
1057
1058        // Generate a second keypair for the token recipient
1059        let recipient = Keypair::new();
1060
1061        // Airdrop 1 SOL to fee payer
1062        client
1063            .context
1064            .svm_locker
1065            .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1066            .unwrap();
1067
1068        // Airdrop 1 SOL to recipient for rent exemption
1069        client
1070            .context
1071            .svm_locker
1072            .airdrop(&recipient.pubkey(), 1_000_000_000)
1073            .unwrap();
1074
1075        // Generate keypair to use as address of mint
1076        let mint = Keypair::new();
1077
1078        // Get default mint account size (in bytes), no extensions enabled
1079        let mint_space = Mint::LEN;
1080        let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1081            svm_reader
1082                .inner
1083                .minimum_balance_for_rent_exemption(mint_space)
1084        });
1085
1086        // Instruction to create new account for mint (token 2022 program)
1087        let create_account_instruction = create_account(
1088            &fee_payer.pubkey(),   // payer
1089            &mint.pubkey(),        // new account (mint)
1090            mint_rent,             // lamports
1091            mint_space as u64,     // space
1092            &spl_token_2022::id(), // program id
1093        );
1094
1095        // Instruction to initialize mint account data
1096        let initialize_mint_instruction = initialize_mint2(
1097            &spl_token_2022::id(),
1098            &mint.pubkey(),            // mint
1099            &fee_payer.pubkey(),       // mint authority
1100            Some(&fee_payer.pubkey()), // freeze authority
1101            2,                         // decimals
1102        )
1103        .unwrap();
1104
1105        // Calculate the associated token account address for fee_payer
1106        let source_token_address = get_associated_token_address_with_program_id(
1107            &fee_payer.pubkey(),   // owner
1108            &mint.pubkey(),        // mint
1109            &spl_token_2022::id(), // program_id
1110        );
1111
1112        // Instruction to create associated token account for fee_payer
1113        let create_source_ata_instruction = create_associated_token_account(
1114            &fee_payer.pubkey(),   // funding address
1115            &fee_payer.pubkey(),   // wallet address
1116            &mint.pubkey(),        // mint address
1117            &spl_token_2022::id(), // program id
1118        );
1119
1120        // Calculate the associated token account address for recipient
1121        let destination_token_address = get_associated_token_address_with_program_id(
1122            &recipient.pubkey(),   // owner
1123            &mint.pubkey(),        // mint
1124            &spl_token_2022::id(), // program_id
1125        );
1126
1127        // Instruction to create associated token account for recipient
1128        let create_destination_ata_instruction = create_associated_token_account(
1129            &fee_payer.pubkey(),   // funding address
1130            &recipient.pubkey(),   // wallet address
1131            &mint.pubkey(),        // mint address
1132            &spl_token_2022::id(), // program id
1133        );
1134
1135        // Amount of tokens to mint (100 tokens with 2 decimal places)
1136        let amount = 100_00;
1137
1138        // Create mint_to instruction to mint tokens to the source token account
1139        let mint_to_instruction = mint_to(
1140            &spl_token_2022::id(),
1141            &mint.pubkey(),         // mint
1142            &source_token_address,  // destination
1143            &fee_payer.pubkey(),    // authority
1144            &[&fee_payer.pubkey()], // signer
1145            amount,                 // amount
1146        )
1147        .unwrap();
1148
1149        // Create transaction and add instructions
1150        let transaction = Transaction::new_signed_with_payer(
1151            &[
1152                create_account_instruction,
1153                initialize_mint_instruction,
1154                create_source_ata_instruction,
1155                create_destination_ata_instruction,
1156                mint_to_instruction,
1157            ],
1158            Some(&fee_payer.pubkey()),
1159            &[&fee_payer, &mint],
1160            recent_blockhash,
1161        );
1162
1163        // Send and confirm transaction
1164        client.context.svm_locker.with_svm_writer(|svm_writer| {
1165            svm_writer
1166                .send_transaction(transaction.clone().into(), false)
1167                .unwrap();
1168        });
1169
1170        println!("Mint Address: {}", mint.pubkey());
1171        println!("Recipient Address: {}", recipient.pubkey());
1172        println!("Source Token Account Address: {}", source_token_address);
1173        println!(
1174            "Destination Token Account Address: {}",
1175            destination_token_address
1176        );
1177        println!("Minted {} tokens to the source token account", amount);
1178
1179        // Get the latest blockhash for the transfer transaction
1180        let recent_blockhash = client
1181            .context
1182            .svm_locker
1183            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1184
1185        // Amount of tokens to transfer (0.50 tokens with 2 decimals)
1186        let transfer_amount = 50;
1187
1188        // Create transfer_checked instruction to send tokens from source to destination
1189        let transfer_instruction = transfer_checked(
1190            &spl_token_2022::id(),      // program id
1191            &source_token_address,      // source
1192            &mint.pubkey(),             // mint
1193            &destination_token_address, // destination
1194            &fee_payer.pubkey(),        // owner of source
1195            &[&fee_payer.pubkey()],     // signers
1196            transfer_amount,            // amount
1197            2,                          // decimals
1198        )
1199        .unwrap();
1200
1201        // Create transaction for transferring tokens
1202        let transaction = Transaction::new_signed_with_payer(
1203            &[transfer_instruction],
1204            Some(&fee_payer.pubkey()),
1205            &[&fee_payer],
1206            recent_blockhash,
1207        );
1208
1209        // Send and confirm transaction
1210        client.context.svm_locker.with_svm_writer(|svm_writer| {
1211            svm_writer
1212                .send_transaction(transaction.clone().into(), false)
1213                .unwrap();
1214        });
1215
1216        println!("Successfully transferred 0.50 tokens from sender to recipient");
1217
1218        // Get token account balances to verify the transfer
1219        let source_token_account = client
1220            .context
1221            .svm_locker
1222            .get_account_local(&source_token_address)
1223            .inner;
1224        let destination_token_account = client
1225            .context
1226            .svm_locker
1227            .get_account_local(&destination_token_address)
1228            .inner;
1229
1230        if let GetAccountResult::FoundAccount(_, source_account, _) = source_token_account {
1231            let unpacked =
1232                StateWithExtensions::<Token2022Account>::unpack(&source_account.data).unwrap();
1233            println!(
1234                "Source Token Account Balance: {} tokens",
1235                unpacked.base.amount
1236            );
1237            assert_eq!(unpacked.base.amount, 9950);
1238        }
1239
1240        if let GetAccountResult::FoundAccount(_, destination_account, _) = destination_token_account
1241        {
1242            let unpacked =
1243                StateWithExtensions::<Token2022Account>::unpack(&destination_account.data).unwrap();
1244            println!(
1245                "Destination Token Account Balance: {} tokens",
1246                unpacked.base.amount
1247            );
1248            assert_eq!(unpacked.base.amount, 50);
1249        }
1250    }
1251}