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}