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            #[cfg(feature = "prometheus")]
610            let rpc_start = std::time::Instant::now();
611
612            let SvmAccessContext {
613                slot,
614                inner: account_update,
615                ..
616            } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
617
618            if let Some(min_slot) = min_ctx_slot
619                && slot < min_slot
620            {
621                return Err(RpcCustomError::MinContextSlotNotReached {
622                    context_slot: min_slot,
623                }
624                .into());
625            }
626
627            let balance = match &account_update {
628                GetAccountResult::FoundAccount(_, account, _)
629                | GetAccountResult::FoundProgramAccount((_, account), _)
630                | GetAccountResult::FoundTokenAccount((_, account), _) => account.lamports,
631                GetAccountResult::None(_) => 0,
632            };
633
634            svm_locker.write_account_update(account_update);
635
636            #[cfg(feature = "prometheus")]
637            if let Some(m) = crate::telemetry::metrics() {
638                m.record_rpc_request("getBalance", rpc_start.elapsed().as_millis() as u64);
639            }
640
641            Ok(RpcResponse {
642                context: RpcResponseContext::new(slot),
643                value: balance,
644            })
645        })
646    }
647
648    fn get_epoch_info(
649        &self,
650        meta: Self::Metadata,
651        _config: Option<RpcContextConfig>,
652    ) -> Result<EpochInfo> {
653        meta.with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.clone())
654            .map_err(Into::into)
655    }
656
657    fn get_genesis_hash(&self, meta: Self::Metadata) -> BoxFuture<Result<String>> {
658        let SurfnetRpcContext {
659            svm_locker,
660            remote_ctx,
661        } = match meta.get_rpc_context(()) {
662            Ok(res) => res,
663            Err(e) => return e.into(),
664        };
665
666        Box::pin(async move {
667            Ok(svm_locker
668                .get_genesis_hash(&remote_ctx.map(|(client, _)| client))
669                .await?
670                .inner
671                .to_string())
672        })
673    }
674
675    fn get_health(&self, _meta: Self::Metadata) -> Result<String> {
676        // todo: we could check the time from the state clock and compare
677        Ok("ok".to_string())
678    }
679
680    fn get_identity(&self, _meta: Self::Metadata) -> Result<RpcIdentity> {
681        Ok(RpcIdentity {
682            identity: SURFPOOL_IDENTITY_PUBKEY.to_string(),
683        })
684    }
685
686    fn get_slot(&self, meta: Self::Metadata, config: Option<RpcContextConfig>) -> Result<Slot> {
687        let config = config.unwrap_or_default();
688        let latest_absolute_slot = meta
689            .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
690            .map_err(Into::<jsonrpc_core::Error>::into)?;
691        let slot = match config.commitment.unwrap_or_default().commitment {
692            CommitmentLevel::Processed => latest_absolute_slot,
693            CommitmentLevel::Confirmed => latest_absolute_slot - 1,
694            CommitmentLevel::Finalized => latest_absolute_slot - FINALIZATION_SLOT_THRESHOLD,
695        };
696
697        if let Some(min_context_slot) = config.min_context_slot {
698            if slot < min_context_slot {
699                return Err(RpcCustomError::MinContextSlotNotReached {
700                    context_slot: min_context_slot,
701                }
702                .into());
703            }
704        }
705
706        Ok(slot)
707    }
708
709    fn get_block_height(
710        &self,
711        meta: Self::Metadata,
712        config: Option<RpcContextConfig>,
713    ) -> Result<u64> {
714        let config = config.unwrap_or_default();
715
716        if let Some(target_slot) = config.min_context_slot {
717            let block_exists =
718                meta.with_svm_reader(|svm_reader| svm_reader.blocks.contains_key(&target_slot))??;
719
720            if !block_exists {
721                return Err(jsonrpc_core::Error::invalid_params(format!(
722                    "Block not found for slot: {}",
723                    target_slot
724                )));
725            }
726        }
727
728        meta.with_svm_reader(|svm_reader| {
729            if let Some(target_slot) = config.min_context_slot {
730                if let Some(block_header) = svm_reader.blocks.get(&target_slot)? {
731                    return Ok(block_header.block_height);
732                }
733            }
734
735            // default behavior: return the latest block height with commitment adjustments
736            let latest_block_height = svm_reader.latest_epoch_info.block_height;
737
738            let block_height = match config.commitment.unwrap_or_default().commitment {
739                CommitmentLevel::Processed => latest_block_height,
740                CommitmentLevel::Confirmed => latest_block_height.saturating_sub(1),
741                CommitmentLevel::Finalized => {
742                    latest_block_height.saturating_sub(FINALIZATION_SLOT_THRESHOLD)
743                }
744            };
745            Ok::<u64, jsonrpc_core::Error>(block_height)
746        })?
747        .map_err(Into::into)
748    }
749
750    fn get_highest_snapshot_slot(&self, _meta: Self::Metadata) -> Result<RpcSnapshotSlotInfo> {
751        Err(jsonrpc_core::Error {
752            code: jsonrpc_core::ErrorCode::ServerError(-32008),
753            message: "No snapshot".into(),
754            data: None,
755        })
756    }
757
758    fn get_transaction_count(
759        &self,
760        meta: Self::Metadata,
761        _config: Option<RpcContextConfig>,
762    ) -> Result<u64> {
763        meta.with_svm_reader(|svm_reader| svm_reader.transactions_processed)
764            .map_err(Into::into)
765    }
766
767    fn get_version(&self, _: Self::Metadata) -> Result<SurfpoolRpcVersionInfo> {
768        let version = solana_version::Version::default();
769
770        Ok(SurfpoolRpcVersionInfo {
771            surfnet_version: SURFPOOL_VERSION.to_string(),
772            solana_core: version.to_string(),
773            feature_set: Some(version.feature_set()),
774        })
775    }
776
777    fn get_vote_accounts(
778        &self,
779        _meta: Self::Metadata,
780        config: Option<RpcGetVoteAccountsConfig>,
781    ) -> Result<RpcVoteAccountStatus> {
782        // validate inputs if provided
783        if let Some(config) = config {
784            // validate vote_pubkey if provided
785            if let Some(vote_pubkey_str) = config.vote_pubkey {
786                verify_pubkey(&vote_pubkey_str)?;
787            }
788        }
789
790        // Return empty vote accounts
791        Ok(RpcVoteAccountStatus {
792            current: vec![],
793            delinquent: vec![],
794        })
795    }
796
797    fn get_leader_schedule(
798        &self,
799        meta: Self::Metadata,
800        options: Option<RpcLeaderScheduleConfigWrapper>,
801        config: Option<RpcLeaderScheduleConfig>,
802    ) -> Result<Option<RpcLeaderSchedule>> {
803        let (slot, maybe_config) = options.map(|options| options.unzip()).unwrap_or_default();
804        let config = maybe_config.or(config).unwrap_or_default();
805
806        if let Some(ref identity) = config.identity {
807            let _ = verify_pubkey(identity)?;
808        }
809
810        let svm_locker = meta.get_svm_locker()?;
811        let epoch_info = svm_locker.get_epoch_info();
812
813        let slot = slot.unwrap_or(epoch_info.absolute_slot);
814
815        let first_slot_in_epoch = epoch_info
816            .absolute_slot
817            .saturating_sub(epoch_info.slot_index);
818        let last_slot_in_epoch = first_slot_in_epoch + epoch_info.slots_in_epoch.saturating_sub(1);
819
820        if slot < first_slot_in_epoch || slot > last_slot_in_epoch {
821            return Ok(None);
822        }
823
824        // return empty leader schedule if epoch found and everything is valid
825        Ok(Some(std::collections::HashMap::new()))
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use jsonrpc_core::ErrorCode;
832    use solana_client::rpc_config::RpcContextConfig;
833    use solana_commitment_config::CommitmentConfig;
834    use solana_epoch_info::EpochInfo;
835    use solana_genesis_config::GenesisConfig;
836    use solana_pubkey::Pubkey;
837
838    use super::*;
839    use crate::{tests::helpers::TestSetup, types::SyntheticBlockhash};
840
841    #[test]
842    fn test_get_block_height_processed_commitment() {
843        let setup = TestSetup::new(SurfpoolMinimalRpc);
844
845        let config = RpcContextConfig {
846            commitment: Some(CommitmentConfig::processed()),
847            min_context_slot: None,
848        };
849
850        let expected_height = setup
851            .rpc
852            .get_block_height(Some(setup.context.clone()), Some(config));
853        assert!(expected_height.is_ok());
854
855        let latest_epoch_block_height = setup
856            .context
857            .svm_locker
858            .with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.block_height);
859
860        assert_eq!(expected_height.unwrap(), latest_epoch_block_height);
861    }
862
863    #[test]
864    fn test_get_block_height_confirmed_commitment() {
865        let setup = TestSetup::new(SurfpoolMinimalRpc);
866
867        let config = RpcContextConfig {
868            commitment: Some(CommitmentConfig::confirmed()),
869            min_context_slot: None,
870        };
871
872        let expected_height = setup
873            .rpc
874            .get_block_height(Some(setup.context.clone()), Some(config));
875        assert!(expected_height.is_ok());
876
877        let latest_epoch_block_height = setup
878            .context
879            .svm_locker
880            .with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.block_height);
881
882        assert_eq!(expected_height.unwrap(), latest_epoch_block_height - 1);
883    }
884
885    #[test]
886    fn test_get_block_height_with_min_context_slot() {
887        let setup = TestSetup::new(SurfpoolMinimalRpc);
888
889        // create blocks at specific slots with known block heights
890        let test_cases = vec![(100, 50), (200, 150), (300, 275)];
891
892        {
893            let mut svm_writer = setup.context.svm_locker.0.blocking_write();
894            for (slot, block_height) in &test_cases {
895                svm_writer
896                    .blocks
897                    .store(
898                        *slot,
899                        crate::surfnet::BlockHeader {
900                            hash: SyntheticBlockhash::new(*slot).to_string(),
901                            previous_blockhash: SyntheticBlockhash::new(slot - 1).to_string(),
902                            block_time: chrono::Utc::now().timestamp_millis(),
903                            block_height: *block_height,
904                            parent_slot: slot - 1,
905                            signatures: Vec::new(),
906                        },
907                    )
908                    .unwrap();
909            }
910        }
911
912        for (slot, expected_height) in test_cases {
913            let config = RpcContextConfig {
914                commitment: None,
915                min_context_slot: Some(slot),
916            };
917
918            let result = setup
919                .rpc
920                .get_block_height(Some(setup.context.clone()), Some(config));
921            assert!(
922                result.is_ok(),
923                "failed to get block height for slot {}",
924                slot
925            );
926            assert_eq!(
927                result.unwrap(),
928                expected_height,
929                "Wrong block height for slot {}",
930                slot
931            );
932        }
933    }
934
935    #[test]
936    fn test_get_block_height_error_case_slot_not_found() {
937        let setup = TestSetup::new(SurfpoolMinimalRpc);
938
939        {
940            let mut svm_writer = setup.context.svm_locker.0.blocking_write();
941            svm_writer
942                .blocks
943                .store(
944                    100,
945                    crate::surfnet::BlockHeader {
946                        hash: SyntheticBlockhash::new(100).to_string(),
947                        previous_blockhash: SyntheticBlockhash::new(99).to_string(),
948                        block_time: chrono::Utc::now().timestamp_millis(),
949                        block_height: 50,
950                        parent_slot: 99,
951                        signatures: Vec::new(),
952                    },
953                )
954                .unwrap();
955        }
956
957        // slot that definitely doesn't exist
958        let nonexistent_slot = 999;
959        let config = RpcContextConfig {
960            commitment: None,
961            min_context_slot: Some(nonexistent_slot),
962        };
963
964        let result = setup
965            .rpc
966            .get_block_height(Some(setup.context), Some(config));
967
968        assert!(
969            result.is_err(),
970            "Expected error for nonexistent slot {}",
971            nonexistent_slot
972        );
973
974        let error = result.unwrap_err();
975
976        assert_eq!(error.code, jsonrpc_core::types::ErrorCode::InvalidParams);
977        assert!(
978            error.message.contains("Block not found for slot"),
979            "Error message should mention block not found, got: {}",
980            error.message
981        );
982        assert!(
983            error.message.contains(&nonexistent_slot.to_string()),
984            "Error message should include the slot number, got: {}",
985            error.message
986        );
987    }
988
989    #[test]
990    fn test_get_health() {
991        let setup = TestSetup::new(SurfpoolMinimalRpc);
992        let result = setup.rpc.get_health(Some(setup.context));
993        assert_eq!(result.unwrap(), "ok");
994    }
995
996    #[tokio::test(flavor = "multi_thread")]
997    async fn test_get_balance() {
998        let setup = TestSetup::new(SurfpoolMinimalRpc);
999
1000        let airdrop_amount = 5 * 1_000_000_000u64;
1001        let to_airdrop_pubkey = Pubkey::new_unique();
1002
1003        setup
1004            .context
1005            .svm_locker
1006            .airdrop(&to_airdrop_pubkey, airdrop_amount)
1007            .unwrap()
1008            .unwrap();
1009
1010        let pass_if_correct_config_result = setup
1011            .rpc
1012            .get_balance(
1013                Some(setup.context.clone()),
1014                to_airdrop_pubkey.to_string(),
1015                None,
1016            )
1017            .await;
1018
1019        assert!(
1020            pass_if_correct_config_result.is_ok(),
1021            "Expected the operation to pass"
1022        );
1023
1024        assert_eq!(
1025            pass_if_correct_config_result.unwrap().value,
1026            airdrop_amount,
1027            "Invalid returned lamports for the account"
1028        );
1029
1030        let wrong_min_slot = setup.context.svm_locker.get_latest_absolute_slot() + 100;
1031
1032        let fail_if_latest_slot_lt_min_ctx_slot_result = setup
1033            .rpc
1034            .get_balance(
1035                Some(setup.context.clone()),
1036                Pubkey::new_unique().to_string(),
1037                Some(RpcContextConfig {
1038                    commitment: None,
1039                    min_context_slot: Some(wrong_min_slot),
1040                }),
1041            )
1042            .await;
1043
1044        let expected_err: Result<()> = Result::Err(
1045            RpcCustomError::MinContextSlotNotReached {
1046                context_slot: wrong_min_slot,
1047            }
1048            .into(),
1049        );
1050
1051        assert!(
1052            fail_if_latest_slot_lt_min_ctx_slot_result.is_err(),
1053            "Expected get_balance rpc method to fail when latest_absolute_slot < min_context_slot"
1054        );
1055
1056        assert_eq!(
1057            fail_if_latest_slot_lt_min_ctx_slot_result.err().unwrap(),
1058            expected_err.err().unwrap()
1059        );
1060    }
1061
1062    #[test]
1063    fn test_get_transaction_count() {
1064        let setup = TestSetup::new(SurfpoolMinimalRpc);
1065        let transactions_processed = setup
1066            .context
1067            .svm_locker
1068            .with_svm_reader(|svm_reader| svm_reader.transactions_processed);
1069        let result = setup.rpc.get_transaction_count(Some(setup.context), None);
1070        assert_eq!(result.unwrap(), transactions_processed);
1071    }
1072
1073    #[test]
1074    fn test_get_epoch_info() {
1075        let info = EpochInfo {
1076            epoch: 1,
1077            slot_index: 1,
1078            slots_in_epoch: 1,
1079            absolute_slot: 1,
1080            block_height: 1,
1081            transaction_count: Some(1),
1082        };
1083        let setup = TestSetup::new_with_epoch_info(SurfpoolMinimalRpc, info.clone());
1084        let result = setup.rpc.get_epoch_info(Some(setup.context), None).unwrap();
1085        assert_eq!(result, info);
1086    }
1087
1088    #[test]
1089    fn test_get_slot() {
1090        let setup = TestSetup::new(SurfpoolMinimalRpc);
1091        let result = setup.rpc.get_slot(Some(setup.context), None).unwrap();
1092        assert_eq!(result, 92);
1093    }
1094
1095    #[test]
1096    fn test_get_highest_snapshot_slot_returns_error() {
1097        let setup = TestSetup::new(SurfpoolMinimalRpc);
1098
1099        let result = setup.rpc.get_highest_snapshot_slot(Some(setup.context));
1100
1101        assert!(
1102            result.is_err(),
1103            "Expected get_highest_snapshot_slot to return an error"
1104        );
1105
1106        if let Err(error) = result {
1107            assert_eq!(
1108                error.code,
1109                jsonrpc_core::ErrorCode::ServerError(-32008),
1110                "Expected error code -32008, got {:?}",
1111                error.code
1112            );
1113            assert_eq!(
1114                error.message, "No snapshot",
1115                "Expected error message 'No snapshot', got '{}'",
1116                error.message
1117            );
1118        }
1119    }
1120
1121    #[tokio::test(flavor = "multi_thread")]
1122    async fn test_get_genesis_hash() {
1123        let setup = TestSetup::new(SurfpoolMinimalRpc);
1124
1125        let genesis_hash = setup
1126            .rpc
1127            .get_genesis_hash(Some(setup.context))
1128            .await
1129            .unwrap();
1130
1131        assert_eq!(genesis_hash, GenesisConfig::default().hash().to_string())
1132    }
1133
1134    #[test]
1135    fn test_get_identity() {
1136        let setup = TestSetup::new(SurfpoolMinimalRpc);
1137        let result = setup.rpc.get_identity(Some(setup.context)).unwrap();
1138        assert_eq!(result.identity, SURFPOOL_IDENTITY_PUBKEY.to_string());
1139    }
1140
1141    #[test]
1142    fn test_get_leader_schedule_valid_cases() {
1143        let setup = TestSetup::new(SurfpoolMinimalRpc);
1144
1145        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1146            svm_writer.latest_epoch_info = EpochInfo {
1147                epoch: 100,
1148                slot_index: 50,
1149                slots_in_epoch: 432000,
1150                absolute_slot: 43200050,
1151                block_height: 43200050,
1152                transaction_count: None,
1153            };
1154        });
1155
1156        // test 1: Valid slot within current epoch, no identity
1157        let result = setup
1158            .rpc
1159            .get_leader_schedule(
1160                Some(setup.context.clone()),
1161                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(43200025))), // Within epoch
1162                None,
1163            )
1164            .unwrap();
1165
1166        assert!(result.is_some());
1167        assert!(result.unwrap().is_empty()); // Should return empty HashMap
1168
1169        // test 2: No slot provided (should use current slot), no identity
1170        let result = setup
1171            .rpc
1172            .get_leader_schedule(Some(setup.context.clone()), None, None)
1173            .unwrap();
1174
1175        assert!(result.is_some());
1176        assert!(result.unwrap().is_empty());
1177
1178        // test 3: Valid slot with valid identity
1179        let valid_pubkey = Pubkey::new_unique();
1180        let result = setup
1181            .rpc
1182            .get_leader_schedule(
1183                Some(setup.context.clone()),
1184                None,
1185                Some(RpcLeaderScheduleConfig {
1186                    identity: Some(valid_pubkey.to_string()),
1187                    commitment: None,
1188                }),
1189            )
1190            .unwrap();
1191
1192        assert!(result.is_some());
1193        assert!(result.unwrap().is_empty());
1194
1195        // test 4: Boundary cases - first slot in epoch
1196        let first_slot_in_epoch = 43200050 - 50;
1197        let result = setup
1198            .rpc
1199            .get_leader_schedule(
1200                Some(setup.context.clone()),
1201                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(
1202                    first_slot_in_epoch,
1203                ))),
1204                None,
1205            )
1206            .unwrap();
1207
1208        assert!(result.is_some());
1209        assert!(result.unwrap().is_empty());
1210
1211        // test 5: Boundary cases - last slot in epoch
1212        let last_slot_in_epoch = first_slot_in_epoch + 432000 - 1;
1213        let result = setup
1214            .rpc
1215            .get_leader_schedule(
1216                Some(setup.context.clone()),
1217                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(
1218                    last_slot_in_epoch,
1219                ))),
1220                None,
1221            )
1222            .unwrap();
1223
1224        assert!(result.is_some());
1225        assert!(result.unwrap().is_empty());
1226    }
1227
1228    #[test]
1229    fn test_get_leader_schedule_invalid_cases() {
1230        let setup = TestSetup::new(SurfpoolMinimalRpc);
1231
1232        setup.context.svm_locker.with_svm_writer(|svm_writer| {
1233            svm_writer.latest_epoch_info = EpochInfo {
1234                epoch: 100,
1235                slot_index: 50,
1236                slots_in_epoch: 432000,
1237                absolute_slot: 43200050,
1238                block_height: 43200050,
1239                transaction_count: None,
1240            };
1241        });
1242
1243        let first_slot_in_epoch = 43200050 - 50;
1244        let last_slot_in_epoch = first_slot_in_epoch + 432000 - 1;
1245
1246        // test 1: Slot before current epoch (should return None)
1247        let result = setup
1248            .rpc
1249            .get_leader_schedule(
1250                Some(setup.context.clone()),
1251                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(
1252                    first_slot_in_epoch - 1,
1253                ))),
1254                None,
1255            )
1256            .unwrap();
1257
1258        assert!(result.is_none());
1259
1260        // test 2: Slot after current epoch (should return None)
1261        let result = setup
1262            .rpc
1263            .get_leader_schedule(
1264                Some(setup.context.clone()),
1265                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(
1266                    last_slot_in_epoch + 1,
1267                ))),
1268                None,
1269            )
1270            .unwrap();
1271
1272        assert!(result.is_none());
1273
1274        // test 3: Way outside epoch range (should return None)
1275        let result = setup
1276            .rpc
1277            .get_leader_schedule(
1278                Some(setup.context.clone()),
1279                Some(RpcLeaderScheduleConfigWrapper::SlotOnly(Some(1000000))), // Very old slot
1280                None,
1281            )
1282            .unwrap();
1283
1284        assert!(result.is_none());
1285
1286        // test 4: Invalid identity pubkey (should return Error)
1287        let result = setup.rpc.get_leader_schedule(
1288            Some(setup.context.clone()),
1289            None,
1290            Some(RpcLeaderScheduleConfig {
1291                identity: Some("invalid_pubkey_string".to_string()),
1292                commitment: None,
1293            }),
1294        );
1295
1296        assert!(result.is_err());
1297        let error = result.unwrap_err();
1298        assert_eq!(error.code, ErrorCode::InvalidParams);
1299
1300        // test 5: Empty identity string (should return Error)
1301        let result = setup.rpc.get_leader_schedule(
1302            Some(setup.context.clone()),
1303            None,
1304            Some(RpcLeaderScheduleConfig {
1305                identity: Some("".to_string()),
1306                commitment: None,
1307            }),
1308        );
1309
1310        assert!(result.is_err());
1311        let error = result.unwrap_err();
1312        assert_eq!(error.code, ErrorCode::InvalidParams);
1313
1314        // test 6: No metadata (should return Error from get_svm_locker)
1315        let result = setup.rpc.get_leader_schedule(None, None, None);
1316
1317        assert!(result.is_err());
1318    }
1319
1320    #[test]
1321    fn test_get_vote_accounts_valid_config_returns_empty() {
1322        let setup = TestSetup::new(SurfpoolMinimalRpc);
1323
1324        // test with valid configuration including all optional parameters
1325        let config = RpcGetVoteAccountsConfig {
1326            vote_pubkey: Some("11111111111111111111111111111112".to_string()),
1327            commitment: Some(CommitmentConfig::processed()),
1328            keep_unstaked_delinquents: Some(true),
1329            delinquent_slot_distance: Some(100),
1330        };
1331
1332        let result = setup
1333            .rpc
1334            .get_vote_accounts(Some(setup.context.clone()), Some(config));
1335
1336        // should succeed with valid inputs
1337        assert!(result.is_ok());
1338
1339        let vote_accounts = result.unwrap();
1340
1341        // should return empty current and delinquent arrays
1342        assert_eq!(vote_accounts.current.len(), 0);
1343        assert_eq!(vote_accounts.delinquent.len(), 0);
1344    }
1345
1346    #[test]
1347    fn test_get_vote_accounts_invalid_pubkey_returns_error() {
1348        let setup = TestSetup::new(SurfpoolMinimalRpc);
1349
1350        // test with invalid vote pubkey that's not valid base58
1351        let config = RpcGetVoteAccountsConfig {
1352            vote_pubkey: Some("invalid_pubkey_not_base58".to_string()),
1353            commitment: Some(CommitmentConfig::finalized()),
1354            keep_unstaked_delinquents: Some(false),
1355            delinquent_slot_distance: Some(50),
1356        };
1357
1358        let result = setup
1359            .rpc
1360            .get_vote_accounts(Some(setup.context.clone()), Some(config));
1361
1362        // should fail due to invalid vote pubkey
1363        assert!(result.is_err());
1364
1365        let error = result.unwrap_err();
1366
1367        // should be invalid params error
1368        assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
1369    }
1370}