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