Skip to main content

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