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