surfpool_core/rpc/
bank_data.rs

1use jsonrpc_core::Result;
2use jsonrpc_derive::rpc;
3use solana_client::{
4    rpc_config::{RpcBlockProductionConfig, RpcContextConfig},
5    rpc_custom_error::RpcCustomError,
6    rpc_response::{
7        RpcBlockProduction, RpcInflationGovernor, RpcInflationRate, RpcResponseContext,
8    },
9};
10use solana_clock::Slot;
11use solana_commitment_config::CommitmentConfig;
12use solana_epoch_schedule::EpochSchedule;
13use solana_rpc_client_api::response::Response as RpcResponse;
14
15use super::{RunloopContext, State};
16use crate::surfnet::SURFPOOL_IDENTITY_PUBKEY;
17
18#[rpc]
19pub trait BankData {
20    type Metadata;
21
22    /// Returns the minimum balance required for rent exemption based on the given data length.
23    ///
24    /// This RPC method calculates the minimum balance required for an account to be exempt from
25    /// rent charges. It uses the data length of the account to determine the balance. The result
26    /// can help users manage their accounts by ensuring they have enough balance to cover rent
27    /// exemption, preventing accounts from being purged by the system.
28    ///
29    /// ## Parameters
30    /// - `data_len`: The length (in bytes) of the account data. This is used to determine the
31    ///   minimum balance required for rent exemption.
32    /// - `commitment`: (Optional) A `CommitmentConfig` that allows specifying the level of
33    ///   commitment for querying. If not provided, the default commitment level will be used.
34    ///
35    /// ## Returns
36    /// - `Result<u64>`: The method returns the minimum balance required for rent exemption
37    ///   as a `u64`. If successful, it will be wrapped in `Ok`, otherwise an error will be
38    ///   returned.
39    ///
40    /// ## Example Request (JSON-RPC)
41    /// ```json
42    /// {
43    ///   "jsonrpc": "2.0",
44    ///   "id": 1,
45    ///   "method": "getMinimumBalanceForRentExemption",
46    ///   "params": [128]
47    /// }
48    /// ```
49    ///
50    /// ## Example Response
51    /// ```json
52    /// {
53    ///   "jsonrpc": "2.0",
54    ///   "result": 2039280,
55    ///   "id": 1
56    /// }
57    /// ```
58    ///
59    /// # Notes
60    /// - This method is commonly used to determine the required balance when creating new accounts
61    ///   or performing account setup operations that need rent exemption.
62    /// - The `commitment` parameter allows users to specify the level of assurance they want
63    ///   regarding the state of the ledger. For example, using `Confirmed` or `Finalized` ensures
64    ///   that the state is more reliable.
65    ///
66    /// ## Errors
67    /// - If there is an issue with the `data_len` or `commitment` parameter (e.g., invalid data),
68    ///   an error will be returned.
69    #[rpc(meta, name = "getMinimumBalanceForRentExemption")]
70    fn get_minimum_balance_for_rent_exemption(
71        &self,
72        meta: Self::Metadata,
73        data_len: usize,
74        commitment: Option<CommitmentConfig>,
75    ) -> Result<u64>;
76
77    /// Retrieves the inflation governor settings for the network.
78    ///
79    /// This RPC method returns the current inflation governor configuration, which controls the
80    /// inflation rate of the network. The inflation governor is responsible for adjusting the
81    /// inflation rate over time, with parameters like the initial and terminal inflation rates,
82    /// the taper rate, the foundation amount, and the foundation term.
83    ///
84    /// ## Parameters
85    /// - `commitment`: (Optional) A `CommitmentConfig` that specifies the commitment level for
86    ///   querying the inflation governor settings. If not provided, the default commitment level
87    ///   is used. Valid commitment levels include `Processed`, `Confirmed`, or `Finalized`.
88    ///
89    /// ## Returns
90    /// - `Result<RpcInflationGovernor>`: The method returns an `RpcInflationGovernor` struct that
91    ///   contains the inflation parameters if successful. Otherwise, an error will be returned.
92    ///
93    /// ## Example Request (JSON-RPC)
94    /// ```json
95    /// {
96    ///   "jsonrpc": "2.0",
97    ///   "id": 1,
98    ///   "method": "getInflationGovernor",
99    ///   "params": []
100    /// }
101    /// ```
102    ///
103    /// ## Example Response
104    /// ```json
105    /// {
106    ///   "jsonrpc": "2.0",
107    ///   "result": {
108    ///     "initial": 0.15,
109    ///     "terminal": 0.05,
110    ///     "taper": 0.9,
111    ///     "foundation": 0.02,
112    ///     "foundation_term": 5.0
113    ///   },
114    ///   "id": 1
115    /// }
116    /// ```
117    ///
118    /// # Notes
119    /// - The inflation governor defines how inflation changes over time, ensuring the network's
120    ///   growth remains stable and sustainable.
121    /// - The `commitment` parameter allows users to define how strongly they want to ensure the
122    ///   inflation data is confirmed or finalized when queried. For example, using `Confirmed` or
123    ///   `Finalized` ensures a more reliable inflation state.
124    ///
125    /// ## Errors
126    /// - If there is an issue with the `commitment` parameter or an internal error occurs,
127    ///   an error will be returned.
128    #[rpc(meta, name = "getInflationGovernor")]
129    fn get_inflation_governor(
130        &self,
131        meta: Self::Metadata,
132        commitment: Option<CommitmentConfig>,
133    ) -> Result<RpcInflationGovernor>;
134
135    /// Retrieves the current inflation rate for the network.
136    ///
137    /// This RPC method returns the current inflation rate, including the breakdown of inflation
138    /// allocated to different entities such as validators and the foundation, along with the current
139    /// epoch during which the rate applies.
140    ///
141    /// ## Parameters
142    /// - No parameters are required for this method.
143    ///
144    /// ## Returns
145    /// - `Result<RpcInflationRate>`: The method returns an `RpcInflationRate` struct that contains
146    ///   the total inflation rate, the validator portion, the foundation portion, and the epoch
147    ///   during which this inflation rate applies.
148    ///
149    /// ## Example Request (JSON-RPC)
150    /// ```json
151    /// {
152    ///   "jsonrpc": "2.0",
153    ///   "id": 1,
154    ///   "method": "getInflationRate",
155    ///   "params": []
156    /// }
157    /// ```
158    ///
159    /// ## Example Response
160    /// ```json
161    /// {
162    ///   "jsonrpc": "2.0",
163    ///   "result": {
164    ///     "total": 0.10,
165    ///     "validator": 0.07,
166    ///     "foundation": 0.03,
167    ///     "epoch": 1500
168    ///   },
169    ///   "id": 1
170    /// }
171    /// ```
172    ///
173    /// # Notes
174    /// - The total inflation rate is distributed among validators and the foundation based on
175    ///   the configuration defined in the inflation governor.
176    /// - The epoch field indicates the current epoch number during which this inflation rate applies.
177    ///   An epoch is a period during which the network operates under certain parameters.
178    /// - Inflation rates can change over time depending on network conditions and governance decisions.
179    ///
180    /// ## Errors
181    /// - If there is an internal error, or if the RPC request is malformed, an error will be returned.
182    #[rpc(meta, name = "getInflationRate")]
183    fn get_inflation_rate(&self, meta: Self::Metadata) -> Result<RpcInflationRate>;
184
185    /// Retrieves the epoch schedule for the network.
186    ///
187    /// This RPC method returns the configuration for the network's epoch schedule, including
188    /// details on the number of slots per epoch, leader schedule offsets, and epoch warmup.
189    ///
190    /// ## Parameters
191    /// - No parameters are required for this method.
192    ///
193    /// ## Returns
194    /// - `Result<EpochSchedule>`: The method returns an `EpochSchedule` struct, which contains
195    ///   information about the slots per epoch, leader schedule offsets, warmup state, and the
196    ///   first epoch after the warmup period.
197    ///
198    /// ## Example Request (JSON-RPC)
199    /// ```json
200    /// {
201    ///   "jsonrpc": "2.0",
202    ///   "id": 1,
203    ///   "method": "getEpochSchedule",
204    ///   "params": []
205    /// }
206    /// ```
207    ///
208    /// ## Example Response
209    /// ```json
210    /// {
211    ///   "jsonrpc": "2.0",
212    ///   "result": {
213    ///     "slotsPerEpoch": 432000,
214    ///     "leaderScheduleSlotOffset": 500,
215    ///     "warmup": true,
216    ///     "firstNormalEpoch": 8,
217    ///     "firstNormalSlot": 1073741824
218    ///   },
219    ///   "id": 1
220    /// }
221    /// ```
222    ///
223    /// # Notes
224    /// - The `slots_per_epoch` defines the maximum number of slots in each epoch, which determines
225    ///   the number of time slots available for network validators to produce blocks.
226    /// - The `leader_schedule_slot_offset` specifies how many slots before an epoch’s start the leader
227    ///   schedule calculation begins for that epoch.
228    /// - The `warmup` field indicates whether the epochs start short and grow over time.
229    /// - The `first_normal_epoch` marks the first epoch after the warmup period.
230    /// - The `first_normal_slot` gives the first slot after the warmup period in terms of the number of slots
231    ///   from the start of the network.
232    ///
233    /// ## Errors
234    /// - If the RPC request is malformed, or if there is an internal error, an error will be returned.
235    #[rpc(meta, name = "getEpochSchedule")]
236    fn get_epoch_schedule(&self, meta: Self::Metadata) -> Result<EpochSchedule>;
237
238    /// Retrieves the leader of the current slot.
239    ///
240    /// This RPC method returns the leader for the current slot in the Solana network. The leader is responsible
241    /// for producing blocks for the current slot. The leader is selected based on the Solana consensus mechanism.
242    ///
243    /// ## Parameters
244    /// - `config`: An optional configuration for the request, which can include:
245    ///     - `commitment`: A commitment level that defines how "final" the data must be.
246    ///     - `min_context_slot`: An optional parameter to specify a minimum slot for the request.
247    ///
248    /// ## Returns
249    /// - `Result<String>`: The method returns a `String` representing the public key of the leader for the current slot.
250    ///
251    /// ## Example Request (JSON-RPC)
252    /// ```json
253    /// {
254    ///   "jsonrpc": "2.0",
255    ///   "id": 1,
256    ///   "method": "getSlotLeader",
257    ///   "params": []
258    /// }
259    /// ```
260    ///
261    /// ## Example Response
262    /// ```json
263    /// {
264    ///   "jsonrpc": "2.0",
265    ///   "result": "3HgA9r8H9z5Pb2L6Pt5Yq1QoFwgr6YwdKKUh9n2ANp5U"
266    /// }
267    /// ```
268    ///
269    /// # Notes
270    /// - The leader for a given slot is selected based on the Solana network's consensus mechanism, and this method
271    ///   allows you to query the current leader.
272    #[rpc(meta, name = "getSlotLeader")]
273    fn get_slot_leader(
274        &self,
275        meta: Self::Metadata,
276        config: Option<RpcContextConfig>,
277    ) -> Result<String>;
278
279    /// Retrieves the leaders for a specified range of slots.
280    ///
281    /// This RPC method returns the leaders for a specified range of slots in the Solana network. You can
282    /// specify the `start_slot` from which the leaders should be queried and limit the number of results
283    /// with the `limit` parameter. The leaders are responsible for producing blocks in the respective slots.
284    ///
285    /// ## Parameters
286    /// - `start_slot`: The starting slot number for which the leaders should be queried.
287    /// - `limit`: The number of slots (starting from `start_slot`) for which the leaders should be retrieved.
288    ///
289    /// ## Returns
290    /// - `Result<Vec<String>>`: A vector of `String` values representing the public keys of the leaders for
291    ///   the specified slot range.
292    ///
293    /// ## Example Request (JSON-RPC)
294    /// ```json
295    /// {
296    ///   "jsonrpc": "2.0",
297    ///   "id": 1,
298    ///   "method": "getSlotLeaders",
299    ///   "params": [1000, 5]
300    /// }
301    /// ```
302    ///
303    /// ## Example Response
304    /// ```json
305    /// {
306    ///   "jsonrpc": "2.0",
307    ///   "result": [
308    ///     "3HgA9r8H9z5Pb2L6Pt5Yq1QoFwgr6YwdKKUh9n2ANp5U",
309    ///     "BBh1FwXts8EZY6rPZ5kS2ygq99wYjFd5K5daRjc7eF9X",
310    ///     "4XYo7yP5J2J8sLNSW3wGYPk3mdS1rbZUy4oFCp7wH1DN",
311    ///     "8v1Cp6sHZh8XfGWS7sHZczH3v9NxdgMbo3g91Sh88dcJ",
312    ///     "N6bPqwEoD9StS4AnzE27rHyz47tPcsZQjvW9w8p2NhF7"
313    ///   ]
314    /// }
315    /// ```
316    ///
317    /// # Notes
318    /// - The leaders are returned in the order corresponding to the slots queried, starting from `start_slot`
319    ///   and continuing for `limit` slots.
320    /// - This method provides an efficient way to get multiple leaders for a range of slots, useful for tracking
321    ///   leaders over time or for scheduling purposes in decentralized applications.
322    #[rpc(meta, name = "getSlotLeaders")]
323    fn get_slot_leaders(
324        &self,
325        meta: Self::Metadata,
326        start_slot: Slot,
327        limit: u64,
328    ) -> Result<Vec<String>>;
329
330    /// Retrieves block production information for the specified validator identity or range of slots.
331    ///
332    /// This RPC method returns block production details for a given validator identity or a range of slots
333    /// within a certain epoch. If no `identity` is provided, the method returns block production data for all
334    /// validators. If a `range` is provided, it will return block production information for the slots within
335    /// the specified range.
336    ///
337    /// ## Parameters
338    /// - `config`: An optional configuration object that can include:
339    ///     - `identity`: The base-58 encoded public key of a validator to query for block production data. If `None`, results for all validators will be returned.
340    ///     - `range`: A range of slots for which block production information is needed. The range will default to the current epoch if `None`.
341    ///     - `commitment`: The commitment level (optional) to use when querying for the block production data.
342    ///
343    /// ## Returns
344    /// - `Result<RpcResponse<RpcBlockProduction>>`: The result contains a response object with block production data, including the number of leader slots and blocks produced by each validator.
345    ///
346    /// ## Example Request (JSON-RPC)
347    /// ```json
348    /// {
349    ///   "jsonrpc": "2.0",
350    ///   "id": 1,
351    ///   "method": "getBlockProduction",
352    ///   "params": [{
353    ///     "identity": "3HgA9r8H9z5Pb2L6Pt5Yq1QoFwgr6YwdKKUh9n2ANp5U",
354    ///     "range": {
355    ///       "firstSlot": 1000,
356    ///       "lastSlot": 1050
357    ///     }
358    ///   }]
359    /// }
360    /// ```
361    ///
362    /// ## Example Response
363    /// ```json
364    /// {
365    ///   "jsonrpc": "2.0",
366    ///   "result": {
367    ///     "byIdentity": {
368    ///       "3HgA9r8H9z5Pb2L6Pt5Yq1QoFwgr6YwdKKUh9n2ANp5U": [10, 8],
369    ///       "BBh1FwXts8EZY6rPZ5kS2ygq99wYjFd5K5daRjc7eF9X": [5, 4]
370    ///     },
371    ///     "range": {
372    ///       "firstSlot": 1000,
373    ///       "lastSlot": 1050
374    ///     }
375    ///   }
376    /// }
377    /// ```
378    ///
379    /// # Notes
380    /// - The response contains a map of validator identities to a tuple of two values:
381    ///     - The first value is the number of leader slots.
382    ///     - The second value is the number of blocks produced by that validator in the queried range.
383    /// - The `range` object specifies the range of slots that the block production information applies to, with `first_slot` being the starting slot and `last_slot` being the optional ending slot.
384    ///
385    /// ## Example Response Interpretation
386    /// - In the example response, the identity `3HgA9r8H9z5Pb2L6Pt5Yq1QoFwgr6YwdKKUh9n2ANp5U` produced 10 leader slots and 8 blocks between slots 1000 and 1050.
387    /// - Similarly, `BBh1FwXts8EZY6rPZ5kS2ygq99wYjFd5K5daRjc7eF9X` produced 5 leader slots and 4 blocks in the same slot range.
388    #[rpc(meta, name = "getBlockProduction")]
389    fn get_block_production(
390        &self,
391        meta: Self::Metadata,
392        config: Option<RpcBlockProductionConfig>,
393    ) -> Result<RpcResponse<RpcBlockProduction>>;
394}
395
396#[derive(Clone)]
397pub struct SurfpoolBankDataRpc;
398impl BankData for SurfpoolBankDataRpc {
399    type Metadata = Option<RunloopContext>;
400
401    fn get_minimum_balance_for_rent_exemption(
402        &self,
403        meta: Self::Metadata,
404        data_len: usize,
405        _commitment: Option<CommitmentConfig>,
406    ) -> Result<u64> {
407        meta.with_svm_reader(move |svm_reader| {
408            svm_reader
409                .inner
410                .minimum_balance_for_rent_exemption(data_len)
411        })
412        .map_err(Into::into)
413    }
414
415    fn get_inflation_governor(
416        &self,
417        meta: Self::Metadata,
418        _commitment: Option<CommitmentConfig>,
419    ) -> Result<RpcInflationGovernor> {
420        meta.with_svm_reader(|svm_reader| svm_reader.inflation.into())
421            .map_err(Into::into)
422    }
423
424    fn get_inflation_rate(&self, meta: Self::Metadata) -> Result<RpcInflationRate> {
425        meta.with_svm_reader(|svm_reader| {
426            let inflation_activation_slot =
427                svm_reader.blocks.keys().min().copied().unwrap_or_default();
428            let epoch_schedule = svm_reader.inner.get_sysvar::<EpochSchedule>();
429            let inflation_start_slot = epoch_schedule.get_first_slot_in_epoch(
430                epoch_schedule
431                    .get_epoch(inflation_activation_slot)
432                    .saturating_sub(1),
433            );
434            let epoch = svm_reader.latest_epoch_info().epoch;
435            let num_slots = epoch_schedule.get_first_slot_in_epoch(epoch) - inflation_start_slot;
436
437            let inflation = svm_reader.inflation;
438            let slots_per_year = svm_reader.genesis_config.slots_per_year();
439
440            let slot_in_year = num_slots as f64 / slots_per_year;
441
442            RpcInflationRate {
443                total: inflation.total(slot_in_year),
444                validator: inflation.validator(slot_in_year),
445                foundation: inflation.foundation(slot_in_year),
446                epoch,
447            }
448        })
449        .map_err(Into::into)
450    }
451
452    fn get_epoch_schedule(&self, meta: Self::Metadata) -> Result<EpochSchedule> {
453        meta.with_svm_reader(move |svm_reader| svm_reader.inner.get_sysvar::<EpochSchedule>())
454            .map_err(Into::into)
455    }
456
457    fn get_slot_leader(
458        &self,
459        meta: Self::Metadata,
460        config: Option<RpcContextConfig>,
461    ) -> Result<String> {
462        let svm_locker = meta.get_svm_locker()?;
463        let config = config.unwrap_or_default();
464
465        let committed_slot =
466            svm_locker.get_slot_for_commitment(&config.commitment.unwrap_or_default());
467
468        // validate minContextSlot if provided
469        if let Some(min_context_slot) = config.min_context_slot {
470            if committed_slot < min_context_slot {
471                return Err(RpcCustomError::MinContextSlotNotReached {
472                    context_slot: min_context_slot,
473                }
474                .into());
475            }
476        }
477
478        Ok(SURFPOOL_IDENTITY_PUBKEY.to_string())
479    }
480
481    fn get_slot_leaders(
482        &self,
483        meta: Self::Metadata,
484        start_slot: Slot,
485        limit: u64,
486    ) -> Result<Vec<String>> {
487        if limit == 0 || limit > 5000 {
488            return Err(jsonrpc_core::Error {
489                code: jsonrpc_core::ErrorCode::InvalidParams,
490                message: "Limit must be between 1 and 5000".to_string(),
491                data: None,
492            });
493        }
494
495        let svm_locker = meta.get_svm_locker()?;
496        let epoch_info = svm_locker.get_epoch_info();
497
498        let first_slot_in_epoch = epoch_info
499            .absolute_slot
500            .saturating_sub(epoch_info.slot_index);
501        let last_slot_in_epoch = first_slot_in_epoch + epoch_info.slots_in_epoch.saturating_sub(1);
502        if start_slot > last_slot_in_epoch || (start_slot + limit) > last_slot_in_epoch {
503            return Err(jsonrpc_core::Error {
504                code: jsonrpc_core::ErrorCode::InvalidParams,
505                message: format!(
506                    "Invalid slot range: leader schedule for epoch {} is unavailable",
507                    epoch_info.epoch
508                ),
509                data: None,
510            });
511        }
512
513        Ok(vec![])
514    }
515
516    fn get_block_production(
517        &self,
518        meta: Self::Metadata,
519        config: Option<RpcBlockProductionConfig>,
520    ) -> Result<RpcResponse<RpcBlockProduction>> {
521        meta.with_svm_reader(|svm_reader| {
522            let current_slot = svm_reader.get_latest_absolute_slot();
523            let epoch_info = &svm_reader.latest_epoch_info;
524
525            let (first_slot, last_slot) = if let Some(ref config) = config {
526                if let Some(ref range) = config.range {
527                    (range.first_slot, range.last_slot.unwrap_or(current_slot))
528                } else {
529                    let epoch_start_slot = epoch_info.absolute_slot - epoch_info.slot_index;
530                    (epoch_start_slot, current_slot)
531                }
532            } else {
533                let epoch_start_slot = epoch_info.absolute_slot - epoch_info.slot_index;
534                (epoch_start_slot, current_slot)
535            };
536
537            RpcResponse {
538                context: RpcResponseContext::new(current_slot),
539                value: RpcBlockProduction {
540                    // Empty HashMap - no validator block production data in simulation
541                    by_identity: std::collections::HashMap::new(),
542                    range: solana_client::rpc_response::RpcBlockProductionRange {
543                        first_slot,
544                        last_slot,
545                    },
546                },
547            }
548        })
549        .map_err(Into::into)
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use solana_client::rpc_config::RpcBlockProductionConfigRange;
556    use solana_commitment_config::CommitmentLevel;
557    use solana_sdk::inflation::Inflation;
558
559    use super::*;
560    use crate::tests::helpers::TestSetup;
561
562    #[tokio::test(flavor = "multi_thread")]
563    async fn test_get_epoch_schedule() {
564        let setup = TestSetup::new(SurfpoolBankDataRpc);
565        let res = setup.rpc.get_epoch_schedule(Some(setup.context)).unwrap();
566
567        assert_eq!(res, EpochSchedule::default());
568    }
569
570    #[test]
571    fn test_get_block_production() {
572        let setup = TestSetup::new(SurfpoolBankDataRpc);
573
574        // test with no config
575        let result = setup
576            .rpc
577            .get_block_production(Some(setup.context.clone()), None)
578            .unwrap();
579
580        // verify empty results (simulation mode)
581        assert!(
582            result.value.by_identity.is_empty(),
583            "Should have no validators in simulation"
584        );
585        assert!(
586            result.value.range.first_slot <= result.value.range.last_slot,
587            "Valid slot range"
588        );
589
590        // test with custom range
591        let config = Some(RpcBlockProductionConfig {
592            identity: None,
593            range: Some(RpcBlockProductionConfigRange {
594                first_slot: 100,
595                last_slot: Some(200),
596            }),
597            commitment: None,
598        });
599
600        let result2 = setup
601            .rpc
602            .get_block_production(Some(setup.context), config)
603            .unwrap();
604
605        assert_eq!(result2.value.range.first_slot, 100);
606        assert_eq!(result2.value.range.last_slot, 200);
607        assert!(result2.value.by_identity.is_empty());
608    }
609
610    #[test]
611    fn test_get_slot_leaders() {
612        let setup = TestSetup::new(SurfpoolBankDataRpc);
613
614        // test with valid parameters
615        let result = setup
616            .rpc
617            .get_slot_leaders(Some(setup.context.clone()), 0, 10)
618            .unwrap();
619
620        assert!(
621            result.is_empty(),
622            "Should return empty leaders in simulation"
623        );
624
625        // test with invalid limit
626        let err = setup
627            .rpc
628            .get_slot_leaders(Some(setup.context.clone()), 0, 6000)
629            .unwrap_err();
630
631        assert_eq!(
632            err.code,
633            jsonrpc_core::ErrorCode::InvalidParams,
634            "Should return InvalidParams error for limit > 5000"
635        );
636
637        let latest_slot = setup.context.svm_locker.get_latest_absolute_slot();
638
639        // test with start_slot >= latest_slot
640        let err = setup
641            .rpc
642            .get_slot_leaders(Some(setup.context), latest_slot + 100, 10)
643            .unwrap_err();
644
645        assert_eq!(
646            err.code,
647            jsonrpc_core::ErrorCode::InvalidParams,
648            "Should return InvalidParams error for start_slot >= latest_slot"
649        );
650    }
651
652    #[test]
653    fn test_get_inflation_rate() {
654        let setup = TestSetup::new(SurfpoolBankDataRpc);
655        let result = setup.rpc.get_inflation_rate(Some(setup.context));
656        assert!(result.is_ok())
657    }
658
659    #[test]
660    fn test_get_inflation_governor() {
661        let setup = TestSetup::new(SurfpoolBankDataRpc);
662
663        let result = setup
664            .rpc
665            .get_inflation_governor(Some(setup.context), None)
666            .unwrap();
667
668        assert_eq!(result, Inflation::default().into());
669    }
670
671    #[test]
672    fn test_get_minimum_balance_for_rent_exemption() {
673        let setup = TestSetup::new(SurfpoolBankDataRpc);
674        let rent = setup
675            .rpc
676            .get_minimum_balance_for_rent_exemption(Some(setup.context), 0, None)
677            .unwrap();
678
679        assert_eq!(rent, 890880)
680    }
681
682    #[tokio::test(flavor = "multi_thread")]
683    async fn test_get_slot_leader_basic() {
684        let setup = TestSetup::new(SurfpoolBankDataRpc);
685
686        let result = setup.rpc.get_slot_leader(Some(setup.context.clone()), None);
687
688        match result {
689            Ok(identity) => {
690                assert_eq!(identity, SURFPOOL_IDENTITY_PUBKEY.to_string());
691                println!("✅ Basic test passed");
692            }
693            Err(e) => {
694                panic!("❌ Test failed: {:?}", e);
695            }
696        }
697    }
698
699    #[tokio::test(flavor = "multi_thread")]
700    async fn test_get_slot_leader_with_config() {
701        let setup = TestSetup::new(SurfpoolBankDataRpc);
702
703        let config = RpcContextConfig {
704            commitment: Some(CommitmentConfig {
705                commitment: CommitmentLevel::Processed,
706            }),
707            min_context_slot: None,
708        };
709
710        let result = setup
711            .rpc
712            .get_slot_leader(Some(setup.context.clone()), Some(config));
713
714        match result {
715            Ok(identity) => {
716                assert_eq!(identity, SURFPOOL_IDENTITY_PUBKEY.to_string());
717                println!("✅ Config test passed");
718            }
719            Err(e) => {
720                panic!("❌ Test failed: {:?}", e);
721            }
722        }
723    }
724
725    #[tokio::test(flavor = "multi_thread")]
726    async fn test_get_slot_leader_min_context_slot_error() {
727        let setup = TestSetup::new(SurfpoolBankDataRpc);
728
729        let config = RpcContextConfig {
730            commitment: Some(CommitmentConfig {
731                commitment: CommitmentLevel::Finalized,
732            }),
733            min_context_slot: Some(999999), // high number that should fail
734        };
735
736        let result = setup
737            .rpc
738            .get_slot_leader(Some(setup.context.clone()), Some(config));
739
740        assert!(result.is_err());
741        println!("✅ MinContextSlot error test passed");
742    }
743}