Skip to main content

surfpool_core/rpc/
full.rs

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