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                            commission_bps: None,
1405                            effective_slot: svm_reader.get_latest_absolute_slot(),
1406                            epoch: svm_reader.latest_epoch_info().epoch,
1407                            post_balance: 0,
1408                        })
1409                    })
1410                    .collect()
1411            })
1412            .map_err(Into::into)
1413        })
1414    }
1415
1416    fn get_cluster_nodes(&self, meta: Self::Metadata) -> Result<Vec<RpcContactInfo>> {
1417        let (gossip, tpu, tpu_quic, rpc, pubsub) = if let Some(ctx) = meta {
1418            let config = ctx.rpc_config;
1419            let to_socket = |port: u16| -> Option<std::net::SocketAddr> {
1420                format!("{}:{}", config.bind_host, port).parse().ok()
1421            };
1422            (
1423                to_socket(config.gossip_port),
1424                to_socket(config.tpu_port),
1425                to_socket(config.tpu_quic_port),
1426                to_socket(config.bind_port),
1427                to_socket(config.ws_port),
1428            )
1429        } else {
1430            (None, None, None, None, None)
1431        };
1432
1433        Ok(vec![RpcContactInfo {
1434            pubkey: SURFPOOL_IDENTITY_PUBKEY.to_string(),
1435            client_id: None,
1436            gossip,
1437            tvu: None,
1438            tpu,
1439            tpu_quic,
1440            tpu_forwards: None,
1441            tpu_forwards_quic: None,
1442            tpu_vote: None,
1443            serve_repair: None,
1444            rpc,
1445            pubsub,
1446            version: None,
1447            feature_set: None,
1448            shred_version: None,
1449        }])
1450    }
1451
1452    fn get_recent_performance_samples(
1453        &self,
1454        meta: Self::Metadata,
1455        limit: Option<usize>,
1456    ) -> Result<Vec<RpcPerfSample>> {
1457        let limit = limit.unwrap_or(720);
1458        if limit > 720 {
1459            return Err(Error::invalid_params("Invalid limit; max 720"));
1460        }
1461
1462        meta.with_svm_reader(|svm_reader| {
1463            svm_reader
1464                .perf_samples
1465                .iter()
1466                .take(limit)
1467                .cloned()
1468                .collect::<Vec<_>>()
1469        })
1470        .map_err(Into::into)
1471    }
1472
1473    fn get_signature_statuses(
1474        &self,
1475        meta: Self::Metadata,
1476        signature_strs: Vec<String>,
1477        _config: Option<RpcSignatureStatusConfig>,
1478    ) -> BoxFuture<Result<RpcResponse<Vec<Option<TransactionStatus>>>>> {
1479        let signatures = match signature_strs
1480            .iter()
1481            .map(|s| {
1482                Signature::from_str(s)
1483                    .map_err(|e| SurfpoolError::invalid_signature(s, e.to_string()))
1484            })
1485            .collect::<std::result::Result<Vec<Signature>, SurfpoolError>>()
1486        {
1487            Ok(sigs) => sigs,
1488            Err(e) => return e.into(),
1489        };
1490
1491        let SurfnetRpcContext {
1492            svm_locker,
1493            remote_ctx,
1494        } = match meta.get_rpc_context(()) {
1495            Ok(res) => res,
1496            Err(e) => return e.into(),
1497        };
1498        let remote_client = remote_ctx.map(|(r, _)| r);
1499
1500        Box::pin(async move {
1501            // Capture the context slot once at the beginning to ensure consistency
1502            // across all signature lookups, even if the slot advances during the loop
1503            let context_slot = svm_locker.get_latest_absolute_slot();
1504
1505            let mut responses = Vec::with_capacity(signatures.len());
1506            for signature in signatures.into_iter() {
1507                let res = svm_locker
1508                    .get_transaction(&remote_client, &signature, get_default_transaction_config())
1509                    .await?;
1510
1511                let mut status = res.map_some_transaction_status();
1512                if let Some(confirmation_status) =
1513                    status.as_ref().and_then(|s| s.confirmation_status.as_ref())
1514                {
1515                    if confirmation_status.eq(&TransactionConfirmationStatus::Processed) {
1516                        // If the transaction is only processed, we cannot be sure it won't be dropped
1517                        // before being confirmed. So we return None in this case to match the behavior
1518                        // of a real Solana node.
1519                        status = None;
1520                    }
1521                }
1522                responses.push(status);
1523            }
1524            Ok(RpcResponse {
1525                context: RpcResponseContext::new(context_slot),
1526                value: responses,
1527            })
1528        })
1529    }
1530
1531    fn get_max_retransmit_slot(&self, meta: Self::Metadata) -> Result<Slot> {
1532        meta.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
1533            .map_err(Into::into)
1534    }
1535
1536    fn get_max_shred_insert_slot(&self, meta: Self::Metadata) -> Result<Slot> {
1537        meta.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
1538            .map_err(Into::into)
1539    }
1540
1541    fn request_airdrop(
1542        &self,
1543        meta: Self::Metadata,
1544        pubkey_str: String,
1545        lamports: u64,
1546        _config: Option<RpcRequestAirdropConfig>,
1547    ) -> Result<String> {
1548        let pubkey = verify_pubkey(&pubkey_str)?;
1549        let Some(ctx) = meta else {
1550            return Err(SurfpoolError::missing_context().into());
1551        };
1552        let svm_locker = ctx.svm_locker;
1553        let res = svm_locker
1554            .airdrop(&pubkey, lamports)
1555            .map_err(Error::from)?
1556            .map_err(|err| Error::invalid_params(format!("failed to send transaction: {err:?}")))?;
1557        let _ = ctx
1558            .simnet_commands_tx
1559            .try_send(SimnetCommand::AirdropProcessed);
1560
1561        Ok(res.signature.to_string())
1562    }
1563
1564    fn send_transaction(
1565        &self,
1566        meta: Self::Metadata,
1567        data: String,
1568        config: Option<SurfpoolRpcSendTransactionConfig>,
1569    ) -> Result<String> {
1570        #[cfg(feature = "prometheus")]
1571        let rpc_start = std::time::Instant::now();
1572
1573        let config = config.unwrap_or_default();
1574        let unsanitized_tx = decode_rpc_versioned_transaction(data, config.base.encoding)?;
1575        let signatures = unsanitized_tx.signatures.clone();
1576        let signature = signatures[0];
1577        // Clone the message before moving the transaction, as we'll need it for error reporting
1578        let tx_message = unsanitized_tx.message.clone();
1579
1580        let Some(ctx) = meta else {
1581            return Err(RpcCustomError::NodeUnhealthy {
1582                num_slots_behind: None,
1583            }
1584            .into());
1585        };
1586
1587        let (status_update_tx, status_update_rx) = crossbeam_channel::bounded(1);
1588        ctx.simnet_commands_tx
1589            .send(SimnetCommand::ProcessTransaction(
1590                ctx.id,
1591                unsanitized_tx,
1592                status_update_tx,
1593                config.base.skip_preflight,
1594                config.skip_sig_verify,
1595            ))
1596            .map_err(|_| RpcCustomError::NodeUnhealthy {
1597                num_slots_behind: None,
1598            })?;
1599
1600        match status_update_rx.recv() {
1601            Ok(TransactionStatusEvent::SimulationFailure((error, metadata))) => {
1602                #[cfg(feature = "prometheus")]
1603                if let Some(m) = crate::telemetry::metrics() {
1604                    m.record_transaction(false, rpc_start.elapsed().as_millis() as u64);
1605                    m.record_rpc_request("sendTransaction", rpc_start.elapsed().as_millis() as u64);
1606                }
1607                return Err(Error {
1608                    data: Some(
1609                        serde_json::to_value(get_simulate_transaction_result(
1610                            surfpool_tx_metadata_to_litesvm_tx_metadata(&metadata),
1611                            None,
1612                            Some(error.clone()),
1613                            None,
1614                            false,
1615                            &tx_message,
1616                            None, // No loaded addresses available in error reporting context
1617                            None,
1618                        ))
1619                        .map_err(|e| {
1620                            Error::invalid_params(format!(
1621                                "Failed to serialize simulation result: {e}"
1622                            ))
1623                        })?,
1624                    ),
1625                    message: format!(
1626                        "Transaction simulation failed: {}{}",
1627                        error,
1628                        if metadata.logs.is_empty() {
1629                            String::new()
1630                        } else {
1631                            format!(
1632                                ": {} log messages:\n{}",
1633                                metadata.logs.len(),
1634                                metadata.logs.iter().map(|l| l.to_string()).join("\n")
1635                            )
1636                        }
1637                    ),
1638                    code: jsonrpc_core::ErrorCode::ServerError(-32002),
1639                });
1640            }
1641            Ok(TransactionStatusEvent::ExecutionFailure(_)) => {
1642                #[cfg(feature = "prometheus")]
1643                if let Some(m) = crate::telemetry::metrics() {
1644                    m.record_transaction(false, rpc_start.elapsed().as_millis() as u64);
1645                }
1646            }
1647            Ok(TransactionStatusEvent::VerificationFailure(signature)) => {
1648                #[cfg(feature = "prometheus")]
1649                if let Some(m) = crate::telemetry::metrics() {
1650                    m.record_transaction(false, rpc_start.elapsed().as_millis() as u64);
1651                    m.record_rpc_request("sendTransaction", rpc_start.elapsed().as_millis() as u64);
1652                }
1653                return Err(Error {
1654                    data: None,
1655                    message: format!("Transaction verification failed for transaction {signature}"),
1656                    code: jsonrpc_core::ErrorCode::ServerError(-32002),
1657                });
1658            }
1659            Err(e) => {
1660                #[cfg(feature = "prometheus")]
1661                if let Some(m) = crate::telemetry::metrics() {
1662                    m.record_transaction(false, rpc_start.elapsed().as_millis() as u64);
1663                    m.record_rpc_request("sendTransaction", rpc_start.elapsed().as_millis() as u64);
1664                }
1665                return Err(Error {
1666                    data: None,
1667                    message: format!("Failed to process transaction: {e}"),
1668                    code: jsonrpc_core::ErrorCode::ServerError(-32002),
1669                });
1670            }
1671            Ok(TransactionStatusEvent::Success(_)) =>
1672            {
1673                #[cfg(feature = "prometheus")]
1674                if let Some(m) = crate::telemetry::metrics() {
1675                    m.record_transaction(true, rpc_start.elapsed().as_millis() as u64);
1676                }
1677            }
1678        }
1679
1680        #[cfg(feature = "prometheus")]
1681        if let Some(m) = crate::telemetry::metrics() {
1682            m.record_rpc_request("sendTransaction", rpc_start.elapsed().as_millis() as u64);
1683        }
1684        Ok(signature.to_string())
1685    }
1686
1687    fn simulate_transaction(
1688        &self,
1689        meta: Self::Metadata,
1690        data: String,
1691        config: Option<RpcSimulateTransactionConfig>,
1692    ) -> BoxFuture<Result<RpcResponse<RpcSimulateTransactionResult>>> {
1693        let config = config.unwrap_or_default();
1694
1695        if config.sig_verify && config.replace_recent_blockhash {
1696            return SurfpoolError::sig_verify_replace_recent_blockhash_collision().into();
1697        }
1698
1699        let mut unsanitized_tx = match decode_rpc_versioned_transaction(data, config.encoding) {
1700            Ok(tx) => tx,
1701            Err(e) => return Box::pin(async move { Err(e) }),
1702        };
1703
1704        let SurfnetRpcContext {
1705            svm_locker,
1706            remote_ctx,
1707        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1708            Ok(res) => res,
1709            Err(e) => return e.into(),
1710        };
1711
1712        Box::pin(async move {
1713            let loaded_addresses = svm_locker
1714                .get_loaded_addresses(&remote_ctx, &unsanitized_tx.message)
1715                .await?;
1716            let transaction_pubkeys = svm_locker.get_pubkeys_from_message(
1717                &unsanitized_tx.message,
1718                loaded_addresses.as_ref().map(|l| l.all_loaded_addresses()),
1719            );
1720
1721            let SvmAccessContext {
1722                slot,
1723                inner: account_updates,
1724                latest_blockhash,
1725                latest_epoch_info,
1726            } = svm_locker
1727                .get_multiple_accounts(&remote_ctx, &transaction_pubkeys, None)
1728                .await?;
1729
1730            let mut seen_accounts = std::collections::HashSet::new();
1731            let mut loaded_accounts_data_size: u64 = 0;
1732
1733            let mut track_accounts_data_size =
1734                |account_update: &GetAccountResult| match account_update {
1735                    GetAccountResult::FoundAccount(pubkey, account, _) => {
1736                        if seen_accounts.insert(*pubkey) {
1737                            loaded_accounts_data_size += account.data.len() as u64;
1738                        }
1739                    }
1740                    // According to SIMD 0186, program data is tracked as well as program accounts
1741                    GetAccountResult::FoundProgramAccount(
1742                        (pubkey, account),
1743                        (pd_pubkey, pd_account),
1744                    ) => {
1745                        if seen_accounts.insert(*pubkey) {
1746                            loaded_accounts_data_size += account.data.len() as u64;
1747                        }
1748                        if let Some(pd) = pd_account {
1749                            if seen_accounts.insert(*pd_pubkey) {
1750                                loaded_accounts_data_size += pd.data.len() as u64;
1751                            }
1752                        }
1753                    }
1754                    GetAccountResult::FoundTokenAccount(
1755                        (pubkey, account),
1756                        (td_pubkey, td_account),
1757                    ) => {
1758                        if seen_accounts.insert(*pubkey) {
1759                            loaded_accounts_data_size += account.data.len() as u64;
1760                        }
1761                        if let Some(td) = td_account {
1762                            let td_key_in_tx_pubkeys =
1763                                transaction_pubkeys.iter().find(|k| **k == *td_pubkey);
1764                            // Only count token data accounts that are explicitly loaded by the transaction
1765                            if td_key_in_tx_pubkeys.is_some() && seen_accounts.insert(*td_pubkey) {
1766                                loaded_accounts_data_size += td.data.len() as u64;
1767                            }
1768                        }
1769                    }
1770                    GetAccountResult::None(_) => {}
1771                };
1772
1773            for res in account_updates.iter() {
1774                track_accounts_data_size(res);
1775            }
1776
1777            svm_locker.write_multiple_account_updates(&account_updates);
1778
1779            // Convert TransactionLoadedAddresses to LoadedAddresses before it gets consumed
1780            let loaded_addresses_data = loaded_addresses.as_ref().map(|la| la.loaded_addresses());
1781
1782            if let Some(alt_pubkeys) = loaded_addresses.map(|l| l.alt_addresses()) {
1783                let alt_updates = svm_locker
1784                    .get_multiple_accounts(&remote_ctx, &alt_pubkeys, None)
1785                    .await?
1786                    .inner;
1787                for res in alt_updates.iter() {
1788                    track_accounts_data_size(res);
1789                }
1790                svm_locker.write_multiple_account_updates(&alt_updates);
1791            }
1792
1793            let replacement_blockhash = if config.replace_recent_blockhash {
1794                unsanitized_tx
1795                    .message
1796                    .set_recent_blockhash(latest_blockhash);
1797                Some(RpcBlockhash {
1798                    blockhash: latest_blockhash.to_string(),
1799                    last_valid_block_height: latest_epoch_info.block_height,
1800                })
1801            } else {
1802                None
1803            };
1804
1805            // Clone the message before moving the transaction for later use in result formatting
1806            let tx_message = unsanitized_tx.message.clone();
1807
1808            let value = match svm_locker.simulate_transaction(unsanitized_tx, config.sig_verify) {
1809                Ok(tx_info) => {
1810                    let mut accounts = None;
1811                    if let Some(observed_accounts) = config.accounts {
1812                        let mut ui_accounts = vec![];
1813                        for observed_pubkey in observed_accounts.addresses.iter() {
1814                            let mut ui_account = None;
1815                            for (updated_pubkey, account) in tx_info.post_accounts.iter() {
1816                                if observed_pubkey.eq(&updated_pubkey.to_string()) {
1817                                    ui_account = Some(
1818                                        svm_locker
1819                                            .account_to_rpc_keyed_account(
1820                                                updated_pubkey,
1821                                                account,
1822                                                &RpcAccountInfoConfig::default(),
1823                                                None,
1824                                            )
1825                                            .account,
1826                                    );
1827                                }
1828                            }
1829                            ui_accounts.push(ui_account);
1830                        }
1831                        accounts = Some(ui_accounts);
1832                    }
1833                    get_simulate_transaction_result(
1834                        tx_info.meta,
1835                        accounts,
1836                        None,
1837                        replacement_blockhash,
1838                        config.inner_instructions,
1839                        &tx_message,
1840                        loaded_addresses_data.as_ref(),
1841                        Some(loaded_accounts_data_size as u32),
1842                    )
1843                }
1844                Err(tx_info) => get_simulate_transaction_result(
1845                    tx_info.meta,
1846                    None,
1847                    Some(tx_info.err),
1848                    replacement_blockhash,
1849                    config.inner_instructions,
1850                    &tx_message,
1851                    loaded_addresses_data.as_ref(),
1852                    Some(loaded_accounts_data_size as u32),
1853                ),
1854            };
1855
1856            Ok(RpcResponse {
1857                context: RpcResponseContext::new(slot),
1858                value,
1859            })
1860        })
1861    }
1862
1863    fn minimum_ledger_slot(&self, meta: Self::Metadata) -> BoxFuture<Result<Slot>> {
1864        let SurfnetRpcContext {
1865            svm_locker,
1866            remote_ctx,
1867        } = match meta.get_rpc_context(()) {
1868            Ok(res) => res,
1869            Err(e) => return e.into(),
1870        };
1871
1872        Box::pin(async move {
1873            // Forward to remote if available, otherwise return genesis_slot for local chains
1874            // With sparse block storage, all slots from genesis_slot onwards are valid
1875            if let Some((remote_client, _)) = remote_ctx {
1876                remote_client
1877                    .client
1878                    .minimum_ledger_slot()
1879                    .await
1880                    .map_err(|e| SurfpoolError::client_error(e).into())
1881            } else {
1882                Ok(svm_locker.with_svm_reader(|svm| svm.genesis_slot))
1883            }
1884        })
1885    }
1886
1887    fn get_block(
1888        &self,
1889        meta: Self::Metadata,
1890        slot: Slot,
1891        config: Option<RpcEncodingConfigWrapper<RpcBlockConfig>>,
1892    ) -> BoxFuture<Result<Option<UiConfirmedBlock>>> {
1893        let config = config.map(|c| c.convert_to_current()).unwrap_or_default();
1894
1895        let SurfnetRpcContext {
1896            svm_locker,
1897            remote_ctx,
1898        } = match meta.get_rpc_context(config.commitment) {
1899            Ok(res) => res,
1900            Err(e) => return e.into(),
1901        };
1902
1903        Box::pin(async move {
1904            let remote_client = remote_ctx.as_ref().map(|(client, _)| client.clone());
1905            let result = svm_locker.get_block(&remote_client, &slot, &config).await;
1906            Ok(result?.inner)
1907        })
1908    }
1909
1910    fn get_block_time(
1911        &self,
1912        meta: Self::Metadata,
1913        slot: Slot,
1914    ) -> BoxFuture<Result<Option<UnixTimestamp>>> {
1915        let svm_locker = match meta.get_svm_locker() {
1916            Ok(locker) => locker,
1917            Err(e) => return e.into(),
1918        };
1919
1920        Box::pin(async move {
1921            let block_time = svm_locker.with_svm_reader(|svm_reader| {
1922                Ok::<_, jsonrpc_core::Error>(match svm_reader.blocks.get(&slot)? {
1923                    Some(block) => Some(block.block_time),
1924                    None => {
1925                        // With sparse block storage, calculate time for missing blocks
1926                        if svm_reader.is_slot_in_valid_range(slot) {
1927                            let time_ms = svm_reader.calculate_block_time_for_slot(slot);
1928                            Some((time_ms / 1_000) as i64)
1929                        } else {
1930                            None
1931                        }
1932                    }
1933                })
1934            })?;
1935            Ok(block_time)
1936        })
1937    }
1938
1939    fn get_blocks(
1940        &self,
1941        meta: Self::Metadata,
1942        start_slot: Slot,
1943        wrapper: Option<RpcBlocksConfigWrapper>,
1944        config: Option<RpcContextConfig>,
1945    ) -> BoxFuture<Result<Vec<Slot>>> {
1946        let end_slot = match wrapper {
1947            Some(RpcBlocksConfigWrapper::EndSlotOnly(end_slot)) => end_slot,
1948            Some(RpcBlocksConfigWrapper::ConfigOnly(_)) => None,
1949            None => None,
1950        };
1951
1952        let config = config.unwrap_or_default();
1953        // get blocks should default to processed rather than finalized to default to the most recent
1954        let commitment = config.commitment.unwrap_or(CommitmentConfig {
1955            commitment: CommitmentLevel::Processed,
1956        });
1957
1958        const MAX_SLOT_RANGE: u64 = 500_000;
1959        if let Some(end) = end_slot {
1960            if end < start_slot {
1961                // early return for invalid range
1962                return Box::pin(async { Ok(vec![]) });
1963            }
1964            if end.saturating_sub(start_slot) > MAX_SLOT_RANGE {
1965                return Box::pin(async move {
1966                    Err(Error::invalid_params(format!(
1967                        "Slot range too large. Maximum: {}, Requested: {}",
1968                        MAX_SLOT_RANGE,
1969                        end.saturating_sub(start_slot)
1970                    )))
1971                });
1972            }
1973        }
1974
1975        let SurfnetRpcContext {
1976            svm_locker,
1977            remote_ctx,
1978        } = match meta.get_rpc_context(commitment) {
1979            Ok(res) => res,
1980            Err(e) => return e.into(),
1981        };
1982
1983        Box::pin(async move {
1984            let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
1985            let effective_end_slot = end_slot
1986                .map(|end| end.min(committed_latest_slot))
1987                .unwrap_or(committed_latest_slot);
1988
1989            let genesis_slot = svm_locker.with_svm_reader(|svm| svm.genesis_slot);
1990
1991            let (local_min_slot, local_slots, effective_end_slot) = if effective_end_slot
1992                < start_slot
1993            {
1994                (None, vec![], effective_end_slot)
1995            } else {
1996                // With sparse block storage, all slots from genesis_slot onwards are valid
1997                // Return all slots in the requested range instead of filtering by stored blocks
1998                let local_min_slot = Some(genesis_slot);
1999                let local_slots: Vec<Slot> = (start_slot.max(genesis_slot)..=effective_end_slot)
2000                    .filter(|slot| *slot <= committed_latest_slot)
2001                    .collect();
2002
2003                (local_min_slot, local_slots, effective_end_slot)
2004            };
2005
2006            if let Some(min_context_slot) = config.min_context_slot {
2007                if committed_latest_slot < min_context_slot {
2008                    return Err(RpcCustomError::MinContextSlotNotReached {
2009                        context_slot: min_context_slot,
2010                    }
2011                    .into());
2012                }
2013            }
2014
2015            if effective_end_slot.saturating_sub(start_slot) > MAX_SLOT_RANGE {
2016                return Err(Error::invalid_params(format!(
2017                    "Slot range too large. Maximum: {}, Requested: {}",
2018                    MAX_SLOT_RANGE,
2019                    effective_end_slot.saturating_sub(start_slot)
2020                )));
2021            }
2022
2023            let remote_slots = if let (Some((remote_client, _)), Some(local_min)) =
2024                (&remote_ctx, local_min_slot)
2025            {
2026                if start_slot < local_min {
2027                    let remote_end = effective_end_slot.min(local_min.saturating_sub(1));
2028                    if start_slot <= remote_end {
2029                        remote_client
2030                            .client
2031                            .get_blocks(start_slot, Some(remote_end))
2032                            .await
2033                            .unwrap_or_else(|_| vec![])
2034                    } else {
2035                        vec![]
2036                    }
2037                } else {
2038                    vec![]
2039                }
2040            } else if remote_ctx.is_some() && local_min_slot.is_none() {
2041                remote_ctx
2042                    .as_ref()
2043                    .unwrap()
2044                    .0
2045                    .client
2046                    .get_blocks(start_slot, Some(effective_end_slot))
2047                    .await
2048                    .unwrap_or_else(|_| vec![])
2049            } else {
2050                vec![]
2051            };
2052
2053            // Combine results
2054            let mut combined_slots = remote_slots;
2055            combined_slots.extend(local_slots);
2056            combined_slots.sort_unstable();
2057            combined_slots.dedup();
2058
2059            if combined_slots.len() > MAX_SLOT_RANGE as usize {
2060                combined_slots.truncate(MAX_SLOT_RANGE as usize);
2061            }
2062
2063            Ok(combined_slots)
2064        })
2065    }
2066
2067    fn get_blocks_with_limit(
2068        &self,
2069        meta: Self::Metadata,
2070        start_slot: Slot,
2071        limit: usize,
2072        config: Option<RpcContextConfig>,
2073    ) -> BoxFuture<Result<Vec<Slot>>> {
2074        let config = config.unwrap_or_default();
2075        let commitment = config.commitment.unwrap_or(CommitmentConfig {
2076            commitment: CommitmentLevel::Processed,
2077        });
2078
2079        if limit == 0 {
2080            return Box::pin(
2081                async move { Err(Error::invalid_params("Limit must be greater than 0")) },
2082            );
2083        }
2084
2085        const MAX_LIMIT: usize = 500_000;
2086        if limit > MAX_LIMIT {
2087            return Box::pin(async move {
2088                Err(Error::invalid_params(format!(
2089                    "Limit too large. Maximum limit allowed: {}",
2090                    MAX_LIMIT
2091                )))
2092            });
2093        }
2094
2095        let SurfnetRpcContext {
2096            svm_locker,
2097            remote_ctx,
2098        } = match meta.get_rpc_context(commitment) {
2099            Ok(res) => res,
2100            Err(e) => return e.into(),
2101        };
2102
2103        Box::pin(async move {
2104            let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
2105            let genesis_slot = svm_locker.with_svm_reader(|svm| svm.genesis_slot);
2106
2107            // With sparse block storage, all slots from genesis_slot onwards are valid
2108            // Return all slots in the requested range instead of filtering by stored blocks
2109            let local_min_slot = Some(genesis_slot);
2110            let local_slots: Vec<Slot> =
2111                (start_slot.max(genesis_slot)..=committed_latest_slot).collect();
2112
2113            if let Some(min_context_slot) = config.min_context_slot {
2114                if committed_latest_slot < min_context_slot {
2115                    return Err(RpcCustomError::MinContextSlotNotReached {
2116                        context_slot: min_context_slot,
2117                    }
2118                    .into());
2119                }
2120            }
2121
2122            // fetch remote blocks when needed, using the same logic as get_blocks
2123            let remote_slots = if let (Some((remote_client, _)), Some(local_min)) =
2124                (&remote_ctx, local_min_slot)
2125            {
2126                if start_slot < local_min {
2127                    let remote_end = committed_latest_slot.min(local_min.saturating_sub(1));
2128                    if start_slot <= remote_end {
2129                        remote_client
2130                            .client
2131                            .get_blocks(start_slot, Some(remote_end))
2132                            .await
2133                            .unwrap_or_else(|_| vec![])
2134                    } else {
2135                        vec![]
2136                    }
2137                } else {
2138                    vec![]
2139                }
2140            } else if remote_ctx.is_some() && local_min_slot.is_none() {
2141                // no local blocks exist, fetch from remote
2142                remote_ctx
2143                    .as_ref()
2144                    .unwrap()
2145                    .0
2146                    .client
2147                    .get_blocks(start_slot, Some(committed_latest_slot))
2148                    .await
2149                    .unwrap_or_else(|_| vec![])
2150            } else {
2151                vec![]
2152            };
2153
2154            let mut combined_slots = remote_slots;
2155            combined_slots.extend(local_slots);
2156            combined_slots.sort_unstable();
2157            combined_slots.dedup();
2158
2159            // apply the limit take only the first 'limit' slots
2160            combined_slots.truncate(limit);
2161
2162            Ok(combined_slots)
2163        })
2164    }
2165
2166    fn get_transaction(
2167        &self,
2168        meta: Self::Metadata,
2169        signature_str: String,
2170        config: Option<RpcEncodingConfigWrapper<RpcTransactionConfig>>,
2171    ) -> BoxFuture<Result<Option<EncodedConfirmedTransactionWithStatusMeta>>> {
2172        let mut config = config.map(|c| c.convert_to_current()).unwrap_or_default();
2173        adjust_default_transaction_config(&mut config);
2174
2175        Box::pin(async move {
2176            let signature = Signature::from_str(&signature_str)
2177                .map_err(|e| SurfpoolError::invalid_signature(&signature_str, e.to_string()))?;
2178
2179            let SurfnetRpcContext {
2180                svm_locker,
2181                remote_ctx,
2182            } = meta.get_rpc_context(())?;
2183
2184            // TODO: implement new interfaces in LiteSVM to get all the relevant info
2185            // needed to return the actual tx, not just some metadata
2186            match svm_locker
2187                .get_transaction(&remote_ctx.map(|(r, _)| r), &signature, config)
2188                .await?
2189            {
2190                GetTransactionResult::None(_) => Ok(None),
2191                GetTransactionResult::FoundTransaction(_, meta, _) => Ok(Some(meta)),
2192            }
2193        })
2194    }
2195
2196    fn get_signatures_for_address(
2197        &self,
2198        meta: Self::Metadata,
2199        address: String,
2200        config: Option<RpcSignaturesForAddressConfig>,
2201    ) -> BoxFuture<Result<Vec<RpcConfirmedTransactionStatusWithSignature>>> {
2202        let pubkey = match verify_pubkey(&address) {
2203            Ok(s) => s,
2204            Err(e) => return e.into(),
2205        };
2206        let SurfnetRpcContext {
2207            svm_locker,
2208            remote_ctx,
2209        } = match meta.get_rpc_context(()) {
2210            Ok(res) => res,
2211            Err(e) => return e.into(),
2212        };
2213
2214        Box::pin(async move {
2215            let signatures = svm_locker
2216                .get_signatures_for_address(&remote_ctx, &pubkey, config.as_ref())
2217                .await?
2218                .inner;
2219            Ok(signatures)
2220        })
2221    }
2222
2223    fn get_first_available_block(&self, meta: Self::Metadata) -> Result<Slot> {
2224        meta.with_svm_reader(|svm_reader| {
2225            Ok::<_, jsonrpc_core::Error>(
2226                svm_reader
2227                    .blocks
2228                    .keys()?
2229                    .into_iter()
2230                    .min()
2231                    .unwrap_or_default(),
2232            )
2233        })?
2234        .map_err(Into::into)
2235    }
2236
2237    fn get_latest_blockhash(
2238        &self,
2239        meta: Self::Metadata,
2240        config: Option<RpcContextConfig>,
2241    ) -> Result<RpcResponse<RpcBlockhash>> {
2242        let svm_locker = meta.get_svm_locker()?;
2243
2244        let config = config.unwrap_or_default();
2245        let commitment = config.commitment.unwrap_or_default();
2246
2247        let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
2248        if let Some(min_context_slot) = config.min_context_slot {
2249            if committed_latest_slot < min_context_slot {
2250                return Err(RpcCustomError::MinContextSlotNotReached {
2251                    context_slot: min_context_slot,
2252                }
2253                .into());
2254            }
2255        }
2256
2257        let blockhash = svm_locker
2258            .get_latest_blockhash(&commitment)
2259            .unwrap_or_else(|| svm_locker.latest_absolute_blockhash());
2260
2261        let current_block_height = svm_locker.get_epoch_info().block_height;
2262        let last_valid_block_height = current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
2263        Ok(RpcResponse {
2264            context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2265            value: RpcBlockhash {
2266                blockhash: blockhash.to_string(),
2267                last_valid_block_height,
2268            },
2269        })
2270    }
2271
2272    fn is_blockhash_valid(
2273        &self,
2274        meta: Self::Metadata,
2275        blockhash: String,
2276        config: Option<RpcContextConfig>,
2277    ) -> Result<RpcResponse<bool>> {
2278        let hash = blockhash
2279            .parse::<solana_hash::Hash>()
2280            .map_err(|e| Error::invalid_params(format!("Invalid blockhash: {e:?}")))?;
2281        let config = config.unwrap_or_default();
2282
2283        let svm_locker = meta.get_svm_locker()?;
2284
2285        let committed_latest_slot =
2286            svm_locker.get_slot_for_commitment(&config.commitment.unwrap_or_default());
2287
2288        let is_valid =
2289            svm_locker.with_svm_reader(|svm_reader| svm_reader.check_blockhash_is_recent(&hash));
2290
2291        if let Some(min_context_slot) = config.min_context_slot {
2292            if committed_latest_slot < min_context_slot {
2293                return Err(RpcCustomError::MinContextSlotNotReached {
2294                    context_slot: min_context_slot,
2295                }
2296                .into());
2297            }
2298        }
2299
2300        Ok(RpcResponse {
2301            context: RpcResponseContext::new(committed_latest_slot),
2302            value: is_valid,
2303        })
2304    }
2305
2306    fn get_fee_for_message(
2307        &self,
2308        meta: Self::Metadata,
2309        encoded: String,
2310        config: Option<RpcContextConfig>,
2311    ) -> Result<RpcResponse<Option<u64>>> {
2312        let (_, message) =
2313            decode_and_deserialize::<VersionedMessage>(encoded, TransactionBinaryEncoding::Base64)?;
2314
2315        let RpcContextConfig {
2316            commitment,
2317            min_context_slot,
2318        } = config.unwrap_or_default();
2319        let min_ctx_slot = min_context_slot.unwrap_or_default();
2320
2321        let svm_locker = meta.get_svm_locker()?;
2322
2323        let slot = if let Some(commitment_config) = commitment {
2324            svm_locker.get_slot_for_commitment(&commitment_config)
2325        } else {
2326            svm_locker.get_latest_absolute_slot()
2327        };
2328
2329        if let Some(min_slot) = min_context_slot
2330            && slot < min_slot
2331        {
2332            return Err(RpcCustomError::MinContextSlotNotReached {
2333                context_slot: min_ctx_slot,
2334            }
2335            .into());
2336        }
2337
2338        Ok(RpcResponse {
2339            context: RpcResponseContext::new(slot),
2340            value: Some((message.header().num_required_signatures as u64) * 5000),
2341        })
2342    }
2343
2344    fn get_stake_minimum_delegation(
2345        &self,
2346        meta: Self::Metadata,
2347        config: Option<RpcContextConfig>,
2348    ) -> Result<RpcResponse<u64>> {
2349        let config = config.unwrap_or_default();
2350        let commitment_config = config.commitment.unwrap_or(CommitmentConfig {
2351            commitment: CommitmentLevel::Processed,
2352        });
2353
2354        meta.with_svm_reader(|svm_reader| {
2355            let context_slot = match commitment_config.commitment {
2356                CommitmentLevel::Processed => svm_reader.get_latest_absolute_slot(),
2357                CommitmentLevel::Confirmed => {
2358                    svm_reader.get_latest_absolute_slot().saturating_sub(1)
2359                }
2360                CommitmentLevel::Finalized => svm_reader
2361                    .get_latest_absolute_slot()
2362                    .saturating_sub(FINALIZATION_SLOT_THRESHOLD),
2363            };
2364
2365            RpcResponse {
2366                context: RpcResponseContext::new(context_slot),
2367                value: 0,
2368            }
2369        })
2370        .map_err(Into::into)
2371    }
2372
2373    fn get_recent_prioritization_fees(
2374        &self,
2375        meta: Self::Metadata,
2376        pubkey_strs: Option<Vec<String>>,
2377    ) -> BoxFuture<Result<Vec<RpcPrioritizationFee>>> {
2378        let pubkeys_filter = match pubkey_strs
2379            .map(|strs| {
2380                strs.iter()
2381                    .map(|s| verify_pubkey(s))
2382                    .collect::<SurfpoolResult<Vec<_>>>()
2383            })
2384            .transpose()
2385        {
2386            Ok(pubkeys) => pubkeys,
2387            Err(e) => return e.into(),
2388        };
2389
2390        let SurfnetRpcContext {
2391            svm_locker,
2392            remote_ctx,
2393        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2394            Ok(res) => res,
2395            Err(e) => return e.into(),
2396        };
2397
2398        Box::pin(async move {
2399            let (blocks, transactions) = svm_locker.with_svm_reader(|svm_reader| {
2400                (svm_reader.blocks.clone(), svm_reader.transactions.clone())
2401            });
2402
2403            // Get MAX_PRIORITIZATION_FEE_BLOCKS_CACHE most recent blocks
2404            let recent_headers = blocks
2405                .into_iter()?
2406                .sorted_by_key(|(slot, _)| std::cmp::Reverse(*slot))
2407                .take(MAX_PRIORITIZATION_FEE_BLOCKS_CACHE)
2408                .collect::<Vec<_>>();
2409
2410            // Flatten the transactions map to get all transactions in the recent blocks
2411            let recent_transactions = recent_headers
2412                .into_iter()
2413                .flat_map(|(slot, header)| {
2414                    header
2415                        .signatures
2416                        .iter()
2417                        .filter_map(|signature| {
2418                            // Check if the signature exists in the transactions map
2419                            transactions
2420                                .get(&signature.to_string())
2421                                .ok()
2422                                .flatten()
2423                                .map(|tx| (slot, tx))
2424                        })
2425                        .collect::<Vec<_>>()
2426                })
2427                .collect::<Vec<_>>();
2428
2429            // Helper function to extract compute unit price from a CompiledInstruction
2430            fn get_compute_unit_price(ix: CompiledInstruction, accounts: &[Pubkey]) -> Option<u64> {
2431                let program_account = accounts.get(ix.program_id_index as usize)?;
2432                if *program_account != compute_budget::id() {
2433                    return None;
2434                }
2435
2436                if let Ok(ComputeBudgetInstruction::SetComputeUnitPrice(price)) =
2437                    borsh::from_slice::<ComputeBudgetInstruction>(&ix.data)
2438                {
2439                    return Some(price);
2440                }
2441
2442                None
2443            }
2444
2445            let mut prioritization_fees = vec![];
2446            for (slot, tx) in recent_transactions {
2447                match tx {
2448                    SurfnetTransactionStatus::Received => {}
2449                    SurfnetTransactionStatus::Processed(data) => {
2450                        let (status_meta, _) = data.as_ref();
2451                        let tx = &status_meta.transaction;
2452
2453                        // If the transaction has an ALT and includes a compute budget instruction,
2454                        // the ALT accounts are included in the recent prioritization fees,
2455                        // so we get _all_ the pubkeys from the message
2456                        let loaded_addresses = svm_locker
2457                            .get_loaded_addresses(&remote_ctx, &tx.message)
2458                            .await?;
2459                        let account_keys = svm_locker.get_pubkeys_from_message(
2460                            &tx.message,
2461                            loaded_addresses.as_ref().map(|l| l.all_loaded_addresses()),
2462                        );
2463
2464                        let instructions = tx.message.instructions();
2465
2466                        // Find all compute unit prices in the transaction's instructions
2467                        let compute_unit_prices = instructions
2468                            .iter()
2469                            .filter_map(|ix| get_compute_unit_price(ix.clone(), &account_keys))
2470                            .collect::<Vec<_>>();
2471
2472                        for compute_unit_price in compute_unit_prices {
2473                            if let Some(pubkeys_filter) = &pubkeys_filter {
2474                                // If none of the accounts involved in this transaction are in the filter,
2475                                // we don't include the prioritization fee, so we continue
2476                                if !pubkeys_filter
2477                                    .iter()
2478                                    .any(|pk| account_keys.iter().any(|a| a == pk))
2479                                {
2480                                    continue;
2481                                }
2482                            }
2483                            // if there's no filter, or if the filter matches an account in this transaction, we include the fee
2484                            prioritization_fees.push(RpcPrioritizationFee {
2485                                slot,
2486                                prioritization_fee: compute_unit_price,
2487                            });
2488                        }
2489                    }
2490                }
2491            }
2492            Ok(prioritization_fees)
2493        })
2494    }
2495}
2496
2497fn get_simulate_transaction_result(
2498    metadata: TransactionMetadata,
2499    accounts: Option<Vec<Option<UiAccount>>>,
2500    error: Option<TransactionError>,
2501    replacement_blockhash: Option<RpcBlockhash>,
2502    include_inner_instructions: bool,
2503    message: &VersionedMessage,
2504    loaded_addresses: Option<&solana_message::v0::LoadedAddresses>,
2505    loaded_accounts_data_size: Option<u32>,
2506) -> RpcSimulateTransactionResult {
2507    RpcSimulateTransactionResult {
2508        accounts,
2509        err: error.map(|e| e.into()),
2510        inner_instructions: if include_inner_instructions {
2511            Some(transform_tx_metadata_to_ui_accounts(
2512                metadata.clone(),
2513                message,
2514                loaded_addresses,
2515            ))
2516        } else {
2517            None
2518        },
2519        logs: Some(metadata.logs.clone()),
2520        replacement_blockhash,
2521        return_data: if metadata.return_data.program_id == system_program::id()
2522            && metadata.return_data.data.is_empty()
2523        {
2524            None
2525        } else {
2526            Some(metadata.return_data.clone().into())
2527        },
2528        units_consumed: Some(metadata.compute_units_consumed),
2529        loaded_accounts_data_size,
2530        fee: None,
2531        pre_balances: None,
2532        post_balances: None,
2533        pre_token_balances: None,
2534        post_token_balances: None,
2535        loaded_addresses: None,
2536    }
2537}
2538
2539#[cfg(test)]
2540mod tests {
2541    pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
2542
2543    use std::thread::JoinHandle;
2544
2545    use base64::{Engine, prelude::BASE64_STANDARD};
2546    use bincode::Options;
2547    use crossbeam_channel::Receiver;
2548    use solana_account_decoder::{UiAccount, UiAccountData, UiAccountEncoding};
2549    use solana_client::rpc_config::RpcSimulateTransactionAccountsConfig;
2550    use solana_commitment_config::CommitmentConfig;
2551    use solana_hash::Hash;
2552    use solana_instruction::Instruction;
2553    use solana_keypair::Keypair;
2554    use solana_message::{
2555        MessageHeader, legacy::Message as LegacyMessage, v0::Message as V0Message,
2556        v1::MAX_TRANSACTION_SIZE,
2557    };
2558    use solana_pubkey::Pubkey;
2559    use solana_signer::Signer;
2560    use solana_system_interface::{
2561        instruction::{self as system_instruction, transfer},
2562        program as system_program,
2563    };
2564    use solana_transaction::{
2565        Transaction,
2566        versioned::{Legacy, TransactionVersion, VersionedTransaction},
2567    };
2568    use solana_transaction_error::TransactionError;
2569    use solana_transaction_status::{
2570        EncodedTransaction, EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiMessage,
2571        UiRawMessage, UiTransaction, UiTransactionEncoding,
2572    };
2573    use surfpool_types::{SimnetCommand, TransactionConfirmationStatus};
2574    use test_case::test_case;
2575
2576    use super::*;
2577    use crate::{
2578        surfnet::{BlockHeader, BlockIdentifier, remote::SurfnetRemoteClient},
2579        tests::helpers::TestSetup,
2580        types::{SyntheticBlockhash, TransactionWithStatusMeta},
2581    };
2582
2583    fn build_v0_transaction(
2584        payer: &Pubkey,
2585        signers: &[&Keypair],
2586        instructions: &[Instruction],
2587        recent_blockhash: &Hash,
2588    ) -> VersionedTransaction {
2589        let msg = VersionedMessage::V0(
2590            V0Message::try_compile(&payer, instructions, &[], *recent_blockhash).unwrap(),
2591        );
2592        VersionedTransaction::try_new(msg, signers).unwrap()
2593    }
2594
2595    fn build_legacy_transaction(
2596        payer: &Pubkey,
2597        signers: &[&Keypair],
2598        instructions: &[Instruction],
2599        recent_blockhash: &Hash,
2600    ) -> VersionedTransaction {
2601        let msg = VersionedMessage::Legacy(LegacyMessage::new_with_blockhash(
2602            instructions,
2603            Some(payer),
2604            recent_blockhash,
2605        ));
2606        VersionedTransaction::try_new(msg, signers).unwrap()
2607    }
2608
2609    async fn send_and_await_transaction(
2610        tx: VersionedTransaction,
2611        setup: TestSetup<SurfpoolFullRpc>,
2612        mempool_rx: Receiver<SimnetCommand>,
2613    ) -> JoinHandle<String> {
2614        let setup_clone = setup.clone();
2615        let handle = hiro_system_kit::thread_named("send_tx")
2616            .spawn(move || {
2617                let res = setup_clone
2618                    .rpc
2619                    .send_transaction(
2620                        Some(setup_clone.context),
2621                        bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2622                        None,
2623                    )
2624                    .unwrap();
2625
2626                res
2627            })
2628            .unwrap();
2629        loop {
2630            match mempool_rx.recv() {
2631                Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => {
2632                    let mut writer = setup.context.svm_locker.0.write().await;
2633                    let slot = writer.get_latest_absolute_slot();
2634                    writer.transactions_queued_for_confirmation.push_back((
2635                        tx.clone(),
2636                        status_tx.clone(),
2637                        None,
2638                    ));
2639                    let sig = tx.signatures[0];
2640                    let tx_with_status_meta = TransactionWithStatusMeta {
2641                        slot,
2642                        transaction: tx,
2643                        ..Default::default()
2644                    };
2645                    let mutated_accounts = std::collections::HashSet::new();
2646                    writer
2647                        .transactions
2648                        .store(
2649                            sig.to_string(),
2650                            SurfnetTransactionStatus::processed(
2651                                tx_with_status_meta,
2652                                mutated_accounts,
2653                            ),
2654                        )
2655                        .unwrap();
2656                    status_tx
2657                        .send(TransactionStatusEvent::Success(
2658                            TransactionConfirmationStatus::Confirmed,
2659                        ))
2660                        .unwrap();
2661                    break;
2662                }
2663                Ok(SimnetCommand::AirdropProcessed) => continue,
2664                _ => panic!("failed to receive transaction from mempool"),
2665            }
2666        }
2667
2668        handle
2669    }
2670
2671    #[test_case(None, false ; "when limit is None")]
2672    #[test_case(Some(1), false ; "when limit is ok")]
2673    #[test_case(Some(1000), true ; "when limit is above max spec")]
2674    fn test_get_recent_performance_samples(limit: Option<usize>, fails: bool) {
2675        let setup = TestSetup::new(SurfpoolFullRpc);
2676        let res = setup
2677            .rpc
2678            .get_recent_performance_samples(Some(setup.context), limit);
2679
2680        if fails {
2681            assert!(res.is_err());
2682        } else {
2683            assert!(res.is_ok());
2684        }
2685    }
2686
2687    #[tokio::test(flavor = "multi_thread")]
2688    async fn test_get_fee_for_message() {
2689        let setup = TestSetup::new(SurfpoolFullRpc);
2690        let runloop_context = setup.context;
2691        let rpc_server = setup.rpc;
2692        let payer = Keypair::new();
2693        let recipient = Pubkey::new_unique();
2694        let lamports_to_send = 5 * LAMPORTS_PER_SOL;
2695        let commitment_config_to_use = CommitmentConfig::confirmed();
2696
2697        let wrong_comm_min_ctx_slot = runloop_context
2698            .svm_locker
2699            .get_slot_for_commitment(&commitment_config_to_use)
2700            + 10;
2701
2702        let wrong_min_slot = runloop_context.svm_locker.get_latest_absolute_slot() + 10;
2703        let rpc_ctx_config_with_wrong_commitment = RpcContextConfig {
2704            commitment: Some(commitment_config_to_use),
2705            min_context_slot: Some(wrong_comm_min_ctx_slot),
2706        };
2707        let rpc_ctx_config_with_wrong_min_slot = RpcContextConfig {
2708            commitment: None,
2709            min_context_slot: Some(wrong_min_slot),
2710        };
2711
2712        let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
2713
2714        let latest_blockhash = runloop_context
2715            .svm_locker
2716            .with_svm_reader(|svm| svm.latest_blockhash());
2717        let message = solana_message::Message::new_with_blockhash(
2718            &[instruction],
2719            Some(&payer.pubkey()),
2720            &latest_blockhash,
2721        );
2722        let num_required_signatures = message.header.num_required_signatures as u64;
2723        let transaction =
2724            VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
2725
2726        let message_bytes = bincode::options()
2727            .with_fixint_encoding()
2728            .serialize(&transaction.message)
2729            .expect("message serialization");
2730        let encoded_message = base64::engine::general_purpose::STANDARD.encode(&message_bytes);
2731
2732        let get_fee_with_correct_config_pass_result = rpc_server.get_fee_for_message(
2733            Some(runloop_context.clone()),
2734            encoded_message.clone(),
2735            None,
2736        );
2737
2738        assert!(
2739            get_fee_with_correct_config_pass_result.is_ok(),
2740            "Expected get_fee_for_message to pass with correct configs"
2741        );
2742        assert_eq!(
2743            get_fee_with_correct_config_pass_result
2744                .unwrap()
2745                .value
2746                .unwrap(),
2747            (num_required_signatures as u64) * 5_000,
2748            "Invalid return value"
2749        );
2750
2751        let get_fee_with_wrong_commitment_fail_result = rpc_server.get_fee_for_message(
2752            Some(runloop_context.clone()),
2753            encoded_message.clone(),
2754            Some(rpc_ctx_config_with_wrong_commitment),
2755        );
2756
2757        let wrong_comm_expected_err: Result<()> = Result::Err(
2758            RpcCustomError::MinContextSlotNotReached {
2759                context_slot: wrong_comm_min_ctx_slot,
2760            }
2761            .into(),
2762        );
2763
2764        assert!(
2765            get_fee_with_wrong_commitment_fail_result.is_err(),
2766            "expected this txn to fail when min_ctx_slot > slot_for_commitment"
2767        );
2768
2769        assert_eq!(
2770            get_fee_with_wrong_commitment_fail_result.err().unwrap(),
2771            wrong_comm_expected_err.err().unwrap()
2772        );
2773
2774        let get_fee_with_wrong_mint_slot_fail_result = rpc_server.get_fee_for_message(
2775            Some(runloop_context.clone()),
2776            encoded_message,
2777            Some(rpc_ctx_config_with_wrong_min_slot),
2778        );
2779
2780        let wrong_min_slot_expected_err: Result<()> = Result::Err(
2781            RpcCustomError::MinContextSlotNotReached {
2782                context_slot: wrong_min_slot,
2783            }
2784            .into(),
2785        );
2786        assert!(
2787            get_fee_with_wrong_mint_slot_fail_result.is_err(),
2788            "expected this txn to fail when min_ctx_slot > absolute_latest_slot"
2789        );
2790        assert_eq!(
2791            get_fee_with_wrong_mint_slot_fail_result.err().unwrap(),
2792            wrong_min_slot_expected_err.err().unwrap()
2793        );
2794    }
2795
2796    #[tokio::test(flavor = "multi_thread")]
2797    async fn test_get_signature_statuses() {
2798        let pks = (0..10).map(|_| Pubkey::new_unique());
2799        let valid_txs = pks.len();
2800        let invalid_txs = pks.len();
2801        let payer = Keypair::new();
2802        let mut setup = TestSetup::new(SurfpoolFullRpc).without_blockhash().await;
2803        let recent_blockhash = setup
2804            .context
2805            .svm_locker
2806            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2807
2808        let valid = pks
2809            .clone()
2810            .map(|pk| {
2811                Transaction::new_signed_with_payer(
2812                    &[system_instruction::transfer(
2813                        &payer.pubkey(),
2814                        &pk,
2815                        LAMPORTS_PER_SOL,
2816                    )],
2817                    Some(&payer.pubkey()),
2818                    &[payer.insecure_clone()],
2819                    recent_blockhash,
2820                )
2821            })
2822            .collect::<Vec<_>>();
2823        let invalid = pks
2824            .map(|pk| {
2825                Transaction::new_unsigned(LegacyMessage::new(
2826                    &[system_instruction::transfer(
2827                        &pk,
2828                        &payer.pubkey(),
2829                        LAMPORTS_PER_SOL,
2830                    )],
2831                    Some(&payer.pubkey()),
2832                ))
2833            })
2834            .collect::<Vec<_>>();
2835        let txs = valid
2836            .into_iter()
2837            .chain(invalid.into_iter())
2838            .map(|tx| VersionedTransaction {
2839                signatures: tx.signatures,
2840                message: VersionedMessage::Legacy(tx.message),
2841            })
2842            .collect::<Vec<_>>();
2843        let _ = setup.context.svm_locker.0.write().await.airdrop(
2844            &payer.pubkey(),
2845            (valid_txs + invalid_txs) as u64 * 2 * LAMPORTS_PER_SOL,
2846        );
2847        setup.process_txs(txs.clone()).await;
2848
2849        // Capture the expected slot before the call to verify context slot consistency
2850        let current_slot = setup
2851            .context
2852            .svm_locker
2853            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
2854
2855        // fetch while transactions are still in processed status
2856        {
2857            let res = setup
2858                .rpc
2859                .get_signature_statuses(
2860                    Some(setup.context.clone()),
2861                    txs.iter().map(|tx| tx.signatures[0].to_string()).collect(),
2862                    None,
2863                )
2864                .await
2865                .unwrap();
2866            assert_eq!(
2867                res.value.iter().flatten().collect::<Vec<_>>().len(),
2868                0,
2869                "processed transactions should not be returning values"
2870            );
2871        }
2872
2873        // confirm a block to move transactions to confirmed status
2874        setup
2875            .context
2876            .svm_locker
2877            .confirm_current_block(&None)
2878            .await
2879            .unwrap();
2880        let res = setup
2881            .rpc
2882            .get_signature_statuses(
2883                Some(setup.context),
2884                txs.iter().map(|tx| tx.signatures[0].to_string()).collect(),
2885                None,
2886            )
2887            .await
2888            .unwrap();
2889
2890        // Verify context slot is captured at the beginning of the call
2891        assert_eq!(
2892            res.context.slot,
2893            current_slot + 1,
2894            "Context slot should be captured at the beginning of the call, not after lookups"
2895        );
2896
2897        assert_eq!(
2898            res.value
2899                .iter()
2900                .filter(|status| {
2901                    println!("status: {:?}", status);
2902                    if let Some(s) = status {
2903                        s.status.is_ok()
2904                    } else {
2905                        false
2906                    }
2907                })
2908                .count(),
2909            valid_txs,
2910            "incorrect number of valid txs"
2911        );
2912        assert_eq!(
2913            res.value
2914                .iter()
2915                .filter(|status| if let Some(s) = status {
2916                    s.status.is_err()
2917                } else {
2918                    true
2919                })
2920                .count(),
2921            invalid_txs,
2922            "incorrect number of invalid txs"
2923        );
2924    }
2925
2926    #[test]
2927    fn test_request_airdrop() {
2928        let pk = Pubkey::new_unique();
2929        let lamports = 1_000_000;
2930        let setup = TestSetup::new(SurfpoolFullRpc);
2931        let res = setup
2932            .rpc
2933            .request_airdrop(Some(setup.context.clone()), pk.to_string(), lamports, None)
2934            .unwrap();
2935        let sig = Signature::from_str(res.as_str()).unwrap();
2936        let state_reader = setup.context.svm_locker.0.blocking_read();
2937        assert_eq!(
2938            state_reader
2939                .inner
2940                .get_account(&pk)
2941                .unwrap()
2942                .unwrap()
2943                .lamports,
2944            lamports,
2945            "airdropped amount is incorrect"
2946        );
2947        assert!(
2948            state_reader.get_transaction(&sig).unwrap().is_some(),
2949            "transaction is not found in the SVM"
2950        );
2951        assert!(
2952            state_reader
2953                .transactions
2954                .get(&sig.to_string())
2955                .unwrap()
2956                .is_some(),
2957            "transaction is not found in the history"
2958        );
2959    }
2960
2961    #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
2962    #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
2963    #[tokio::test(flavor = "multi_thread")]
2964    async fn test_send_transaction(version: TransactionVersion) {
2965        let payer = Keypair::new();
2966        let pk = Pubkey::new_unique();
2967        let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
2968        let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
2969        let recent_blockhash = setup
2970            .context
2971            .svm_locker
2972            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2973
2974        let tx = match version {
2975            TransactionVersion::Legacy(_) => build_legacy_transaction(
2976                &payer.pubkey(),
2977                &[&payer.insecure_clone()],
2978                &[system_instruction::transfer(
2979                    &payer.pubkey(),
2980                    &pk,
2981                    LAMPORTS_PER_SOL,
2982                )],
2983                &recent_blockhash,
2984            ),
2985            TransactionVersion::Number(0) => build_v0_transaction(
2986                &payer.pubkey(),
2987                &[&payer.insecure_clone()],
2988                &[system_instruction::transfer(
2989                    &payer.pubkey(),
2990                    &pk,
2991                    LAMPORTS_PER_SOL,
2992                )],
2993                &recent_blockhash,
2994            ),
2995            _ => unimplemented!(),
2996        };
2997
2998        let _ = setup
2999            .context
3000            .svm_locker
3001            .0
3002            .write()
3003            .await
3004            .airdrop(&payer.pubkey(), 2 * LAMPORTS_PER_SOL);
3005
3006        let handle = send_and_await_transaction(tx.clone(), setup.clone(), mempool_rx).await;
3007        assert_eq!(
3008            handle.join().unwrap(),
3009            tx.signatures[0].to_string(),
3010            "incorrect signature"
3011        );
3012    }
3013
3014    #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3015    #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3016    #[tokio::test(flavor = "multi_thread")]
3017    async fn test_simulate_transaction(version: TransactionVersion) {
3018        let payer = Keypair::new();
3019        let pk = Pubkey::new_unique();
3020        let lamports = LAMPORTS_PER_SOL;
3021        let setup = TestSetup::new(SurfpoolFullRpc);
3022        let recent_blockhash = setup
3023            .context
3024            .svm_locker
3025            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3026
3027        let _ = setup
3028            .rpc
3029            .request_airdrop(
3030                Some(setup.context.clone()),
3031                payer.pubkey().to_string(),
3032                2 * lamports,
3033                None,
3034            )
3035            .unwrap();
3036
3037        let tx = match version {
3038            TransactionVersion::Legacy(_) => build_legacy_transaction(
3039                &payer.pubkey(),
3040                &[&payer.insecure_clone()],
3041                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3042                &recent_blockhash,
3043            ),
3044            TransactionVersion::Number(0) => build_v0_transaction(
3045                &payer.pubkey(),
3046                &[&payer.insecure_clone()],
3047                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3048                &recent_blockhash,
3049            ),
3050            _ => unimplemented!(),
3051        };
3052
3053        let simulation_res = setup
3054            .rpc
3055            .simulate_transaction(
3056                Some(setup.context),
3057                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3058                Some(RpcSimulateTransactionConfig {
3059                    sig_verify: true,
3060                    replace_recent_blockhash: false,
3061                    commitment: Some(CommitmentConfig::finalized()),
3062                    encoding: None,
3063                    accounts: Some(RpcSimulateTransactionAccountsConfig {
3064                        encoding: None,
3065                        addresses: vec![pk.to_string()],
3066                    }),
3067                    min_context_slot: None,
3068                    inner_instructions: false,
3069                }),
3070            )
3071            .await
3072            .unwrap();
3073
3074        assert_eq!(
3075            simulation_res.value.err, None,
3076            "Unexpected simulation error"
3077        );
3078        assert_eq!(
3079            simulation_res.value.accounts,
3080            Some(vec![Some(UiAccount {
3081                lamports,
3082                data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64),
3083                owner: system_program::id().to_string(),
3084                executable: false,
3085                rent_epoch: 0,
3086                space: Some(0),
3087            })]),
3088            "Wrong account content"
3089        );
3090    }
3091
3092    #[tokio::test(flavor = "multi_thread")]
3093    async fn test_simulate_transaction_oversized_base64_returns_invalid_params() {
3094        let setup = TestSetup::new(SurfpoolFullRpc);
3095        let oversized_encoded_len = MAX_TRANSACTION_SIZE.div_ceil(3) * 4 + 1;
3096
3097        let err = setup
3098            .rpc
3099            .simulate_transaction(
3100                Some(setup.context),
3101                "A".repeat(oversized_encoded_len),
3102                Some(RpcSimulateTransactionConfig {
3103                    encoding: Some(UiTransactionEncoding::Base64),
3104                    ..RpcSimulateTransactionConfig::default()
3105                }),
3106            )
3107            .await
3108            .unwrap_err();
3109
3110        assert_eq!(err.code, jsonrpc_core::ErrorCode::InvalidParams);
3111        assert!(
3112            err.message.contains("base64 encoded"),
3113            "expected base64 size validation error, got: {}",
3114            err.message
3115        );
3116    }
3117
3118    #[tokio::test(flavor = "multi_thread")]
3119    async fn test_simulate_transaction_no_signers() {
3120        let payer = Keypair::new();
3121        let pk = Pubkey::new_unique();
3122        let lamports = LAMPORTS_PER_SOL;
3123        let setup = TestSetup::new(SurfpoolFullRpc);
3124        setup
3125            .context
3126            .svm_locker
3127            .with_svm_writer(|svm_writer| svm_writer.inner.set_sigverify(false));
3128        let recent_blockhash = setup
3129            .context
3130            .svm_locker
3131            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3132
3133        let _ = setup
3134            .rpc
3135            .request_airdrop(
3136                Some(setup.context.clone()),
3137                payer.pubkey().to_string(),
3138                2 * lamports,
3139                None,
3140            )
3141            .unwrap();
3142        //build_legacy_transaction
3143        let mut msg = LegacyMessage::new(
3144            &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3145            Some(&payer.pubkey()),
3146        );
3147        msg.recent_blockhash = recent_blockhash;
3148        let tx = Transaction::new_unsigned(msg);
3149
3150        let simulation_res = setup
3151            .rpc
3152            .simulate_transaction(
3153                Some(setup.context),
3154                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3155                Some(RpcSimulateTransactionConfig {
3156                    sig_verify: false,
3157                    replace_recent_blockhash: false,
3158                    commitment: Some(CommitmentConfig::finalized()),
3159                    encoding: None,
3160                    accounts: Some(RpcSimulateTransactionAccountsConfig {
3161                        encoding: None,
3162                        addresses: vec![pk.to_string()],
3163                    }),
3164                    min_context_slot: None,
3165                    inner_instructions: false,
3166                }),
3167            )
3168            .await
3169            .unwrap();
3170
3171        assert_eq!(
3172            simulation_res.value.err, None,
3173            "Unexpected simulation error"
3174        );
3175        assert_eq!(
3176            simulation_res.value.accounts,
3177            Some(vec![Some(UiAccount {
3178                lamports,
3179                data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64),
3180                owner: system_program::id().to_string(),
3181                executable: false,
3182                rent_epoch: 0,
3183                space: Some(0),
3184            })]),
3185            "Wrong account content"
3186        );
3187    }
3188    #[tokio::test(flavor = "multi_thread")]
3189    async fn test_simulate_transaction_no_signers_err() {
3190        let payer = Keypair::new();
3191        let pk = Pubkey::new_unique();
3192        let lamports = LAMPORTS_PER_SOL;
3193        let setup = TestSetup::new(SurfpoolFullRpc);
3194        let recent_blockhash = setup
3195            .context
3196            .svm_locker
3197            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3198
3199        let _ = setup
3200            .rpc
3201            .request_airdrop(
3202                Some(setup.context.clone()),
3203                payer.pubkey().to_string(),
3204                2 * lamports,
3205                None,
3206            )
3207            .unwrap();
3208        setup
3209            .context
3210            .svm_locker
3211            .with_svm_writer(|svm_writer| svm_writer.inner.set_sigverify(false));
3212
3213        //build_legacy_transaction
3214        let mut msg = LegacyMessage::new(
3215            &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3216            Some(&payer.pubkey()),
3217        );
3218        msg.recent_blockhash = recent_blockhash;
3219        let tx = Transaction::new_unsigned(msg);
3220
3221        let simulation_res = setup
3222            .rpc
3223            .simulate_transaction(
3224                Some(setup.context),
3225                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3226                Some(RpcSimulateTransactionConfig {
3227                    sig_verify: true,
3228                    replace_recent_blockhash: false,
3229                    commitment: Some(CommitmentConfig::finalized()),
3230                    encoding: None,
3231                    accounts: Some(RpcSimulateTransactionAccountsConfig {
3232                        encoding: None,
3233                        addresses: vec![pk.to_string()],
3234                    }),
3235                    min_context_slot: None,
3236                    inner_instructions: false,
3237                }),
3238            )
3239            .await
3240            .unwrap();
3241
3242        assert_eq!(
3243            simulation_res.value.err,
3244            Some(TransactionError::SignatureFailure.into())
3245        );
3246    }
3247
3248    #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3249    #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3250    #[tokio::test(flavor = "multi_thread")]
3251    async fn test_simulate_transaction_replace_recent_blockhash(version: TransactionVersion) {
3252        let payer = Keypair::new();
3253        let pk = Pubkey::new_unique();
3254        let lamports = LAMPORTS_PER_SOL;
3255        let setup = TestSetup::new(SurfpoolFullRpc);
3256        let recent_blockhash = setup
3257            .context
3258            .svm_locker
3259            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3260        let block_height = setup
3261            .context
3262            .svm_locker
3263            .with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.block_height);
3264        let bad_blockhash = Hash::new_unique();
3265
3266        let _ = setup
3267            .rpc
3268            .request_airdrop(
3269                Some(setup.context.clone()),
3270                payer.pubkey().to_string(),
3271                2 * lamports,
3272                None,
3273            )
3274            .unwrap();
3275
3276        let mut tx = match version {
3277            TransactionVersion::Legacy(_) => build_legacy_transaction(
3278                &payer.pubkey(),
3279                &[&payer.insecure_clone()],
3280                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3281                &recent_blockhash,
3282            ),
3283            TransactionVersion::Number(0) => build_v0_transaction(
3284                &payer.pubkey(),
3285                &[&payer.insecure_clone()],
3286                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3287                &recent_blockhash,
3288            ),
3289            _ => unimplemented!(),
3290        };
3291        tx.message.set_recent_blockhash(bad_blockhash);
3292
3293        let invalid_config = RpcSimulateTransactionConfig {
3294            sig_verify: true,
3295            replace_recent_blockhash: true,
3296            commitment: Some(CommitmentConfig::finalized()),
3297            encoding: None,
3298            accounts: Some(RpcSimulateTransactionAccountsConfig {
3299                encoding: None,
3300                addresses: vec![pk.to_string()],
3301            }),
3302            min_context_slot: None,
3303            inner_instructions: false,
3304        };
3305        let err = setup
3306            .rpc
3307            .simulate_transaction(
3308                Some(setup.context.clone()),
3309                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3310                Some(invalid_config.clone()),
3311            )
3312            .await
3313            .unwrap_err();
3314
3315        assert_eq!(
3316            err.message, "sigVerify may not be used with replaceRecentBlockhash",
3317            "sigVerify should not be allowed to be used with replaceRecentBlockhash"
3318        );
3319
3320        let mut valid_config = invalid_config;
3321        valid_config.sig_verify = false;
3322        let simulation_res = setup
3323            .rpc
3324            .simulate_transaction(
3325                Some(setup.context),
3326                bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3327                Some(valid_config),
3328            )
3329            .await
3330            .unwrap();
3331
3332        assert_eq!(
3333            simulation_res.value.err, None,
3334            "Unexpected simulation error"
3335        );
3336        assert_eq!(
3337            simulation_res.value.replacement_blockhash,
3338            Some(RpcBlockhash {
3339                blockhash: recent_blockhash.to_string(),
3340                last_valid_block_height: block_height
3341            }),
3342            "Replacement blockhash should be the latest blockhash"
3343        );
3344    }
3345
3346    #[tokio::test(flavor = "multi_thread")]
3347    async fn test_get_block() {
3348        let setup = TestSetup::new(SurfpoolFullRpc);
3349
3350        let res = setup
3351            .rpc
3352            .get_block(Some(setup.context), FINALIZATION_SLOT_THRESHOLD, None)
3353            .await
3354            .unwrap();
3355
3356        // With sparse block storage, empty blocks are reconstructed on-the-fly
3357        assert!(res.is_some(), "Empty blocks should be reconstructed");
3358        let block = res.unwrap();
3359        assert!(
3360            block.signatures.is_none() || block.signatures.as_ref().unwrap().is_empty(),
3361            "Reconstructed empty block should have no signatures"
3362        );
3363    }
3364
3365    #[tokio::test(flavor = "multi_thread")]
3366    async fn test_get_block_time() {
3367        let setup = TestSetup::new(SurfpoolFullRpc);
3368
3369        let res = setup
3370            .rpc
3371            .get_block_time(Some(setup.context), FINALIZATION_SLOT_THRESHOLD)
3372            .await
3373            .unwrap();
3374
3375        // With sparse block storage, block time is calculated for any slot within range
3376        assert!(
3377            res.is_some(),
3378            "Block time should be calculated for valid slots"
3379        );
3380    }
3381
3382    #[tokio::test(flavor = "multi_thread")]
3383    async fn test_get_block_respects_confirmed_commitment_visibility() {
3384        let setup = TestSetup::new(SurfpoolFullRpc);
3385
3386        setup.context.svm_locker.with_svm_writer(|svm_writer| {
3387            svm_writer.latest_epoch_info.absolute_slot = 10;
3388        });
3389
3390        let res = setup
3391            .rpc
3392            .get_block(
3393                Some(setup.context),
3394                10,
3395                Some(RpcEncodingConfigWrapper::Current(Some(RpcBlockConfig {
3396                    commitment: Some(CommitmentConfig::confirmed()),
3397                    ..RpcBlockConfig::default()
3398                }))),
3399            )
3400            .await
3401            .unwrap();
3402
3403        assert!(
3404            res.is_none(),
3405            "A confirmed getBlock request should not expose a slot newer than the confirmed slot"
3406        );
3407    }
3408
3409    #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3410    #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3411    #[tokio::test(flavor = "multi_thread")]
3412    async fn test_get_transaction(version: TransactionVersion) {
3413        let payer = Keypair::new();
3414        let pk = Pubkey::new_unique();
3415        let lamports = LAMPORTS_PER_SOL;
3416        let mut setup = TestSetup::new(SurfpoolFullRpc);
3417        let recent_blockhash = setup
3418            .context
3419            .svm_locker
3420            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3421
3422        let _ = setup
3423            .rpc
3424            .request_airdrop(
3425                Some(setup.context.clone()),
3426                payer.pubkey().to_string(),
3427                2 * lamports,
3428                None,
3429            )
3430            .unwrap();
3431
3432        let tx = match version {
3433            TransactionVersion::Legacy(_) => build_legacy_transaction(
3434                &payer.pubkey(),
3435                &[&payer.insecure_clone()],
3436                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3437                &recent_blockhash,
3438            ),
3439            TransactionVersion::Number(0) => build_v0_transaction(
3440                &payer.pubkey(),
3441                &[&payer.insecure_clone()],
3442                &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3443                &recent_blockhash,
3444            ),
3445            _ => unimplemented!(),
3446        };
3447
3448        setup.process_txs(vec![tx.clone()]).await;
3449
3450        let res = setup
3451            .rpc
3452            .get_transaction(
3453                Some(setup.context.clone()),
3454                tx.signatures[0].to_string(),
3455                Some(RpcEncodingConfigWrapper::Current(Some(
3456                    get_default_transaction_config(),
3457                ))),
3458            )
3459            .await
3460            .unwrap()
3461            .unwrap();
3462
3463        let instructions = tx
3464            .message
3465            .instructions()
3466            .iter()
3467            .map(|ix| UiCompiledInstruction::from(ix, Some(1)))
3468            .collect();
3469
3470        assert_eq!(
3471            res,
3472            EncodedConfirmedTransactionWithStatusMeta {
3473                slot: 123,
3474                transaction: EncodedTransactionWithStatusMeta {
3475                    transaction: EncodedTransaction::Json(UiTransaction {
3476                        signatures: vec![tx.signatures[0].to_string()],
3477                        message: UiMessage::Raw(UiRawMessage {
3478                            header: MessageHeader {
3479                                num_required_signatures: 1,
3480                                num_readonly_signed_accounts: 0,
3481                                num_readonly_unsigned_accounts: 1
3482                            },
3483                            account_keys: vec![
3484                                payer.pubkey().to_string(),
3485                                pk.to_string(),
3486                                system_program::id().to_string()
3487                            ],
3488                            recent_blockhash: recent_blockhash.to_string(),
3489                            instructions,
3490                            address_table_lookups: match tx.message {
3491                                VersionedMessage::Legacy(_) => None,
3492                                VersionedMessage::V0(_) => Some(vec![]),
3493                                VersionedMessage::V1(_) => None,
3494                            },
3495                            transaction_config: None,
3496                        })
3497                    }),
3498                    meta: res.transaction.clone().meta, // Using the same values to avoid reintroducing processing logic errors
3499                    version: Some(version)
3500                },
3501                block_time: res.block_time, // Using the same values to avoid flakyness
3502                transaction_index: None,
3503            }
3504        );
3505    }
3506
3507    #[tokio::test(flavor = "multi_thread")]
3508    #[allow(deprecated)]
3509    async fn test_get_first_available_block() {
3510        let setup = TestSetup::new(SurfpoolFullRpc);
3511
3512        {
3513            let mut svm_writer = setup.context.svm_locker.0.write().await;
3514
3515            let previous_chain_tip = svm_writer.chain_tip.clone();
3516
3517            let latest_entries = svm_writer
3518                .inner
3519                .get_sysvar::<solana_sysvar::recent_blockhashes::RecentBlockhashes>(
3520            );
3521            let latest_entry = latest_entries.first().unwrap();
3522
3523            svm_writer.chain_tip = BlockIdentifier::new(
3524                svm_writer.chain_tip.index + 1,
3525                latest_entry.blockhash.to_string().as_str(),
3526            );
3527
3528            let hash = svm_writer.chain_tip.hash.clone();
3529            let block_height = svm_writer.chain_tip.index;
3530            let parent_slot = svm_writer.get_latest_absolute_slot();
3531
3532            svm_writer
3533                .blocks
3534                .store(
3535                    parent_slot,
3536                    BlockHeader {
3537                        hash,
3538                        previous_blockhash: previous_chain_tip.hash.clone(),
3539                        block_time: chrono::Utc::now().timestamp_millis(),
3540                        block_height,
3541                        parent_slot,
3542                        signatures: Vec::new(),
3543                    },
3544                )
3545                .unwrap();
3546        }
3547
3548        let res = setup
3549            .rpc
3550            .get_first_available_block(Some(setup.context))
3551            .unwrap();
3552
3553        assert_eq!(res, 123);
3554    }
3555
3556    #[test]
3557    fn test_get_latest_blockhash() {
3558        let setup = TestSetup::new(SurfpoolFullRpc);
3559
3560        insert_test_blocks(&setup, 100..=150);
3561
3562        // processed commitment
3563        {
3564            let commitment = CommitmentConfig::processed();
3565            let res = setup
3566                .rpc
3567                .get_latest_blockhash(
3568                    Some(setup.context.clone()),
3569                    Some(RpcContextConfig {
3570                        commitment: Some(commitment.clone()),
3571                        ..Default::default()
3572                    }),
3573                )
3574                .unwrap();
3575            let expected_blockhash = setup
3576                .context
3577                .svm_locker
3578                .get_latest_blockhash(&commitment)
3579                .unwrap();
3580
3581            let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3582            let expected_last_valid_block_height =
3583                current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3584
3585            assert_eq!(
3586                res.value.blockhash,
3587                expected_blockhash.to_string(),
3588                "Latest blockhash does not match expected value"
3589            );
3590            assert_eq!(
3591                res.value.last_valid_block_height, expected_last_valid_block_height,
3592                "Last valid block height does not match expected value"
3593            );
3594        }
3595
3596        // confirmed commitment
3597        {
3598            let commitment = CommitmentConfig::confirmed();
3599            let res = setup
3600                .rpc
3601                .get_latest_blockhash(
3602                    Some(setup.context.clone()),
3603                    Some(RpcContextConfig {
3604                        commitment: Some(commitment.clone()),
3605                        ..Default::default()
3606                    }),
3607                )
3608                .unwrap();
3609            let expected_blockhash = setup
3610                .context
3611                .svm_locker
3612                .get_latest_blockhash(&commitment)
3613                .unwrap();
3614
3615            let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3616            let expected_last_valid_block_height =
3617                current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3618
3619            assert_eq!(
3620                res.value.blockhash,
3621                expected_blockhash.to_string(),
3622                "Latest blockhash does not match expected value"
3623            );
3624            assert_eq!(
3625                res.value.last_valid_block_height, expected_last_valid_block_height,
3626                "Last valid block height does not match expected value"
3627            );
3628        }
3629
3630        // confirmed finalized
3631        {
3632            let commitment = CommitmentConfig::finalized();
3633            let res = setup
3634                .rpc
3635                .get_latest_blockhash(
3636                    Some(setup.context.clone()),
3637                    Some(RpcContextConfig {
3638                        commitment: Some(commitment.clone()),
3639                        ..Default::default()
3640                    }),
3641                )
3642                .unwrap();
3643            let expected_blockhash = setup
3644                .context
3645                .svm_locker
3646                .get_latest_blockhash(&commitment)
3647                .unwrap();
3648
3649            let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3650            let expected_last_valid_block_height =
3651                current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3652
3653            assert_eq!(
3654                res.value.blockhash,
3655                expected_blockhash.to_string(),
3656                "Latest blockhash does not match expected value"
3657            );
3658            assert_eq!(
3659                res.value.last_valid_block_height, expected_last_valid_block_height,
3660                "Last valid block height does not match expected value"
3661            );
3662        }
3663    }
3664
3665    #[tokio::test(flavor = "multi_thread")]
3666    async fn test_get_recent_prioritization_fees() {
3667        let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
3668        let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
3669
3670        let recent_blockhash = setup
3671            .context
3672            .svm_locker
3673            .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3674
3675        let payer_1 = Keypair::new();
3676        let payer_2 = Keypair::new();
3677        let receiver_pubkey = Pubkey::new_unique();
3678        let random_pubkey = Pubkey::new_unique();
3679
3680        // setup accounts
3681        {
3682            let _ = setup
3683                .rpc
3684                .request_airdrop(
3685                    Some(setup.context.clone()),
3686                    payer_1.pubkey().to_string(),
3687                    2 * LAMPORTS_PER_SOL,
3688                    None,
3689                )
3690                .unwrap();
3691            let _ = setup
3692                .rpc
3693                .request_airdrop(
3694                    Some(setup.context.clone()),
3695                    payer_2.pubkey().to_string(),
3696                    2 * LAMPORTS_PER_SOL,
3697                    None,
3698                )
3699                .unwrap();
3700
3701            setup
3702                .context
3703                .svm_locker
3704                .confirm_current_block(&None)
3705                .await
3706                .unwrap();
3707        }
3708
3709        // send two transactions that include a compute budget instruction
3710        {
3711            let tx_1 = build_legacy_transaction(
3712                &payer_1.pubkey(),
3713                &[&payer_1.insecure_clone()],
3714                &[
3715                    system_instruction::transfer(
3716                        &payer_1.pubkey(),
3717                        &receiver_pubkey,
3718                        LAMPORTS_PER_SOL,
3719                    ),
3720                    ComputeBudgetInstruction::set_compute_unit_price(1000),
3721                ],
3722                &recent_blockhash,
3723            );
3724            let tx_2 = build_legacy_transaction(
3725                &payer_2.pubkey(),
3726                &[&payer_2.insecure_clone()],
3727                &[
3728                    system_instruction::transfer(
3729                        &payer_2.pubkey(),
3730                        &receiver_pubkey,
3731                        LAMPORTS_PER_SOL,
3732                    ),
3733                    ComputeBudgetInstruction::set_compute_unit_price(1002),
3734                ],
3735                &recent_blockhash,
3736            );
3737
3738            send_and_await_transaction(tx_1, setup.clone(), mempool_rx.clone())
3739                .await
3740                .join()
3741                .unwrap();
3742            send_and_await_transaction(tx_2, setup.clone(), mempool_rx)
3743                .await
3744                .join()
3745                .unwrap();
3746            setup
3747                .context
3748                .svm_locker
3749                .confirm_current_block(&None)
3750                .await
3751                .unwrap();
3752        }
3753
3754        // sending the get_recent_prioritization_fees request with an account
3755        // should filter the results to only include fees for that account
3756        let res = setup
3757            .rpc
3758            .get_recent_prioritization_fees(
3759                Some(setup.context.clone()),
3760                Some(vec![payer_1.pubkey().to_string()]),
3761            )
3762            .await
3763            .unwrap();
3764        assert_eq!(res.len(), 1);
3765        assert_eq!(res[0].prioritization_fee, 1000);
3766
3767        // sending the get_recent_prioritization_fees request without an account
3768        // should return all prioritization fees
3769        let res = setup
3770            .rpc
3771            .get_recent_prioritization_fees(Some(setup.context.clone()), None)
3772            .await
3773            .unwrap();
3774        assert_eq!(res.len(), 2);
3775        assert_eq!(res[0].prioritization_fee, 1000);
3776        assert_eq!(res[1].prioritization_fee, 1002);
3777
3778        // sending the get_recent_prioritization_fees request with some random account
3779        // to filter should return no results
3780        let res = setup
3781            .rpc
3782            .get_recent_prioritization_fees(
3783                Some(setup.context.clone()),
3784                Some(vec![random_pubkey.to_string()]),
3785            )
3786            .await
3787            .unwrap();
3788        assert!(
3789            res.is_empty(),
3790            "Expected no prioritization fees for random account"
3791        );
3792    }
3793
3794    #[tokio::test(flavor = "multi_thread")]
3795    async fn test_get_blocks_with_limit() {
3796        let setup = TestSetup::new(SurfpoolFullRpc);
3797
3798        insert_test_blocks(&setup, 100..=110);
3799
3800        let result = setup
3801            .rpc
3802            .get_blocks_with_limit(Some(setup.context.clone()), 100, 5, None)
3803            .await
3804            .unwrap();
3805
3806        assert_eq!(result, vec![100, 101, 102, 103, 104]);
3807    }
3808
3809    #[tokio::test(flavor = "multi_thread")]
3810    async fn test_get_blocks_with_limit_exceeds_available() {
3811        let setup = TestSetup::new(SurfpoolFullRpc);
3812
3813        insert_test_blocks(&setup, 100..=102);
3814
3815        let result = setup
3816            .rpc
3817            .get_blocks_with_limit(Some(setup.context.clone()), 100, 10, None)
3818            .await
3819            .unwrap();
3820
3821        assert_eq!(result, vec![100, 101, 102]);
3822    }
3823
3824    #[tokio::test(flavor = "multi_thread")]
3825    async fn test_get_blocks_with_limit_commitment_levels() {
3826        let setup = TestSetup::new(SurfpoolFullRpc);
3827
3828        insert_test_blocks(&setup, 80..=120);
3829
3830        // Test processed commitment (latest = 120)
3831        let processed_result = setup
3832            .rpc
3833            .get_blocks_with_limit(
3834                Some(setup.context.clone()),
3835                115,
3836                10,
3837                Some(RpcContextConfig {
3838                    commitment: Some(CommitmentConfig {
3839                        commitment: CommitmentLevel::Processed,
3840                    }),
3841                    min_context_slot: None,
3842                }),
3843            )
3844            .await
3845            .unwrap();
3846        assert_eq!(processed_result, vec![115, 116, 117, 118, 119, 120]);
3847
3848        // Test confirmed commitment (latest = 119)
3849        let confirmed_result = setup
3850            .rpc
3851            .get_blocks_with_limit(
3852                Some(setup.context.clone()),
3853                115,
3854                10,
3855                Some(RpcContextConfig {
3856                    commitment: Some(CommitmentConfig {
3857                        commitment: CommitmentLevel::Confirmed,
3858                    }),
3859                    min_context_slot: None,
3860                }),
3861            )
3862            .await
3863            .unwrap();
3864        assert_eq!(confirmed_result, vec![115, 116, 117, 118, 119]);
3865
3866        // Test finalized commitment (latest = 120 - 31 = 89)
3867        let finalized_result = setup
3868            .rpc
3869            .get_blocks_with_limit(
3870                Some(setup.context.clone()),
3871                85,
3872                10,
3873                Some(RpcContextConfig {
3874                    commitment: Some(CommitmentConfig {
3875                        commitment: CommitmentLevel::Finalized,
3876                    }),
3877                    min_context_slot: None,
3878                }),
3879            )
3880            .await
3881            .unwrap();
3882        assert_eq!(finalized_result, vec![85, 86, 87, 88, 89]);
3883    }
3884
3885    #[tokio::test(flavor = "multi_thread")]
3886    async fn test_get_blocks_with_limit_sparse_blocks() {
3887        let setup = TestSetup::new(SurfpoolFullRpc);
3888
3889        insert_test_blocks(
3890            &setup,
3891            vec![100, 103, 105, 107, 109, 112, 115, 118, 120, 122],
3892        );
3893
3894        let result = setup
3895            .rpc
3896            .get_blocks_with_limit(Some(setup.context.clone()), 100, 6, None)
3897            .await
3898            .unwrap();
3899
3900        // With sparse block storage, all slots in range are returned (empty blocks are reconstructed)
3901        assert_eq!(result, vec![100, 101, 102, 103, 104, 105]);
3902    }
3903
3904    #[tokio::test(flavor = "multi_thread")]
3905    async fn test_get_blocks_with_limit_empty_result() {
3906        let setup = TestSetup::new(SurfpoolFullRpc);
3907
3908        {
3909            let mut svm_writer = setup.context.svm_locker.0.write().await;
3910            svm_writer.latest_epoch_info.absolute_slot = 100;
3911            // no blocks added - empty blockchain state (but empty blocks can be reconstructed)
3912        }
3913
3914        // request blocks starting at slot 50 with limit 10
3915        let result = setup
3916            .rpc
3917            .get_blocks_with_limit(Some(setup.context.clone()), 50, 10, None)
3918            .await
3919            .unwrap();
3920
3921        // With sparse block storage, empty blocks within the valid slot range are returned
3922        let expected: Vec<Slot> = (50..60).collect();
3923        assert_eq!(result, expected);
3924    }
3925
3926    #[tokio::test(flavor = "multi_thread")]
3927    async fn test_get_blocks_with_limit_large_limit() {
3928        let setup = TestSetup::new(SurfpoolFullRpc);
3929
3930        insert_test_blocks(
3931            &setup,
3932            FINALIZATION_SLOT_THRESHOLD..1000 + FINALIZATION_SLOT_THRESHOLD,
3933        );
3934
3935        let result = setup
3936            .rpc
3937            .get_blocks_with_limit(
3938                Some(setup.context.clone()),
3939                FINALIZATION_SLOT_THRESHOLD,
3940                1000,
3941                None,
3942            )
3943            .await
3944            .unwrap();
3945
3946        assert_eq!(result.len(), 1000);
3947        assert_eq!(result[0], FINALIZATION_SLOT_THRESHOLD);
3948        assert_eq!(result[999], 999 + FINALIZATION_SLOT_THRESHOLD);
3949
3950        for i in 1..result.len() {
3951            assert!(
3952                result[i] > result[i - 1],
3953                "Results should be in ascending order"
3954            );
3955        }
3956    }
3957
3958    #[tokio::test(flavor = "multi_thread")]
3959    async fn test_get_blocks_basic() {
3960        // basic functionality with explicit start and end slots
3961        let setup = TestSetup::new(SurfpoolFullRpc);
3962
3963        insert_test_blocks(&setup, 100..=102);
3964
3965        setup.context.svm_locker.with_svm_writer(|svm_writer| {
3966            svm_writer.latest_epoch_info.absolute_slot = 150;
3967        });
3968
3969        let result = setup
3970            .rpc
3971            .get_blocks(
3972                Some(setup.context.clone()),
3973                100,
3974                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(102))),
3975                None,
3976            )
3977            .await
3978            .unwrap();
3979
3980        assert_eq!(result, vec![100, 101, 102]);
3981    }
3982
3983    #[tokio::test(flavor = "multi_thread")]
3984    async fn test_get_blocks_no_end_slot() {
3985        let setup = TestSetup::new(SurfpoolFullRpc);
3986
3987        insert_test_blocks(&setup, 100..=105);
3988
3989        // test without end slot - should return up to committed latest
3990        let result = setup
3991            .rpc
3992            .get_blocks(
3993                Some(setup.context.clone()),
3994                100,
3995                None,
3996                Some(RpcContextConfig {
3997                    commitment: Some(CommitmentConfig {
3998                        commitment: CommitmentLevel::Confirmed,
3999                    }),
4000                    min_context_slot: None,
4001                }),
4002            )
4003            .await
4004            .unwrap();
4005
4006        // with confirmed commitment, latest should be 105 - 1 = 104
4007        assert_eq!(result, vec![100, 101, 102, 103, 104]);
4008    }
4009
4010    #[tokio::test(flavor = "multi_thread")]
4011    async fn test_get_blocks_commitment_levels() {
4012        let setup = TestSetup::new(SurfpoolFullRpc);
4013
4014        insert_test_blocks(&setup, 50..=100);
4015
4016        // processed commitment -> latest = 100
4017        let processed_result = setup
4018            .rpc
4019            .get_blocks(
4020                Some(setup.context.clone()),
4021                95,
4022                None,
4023                Some(RpcContextConfig {
4024                    commitment: Some(CommitmentConfig {
4025                        commitment: CommitmentLevel::Processed,
4026                    }),
4027                    min_context_slot: None,
4028                }),
4029            )
4030            .await
4031            .unwrap();
4032        assert_eq!(processed_result, vec![95, 96, 97, 98, 99, 100]);
4033
4034        // confirmed commitment -> latest = 99
4035        let confirmed_result = setup
4036            .rpc
4037            .get_blocks(
4038                Some(setup.context.clone()),
4039                95,
4040                None,
4041                Some(RpcContextConfig {
4042                    commitment: Some(CommitmentConfig {
4043                        commitment: CommitmentLevel::Confirmed,
4044                    }),
4045                    min_context_slot: None,
4046                }),
4047            )
4048            .await
4049            .unwrap();
4050        assert_eq!(confirmed_result, vec![95, 96, 97, 98, 99]);
4051
4052        // finalized commitment -> latest = 100 - 31(finalization threshold)
4053        let finalized_result = setup
4054            .rpc
4055            .get_blocks(
4056                Some(setup.context.clone()),
4057                65,
4058                None,
4059                Some(RpcContextConfig {
4060                    commitment: Some(CommitmentConfig {
4061                        commitment: CommitmentLevel::Finalized,
4062                    }),
4063                    min_context_slot: None,
4064                }),
4065            )
4066            .await
4067            .unwrap();
4068        assert_eq!(finalized_result, vec![65, 66, 67, 68, 69]);
4069    }
4070
4071    #[tokio::test(flavor = "multi_thread")]
4072    async fn test_get_blocks_min_context_slot() {
4073        let setup = TestSetup::new(SurfpoolFullRpc);
4074
4075        insert_test_blocks(&setup, 100..=110);
4076
4077        // min_context_slot = 105 > 79, so should return MinContextSlotNotReached error
4078        let result = setup
4079            .rpc
4080            .get_blocks(
4081                Some(setup.context.clone()),
4082                100,
4083                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(105))),
4084                Some(RpcContextConfig {
4085                    commitment: Some(CommitmentConfig::finalized()),
4086                    min_context_slot: Some(105),
4087                }),
4088            )
4089            .await;
4090
4091        assert!(result.is_err());
4092
4093        let result = setup
4094            .rpc
4095            .get_blocks(
4096                Some(setup.context.clone()),
4097                105,
4098                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(108))),
4099                Some(RpcContextConfig {
4100                    commitment: Some(CommitmentConfig {
4101                        commitment: CommitmentLevel::Processed,
4102                    }),
4103                    min_context_slot: Some(105),
4104                }),
4105            )
4106            .await
4107            .unwrap();
4108
4109        assert_eq!(result, vec![105, 106, 107, 108]);
4110    }
4111
4112    #[tokio::test(flavor = "multi_thread")]
4113    async fn test_get_blocks_sparse_blocks() {
4114        let setup = TestSetup::new(SurfpoolFullRpc);
4115
4116        // sparse blocks (only some slots have blocks stored, but all can be reconstructed)
4117        insert_test_blocks(&setup, vec![100, 102, 105, 107, 110]);
4118
4119        let result = setup
4120            .rpc
4121            .get_blocks(
4122                Some(setup.context.clone()),
4123                100,
4124                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(115))),
4125                None,
4126            )
4127            .await
4128            .unwrap();
4129
4130        // With sparse block storage, all slots in range up to latest_slot are returned
4131        // insert_test_blocks sets latest_slot to 110 (max of inserted slots)
4132        let expected: Vec<Slot> = (100..=110).collect();
4133        assert_eq!(result, expected);
4134    }
4135
4136    // helper to insert blocks into the SVM at specific slots
4137    fn insert_test_blocks<I>(setup: &TestSetup<SurfpoolFullRpc>, slots: I)
4138    where
4139        I: IntoIterator<Item = u64>,
4140    {
4141        let slots: Vec<u64> = slots.into_iter().collect();
4142        setup.context.svm_locker.with_svm_writer(|svm_writer| {
4143            for slot in slots.iter() {
4144                svm_writer
4145                    .blocks
4146                    .store(
4147                        *slot,
4148                        BlockHeader {
4149                            hash: SyntheticBlockhash::new(*slot).to_string(),
4150                            previous_blockhash: SyntheticBlockhash::new(slot.saturating_sub(1))
4151                                .to_string(),
4152                            block_time: chrono::Utc::now().timestamp_millis(),
4153                            block_height: *slot,
4154                            parent_slot: slot.saturating_sub(1),
4155                            signatures: vec![],
4156                        },
4157                    )
4158                    .unwrap();
4159            }
4160            svm_writer.latest_epoch_info.absolute_slot = slots.into_iter().max().unwrap_or(0);
4161        });
4162    }
4163
4164    #[tokio::test(flavor = "multi_thread")]
4165    async fn test_get_blocks_local_only() {
4166        let setup = TestSetup::new(SurfpoolFullRpc);
4167
4168        insert_test_blocks(&setup, 50..=100);
4169
4170        // request blocks 75-90 (all local)
4171        let result = setup
4172            .rpc
4173            .get_blocks(
4174                Some(setup.context),
4175                75,
4176                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(90))),
4177                None,
4178            )
4179            .await
4180            .unwrap();
4181
4182        let expected: Vec<Slot> = (75..=90).collect();
4183        assert_eq!(result, expected, "Should return all local blocks in range");
4184    }
4185
4186    #[tokio::test(flavor = "multi_thread")]
4187    async fn test_get_blocks_no_remote_context() {
4188        let setup = TestSetup::new(SurfpoolFullRpc);
4189
4190        insert_test_blocks(&setup, 50..=100);
4191
4192        let result = setup
4193            .rpc
4194            .get_blocks(
4195                Some(setup.context),
4196                FINALIZATION_SLOT_THRESHOLD,
4197                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(60))),
4198                None,
4199            )
4200            .await
4201            .unwrap();
4202
4203        // With sparse block storage, all slots in range are returned (empty blocks reconstructed)
4204        // local_min is now 0, so slots 10-60 are all within local range
4205        let expected: Vec<Slot> = (FINALIZATION_SLOT_THRESHOLD..=60).collect();
4206        assert_eq!(
4207            result, expected,
4208            "Should return all local blocks in range including reconstructed empty blocks"
4209        );
4210    }
4211
4212    #[tokio::test(flavor = "multi_thread")]
4213    async fn test_get_blocks_remote_fetch_below_local_minimum() {
4214        let setup = TestSetup::new(SurfpoolFullRpc);
4215
4216        let local_slots = vec![50, 51, 52, 60, 61, 70, 80, 90, 100];
4217        insert_test_blocks(&setup, local_slots.clone());
4218
4219        // Verify stored blocks minimum (used to be the local_min, but with sparse storage local_min is always 0)
4220        let stored_min = setup
4221            .context
4222            .svm_locker
4223            .with_svm_reader(|svm_reader| svm_reader.blocks.keys().unwrap().into_iter().min());
4224        assert_eq!(
4225            stored_min,
4226            Some(50),
4227            "Stored blocks minimum should be slot 50"
4228        );
4229
4230        let start_slot = FINALIZATION_SLOT_THRESHOLD;
4231        // case 1: request blocks 31-40 (entirely "before" stored blocks, but within reconstructible range)
4232        // With sparse storage, local_min is 0, so slots 31-40 are all within local range
4233        let result = setup
4234            .rpc
4235            .get_blocks(
4236                Some(setup.context.clone()),
4237                start_slot,
4238                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(40))),
4239                None,
4240            )
4241            .await
4242            .unwrap();
4243
4244        // With sparse storage, all slots in range up to latest_slot (100) can be reconstructed
4245        let expected: Vec<Slot> = (start_slot..=40).collect();
4246        assert_eq!(
4247            result, expected,
4248            "Should return all slots in range (empty blocks are reconstructed)"
4249        );
4250
4251        // case 2: request blocks 31-60 (spans entire reconstructible range)
4252        let result = setup
4253            .rpc
4254            .get_blocks(
4255                Some(setup.context.clone()),
4256                start_slot,
4257                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(60))),
4258                None,
4259            )
4260            .await
4261            .unwrap();
4262
4263        let expected: Vec<Slot> = (start_slot..=60).collect();
4264        assert_eq!(
4265            result, expected,
4266            "Should return all local slots (empty blocks reconstructed)"
4267        );
4268
4269        // case 3: request blocks 45-55
4270        let result = setup
4271            .rpc
4272            .get_blocks(
4273                Some(setup.context.clone()),
4274                45,
4275                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(55))),
4276                None,
4277            )
4278            .await
4279            .unwrap();
4280
4281        let expected: Vec<Slot> = (45..=55).collect();
4282        assert_eq!(
4283            result, expected,
4284            "Should return all slots in range (empty blocks reconstructed)"
4285        );
4286
4287        // case 4: Request blocks 55-65
4288        let result = setup
4289            .rpc
4290            .get_blocks(
4291                Some(setup.context),
4292                55,
4293                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(65))),
4294                None,
4295            )
4296            .await
4297            .unwrap();
4298
4299        let expected: Vec<Slot> = (55..=65).collect();
4300        assert_eq!(
4301            result, expected,
4302            "Should return all slots in range (empty blocks reconstructed)"
4303        );
4304    }
4305
4306    #[tokio::test(flavor = "multi_thread")]
4307    async fn test_get_blocks_all_below_range_mock_remote() {
4308        let setup = TestSetup::new(SurfpoolFullRpc);
4309
4310        insert_test_blocks(&setup, 100..=150);
4311
4312        setup.context.svm_locker.with_svm_writer(|svm_writer| {
4313            svm_writer.latest_epoch_info.absolute_slot = 200; // set to 200 so all blocks are "committed"
4314        });
4315
4316        let (stored_min, latest_slot) = setup.context.svm_locker.with_svm_reader(|svm_reader| {
4317            let min = svm_reader.blocks.keys().unwrap().into_iter().min();
4318            let latest = svm_reader.get_latest_absolute_slot();
4319            (min, latest)
4320        });
4321        assert_eq!(stored_min, Some(100), "Stored blocks minimum should be 100");
4322        assert_eq!(latest_slot, 200, "Latest slot should be 200");
4323        let start_slot = FINALIZATION_SLOT_THRESHOLD;
4324        // Case 1: slots start_slot-50 - with sparse storage, all slots in range are returned
4325        let result = setup
4326            .rpc
4327            .get_blocks(
4328                Some(setup.context.clone()),
4329                start_slot,
4330                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(50))),
4331                None,
4332            )
4333            .await
4334            .unwrap();
4335
4336        let expected: Vec<Slot> = (start_slot..=50).collect();
4337        assert_eq!(
4338            result, expected,
4339            "Should return all slots (empty blocks reconstructed)"
4340        );
4341
4342        // case 3: Request blocks 80-120
4343        let result = setup
4344            .rpc
4345            .get_blocks(
4346                Some(setup.context),
4347                80,
4348                Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(120))),
4349                None,
4350            )
4351            .await
4352            .unwrap();
4353
4354        let expected: Vec<Slot> = (80..=120).collect();
4355        assert_eq!(result, expected, "Should return all slots 80-120");
4356    }
4357
4358    #[test]
4359    fn test_get_max_shred_insert_slot() {
4360        let setup = TestSetup::new(SurfpoolFullRpc);
4361
4362        let result = setup
4363            .rpc
4364            .get_max_shred_insert_slot(Some(setup.context.clone()))
4365            .unwrap();
4366        let stake_min_delegation = setup
4367            .rpc
4368            .get_stake_minimum_delegation(Some(setup.context.clone()), None)
4369            .unwrap();
4370
4371        let expected_slot = setup
4372            .context
4373            .svm_locker
4374            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4375
4376        assert_eq!(result, expected_slot);
4377        assert_eq!(stake_min_delegation.context.slot, expected_slot);
4378        assert_eq!(stake_min_delegation.value, 0); // minimum delegation
4379    }
4380
4381    #[test]
4382    fn test_get_max_retransmit_slot() {
4383        let setup = TestSetup::new(SurfpoolFullRpc);
4384
4385        let result = setup
4386            .rpc
4387            .get_max_retransmit_slot(Some(setup.context.clone()))
4388            .unwrap();
4389        let slot = setup
4390            .context
4391            .clone()
4392            .svm_locker
4393            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4394
4395        assert_eq!(result, slot)
4396    }
4397
4398    #[test]
4399    fn test_get_cluster_nodes() {
4400        let setup = TestSetup::new(SurfpoolFullRpc);
4401
4402        let cluster_nodes = setup.rpc.get_cluster_nodes(Some(setup.context)).unwrap();
4403
4404        assert_eq!(
4405            cluster_nodes,
4406            vec![RpcContactInfo {
4407                pubkey: SURFPOOL_IDENTITY_PUBKEY.to_string(),
4408                client_id: None,
4409                gossip: Some("127.0.0.1:8001".parse().unwrap()),
4410                tvu: None,
4411                tpu: Some("127.0.0.1:8003".parse().unwrap()),
4412                tpu_quic: Some("127.0.0.1:8004".parse().unwrap()),
4413                tpu_forwards: None,
4414                tpu_forwards_quic: None,
4415                tpu_vote: None,
4416                serve_repair: None,
4417                rpc: Some("127.0.0.1:8899".parse().unwrap()),
4418                pubsub: Some("127.0.0.1:8900".parse().unwrap()),
4419                version: None,
4420                feature_set: None,
4421                shred_version: None,
4422            }]
4423        );
4424    }
4425
4426    #[test]
4427    fn test_get_stake_minimum_delegation_default() {
4428        let setup = TestSetup::new(SurfpoolFullRpc);
4429
4430        let result = setup
4431            .rpc
4432            .get_max_shred_insert_slot(Some(setup.context.clone()))
4433            .unwrap();
4434
4435        let stake_min_delegation = setup
4436            .rpc
4437            .get_stake_minimum_delegation(Some(setup.context.clone()), None)
4438            .unwrap();
4439
4440        let expected_slot = setup
4441            .context
4442            .svm_locker
4443            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4444
4445        assert_eq!(result, expected_slot);
4446        assert_eq!(stake_min_delegation.context.slot, expected_slot);
4447        assert_eq!(stake_min_delegation.value, 0); // minimum delegation
4448    }
4449
4450    #[test]
4451    fn test_get_stake_minimum_delegation_with_finalized_commitment() {
4452        let setup = TestSetup::new(SurfpoolFullRpc);
4453
4454        let config = Some(RpcContextConfig {
4455            commitment: Some(CommitmentConfig {
4456                commitment: CommitmentLevel::Finalized,
4457            }),
4458            min_context_slot: None,
4459        });
4460
4461        let result = setup
4462            .rpc
4463            .get_stake_minimum_delegation(Some(setup.context.clone()), config)
4464            .unwrap();
4465
4466        // Should return finalized slot
4467        let expected_slot = setup.context.svm_locker.with_svm_reader(|svm_reader| {
4468            svm_reader
4469                .get_latest_absolute_slot()
4470                .saturating_sub(FINALIZATION_SLOT_THRESHOLD)
4471        });
4472
4473        assert_eq!(result.context.slot, expected_slot);
4474        assert_eq!(result.value, 0);
4475    }
4476
4477    #[tokio::test(flavor = "multi_thread")]
4478    async fn test_is_blockhash_valid_recent_blockhash() {
4479        let setup = TestSetup::new(SurfpoolFullRpc);
4480
4481        // Get the current recent blockhash from the SVM
4482        let recent_blockhash = setup
4483            .context
4484            .svm_locker
4485            .with_svm_reader(|svm| svm.latest_blockhash());
4486
4487        let result = setup
4488            .rpc
4489            .is_blockhash_valid(
4490                Some(setup.context.clone()),
4491                recent_blockhash.to_string(),
4492                None,
4493            )
4494            .unwrap();
4495
4496        assert_eq!(result.value, true);
4497        assert!(result.context.slot > 0);
4498
4499        // Test with explicit processed commitment
4500        let result_processed = setup
4501            .rpc
4502            .is_blockhash_valid(
4503                Some(setup.context.clone()),
4504                recent_blockhash.to_string(),
4505                Some(RpcContextConfig {
4506                    commitment: Some(CommitmentConfig {
4507                        commitment: CommitmentLevel::Processed,
4508                    }),
4509                    min_context_slot: None,
4510                }),
4511            )
4512            .unwrap();
4513
4514        assert_eq!(result_processed.value, true);
4515    }
4516
4517    #[tokio::test(flavor = "multi_thread")]
4518    async fn test_is_blockhash_valid_invalid_blockhash() {
4519        let setup = TestSetup::new(SurfpoolFullRpc);
4520
4521        let fake_blockhash = Hash::new_from_array([1u8; 32]);
4522
4523        // Non-existent blockhash returns false
4524        let result = setup
4525            .rpc
4526            .is_blockhash_valid(
4527                Some(setup.context.clone()),
4528                fake_blockhash.to_string(),
4529                None,
4530            )
4531            .unwrap();
4532
4533        assert_eq!(result.value, false);
4534
4535        // Test with different commitment levels - should still be false
4536        let result_confirmed = setup
4537            .rpc
4538            .is_blockhash_valid(
4539                Some(setup.context.clone()),
4540                fake_blockhash.to_string(),
4541                Some(RpcContextConfig {
4542                    commitment: Some(CommitmentConfig {
4543                        commitment: CommitmentLevel::Confirmed,
4544                    }),
4545                    min_context_slot: None,
4546                }),
4547            )
4548            .unwrap();
4549
4550        assert_eq!(result_confirmed.value, false);
4551
4552        // Test another fake blockhash to be thorough
4553        let another_fake = Hash::new_from_array([255u8; 32]);
4554        let result2 = setup
4555            .rpc
4556            .is_blockhash_valid(Some(setup.context.clone()), another_fake.to_string(), None)
4557            .unwrap();
4558
4559        assert_eq!(result2.value, false);
4560
4561        let invalid_result = setup.rpc.is_blockhash_valid(
4562            Some(setup.context.clone()),
4563            "invalid-blockhash-format".to_string(),
4564            None,
4565        );
4566
4567        assert!(invalid_result.is_err());
4568
4569        let short_result =
4570            setup
4571                .rpc
4572                .is_blockhash_valid(Some(setup.context.clone()), "123".to_string(), None);
4573        assert!(short_result.is_err());
4574
4575        // Test with invalid base58 characters
4576        let invalid_chars_result =
4577            setup
4578                .rpc
4579                .is_blockhash_valid(Some(setup.context.clone()), "0OIl".to_string(), None);
4580        assert!(invalid_chars_result.is_err());
4581    }
4582
4583    #[tokio::test(flavor = "multi_thread")]
4584    async fn test_is_blockhash_valid_commitment_and_context_slot() {
4585        let setup = TestSetup::new(SurfpoolFullRpc);
4586
4587        // Set up some block history to test commitment levels
4588        insert_test_blocks(&setup, 70..=100);
4589
4590        let recent_blockhash = setup
4591            .context
4592            .svm_locker
4593            .with_svm_reader(|svm| svm.latest_blockhash());
4594
4595        // Test processed commitment (should use latest slot = 100)
4596        let processed_result = setup
4597            .rpc
4598            .is_blockhash_valid(
4599                Some(setup.context.clone()),
4600                recent_blockhash.to_string(),
4601                Some(RpcContextConfig {
4602                    commitment: Some(CommitmentConfig {
4603                        commitment: CommitmentLevel::Processed,
4604                    }),
4605                    min_context_slot: None,
4606                }),
4607            )
4608            .unwrap();
4609
4610        assert_eq!(processed_result.value, true);
4611        assert_eq!(processed_result.context.slot, 100);
4612
4613        // Test confirmed commitment (should use slot = 99)
4614        let confirmed_result = setup
4615            .rpc
4616            .is_blockhash_valid(
4617                Some(setup.context.clone()),
4618                recent_blockhash.to_string(),
4619                Some(RpcContextConfig {
4620                    commitment: Some(CommitmentConfig {
4621                        commitment: CommitmentLevel::Confirmed,
4622                    }),
4623                    min_context_slot: None,
4624                }),
4625            )
4626            .unwrap();
4627
4628        assert_eq!(confirmed_result.value, true);
4629        assert_eq!(confirmed_result.context.slot, 99);
4630
4631        // Test finalized commitment (should use slot = 100 - 31 = 69)
4632        let finalized_result = setup
4633            .rpc
4634            .is_blockhash_valid(
4635                Some(setup.context.clone()),
4636                recent_blockhash.to_string(),
4637                Some(RpcContextConfig {
4638                    commitment: Some(CommitmentConfig {
4639                        commitment: CommitmentLevel::Finalized,
4640                    }),
4641                    min_context_slot: None,
4642                }),
4643            )
4644            .unwrap();
4645
4646        assert_eq!(finalized_result.value, true);
4647        assert_eq!(finalized_result.context.slot, 69);
4648
4649        // Test min_context_slot validation - should succeed when slot is high enough
4650        let min_context_success = setup
4651            .rpc
4652            .is_blockhash_valid(
4653                Some(setup.context.clone()),
4654                recent_blockhash.to_string(),
4655                Some(RpcContextConfig {
4656                    commitment: Some(CommitmentConfig {
4657                        commitment: CommitmentLevel::Processed,
4658                    }),
4659                    min_context_slot: Some(95),
4660                }),
4661            )
4662            .unwrap();
4663
4664        assert_eq!(min_context_success.value, true);
4665
4666        // Test min_context_slot validation - should fail when slot is too low
4667        let min_context_failure = setup.rpc.is_blockhash_valid(
4668            Some(setup.context.clone()),
4669            recent_blockhash.to_string(),
4670            Some(RpcContextConfig {
4671                commitment: Some(CommitmentConfig {
4672                    commitment: CommitmentLevel::Finalized,
4673                }),
4674                min_context_slot: Some(80),
4675            }),
4676        );
4677
4678        assert!(min_context_failure.is_err());
4679    }
4680
4681    #[ignore = "requires-network"]
4682    #[tokio::test(flavor = "multi_thread")]
4683    async fn test_minimum_ledger_slot_from_remote() {
4684        // Forwarding to remote mainnet
4685        let remote_client = SurfnetRemoteClient::new("https://api.mainnet-beta.solana.com");
4686        let mut setup = TestSetup::new(SurfpoolFullRpc);
4687        setup.context.remote_rpc_client = Some(remote_client);
4688
4689        let result = setup
4690            .rpc
4691            .minimum_ledger_slot(Some(setup.context))
4692            .await
4693            .unwrap();
4694
4695        assert!(
4696            result > 0,
4697            "Mainnet should return a valid minimum ledger slot > 0"
4698        );
4699        println!("Mainnet minimum ledger slot: {}", result);
4700    }
4701
4702    #[tokio::test(flavor = "multi_thread")]
4703    async fn test_minimum_ledger_slot_missing_context_fails() {
4704        // fail gracefully when called without metadata context
4705        let setup = TestSetup::new(SurfpoolFullRpc);
4706
4707        let result = setup.rpc.minimum_ledger_slot(None).await;
4708
4709        assert!(
4710            result.is_err(),
4711            "Should fail when called without metadata context"
4712        );
4713    }
4714
4715    #[tokio::test(flavor = "multi_thread")]
4716    async fn test_minimum_ledger_slot_finds_minimum() {
4717        // With sparse block storage, minimum ledger slot is always FINALIZATION_SLOT_THRESHOLD for local surfnets
4718        let setup = TestSetup::new(SurfpoolFullRpc);
4719
4720        insert_test_blocks(&setup, vec![500, 100, 1000, 50, 750]);
4721
4722        let result = setup
4723            .rpc
4724            .minimum_ledger_slot(Some(setup.context))
4725            .await
4726            .unwrap();
4727
4728        // With sparse block storage, get_first_local_slot() returns 0 since all
4729        // blocks from slot 0 can be reconstructed on-the-fly
4730        assert_eq!(
4731            result, FINALIZATION_SLOT_THRESHOLD,
4732            "Should return FINALIZATION_SLOT_THRESHOLD since empty blocks can be reconstructed from that slot"
4733        );
4734    }
4735
4736    #[tokio::test(flavor = "multi_thread")]
4737    async fn test_get_inflation_reward() {
4738        let setup = TestSetup::new(SurfpoolFullRpc);
4739
4740        let (epoch, effective_slot) =
4741            setup
4742                .context
4743                .clone()
4744                .svm_locker
4745                .with_svm_reader(|svm_reader| {
4746                    (
4747                        svm_reader.latest_epoch_info().epoch,
4748                        svm_reader.get_latest_absolute_slot(),
4749                    )
4750                });
4751
4752        let result = setup
4753            .rpc
4754            .get_inflation_reward(
4755                Some(setup.context),
4756                vec![Pubkey::new_unique().to_string()],
4757                None,
4758            )
4759            .await
4760            .unwrap();
4761
4762        assert_eq!(
4763            result[0],
4764            Some(RpcInflationReward {
4765                epoch,
4766                effective_slot,
4767                amount: 0,
4768                post_balance: 0,
4769                commission: None,
4770                commission_bps: None,
4771            })
4772        )
4773    }
4774
4775    /// tests for skip_sig_verify feature
4776    mod test_skip_sig_verify {
4777        use solana_client::rpc_config::RpcSendTransactionConfig;
4778        use solana_signature::Signature;
4779
4780        use super::*;
4781
4782        fn build_transaction_with_invalid_signature(
4783            payer: &Keypair,
4784            recipient: &Pubkey,
4785            recent_blockhash: &Hash,
4786        ) -> VersionedTransaction {
4787            let msg = VersionedMessage::Legacy(LegacyMessage::new_with_blockhash(
4788                &[system_instruction::transfer(
4789                    &payer.pubkey(),
4790                    recipient,
4791                    LAMPORTS_PER_SOL,
4792                )],
4793                Some(&payer.pubkey()),
4794                recent_blockhash,
4795            ));
4796
4797            VersionedTransaction {
4798                signatures: vec![Signature::new_unique()],
4799                message: msg,
4800            }
4801        }
4802
4803        #[tokio::test(flavor = "multi_thread")]
4804        async fn test_send_transaction_with_skip_sig_verify_succeeds() {
4805            let payer = Keypair::new();
4806            let recipient = Pubkey::new_unique();
4807            let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
4808            let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
4809            let recent_blockhash = setup
4810                .context
4811                .svm_locker
4812                .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
4813
4814            let _ = setup
4815                .context
4816                .svm_locker
4817                .0
4818                .write()
4819                .await
4820                .airdrop(&payer.pubkey(), 2 * LAMPORTS_PER_SOL);
4821
4822            let tx =
4823                build_transaction_with_invalid_signature(&payer, &recipient, &recent_blockhash);
4824            let tx_encoded = bs58::encode(bincode::serialize(&tx).unwrap()).into_string();
4825
4826            let config = SurfpoolRpcSendTransactionConfig {
4827                base: RpcSendTransactionConfig::default(),
4828                skip_sig_verify: Some(true),
4829            };
4830
4831            let setup_clone = setup.clone();
4832            let handle = hiro_system_kit::thread_named("send_tx_skip_verify")
4833                .spawn(move || {
4834                    setup_clone.rpc.send_transaction(
4835                        Some(setup_clone.context),
4836                        tx_encoded,
4837                        Some(config),
4838                    )
4839                })
4840                .unwrap();
4841
4842            loop {
4843                match mempool_rx.recv() {
4844                    Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => {
4845                        let mut writer = setup.context.svm_locker.0.write().await;
4846                        let slot = writer.get_latest_absolute_slot();
4847                        writer.transactions_queued_for_confirmation.push_back((
4848                            tx.clone(),
4849                            status_tx.clone(),
4850                            None,
4851                        ));
4852                        let sig = tx.signatures[0];
4853                        let tx_with_status_meta = TransactionWithStatusMeta {
4854                            slot,
4855                            transaction: tx,
4856                            ..Default::default()
4857                        };
4858                        let mutated_accounts = std::collections::HashSet::new();
4859                        writer
4860                            .transactions
4861                            .store(
4862                                sig.to_string(),
4863                                SurfnetTransactionStatus::processed(
4864                                    tx_with_status_meta,
4865                                    mutated_accounts,
4866                                ),
4867                            )
4868                            .unwrap();
4869                        status_tx
4870                            .send(TransactionStatusEvent::Success(
4871                                TransactionConfirmationStatus::Processed,
4872                            ))
4873                            .unwrap();
4874                        break;
4875                    }
4876                    _ => continue,
4877                }
4878            }
4879
4880            let result = handle.join().unwrap();
4881            assert!(
4882                result.is_ok(),
4883                "Transaction with skip_sig_verify=true should succeed: {:?}",
4884                result
4885            );
4886        }
4887
4888        #[test]
4889        fn test_surfpool_rpc_send_transaction_config_json_serialization() {
4890            // Test that the config serializes correctly with serde flatten
4891            let config = SurfpoolRpcSendTransactionConfig {
4892                base: RpcSendTransactionConfig {
4893                    skip_preflight: true,
4894                    ..Default::default()
4895                },
4896                skip_sig_verify: Some(true),
4897            };
4898
4899            let json = serde_json::to_string(&config).unwrap();
4900            assert!(json.contains("skipSigVerify"));
4901            assert!(json.contains("skipPreflight"));
4902
4903            // Verify it can be deserialized back
4904            let parsed: SurfpoolRpcSendTransactionConfig = serde_json::from_str(&json).unwrap();
4905            assert_eq!(parsed.skip_sig_verify, Some(true));
4906            assert!(parsed.base.skip_preflight);
4907        }
4908
4909        #[test]
4910        fn test_surfpool_rpc_send_transaction_config_backwards_compatible() {
4911            // Test that a standard Solana RPC config can be parsed (skip_sig_verify absent)
4912            let json = r#"{"skipPreflight": true}"#;
4913            let parsed: SurfpoolRpcSendTransactionConfig = serde_json::from_str(json).unwrap();
4914            assert!(parsed.base.skip_preflight);
4915            assert!(
4916                parsed.skip_sig_verify.is_none(),
4917                "skip_sig_verify should be None when not provided"
4918            );
4919        }
4920
4921        #[test]
4922        fn test_surfpool_rpc_send_transaction_config_defaults() {
4923            let config = SurfpoolRpcSendTransactionConfig::default();
4924            assert!(
4925                config.skip_sig_verify.is_none(),
4926                "skip_sig_verify should default to None"
4927            );
4928            assert!(
4929                !config.base.skip_preflight,
4930                "skip_preflight should default to false"
4931            );
4932        }
4933
4934        #[test]
4935        fn test_surfpool_rpc_send_transaction_config_with_skip_sig_verify() {
4936            let config = SurfpoolRpcSendTransactionConfig {
4937                base: RpcSendTransactionConfig::default(),
4938                skip_sig_verify: Some(true),
4939            };
4940            assert_eq!(config.skip_sig_verify, Some(true));
4941
4942            let config_false = SurfpoolRpcSendTransactionConfig {
4943                base: RpcSendTransactionConfig::default(),
4944                skip_sig_verify: Some(false),
4945            };
4946            assert_eq!(config_false.skip_sig_verify, Some(false));
4947        }
4948    }
4949
4950    mod test_skip_blockhash_check {
4951        use super::*;
4952
4953        fn build_transaction_with_invalid_blockhash(
4954            payer: &Keypair,
4955            recipient: &Pubkey,
4956            lamports: u64,
4957        ) -> VersionedTransaction {
4958            build_legacy_transaction(
4959                &payer.pubkey(),
4960                &[payer],
4961                &[system_instruction::transfer(
4962                    &payer.pubkey(),
4963                    recipient,
4964                    lamports,
4965                )],
4966                &Hash::new_unique(),
4967            )
4968        }
4969
4970        #[tokio::test(flavor = "multi_thread")]
4971        async fn test_simulate_transaction_invalid_blockhash_fails_by_default() {
4972            let payer = Keypair::new();
4973            let recipient = Pubkey::new_unique();
4974            let lamports = LAMPORTS_PER_SOL;
4975            let setup = TestSetup::new(SurfpoolFullRpc);
4976
4977            let _ = setup
4978                .rpc
4979                .request_airdrop(
4980                    Some(setup.context.clone()),
4981                    payer.pubkey().to_string(),
4982                    2 * lamports,
4983                    None,
4984                )
4985                .unwrap();
4986
4987            let tx = build_transaction_with_invalid_blockhash(&payer, &recipient, lamports);
4988            assert!(
4989                !setup.context.svm_locker.with_svm_reader(|svm_reader| {
4990                    svm_reader.check_blockhash_is_recent(tx.message.recent_blockhash())
4991                }),
4992                "test transaction should use a non-recent blockhash"
4993            );
4994
4995            let simulation_res = setup
4996                .rpc
4997                .simulate_transaction(
4998                    Some(setup.context),
4999                    bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
5000                    Some(RpcSimulateTransactionConfig {
5001                        sig_verify: true,
5002                        replace_recent_blockhash: false,
5003                        commitment: Some(CommitmentConfig::finalized()),
5004                        encoding: None,
5005                        accounts: Some(RpcSimulateTransactionAccountsConfig {
5006                            encoding: None,
5007                            addresses: vec![recipient.to_string()],
5008                        }),
5009                        min_context_slot: None,
5010                        inner_instructions: false,
5011                    }),
5012                )
5013                .await
5014                .unwrap();
5015
5016            assert_eq!(
5017                simulation_res.value.err,
5018                Some(TransactionError::BlockhashNotFound.into())
5019            );
5020        }
5021
5022        #[tokio::test(flavor = "multi_thread")]
5023        async fn test_simulate_transaction_invalid_blockhash_succeeds_when_skipped() {
5024            let payer = Keypair::new();
5025            let recipient = Pubkey::new_unique();
5026            let lamports = LAMPORTS_PER_SOL;
5027            let setup = TestSetup::new(SurfpoolFullRpc);
5028
5029            setup
5030                .context
5031                .svm_locker
5032                .with_svm_writer(|svm_writer| svm_writer.skip_blockhash_check = true);
5033
5034            let _ = setup
5035                .rpc
5036                .request_airdrop(
5037                    Some(setup.context.clone()),
5038                    payer.pubkey().to_string(),
5039                    2 * lamports,
5040                    None,
5041                )
5042                .unwrap();
5043
5044            let tx = build_transaction_with_invalid_blockhash(&payer, &recipient, lamports);
5045            assert!(
5046                !setup.context.svm_locker.with_svm_reader(|svm_reader| {
5047                    svm_reader.check_blockhash_is_recent(tx.message.recent_blockhash())
5048                }),
5049                "test transaction should use a non-recent blockhash"
5050            );
5051
5052            let simulation_res = setup
5053                .rpc
5054                .simulate_transaction(
5055                    Some(setup.context),
5056                    bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
5057                    Some(RpcSimulateTransactionConfig {
5058                        sig_verify: true,
5059                        replace_recent_blockhash: false,
5060                        commitment: Some(CommitmentConfig::finalized()),
5061                        encoding: None,
5062                        accounts: Some(RpcSimulateTransactionAccountsConfig {
5063                            encoding: None,
5064                            addresses: vec![recipient.to_string()],
5065                        }),
5066                        min_context_slot: None,
5067                        inner_instructions: false,
5068                    }),
5069                )
5070                .await
5071                .unwrap();
5072
5073            assert_eq!(simulation_res.value.err, None);
5074        }
5075    }
5076}