Skip to main content

surfpool_core/rpc/
full.rs

1use std::str::FromStr;
2
3use itertools::Itertools;
4use jsonrpc_core::{BoxFuture, Error, Result};
5use jsonrpc_derive::rpc;
6use litesvm::types::TransactionMetadata;
7use solana_account_decoder::UiAccount;
8use solana_client::{
9    rpc_config::{
10        RpcAccountInfoConfig, RpcBlockConfig, RpcBlocksConfigWrapper, RpcContextConfig,
11        RpcEncodingConfigWrapper, RpcEpochConfig, RpcRequestAirdropConfig,
12        RpcSendTransactionConfig, RpcSignatureStatusConfig, RpcSignaturesForAddressConfig,
13        RpcSimulateTransactionConfig, RpcTransactionConfig,
14    },
15    rpc_custom_error::RpcCustomError,
16    rpc_response::{
17        RpcBlockhash, RpcConfirmedTransactionStatusWithSignature, RpcContactInfo,
18        RpcInflationReward, RpcPerfSample, RpcPrioritizationFee, RpcResponseContext,
19        RpcSimulateTransactionResult,
20    },
21};
22use solana_clock::{Slot, UnixTimestamp};
23use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
24use solana_compute_budget_interface::ComputeBudgetInstruction;
25use solana_message::{VersionedMessage, compiled_instruction::CompiledInstruction};
26use solana_pubkey::Pubkey;
27use solana_rpc_client_api::response::Response as RpcResponse;
28use solana_sdk_ids::compute_budget;
29use solana_signature::Signature;
30use solana_system_interface::program as system_program;
31use solana_transaction::versioned::VersionedTransaction;
32use solana_transaction_error::TransactionError;
33use solana_transaction_status::{
34    EncodedConfirmedTransactionWithStatusMeta, TransactionBinaryEncoding,
35    TransactionConfirmationStatus, TransactionStatus, UiConfirmedBlock, UiTransactionEncoding,
36};
37use surfpool_types::{SimnetCommand, TransactionStatusEvent};
38
39use super::{
40    RunloopContext, State, SurfnetRpcContext,
41    utils::{decode_and_deserialize, transform_tx_metadata_to_ui_accounts, verify_pubkey},
42};
43use crate::{
44    SURFPOOL_IDENTITY_PUBKEY,
45    error::{SurfpoolError, SurfpoolResult},
46    rpc::utils::{adjust_default_transaction_config, get_default_transaction_config},
47    surfnet::{
48        FINALIZATION_SLOT_THRESHOLD, GetAccountResult, GetTransactionResult,
49        locker::SvmAccessContext, svm::MAX_RECENT_BLOCKHASHES_STANDARD,
50    },
51    types::{SurfnetTransactionStatus, surfpool_tx_metadata_to_litesvm_tx_metadata},
52};
53
54const MAX_PRIORITIZATION_FEE_BLOCKS_CACHE: usize = 150;
55
56#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct SurfpoolRpcSendTransactionConfig {
59    #[serde(flatten)]
60    pub base: RpcSendTransactionConfig,
61    /// skip sign verification for this txn (overrides global config)
62    pub skip_sig_verify: Option<bool>,
63}
64
65#[rpc]
66pub trait Full {
67    type Metadata;
68
69    /// Retrieves inflation rewards for a list of addresses over a specified epoch or context.
70    ///
71    /// This RPC method allows you to query the inflation rewards credited to specific validator or voter addresses
72    /// in a given epoch or range of slots. The rewards are provided as lamports, which are the smallest unit of SOL.
73    ///
74    /// ## Parameters
75    /// - `address_strs`: A list of base-58 encoded public keys for which to query inflation rewards.
76    /// - `config`: An optional configuration that allows you to specify:
77    ///     - `epoch`: The epoch to query for inflation rewards. If `None`, the current epoch is used.
78    ///     - `commitment`: The optional commitment level to use when querying for rewards.
79    ///     - `min_context_slot`: The minimum slot to be considered when retrieving the rewards.
80    ///
81    /// ## Returns
82    /// - `BoxFuture<Result<Vec<Option<RpcInflationReward>>>>`: A future that resolves to a vector of inflation reward information for each address provided.
83    ///
84    /// ## Example Request (JSON-RPC)
85    /// ```json
86    /// {
87    ///   "jsonrpc": "2.0",
88    ///   "id": 1,
89    ///   "method": "getInflationReward",
90    ///   "params": [
91    ///     ["3HgA9r8H9z5Pb2L6Pt5Yq1QoFwgr6YwdKKUh9n2ANp5U", "BBh1FwXts8EZY6rPZ5kS2ygq99wYjFd5K5daRjc7eF9X"],
92    ///     {
93    ///       "epoch": 200,
94    ///       "commitment": {"commitment": "finalized"}
95    ///     }
96    ///   ]
97    /// }
98    /// ```
99    ///
100    /// ## Example Response
101    /// ```json
102    /// {
103    ///   "jsonrpc": "2.0",
104    ///   "result": [
105    ///     {
106    ///       "epoch": 200,
107    ///       "effectiveSlot": 123456,
108    ///       "amount": 5000000,
109    ///       "postBalance": 1000000000,
110    ///       "commission": 10
111    ///     },
112    ///     null
113    ///   ]
114    /// }
115    /// ```
116    ///
117    /// # Notes
118    /// - The `address_strs` parameter should contain the list of addresses for which to query rewards.
119    /// - The response is a vector where each entry corresponds to an address in the `address_strs` input list.
120    /// - If an address did not receive any reward during the query period, its corresponding entry in the result will be `null`.
121    /// - The `amount` field represents the inflation reward (in lamports) that was credited to the address during the epoch.
122    /// - The `post_balance` field represents the account balance after the reward was applied.
123    /// - The `commission` field, if present, indicates the percentage commission (as an integer) for a vote account when the reward was credited.
124    ///
125    /// ## Example Response Interpretation
126    /// - In the example response, the first address `3HgA9r8H9z5Pb2L6Pt5Yq1QoFwgr6YwdKKUh9n2ANp5U` received 5,000,000 lamports during epoch 200, with a post-reward balance of 1,000,000,000 lamports and a 10% commission.
127    /// - The second address did not receive any inflation reward (represented as `null`).
128    #[rpc(meta, name = "getInflationReward")]
129    fn get_inflation_reward(
130        &self,
131        meta: Self::Metadata,
132        address_strs: Vec<String>,
133        config: Option<RpcEpochConfig>,
134    ) -> BoxFuture<Result<Vec<Option<RpcInflationReward>>>>;
135
136    /// Retrieves the list of cluster nodes and their contact information.
137    ///
138    /// This RPC method returns a list of nodes in the cluster, including their public keys and various
139    /// communication ports, such as the gossip, Tpu, and RPC ports. This information is essential for
140    /// understanding the connectivity and configuration of nodes in a Solana cluster.
141    ///
142    /// ## Returns
143    /// - `Result<Vec<RpcContactInfo>>`: A result containing a vector of `RpcContactInfo` objects, each representing a node's contact information in the cluster.
144    ///
145    /// ## Example Request (JSON-RPC)
146    /// ```json
147    /// {
148    ///   "jsonrpc": "2.0",
149    ///   "id": 1,
150    ///   "method": "getClusterNodes",
151    ///   "params": []
152    /// }
153    /// ```
154    ///
155    /// ## Example Response
156    /// ```json
157    /// {
158    ///   "jsonrpc": "2.0",
159    ///   "result": [
160    ///     {
161    ///       "pubkey": "3HgA9r8H9z5Pb2L6Pt5Yq1QoFwgr6YwdKKUh9n2ANp5U",
162    ///       "gossip": "127.0.0.1:8001",
163    ///       "tvu": "127.0.0.1:8002",
164    ///       "tpu": "127.0.0.1:8003",
165    ///       "tpu_quic": "127.0.0.1:8004",
166    ///       "rpc": "127.0.0.1:8899",
167    ///       "pubsub": "127.0.0.1:8900",
168    ///       "version": "v1.9.0",
169    ///       "feature_set": 1,
170    ///       "shred_version": 3
171    ///     }
172    ///   ]
173    /// }
174    /// ```
175    ///
176    /// # Notes
177    /// - The response contains a list of nodes, each identified by its public key and with multiple optional ports for different services.
178    /// - If a port is not configured, its value will be `null`.
179    /// - The `version` field contains the software version of the node.
180    #[rpc(meta, name = "getClusterNodes")]
181    fn get_cluster_nodes(&self, meta: Self::Metadata) -> Result<Vec<RpcContactInfo>>;
182
183    /// Retrieves recent performance samples of the Solana network.
184    ///
185    /// This RPC method provides performance metrics from the most recent samples, such as the number
186    /// of transactions processed, slots, and the period over which these metrics were collected.
187    ///
188    /// ## Parameters
189    /// - `limit`: An optional parameter that specifies the maximum number of performance samples to return. If not provided, all available samples will be returned.
190    ///
191    /// ## Returns
192    /// - `Result<Vec<RpcPerfSample>>`: A result containing a vector of `RpcPerfSample` objects, each representing a snapshot of the network's performance for a particular slot.
193    ///
194    /// ## Example Request (JSON-RPC)
195    /// ```json
196    /// {
197    ///   "jsonrpc": "2.0",
198    ///   "id": 1,
199    ///   "method": "getRecentPerformanceSamples",
200    ///   "params": [10]
201    /// }
202    /// ```
203    ///
204    /// ## Example Response
205    /// ```json
206    /// {
207    ///   "jsonrpc": "2.0",
208    ///   "result": [
209    ///     {
210    ///       "slot": 12345,
211    ///       "num_transactions": 1000,
212    ///       "num_non_vote_transactions": 800,
213    ///       "num_slots": 10,
214    ///       "sample_period_secs": 60
215    ///     }
216    ///   ]
217    /// }
218    /// ```
219    ///
220    /// # Notes
221    /// - The `num_transactions` field represents the total number of transactions processed in the given slot.
222    /// - The `num_non_vote_transactions` field is optional and represents the number of transactions that are not related to voting.
223    /// - The `num_slots` field indicates the number of slots sampled for the given period.
224    /// - The `sample_period_secs` represents the time period in seconds over which the performance sample was taken.
225    #[rpc(meta, name = "getRecentPerformanceSamples")]
226    fn get_recent_performance_samples(
227        &self,
228        meta: Self::Metadata,
229        limit: Option<usize>,
230    ) -> Result<Vec<RpcPerfSample>>;
231
232    /// Retrieves the status of multiple transactions given their signatures.
233    ///
234    /// This RPC call returns the status of transactions, including details such as the transaction's
235    /// slot, the number of confirmations it has, its success or failure status, and any errors that might have occurred.
236    /// Optionally, it can also provide transaction history search results based on the provided configuration.
237    ///
238    /// ## Parameters
239    /// - `signatureStrs`: A list of base-58 encoded transaction signatures for which the statuses are to be retrieved.
240    /// - `config`: An optional configuration object to modify the query, such as enabling search for transaction history.
241    ///   - If `None`, defaults to querying the current status of the provided transactions.
242    ///
243    /// ## Returns
244    /// A response containing:
245    /// - `value`: A list of transaction statuses corresponding to the provided transaction signatures. Each entry in the list can be:
246    ///   - A successful status (`status` field set to `"Ok"`)
247    ///   - An error status (`status` field set to `"Err"`)
248    ///   - A transaction's error information (e.g., `InsufficientFundsForFee`, `AccountNotFound`, etc.)
249    ///   - The slot in which the transaction was processed.
250    ///   - The number of confirmations the transaction has received (if applicable).
251    ///   - The confirmation status (`"processed"`, `"confirmed"`, or `"finalized"`).
252    ///
253    /// ## Example Request
254    /// ```json
255    /// {
256    ///   "jsonrpc": "2.0",
257    ///   "id": 1,
258    ///   "method": "getSignatureStatuses",
259    ///   "params": [
260    ///     [
261    ///       "5FJkGv5JrMwWe6Eqn24Lz6vgsJ9y8g4rVZn3z9pKfqGhWR23Zef5GjS6SCN8h4J7rb42yYoA4m83d5V7A2KhQkm3",
262    ///       "5eJZXh7FnSeFw5uJ5t9t5bjsKqS7khtjeFu6gAtfhsNj5fQYs5KZ5ZscknzFhfQj2rNJ4W2QqijKsyZk8tqbrT9m"
263    ///     ],
264    ///     {
265    ///       "searchTransactionHistory": true
266    ///     }
267    ///   ]
268    /// }
269    /// ```
270    ///
271    /// ## Example Response
272    /// ```json
273    /// {
274    ///   "jsonrpc": "2.0",
275    ///   "id": 1,
276    ///   "result": {
277    ///     "value": [
278    ///       {
279    ///         "slot": 1234567,
280    ///         "confirmations": 5,
281    ///         "status": {
282    ///           "ok": {}
283    ///         },
284    ///         "err": null,
285    ///         "confirmationStatus": "confirmed"
286    ///       },
287    ///       {
288    ///         "slot": 1234568,
289    ///         "confirmations": 3,
290    ///         "status": {
291    ///           "err": {
292    ///             "insufficientFundsForFee": {}
293    ///           }
294    ///         },
295    ///         "err": {
296    ///           "insufficientFundsForFee": {}
297    ///         },
298    ///         "confirmationStatus": "processed"
299    ///       }
300    ///     ]
301    ///   }
302    /// }
303    /// ```
304    ///
305    /// ## Errors
306    /// - Returns an error if there was an issue processing the request, such as network failures or invalid signatures.
307    ///
308    /// # Notes
309    /// - The `TransactionStatus` contains various error types (e.g., `TransactionError`) and confirmation statuses (e.g., `TransactionConfirmationStatus`), which can be used to determine the cause of failure or the progress of the transaction's confirmation.
310    ///
311    /// # See Also
312    /// - [`TransactionStatus`](#TransactionStatus)
313    /// - [`RpcSignatureStatusConfig`](#RpcSignatureStatusConfig)
314    /// - [`TransactionError`](#TransactionError)
315    /// - [`TransactionConfirmationStatus`](#TransactionConfirmationStatus)
316    #[rpc(meta, name = "getSignatureStatuses")]
317    fn get_signature_statuses(
318        &self,
319        meta: Self::Metadata,
320        signature_strs: Vec<String>,
321        config: Option<RpcSignatureStatusConfig>,
322    ) -> BoxFuture<Result<RpcResponse<Vec<Option<TransactionStatus>>>>>;
323
324    /// Retrieves the maximum slot number for which data may be retransmitted.
325    ///
326    /// This RPC call returns the highest slot that can be retransmitted in the cluster, typically
327    /// representing the latest possible slot that may still be valid for network retransmissions.
328    ///
329    /// ## Returns
330    /// A response containing:
331    /// - `value`: The maximum slot number available for retransmission. This is an integer value representing the highest slot
332    ///   for which data can be retrieved or retransmitted from the network.
333    ///
334    /// ## Example Request
335    /// ```json
336    /// {
337    ///   "jsonrpc": "2.0",
338    ///   "id": 1,
339    ///   "method": "getMaxRetransmitSlot",
340    ///   "params": []
341    /// }
342    /// ```
343    ///
344    /// ## Example Response
345    /// ```json
346    /// {
347    ///   "jsonrpc": "2.0",
348    ///   "id": 1,
349    ///   "result": {
350    ///     "value": 1234567
351    ///   }
352    /// }
353    /// ```
354    ///
355    /// ## Errors
356    /// - Returns an error if there was an issue processing the request, such as network failure.
357    ///
358    /// # Notes
359    /// - The slot number returned by this RPC call can be used to identify the highest valid slot for retransmission,
360    ///   which may be useful for managing data synchronization across nodes in the cluster.
361    ///
362    /// # See Also
363    /// - `getSlot`
364    #[rpc(meta, name = "getMaxRetransmitSlot")]
365    fn get_max_retransmit_slot(&self, meta: Self::Metadata) -> Result<Slot>;
366
367    /// Retrieves the maximum slot number for which shreds may be inserted into the ledger.
368    ///
369    /// This RPC call returns the highest slot for which data can still be inserted (shredded) into the ledger,
370    /// typically indicating the most recent slot that can be included in the block production process.
371    ///
372    /// ## Returns
373    /// A response containing:
374    /// - `value`: The maximum slot number for which shreds can be inserted. This is an integer value that represents
375    ///   the latest valid slot for including data in the ledger.
376    ///
377    /// ## Example Request
378    /// ```json
379    /// {
380    ///   "jsonrpc": "2.0",
381    ///   "id": 1,
382    ///   "method": "getMaxShredInsertSlot",
383    ///   "params": []
384    /// }
385    /// ```
386    ///
387    /// ## Example Response
388    /// ```json
389    /// {
390    ///   "jsonrpc": "2.0",
391    ///   "id": 1,
392    ///   "result": {
393    ///     "value": 1234567
394    ///   }
395    /// }
396    /// ```
397    ///
398    /// ## Errors
399    /// - Returns an error if there was an issue processing the request, such as network failure.
400    ///
401    /// # Notes
402    /// - This method is used to identify the highest slot where data can still be added to the ledger.
403    ///   This is useful for managing the block insertion process and synchronizing data across the network.
404    ///
405    /// # See Also
406    /// - `getSlot`
407    #[rpc(meta, name = "getMaxShredInsertSlot")]
408    fn get_max_shred_insert_slot(&self, meta: Self::Metadata) -> Result<Slot>;
409
410    /// Requests an airdrop of lamports to the specified public key.
411    ///
412    /// This RPC call triggers the network to send a specified amount of lamports to the given public key.
413    /// It is commonly used for testing or initial setup of accounts.
414    ///
415    /// ## Parameters
416    /// - `pubkeyStr`: The public key (as a base-58 encoded string) to which the airdrop will be sent.
417    /// - `lamports`: The amount of lamports to be sent. This is the smallest unit of the native cryptocurrency.
418    /// - `config`: Optional configuration for the airdrop request.
419    ///
420    /// ## Returns
421    /// A response containing:
422    /// - `value`: A string representing the transaction signature for the airdrop request. This signature can be
423    ///   used to track the status of the transaction.
424    ///
425    /// ## Example Request
426    /// ```json
427    /// {
428    ///   "jsonrpc": "2.0",
429    ///   "id": 1,
430    ///   "method": "requestAirdrop",
431    ///   "params": [
432    ///     "PublicKeyHere",
433    ///     1000000,
434    ///     {}
435    ///   ]
436    /// }
437    /// ```
438    ///
439    /// ## Example Response
440    /// ```json
441    /// {
442    ///   "jsonrpc": "2.0",
443    ///   "id": 1,
444    ///   "result": "TransactionSignatureHere"
445    /// }
446    /// ```
447    ///
448    /// ## Errors
449    /// - Returns an error if there is an issue with the airdrop request, such as invalid public key or insufficient funds.
450    ///
451    /// # Notes
452    /// - Airdrop requests are commonly used for testing or initializing accounts in the development environment.
453    ///   This is not typically used in a production environment where real funds are at stake.
454    ///
455    /// # See Also
456    /// - `getBalance`
457    #[rpc(meta, name = "requestAirdrop")]
458    fn request_airdrop(
459        &self,
460        meta: Self::Metadata,
461        pubkey_str: String,
462        lamports: u64,
463        config: Option<RpcRequestAirdropConfig>,
464    ) -> Result<String>;
465
466    /// Sends a transaction to the network.
467    ///
468    /// This RPC method is used to submit a signed transaction to the network for processing.
469    /// The transaction will be broadcast to the network, and the method returns a transaction signature
470    /// that can be used to track the transaction's status.
471    ///
472    /// ## Parameters
473    /// - `data`: The serialized transaction data in a specified encoding format.
474    /// - `config`: Optional configuration for the transaction submission, including settings for retries, commitment level,
475    ///   and encoding.
476    ///
477    /// ## Returns
478    /// A response containing:
479    /// - `value`: A string representing the transaction signature for the submitted transaction.
480    ///
481    /// ## Example Request
482    /// ```json
483    /// {
484    ///   "jsonrpc": "2.0",
485    ///   "id": 1,
486    ///   "method": "sendTransaction",
487    ///   "params": [
488    ///     "TransactionDataHere",
489    ///     {
490    ///       "skipPreflight": false,
491    ///       "preflightCommitment": "processed",
492    ///       "encoding": "base64",
493    ///       "maxRetries": 3
494    ///     }
495    ///   ]
496    /// }
497    /// ```
498    ///
499    /// ## Example Response
500    /// ```json
501    /// {
502    ///   "jsonrpc": "2.0",
503    ///   "id": 1,
504    ///   "result": "TransactionSignatureHere"
505    /// }
506    /// ```
507    ///
508    /// ## Errors
509    /// - Returns an error if the transaction fails to send, such as network issues or invalid transaction data.
510    ///
511    /// # Notes
512    /// - This method is primarily used for submitting a signed transaction to the network and obtaining a signature
513    ///   to track the transaction's status.
514    /// - The `skipPreflight` option, if set to true, bypasses the preflight checks to speed up the transaction submission.
515    ///
516    /// # See Also
517    /// - `getTransactionStatus`
518    #[rpc(meta, name = "sendTransaction")]
519    fn send_transaction(
520        &self,
521        meta: Self::Metadata,
522        data: String,
523        config: Option<SurfpoolRpcSendTransactionConfig>,
524    ) -> Result<String>;
525
526    /// Simulates a transaction without sending it to the network.
527    ///
528    /// This RPC method simulates a transaction locally, allowing users to check how a transaction would
529    /// behave on the blockchain without actually broadcasting it. It is useful for testing and debugging
530    /// before sending a transaction to the network.
531    ///
532    /// ## Parameters
533    /// - `data`: The serialized transaction data in a specified encoding format.
534    /// - `config`: Optional configuration for simulating the transaction, including settings for signature verification,
535    ///   blockhash replacement, and more.
536    ///
537    /// ## Returns
538    /// A response containing:
539    /// - `value`: An object with the result of the simulation, which includes information such as errors,
540    ///   logs, accounts, units consumed, and return data.
541    ///
542    /// ## Example Request
543    /// ```json
544    /// {
545    ///   "jsonrpc": "2.0",
546    ///   "id": 1,
547    ///   "method": "simulateTransaction",
548    ///   "params": [
549    ///     "TransactionDataHere",
550    ///     {
551    ///       "sigVerify": true,
552    ///       "replaceRecentBlockhash": true,
553    ///       "encoding": "base64",
554    ///       "innerInstructions": true
555    ///     }
556    ///   ]
557    /// }
558    /// ```
559    ///
560    /// ## Example Response
561    /// ```json
562    /// {
563    ///   "jsonrpc": "2.0",
564    ///   "id": 1,
565    ///   "result": {
566    ///     "err": null,
567    ///     "logs": ["Log output"],
568    ///     "accounts": [null, {}],
569    ///     "unitsConsumed": 12345,
570    ///     "returnData": {
571    ///       "programId": "ProgramIDHere",
572    ///       "data": ["returnDataHere", "base64"]
573    ///     },
574    ///     "innerInstructions": [{
575    ///       "index": 0,
576    ///       "instructions": [{ "parsed": { "programIdIndex": 0 } }]
577    ///     }],
578    ///     "replacementBlockhash": "BlockhashHere"
579    ///   }
580    /// }
581    /// ```
582    ///
583    /// ## Errors
584    /// - Returns an error if the transaction simulation fails due to invalid data or other issues.
585    ///
586    /// # Notes
587    /// - This method simulates the transaction locally and does not affect the actual blockchain state.
588    /// - The `sigVerify` flag determines whether the transaction's signature should be verified during the simulation.
589    /// - The `replaceRecentBlockhash` flag allows the simulation to use the most recent blockhash for the transaction.
590    ///
591    /// # See Also
592    /// - `getTransactionStatus`
593    #[rpc(meta, name = "simulateTransaction")]
594    fn simulate_transaction(
595        &self,
596        meta: Self::Metadata,
597        data: String,
598        config: Option<RpcSimulateTransactionConfig>,
599    ) -> BoxFuture<Result<RpcResponse<RpcSimulateTransactionResult>>>;
600
601    /// Retrieves the minimum ledger slot.
602    ///
603    /// This RPC method returns the minimum ledger slot, which is the smallest slot number that
604    /// contains some data or transaction. It is useful for understanding the earliest point in the
605    /// blockchain's history where data is available.
606    ///
607    /// ## Parameters
608    /// - None.
609    ///
610    /// ## Returns
611    /// The minimum ledger slot as an integer representing the earliest slot where data is available.
612    ///
613    /// ## Example Request
614    /// ```json
615    /// {
616    ///   "jsonrpc": "2.0",
617    ///   "id": 1,
618    ///   "method": "minimumLedgerSlot",
619    ///   "params": []
620    /// }
621    /// ```
622    ///
623    /// ## Example Response
624    /// ```json
625    /// {
626    ///   "jsonrpc": "2.0",
627    ///   "id": 1,
628    ///   "result": 123456
629    /// }
630    /// ```
631    ///
632    /// ## Errors
633    /// - Returns an error if the ledger slot retrieval fails.
634    ///
635    /// # Notes
636    /// - The returned slot is typically the earliest slot that contains useful data for the ledger.
637    ///
638    /// # See Also
639    /// - `getSlot`
640    #[rpc(meta, name = "minimumLedgerSlot")]
641    fn minimum_ledger_slot(&self, meta: Self::Metadata) -> BoxFuture<Result<Slot>>;
642
643    /// Retrieves the details of a block in the blockchain.
644    ///
645    /// This RPC method fetches a block's details, including its transactions and associated metadata,
646    /// given a specific slot number. The response includes information like the block's hash, previous
647    /// block hash, rewards, transactions, and more.
648    ///
649    /// ## Parameters
650    /// - `slot`: The slot number of the block you want to retrieve. This is the block's position in the
651    ///   chain.
652    /// - `config`: Optional configuration for the block retrieval. This allows you to customize the
653    ///   encoding and details returned in the response (e.g., full transaction details, rewards, etc.).
654    ///
655    /// ## Returns
656    /// A `UiConfirmedBlock` containing the block's information, such as the block's hash, previous block
657    /// hash, and an optional list of transactions and rewards. If no block is found for the provided slot,
658    /// the response will be `None`.
659    ///
660    /// ## Example Request
661    /// ```json
662    /// {
663    ///   "jsonrpc": "2.0",
664    ///   "id": 1,
665    ///   "method": "getBlock",
666    ///   "params": [123456, {"encoding": "json", "transactionDetails": "full"}]
667    /// }
668    /// ```
669    ///
670    /// ## Example Response
671    /// ```json
672    /// {
673    ///   "jsonrpc": "2.0",
674    ///   "id": 1,
675    ///   "result": {
676    ///     "previousBlockhash": "abc123",
677    ///     "blockhash": "def456",
678    ///     "parentSlot": 123455,
679    ///     "transactions": [ ... ],
680    ///     "rewards": [ ... ],
681    ///     "blockTime": 1620000000,
682    ///     "blockHeight": 1000
683    ///   }
684    /// }
685    /// ```
686    ///
687    /// ## Errors
688    /// - Returns an error if the block cannot be found for the specified slot.
689    /// - Returns an error if there is an issue with the configuration options provided.
690    ///
691    /// # Notes
692    /// - The `transactionDetails` field in the configuration can be used to specify the level of detail
693    ///   you want for transactions within the block (e.g., full transaction data, only signatures, etc.).
694    /// - The block's `blockhash` and `previousBlockhash` are crucial for navigating through the blockchain's
695    ///   history.
696    ///
697    /// # See Also
698    /// - `getSlot`, `getBlockHeight`
699    #[rpc(meta, name = "getBlock")]
700    fn get_block(
701        &self,
702        meta: Self::Metadata,
703        slot: Slot,
704        config: Option<RpcEncodingConfigWrapper<RpcBlockConfig>>,
705    ) -> BoxFuture<Result<Option<UiConfirmedBlock>>>;
706
707    /// Retrieves the timestamp for a block, given its slot number.
708    ///
709    /// This RPC method fetches the timestamp of the block associated with a given slot. The timestamp
710    /// represents the time at which the block was created.
711    ///
712    /// ## Parameters
713    /// - `slot`: The slot number of the block you want to retrieve the timestamp for. This is the block's
714    ///   position in the chain.
715    ///
716    /// ## Returns
717    /// A `UnixTimestamp` containing the block's creation time in seconds since the Unix epoch. If no
718    /// block exists for the provided slot, the response will be `None`.
719    ///
720    /// ## Example Request
721    /// ```json
722    /// {
723    ///   "jsonrpc": "2.0",
724    ///   "id": 1,
725    ///   "method": "getBlockTime",
726    ///   "params": [123456]
727    /// }
728    /// ```
729    ///
730    /// ## Example Response
731    /// ```json
732    /// {
733    ///   "jsonrpc": "2.0",
734    ///   "id": 1,
735    ///   "result": 1752080472
736    /// }
737    /// ```
738    ///
739    /// ## Errors
740    /// - Returns an error if there is an issue with the provided slot or if the slot is invalid.
741    ///
742    /// # Notes
743    /// - The returned `UnixTimestamp` represents the time in seconds since the Unix epoch (1970-01-01 00:00:00 UTC).
744    /// - If the block for the given slot has not been processed or does not exist, the response will be `None`.
745    ///
746    /// # See Also
747    /// - `getBlock`, `getSlot`, `getBlockHeight`
748    #[rpc(meta, name = "getBlockTime")]
749    fn get_block_time(
750        &self,
751        meta: Self::Metadata,
752        slot: Slot,
753    ) -> BoxFuture<Result<Option<UnixTimestamp>>>;
754
755    /// Retrieves a list of slot numbers starting from a given `start_slot`.
756    ///
757    /// This RPC method fetches a sequence of block slots starting from the specified `start_slot`
758    /// and continuing until a defined `end_slot` (if provided). If no `end_slot` is specified,
759    /// it will return all blocks from the `start_slot` onward.
760    ///
761    /// ## Parameters
762    /// - `start_slot`: The slot number from which to begin retrieving blocks.
763    /// - `wrapper`: An optional parameter that can either specify an `end_slot` or contain a configuration
764    ///   (`RpcContextConfig`) to define additional context settings such as commitment and minimum context slot.
765    /// - `config`: An optional configuration for additional context parameters like commitment and minimum context slot.
766    ///
767    /// ## Returns
768    /// A list of slot numbers, representing the sequence of blocks starting from `start_slot`.
769    /// The returned slots are in ascending order. If no blocks are found, the response will be an empty list.
770    ///
771    /// ## Example Request
772    /// ```json
773    /// {
774    ///   "jsonrpc": "2.0",
775    ///   "id": 1,
776    ///   "method": "getBlocks",
777    ///   "params": [123456, {"endSlotOnly": 123500}, {"commitment": "finalized"}]
778    /// }
779    /// ```
780    ///
781    /// ## Example Response
782    /// ```json
783    /// {
784    ///   "jsonrpc": "2.0",
785    ///   "id": 1,
786    ///   "result": [123456, 123457, 123458, 123459]
787    /// }
788    /// ```
789    ///
790    /// ## Errors
791    /// - Returns an error if the provided `start_slot` is invalid or if there is an issue processing the request.
792    ///
793    /// # Notes
794    /// - The response will return all blocks starting from the `start_slot` and up to the `end_slot` if specified.
795    ///   If no `end_slot` is provided, the server will return all available blocks starting from `start_slot`.
796    /// - The `commitment` setting determines the level of finality for the blocks returned (e.g., "finalized", "confirmed", etc.).
797    ///
798    /// # See Also
799    /// - `getBlock`, `getSlot`, `getBlockTime`
800    #[rpc(meta, name = "getBlocks")]
801    fn get_blocks(
802        &self,
803        meta: Self::Metadata,
804        start_slot: Slot,
805        wrapper: Option<RpcBlocksConfigWrapper>,
806        config: Option<RpcContextConfig>,
807    ) -> BoxFuture<Result<Vec<Slot>>>;
808
809    /// Retrieves a limited list of block slots starting from a given `start_slot`.
810    ///
811    /// This RPC method fetches a sequence of block slots starting from the specified `start_slot`,
812    /// but limits the number of blocks returned to the specified `limit`. This is useful when you want
813    /// to quickly retrieve a small number of blocks from a specific point in the blockchain.
814    ///
815    /// ## Parameters
816    /// - `start_slot`: The slot number from which to begin retrieving blocks.
817    /// - `limit`: The maximum number of block slots to return. This limits the size of the response.
818    /// - `config`: An optional configuration for additional context parameters like commitment and minimum context slot.
819    ///
820    /// ## Returns
821    /// A list of slot numbers, representing the sequence of blocks starting from `start_slot`, up to the specified `limit`.
822    /// If fewer blocks are available, the response will contain only the available blocks.
823    ///
824    /// ## Example Request
825    /// ```json
826    /// {
827    ///   "jsonrpc": "2.0",
828    ///   "id": 1,
829    ///   "method": "getBlocksWithLimit",
830    ///   "params": [123456, 5, {"commitment": "finalized"}]
831    /// }
832    /// ```
833    ///
834    /// ## Example Response
835    /// ```json
836    /// {
837    ///   "jsonrpc": "2.0",
838    ///   "id": 1,
839    ///   "result": [123456, 123457, 123458, 123459, 123460]
840    /// }
841    /// ```
842    ///
843    /// ## Errors
844    /// - Returns an error if the provided `start_slot` is invalid, if the `limit` is zero, or if there is an issue processing the request.
845    ///
846    /// # Notes
847    /// - The response will return up to the specified `limit` number of blocks starting from `start_slot`.
848    /// - If the blockchain contains fewer than the requested number of blocks, the response will contain only the available blocks.
849    /// - The `commitment` setting determines the level of finality for the blocks returned (e.g., "finalized", "confirmed", etc.).
850    ///
851    /// # See Also
852    /// - `getBlocks`, `getBlock`, `getSlot`
853    #[rpc(meta, name = "getBlocksWithLimit")]
854    fn get_blocks_with_limit(
855        &self,
856        meta: Self::Metadata,
857        start_slot: Slot,
858        limit: usize,
859        config: Option<RpcContextConfig>,
860    ) -> BoxFuture<Result<Vec<Slot>>>;
861
862    /// Retrieves the details of a specific transaction by its signature.
863    ///
864    /// This RPC method allows clients to fetch a previously confirmed transaction
865    /// along with its metadata. It supports multiple encoding formats and lets you
866    /// optionally limit which transaction versions are returned.
867    ///
868    /// ## Parameters
869    /// - `signature`: The base-58 encoded signature of the transaction to fetch.
870    /// - `config` (optional): Configuration for the encoding, commitment level, and supported transaction version.
871    ///
872    /// ## Returns
873    /// If the transaction is found, returns an object containing:
874    /// - `slot`: The slot in which the transaction was confirmed.
875    /// - `blockTime`: The estimated production time of the block containing the transaction (in Unix timestamp).
876    /// - `transaction`: The transaction itself, including all metadata such as status, logs, and account changes.
877    ///
878    /// Returns `null` if the transaction is not found.
879    ///
880    /// ## Example Request
881    /// ```json
882    /// {
883    ///   "jsonrpc": "2.0",
884    ///   "id": 1,
885    ///   "method": "getTransaction",
886    ///   "params": [
887    ///     "5YwKXNYCnbAednZcJ2Qu9swiyWLUWaKkTZb2tFCSM1uCEmFHe5zoHQaKzwX4e6RGXkPRqRpxwWBLTeYEGqZtA6nW",
888    ///     {
889    ///       "encoding": "jsonParsed",
890    ///       "commitment": "finalized",
891    ///       "maxSupportedTransactionVersion": 0
892    ///     }
893    ///   ]
894    /// }
895    /// ```
896    ///
897    /// ## Example Response
898    /// ```json
899    /// {
900    ///   "jsonrpc": "2.0",
901    ///   "id": 1,
902    ///   "result": {
903    ///     "slot": 175512345,
904    ///     "blockTime": 1702345678,
905    ///     "transaction": {
906    ///       "version": 0,
907    ///       "transaction": {
908    ///         "message": { ... },
909    ///         "signatures": [ ... ]
910    ///       },
911    ///       "meta": {
912    ///         "err": null,
913    ///         "status": { "Ok": null },
914    ///         ...
915    ///       }
916    ///     }
917    ///   }
918    /// }
919    /// ```
920    ///
921    /// ## Errors
922    /// - Returns an error if the signature is invalid or if there is a backend failure.
923    /// - Returns `null` if the transaction is not found (e.g., dropped or not yet confirmed).
924    ///
925    /// # Notes
926    /// - The `encoding` field supports formats like `base64`, `base58`, `json`, and `jsonParsed`.
927    /// - If `maxSupportedTransactionVersion` is specified, transactions using a newer version will not be returned.
928    /// - Depending on the commitment level, this method may or may not return the latest transactions.
929    ///
930    /// # See Also
931    /// - `getSignatureStatuses`, `getConfirmedTransaction`, `getBlock`
932    #[rpc(meta, name = "getTransaction")]
933    fn get_transaction(
934        &self,
935        meta: Self::Metadata,
936        signature_str: String,
937        config: Option<RpcEncodingConfigWrapper<RpcTransactionConfig>>,
938    ) -> BoxFuture<Result<Option<EncodedConfirmedTransactionWithStatusMeta>>>;
939
940    /// Returns confirmed transaction signatures for transactions involving an address.
941    ///
942    /// This RPC method allows clients to look up historical transaction signatures
943    /// that involved a given account address. The list is returned in reverse
944    /// chronological order (most recent first) and can be paginated.
945    ///
946    /// ## Parameters
947    /// - `address`: The base-58 encoded address to query.
948    /// - `config` (optional): Configuration object with the following fields:
949    ///   - `before`: Start search before this signature.
950    ///   - `until`: Search until this signature (exclusive).
951    ///   - `limit`: Maximum number of results to return (default: 1,000; max: 1,000).
952    ///   - `commitment`: The level of commitment desired (e.g., finalized).
953    ///   - `minContextSlot`: The minimum slot that the query should be evaluated at.
954    ///
955    /// ## Returns
956    /// A list of confirmed transaction summaries, each including:
957    /// - `signature`: Transaction signature (base-58).
958    /// - `slot`: The slot in which the transaction was confirmed.
959    /// - `err`: If the transaction failed, an error object; otherwise `null`.
960    /// - `memo`: Optional memo attached to the transaction.
961    /// - `blockTime`: Approximate production time of the block containing the transaction (Unix timestamp).
962    /// - `confirmationStatus`: One of `processed`, `confirmed`, or `finalized`.
963    ///
964    /// ## Example Request
965    /// ```json
966    /// {
967    ///   "jsonrpc": "2.0",
968    ///   "id": 1,
969    ///   "method": "getSignaturesForAddress",
970    ///   "params": [
971    ///     "5ZJShu4hxq7gxcu1RUVUMhNeyPmnASvokhZ8QgxtzVzm",
972    ///     {
973    ///       "limit": 2,
974    ///       "commitment": "confirmed"
975    ///     }
976    ///   ]
977    /// }
978    /// ```
979    ///
980    /// ## Example Response
981    /// ```json
982    /// {
983    ///   "jsonrpc": "2.0",
984    ///   "id": 1,
985    ///   "result": [
986    ///     {
987    ///       "signature": "5VnFgjCwQoM2aBymRkdaV74ZKbbfUpR2zhfn9qN7shHPfLCXcfSBTfxhcuHsjVYz2UkAxw1cw6azS4qPGaKMyrjy",
988    ///       "slot": 176012345,
989    ///       "err": null,
990    ///       "memo": null,
991    ///       "blockTime": 1703456789,
992    ///       "confirmationStatus": "finalized"
993    ///     },
994    ///     {
995    ///       "signature": "3h1QfUHyjFdqLy5PSTLDmYqL2NhVLz9P9LtS43jJP3aNUv9yP1JWhnzMVg5crEXnEvhP6bLgRtbgi6Z1EGgdA1yF",
996    ///       "slot": 176012344,
997    ///       "err": null,
998    ///       "memo": "example-memo",
999    ///       "blockTime": 1703456770,
1000    ///       "confirmationStatus": "confirmed"
1001    ///     }
1002    ///   ]
1003    /// }
1004    /// ```
1005    ///
1006    /// ## Errors
1007    /// - Returns an error if the address is invalid or if the request exceeds internal limits.
1008    /// - May return fewer results than requested if pagination is constrained by chain history.
1009    ///
1010    /// # Notes
1011    /// - For full transaction details, use the returned signatures with `getTransaction`.
1012    /// - The default `limit` is 1,000 and is capped at 1,000.
1013    ///
1014    /// # See Also
1015    /// - `getTransaction`, `getConfirmedSignaturesForAddress2` (legacy)
1016    #[rpc(meta, name = "getSignaturesForAddress")]
1017    fn get_signatures_for_address(
1018        &self,
1019        meta: Self::Metadata,
1020        address: String,
1021        config: Option<RpcSignaturesForAddressConfig>,
1022    ) -> BoxFuture<Result<Vec<RpcConfirmedTransactionStatusWithSignature>>>;
1023
1024    /// Returns the slot of the lowest confirmed block that has not been purged from the ledger.
1025    ///
1026    /// This RPC method is useful for determining the oldest block that is still available
1027    /// from the node. Blocks before this slot have likely been purged and are no longer accessible
1028    /// for queries such as `getBlock`, `getTransaction`, etc.
1029    ///
1030    /// ## Parameters
1031    /// None.
1032    ///
1033    /// ## Returns
1034    /// A single integer representing the first available slot (block) that has not been purged.
1035    ///
1036    /// ## Example Request
1037    /// ```json
1038    /// {
1039    ///   "jsonrpc": "2.0",
1040    ///   "id": 1,
1041    ///   "method": "getFirstAvailableBlock",
1042    ///   "params": []
1043    /// }
1044    /// ```
1045    ///
1046    /// ## Example Response
1047    /// ```json
1048    /// {
1049    ///   "jsonrpc": "2.0",
1050    ///   "id": 1,
1051    ///   "result": 146392340
1052    /// }
1053    /// ```
1054    ///
1055    /// ## Errors
1056    /// - Returns an error if the node is not fully initialized or if the ledger is inaccessible.
1057    ///
1058    /// # Notes
1059    /// - This value is typically useful for pagination or historical data indexing.
1060    /// - This slot may increase over time as the node prunes old ledger data.
1061    ///
1062    /// # See Also
1063    /// - `getBlock`, `getBlockTime`, `minimumLedgerSlot`
1064    #[rpc(meta, name = "getFirstAvailableBlock")]
1065    fn get_first_available_block(&self, meta: Self::Metadata) -> Result<Slot>;
1066
1067    /// Returns the latest blockhash and associated metadata needed to sign and send a transaction.
1068    ///
1069    /// This method is essential for transaction construction. It provides the most recent
1070    /// blockhash that should be included in a transaction to be considered valid. It may
1071    /// also include metadata such as the last valid block height and the minimum context slot.
1072    ///
1073    /// ## Parameters
1074    /// - `config` *(optional)*: Optional context settings, such as commitment level and minimum slot.
1075    ///
1076    /// ## Returns
1077    /// A JSON object containing the recent blockhash, last valid block height,
1078    /// and the context slot of the response.
1079    ///
1080    /// ## Example Request
1081    /// ```json
1082    /// {
1083    ///   "jsonrpc": "2.0",
1084    ///   "id": 1,
1085    ///   "method": "getLatestBlockhash",
1086    ///   "params": [
1087    ///     {
1088    ///       "commitment": "confirmed"
1089    ///     }
1090    ///   ]
1091    /// }
1092    /// ```
1093    ///
1094    /// ## Example Response
1095    /// ```json
1096    /// {
1097    ///   "jsonrpc": "2.0",
1098    ///   "result": {
1099    ///     "context": {
1100    ///       "slot": 18123942
1101    ///     },
1102    ///     "value": {
1103    ///       "blockhash": "9Xc7XmXmpRmFAqMQUvn2utY5BJeXFY2ZHMxu2fbjZkfy",
1104    ///       "lastValidBlockHeight": 18123971
1105    ///     }
1106    ///   },
1107    ///   "id": 1
1108    /// }
1109    /// ```
1110    ///
1111    /// ## Errors
1112    /// - Returns an error if the node is behind or if the blockhash cache is temporarily unavailable.
1113    ///
1114    /// # Notes
1115    /// - Transactions must include a recent blockhash to be accepted.
1116    /// - The blockhash will expire after a certain number of slots (around 150 slots typically).
1117    ///
1118    /// # See Also
1119    /// - `sendTransaction`, `simulateTransaction`, `requestAirdrop`
1120    #[rpc(meta, name = "getLatestBlockhash")]
1121    fn get_latest_blockhash(
1122        &self,
1123        meta: Self::Metadata,
1124        config: Option<RpcContextConfig>,
1125    ) -> Result<RpcResponse<RpcBlockhash>>;
1126
1127    /// Checks if a given blockhash is still valid for transaction inclusion.
1128    ///
1129    /// This method can be used to determine whether a specific blockhash can still
1130    /// be used in a transaction. Blockhashes expire after approximately 150 slots,
1131    /// and transactions that reference an expired blockhash will be rejected.
1132    ///
1133    /// ## Parameters
1134    /// - `blockhash`: A base-58 encoded string representing the blockhash to validate.
1135    /// - `config` *(optional)*: Optional context configuration such as commitment level or minimum context slot.
1136    ///
1137    /// ## Returns
1138    /// A boolean value wrapped in a `RpcResponse`:
1139    /// - `true` if the blockhash is valid and usable.
1140    /// - `false` if the blockhash has expired or is unknown to the node.
1141    ///
1142    /// ## Example Request
1143    /// ```json
1144    /// {
1145    ///   "jsonrpc": "2.0",
1146    ///   "id": 1,
1147    ///   "method": "isBlockhashValid",
1148    ///   "params": [
1149    ///     "9Xc7XmXmpRmFAqMQUvn2utY5BJeXFY2ZHMxu2fbjZkfy",
1150    ///     {
1151    ///       "commitment": "confirmed"
1152    ///     }
1153    ///   ]
1154    /// }
1155    /// ```
1156    ///
1157    /// ## Example Response
1158    /// ```json
1159    /// {
1160    ///   "jsonrpc": "2.0",
1161    ///   "result": {
1162    ///     "context": {
1163    ///       "slot": 18123945
1164    ///     },
1165    ///     "value": true
1166    ///   },
1167    ///   "id": 1
1168    /// }
1169    /// ```
1170    ///
1171    /// ## Errors
1172    /// - Returns an error if the node is unable to validate the blockhash (e.g., blockhash not found).
1173    ///
1174    /// # Notes
1175    /// - This endpoint is useful for transaction retries or for validating manually constructed transactions.
1176    ///
1177    /// # See Also
1178    /// - `getLatestBlockhash`, `sendTransaction`
1179    #[rpc(meta, name = "isBlockhashValid")]
1180    fn is_blockhash_valid(
1181        &self,
1182        meta: Self::Metadata,
1183        blockhash: String,
1184        config: Option<RpcContextConfig>,
1185    ) -> Result<RpcResponse<bool>>;
1186
1187    /// Returns the estimated fee required to submit a given transaction message.
1188    ///
1189    /// This method takes a base64-encoded `Message` (the serialized form of a transaction's message),
1190    /// and returns the fee in lamports that would be charged for processing that message,
1191    /// assuming it was submitted as a transaction.
1192    ///
1193    /// ## Parameters
1194    /// - `data`: A base64-encoded string of the binary-encoded `Message`.
1195    /// - `config` *(optional)*: Optional context configuration such as commitment level or minimum context slot.
1196    ///
1197    /// ## Returns
1198    /// A `RpcResponse` wrapping an `Option<u64>`:
1199    /// - `Some(fee)` if the fee could be calculated for the given message.
1200    /// - `None` if the fee could not be determined (e.g., due to invalid inputs or expired blockhash).
1201    ///
1202    /// ## Example Request
1203    /// ```json
1204    /// {
1205    ///   "jsonrpc": "2.0",
1206    ///   "id": 1,
1207    ///   "method": "getFeeForMessage",
1208    ///   "params": [
1209    ///     "Af4F...base64-encoded-message...==",
1210    ///     {
1211    ///       "commitment": "processed"
1212    ///     }
1213    ///   ]
1214    /// }
1215    /// ```
1216    ///
1217    /// ## Example Response
1218    /// ```json
1219    /// {
1220    ///   "jsonrpc": "2.0",
1221    ///   "result": {
1222    ///     "context": {
1223    ///       "slot": 19384722
1224    ///     },
1225    ///     "value": 5000
1226    ///   },
1227    ///   "id": 1
1228    /// }
1229    /// ```
1230    ///
1231    /// ## Errors
1232    /// - Returns an error if the input is not a valid message.
1233    /// - Returns `null` (i.e., `None`) if the fee cannot be determined.
1234    ///
1235    /// # Notes
1236    /// - This method is useful for estimating fees before submitting transactions.
1237    /// - It helps users decide whether to rebroadcast or update a transaction.
1238    ///
1239    /// # See Also
1240    /// - `sendTransaction`, `simulateTransaction`
1241    #[rpc(meta, name = "getFeeForMessage")]
1242    fn get_fee_for_message(
1243        &self,
1244        meta: Self::Metadata,
1245        data: String,
1246        config: Option<RpcContextConfig>,
1247    ) -> Result<RpcResponse<Option<u64>>>;
1248
1249    /// Returns the current minimum delegation amount required for a stake account.
1250    ///
1251    /// This method provides the minimum number of lamports that must be delegated
1252    /// in order to be considered active in the staking system. It helps users determine
1253    /// the minimum threshold to avoid their stake being considered inactive or rent-exempt only.
1254    ///
1255    /// ## Parameters
1256    /// - `config` *(optional)*: Optional context configuration including commitment level or minimum context slot.
1257    ///
1258    /// ## Returns
1259    /// A `RpcResponse` containing a `u64` value indicating the minimum required lamports for stake delegation.
1260    ///
1261    /// ## Example Request
1262    /// ```json
1263    /// {
1264    ///   "jsonrpc": "2.0",
1265    ///   "id": 1,
1266    ///   "method": "getStakeMinimumDelegation",
1267    ///   "params": [
1268    ///     {
1269    ///       "commitment": "finalized"
1270    ///     }
1271    ///   ]
1272    /// }
1273    /// ```
1274    ///
1275    /// ## Example Response
1276    /// ```json
1277    /// {
1278    ///   "jsonrpc": "2.0",
1279    ///   "result": {
1280    ///     "context": {
1281    ///       "slot": 21283712
1282    ///     },
1283    ///     "value": 10000000
1284    ///   },
1285    ///   "id": 1
1286    /// }
1287    /// ```
1288    ///
1289    /// # Notes
1290    /// - This value may change over time due to protocol updates or inflation.
1291    /// - Stake accounts with a delegated amount below this value may not earn rewards.
1292    ///
1293    /// # See Also
1294    /// - `getStakeActivation`, `getInflationReward`, `getEpochInfo`
1295    #[rpc(meta, name = "getStakeMinimumDelegation")]
1296    fn get_stake_minimum_delegation(
1297        &self,
1298        meta: Self::Metadata,
1299        config: Option<RpcContextConfig>,
1300    ) -> Result<RpcResponse<u64>>;
1301
1302    /// Returns recent prioritization fees for one or more accounts.
1303    ///
1304    /// This method is useful for estimating the prioritization fee required
1305    /// for a transaction to be included quickly in a block. It returns the
1306    /// most recent prioritization fee paid by each account provided.
1307    ///
1308    /// ## Parameters
1309    /// - `pubkey_strs` *(optional)*: A list of base-58 encoded account public keys (as strings).
1310    ///   If omitted, the node may return a default or empty set.
1311    ///
1312    /// ## Returns
1313    /// A list of `RpcPrioritizationFee` entries, each containing the slot and the fee paid
1314    /// to prioritize transactions.
1315    ///
1316    /// ## Example Request
1317    /// ```json
1318    /// {
1319    ///   "jsonrpc": "2.0",
1320    ///   "id": 1,
1321    ///   "method": "getRecentPrioritizationFees",
1322    ///   "params": [
1323    ///     [
1324    ///       "9xz7uXmf3CjFWW5E8v9XJXuGzTZ2V7UtEG1epF2Tt6TL"
1325    ///     ]
1326    ///   ]
1327    /// }
1328    /// ```
1329    ///
1330    /// ## Example Response
1331    /// ```json
1332    /// {
1333    ///   "jsonrpc": "2.0",
1334    ///   "result": [
1335    ///     {
1336    ///       "slot": 21458900,
1337    ///       "prioritizationFee": 5000
1338    ///     }
1339    ///   ],
1340    ///   "id": 1
1341    /// }
1342    /// ```
1343    ///
1344    /// # Notes
1345    /// - The prioritization fee helps validators prioritize transactions for inclusion in blocks.
1346    /// - These fees are dynamic and can vary significantly depending on network congestion.
1347    ///
1348    /// # See Also
1349    /// - `getFeeForMessage`, `simulateTransaction`
1350    #[rpc(meta, name = "getRecentPrioritizationFees")]
1351    fn get_recent_prioritization_fees(
1352        &self,
1353        meta: Self::Metadata,
1354        pubkey_strs: Option<Vec<String>>,
1355    ) -> BoxFuture<Result<Vec<RpcPrioritizationFee>>>;
1356}
1357
1358#[derive(Clone)]
1359pub struct SurfpoolFullRpc;
1360impl Full for SurfpoolFullRpc {
1361    type Metadata = Option<RunloopContext>;
1362
1363    fn get_inflation_reward(
1364        &self,
1365        meta: Self::Metadata,
1366        address_strs: Vec<String>,
1367        config: Option<RpcEpochConfig>,
1368    ) -> BoxFuture<Result<Vec<Option<RpcInflationReward>>>> {
1369        Box::pin(async move {
1370            let svm_locker = meta.get_svm_locker()?;
1371
1372            let current_epoch = svm_locker.get_epoch_info().epoch;
1373            if let Some(epoch) = config.as_ref().and_then(|config| config.epoch) {
1374                if epoch > current_epoch {
1375                    return Err(Error::invalid_params(
1376                        "Invalid epoch. Epoch is larger that current epoch",
1377                    ));
1378                }
1379            };
1380
1381            let current_slot = svm_locker.get_epoch_info().absolute_slot;
1382            if let Some(slot) = config.as_ref().and_then(|config| config.min_context_slot) {
1383                if slot > current_slot {
1384                    return Err(Error::invalid_params(
1385                        "Minimum context slot has not been reached",
1386                    ));
1387                }
1388            };
1389
1390            let pubkeys = address_strs
1391                .iter()
1392                .map(|addr| verify_pubkey(addr))
1393                .collect::<std::result::Result<Vec<Pubkey>, SurfpoolError>>()?;
1394
1395            meta.with_svm_reader(|svm_reader| {
1396                pubkeys
1397                    .iter()
1398                    .map(|_| {
1399                        Some(RpcInflationReward {
1400                            amount: 0,
1401                            commission: None,
1402                            effective_slot: svm_reader.get_latest_absolute_slot(),
1403                            epoch: svm_reader.latest_epoch_info().epoch,
1404                            post_balance: 0,
1405                        })
1406                    })
1407                    .collect()
1408            })
1409            .map_err(Into::into)
1410        })
1411    }
1412
1413    fn get_cluster_nodes(&self, meta: Self::Metadata) -> Result<Vec<RpcContactInfo>> {
1414        let (gossip, tpu, tpu_quic, rpc, pubsub) = if let Some(ctx) = meta {
1415            let config = ctx.rpc_config;
1416            let to_socket = |port: u16| -> Option<std::net::SocketAddr> {
1417                format!("{}:{}", config.bind_host, port).parse().ok()
1418            };
1419            (
1420                to_socket(config.gossip_port),
1421                to_socket(config.tpu_port),
1422                to_socket(config.tpu_quic_port),
1423                to_socket(config.bind_port),
1424                to_socket(config.ws_port),
1425            )
1426        } else {
1427            (None, None, None, None, None)
1428        };
1429
1430        Ok(vec![RpcContactInfo {
1431            pubkey: SURFPOOL_IDENTITY_PUBKEY.to_string(),
1432            gossip,
1433            tvu: None,
1434            tpu,
1435            tpu_quic,
1436            tpu_forwards: None,
1437            tpu_forwards_quic: None,
1438            tpu_vote: None,
1439            serve_repair: None,
1440            rpc,
1441            pubsub,
1442            version: None,
1443            feature_set: None,
1444            shred_version: None,
1445        }])
1446    }
1447
1448    fn get_recent_performance_samples(
1449        &self,
1450        meta: Self::Metadata,
1451        limit: Option<usize>,
1452    ) -> Result<Vec<RpcPerfSample>> {
1453        let limit = limit.unwrap_or(720);
1454        if limit > 720 {
1455            return Err(Error::invalid_params("Invalid limit; max 720"));
1456        }
1457
1458        meta.with_svm_reader(|svm_reader| {
1459            svm_reader
1460                .perf_samples
1461                .iter()
1462                .take(limit)
1463                .cloned()
1464                .collect::<Vec<_>>()
1465        })
1466        .map_err(Into::into)
1467    }
1468
1469    fn get_signature_statuses(
1470        &self,
1471        meta: Self::Metadata,
1472        signature_strs: Vec<String>,
1473        _config: Option<RpcSignatureStatusConfig>,
1474    ) -> BoxFuture<Result<RpcResponse<Vec<Option<TransactionStatus>>>>> {
1475        let signatures = match signature_strs
1476            .iter()
1477            .map(|s| {
1478                Signature::from_str(s)
1479                    .map_err(|e| SurfpoolError::invalid_signature(s, e.to_string()))
1480            })
1481            .collect::<std::result::Result<Vec<Signature>, SurfpoolError>>()
1482        {
1483            Ok(sigs) => sigs,
1484            Err(e) => return e.into(),
1485        };
1486
1487        let SurfnetRpcContext {
1488            svm_locker,
1489            remote_ctx,
1490        } = match meta.get_rpc_context(()) {
1491            Ok(res) => res,
1492            Err(e) => return e.into(),
1493        };
1494        let remote_client = remote_ctx.map(|(r, _)| r);
1495
1496        Box::pin(async move {
1497            // Capture the context slot once at the beginning to ensure consistency
1498            // across all signature lookups, even if the slot advances during the loop
1499            let context_slot = svm_locker.get_latest_absolute_slot();
1500
1501            let mut responses = Vec::with_capacity(signatures.len());
1502            for signature in signatures.into_iter() {
1503                let res = svm_locker
1504                    .get_transaction(&remote_client, &signature, get_default_transaction_config())
1505                    .await?;
1506
1507                let mut status = res.map_some_transaction_status();
1508                if let Some(confirmation_status) =
1509                    status.as_ref().and_then(|s| s.confirmation_status.as_ref())
1510                {
1511                    if confirmation_status.eq(&TransactionConfirmationStatus::Processed) {
1512                        // If the transaction is only processed, we cannot be sure it won't be dropped
1513                        // before being confirmed. So we return None in this case to match the behavior
1514                        // of a real Solana node.
1515                        status = None;
1516                    }
1517                }
1518                responses.push(status);
1519            }
1520            Ok(RpcResponse {
1521                context: RpcResponseContext::new(context_slot),
1522                value: responses,
1523            })
1524        })
1525    }
1526
1527    fn get_max_retransmit_slot(&self, meta: Self::Metadata) -> Result<Slot> {
1528        meta.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
1529            .map_err(Into::into)
1530    }
1531
1532    fn get_max_shred_insert_slot(&self, meta: Self::Metadata) -> Result<Slot> {
1533        meta.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
1534            .map_err(Into::into)
1535    }
1536
1537    fn request_airdrop(
1538        &self,
1539        meta: Self::Metadata,
1540        pubkey_str: String,
1541        lamports: u64,
1542        _config: Option<RpcRequestAirdropConfig>,
1543    ) -> Result<String> {
1544        let pubkey = verify_pubkey(&pubkey_str)?;
1545        let Some(ctx) = meta else {
1546            return Err(SurfpoolError::missing_context().into());
1547        };
1548        let svm_locker = ctx.svm_locker;
1549        let res = svm_locker
1550            .airdrop(&pubkey, lamports)?
1551            .map_err(|err| Error::invalid_params(format!("failed to send transaction: {err:?}")))?;
1552        let _ = ctx
1553            .simnet_commands_tx
1554            .try_send(SimnetCommand::AirdropProcessed);
1555
1556        Ok(res.signature.to_string())
1557    }
1558
1559    fn send_transaction(
1560        &self,
1561        meta: Self::Metadata,
1562        data: String,
1563        config: Option<SurfpoolRpcSendTransactionConfig>,
1564    ) -> Result<String> {
1565        let config = config.unwrap_or_default();
1566        let tx_encoding = config
1567            .base
1568            .encoding
1569            .unwrap_or(UiTransactionEncoding::Base58);
1570        let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
1571            Error::invalid_params(format!(
1572                "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
1573            ))
1574        })?;
1575        let (_, unsanitized_tx) =
1576            decode_and_deserialize::<VersionedTransaction>(data, binary_encoding)?;
1577        let signatures = unsanitized_tx.signatures.clone();
1578        let signature = signatures[0];
1579        // Clone the message before moving the transaction, as we'll need it for error reporting
1580        let tx_message = unsanitized_tx.message.clone();
1581        let Some(ctx) = meta else {
1582            return Err(RpcCustomError::NodeUnhealthy {
1583                num_slots_behind: None,
1584            }
1585            .into());
1586        };
1587
1588        let (status_update_tx, status_update_rx) = crossbeam_channel::bounded(1);
1589        ctx.simnet_commands_tx
1590            .send(SimnetCommand::ProcessTransaction(
1591                ctx.id,
1592                unsanitized_tx,
1593                status_update_tx,
1594                config.base.skip_preflight,
1595                config.skip_sig_verify,
1596            ))
1597            .map_err(|_| RpcCustomError::NodeUnhealthy {
1598                num_slots_behind: None,
1599            })?;
1600
1601        match status_update_rx.recv() {
1602            Ok(TransactionStatusEvent::SimulationFailure((error, metadata))) => {
1603                return Err(Error {
1604                    data: Some(
1605                        serde_json::to_value(get_simulate_transaction_result(
1606                            surfpool_tx_metadata_to_litesvm_tx_metadata(&metadata),
1607                            None,
1608                            Some(error.clone()),
1609                            None,
1610                            false,
1611                            &tx_message,
1612                            None, // No loaded addresses available in error reporting context
1613                            None,
1614                        ))
1615                        .map_err(|e| {
1616                            Error::invalid_params(format!(
1617                                "Failed to serialize simulation result: {e}"
1618                            ))
1619                        })?,
1620                    ),
1621                    message: format!(
1622                        "Transaction simulation failed: {}{}",
1623                        error,
1624                        if metadata.logs.is_empty() {
1625                            String::new()
1626                        } else {
1627                            format!(
1628                                ": {} log messages:\n{}",
1629                                metadata.logs.len(),
1630                                metadata.logs.iter().map(|l| l.to_string()).join("\n")
1631                            )
1632                        }
1633                    ),
1634                    code: jsonrpc_core::ErrorCode::ServerError(-32002),
1635                });
1636            }
1637            Ok(TransactionStatusEvent::ExecutionFailure(_)) => {}
1638            Ok(TransactionStatusEvent::VerificationFailure(signature)) => {
1639                return Err(Error {
1640                    data: None,
1641                    message: format!("Transaction verification failed for transaction {signature}"),
1642                    code: jsonrpc_core::ErrorCode::ServerError(-32002),
1643                });
1644            }
1645            Err(e) => {
1646                return Err(Error {
1647                    data: None,
1648                    message: format!("Failed to process transaction: {e}"),
1649                    code: jsonrpc_core::ErrorCode::ServerError(-32002),
1650                });
1651            }
1652            Ok(TransactionStatusEvent::Success(_)) => {}
1653        }
1654        Ok(signature.to_string())
1655    }
1656
1657    fn simulate_transaction(
1658        &self,
1659        meta: Self::Metadata,
1660        data: String,
1661        config: Option<RpcSimulateTransactionConfig>,
1662    ) -> BoxFuture<Result<RpcResponse<RpcSimulateTransactionResult>>> {
1663        let config = config.unwrap_or_default();
1664
1665        if config.sig_verify && config.replace_recent_blockhash {
1666            return SurfpoolError::sig_verify_replace_recent_blockhash_collision().into();
1667        }
1668
1669        let tx_encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
1670        let binary_encoding = match tx_encoding.into_binary_encoding() {
1671            Some(binary_encoding) => binary_encoding,
1672            None => {
1673                return Box::pin(async move {
1674                    Err(Error::invalid_params(format!(
1675                        "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
1676                    )))
1677                });
1678            }
1679        };
1680        let (_, mut unsanitized_tx) =
1681            match decode_and_deserialize::<VersionedTransaction>(data, binary_encoding) {
1682                Ok(res) => res,
1683                Err(e) => return Box::pin(async move { Err(e) }),
1684            };
1685
1686        let SurfnetRpcContext {
1687            svm_locker,
1688            remote_ctx,
1689        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1690            Ok(res) => res,
1691            Err(e) => return e.into(),
1692        };
1693
1694        Box::pin(async move {
1695            let loaded_addresses = svm_locker
1696                .get_loaded_addresses(&remote_ctx, &unsanitized_tx.message)
1697                .await?;
1698            let transaction_pubkeys = svm_locker.get_pubkeys_from_message(
1699                &unsanitized_tx.message,
1700                loaded_addresses.as_ref().map(|l| l.all_loaded_addresses()),
1701            );
1702
1703            let SvmAccessContext {
1704                slot,
1705                inner: account_updates,
1706                latest_blockhash,
1707                latest_epoch_info,
1708            } = svm_locker
1709                .get_multiple_accounts(&remote_ctx, &transaction_pubkeys, None)
1710                .await?;
1711
1712            let mut seen_accounts = std::collections::HashSet::new();
1713            let mut loaded_accounts_data_size: u64 = 0;
1714
1715            let mut track_accounts_data_size =
1716                |account_update: &GetAccountResult| match account_update {
1717                    GetAccountResult::FoundAccount(pubkey, account, _) => {
1718                        if seen_accounts.insert(*pubkey) {
1719                            loaded_accounts_data_size += account.data.len() as u64;
1720                        }
1721                    }
1722                    // According to SIMD 0186, program data is tracked as well as program accounts
1723                    GetAccountResult::FoundProgramAccount(
1724                        (pubkey, account),
1725                        (pd_pubkey, pd_account),
1726                    ) => {
1727                        if seen_accounts.insert(*pubkey) {
1728                            loaded_accounts_data_size += account.data.len() as u64;
1729                        }
1730                        if let Some(pd) = pd_account {
1731                            if seen_accounts.insert(*pd_pubkey) {
1732                                loaded_accounts_data_size += pd.data.len() as u64;
1733                            }
1734                        }
1735                    }
1736                    GetAccountResult::FoundTokenAccount(
1737                        (pubkey, account),
1738                        (td_pubkey, td_account),
1739                    ) => {
1740                        if seen_accounts.insert(*pubkey) {
1741                            loaded_accounts_data_size += account.data.len() as u64;
1742                        }
1743                        if let Some(td) = td_account {
1744                            let td_key_in_tx_pubkeys =
1745                                transaction_pubkeys.iter().find(|k| **k == *td_pubkey);
1746                            // Only count token data accounts that are explicitly loaded by the transaction
1747                            if td_key_in_tx_pubkeys.is_some() && seen_accounts.insert(*td_pubkey) {
1748                                loaded_accounts_data_size += td.data.len() as u64;
1749                            }
1750                        }
1751                    }
1752                    GetAccountResult::None(_) => {}
1753                };
1754
1755            for res in account_updates.iter() {
1756                track_accounts_data_size(res);
1757            }
1758
1759            svm_locker.write_multiple_account_updates(&account_updates);
1760
1761            // Convert TransactionLoadedAddresses to LoadedAddresses before it gets consumed
1762            let loaded_addresses_data = loaded_addresses.as_ref().map(|la| la.loaded_addresses());
1763
1764            if let Some(alt_pubkeys) = loaded_addresses.map(|l| l.alt_addresses()) {
1765                let alt_updates = svm_locker
1766                    .get_multiple_accounts(&remote_ctx, &alt_pubkeys, None)
1767                    .await?
1768                    .inner;
1769                for res in alt_updates.iter() {
1770                    track_accounts_data_size(res);
1771                }
1772                svm_locker.write_multiple_account_updates(&alt_updates);
1773            }
1774
1775            let replacement_blockhash = if config.replace_recent_blockhash {
1776                match &mut unsanitized_tx.message {
1777                    VersionedMessage::Legacy(message) => {
1778                        message.recent_blockhash = latest_blockhash
1779                    }
1780                    VersionedMessage::V0(message) => message.recent_blockhash = latest_blockhash,
1781                }
1782                Some(RpcBlockhash {
1783                    blockhash: latest_blockhash.to_string(),
1784                    last_valid_block_height: latest_epoch_info.block_height,
1785                })
1786            } else {
1787                None
1788            };
1789
1790            // Clone the message before moving the transaction for later use in result formatting
1791            let tx_message = unsanitized_tx.message.clone();
1792
1793            let value = match svm_locker.simulate_transaction(unsanitized_tx, config.sig_verify) {
1794                Ok(tx_info) => {
1795                    let mut accounts = None;
1796                    if let Some(observed_accounts) = config.accounts {
1797                        let mut ui_accounts = vec![];
1798                        for observed_pubkey in observed_accounts.addresses.iter() {
1799                            let mut ui_account = None;
1800                            for (updated_pubkey, account) in tx_info.post_accounts.iter() {
1801                                if observed_pubkey.eq(&updated_pubkey.to_string()) {
1802                                    ui_account = Some(
1803                                        svm_locker
1804                                            .account_to_rpc_keyed_account(
1805                                                updated_pubkey,
1806                                                account,
1807                                                &RpcAccountInfoConfig::default(),
1808                                                None,
1809                                            )
1810                                            .account,
1811                                    );
1812                                }
1813                            }
1814                            ui_accounts.push(ui_account);
1815                        }
1816                        accounts = Some(ui_accounts);
1817                    }
1818                    get_simulate_transaction_result(
1819                        tx_info.meta,
1820                        accounts,
1821                        None,
1822                        replacement_blockhash,
1823                        config.inner_instructions,
1824                        &tx_message,
1825                        loaded_addresses_data.as_ref(),
1826                        Some(loaded_accounts_data_size as u32),
1827                    )
1828                }
1829                Err(tx_info) => get_simulate_transaction_result(
1830                    tx_info.meta,
1831                    None,
1832                    Some(tx_info.err),
1833                    replacement_blockhash,
1834                    config.inner_instructions,
1835                    &tx_message,
1836                    loaded_addresses_data.as_ref(),
1837                    Some(loaded_accounts_data_size as u32),
1838                ),
1839            };
1840
1841            Ok(RpcResponse {
1842                context: RpcResponseContext::new(slot),
1843                value,
1844            })
1845        })
1846    }
1847
1848    fn minimum_ledger_slot(&self, meta: Self::Metadata) -> BoxFuture<Result<Slot>> {
1849        let SurfnetRpcContext {
1850            svm_locker,
1851            remote_ctx,
1852        } = match meta.get_rpc_context(()) {
1853            Ok(res) => res,
1854            Err(e) => return e.into(),
1855        };
1856
1857        Box::pin(async move {
1858            // Forward to remote if available, otherwise return genesis_slot for local chains
1859            // With sparse block storage, all slots from genesis_slot onwards are valid
1860            if let Some((remote_client, _)) = remote_ctx {
1861                remote_client
1862                    .client
1863                    .minimum_ledger_slot()
1864                    .await
1865                    .map_err(|e| SurfpoolError::client_error(e).into())
1866            } else {
1867                Ok(svm_locker.with_svm_reader(|svm| svm.genesis_slot))
1868            }
1869        })
1870    }
1871
1872    fn get_block(
1873        &self,
1874        meta: Self::Metadata,
1875        slot: Slot,
1876        config: Option<RpcEncodingConfigWrapper<RpcBlockConfig>>,
1877    ) -> BoxFuture<Result<Option<UiConfirmedBlock>>> {
1878        let config = config.map(|c| c.convert_to_current()).unwrap_or_default();
1879
1880        let SurfnetRpcContext {
1881            svm_locker,
1882            remote_ctx,
1883        } = match meta.get_rpc_context(config.commitment) {
1884            Ok(res) => res,
1885            Err(e) => return e.into(),
1886        };
1887
1888        Box::pin(async move {
1889            let remote_client = remote_ctx.as_ref().map(|(client, _)| client.clone());
1890            let result = svm_locker.get_block(&remote_client, &slot, &config).await;
1891            Ok(result?.inner)
1892        })
1893    }
1894
1895    fn get_block_time(
1896        &self,
1897        meta: Self::Metadata,
1898        slot: Slot,
1899    ) -> BoxFuture<Result<Option<UnixTimestamp>>> {
1900        let svm_locker = match meta.get_svm_locker() {
1901            Ok(locker) => locker,
1902            Err(e) => return e.into(),
1903        };
1904
1905        Box::pin(async move {
1906            let block_time = svm_locker.with_svm_reader(|svm_reader| {
1907                Ok::<_, jsonrpc_core::Error>(match svm_reader.blocks.get(&slot)? {
1908                    Some(block) => Some(block.block_time),
1909                    None => {
1910                        // With sparse block storage, calculate time for missing blocks
1911                        if svm_reader.is_slot_in_valid_range(slot) {
1912                            let time_ms = svm_reader.calculate_block_time_for_slot(slot);
1913                            Some((time_ms / 1_000) as i64)
1914                        } else {
1915                            None
1916                        }
1917                    }
1918                })
1919            })?;
1920            Ok(block_time)
1921        })
1922    }
1923
1924    fn get_blocks(
1925        &self,
1926        meta: Self::Metadata,
1927        start_slot: Slot,
1928        wrapper: Option<RpcBlocksConfigWrapper>,
1929        config: Option<RpcContextConfig>,
1930    ) -> BoxFuture<Result<Vec<Slot>>> {
1931        let end_slot = match wrapper {
1932            Some(RpcBlocksConfigWrapper::EndSlotOnly(end_slot)) => end_slot,
1933            Some(RpcBlocksConfigWrapper::ConfigOnly(_)) => None,
1934            None => None,
1935        };
1936
1937        let config = config.unwrap_or_default();
1938        // get blocks should default to processed rather than finalized to default to the most recent
1939        let commitment = config.commitment.unwrap_or(CommitmentConfig {
1940            commitment: CommitmentLevel::Processed,
1941        });
1942
1943        const MAX_SLOT_RANGE: u64 = 500_000;
1944        if let Some(end) = end_slot {
1945            if end < start_slot {
1946                // early return for invalid range
1947                return Box::pin(async { Ok(vec![]) });
1948            }
1949            if end.saturating_sub(start_slot) > MAX_SLOT_RANGE {
1950                return Box::pin(async move {
1951                    Err(Error::invalid_params(format!(
1952                        "Slot range too large. Maximum: {}, Requested: {}",
1953                        MAX_SLOT_RANGE,
1954                        end.saturating_sub(start_slot)
1955                    )))
1956                });
1957            }
1958        }
1959
1960        let SurfnetRpcContext {
1961            svm_locker,
1962            remote_ctx,
1963        } = match meta.get_rpc_context(commitment) {
1964            Ok(res) => res,
1965            Err(e) => return e.into(),
1966        };
1967
1968        Box::pin(async move {
1969            let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
1970            let effective_end_slot = end_slot
1971                .map(|end| end.min(committed_latest_slot))
1972                .unwrap_or(committed_latest_slot);
1973
1974            let genesis_slot = svm_locker.with_svm_reader(|svm| svm.genesis_slot);
1975
1976            let (local_min_slot, local_slots, effective_end_slot) = if effective_end_slot
1977                < start_slot
1978            {
1979                (None, vec![], effective_end_slot)
1980            } else {
1981                // With sparse block storage, all slots from genesis_slot onwards are valid
1982                // Return all slots in the requested range instead of filtering by stored blocks
1983                let local_min_slot = Some(genesis_slot);
1984                let local_slots: Vec<Slot> = (start_slot.max(genesis_slot)..=effective_end_slot)
1985                    .filter(|slot| *slot <= committed_latest_slot)
1986                    .collect();
1987
1988                (local_min_slot, local_slots, effective_end_slot)
1989            };
1990
1991            if let Some(min_context_slot) = config.min_context_slot {
1992                if committed_latest_slot < min_context_slot {
1993                    return Err(RpcCustomError::MinContextSlotNotReached {
1994                        context_slot: min_context_slot,
1995                    }
1996                    .into());
1997                }
1998            }
1999
2000            if effective_end_slot.saturating_sub(start_slot) > MAX_SLOT_RANGE {
2001                return Err(Error::invalid_params(format!(
2002                    "Slot range too large. Maximum: {}, Requested: {}",
2003                    MAX_SLOT_RANGE,
2004                    effective_end_slot.saturating_sub(start_slot)
2005                )));
2006            }
2007
2008            let remote_slots = if let (Some((remote_client, _)), Some(local_min)) =
2009                (&remote_ctx, local_min_slot)
2010            {
2011                if start_slot < local_min {
2012                    let remote_end = effective_end_slot.min(local_min.saturating_sub(1));
2013                    if start_slot <= remote_end {
2014                        remote_client
2015                            .client
2016                            .get_blocks(start_slot, Some(remote_end))
2017                            .await
2018                            .unwrap_or_else(|_| vec![])
2019                    } else {
2020                        vec![]
2021                    }
2022                } else {
2023                    vec![]
2024                }
2025            } else if remote_ctx.is_some() && local_min_slot.is_none() {
2026                remote_ctx
2027                    .as_ref()
2028                    .unwrap()
2029                    .0
2030                    .client
2031                    .get_blocks(start_slot, Some(effective_end_slot))
2032                    .await
2033                    .unwrap_or_else(|_| vec![])
2034            } else {
2035                vec![]
2036            };
2037
2038            // Combine results
2039            let mut combined_slots = remote_slots;
2040            combined_slots.extend(local_slots);
2041            combined_slots.sort_unstable();
2042            combined_slots.dedup();
2043
2044            if combined_slots.len() > MAX_SLOT_RANGE as usize {
2045                combined_slots.truncate(MAX_SLOT_RANGE as usize);
2046            }
2047
2048            Ok(combined_slots)
2049        })
2050    }
2051
2052    fn get_blocks_with_limit(
2053        &self,
2054        meta: Self::Metadata,
2055        start_slot: Slot,
2056        limit: usize,
2057        config: Option<RpcContextConfig>,
2058    ) -> BoxFuture<Result<Vec<Slot>>> {
2059        let config = config.unwrap_or_default();
2060        let commitment = config.commitment.unwrap_or(CommitmentConfig {
2061            commitment: CommitmentLevel::Processed,
2062        });
2063
2064        if limit == 0 {
2065            return Box::pin(
2066                async move { Err(Error::invalid_params("Limit must be greater than 0")) },
2067            );
2068        }
2069
2070        const MAX_LIMIT: usize = 500_000;
2071        if limit > MAX_LIMIT {
2072            return Box::pin(async move {
2073                Err(Error::invalid_params(format!(
2074                    "Limit too large. Maximum limit allowed: {}",
2075                    MAX_LIMIT
2076                )))
2077            });
2078        }
2079
2080        let SurfnetRpcContext {
2081            svm_locker,
2082            remote_ctx,
2083        } = match meta.get_rpc_context(commitment) {
2084            Ok(res) => res,
2085            Err(e) => return e.into(),
2086        };
2087
2088        Box::pin(async move {
2089            let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
2090            let genesis_slot = svm_locker.with_svm_reader(|svm| svm.genesis_slot);
2091
2092            // With sparse block storage, all slots from genesis_slot onwards are valid
2093            // Return all slots in the requested range instead of filtering by stored blocks
2094            let local_min_slot = Some(genesis_slot);
2095            let local_slots: Vec<Slot> =
2096                (start_slot.max(genesis_slot)..=committed_latest_slot).collect();
2097
2098            if let Some(min_context_slot) = config.min_context_slot {
2099                if committed_latest_slot < min_context_slot {
2100                    return Err(RpcCustomError::MinContextSlotNotReached {
2101                        context_slot: min_context_slot,
2102                    }
2103                    .into());
2104                }
2105            }
2106
2107            // fetch remote blocks when needed, using the same logic as get_blocks
2108            let remote_slots = if let (Some((remote_client, _)), Some(local_min)) =
2109                (&remote_ctx, local_min_slot)
2110            {
2111                if start_slot < local_min {
2112                    let remote_end = committed_latest_slot.min(local_min.saturating_sub(1));
2113                    if start_slot <= remote_end {
2114                        remote_client
2115                            .client
2116                            .get_blocks(start_slot, Some(remote_end))
2117                            .await
2118                            .unwrap_or_else(|_| vec![])
2119                    } else {
2120                        vec![]
2121                    }
2122                } else {
2123                    vec![]
2124                }
2125            } else if remote_ctx.is_some() && local_min_slot.is_none() {
2126                // no local blocks exist, fetch from remote
2127                remote_ctx
2128                    .as_ref()
2129                    .unwrap()
2130                    .0
2131                    .client
2132                    .get_blocks(start_slot, Some(committed_latest_slot))
2133                    .await
2134                    .unwrap_or_else(|_| vec![])
2135            } else {
2136                vec![]
2137            };
2138
2139            let mut combined_slots = remote_slots;
2140            combined_slots.extend(local_slots);
2141            combined_slots.sort_unstable();
2142            combined_slots.dedup();
2143
2144            // apply the limit take only the first 'limit' slots
2145            combined_slots.truncate(limit);
2146
2147            Ok(combined_slots)
2148        })
2149    }
2150
2151    fn get_transaction(
2152        &self,
2153        meta: Self::Metadata,
2154        signature_str: String,
2155        config: Option<RpcEncodingConfigWrapper<RpcTransactionConfig>>,
2156    ) -> BoxFuture<Result<Option<EncodedConfirmedTransactionWithStatusMeta>>> {
2157        let mut config = config.map(|c| c.convert_to_current()).unwrap_or_default();
2158        adjust_default_transaction_config(&mut config);
2159
2160        Box::pin(async move {
2161            let signature = Signature::from_str(&signature_str)
2162                .map_err(|e| SurfpoolError::invalid_signature(&signature_str, e.to_string()))?;
2163
2164            let SurfnetRpcContext {
2165                svm_locker,
2166                remote_ctx,
2167            } = meta.get_rpc_context(())?;
2168
2169            // TODO: implement new interfaces in LiteSVM to get all the relevant info
2170            // needed to return the actual tx, not just some metadata
2171            match svm_locker
2172                .get_transaction(&remote_ctx.map(|(r, _)| r), &signature, config)
2173                .await?
2174            {
2175                GetTransactionResult::None(_) => Ok(None),
2176                GetTransactionResult::FoundTransaction(_, meta, _) => Ok(Some(meta)),
2177            }
2178        })
2179    }
2180
2181    fn get_signatures_for_address(
2182        &self,
2183        meta: Self::Metadata,
2184        address: String,
2185        config: Option<RpcSignaturesForAddressConfig>,
2186    ) -> BoxFuture<Result<Vec<RpcConfirmedTransactionStatusWithSignature>>> {
2187        let pubkey = match verify_pubkey(&address) {
2188            Ok(s) => s,
2189            Err(e) => return e.into(),
2190        };
2191        let SurfnetRpcContext {
2192            svm_locker,
2193            remote_ctx,
2194        } = match meta.get_rpc_context(()) {
2195            Ok(res) => res,
2196            Err(e) => return e.into(),
2197        };
2198
2199        Box::pin(async move {
2200            let signatures = svm_locker
2201                .get_signatures_for_address(&remote_ctx, &pubkey, config)
2202                .await?
2203                .inner;
2204            Ok(signatures)
2205        })
2206    }
2207
2208    fn get_first_available_block(&self, meta: Self::Metadata) -> Result<Slot> {
2209        meta.with_svm_reader(|svm_reader| {
2210            Ok::<_, jsonrpc_core::Error>(
2211                svm_reader
2212                    .blocks
2213                    .keys()?
2214                    .into_iter()
2215                    .min()
2216                    .unwrap_or_default(),
2217            )
2218        })?
2219        .map_err(Into::into)
2220    }
2221
2222    fn get_latest_blockhash(
2223        &self,
2224        meta: Self::Metadata,
2225        config: Option<RpcContextConfig>,
2226    ) -> Result<RpcResponse<RpcBlockhash>> {
2227        let svm_locker = meta.get_svm_locker()?;
2228
2229        let config = config.unwrap_or_default();
2230        let commitment = config.commitment.unwrap_or_default();
2231
2232        let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
2233        if let Some(min_context_slot) = config.min_context_slot {
2234            if committed_latest_slot < min_context_slot {
2235                return Err(RpcCustomError::MinContextSlotNotReached {
2236                    context_slot: min_context_slot,
2237                }
2238                .into());
2239            }
2240        }
2241
2242        let blockhash = svm_locker
2243            .get_latest_blockhash(&commitment)
2244            .unwrap_or_else(|| svm_locker.latest_absolute_blockhash());
2245
2246        let current_block_height = svm_locker.get_epoch_info().block_height;
2247        let last_valid_block_height = current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
2248        Ok(RpcResponse {
2249            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2250            value: RpcBlockhash {
2251                blockhash: blockhash.to_string(),
2252                last_valid_block_height,
2253            },
2254        })
2255    }
2256
2257    fn is_blockhash_valid(
2258        &self,
2259        meta: Self::Metadata,
2260        blockhash: String,
2261        config: Option<RpcContextConfig>,
2262    ) -> Result<RpcResponse<bool>> {
2263        let hash = blockhash
2264            .parse::<solana_hash::Hash>()
2265            .map_err(|e| Error::invalid_params(format!("Invalid blockhash: {e:?}")))?;
2266        let config = config.unwrap_or_default();
2267
2268        let svm_locker = meta.get_svm_locker()?;
2269
2270        let committed_latest_slot =
2271            svm_locker.get_slot_for_commitment(&config.commitment.unwrap_or_default());
2272
2273        let is_valid =
2274            svm_locker.with_svm_reader(|svm_reader| svm_reader.check_blockhash_is_recent(&hash));
2275
2276        if let Some(min_context_slot) = config.min_context_slot {
2277            if committed_latest_slot < min_context_slot {
2278                return Err(RpcCustomError::MinContextSlotNotReached {
2279                    context_slot: min_context_slot,
2280                }
2281                .into());
2282            }
2283        }
2284
2285        Ok(RpcResponse {
2286            context: RpcResponseContext::new(committed_latest_slot),
2287            value: is_valid,
2288        })
2289    }
2290
2291    fn get_fee_for_message(
2292        &self,
2293        meta: Self::Metadata,
2294        encoded: String,
2295        config: Option<RpcContextConfig>,
2296    ) -> Result<RpcResponse<Option<u64>>> {
2297        let (_, message) =
2298            decode_and_deserialize::<VersionedMessage>(encoded, TransactionBinaryEncoding::Base64)?;
2299
2300        let RpcContextConfig {
2301            commitment,
2302            min_context_slot,
2303        } = config.unwrap_or_default();
2304        let min_ctx_slot = min_context_slot.unwrap_or_default();
2305
2306        let svm_locker = meta.get_svm_locker()?;
2307
2308        let slot = if let Some(commitment_config) = commitment {
2309            svm_locker.get_slot_for_commitment(&commitment_config)
2310        } else {
2311            svm_locker.get_latest_absolute_slot()
2312        };
2313
2314        if let Some(min_slot) = min_context_slot
2315            && slot < min_slot
2316        {
2317            return Err(RpcCustomError::MinContextSlotNotReached {
2318                context_slot: min_ctx_slot,
2319            }
2320            .into());
2321        }
2322
2323        Ok(RpcResponse {
2324            context: RpcResponseContext::new(slot),
2325            value: Some((message.header().num_required_signatures as u64) * 5000),
2326        })
2327    }
2328
2329    fn get_stake_minimum_delegation(
2330        &self,
2331        meta: Self::Metadata,
2332        config: Option<RpcContextConfig>,
2333    ) -> Result<RpcResponse<u64>> {
2334        let config = config.unwrap_or_default();
2335        let commitment_config = config.commitment.unwrap_or(CommitmentConfig {
2336            commitment: CommitmentLevel::Processed,
2337        });
2338
2339        meta.with_svm_reader(|svm_reader| {
2340            let context_slot = match commitment_config.commitment {
2341                CommitmentLevel::Processed => svm_reader.get_latest_absolute_slot(),
2342                CommitmentLevel::Confirmed => {
2343                    svm_reader.get_latest_absolute_slot().saturating_sub(1)
2344                }
2345                CommitmentLevel::Finalized => svm_reader
2346                    .get_latest_absolute_slot()
2347                    .saturating_sub(FINALIZATION_SLOT_THRESHOLD),
2348            };
2349
2350            RpcResponse {
2351                context: RpcResponseContext::new(context_slot),
2352                value: 0,
2353            }
2354        })
2355        .map_err(Into::into)
2356    }
2357
2358    fn get_recent_prioritization_fees(
2359        &self,
2360        meta: Self::Metadata,
2361        pubkey_strs: Option<Vec<String>>,
2362    ) -> BoxFuture<Result<Vec<RpcPrioritizationFee>>> {
2363        let pubkeys_filter = match pubkey_strs
2364            .map(|strs| {
2365                strs.iter()
2366                    .map(|s| verify_pubkey(s))
2367                    .collect::<SurfpoolResult<Vec<_>>>()
2368            })
2369            .transpose()
2370        {
2371            Ok(pubkeys) => pubkeys,
2372            Err(e) => return e.into(),
2373        };
2374
2375        let SurfnetRpcContext {
2376            svm_locker,
2377            remote_ctx,
2378        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2379            Ok(res) => res,
2380            Err(e) => return e.into(),
2381        };
2382
2383        Box::pin(async move {
2384            let (blocks, transactions) = svm_locker.with_svm_reader(|svm_reader| {
2385                (svm_reader.blocks.clone(), svm_reader.transactions.clone())
2386            });
2387
2388            // Get MAX_PRIORITIZATION_FEE_BLOCKS_CACHE most recent blocks
2389            let recent_headers = blocks
2390                .into_iter()?
2391                .sorted_by_key(|(slot, _)| std::cmp::Reverse(*slot))
2392                .take(MAX_PRIORITIZATION_FEE_BLOCKS_CACHE)
2393                .collect::<Vec<_>>();
2394
2395            // Flatten the transactions map to get all transactions in the recent blocks
2396            let recent_transactions = recent_headers
2397                .into_iter()
2398                .flat_map(|(slot, header)| {
2399                    header
2400                        .signatures
2401                        .iter()
2402                        .filter_map(|signature| {
2403                            // Check if the signature exists in the transactions map
2404                            transactions
2405                                .get(&signature.to_string())
2406                                .ok()
2407                                .flatten()
2408                                .map(|tx| (slot, tx))
2409                        })
2410                        .collect::<Vec<_>>()
2411                })
2412                .collect::<Vec<_>>();
2413
2414            // Helper function to extract compute unit price from a CompiledInstruction
2415            fn get_compute_unit_price(ix: CompiledInstruction, accounts: &[Pubkey]) -> Option<u64> {
2416                let program_account = accounts.get(ix.program_id_index as usize)?;
2417                if *program_account != compute_budget::id() {
2418                    return None;
2419                }
2420
2421                if let Ok(ComputeBudgetInstruction::SetComputeUnitPrice(price)) =
2422                    borsh::from_slice::<ComputeBudgetInstruction>(&ix.data)
2423                {
2424                    return Some(price);
2425                }
2426
2427                None
2428            }
2429
2430            let mut prioritization_fees = vec![];
2431            for (slot, tx) in recent_transactions {
2432                match tx {
2433                    SurfnetTransactionStatus::Received => {}
2434                    SurfnetTransactionStatus::Processed(data) => {
2435                        let (status_meta, _) = data.as_ref();
2436                        let tx = &status_meta.transaction;
2437
2438                        // If the transaction has an ALT and includes a compute budget instruction,
2439                        // the ALT accounts are included in the recent prioritization fees,
2440                        // so we get _all_ the pubkeys from the message
2441                        let loaded_addresses = svm_locker
2442                            .get_loaded_addresses(&remote_ctx, &tx.message)
2443                            .await?;
2444                        let account_keys = svm_locker.get_pubkeys_from_message(
2445                            &tx.message,
2446                            loaded_addresses.as_ref().map(|l| l.all_loaded_addresses()),
2447                        );
2448
2449                        let instructions = match &tx.message {
2450                            VersionedMessage::V0(msg) => &msg.instructions,
2451                            VersionedMessage::Legacy(msg) => &msg.instructions,
2452                        };
2453
2454                        // Find all compute unit prices in the transaction's instructions
2455                        let compute_unit_prices = instructions
2456                            .iter()
2457                            .filter_map(|ix| get_compute_unit_price(ix.clone(), &account_keys))
2458                            .collect::<Vec<_>>();
2459
2460                        for compute_unit_price in compute_unit_prices {
2461                            if let Some(pubkeys_filter) = &pubkeys_filter {
2462                                // If none of the accounts involved in this transaction are in the filter,
2463                                // we don't include the prioritization fee, so we continue
2464                                if !pubkeys_filter
2465                                    .iter()
2466                                    .any(|pk| account_keys.iter().any(|a| a == pk))
2467                                {
2468                                    continue;
2469                                }
2470                            }
2471                            // if there's no filter, or if the filter matches an account in this transaction, we include the fee
2472                            prioritization_fees.push(RpcPrioritizationFee {
2473                                slot,
2474                                prioritization_fee: compute_unit_price,
2475                            });
2476                        }
2477                    }
2478                }
2479            }
2480            Ok(prioritization_fees)
2481        })
2482    }
2483}
2484
2485fn get_simulate_transaction_result(
2486    metadata: TransactionMetadata,
2487    accounts: Option<Vec<Option<UiAccount>>>,
2488    error: Option<TransactionError>,
2489    replacement_blockhash: Option<RpcBlockhash>,
2490    include_inner_instructions: bool,
2491    message: &VersionedMessage,
2492    loaded_addresses: Option<&solana_message::v0::LoadedAddresses>,
2493    loaded_accounts_data_size: Option<u32>,
2494) -> RpcSimulateTransactionResult {
2495    RpcSimulateTransactionResult {
2496        accounts,
2497        err: error.map(|e| e.into()),
2498        inner_instructions: if include_inner_instructions {
2499            Some(transform_tx_metadata_to_ui_accounts(
2500                metadata.clone(),
2501                message,
2502                loaded_addresses,
2503            ))
2504        } else {
2505            None
2506        },
2507        logs: Some(metadata.logs.clone()),
2508        replacement_blockhash,
2509        return_data: if metadata.return_data.program_id == system_program::id()
2510            && metadata.return_data.data.is_empty()
2511        {
2512            None
2513        } else {
2514            Some(metadata.return_data.clone().into())
2515        },
2516        units_consumed: Some(metadata.compute_units_consumed),
2517        loaded_accounts_data_size,
2518        fee: None,
2519        pre_balances: None,
2520        post_balances: None,
2521        pre_token_balances: None,
2522        post_token_balances: None,
2523        loaded_addresses: None,
2524    }
2525}
2526
2527#[cfg(test)]
2528mod tests {
2529    pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
2530
2531    use std::thread::JoinHandle;
2532
2533    use base64::{Engine, prelude::BASE64_STANDARD};
2534    use bincode::Options;
2535    use crossbeam_channel::Receiver;
2536    use solana_account_decoder::{UiAccount, UiAccountData, UiAccountEncoding};
2537    use solana_client::rpc_config::RpcSimulateTransactionAccountsConfig;
2538    use solana_commitment_config::CommitmentConfig;
2539    use solana_hash::Hash;
2540    use solana_instruction::Instruction;
2541    use solana_keypair::Keypair;
2542    use solana_message::{
2543        MessageHeader, legacy::Message as LegacyMessage, v0::Message as V0Message,
2544    };
2545    use solana_pubkey::Pubkey;
2546    use solana_signer::Signer;
2547    use solana_system_interface::{
2548        instruction::{self as system_instruction, transfer},
2549        program as system_program,
2550    };
2551    use solana_transaction::{
2552        Transaction,
2553        versioned::{Legacy, TransactionVersion},
2554    };
2555    use solana_transaction_error::TransactionError;
2556    use solana_transaction_status::{
2557        EncodedTransaction, EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiMessage,
2558        UiRawMessage, UiTransaction,
2559    };
2560    use surfpool_types::{SimnetCommand, TransactionConfirmationStatus};
2561    use test_case::test_case;
2562
2563    use super::*;
2564    use crate::{
2565        surfnet::{BlockHeader, BlockIdentifier, remote::SurfnetRemoteClient},
2566        tests::helpers::TestSetup,
2567        types::{SyntheticBlockhash, TransactionWithStatusMeta},
2568    };
2569
2570    fn build_v0_transaction(
2571        payer: &Pubkey,
2572        signers: &[&Keypair],
2573        instructions: &[Instruction],
2574        recent_blockhash: &Hash,
2575    ) -> VersionedTransaction {
2576        let msg = VersionedMessage::V0(
2577            V0Message::try_compile(&payer, instructions, &[], *recent_blockhash).unwrap(),
2578        );
2579        VersionedTransaction::try_new(msg, signers).unwrap()
2580    }
2581
2582    fn build_legacy_transaction(
2583        payer: &Pubkey,
2584        signers: &[&Keypair],
2585        instructions: &[Instruction],
2586        recent_blockhash: &Hash,
2587    ) -> VersionedTransaction {
2588        let msg = VersionedMessage::Legacy(LegacyMessage::new_with_blockhash(
2589            instructions,
2590            Some(payer),
2591            recent_blockhash,
2592        ));
2593        VersionedTransaction::try_new(msg, signers).unwrap()
2594    }
2595
2596    async fn send_and_await_transaction(
2597        tx: VersionedTransaction,
2598        setup: TestSetup<SurfpoolFullRpc>,
2599        mempool_rx: Receiver<SimnetCommand>,
2600    ) -> JoinHandle<String> {
2601        let setup_clone = setup.clone();
2602        let handle = hiro_system_kit::thread_named("send_tx")
2603            .spawn(move || {
2604                let res = setup_clone
2605                    .rpc
2606                    .send_transaction(
2607                        Some(setup_clone.context),
2608                        bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2609                        None,
2610                    )
2611                    .unwrap();
2612
2613                res
2614            })
2615            .unwrap();
2616        loop {
2617            match mempool_rx.recv() {
2618                Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => {
2619                    let mut writer = setup.context.svm_locker.0.write().await;
2620                    let slot = writer.get_latest_absolute_slot();
2621                    writer.transactions_queued_for_confirmation.push_back((
2622                        tx.clone(),
2623                        status_tx.clone(),
2624                        None,
2625                    ));
2626                    let sig = tx.signatures[0];
2627                    let tx_with_status_meta = TransactionWithStatusMeta {
2628                        slot,
2629                        transaction: tx,
2630                        ..Default::default()
2631                    };
2632                    let mutated_accounts = std::collections::HashSet::new();
2633                    writer
2634                        .transactions
2635                        .store(
2636                            sig.to_string(),
2637                            SurfnetTransactionStatus::processed(
2638                                tx_with_status_meta,
2639                                mutated_accounts,
2640                            ),
2641                        )
2642                        .unwrap();
2643                    status_tx
2644                        .send(TransactionStatusEvent::Success(
2645                            TransactionConfirmationStatus::Confirmed,
2646                        ))
2647                        .unwrap();
2648                    break;
2649                }
2650                Ok(SimnetCommand::AirdropProcessed) => continue,
2651                _ => panic!("failed to receive transaction from mempool"),
2652            }
2653        }
2654
2655        handle
2656    }
2657
2658    #[test_case(None, false ; "when limit is None")]
2659    #[test_case(Some(1), false ; "when limit is ok")]
2660    #[test_case(Some(1000), true ; "when limit is above max spec")]
2661    fn test_get_recent_performance_samples(limit: Option<usize>, fails: bool) {
2662        let setup = TestSetup::new(SurfpoolFullRpc);
2663        let res = setup
2664            .rpc
2665            .get_recent_performance_samples(Some(setup.context), limit);
2666
2667        if fails {
2668            assert!(res.is_err());
2669        } else {
2670            assert!(res.is_ok());
2671        }
2672    }
2673
2674    #[tokio::test(flavor = "multi_thread")]
2675    async fn test_get_fee_for_message() {
2676        let setup = TestSetup::new(SurfpoolFullRpc);
2677        let runloop_context = setup.context;
2678        let rpc_server = setup.rpc;
2679        let payer = Keypair::new();
2680        let recipient = Pubkey::new_unique();
2681        let lamports_to_send = 5 * LAMPORTS_PER_SOL;
2682        let commitment_config_to_use = CommitmentConfig::confirmed();
2683
2684        let wrong_comm_min_ctx_slot = runloop_context
2685            .svm_locker
2686            .get_slot_for_commitment(&commitment_config_to_use)
2687            + 10;
2688
2689        let wrong_min_slot = runloop_context.svm_locker.get_latest_absolute_slot() + 10;
2690        let rpc_ctx_config_with_wrong_commitment = RpcContextConfig {
2691            commitment: Some(commitment_config_to_use),
2692            min_context_slot: Some(wrong_comm_min_ctx_slot),
2693        };
2694        let rpc_ctx_config_with_wrong_min_slot = RpcContextConfig {
2695            commitment: None,
2696            min_context_slot: Some(wrong_min_slot),
2697        };
2698
2699        let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
2700
2701        let latest_blockhash = runloop_context
2702            .svm_locker
2703            .with_svm_reader(|svm| svm.latest_blockhash());
2704        let message = solana_message::Message::new_with_blockhash(
2705            &[instruction],
2706            Some(&payer.pubkey()),
2707            &latest_blockhash,
2708        );
2709        let num_required_signatures = message.header.num_required_signatures as u64;
2710        let transaction =
2711            VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
2712
2713        let message_bytes = bincode::options()
2714            .with_fixint_encoding()
2715            .serialize(&transaction.message)
2716            .expect("message serialization");
2717        let encoded_message = base64::engine::general_purpose::STANDARD.encode(&message_bytes);
2718
2719        let get_fee_with_correct_config_pass_result = rpc_server.get_fee_for_message(
2720            Some(runloop_context.clone()),
2721            encoded_message.clone(),
2722            None,
2723        );
2724
2725        assert!(
2726            get_fee_with_correct_config_pass_result.is_ok(),
2727            "Expected get_fee_for_message to pass with correct configs"
2728        );
2729        assert_eq!(
2730            get_fee_with_correct_config_pass_result
2731                .unwrap()
2732                .value
2733                .unwrap(),
2734            (num_required_signatures as u64) * 5_000,
2735            "Invalid return value"
2736        );
2737
2738        let get_fee_with_wrong_commitment_fail_result = rpc_server.get_fee_for_message(
2739            Some(runloop_context.clone()),
2740            encoded_message.clone(),
2741            Some(rpc_ctx_config_with_wrong_commitment),
2742        );
2743
2744        let wrong_comm_expected_err: Result<()> = Result::Err(
2745            RpcCustomError::MinContextSlotNotReached {
2746                context_slot: wrong_comm_min_ctx_slot,
2747            }
2748            .into(),
2749        );
2750
2751        assert!(
2752            get_fee_with_wrong_commitment_fail_result.is_err(),
2753            "expected this txn to fail when min_ctx_slot > slot_for_commitment"
2754        );
2755
2756        assert_eq!(
2757            get_fee_with_wrong_commitment_fail_result.err().unwrap(),
2758            wrong_comm_expected_err.err().unwrap()
2759        );
2760
2761        let get_fee_with_wrong_mint_slot_fail_result = rpc_server.get_fee_for_message(
2762            Some(runloop_context.clone()),
2763            encoded_message,
2764            Some(rpc_ctx_config_with_wrong_min_slot),
2765        );
2766
2767        let wrong_min_slot_expected_err: Result<()> = Result::Err(
2768            RpcCustomError::MinContextSlotNotReached {
2769                context_slot: wrong_min_slot,
2770            }
2771            .into(),
2772        );
2773        assert!(
2774            get_fee_with_wrong_mint_slot_fail_result.is_err(),
2775            "expected this txn to fail when min_ctx_slot > absolute_latest_slot"
2776        );
2777        assert_eq!(
2778            get_fee_with_wrong_mint_slot_fail_result.err().unwrap(),
2779            wrong_min_slot_expected_err.err().unwrap()
2780        );
2781    }
2782
2783    #[tokio::test(flavor = "multi_thread")]
2784    async fn test_get_signature_statuses() {
2785        let pks = (0..10).map(|_| Pubkey::new_unique());
2786        let valid_txs = pks.len();
2787        let invalid_txs = pks.len();
2788        let payer = Keypair::new();
2789        let mut setup = TestSetup::new(SurfpoolFullRpc).without_blockhash().await;
2790        let recent_blockhash = setup
2791            .context
2792            .svm_locker
2793            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2794
2795        let valid = pks
2796            .clone()
2797            .map(|pk| {
2798                Transaction::new_signed_with_payer(
2799                    &[system_instruction::transfer(
2800                        &payer.pubkey(),
2801                        &pk,
2802                        LAMPORTS_PER_SOL,
2803                    )],
2804                    Some(&payer.pubkey()),
2805                    &[payer.insecure_clone()],
2806                    recent_blockhash,
2807                )
2808            })
2809            .collect::<Vec<_>>();
2810        let invalid = pks
2811            .map(|pk| {
2812                Transaction::new_unsigned(LegacyMessage::new(
2813                    &[system_instruction::transfer(
2814                        &pk,
2815                        &payer.pubkey(),
2816                        LAMPORTS_PER_SOL,
2817                    )],
2818                    Some(&payer.pubkey()),
2819                ))
2820            })
2821            .collect::<Vec<_>>();
2822        let txs = valid
2823            .into_iter()
2824            .chain(invalid.into_iter())
2825            .map(|tx| VersionedTransaction {
2826                signatures: tx.signatures,
2827                message: VersionedMessage::Legacy(tx.message),
2828            })
2829            .collect::<Vec<_>>();
2830        let _ = setup.context.svm_locker.0.write().await.airdrop(
2831            &payer.pubkey(),
2832            (valid_txs + invalid_txs) as u64 * 2 * LAMPORTS_PER_SOL,
2833        );
2834        setup.process_txs(txs.clone()).await;
2835
2836        // Capture the expected slot before the call to verify context slot consistency
2837        let current_slot = setup
2838            .context
2839            .svm_locker
2840            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
2841
2842        // fetch while transactions are still in processed status
2843        {
2844            let res = setup
2845                .rpc
2846                .get_signature_statuses(
2847                    Some(setup.context.clone()),
2848                    txs.iter().map(|tx| tx.signatures[0].to_string()).collect(),
2849                    None,
2850                )
2851                .await
2852                .unwrap();
2853            assert_eq!(
2854                res.value.iter().flatten().collect::<Vec<_>>().len(),
2855                0,
2856                "processed transactions should not be returning values"
2857            );
2858        }
2859
2860        // confirm a block to move transactions to confirmed status
2861        setup
2862            .context
2863            .svm_locker
2864            .confirm_current_block(&None)
2865            .await
2866            .unwrap();
2867        let res = setup
2868            .rpc
2869            .get_signature_statuses(
2870                Some(setup.context),
2871                txs.iter().map(|tx| tx.signatures[0].to_string()).collect(),
2872                None,
2873            )
2874            .await
2875            .unwrap();
2876
2877        // Verify context slot is captured at the beginning of the call
2878        assert_eq!(
2879            res.context.slot,
2880            current_slot + 1,
2881            "Context slot should be captured at the beginning of the call, not after lookups"
2882        );
2883
2884        assert_eq!(
2885            res.value
2886                .iter()
2887                .filter(|status| {
2888                    println!("status: {:?}", status);
2889                    if let Some(s) = status {
2890                        s.status.is_ok()
2891                    } else {
2892                        false
2893                    }
2894                })
2895                .count(),
2896            valid_txs,
2897            "incorrect number of valid txs"
2898        );
2899        assert_eq!(
2900            res.value
2901                .iter()
2902                .filter(|status| if let Some(s) = status {
2903                    s.status.is_err()
2904                } else {
2905                    true
2906                })
2907                .count(),
2908            invalid_txs,
2909            "incorrect number of invalid txs"
2910        );
2911    }
2912
2913    #[test]
2914    fn test_request_airdrop() {
2915        let pk = Pubkey::new_unique();
2916        let lamports = 1000;
2917        let setup = TestSetup::new(SurfpoolFullRpc);
2918        let res = setup
2919            .rpc
2920            .request_airdrop(Some(setup.context.clone()), pk.to_string(), lamports, None)
2921            .unwrap();
2922        let sig = Signature::from_str(res.as_str()).unwrap();
2923        let state_reader = setup.context.svm_locker.0.blocking_read();
2924        assert_eq!(
2925            state_reader
2926                .inner
2927                .get_account(&pk)
2928                .unwrap()
2929                .unwrap()
2930                .lamports,
2931            lamports,
2932            "airdropped amount is incorrect"
2933        );
2934        assert!(
2935            state_reader.get_transaction(&sig).unwrap().is_some(),
2936            "transaction is not found in the SVM"
2937        );
2938        assert!(
2939            state_reader
2940                .transactions
2941                .get(&sig.to_string())
2942                .unwrap()
2943                .is_some(),
2944            "transaction is not found in the history"
2945        );
2946    }
2947
2948    #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
2949    #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
2950    #[tokio::test(flavor = "multi_thread")]
2951    async fn test_send_transaction(version: TransactionVersion) {
2952        let payer = Keypair::new();
2953        let pk = Pubkey::new_unique();
2954        let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
2955        let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
2956        let recent_blockhash = setup
2957            .context
2958            .svm_locker
2959            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2960
2961        let tx = match version {
2962            TransactionVersion::Legacy(_) => build_legacy_transaction(
2963                &payer.pubkey(),
2964                &[&payer.insecure_clone()],
2965                &[system_instruction::transfer(
2966                    &payer.pubkey(),
2967                    &pk,
2968                    LAMPORTS_PER_SOL,
2969                )],
2970                &recent_blockhash,
2971            ),
2972            TransactionVersion::Number(0) => build_v0_transaction(
2973                &payer.pubkey(),
2974                &[&payer.insecure_clone()],
2975                &[system_instruction::transfer(
2976                    &payer.pubkey(),
2977                    &pk,
2978                    LAMPORTS_PER_SOL,
2979                )],
2980                &recent_blockhash,
2981            ),
2982            _ => unimplemented!(),
2983        };
2984
2985        let _ = setup
2986            .context
2987            .svm_locker
2988            .0
2989            .write()
2990            .await
2991            .airdrop(&payer.pubkey(), 2 * LAMPORTS_PER_SOL);
2992
2993        let handle = send_and_await_transaction(tx.clone(), setup.clone(), mempool_rx).await;
2994        assert_eq!(
2995            handle.join().unwrap(),
2996            tx.signatures[0].to_string(),
2997            "incorrect signature"
2998        );
2999    }
3000
3001    #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3002    #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3003    #[tokio::test(flavor = "multi_thread")]
3004    async fn test_simulate_transaction(version: TransactionVersion) {
3005        let payer = Keypair::new();
3006        let pk = Pubkey::new_unique();
3007        let lamports = LAMPORTS_PER_SOL;
3008        let setup = TestSetup::new(SurfpoolFullRpc);
3009        let recent_blockhash = setup
3010            .context
3011            .svm_locker
3012            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3013
3014        let _ = setup
3015            .rpc
3016            .request_airdrop(
3017                Some(setup.context.clone()),
3018                payer.pubkey().to_string(),
3019                2 * lamports,
3020                None,
3021            )
3022            .unwrap();
3023
3024        let tx = match version {
3025            TransactionVersion::Legacy(_) => build_legacy_transaction(
3026                &payer.pubkey(),
3027                &[&payer.insecure_clone()],
3028                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3029                &recent_blockhash,
3030            ),
3031            TransactionVersion::Number(0) => build_v0_transaction(
3032                &payer.pubkey(),
3033                &[&payer.insecure_clone()],
3034                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3035                &recent_blockhash,
3036            ),
3037            _ => unimplemented!(),
3038        };
3039
3040        let simulation_res = setup
3041            .rpc
3042            .simulate_transaction(
3043                Some(setup.context),
3044                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3045                Some(RpcSimulateTransactionConfig {
3046                    sig_verify: true,
3047                    replace_recent_blockhash: false,
3048                    commitment: Some(CommitmentConfig::finalized()),
3049                    encoding: None,
3050                    accounts: Some(RpcSimulateTransactionAccountsConfig {
3051                        encoding: None,
3052                        addresses: vec![pk.to_string()],
3053                    }),
3054                    min_context_slot: None,
3055                    inner_instructions: false,
3056                }),
3057            )
3058            .await
3059            .unwrap();
3060
3061        assert_eq!(
3062            simulation_res.value.err, None,
3063            "Unexpected simulation error"
3064        );
3065        assert_eq!(
3066            simulation_res.value.accounts,
3067            Some(vec![Some(UiAccount {
3068                lamports,
3069                data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64),
3070                owner: system_program::id().to_string(),
3071                executable: false,
3072                rent_epoch: 0,
3073                space: Some(0),
3074            })]),
3075            "Wrong account content"
3076        );
3077    }
3078
3079    #[tokio::test(flavor = "multi_thread")]
3080    async fn test_simulate_transaction_oversized_base64_returns_invalid_params() {
3081        let setup = TestSetup::new(SurfpoolFullRpc);
3082
3083        let err = setup
3084            .rpc
3085            .simulate_transaction(
3086                Some(setup.context),
3087                "A".repeat(1645),
3088                Some(RpcSimulateTransactionConfig {
3089                    encoding: Some(UiTransactionEncoding::Base64),
3090                    ..RpcSimulateTransactionConfig::default()
3091                }),
3092            )
3093            .await
3094            .unwrap_err();
3095
3096        assert_eq!(err.code, jsonrpc_core::ErrorCode::InvalidParams);
3097        assert!(
3098            err.message.contains("base64 encoded"),
3099            "expected base64 size validation error, got: {}",
3100            err.message
3101        );
3102    }
3103
3104    #[tokio::test(flavor = "multi_thread")]
3105    async fn test_simulate_transaction_no_signers() {
3106        let payer = Keypair::new();
3107        let pk = Pubkey::new_unique();
3108        let lamports = LAMPORTS_PER_SOL;
3109        let setup = TestSetup::new(SurfpoolFullRpc);
3110        setup
3111            .context
3112            .svm_locker
3113            .with_svm_writer(|svm_writer| svm_writer.inner.set_sigverify(false));
3114        let recent_blockhash = setup
3115            .context
3116            .svm_locker
3117            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3118
3119        let _ = setup
3120            .rpc
3121            .request_airdrop(
3122                Some(setup.context.clone()),
3123                payer.pubkey().to_string(),
3124                2 * lamports,
3125                None,
3126            )
3127            .unwrap();
3128        //build_legacy_transaction
3129        let mut msg = LegacyMessage::new(
3130            &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3131            Some(&payer.pubkey()),
3132        );
3133        msg.recent_blockhash = recent_blockhash;
3134        let tx = Transaction::new_unsigned(msg);
3135
3136        let simulation_res = setup
3137            .rpc
3138            .simulate_transaction(
3139                Some(setup.context),
3140                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3141                Some(RpcSimulateTransactionConfig {
3142                    sig_verify: false,
3143                    replace_recent_blockhash: false,
3144                    commitment: Some(CommitmentConfig::finalized()),
3145                    encoding: None,
3146                    accounts: Some(RpcSimulateTransactionAccountsConfig {
3147                        encoding: None,
3148                        addresses: vec![pk.to_string()],
3149                    }),
3150                    min_context_slot: None,
3151                    inner_instructions: false,
3152                }),
3153            )
3154            .await
3155            .unwrap();
3156
3157        assert_eq!(
3158            simulation_res.value.err, None,
3159            "Unexpected simulation error"
3160        );
3161        assert_eq!(
3162            simulation_res.value.accounts,
3163            Some(vec![Some(UiAccount {
3164                lamports,
3165                data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64),
3166                owner: system_program::id().to_string(),
3167                executable: false,
3168                rent_epoch: 0,
3169                space: Some(0),
3170            })]),
3171            "Wrong account content"
3172        );
3173    }
3174    #[tokio::test(flavor = "multi_thread")]
3175    async fn test_simulate_transaction_no_signers_err() {
3176        let payer = Keypair::new();
3177        let pk = Pubkey::new_unique();
3178        let lamports = LAMPORTS_PER_SOL;
3179        let setup = TestSetup::new(SurfpoolFullRpc);
3180        let recent_blockhash = setup
3181            .context
3182            .svm_locker
3183            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3184
3185        let _ = setup
3186            .rpc
3187            .request_airdrop(
3188                Some(setup.context.clone()),
3189                payer.pubkey().to_string(),
3190                2 * lamports,
3191                None,
3192            )
3193            .unwrap();
3194        setup
3195            .context
3196            .svm_locker
3197            .with_svm_writer(|svm_writer| svm_writer.inner.set_sigverify(false));
3198
3199        //build_legacy_transaction
3200        let mut msg = LegacyMessage::new(
3201            &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3202            Some(&payer.pubkey()),
3203        );
3204        msg.recent_blockhash = recent_blockhash;
3205        let tx = Transaction::new_unsigned(msg);
3206
3207        let simulation_res = setup
3208            .rpc
3209            .simulate_transaction(
3210                Some(setup.context),
3211                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3212                Some(RpcSimulateTransactionConfig {
3213                    sig_verify: true,
3214                    replace_recent_blockhash: false,
3215                    commitment: Some(CommitmentConfig::finalized()),
3216                    encoding: None,
3217                    accounts: Some(RpcSimulateTransactionAccountsConfig {
3218                        encoding: None,
3219                        addresses: vec![pk.to_string()],
3220                    }),
3221                    min_context_slot: None,
3222                    inner_instructions: false,
3223                }),
3224            )
3225            .await
3226            .unwrap();
3227
3228        assert_eq!(
3229            simulation_res.value.err,
3230            Some(TransactionError::SignatureFailure.into())
3231        );
3232    }
3233
3234    #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3235    #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3236    #[tokio::test(flavor = "multi_thread")]
3237    async fn test_simulate_transaction_replace_recent_blockhash(version: TransactionVersion) {
3238        let payer = Keypair::new();
3239        let pk = Pubkey::new_unique();
3240        let lamports = LAMPORTS_PER_SOL;
3241        let setup = TestSetup::new(SurfpoolFullRpc);
3242        let recent_blockhash = setup
3243            .context
3244            .svm_locker
3245            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3246        let block_height = setup
3247            .context
3248            .svm_locker
3249            .with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.block_height);
3250        let bad_blockhash = Hash::new_unique();
3251
3252        let _ = setup
3253            .rpc
3254            .request_airdrop(
3255                Some(setup.context.clone()),
3256                payer.pubkey().to_string(),
3257                2 * lamports,
3258                None,
3259            )
3260            .unwrap();
3261
3262        let mut tx = match version {
3263            TransactionVersion::Legacy(_) => build_legacy_transaction(
3264                &payer.pubkey(),
3265                &[&payer.insecure_clone()],
3266                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3267                &recent_blockhash,
3268            ),
3269            TransactionVersion::Number(0) => build_v0_transaction(
3270                &payer.pubkey(),
3271                &[&payer.insecure_clone()],
3272                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3273                &recent_blockhash,
3274            ),
3275            _ => unimplemented!(),
3276        };
3277        match &mut tx.message {
3278            VersionedMessage::Legacy(msg) => {
3279                msg.recent_blockhash = bad_blockhash;
3280            }
3281            VersionedMessage::V0(msg) => {
3282                msg.recent_blockhash = bad_blockhash;
3283            }
3284        }
3285
3286        let invalid_config = RpcSimulateTransactionConfig {
3287            sig_verify: true,
3288            replace_recent_blockhash: true,
3289            commitment: Some(CommitmentConfig::finalized()),
3290            encoding: None,
3291            accounts: Some(RpcSimulateTransactionAccountsConfig {
3292                encoding: None,
3293                addresses: vec![pk.to_string()],
3294            }),
3295            min_context_slot: None,
3296            inner_instructions: false,
3297        };
3298        let err = setup
3299            .rpc
3300            .simulate_transaction(
3301                Some(setup.context.clone()),
3302                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3303                Some(invalid_config.clone()),
3304            )
3305            .await
3306            .unwrap_err();
3307
3308        assert_eq!(
3309            err.message, "sigVerify may not be used with replaceRecentBlockhash",
3310            "sigVerify should not be allowed to be used with replaceRecentBlockhash"
3311        );
3312
3313        let mut valid_config = invalid_config;
3314        valid_config.sig_verify = false;
3315        let simulation_res = setup
3316            .rpc
3317            .simulate_transaction(
3318                Some(setup.context),
3319                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3320                Some(valid_config),
3321            )
3322            .await
3323            .unwrap();
3324
3325        assert_eq!(
3326            simulation_res.value.err, None,
3327            "Unexpected simulation error"
3328        );
3329        assert_eq!(
3330            simulation_res.value.replacement_blockhash,
3331            Some(RpcBlockhash {
3332                blockhash: recent_blockhash.to_string(),
3333                last_valid_block_height: block_height
3334            }),
3335            "Replacement blockhash should be the latest blockhash"
3336        );
3337    }
3338
3339    #[tokio::test(flavor = "multi_thread")]
3340    async fn test_get_block() {
3341        let setup = TestSetup::new(SurfpoolFullRpc);
3342
3343        // Set up the latest slot so slot 0 is within valid range
3344        setup.context.svm_locker.with_svm_writer(|svm_writer| {
3345            svm_writer.latest_epoch_info.absolute_slot = 10;
3346        });
3347
3348        let res = setup
3349            .rpc
3350            .get_block(Some(setup.context), 0, None)
3351            .await
3352            .unwrap();
3353
3354        // With sparse block storage, empty blocks are reconstructed on-the-fly
3355        assert!(res.is_some(), "Empty blocks should be reconstructed");
3356        let block = res.unwrap();
3357        assert!(
3358            block.signatures.is_none() || block.signatures.as_ref().unwrap().is_empty(),
3359            "Reconstructed empty block should have no signatures"
3360        );
3361    }
3362
3363    #[tokio::test(flavor = "multi_thread")]
3364    async fn test_get_block_time() {
3365        let setup = TestSetup::new(SurfpoolFullRpc);
3366
3367        // Set up the latest slot so slot 0 is within valid range
3368        setup.context.svm_locker.with_svm_writer(|svm_writer| {
3369            svm_writer.latest_epoch_info.absolute_slot = 10;
3370        });
3371
3372        let res = setup
3373            .rpc
3374            .get_block_time(Some(setup.context), 0)
3375            .await
3376            .unwrap();
3377
3378        // With sparse block storage, block time is calculated for any slot within range
3379        assert!(
3380            res.is_some(),
3381            "Block time should be calculated for valid slots"
3382        );
3383    }
3384
3385    #[tokio::test(flavor = "multi_thread")]
3386    async fn test_get_block_respects_confirmed_commitment_visibility() {
3387        let setup = TestSetup::new(SurfpoolFullRpc);
3388
3389        setup.context.svm_locker.with_svm_writer(|svm_writer| {
3390            svm_writer.latest_epoch_info.absolute_slot = 10;
3391        });
3392
3393        let res = setup
3394            .rpc
3395            .get_block(
3396                Some(setup.context),
3397                10,
3398                Some(RpcEncodingConfigWrapper::Current(Some(RpcBlockConfig {
3399                    commitment: Some(CommitmentConfig::confirmed()),
3400                    ..RpcBlockConfig::default()
3401                }))),
3402            )
3403            .await
3404            .unwrap();
3405
3406        assert!(
3407            res.is_none(),
3408            "A confirmed getBlock request should not expose a slot newer than the confirmed slot"
3409        );
3410    }
3411
3412    #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3413    #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3414    #[tokio::test(flavor = "multi_thread")]
3415    async fn test_get_transaction(version: TransactionVersion) {
3416        let payer = Keypair::new();
3417        let pk = Pubkey::new_unique();
3418        let lamports = LAMPORTS_PER_SOL;
3419        let mut setup = TestSetup::new(SurfpoolFullRpc);
3420        let recent_blockhash = setup
3421            .context
3422            .svm_locker
3423            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3424
3425        let _ = setup
3426            .rpc
3427            .request_airdrop(
3428                Some(setup.context.clone()),
3429                payer.pubkey().to_string(),
3430                2 * lamports,
3431                None,
3432            )
3433            .unwrap();
3434
3435        let tx = match version {
3436            TransactionVersion::Legacy(_) => build_legacy_transaction(
3437                &payer.pubkey(),
3438                &[&payer.insecure_clone()],
3439                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3440                &recent_blockhash,
3441            ),
3442            TransactionVersion::Number(0) => build_v0_transaction(
3443                &payer.pubkey(),
3444                &[&payer.insecure_clone()],
3445                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3446                &recent_blockhash,
3447            ),
3448            _ => unimplemented!(),
3449        };
3450
3451        setup.process_txs(vec![tx.clone()]).await;
3452
3453        let res = setup
3454            .rpc
3455            .get_transaction(
3456                Some(setup.context.clone()),
3457                tx.signatures[0].to_string(),
3458                Some(RpcEncodingConfigWrapper::Current(Some(
3459                    get_default_transaction_config(),
3460                ))),
3461            )
3462            .await
3463            .unwrap()
3464            .unwrap();
3465
3466        let instructions = match tx.message.clone() {
3467            VersionedMessage::Legacy(message) => message
3468                .instructions
3469                .iter()
3470                .map(|ix| UiCompiledInstruction::from(ix, Some(1)))
3471                .collect(),
3472            VersionedMessage::V0(message) => message
3473                .instructions
3474                .iter()
3475                .map(|ix| UiCompiledInstruction::from(ix, Some(1)))
3476                .collect(),
3477        };
3478
3479        assert_eq!(
3480            res,
3481            EncodedConfirmedTransactionWithStatusMeta {
3482                slot: 123,
3483                transaction: EncodedTransactionWithStatusMeta {
3484                    transaction: EncodedTransaction::Json(UiTransaction {
3485                        signatures: vec![tx.signatures[0].to_string()],
3486                        message: UiMessage::Raw(UiRawMessage {
3487                            header: MessageHeader {
3488                                num_required_signatures: 1,
3489                                num_readonly_signed_accounts: 0,
3490                                num_readonly_unsigned_accounts: 1
3491                            },
3492                            account_keys: vec![
3493                                payer.pubkey().to_string(),
3494                                pk.to_string(),
3495                                system_program::id().to_string()
3496                            ],
3497                            recent_blockhash: recent_blockhash.to_string(),
3498                            instructions,
3499                            address_table_lookups: match tx.message {
3500                                VersionedMessage::Legacy(_) => None,
3501                                VersionedMessage::V0(_) => Some(vec![]),
3502                            },
3503                        })
3504                    }),
3505                    meta: res.transaction.clone().meta, // Using the same values to avoid reintroducing processing logic errors
3506                    version: Some(version)
3507                },
3508                block_time: res.block_time // Using the same values to avoid flakyness
3509            }
3510        );
3511    }
3512
3513    #[tokio::test(flavor = "multi_thread")]
3514    #[allow(deprecated)]
3515    async fn test_get_first_available_block() {
3516        let setup = TestSetup::new(SurfpoolFullRpc);
3517
3518        {
3519            let mut svm_writer = setup.context.svm_locker.0.write().await;
3520
3521            let previous_chain_tip = svm_writer.chain_tip.clone();
3522
3523            let latest_entries = svm_writer
3524                .inner
3525                .get_sysvar::<solana_sysvar::recent_blockhashes::RecentBlockhashes>(
3526            );
3527            let latest_entry = latest_entries.first().unwrap();
3528
3529            svm_writer.chain_tip = BlockIdentifier::new(
3530                svm_writer.chain_tip.index + 1,
3531                latest_entry.blockhash.to_string().as_str(),
3532            );
3533
3534            let hash = svm_writer.chain_tip.hash.clone();
3535            let block_height = svm_writer.chain_tip.index;
3536            let parent_slot = svm_writer.get_latest_absolute_slot();
3537
3538            svm_writer
3539                .blocks
3540                .store(
3541                    parent_slot,
3542                    BlockHeader {
3543                        hash,
3544                        previous_blockhash: previous_chain_tip.hash.clone(),
3545                        block_time: chrono::Utc::now().timestamp_millis(),
3546                        block_height,
3547                        parent_slot,
3548                        signatures: Vec::new(),
3549                    },
3550                )
3551                .unwrap();
3552        }
3553
3554        let res = setup
3555            .rpc
3556            .get_first_available_block(Some(setup.context))
3557            .unwrap();
3558
3559        assert_eq!(res, 123);
3560    }
3561
3562    #[test]
3563    fn test_get_latest_blockhash() {
3564        let setup = TestSetup::new(SurfpoolFullRpc);
3565
3566        insert_test_blocks(&setup, 100..=150);
3567
3568        // processed commitment
3569        {
3570            let commitment = CommitmentConfig::processed();
3571            let res = setup
3572                .rpc
3573                .get_latest_blockhash(
3574                    Some(setup.context.clone()),
3575                    Some(RpcContextConfig {
3576                        commitment: Some(commitment.clone()),
3577                        ..Default::default()
3578                    }),
3579                )
3580                .unwrap();
3581            let expected_blockhash = setup
3582                .context
3583                .svm_locker
3584                .get_latest_blockhash(&commitment)
3585                .unwrap();
3586
3587            let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3588            let expected_last_valid_block_height =
3589                current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3590
3591            assert_eq!(
3592                res.value.blockhash,
3593                expected_blockhash.to_string(),
3594                "Latest blockhash does not match expected value"
3595            );
3596            assert_eq!(
3597                res.value.last_valid_block_height, expected_last_valid_block_height,
3598                "Last valid block height does not match expected value"
3599            );
3600        }
3601
3602        // confirmed commitment
3603        {
3604            let commitment = CommitmentConfig::confirmed();
3605            let res = setup
3606                .rpc
3607                .get_latest_blockhash(
3608                    Some(setup.context.clone()),
3609                    Some(RpcContextConfig {
3610                        commitment: Some(commitment.clone()),
3611                        ..Default::default()
3612                    }),
3613                )
3614                .unwrap();
3615            let expected_blockhash = setup
3616                .context
3617                .svm_locker
3618                .get_latest_blockhash(&commitment)
3619                .unwrap();
3620
3621            let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3622            let expected_last_valid_block_height =
3623                current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3624
3625            assert_eq!(
3626                res.value.blockhash,
3627                expected_blockhash.to_string(),
3628                "Latest blockhash does not match expected value"
3629            );
3630            assert_eq!(
3631                res.value.last_valid_block_height, expected_last_valid_block_height,
3632                "Last valid block height does not match expected value"
3633            );
3634        }
3635
3636        // confirmed finalized
3637        {
3638            let commitment = CommitmentConfig::finalized();
3639            let res = setup
3640                .rpc
3641                .get_latest_blockhash(
3642                    Some(setup.context.clone()),
3643                    Some(RpcContextConfig {
3644                        commitment: Some(commitment.clone()),
3645                        ..Default::default()
3646                    }),
3647                )
3648                .unwrap();
3649            let expected_blockhash = setup
3650                .context
3651                .svm_locker
3652                .get_latest_blockhash(&commitment)
3653                .unwrap();
3654
3655            let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3656            let expected_last_valid_block_height =
3657                current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3658
3659            assert_eq!(
3660                res.value.blockhash,
3661                expected_blockhash.to_string(),
3662                "Latest blockhash does not match expected value"
3663            );
3664            assert_eq!(
3665                res.value.last_valid_block_height, expected_last_valid_block_height,
3666                "Last valid block height does not match expected value"
3667            );
3668        }
3669    }
3670
3671    #[tokio::test(flavor = "multi_thread")]
3672    async fn test_get_recent_prioritization_fees() {
3673        let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
3674        let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
3675
3676        let recent_blockhash = setup
3677            .context
3678            .svm_locker
3679            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3680
3681        let payer_1 = Keypair::new();
3682        let payer_2 = Keypair::new();
3683        let receiver_pubkey = Pubkey::new_unique();
3684        let random_pubkey = Pubkey::new_unique();
3685
3686        // setup accounts
3687        {
3688            let _ = setup
3689                .rpc
3690                .request_airdrop(
3691                    Some(setup.context.clone()),
3692                    payer_1.pubkey().to_string(),
3693                    2 * LAMPORTS_PER_SOL,
3694                    None,
3695                )
3696                .unwrap();
3697            let _ = setup
3698                .rpc
3699                .request_airdrop(
3700                    Some(setup.context.clone()),
3701                    payer_2.pubkey().to_string(),
3702                    2 * LAMPORTS_PER_SOL,
3703                    None,
3704                )
3705                .unwrap();
3706
3707            setup
3708                .context
3709                .svm_locker
3710                .confirm_current_block(&None)
3711                .await
3712                .unwrap();
3713        }
3714
3715        // send two transactions that include a compute budget instruction
3716        {
3717            let tx_1 = build_legacy_transaction(
3718                &payer_1.pubkey(),
3719                &[&payer_1.insecure_clone()],
3720                &[
3721                    system_instruction::transfer(
3722                        &payer_1.pubkey(),
3723                        &receiver_pubkey,
3724                        LAMPORTS_PER_SOL,
3725                    ),
3726                    ComputeBudgetInstruction::set_compute_unit_price(1000),
3727                ],
3728                &recent_blockhash,
3729            );
3730            let tx_2 = build_legacy_transaction(
3731                &payer_2.pubkey(),
3732                &[&payer_2.insecure_clone()],
3733                &[
3734                    system_instruction::transfer(
3735                        &payer_2.pubkey(),
3736                        &receiver_pubkey,
3737                        LAMPORTS_PER_SOL,
3738                    ),
3739                    ComputeBudgetInstruction::set_compute_unit_price(1002),
3740                ],
3741                &recent_blockhash,
3742            );
3743
3744            send_and_await_transaction(tx_1, setup.clone(), mempool_rx.clone())
3745                .await
3746                .join()
3747                .unwrap();
3748            send_and_await_transaction(tx_2, setup.clone(), mempool_rx)
3749                .await
3750                .join()
3751                .unwrap();
3752            setup
3753                .context
3754                .svm_locker
3755                .confirm_current_block(&None)
3756                .await
3757                .unwrap();
3758        }
3759
3760        // sending the get_recent_prioritization_fees request with an account
3761        // should filter the results to only include fees for that account
3762        let res = setup
3763            .rpc
3764            .get_recent_prioritization_fees(
3765                Some(setup.context.clone()),
3766                Some(vec![payer_1.pubkey().to_string()]),
3767            )
3768            .await
3769            .unwrap();
3770        assert_eq!(res.len(), 1);
3771        assert_eq!(res[0].prioritization_fee, 1000);
3772
3773        // sending the get_recent_prioritization_fees request without an account
3774        // should return all prioritization fees
3775        let res = setup
3776            .rpc
3777            .get_recent_prioritization_fees(Some(setup.context.clone()), None)
3778            .await
3779            .unwrap();
3780        assert_eq!(res.len(), 2);
3781        assert_eq!(res[0].prioritization_fee, 1000);
3782        assert_eq!(res[1].prioritization_fee, 1002);
3783
3784        // sending the get_recent_prioritization_fees request with some random account
3785        // to filter should return no results
3786        let res = setup
3787            .rpc
3788            .get_recent_prioritization_fees(
3789                Some(setup.context.clone()),
3790                Some(vec![random_pubkey.to_string()]),
3791            )
3792            .await
3793            .unwrap();
3794        assert!(
3795            res.is_empty(),
3796            "Expected no prioritization fees for random account"
3797        );
3798    }
3799
3800    #[tokio::test(flavor = "multi_thread")]
3801    async fn test_get_blocks_with_limit() {
3802        let setup = TestSetup::new(SurfpoolFullRpc);
3803
3804        insert_test_blocks(&setup, 100..=110);
3805
3806        let result = setup
3807            .rpc
3808            .get_blocks_with_limit(Some(setup.context.clone()), 100, 5, None)
3809            .await
3810            .unwrap();
3811
3812        assert_eq!(result, vec![100, 101, 102, 103, 104]);
3813    }
3814
3815    #[tokio::test(flavor = "multi_thread")]
3816    async fn test_get_blocks_with_limit_exceeds_available() {
3817        let setup = TestSetup::new(SurfpoolFullRpc);
3818
3819        insert_test_blocks(&setup, 100..=102);
3820
3821        let result = setup
3822            .rpc
3823            .get_blocks_with_limit(Some(setup.context.clone()), 100, 10, None)
3824            .await
3825            .unwrap();
3826
3827        assert_eq!(result, vec![100, 101, 102]);
3828    }
3829
3830    #[tokio::test(flavor = "multi_thread")]
3831    async fn test_get_blocks_with_limit_commitment_levels() {
3832        let setup = TestSetup::new(SurfpoolFullRpc);
3833
3834        insert_test_blocks(&setup, 80..=120);
3835
3836        // Test processed commitment (latest = 120)
3837        let processed_result = setup
3838            .rpc
3839            .get_blocks_with_limit(
3840                Some(setup.context.clone()),
3841                115,
3842                10,
3843                Some(RpcContextConfig {
3844                    commitment: Some(CommitmentConfig {
3845                        commitment: CommitmentLevel::Processed,
3846                    }),
3847                    min_context_slot: None,
3848                }),
3849            )
3850            .await
3851            .unwrap();
3852        assert_eq!(processed_result, vec![115, 116, 117, 118, 119, 120]);
3853
3854        // Test confirmed commitment (latest = 119)
3855        let confirmed_result = setup
3856            .rpc
3857            .get_blocks_with_limit(
3858                Some(setup.context.clone()),
3859                115,
3860                10,
3861                Some(RpcContextConfig {
3862                    commitment: Some(CommitmentConfig {
3863                        commitment: CommitmentLevel::Confirmed,
3864                    }),
3865                    min_context_slot: None,
3866                }),
3867            )
3868            .await
3869            .unwrap();
3870        assert_eq!(confirmed_result, vec![115, 116, 117, 118, 119]);
3871
3872        // Test finalized commitment (latest = 120 - 31 = 89)
3873        let finalized_result = setup
3874            .rpc
3875            .get_blocks_with_limit(
3876                Some(setup.context.clone()),
3877                85,
3878                10,
3879                Some(RpcContextConfig {
3880                    commitment: Some(CommitmentConfig {
3881                        commitment: CommitmentLevel::Finalized,
3882                    }),
3883                    min_context_slot: None,
3884                }),
3885            )
3886            .await
3887            .unwrap();
3888        assert_eq!(finalized_result, vec![85, 86, 87, 88, 89]);
3889    }
3890
3891    #[tokio::test(flavor = "multi_thread")]
3892    async fn test_get_blocks_with_limit_sparse_blocks() {
3893        let setup = TestSetup::new(SurfpoolFullRpc);
3894
3895        insert_test_blocks(
3896            &setup,
3897            vec![100, 103, 105, 107, 109, 112, 115, 118, 120, 122],
3898        );
3899
3900        let result = setup
3901            .rpc
3902            .get_blocks_with_limit(Some(setup.context.clone()), 100, 6, None)
3903            .await
3904            .unwrap();
3905
3906        // With sparse block storage, all slots in range are returned (empty blocks are reconstructed)
3907        assert_eq!(result, vec![100, 101, 102, 103, 104, 105]);
3908    }
3909
3910    #[tokio::test(flavor = "multi_thread")]
3911    async fn test_get_blocks_with_limit_empty_result() {
3912        let setup = TestSetup::new(SurfpoolFullRpc);
3913
3914        {
3915            let mut svm_writer = setup.context.svm_locker.0.write().await;
3916            svm_writer.latest_epoch_info.absolute_slot = 100;
3917            // no blocks added - empty blockchain state (but empty blocks can be reconstructed)
3918        }
3919
3920        // request blocks starting at slot 50 with limit 10
3921        let result = setup
3922            .rpc
3923            .get_blocks_with_limit(Some(setup.context.clone()), 50, 10, None)
3924            .await
3925            .unwrap();
3926
3927        // With sparse block storage, empty blocks within the valid slot range are returned
3928        let expected: Vec<Slot> = (50..60).collect();
3929        assert_eq!(result, expected);
3930    }
3931
3932    #[tokio::test(flavor = "multi_thread")]
3933    async fn test_get_blocks_with_limit_large_limit() {
3934        let setup = TestSetup::new(SurfpoolFullRpc);
3935
3936        insert_test_blocks(&setup, 0..1000);
3937
3938        let result = setup
3939            .rpc
3940            .get_blocks_with_limit(Some(setup.context.clone()), 0, 1000, None)
3941            .await
3942            .unwrap();
3943
3944        assert_eq!(result.len(), 1000);
3945        assert_eq!(result[0], 0);
3946        assert_eq!(result[999], 999);
3947
3948        for i in 1..result.len() {
3949            assert!(
3950                result[i] > result[i - 1],
3951                "Results should be in ascending order"
3952            );
3953        }
3954    }
3955
3956    #[tokio::test(flavor = "multi_thread")]
3957    async fn test_get_blocks_basic() {
3958        // basic functionality with explicit start and end slots
3959        let setup = TestSetup::new(SurfpoolFullRpc);
3960
3961        insert_test_blocks(&setup, 100..=102);
3962
3963        setup.context.svm_locker.with_svm_writer(|svm_writer| {
3964            svm_writer.latest_epoch_info.absolute_slot = 150;
3965        });
3966
3967        let result = setup
3968            .rpc
3969            .get_blocks(
3970                Some(setup.context.clone()),
3971                100,
3972                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(102))),
3973                None,
3974            )
3975            .await
3976            .unwrap();
3977
3978        assert_eq!(result, vec![100, 101, 102]);
3979    }
3980
3981    #[tokio::test(flavor = "multi_thread")]
3982    async fn test_get_blocks_no_end_slot() {
3983        let setup = TestSetup::new(SurfpoolFullRpc);
3984
3985        insert_test_blocks(&setup, 100..=105);
3986
3987        // test without end slot - should return up to committed latest
3988        let result = setup
3989            .rpc
3990            .get_blocks(
3991                Some(setup.context.clone()),
3992                100,
3993                None,
3994                Some(RpcContextConfig {
3995                    commitment: Some(CommitmentConfig {
3996                        commitment: CommitmentLevel::Confirmed,
3997                    }),
3998                    min_context_slot: None,
3999                }),
4000            )
4001            .await
4002            .unwrap();
4003
4004        // with confirmed commitment, latest should be 105 - 1 = 104
4005        assert_eq!(result, vec![100, 101, 102, 103, 104]);
4006    }
4007
4008    #[tokio::test(flavor = "multi_thread")]
4009    async fn test_get_blocks_commitment_levels() {
4010        let setup = TestSetup::new(SurfpoolFullRpc);
4011
4012        insert_test_blocks(&setup, 50..=100);
4013
4014        // processed commitment -> latest = 100
4015        let processed_result = setup
4016            .rpc
4017            .get_blocks(
4018                Some(setup.context.clone()),
4019                95,
4020                None,
4021                Some(RpcContextConfig {
4022                    commitment: Some(CommitmentConfig {
4023                        commitment: CommitmentLevel::Processed,
4024                    }),
4025                    min_context_slot: None,
4026                }),
4027            )
4028            .await
4029            .unwrap();
4030        assert_eq!(processed_result, vec![95, 96, 97, 98, 99, 100]);
4031
4032        // confirmed commitment -> latest = 99
4033        let confirmed_result = setup
4034            .rpc
4035            .get_blocks(
4036                Some(setup.context.clone()),
4037                95,
4038                None,
4039                Some(RpcContextConfig {
4040                    commitment: Some(CommitmentConfig {
4041                        commitment: CommitmentLevel::Confirmed,
4042                    }),
4043                    min_context_slot: None,
4044                }),
4045            )
4046            .await
4047            .unwrap();
4048        assert_eq!(confirmed_result, vec![95, 96, 97, 98, 99]);
4049
4050        // finalized commitment -> latest = 100 - 31(finalization threshold)
4051        let finalized_result = setup
4052            .rpc
4053            .get_blocks(
4054                Some(setup.context.clone()),
4055                65,
4056                None,
4057                Some(RpcContextConfig {
4058                    commitment: Some(CommitmentConfig {
4059                        commitment: CommitmentLevel::Finalized,
4060                    }),
4061                    min_context_slot: None,
4062                }),
4063            )
4064            .await
4065            .unwrap();
4066        assert_eq!(finalized_result, vec![65, 66, 67, 68, 69]);
4067    }
4068
4069    #[tokio::test(flavor = "multi_thread")]
4070    async fn test_get_blocks_min_context_slot() {
4071        let setup = TestSetup::new(SurfpoolFullRpc);
4072
4073        insert_test_blocks(&setup, 100..=110);
4074
4075        // min_context_slot = 105 > 79, so should return MinContextSlotNotReached error
4076        let result = setup
4077            .rpc
4078            .get_blocks(
4079                Some(setup.context.clone()),
4080                100,
4081                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(105))),
4082                Some(RpcContextConfig {
4083                    commitment: Some(CommitmentConfig::finalized()),
4084                    min_context_slot: Some(105),
4085                }),
4086            )
4087            .await;
4088
4089        assert!(result.is_err());
4090
4091        let result = setup
4092            .rpc
4093            .get_blocks(
4094                Some(setup.context.clone()),
4095                105,
4096                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(108))),
4097                Some(RpcContextConfig {
4098                    commitment: Some(CommitmentConfig {
4099                        commitment: CommitmentLevel::Processed,
4100                    }),
4101                    min_context_slot: Some(105),
4102                }),
4103            )
4104            .await
4105            .unwrap();
4106
4107        assert_eq!(result, vec![105, 106, 107, 108]);
4108    }
4109
4110    #[tokio::test(flavor = "multi_thread")]
4111    async fn test_get_blocks_sparse_blocks() {
4112        let setup = TestSetup::new(SurfpoolFullRpc);
4113
4114        // sparse blocks (only some slots have blocks stored, but all can be reconstructed)
4115        insert_test_blocks(&setup, vec![100, 102, 105, 107, 110]);
4116
4117        let result = setup
4118            .rpc
4119            .get_blocks(
4120                Some(setup.context.clone()),
4121                100,
4122                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(115))),
4123                None,
4124            )
4125            .await
4126            .unwrap();
4127
4128        // With sparse block storage, all slots in range up to latest_slot are returned
4129        // insert_test_blocks sets latest_slot to 110 (max of inserted slots)
4130        let expected: Vec<Slot> = (100..=110).collect();
4131        assert_eq!(result, expected);
4132    }
4133
4134    // helper to insert blocks into the SVM at specific slots
4135    fn insert_test_blocks<I>(setup: &TestSetup<SurfpoolFullRpc>, slots: I)
4136    where
4137        I: IntoIterator<Item = u64>,
4138    {
4139        let slots: Vec<u64> = slots.into_iter().collect();
4140        setup.context.svm_locker.with_svm_writer(|svm_writer| {
4141            for slot in slots.iter() {
4142                svm_writer
4143                    .blocks
4144                    .store(
4145                        *slot,
4146                        BlockHeader {
4147                            hash: SyntheticBlockhash::new(*slot).to_string(),
4148                            previous_blockhash: SyntheticBlockhash::new(slot.saturating_sub(1))
4149                                .to_string(),
4150                            block_time: chrono::Utc::now().timestamp_millis(),
4151                            block_height: *slot,
4152                            parent_slot: slot.saturating_sub(1),
4153                            signatures: vec![],
4154                        },
4155                    )
4156                    .unwrap();
4157            }
4158            svm_writer.latest_epoch_info.absolute_slot = slots.into_iter().max().unwrap_or(0);
4159        });
4160    }
4161
4162    #[tokio::test(flavor = "multi_thread")]
4163    async fn test_get_blocks_local_only() {
4164        let setup = TestSetup::new(SurfpoolFullRpc);
4165
4166        insert_test_blocks(&setup, 50..=100);
4167
4168        // request blocks 75-90 (all local)
4169        let result = setup
4170            .rpc
4171            .get_blocks(
4172                Some(setup.context),
4173                75,
4174                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(90))),
4175                None,
4176            )
4177            .await
4178            .unwrap();
4179
4180        let expected: Vec<Slot> = (75..=90).collect();
4181        assert_eq!(result, expected, "Should return all local blocks in range");
4182    }
4183
4184    #[tokio::test(flavor = "multi_thread")]
4185    async fn test_get_blocks_no_remote_context() {
4186        let setup = TestSetup::new(SurfpoolFullRpc);
4187
4188        insert_test_blocks(&setup, 50..=100);
4189
4190        let result = setup
4191            .rpc
4192            .get_blocks(
4193                Some(setup.context),
4194                10,
4195                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(60))),
4196                None,
4197            )
4198            .await
4199            .unwrap();
4200
4201        // With sparse block storage, all slots in range are returned (empty blocks reconstructed)
4202        // local_min is now 0, so slots 10-60 are all within local range
4203        let expected: Vec<Slot> = (10..=60).collect();
4204        assert_eq!(
4205            result, expected,
4206            "Should return all local blocks in range including reconstructed empty blocks"
4207        );
4208    }
4209
4210    #[tokio::test(flavor = "multi_thread")]
4211    async fn test_get_blocks_remote_fetch_below_local_minimum() {
4212        let setup = TestSetup::new(SurfpoolFullRpc);
4213
4214        let local_slots = vec![50, 51, 52, 60, 61, 70, 80, 90, 100];
4215        insert_test_blocks(&setup, local_slots.clone());
4216
4217        // Verify stored blocks minimum (used to be the local_min, but with sparse storage local_min is always 0)
4218        let stored_min = setup
4219            .context
4220            .svm_locker
4221            .with_svm_reader(|svm_reader| svm_reader.blocks.keys().unwrap().into_iter().min());
4222        assert_eq!(
4223            stored_min,
4224            Some(50),
4225            "Stored blocks minimum should be slot 50"
4226        );
4227
4228        // case 1: request blocks 10-30 (entirely "before" stored blocks, but within reconstructible range)
4229        // With sparse storage, local_min is 0, so slots 10-30 are all within local range
4230        let result = setup
4231            .rpc
4232            .get_blocks(
4233                Some(setup.context.clone()),
4234                10,
4235                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(30))),
4236                None,
4237            )
4238            .await
4239            .unwrap();
4240
4241        // With sparse storage, all slots in range up to latest_slot (100) can be reconstructed
4242        let expected: Vec<Slot> = (10..=30).collect();
4243        assert_eq!(
4244            result, expected,
4245            "Should return all slots in range (empty blocks are reconstructed)"
4246        );
4247
4248        // case 2: request blocks 10-60 (spans entire reconstructible range)
4249        let result = setup
4250            .rpc
4251            .get_blocks(
4252                Some(setup.context.clone()),
4253                10,
4254                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(60))),
4255                None,
4256            )
4257            .await
4258            .unwrap();
4259
4260        let expected: Vec<Slot> = (10..=60).collect();
4261        assert_eq!(
4262            result, expected,
4263            "Should return all local slots (empty blocks reconstructed)"
4264        );
4265
4266        // case 3: request blocks 45-55
4267        let result = setup
4268            .rpc
4269            .get_blocks(
4270                Some(setup.context.clone()),
4271                45,
4272                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(55))),
4273                None,
4274            )
4275            .await
4276            .unwrap();
4277
4278        let expected: Vec<Slot> = (45..=55).collect();
4279        assert_eq!(
4280            result, expected,
4281            "Should return all slots in range (empty blocks reconstructed)"
4282        );
4283
4284        // case 4: Request blocks 55-65
4285        let result = setup
4286            .rpc
4287            .get_blocks(
4288                Some(setup.context),
4289                55,
4290                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(65))),
4291                None,
4292            )
4293            .await
4294            .unwrap();
4295
4296        let expected: Vec<Slot> = (55..=65).collect();
4297        assert_eq!(
4298            result, expected,
4299            "Should return all slots in range (empty blocks reconstructed)"
4300        );
4301    }
4302
4303    #[tokio::test(flavor = "multi_thread")]
4304    async fn test_get_blocks_all_below_range_mock_remote() {
4305        let setup = TestSetup::new(SurfpoolFullRpc);
4306
4307        insert_test_blocks(&setup, 100..=150);
4308
4309        setup.context.svm_locker.with_svm_writer(|svm_writer| {
4310            svm_writer.latest_epoch_info.absolute_slot = 200; // set to 200 so all blocks are "committed"
4311        });
4312
4313        let (stored_min, latest_slot) = setup.context.svm_locker.with_svm_reader(|svm_reader| {
4314            let min = svm_reader.blocks.keys().unwrap().into_iter().min();
4315            let latest = svm_reader.get_latest_absolute_slot();
4316            (min, latest)
4317        });
4318        assert_eq!(stored_min, Some(100), "Stored blocks minimum should be 100");
4319        assert_eq!(latest_slot, 200, "Latest slot should be 200");
4320
4321        // Case 1: slots 10-50 - with sparse storage, all slots in range are returned
4322        let result = setup
4323            .rpc
4324            .get_blocks(
4325                Some(setup.context.clone()),
4326                10,
4327                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(50))),
4328                None,
4329            )
4330            .await
4331            .unwrap();
4332
4333        let expected: Vec<Slot> = (10..=50).collect();
4334        assert_eq!(
4335            result, expected,
4336            "Should return all slots (empty blocks reconstructed)"
4337        );
4338
4339        // case 2: Request blocks 5-30
4340        let result = setup
4341            .rpc
4342            .get_blocks(
4343                Some(setup.context.clone()),
4344                5,
4345                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(30))),
4346                None,
4347            )
4348            .await
4349            .unwrap();
4350
4351        let expected: Vec<Slot> = (5..=30).collect();
4352        assert_eq!(
4353            result, expected,
4354            "Should return all slots (empty blocks reconstructed)"
4355        );
4356
4357        // case 3: Request blocks 80-120
4358        let result = setup
4359            .rpc
4360            .get_blocks(
4361                Some(setup.context),
4362                80,
4363                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(120))),
4364                None,
4365            )
4366            .await
4367            .unwrap();
4368
4369        let expected: Vec<Slot> = (80..=120).collect();
4370        assert_eq!(result, expected, "Should return all slots 80-120");
4371    }
4372
4373    #[test]
4374    fn test_get_max_shred_insert_slot() {
4375        let setup = TestSetup::new(SurfpoolFullRpc);
4376
4377        let result = setup
4378            .rpc
4379            .get_max_shred_insert_slot(Some(setup.context.clone()))
4380            .unwrap();
4381        let stake_min_delegation = setup
4382            .rpc
4383            .get_stake_minimum_delegation(Some(setup.context.clone()), None)
4384            .unwrap();
4385
4386        let expected_slot = setup
4387            .context
4388            .svm_locker
4389            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4390
4391        assert_eq!(result, expected_slot);
4392        assert_eq!(stake_min_delegation.context.slot, expected_slot);
4393        assert_eq!(stake_min_delegation.value, 0); // minimum delegation
4394    }
4395
4396    #[test]
4397    fn test_get_max_retransmit_slot() {
4398        let setup = TestSetup::new(SurfpoolFullRpc);
4399
4400        let result = setup
4401            .rpc
4402            .get_max_retransmit_slot(Some(setup.context.clone()))
4403            .unwrap();
4404        let slot = setup
4405            .context
4406            .clone()
4407            .svm_locker
4408            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4409
4410        assert_eq!(result, slot)
4411    }
4412
4413    #[test]
4414    fn test_get_cluster_nodes() {
4415        let setup = TestSetup::new(SurfpoolFullRpc);
4416
4417        let cluster_nodes = setup.rpc.get_cluster_nodes(Some(setup.context)).unwrap();
4418
4419        assert_eq!(
4420            cluster_nodes,
4421            vec![RpcContactInfo {
4422                pubkey: SURFPOOL_IDENTITY_PUBKEY.to_string(),
4423                gossip: Some("127.0.0.1:8001".parse().unwrap()),
4424                tvu: None,
4425                tpu: Some("127.0.0.1:8003".parse().unwrap()),
4426                tpu_quic: Some("127.0.0.1:8004".parse().unwrap()),
4427                tpu_forwards: None,
4428                tpu_forwards_quic: None,
4429                tpu_vote: None,
4430                serve_repair: None,
4431                rpc: Some("127.0.0.1:8899".parse().unwrap()),
4432                pubsub: Some("127.0.0.1:8900".parse().unwrap()),
4433                version: None,
4434                feature_set: None,
4435                shred_version: None,
4436            }]
4437        );
4438    }
4439
4440    #[test]
4441    fn test_get_stake_minimum_delegation_default() {
4442        let setup = TestSetup::new(SurfpoolFullRpc);
4443
4444        let result = setup
4445            .rpc
4446            .get_max_shred_insert_slot(Some(setup.context.clone()))
4447            .unwrap();
4448
4449        let stake_min_delegation = setup
4450            .rpc
4451            .get_stake_minimum_delegation(Some(setup.context.clone()), None)
4452            .unwrap();
4453
4454        let expected_slot = setup
4455            .context
4456            .svm_locker
4457            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4458
4459        assert_eq!(result, expected_slot);
4460        assert_eq!(stake_min_delegation.context.slot, expected_slot);
4461        assert_eq!(stake_min_delegation.value, 0); // minimum delegation
4462    }
4463
4464    #[test]
4465    fn test_get_stake_minimum_delegation_with_finalized_commitment() {
4466        let setup = TestSetup::new(SurfpoolFullRpc);
4467
4468        let config = Some(RpcContextConfig {
4469            commitment: Some(CommitmentConfig {
4470                commitment: CommitmentLevel::Finalized,
4471            }),
4472            min_context_slot: None,
4473        });
4474
4475        let result = setup
4476            .rpc
4477            .get_stake_minimum_delegation(Some(setup.context.clone()), config)
4478            .unwrap();
4479
4480        // Should return finalized slot
4481        let expected_slot = setup.context.svm_locker.with_svm_reader(|svm_reader| {
4482            svm_reader
4483                .get_latest_absolute_slot()
4484                .saturating_sub(FINALIZATION_SLOT_THRESHOLD)
4485        });
4486
4487        assert_eq!(result.context.slot, expected_slot);
4488        assert_eq!(result.value, 0);
4489    }
4490
4491    #[tokio::test(flavor = "multi_thread")]
4492    async fn test_is_blockhash_valid_recent_blockhash() {
4493        let setup = TestSetup::new(SurfpoolFullRpc);
4494
4495        // Get the current recent blockhash from the SVM
4496        let recent_blockhash = setup
4497            .context
4498            .svm_locker
4499            .with_svm_reader(|svm| svm.latest_blockhash());
4500
4501        let result = setup
4502            .rpc
4503            .is_blockhash_valid(
4504                Some(setup.context.clone()),
4505                recent_blockhash.to_string(),
4506                None,
4507            )
4508            .unwrap();
4509
4510        assert_eq!(result.value, true);
4511        assert!(result.context.slot > 0);
4512
4513        // Test with explicit processed commitment
4514        let result_processed = setup
4515            .rpc
4516            .is_blockhash_valid(
4517                Some(setup.context.clone()),
4518                recent_blockhash.to_string(),
4519                Some(RpcContextConfig {
4520                    commitment: Some(CommitmentConfig {
4521                        commitment: CommitmentLevel::Processed,
4522                    }),
4523                    min_context_slot: None,
4524                }),
4525            )
4526            .unwrap();
4527
4528        assert_eq!(result_processed.value, true);
4529    }
4530
4531    #[tokio::test(flavor = "multi_thread")]
4532    async fn test_is_blockhash_valid_invalid_blockhash() {
4533        let setup = TestSetup::new(SurfpoolFullRpc);
4534
4535        let fake_blockhash = Hash::new_from_array([1u8; 32]);
4536
4537        // Non-existent blockhash returns false
4538        let result = setup
4539            .rpc
4540            .is_blockhash_valid(
4541                Some(setup.context.clone()),
4542                fake_blockhash.to_string(),
4543                None,
4544            )
4545            .unwrap();
4546
4547        assert_eq!(result.value, false);
4548
4549        // Test with different commitment levels - should still be false
4550        let result_confirmed = setup
4551            .rpc
4552            .is_blockhash_valid(
4553                Some(setup.context.clone()),
4554                fake_blockhash.to_string(),
4555                Some(RpcContextConfig {
4556                    commitment: Some(CommitmentConfig {
4557                        commitment: CommitmentLevel::Confirmed,
4558                    }),
4559                    min_context_slot: None,
4560                }),
4561            )
4562            .unwrap();
4563
4564        assert_eq!(result_confirmed.value, false);
4565
4566        // Test another fake blockhash to be thorough
4567        let another_fake = Hash::new_from_array([255u8; 32]);
4568        let result2 = setup
4569            .rpc
4570            .is_blockhash_valid(Some(setup.context.clone()), another_fake.to_string(), None)
4571            .unwrap();
4572
4573        assert_eq!(result2.value, false);
4574
4575        let invalid_result = setup.rpc.is_blockhash_valid(
4576            Some(setup.context.clone()),
4577            "invalid-blockhash-format".to_string(),
4578            None,
4579        );
4580
4581        assert!(invalid_result.is_err());
4582
4583        let short_result =
4584            setup
4585                .rpc
4586                .is_blockhash_valid(Some(setup.context.clone()), "123".to_string(), None);
4587        assert!(short_result.is_err());
4588
4589        // Test with invalid base58 characters
4590        let invalid_chars_result =
4591            setup
4592                .rpc
4593                .is_blockhash_valid(Some(setup.context.clone()), "0OIl".to_string(), None);
4594        assert!(invalid_chars_result.is_err());
4595    }
4596
4597    #[tokio::test(flavor = "multi_thread")]
4598    async fn test_is_blockhash_valid_commitment_and_context_slot() {
4599        let setup = TestSetup::new(SurfpoolFullRpc);
4600
4601        // Set up some block history to test commitment levels
4602        insert_test_blocks(&setup, 70..=100);
4603
4604        let recent_blockhash = setup
4605            .context
4606            .svm_locker
4607            .with_svm_reader(|svm| svm.latest_blockhash());
4608
4609        // Test processed commitment (should use latest slot = 100)
4610        let processed_result = setup
4611            .rpc
4612            .is_blockhash_valid(
4613                Some(setup.context.clone()),
4614                recent_blockhash.to_string(),
4615                Some(RpcContextConfig {
4616                    commitment: Some(CommitmentConfig {
4617                        commitment: CommitmentLevel::Processed,
4618                    }),
4619                    min_context_slot: None,
4620                }),
4621            )
4622            .unwrap();
4623
4624        assert_eq!(processed_result.value, true);
4625        assert_eq!(processed_result.context.slot, 100);
4626
4627        // Test confirmed commitment (should use slot = 99)
4628        let confirmed_result = setup
4629            .rpc
4630            .is_blockhash_valid(
4631                Some(setup.context.clone()),
4632                recent_blockhash.to_string(),
4633                Some(RpcContextConfig {
4634                    commitment: Some(CommitmentConfig {
4635                        commitment: CommitmentLevel::Confirmed,
4636                    }),
4637                    min_context_slot: None,
4638                }),
4639            )
4640            .unwrap();
4641
4642        assert_eq!(confirmed_result.value, true);
4643        assert_eq!(confirmed_result.context.slot, 99);
4644
4645        // Test finalized commitment (should use slot = 100 - 31 = 69)
4646        let finalized_result = setup
4647            .rpc
4648            .is_blockhash_valid(
4649                Some(setup.context.clone()),
4650                recent_blockhash.to_string(),
4651                Some(RpcContextConfig {
4652                    commitment: Some(CommitmentConfig {
4653                        commitment: CommitmentLevel::Finalized,
4654                    }),
4655                    min_context_slot: None,
4656                }),
4657            )
4658            .unwrap();
4659
4660        assert_eq!(finalized_result.value, true);
4661        assert_eq!(finalized_result.context.slot, 69);
4662
4663        // Test min_context_slot validation - should succeed when slot is high enough
4664        let min_context_success = setup
4665            .rpc
4666            .is_blockhash_valid(
4667                Some(setup.context.clone()),
4668                recent_blockhash.to_string(),
4669                Some(RpcContextConfig {
4670                    commitment: Some(CommitmentConfig {
4671                        commitment: CommitmentLevel::Processed,
4672                    }),
4673                    min_context_slot: Some(95),
4674                }),
4675            )
4676            .unwrap();
4677
4678        assert_eq!(min_context_success.value, true);
4679
4680        // Test min_context_slot validation - should fail when slot is too low
4681        let min_context_failure = setup.rpc.is_blockhash_valid(
4682            Some(setup.context.clone()),
4683            recent_blockhash.to_string(),
4684            Some(RpcContextConfig {
4685                commitment: Some(CommitmentConfig {
4686                    commitment: CommitmentLevel::Finalized,
4687                }),
4688                min_context_slot: Some(80),
4689            }),
4690        );
4691
4692        assert!(min_context_failure.is_err());
4693    }
4694
4695    #[ignore = "requires-network"]
4696    #[tokio::test(flavor = "multi_thread")]
4697    async fn test_minimum_ledger_slot_from_remote() {
4698        // Forwarding to remote mainnet
4699        let remote_client = SurfnetRemoteClient::new("https://api.mainnet-beta.solana.com");
4700        let mut setup = TestSetup::new(SurfpoolFullRpc);
4701        setup.context.remote_rpc_client = Some(remote_client);
4702
4703        let result = setup
4704            .rpc
4705            .minimum_ledger_slot(Some(setup.context))
4706            .await
4707            .unwrap();
4708
4709        assert!(
4710            result > 0,
4711            "Mainnet should return a valid minimum ledger slot > 0"
4712        );
4713        println!("Mainnet minimum ledger slot: {}", result);
4714    }
4715
4716    #[tokio::test(flavor = "multi_thread")]
4717    async fn test_minimum_ledger_slot_missing_context_fails() {
4718        // fail gracefully when called without metadata context
4719        let setup = TestSetup::new(SurfpoolFullRpc);
4720
4721        let result = setup.rpc.minimum_ledger_slot(None).await;
4722
4723        assert!(
4724            result.is_err(),
4725            "Should fail when called without metadata context"
4726        );
4727    }
4728
4729    #[tokio::test(flavor = "multi_thread")]
4730    async fn test_minimum_ledger_slot_finds_minimum() {
4731        // With sparse block storage, minimum ledger slot is always 0 for local surfnets
4732        let setup = TestSetup::new(SurfpoolFullRpc);
4733
4734        insert_test_blocks(&setup, vec![500, 100, 1000, 50, 750]);
4735
4736        let result = setup
4737            .rpc
4738            .minimum_ledger_slot(Some(setup.context))
4739            .await
4740            .unwrap();
4741
4742        // With sparse block storage, get_first_local_slot() returns 0 since all
4743        // blocks from slot 0 can be reconstructed on-the-fly
4744        assert_eq!(
4745            result, 0,
4746            "Should return 0 since empty blocks can be reconstructed from slot 0"
4747        );
4748    }
4749
4750    #[tokio::test(flavor = "multi_thread")]
4751    async fn test_get_inflation_reward() {
4752        let setup = TestSetup::new(SurfpoolFullRpc);
4753
4754        let (epoch, effective_slot) =
4755            setup
4756                .context
4757                .clone()
4758                .svm_locker
4759                .with_svm_reader(|svm_reader| {
4760                    (
4761                        svm_reader.latest_epoch_info().epoch,
4762                        svm_reader.get_latest_absolute_slot(),
4763                    )
4764                });
4765
4766        let result = setup
4767            .rpc
4768            .get_inflation_reward(
4769                Some(setup.context),
4770                vec![Pubkey::new_unique().to_string()],
4771                None,
4772            )
4773            .await
4774            .unwrap();
4775
4776        assert_eq!(
4777            result[0],
4778            Some(RpcInflationReward {
4779                epoch,
4780                effective_slot,
4781                amount: 0,
4782                post_balance: 0,
4783                commission: None
4784            })
4785        )
4786    }
4787
4788    /// tests for skip_sig_verify feature
4789    mod test_skip_sig_verify {
4790        use solana_client::rpc_config::RpcSendTransactionConfig;
4791        use solana_signature::Signature;
4792
4793        use super::*;
4794
4795        fn build_transaction_with_invalid_signature(
4796            payer: &Keypair,
4797            recipient: &Pubkey,
4798            recent_blockhash: &Hash,
4799        ) -> VersionedTransaction {
4800            let msg = VersionedMessage::Legacy(LegacyMessage::new_with_blockhash(
4801                &[system_instruction::transfer(
4802                    &payer.pubkey(),
4803                    recipient,
4804                    LAMPORTS_PER_SOL,
4805                )],
4806                Some(&payer.pubkey()),
4807                recent_blockhash,
4808            ));
4809
4810            VersionedTransaction {
4811                signatures: vec![Signature::new_unique()],
4812                message: msg,
4813            }
4814        }
4815
4816        #[tokio::test(flavor = "multi_thread")]
4817        async fn test_send_transaction_with_skip_sig_verify_succeeds() {
4818            let payer = Keypair::new();
4819            let recipient = Pubkey::new_unique();
4820            let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
4821            let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
4822            let recent_blockhash = setup
4823                .context
4824                .svm_locker
4825                .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
4826
4827            let _ = setup
4828                .context
4829                .svm_locker
4830                .0
4831                .write()
4832                .await
4833                .airdrop(&payer.pubkey(), 2 * LAMPORTS_PER_SOL);
4834
4835            let tx =
4836                build_transaction_with_invalid_signature(&payer, &recipient, &recent_blockhash);
4837            let tx_encoded = bs58::encode(bincode::serialize(&tx).unwrap()).into_string();
4838
4839            let config = SurfpoolRpcSendTransactionConfig {
4840                base: RpcSendTransactionConfig::default(),
4841                skip_sig_verify: Some(true),
4842            };
4843
4844            let setup_clone = setup.clone();
4845            let handle = hiro_system_kit::thread_named("send_tx_skip_verify")
4846                .spawn(move || {
4847                    setup_clone.rpc.send_transaction(
4848                        Some(setup_clone.context),
4849                        tx_encoded,
4850                        Some(config),
4851                    )
4852                })
4853                .unwrap();
4854
4855            loop {
4856                match mempool_rx.recv() {
4857                    Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => {
4858                        let mut writer = setup.context.svm_locker.0.write().await;
4859                        let slot = writer.get_latest_absolute_slot();
4860                        writer.transactions_queued_for_confirmation.push_back((
4861                            tx.clone(),
4862                            status_tx.clone(),
4863                            None,
4864                        ));
4865                        let sig = tx.signatures[0];
4866                        let tx_with_status_meta = TransactionWithStatusMeta {
4867                            slot,
4868                            transaction: tx,
4869                            ..Default::default()
4870                        };
4871                        let mutated_accounts = std::collections::HashSet::new();
4872                        writer
4873                            .transactions
4874                            .store(
4875                                sig.to_string(),
4876                                SurfnetTransactionStatus::processed(
4877                                    tx_with_status_meta,
4878                                    mutated_accounts,
4879                                ),
4880                            )
4881                            .unwrap();
4882                        status_tx
4883                            .send(TransactionStatusEvent::Success(
4884                                TransactionConfirmationStatus::Processed,
4885                            ))
4886                            .unwrap();
4887                        break;
4888                    }
4889                    _ => continue,
4890                }
4891            }
4892
4893            let result = handle.join().unwrap();
4894            assert!(
4895                result.is_ok(),
4896                "Transaction with skip_sig_verify=true should succeed: {:?}",
4897                result
4898            );
4899        }
4900
4901        #[test]
4902        fn test_surfpool_rpc_send_transaction_config_json_serialization() {
4903            // Test that the config serializes correctly with serde flatten
4904            let config = SurfpoolRpcSendTransactionConfig {
4905                base: RpcSendTransactionConfig {
4906                    skip_preflight: true,
4907                    ..Default::default()
4908                },
4909                skip_sig_verify: Some(true),
4910            };
4911
4912            let json = serde_json::to_string(&config).unwrap();
4913            assert!(json.contains("skipSigVerify"));
4914            assert!(json.contains("skipPreflight"));
4915
4916            // Verify it can be deserialized back
4917            let parsed: SurfpoolRpcSendTransactionConfig = serde_json::from_str(&json).unwrap();
4918            assert_eq!(parsed.skip_sig_verify, Some(true));
4919            assert!(parsed.base.skip_preflight);
4920        }
4921
4922        #[test]
4923        fn test_surfpool_rpc_send_transaction_config_backwards_compatible() {
4924            // Test that a standard Solana RPC config can be parsed (skip_sig_verify absent)
4925            let json = r#"{"skipPreflight": true}"#;
4926            let parsed: SurfpoolRpcSendTransactionConfig = serde_json::from_str(json).unwrap();
4927            assert!(parsed.base.skip_preflight);
4928            assert!(
4929                parsed.skip_sig_verify.is_none(),
4930                "skip_sig_verify should be None when not provided"
4931            );
4932        }
4933
4934        #[test]
4935        fn test_surfpool_rpc_send_transaction_config_defaults() {
4936            let config = SurfpoolRpcSendTransactionConfig::default();
4937            assert!(
4938                config.skip_sig_verify.is_none(),
4939                "skip_sig_verify should default to None"
4940            );
4941            assert!(
4942                !config.base.skip_preflight,
4943                "skip_preflight should default to false"
4944            );
4945        }
4946
4947        #[test]
4948        fn test_surfpool_rpc_send_transaction_config_with_skip_sig_verify() {
4949            let config = SurfpoolRpcSendTransactionConfig {
4950                base: RpcSendTransactionConfig::default(),
4951                skip_sig_verify: Some(true),
4952            };
4953            assert_eq!(config.skip_sig_verify, Some(true));
4954
4955            let config_false = SurfpoolRpcSendTransactionConfig {
4956                base: RpcSendTransactionConfig::default(),
4957                skip_sig_verify: Some(false),
4958            };
4959            assert_eq!(config_false.skip_sig_verify, Some(false));
4960        }
4961    }
4962}