surfpool_core/rpc/
minimal.rs

1use jsonrpc_core::{BoxFuture, Result};
2use jsonrpc_derive::rpc;
3use solana_client::{
4    rpc_config::{
5        RpcContextConfig, RpcGetVoteAccountsConfig, RpcLeaderScheduleConfig,
6        RpcLeaderScheduleConfigWrapper,
7    },
8    rpc_custom_error::RpcCustomError,
9    rpc_response::{
10        RpcIdentity, RpcLeaderSchedule, RpcResponseContext, RpcSnapshotSlotInfo,
11        RpcVoteAccountStatus,
12    },
13};
14use solana_clock::Slot;
15use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
16use solana_epoch_info::EpochInfo;
17use solana_rpc_client_api::response::Response as RpcResponse;
18
19use super::{RunloopContext, SurfnetRpcContext};
20use crate::{
21    rpc::{utils::verify_pubkey, State},
22    surfnet::{
23        locker::SvmAccessContext, GetAccountResult, FINALIZATION_SLOT_THRESHOLD,
24        SURFPOOL_IDENTITY_PUBKEY,
25    },
26};
27
28const SURFPOOL_VERSION: &str = env!("CARGO_PKG_VERSION");
29
30#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
31#[serde(rename_all = "kebab-case")]
32pub struct SurfpoolRpcVersionInfo {
33    /// The current version of surfpool
34    pub surfnet_version: String,
35    /// The current version of solana-core
36    pub solana_core: String,
37    /// first 4 bytes of the FeatureSet identifier
38    pub feature_set: Option<u32>,
39}
40
41#[rpc]
42pub trait Minimal {
43    type Metadata;
44
45    /// Returns the balance (in lamports) of the account at the provided public key.
46    ///
47    /// This endpoint queries the current or historical balance of an account, depending on the optional commitment level provided in the config.
48    ///
49    /// ## Parameters
50    /// - `pubkey_str`: The base-58 encoded public key of the account to query.
51    /// - `_config` *(optional)*: [`RpcContextConfig`] specifying commitment level and/or minimum context slot.
52    ///
53    /// ## Returns
54    /// An [`RpcResponse<u64>`] where the value is the balance in lamports.
55    ///
56    /// ## Example Request
57    /// ```json
58    /// {
59    ///   "jsonrpc": "2.0",
60    ///   "id": 1,
61    ///   "method": "getBalance",
62    ///   "params": [
63    ///     "4Nd1mXUmh23rQk8VN7wM9hEnfxqrrB1yrn11eW9gMoVr"
64    ///   ]
65    /// }
66    /// ```
67    ///
68    /// ## Example Response
69    /// ```json
70    /// {
71    ///   "jsonrpc": "2.0",
72    ///   "result": {
73    ///     "context": {
74    ///       "slot": 1085597
75    ///     },
76    ///     "value": 20392800
77    ///   },
78    ///   "id": 1
79    /// }
80    /// ```
81    ///
82    /// # Notes
83    /// - 1 SOL = 1,000,000,000 lamports.
84    /// - Use commitment level in the config to specify whether the balance should be fetched from processed, confirmed, or finalized state.
85    ///
86    /// # See Also
87    /// - `getAccountInfo`, `getTokenAccountBalance`
88    #[rpc(meta, name = "getBalance")]
89    fn get_balance(
90        &self,
91        meta: Self::Metadata,
92        pubkey_str: String,
93        _config: Option<RpcContextConfig>,
94    ) -> BoxFuture<Result<RpcResponse<u64>>>;
95
96    /// Returns information about the current epoch.
97    ///
98    /// This endpoint provides epoch-related data such as the current epoch number, the total number of slots in the epoch,
99    /// the current slot index within the epoch, and the absolute slot number.
100    ///
101    /// ## Parameters
102    /// - `config` *(optional)*: [`RpcContextConfig`] for specifying commitment level and/or minimum context slot.
103    ///
104    /// ## Returns
105    /// An [`EpochInfo`] struct containing information about the current epoch.
106    ///
107    /// ## Example Request
108    /// ```json
109    /// {
110    ///   "jsonrpc": "2.0",
111    ///   "id": 1,
112    ///   "method": "getEpochInfo"
113    /// }
114    /// ```
115    ///
116    /// ## Example Response
117    /// ```json
118    /// {
119    ///   "jsonrpc": "2.0",
120    ///   "result": {
121    ///     "epoch": 278,
122    ///     "slotIndex": 423,
123    ///     "slotsInEpoch": 432000,
124    ///     "absoluteSlot": 124390823,
125    ///     "blockHeight": 18962432,
126    ///     "transactionCount": 981234523
127    ///   },
128    ///   "id": 1
129    /// }
130    /// ```
131    ///
132    /// # Notes
133    /// - The `slotIndex` is the current slot's position within the epoch.
134    /// - `slotsInEpoch` may vary due to network adjustments (e.g., warm-up periods).
135    /// - The `commitment` field in the config can influence how recent the returned data is.
136    ///
137    /// # See Also
138    /// - `getEpochSchedule`, `getSlot`, `getBlockHeight`
139    #[rpc(meta, name = "getEpochInfo")]
140    fn get_epoch_info(
141        &self,
142        meta: Self::Metadata,
143        config: Option<RpcContextConfig>,
144    ) -> Result<EpochInfo>;
145
146    /// Returns the genesis hash of the blockchain.
147    ///
148    /// The genesis hash is a unique identifier that represents the state of the blockchain at the genesis block (the very first block).
149    /// This can be used to validate the integrity of the blockchain and ensure that a node is operating with the correct blockchain data.
150    ///
151    /// ## Parameters
152    /// - None.
153    ///
154    /// ## Returns
155    /// A `String` containing the base-58 encoded genesis hash.
156    ///
157    /// ## Example Request
158    /// ```json
159    /// {
160    ///   "jsonrpc": "2.0",
161    ///   "id": 1,
162    ///   "method": "getGenesisHash"
163    /// }
164    /// ```
165    ///
166    /// ## Example Response
167    /// ```json
168    /// {
169    ///   "jsonrpc": "2.0",
170    ///   "result": "5eymX3jrWXcKqD1tsB2BzAB6gX9LP2pLrpVG6KwBSoZJ"
171    ///   "id": 1
172    /// }
173    /// ```
174    ///
175    /// # Notes
176    /// - The genesis hash is a critical identifier for validating the blockchain’s origin and initial state.
177    /// - This endpoint does not require any parameters and provides a quick way to verify the genesis hash for blockchain verification or initial setup.
178    ///
179    /// # See Also
180    /// - `getEpochInfo`, `getBlock`, `getClusterNodes`
181    #[rpc(meta, name = "getGenesisHash")]
182    fn get_genesis_hash(&self, meta: Self::Metadata) -> BoxFuture<Result<String>>;
183
184    /// Returns the health status of the blockchain node.
185    ///
186    /// This method checks the health of the node and returns a status indicating whether the node is in a healthy state
187    /// or if it is experiencing any issues such as being out of sync with the network or encountering any failures.
188    ///
189    /// ## Parameters
190    /// - None.
191    ///
192    /// ## Returns
193    /// A `String` indicating the health status of the node:
194    /// - `"ok"`: The node is healthy and synchronized with the network.
195    /// - `"failed"`: The node is not healthy or is experiencing issues.
196    ///
197    /// ## Example Request
198    /// ```json
199    /// {
200    ///   "jsonrpc": "2.0",
201    ///   "id": 1,
202    ///   "method": "getHealth"
203    /// }
204    /// ```
205    ///
206    /// ## Example Response
207    /// ```json
208    /// {
209    ///   "jsonrpc": "2.0",
210    ///   "result": "ok",
211    ///   "id": 1
212    /// }
213    /// ```
214    ///
215    /// # Notes
216    /// - The `"ok"` response means that the node is fully operational and synchronized with the blockchain.
217    /// - The `"failed"` response indicates that the node is either out of sync, has encountered an error, or is not functioning properly.
218    /// - This is typically used to monitor the health of the node in production environments.
219    ///
220    /// # See Also
221    /// - `getGenesisHash`, `getEpochInfo`, `getBlock`
222    #[rpc(meta, name = "getHealth")]
223    fn get_health(&self, meta: Self::Metadata) -> Result<String>;
224
225    /// Returns the identity (public key) of the node.
226    ///
227    /// This method retrieves the current identity of the node, which is represented by a public key.
228    /// The identity is used to uniquely identify the node on the network.
229    ///
230    /// ## Parameters
231    /// - None.
232    ///
233    /// ## Returns
234    /// A `RpcIdentity` object containing the identity of the node:
235    /// - `identity`: The base-58 encoded public key of the node's identity.
236    ///
237    /// ## Example Request
238    /// ```json
239    /// {
240    ///   "jsonrpc": "2.0",
241    ///   "id": 1,
242    ///   "method": "getIdentity"
243    /// }
244    /// ```
245    ///
246    /// ## Example Response
247    /// ```json
248    /// {
249    ///   "jsonrpc": "2.0",
250    ///   "result": {
251    ///     "identity": "Base58EncodedPublicKeyHere"
252    ///   },
253    ///   "id": 1
254    /// }
255    /// ```
256    ///
257    /// # Notes
258    /// - The identity returned is a base-58 encoded public key representing the current node.
259    /// - This identity is often used for network identification and security.
260    ///
261    /// # See Also
262    /// - `getGenesisHash`, `getHealth`, `getBlock`
263    #[rpc(meta, name = "getIdentity")]
264    fn get_identity(&self, meta: Self::Metadata) -> Result<RpcIdentity>;
265
266    /// Returns the current slot of the ledger.
267    ///
268    /// This method retrieves the current slot number in the blockchain, which represents a point in the ledger's history.
269    /// Slots are used to organize and validate the timing of transactions in the network.
270    ///
271    /// ## Parameters
272    /// - `config` (optional): Configuration options for the request, such as commitment level or context slot. Defaults to `None`.
273    ///
274    /// ## Returns
275    /// A `Slot` value representing the current slot of the ledger.
276    ///
277    /// ## Example Request
278    /// ```json
279    /// {
280    ///   "jsonrpc": "2.0",
281    ///   "id": 1,
282    ///   "method": "getSlot"
283    /// }
284    /// ```
285    ///
286    /// ## Example Response
287    /// ```json
288    /// {
289    ///   "jsonrpc": "2.0",
290    ///   "result": 12345678,
291    ///   "id": 1
292    /// }
293    /// ```
294    ///
295    /// # Notes
296    /// - The slot represents the position in the ledger. It increments over time as new blocks are produced.
297    ///
298    /// # See Also
299    /// - `getBlock`, `getEpochInfo`, `getGenesisHash`
300    #[rpc(meta, name = "getSlot")]
301    fn get_slot(&self, meta: Self::Metadata, config: Option<RpcContextConfig>) -> Result<Slot>;
302
303    /// Returns the current block height.
304    ///
305    /// This method retrieves the height of the most recent block in the ledger, which is an indicator of how many blocks have been added to the blockchain. The block height is the number of blocks that have been produced since the genesis block.
306    ///
307    /// ## Parameters
308    /// - `config` (optional): Configuration options for the request, such as commitment level or context slot. Defaults to `None`.
309    ///
310    /// ## Returns
311    /// A `u64` representing the current block height of the ledger.
312    ///
313    /// ## Example Request
314    /// ```json
315    /// {
316    ///   "jsonrpc": "2.0",
317    ///   "id": 1,
318    ///   "method": "getBlockHeight"
319    /// }
320    /// ```
321    ///
322    /// ## Example Response
323    /// ```json
324    /// {
325    ///   "jsonrpc": "2.0",
326    ///   "result": 12345678,
327    ///   "id": 1
328    /// }
329    /// ```
330    ///
331    /// # Notes
332    /// - The block height reflects the number of blocks produced in the ledger, starting from the genesis block. It is incremented each time a new block is added.
333    ///
334    /// # See Also
335    /// - `getSlot`, `getEpochInfo`, `getGenesisHash`
336    #[rpc(meta, name = "getBlockHeight")]
337    fn get_block_height(
338        &self,
339        meta: Self::Metadata,
340        config: Option<RpcContextConfig>,
341    ) -> Result<u64>;
342
343    /// Returns information about the highest snapshot slot.
344    ///
345    /// This method retrieves information about the most recent snapshot slot, which refers to the slot in the blockchain where the most recent snapshot has been taken. A snapshot is a point-in-time capture of the state of the ledger, allowing for quicker validation of the state without processing every transaction.
346    ///
347    /// ## Parameters
348    /// - `meta`: Metadata passed with the request, such as the client’s request context.
349    ///
350    /// ## Returns
351    /// A `RpcSnapshotSlotInfo` containing information about the highest snapshot slot.
352    ///
353    /// ## Example Request
354    /// ```json
355    /// {
356    ///   "jsonrpc": "2.0",
357    ///   "id": 1,
358    ///   "method": "getHighestSnapshotSlot"
359    /// }
360    /// ```
361    ///
362    /// ## Example Response
363    /// ```json
364    /// {
365    ///   "jsonrpc": "2.0",
366    ///   "result": {
367    ///     "slot": 987654,
368    ///     "root": "A9B7F1A4D1D55D0635B905E5AB6341C5D9F7F4D2A1160C53B5647B1E3259BB24"
369    ///   },
370    ///   "id": 1
371    /// }
372    /// ```
373    ///
374    /// # Notes
375    /// - The snapshot slot represents the most recent snapshot in the blockchain and is used for more efficient state validation and recovery.
376    /// - The result also includes the root, which is the blockhash at the snapshot point.
377    ///
378    /// # See Also
379    /// - `getBlock`, `getSnapshotInfo`
380    #[rpc(meta, name = "getHighestSnapshotSlot")]
381    fn get_highest_snapshot_slot(&self, meta: Self::Metadata) -> Result<RpcSnapshotSlotInfo>;
382
383    /// Returns the total number of transactions processed by the blockchain.
384    ///
385    /// This method retrieves the number of transactions that have been processed in the blockchain up to the current point. It provides a snapshot of the transaction throughput and can be useful for monitoring and performance analysis.
386    ///
387    /// ## Parameters
388    /// - `meta`: Metadata passed with the request, such as the client’s request context.
389    /// - `config`: Optional configuration for the request, such as commitment settings or minimum context slot.
390    ///
391    /// ## Returns
392    /// A `u64` representing the total transaction count.
393    ///
394    /// ## Example Request
395    /// ```json
396    /// {
397    ///   "jsonrpc": "2.0",
398    ///   "id": 1,
399    ///   "method": "getTransactionCount"
400    /// }
401    /// ```
402    ///
403    /// ## Example Response
404    /// ```json
405    /// {
406    ///   "jsonrpc": "2.0",
407    ///   "result": 1234567890,
408    ///   "id": 1
409    /// }
410    /// ```
411    ///
412    /// # Notes
413    /// - This method gives a cumulative count of all transactions in the blockchain from the start of the network.
414    ///
415    /// # See Also
416    /// - `getBlockHeight`, `getEpochInfo`
417    #[rpc(meta, name = "getTransactionCount")]
418    fn get_transaction_count(
419        &self,
420        meta: Self::Metadata,
421        config: Option<RpcContextConfig>,
422    ) -> Result<u64>;
423
424    /// Returns the current version of the server or application.
425    ///
426    /// This method retrieves the version information for the server or application. It provides details such as the version number and additional metadata that can help with compatibility checks or updates.
427    ///
428    /// ## Parameters
429    /// - `meta`: Metadata passed with the request, such as the client’s request context.
430    ///
431    /// ## Returns
432    /// A `SurfpoolRpcVersionInfo` object containing the version details.
433    ///
434    /// ## Example Request
435    /// ```json
436    /// {
437    ///   "jsonrpc": "2.0",
438    ///   "id": 1,
439    ///   "method": "getVersion"
440    /// }
441    /// ```
442    ///
443    /// ## Example Response
444    /// ```json
445    /// {
446    ///   "jsonrpc": "2.0",
447    ///   "result": {
448    ///     "surfnet_version": "1.2.3",
449    ///     "solana_core": "1.9.0",
450    ///     "feature_set": 12345
451    ///   },
452    ///   "id": 1
453    /// }
454    /// ```
455    ///
456    /// # Notes
457    /// - The version information typically includes the version number of `surfpool`, the version of `solana-core`, and a `feature_set` identifier (first 4 bytes).
458    /// - The `feature_set` field may not always be present, depending on whether a feature set identifier is available.
459    ///
460    /// # See Also
461    /// - `getHealth`, `getIdentity`
462    #[rpc(meta, name = "getVersion")]
463    fn get_version(&self, meta: Self::Metadata) -> Result<SurfpoolRpcVersionInfo>;
464
465    /// Returns vote account information.
466    ///
467    /// This method retrieves the current status of vote accounts, including information about the validator’s vote account and whether it is delinquent. The response includes vote account details such as the stake, commission, vote history, and more.
468    ///
469    /// ## Parameters
470    /// - `meta`: Metadata passed with the request, such as the client’s request context.
471    /// - `config`: Optional configuration parameters, such as specific vote account addresses or commitment settings.
472    ///
473    /// ## Returns
474    /// A `RpcVoteAccountStatus` object containing details about the current and delinquent vote accounts.
475    ///
476    /// ## Example Request
477    /// ```json
478    /// {
479    ///   "jsonrpc": "2.0",
480    ///   "id": 1,
481    ///   "method": "getVoteAccounts",
482    ///   "params": [{}]
483    /// }
484    /// ```
485    ///
486    /// ## Example Response
487    /// ```json
488    /// {
489    ///   "jsonrpc": "2.0",
490    ///   "result": {
491    ///     "current": [
492    ///       {
493    ///         "votePubkey": "votePubkeyBase58",
494    ///         "nodePubkey": "nodePubkeyBase58",
495    ///         "activatedStake": 1000000,
496    ///         "commission": 5,
497    ///         "epochVoteAccount": true,
498    ///         "epochCredits": [[1, 1000, 900], [2, 1100, 1000]],
499    ///         "lastVote": 1000,
500    ///         "rootSlot": 1200
501    ///       }
502    ///     ],
503    ///     "delinquent": [
504    ///       {
505    ///         "votePubkey": "delinquentVotePubkeyBase58",
506    ///         "nodePubkey": "delinquentNodePubkeyBase58",
507    ///         "activatedStake": 0,
508    ///         "commission": 10,
509    ///         "epochVoteAccount": false,
510    ///         "epochCredits": [[1, 500, 400]],
511    ///         "lastVote": 0,
512    ///         "rootSlot": 0
513    ///       }
514    ///     ]
515    ///   },
516    ///   "id": 1
517    /// }
518    /// ```
519    ///
520    /// # Notes
521    /// - The `current` field contains details about vote accounts that are active and have current stake.
522    /// - The `delinquent` field contains details about vote accounts that have become delinquent due to inactivity or other issues.
523    /// - The `epochCredits` field contains historical voting data.
524    ///
525    /// # See Also
526    /// - `getHealth`, `getIdentity`, `getVersion`
527    #[rpc(meta, name = "getVoteAccounts")]
528    fn get_vote_accounts(
529        &self,
530        meta: Self::Metadata,
531        config: Option<RpcGetVoteAccountsConfig>,
532    ) -> Result<RpcVoteAccountStatus>;
533
534    /// Returns the leader schedule for the given configuration or slot.
535    ///
536    /// This method retrieves the leader schedule for the given slot or configuration, providing a map of validator identities to slot indices within a given range.
537    ///
538    /// ## Parameters
539    /// - `meta`: Metadata passed with the request, such as the client’s request context.
540    /// - `options`: Optional configuration wrapper, which can either be a specific slot or a full configuration.
541    /// - `config`: Optional configuration containing the validator identity and commitment level.
542    ///
543    /// ## Returns
544    /// An `Option<RpcLeaderSchedule>` containing a map of leader identities (base-58 encoded pubkeys) to slot indices, relative to the first epoch slot.
545    ///
546    /// ## Example Request
547    /// ```json
548    /// {
549    ///   "jsonrpc": "2.0",
550    ///   "id": 1,
551    ///   "method": "getLeaderSchedule",
552    ///   "params": [{}]
553    /// }
554    /// ```
555    ///
556    /// ## Example Response
557    /// ```json
558    /// {
559    ///   "jsonrpc": "2.0",
560    ///   "result": {
561    ///     "votePubkey1": [0, 2, 4],
562    ///     "votePubkey2": [1, 3, 5]
563    ///   },
564    ///   "id": 1
565    /// }
566    /// ```
567    ///
568    /// # Notes
569    /// - The returned map contains validator identities as keys (base-58 encoded strings), with slot indices as values.
570    ///
571    /// # See Also
572    /// - `getSlot`, `getBlockHeight`, `getEpochInfo`
573    #[rpc(meta, name = "getLeaderSchedule")]
574    fn get_leader_schedule(
575        &self,
576        meta: Self::Metadata,
577        options: Option<RpcLeaderScheduleConfigWrapper>,
578        config: Option<RpcLeaderScheduleConfig>,
579    ) -> Result<Option<RpcLeaderSchedule>>;
580}
581
582#[derive(Clone)]
583pub struct SurfpoolMinimalRpc;
584impl Minimal for SurfpoolMinimalRpc {
585    type Metadata = Option<RunloopContext>;
586
587    fn get_balance(
588        &self,
589        meta: Self::Metadata,
590        pubkey_str: String,
591        _config: Option<RpcContextConfig>, // TODO: use config
592    ) -> BoxFuture<Result<RpcResponse<u64>>> {
593        let pubkey = match verify_pubkey(&pubkey_str) {
594            Ok(res) => res,
595            Err(e) => return e.into(),
596        };
597
598        let SurfnetRpcContext {
599            svm_locker,
600            remote_ctx,
601        } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
602            Ok(res) => res,
603            Err(e) => return e.into(),
604        };
605
606        Box::pin(async move {
607            let SvmAccessContext {
608                slot,
609                inner: account_update,
610                ..
611            } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
612
613            let balance = match &account_update {
614                GetAccountResult::FoundAccount(_, account, _)
615                | GetAccountResult::FoundProgramAccount((_, account), _) => account.lamports,
616                GetAccountResult::None(_) => 0,
617            };
618
619            svm_locker.write_account_update(account_update);
620
621            Ok(RpcResponse {
622                context: RpcResponseContext::new(slot),
623                value: balance,
624            })
625        })
626    }
627
628    fn get_epoch_info(
629        &self,
630        meta: Self::Metadata,
631        _config: Option<RpcContextConfig>,
632    ) -> Result<EpochInfo> {
633        meta.with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.clone())
634            .map_err(Into::into)
635    }
636
637    fn get_genesis_hash(&self, meta: Self::Metadata) -> BoxFuture<Result<String>> {
638        let SurfnetRpcContext {
639            svm_locker,
640            remote_ctx,
641        } = match meta.get_rpc_context(()) {
642            Ok(res) => res,
643            Err(e) => return e.into(),
644        };
645
646        Box::pin(async move {
647            Ok(svm_locker
648                .get_genesis_hash(&remote_ctx.map(|(client, _)| client))
649                .await?
650                .inner
651                .to_string())
652        })
653    }
654
655    fn get_health(&self, _meta: Self::Metadata) -> Result<String> {
656        // todo: we could check the time from the state clock and compare
657        Ok("ok".to_string())
658    }
659
660    fn get_identity(&self, _meta: Self::Metadata) -> Result<RpcIdentity> {
661        Ok(RpcIdentity {
662            identity: SURFPOOL_IDENTITY_PUBKEY.to_string(),
663        })
664    }
665
666    fn get_slot(&self, meta: Self::Metadata, config: Option<RpcContextConfig>) -> Result<Slot> {
667        let config = config.unwrap_or_default();
668        let latest_absolute_slot = meta
669            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
670            .map_err(Into::<jsonrpc_core::Error>::into)?;
671        let slot = match config.commitment.unwrap_or_default().commitment {
672            CommitmentLevel::Processed => latest_absolute_slot,
673            CommitmentLevel::Confirmed => latest_absolute_slot - 1,
674            CommitmentLevel::Finalized => latest_absolute_slot - FINALIZATION_SLOT_THRESHOLD,
675        };
676
677        if let Some(min_context_slot) = config.min_context_slot {
678            if slot < min_context_slot {
679                return Err(RpcCustomError::MinContextSlotNotReached {
680                    context_slot: min_context_slot,
681                }
682                .into());
683            }
684        }
685
686        Ok(slot)
687    }
688
689    fn get_block_height(
690        &self,
691        meta: Self::Metadata,
692        config: Option<RpcContextConfig>,
693    ) -> Result<u64> {
694        let config = config.unwrap_or_default();
695
696        if let Some(target_slot) = config.min_context_slot {
697            let block_exists = meta
698                .with_svm_reader(|svm_reader| svm_reader.blocks.contains_key(&target_slot))
699                .map_err(Into::<jsonrpc_core::Error>::into)?;
700
701            if !block_exists {
702                return Err(jsonrpc_core::Error::invalid_params(format!(
703                    "Block not found for slot: {}",
704                    target_slot
705                )));
706            }
707        }
708
709        meta.with_svm_reader(|svm_reader| {
710            if let Some(target_slot) = config.min_context_slot {
711                if let Some(block_header) = svm_reader.blocks.get(&target_slot) {
712                    return block_header.block_height;
713                }
714            }
715
716            // default behavior: return the latest block height with commitment adjustments
717            let latest_block_height = svm_reader.latest_epoch_info.block_height;
718
719            match config.commitment.unwrap_or_default().commitment {
720                CommitmentLevel::Processed => latest_block_height,
721                CommitmentLevel::Confirmed => latest_block_height.saturating_sub(1),
722                CommitmentLevel::Finalized => {
723                    latest_block_height.saturating_sub(FINALIZATION_SLOT_THRESHOLD)
724                }
725            }
726        })
727        .map_err(Into::into)
728    }
729
730    fn get_highest_snapshot_slot(&self, _meta: Self::Metadata) -> Result<RpcSnapshotSlotInfo> {
731        Err(jsonrpc_core::Error {
732            code: jsonrpc_core::ErrorCode::ServerError(-32008),
733            message: "No snapshot".into(),
734            data: None,
735        })
736    }
737
738    fn get_transaction_count(
739        &self,
740        meta: Self::Metadata,
741        _config: Option<RpcContextConfig>,
742    ) -> Result<u64> {
743        meta.with_svm_reader(|svm_reader| svm_reader.transactions_processed)
744            .map_err(Into::into)
745    }
746
747    fn get_version(&self, _: Self::Metadata) -> Result<SurfpoolRpcVersionInfo> {
748        let version = solana_version::Version::default();
749
750        Ok(SurfpoolRpcVersionInfo {
751            surfnet_version: SURFPOOL_VERSION.to_string(),
752            solana_core: version.to_string(),
753            feature_set: Some(version.feature_set),
754        })
755    }
756
757    fn get_vote_accounts(
758        &self,
759        _meta: Self::Metadata,
760        config: Option<RpcGetVoteAccountsConfig>,
761    ) -> Result<RpcVoteAccountStatus> {
762        // validate inputs if provided
763        if let Some(config) = config {
764            // validate vote_pubkey if provided
765            if let Some(vote_pubkey_str) = config.vote_pubkey {
766                verify_pubkey(&vote_pubkey_str)?;
767            }
768        }
769
770        // Return empty vote accounts
771        Ok(RpcVoteAccountStatus {
772            current: vec![],
773            delinquent: vec![],
774        })
775    }
776
777    fn get_leader_schedule(
778        &self,
779        meta: Self::Metadata,
780        options: Option<RpcLeaderScheduleConfigWrapper>,
781        config: Option<RpcLeaderScheduleConfig>,
782    ) -> Result<Option<RpcLeaderSchedule>> {
783        let (slot, maybe_config) = options.map(|options| options.unzip()).unwrap_or_default();
784        let config = maybe_config.or(config).unwrap_or_default();
785
786        if let Some(ref identity) = config.identity {
787            let _ = verify_pubkey(identity)?;
788        }
789
790        let svm_locker = meta.get_svm_locker()?;
791        let epoch_info = svm_locker.get_epoch_info();
792
793        let slot = slot.unwrap_or(epoch_info.absolute_slot);
794
795        let first_slot_in_epoch = epoch_info
796            .absolute_slot
797            .saturating_sub(epoch_info.slot_index);
798        let last_slot_in_epoch = first_slot_in_epoch + epoch_info.slots_in_epoch.saturating_sub(1);
799
800        if slot < first_slot_in_epoch || slot > last_slot_in_epoch {
801            return Ok(None);
802        }
803
804        // return empty leader schedule if epoch found and everything is valid
805        Ok(Some(std::collections::HashMap::new()))
806    }
807}
808
809#[cfg(test)]
810mod tests {
811    use jsonrpc_core::ErrorCode;
812    use solana_client::rpc_config::RpcContextConfig;
813    use solana_commitment_config::CommitmentConfig;
814    use solana_epoch_info::EpochInfo;
815    use solana_pubkey::Pubkey;
816    use solana_sdk::genesis_config::GenesisConfig;
817
818    use super::*;
819    use crate::tests::helpers::TestSetup;
820
821    #[test]
822    fn test_get_block_height_processed_commitment() {
823        let setup = TestSetup::new(SurfpoolMinimalRpc);
824
825        let config = RpcContextConfig {
826            commitment: Some(CommitmentConfig::processed()),
827            min_context_slot: None,
828        };
829
830        let expected_height = setup
831            .rpc
832            .get_block_height(Some(setup.context.clone()), Some(config));
833        assert!(expected_height.is_ok());
834
835        let latest_epoch_block_height = setup
836            .context
837            .svm_locker
838            .with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.block_height);
839
840        assert_eq!(expected_height.unwrap(), latest_epoch_block_height);
841    }
842
843    #[test]
844    fn test_get_block_height_confirmed_commitment() {
845        let setup = TestSetup::new(SurfpoolMinimalRpc);
846
847        let config = RpcContextConfig {
848            commitment: Some(CommitmentConfig::confirmed()),
849            min_context_slot: None,
850        };
851
852        let expected_height = setup
853            .rpc
854            .get_block_height(Some(setup.context.clone()), Some(config));
855        assert!(expected_height.is_ok());
856
857        let latest_epoch_block_height = setup
858            .context
859            .svm_locker
860            .with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.block_height);
861
862        assert_eq!(expected_height.unwrap(), latest_epoch_block_height - 1);
863    }
864
865    #[test]
866    fn test_get_block_height_with_min_context_slot() {
867        let setup = TestSetup::new(SurfpoolMinimalRpc);
868
869        // create blocks at specific slots with known block heights
870        let test_cases = vec![(100, 50), (200, 150), (300, 275)];
871
872        {
873            let mut svm_writer = setup.context.svm_locker.0.blocking_write();
874            for (slot, block_height) in &test_cases {
875                svm_writer.blocks.insert(
876                    *slot,
877                    crate::surfnet::BlockHeader {
878                        hash: format!("hash_{}", slot),
879                        previous_blockhash: format!("prev_hash_{}", slot - 1),
880                        block_time: chrono::Utc::now().timestamp_millis(),
881                        block_height: *block_height,
882                        parent_slot: slot - 1,
883                        signatures: Vec::new(),
884                    },
885                );
886            }
887        }
888
889        for (slot, expected_height) in test_cases {
890            let config = RpcContextConfig {
891                commitment: None,
892                min_context_slot: Some(slot),
893            };
894
895            let result = setup
896                .rpc
897                .get_block_height(Some(setup.context.clone()), Some(config));
898            assert!(
899                result.is_ok(),
900                "failed to get block height for slot {}",
901                slot
902            );
903            assert_eq!(
904                result.unwrap(),
905                expected_height,
906                "Wrong block height for slot {}",
907                slot
908            );
909        }
910    }
911
912    #[test]
913    fn test_get_block_height_error_case_slot_not_found() {
914        let setup = TestSetup::new(SurfpoolMinimalRpc);
915
916        {
917            let mut svm_writer = setup.context.svm_locker.0.blocking_write();
918            svm_writer.blocks.insert(
919                100,
920                crate::surfnet::BlockHeader {
921                    hash: "hash_100".to_string(),
922                    previous_blockhash: "prev_hash_99".to_string(),
923                    block_time: chrono::Utc::now().timestamp_millis(),
924                    block_height: 50,
925                    parent_slot: 99,
926                    signatures: Vec::new(),
927                },
928            );
929        }
930
931        // slot that definitely doesn't exist
932        let nonexistent_slot = 999;
933        let config = RpcContextConfig {
934            commitment: None,
935            min_context_slot: Some(nonexistent_slot),
936        };
937
938        let result = setup
939            .rpc
940            .get_block_height(Some(setup.context), Some(config));
941
942        assert!(
943            result.is_err(),
944            "Expected error for nonexistent slot {}",
945            nonexistent_slot
946        );
947
948        let error = result.unwrap_err();
949
950        assert_eq!(error.code, jsonrpc_core::types::ErrorCode::InvalidParams);
951        assert!(
952            error.message.contains("Block not found for slot"),
953            "Error message should mention block not found, got: {}",
954            error.message
955        );
956        assert!(
957            error.message.contains(&nonexistent_slot.to_string()),
958            "Error message should include the slot number, got: {}",
959            error.message
960        );
961    }
962
963    #[test]
964    fn test_get_health() {
965        let setup = TestSetup::new(SurfpoolMinimalRpc);
966        let result = setup.rpc.get_health(Some(setup.context));
967        assert_eq!(result.unwrap(), "ok");
968    }
969
970    #[test]
971    fn test_get_transaction_count() {
972        let setup = TestSetup::new(SurfpoolMinimalRpc);
973        let transactions_processed = setup
974            .context
975            .svm_locker
976            .with_svm_reader(|svm_reader| svm_reader.transactions_processed);
977        let result = setup.rpc.get_transaction_count(Some(setup.context), None);
978        assert_eq!(result.unwrap(), transactions_processed);
979    }
980
981    #[test]
982    fn test_get_epoch_info() {
983        let info = EpochInfo {
984            epoch: 1,
985            slot_index: 1,
986            slots_in_epoch: 1,
987            absolute_slot: 1,
988            block_height: 1,
989            transaction_count: Some(1),
990        };
991        let setup = TestSetup::new_with_epoch_info(SurfpoolMinimalRpc, info.clone());
992        let result = setup.rpc.get_epoch_info(Some(setup.context), None).unwrap();
993        assert_eq!(result, info);
994    }
995
996    #[test]
997    fn test_get_slot() {
998        let setup = TestSetup::new(SurfpoolMinimalRpc);
999        let result = setup.rpc.get_slot(Some(setup.context), None).unwrap();
1000        assert_eq!(result, 92);
1001    }
1002
1003    #[test]
1004    fn test_get_highest_snapshot_slot_returns_error() {
1005        let setup = TestSetup::new(SurfpoolMinimalRpc);
1006
1007        let result = setup.rpc.get_highest_snapshot_slot(Some(setup.context));
1008
1009        assert!(
1010            result.is_err(),
1011            "Expected get_highest_snapshot_slot to return an error"
1012        );
1013
1014        if let Err(error) = result {
1015            assert_eq!(
1016                error.code,
1017                jsonrpc_core::ErrorCode::ServerError(-32008),
1018                "Expected error code -32008, got {:?}",
1019                error.code
1020            );
1021            assert_eq!(
1022                error.message, "No snapshot",
1023                "Expected error message 'No snapshot', got '{}'",
1024                error.message
1025            );
1026        }
1027    }
1028
1029    #[tokio::test(flavor = "multi_thread")]
1030    async fn test_get_genesis_hash() {
1031        let setup = TestSetup::new(SurfpoolMinimalRpc);
1032
1033        let genesis_hash = setup
1034            .rpc
1035            .get_genesis_hash(Some(setup.context))
1036            .await
1037            .unwrap();
1038
1039        assert_eq!(genesis_hash, GenesisConfig::default().hash().to_string())
1040    }
1041
1042    #[test]
1043    fn test_get_identity() {
1044        let setup = TestSetup::new(SurfpoolMinimalRpc);
1045        let result = setup.rpc.get_identity(Some(setup.context)).unwrap();
1046        assert_eq!(result.identity, SURFPOOL_IDENTITY_PUBKEY.to_string());
1047    }
1048
1049    #[test]
1050    fn test_get_leader_schedule_valid_cases() {
1051        let setup = TestSetup::new(SurfpoolMinimalRpc);
1052
1053        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1054            svm_writer.latest_epoch_info = EpochInfo {
1055                epoch: 100,
1056                slot_index: 50,
1057                slots_in_epoch: 432000,
1058                absolute_slot: 43200050,
1059                block_height: 43200050,
1060                transaction_count: None,
1061            };
1062        });
1063
1064        // test 1: Valid slot within current epoch, no identity
1065        let result = setup
1066            .rpc
1067            .get_leader_schedule(
1068                Some(setup.context.clone()),
1069                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(43200025))), // Within epoch
1070                None,
1071            )
1072            .unwrap();
1073
1074        assert!(result.is_some());
1075        assert!(result.unwrap().is_empty()); // Should return empty HashMap
1076
1077        // test 2: No slot provided (should use current slot), no identity
1078        let result = setup
1079            .rpc
1080            .get_leader_schedule(Some(setup.context.clone()), None, None)
1081            .unwrap();
1082
1083        assert!(result.is_some());
1084        assert!(result.unwrap().is_empty());
1085
1086        // test 3: Valid slot with valid identity
1087        let valid_pubkey = Pubkey::new_unique();
1088        let result = setup
1089            .rpc
1090            .get_leader_schedule(
1091                Some(setup.context.clone()),
1092                None,
1093                Some(RpcLeaderScheduleConfig {
1094                    identity: Some(valid_pubkey.to_string()),
1095                    commitment: None,
1096                }),
1097            )
1098            .unwrap();
1099
1100        assert!(result.is_some());
1101        assert!(result.unwrap().is_empty());
1102
1103        // test 4: Boundary cases - first slot in epoch
1104        let first_slot_in_epoch = 43200050 - 50;
1105        let result = setup
1106            .rpc
1107            .get_leader_schedule(
1108                Some(setup.context.clone()),
1109                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(
1110                    first_slot_in_epoch,
1111                ))),
1112                None,
1113            )
1114            .unwrap();
1115
1116        assert!(result.is_some());
1117        assert!(result.unwrap().is_empty());
1118
1119        // test 5: Boundary cases - last slot in epoch
1120        let last_slot_in_epoch = first_slot_in_epoch + 432000 - 1;
1121        let result = setup
1122            .rpc
1123            .get_leader_schedule(
1124                Some(setup.context.clone()),
1125                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(
1126                    last_slot_in_epoch,
1127                ))),
1128                None,
1129            )
1130            .unwrap();
1131
1132        assert!(result.is_some());
1133        assert!(result.unwrap().is_empty());
1134    }
1135
1136    #[test]
1137    fn test_get_leader_schedule_invalid_cases() {
1138        let setup = TestSetup::new(SurfpoolMinimalRpc);
1139
1140        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1141            svm_writer.latest_epoch_info = EpochInfo {
1142                epoch: 100,
1143                slot_index: 50,
1144                slots_in_epoch: 432000,
1145                absolute_slot: 43200050,
1146                block_height: 43200050,
1147                transaction_count: None,
1148            };
1149        });
1150
1151        let first_slot_in_epoch = 43200050 - 50;
1152        let last_slot_in_epoch = first_slot_in_epoch + 432000 - 1;
1153
1154        // test 1: Slot before current epoch (should return None)
1155        let result = setup
1156            .rpc
1157            .get_leader_schedule(
1158                Some(setup.context.clone()),
1159                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(
1160                    first_slot_in_epoch - 1,
1161                ))),
1162                None,
1163            )
1164            .unwrap();
1165
1166        assert!(result.is_none());
1167
1168        // test 2: Slot after current epoch (should return None)
1169        let result = setup
1170            .rpc
1171            .get_leader_schedule(
1172                Some(setup.context.clone()),
1173                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(
1174                    last_slot_in_epoch + 1,
1175                ))),
1176                None,
1177            )
1178            .unwrap();
1179
1180        assert!(result.is_none());
1181
1182        // test 3: Way outside epoch range (should return None)
1183        let result = setup
1184            .rpc
1185            .get_leader_schedule(
1186                Some(setup.context.clone()),
1187                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(1000000))), // Very old slot
1188                None,
1189            )
1190            .unwrap();
1191
1192        assert!(result.is_none());
1193
1194        // test 4: Invalid identity pubkey (should return Error)
1195        let result = setup.rpc.get_leader_schedule(
1196            Some(setup.context.clone()),
1197            None,
1198            Some(RpcLeaderScheduleConfig {
1199                identity: Some("invalid_pubkey_string".to_string()),
1200                commitment: None,
1201            }),
1202        );
1203
1204        assert!(result.is_err());
1205        let error = result.unwrap_err();
1206        assert_eq!(error.code, ErrorCode::InvalidParams);
1207
1208        // test 5: Empty identity string (should return Error)
1209        let result = setup.rpc.get_leader_schedule(
1210            Some(setup.context.clone()),
1211            None,
1212            Some(RpcLeaderScheduleConfig {
1213                identity: Some("".to_string()),
1214                commitment: None,
1215            }),
1216        );
1217
1218        assert!(result.is_err());
1219        let error = result.unwrap_err();
1220        assert_eq!(error.code, ErrorCode::InvalidParams);
1221
1222        // test 6: No metadata (should return Error from get_svm_locker)
1223        let result = setup.rpc.get_leader_schedule(None, None, None);
1224
1225        assert!(result.is_err());
1226    }
1227
1228    #[test]
1229    fn test_get_vote_accounts_valid_config_returns_empty() {
1230        let setup = TestSetup::new(SurfpoolMinimalRpc);
1231
1232        // test with valid configuration including all optional parameters
1233        let config = RpcGetVoteAccountsConfig {
1234            vote_pubkey: Some("11111111111111111111111111111112".to_string()),
1235            commitment: Some(CommitmentConfig::processed()),
1236            keep_unstaked_delinquents: Some(true),
1237            delinquent_slot_distance: Some(100),
1238        };
1239
1240        let result = setup
1241            .rpc
1242            .get_vote_accounts(Some(setup.context.clone()), Some(config));
1243
1244        // should succeed with valid inputs
1245        assert!(result.is_ok());
1246
1247        let vote_accounts = result.unwrap();
1248
1249        // should return empty current and delinquent arrays
1250        assert_eq!(vote_accounts.current.len(), 0);
1251        assert_eq!(vote_accounts.delinquent.len(), 0);
1252    }
1253
1254    #[test]
1255    fn test_get_vote_accounts_invalid_pubkey_returns_error() {
1256        let setup = TestSetup::new(SurfpoolMinimalRpc);
1257
1258        // test with invalid vote pubkey that's not valid base58
1259        let config = RpcGetVoteAccountsConfig {
1260            vote_pubkey: Some("invalid_pubkey_not_base58".to_string()),
1261            commitment: Some(CommitmentConfig::finalized()),
1262            keep_unstaked_delinquents: Some(false),
1263            delinquent_slot_distance: Some(50),
1264        };
1265
1266        let result = setup
1267            .rpc
1268            .get_vote_accounts(Some(setup.context.clone()), Some(config));
1269
1270        // should fail due to invalid vote pubkey
1271        assert!(result.is_err());
1272
1273        let error = result.unwrap_err();
1274
1275        // should be invalid params error
1276        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
1277    }
1278}