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}