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