Skip to main content

near_kit/types/
block_reference.rs

1//! Block reference types for RPC queries.
2
3use serde::{Deserialize, Serialize};
4
5use super::CryptoHash;
6
7/// Sync checkpoint for block references.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum SyncCheckpoint {
11    /// Genesis block.
12    Genesis,
13    /// Earliest available block.
14    EarliestAvailable,
15}
16
17/// Reference to a specific block for RPC queries.
18///
19/// Every NEAR RPC query operates on state at a specific block.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum BlockReference {
22    /// Query at latest block with specified finality.
23    Finality(Finality),
24    /// Query at specific block height.
25    Height(u64),
26    /// Query at specific block hash.
27    Hash(CryptoHash),
28    /// Query at a sync checkpoint (genesis or earliest available).
29    SyncCheckpoint(SyncCheckpoint),
30}
31
32impl Default for BlockReference {
33    fn default() -> Self {
34        Self::Finality(Finality::Final)
35    }
36}
37
38impl BlockReference {
39    /// Query at final block.
40    pub fn final_() -> Self {
41        Self::Finality(Finality::Final)
42    }
43
44    /// Query at optimistic (latest) block.
45    pub fn optimistic() -> Self {
46        Self::Finality(Finality::Optimistic)
47    }
48
49    /// Query at near-final block.
50    pub fn near_final() -> Self {
51        Self::Finality(Finality::NearFinal)
52    }
53
54    /// Query at specific height.
55    pub fn at_height(height: u64) -> Self {
56        Self::Height(height)
57    }
58
59    /// Query at specific hash.
60    pub fn at_hash(hash: CryptoHash) -> Self {
61        Self::Hash(hash)
62    }
63
64    /// Query at genesis block.
65    pub fn genesis() -> Self {
66        Self::SyncCheckpoint(SyncCheckpoint::Genesis)
67    }
68
69    /// Query at earliest available block.
70    pub fn earliest_available() -> Self {
71        Self::SyncCheckpoint(SyncCheckpoint::EarliestAvailable)
72    }
73
74    /// Convert to JSON for RPC requests.
75    pub fn to_rpc_params(&self) -> serde_json::Value {
76        match self {
77            BlockReference::Finality(f) => {
78                serde_json::json!({ "finality": f.as_str() })
79            }
80            BlockReference::Height(h) => {
81                serde_json::json!({ "block_id": *h })
82            }
83            BlockReference::Hash(h) => {
84                serde_json::json!({ "block_id": h.to_string() })
85            }
86            BlockReference::SyncCheckpoint(cp) => {
87                let cp_str = match cp {
88                    SyncCheckpoint::Genesis => "genesis",
89                    SyncCheckpoint::EarliestAvailable => "earliest_available",
90                };
91                serde_json::json!({ "sync_checkpoint": cp_str })
92            }
93        }
94    }
95}
96
97impl From<Finality> for BlockReference {
98    fn from(f: Finality) -> Self {
99        Self::Finality(f)
100    }
101}
102
103impl From<u64> for BlockReference {
104    fn from(height: u64) -> Self {
105        Self::Height(height)
106    }
107}
108
109impl From<CryptoHash> for BlockReference {
110    fn from(hash: CryptoHash) -> Self {
111        Self::Hash(hash)
112    }
113}
114
115/// Finality level for queries.
116#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "lowercase")]
118pub enum Finality {
119    /// Latest optimistic block. Fastest, but may be reorged.
120    Optimistic,
121    /// Doomslug finality. Irreversible unless validator slashed.
122    #[serde(rename = "near-final")]
123    NearFinal,
124    /// Fully finalized. Slowest, 100% guaranteed.
125    #[default]
126    Final,
127}
128
129impl Finality {
130    /// Get the string representation for RPC.
131    pub fn as_str(&self) -> &'static str {
132        match self {
133            Finality::Optimistic => "optimistic",
134            Finality::NearFinal => "near-final",
135            Finality::Final => "final",
136        }
137    }
138}
139
140/// Transaction execution status for send_tx wait_until parameter.
141#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
143pub enum TxExecutionStatus {
144    /// Don't wait, return immediately after RPC accepts.
145    None,
146    /// Wait for inclusion in a block.
147    Included,
148    /// Wait for execution (optimistic).
149    #[default]
150    ExecutedOptimistic,
151    /// Wait for inclusion in final block.
152    IncludedFinal,
153    /// Wait for execution in final block.
154    Executed,
155    /// Wait for full finality.
156    Final,
157}
158
159impl TxExecutionStatus {
160    /// Get the string representation for RPC.
161    pub fn as_str(&self) -> &'static str {
162        match self {
163            Self::None => "NONE",
164            Self::Included => "INCLUDED",
165            Self::ExecutedOptimistic => "EXECUTED_OPTIMISTIC",
166            Self::IncludedFinal => "INCLUDED_FINAL",
167            Self::Executed => "EXECUTED",
168            Self::Final => "FINAL",
169        }
170    }
171
172    /// Returns `true` if all non-refund receipt outcomes are available.
173    ///
174    /// True for `ExecutedOptimistic`, `Executed`, and `Final`.
175    pub fn is_executed(&self) -> bool {
176        matches!(
177            self,
178            Self::ExecutedOptimistic | Self::Executed | Self::Final
179        )
180    }
181
182    /// Returns `true` if the transaction's block has reached finality.
183    ///
184    /// True for `IncludedFinal`, `Executed`, and `Final`.
185    pub fn is_block_final(&self) -> bool {
186        matches!(self, Self::IncludedFinal | Self::Executed | Self::Final)
187    }
188
189    /// Returns `true` if all receipts are executed and all blocks finalized.
190    pub fn is_final(&self) -> bool {
191        matches!(self, Self::Final)
192    }
193}
194
195/// Partial ordering for `TxExecutionStatus` forms a diamond lattice:
196///
197/// ```text
198///         None
199///          |
200///       Included
201///       /      \
202/// ExecutedOptimistic  IncludedFinal
203///       \      /
204///       Executed
205///          |
206///        Final
207/// ```
208///
209/// `ExecutedOptimistic` and `IncludedFinal` are **incomparable** because they
210/// represent progress on orthogonal axes (execution vs block finality).
211impl PartialOrd for TxExecutionStatus {
212    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
213        use TxExecutionStatus::*;
214        use std::cmp::Ordering::*;
215
216        if self == other {
217            return Some(Equal);
218        }
219
220        // Assign each variant a position in the lattice.
221        // Returns (execution_level, finality_level).
222        fn axes(s: &TxExecutionStatus) -> (u8, u8) {
223            match s {
224                None => (0, 0),
225                Included => (1, 1),
226                ExecutedOptimistic => (2, 1),
227                IncludedFinal => (1, 2),
228                Executed => (2, 2),
229                Final => (3, 3),
230            }
231        }
232
233        let (ex_a, fin_a) = axes(self);
234        let (ex_b, fin_b) = axes(other);
235
236        match (ex_a.cmp(&ex_b), fin_a.cmp(&fin_b)) {
237            (Equal, Equal) => Some(Equal),
238            (Less | Equal, Less | Equal) => Some(Less),
239            (Greater | Equal, Greater | Equal) => Some(Greater),
240            _ => Option::None, // incomparable
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_block_reference_rpc_params() {
251        let final_ref = BlockReference::final_();
252        let params = final_ref.to_rpc_params();
253        assert_eq!(params["finality"], "final");
254
255        let height_ref = BlockReference::Height(12345);
256        let params = height_ref.to_rpc_params();
257        assert_eq!(params["block_id"], 12345);
258    }
259
260    #[test]
261    fn test_finality_as_str() {
262        assert_eq!(Finality::Final.as_str(), "final");
263        assert_eq!(Finality::Optimistic.as_str(), "optimistic");
264        assert_eq!(Finality::NearFinal.as_str(), "near-final");
265    }
266
267    #[test]
268    fn test_tx_execution_status_as_str() {
269        assert_eq!(TxExecutionStatus::Final.as_str(), "FINAL");
270        assert_eq!(
271            TxExecutionStatus::ExecutedOptimistic.as_str(),
272            "EXECUTED_OPTIMISTIC"
273        );
274    }
275
276    #[test]
277    fn test_block_reference_hash_to_rpc_params() {
278        let hash = CryptoHash::hash(b"test block");
279        let block_ref = BlockReference::at_hash(hash);
280        let params = block_ref.to_rpc_params();
281        assert_eq!(params["block_id"], hash.to_string());
282    }
283
284    #[test]
285    fn test_block_reference_constructors() {
286        // Test all constructor methods
287        let final_ref = BlockReference::final_();
288        assert!(matches!(
289            final_ref,
290            BlockReference::Finality(Finality::Final)
291        ));
292
293        let optimistic_ref = BlockReference::optimistic();
294        assert!(matches!(
295            optimistic_ref,
296            BlockReference::Finality(Finality::Optimistic)
297        ));
298
299        let near_final_ref = BlockReference::near_final();
300        assert!(matches!(
301            near_final_ref,
302            BlockReference::Finality(Finality::NearFinal)
303        ));
304
305        let height_ref = BlockReference::at_height(12345);
306        assert!(matches!(height_ref, BlockReference::Height(12345)));
307
308        let hash = CryptoHash::hash(b"test");
309        let hash_ref = BlockReference::at_hash(hash);
310        assert!(matches!(hash_ref, BlockReference::Hash(_)));
311    }
312
313    #[test]
314    fn test_block_reference_default() {
315        let default = BlockReference::default();
316        assert_eq!(default, BlockReference::Finality(Finality::Final));
317    }
318
319    #[test]
320    fn test_block_reference_from_finality() {
321        let block_ref: BlockReference = Finality::Optimistic.into();
322        assert_eq!(block_ref, BlockReference::Finality(Finality::Optimistic));
323    }
324
325    #[test]
326    fn test_block_reference_from_height() {
327        let block_ref: BlockReference = 99999u64.into();
328        assert_eq!(block_ref, BlockReference::Height(99999));
329    }
330
331    #[test]
332    fn test_block_reference_from_hash() {
333        let hash = CryptoHash::hash(b"block");
334        let block_ref: BlockReference = hash.into();
335        assert_eq!(block_ref, BlockReference::Hash(hash));
336    }
337
338    #[test]
339    fn test_finality_default() {
340        let default = Finality::default();
341        assert_eq!(default, Finality::Final);
342    }
343
344    #[test]
345    fn test_tx_execution_status_default() {
346        let default = TxExecutionStatus::default();
347        assert_eq!(default, TxExecutionStatus::ExecutedOptimistic);
348    }
349
350    #[test]
351    fn test_tx_execution_status_all_variants() {
352        assert_eq!(TxExecutionStatus::None.as_str(), "NONE");
353        assert_eq!(TxExecutionStatus::Included.as_str(), "INCLUDED");
354        assert_eq!(
355            TxExecutionStatus::ExecutedOptimistic.as_str(),
356            "EXECUTED_OPTIMISTIC"
357        );
358        assert_eq!(TxExecutionStatus::IncludedFinal.as_str(), "INCLUDED_FINAL");
359        assert_eq!(TxExecutionStatus::Executed.as_str(), "EXECUTED");
360        assert_eq!(TxExecutionStatus::Final.as_str(), "FINAL");
361    }
362
363    #[test]
364    fn test_finality_serde_roundtrip() {
365        // Test all variants serialize/deserialize correctly
366        for finality in [Finality::Optimistic, Finality::NearFinal, Finality::Final] {
367            let json = serde_json::to_string(&finality).unwrap();
368            let parsed: Finality = serde_json::from_str(&json).unwrap();
369            assert_eq!(finality, parsed);
370        }
371    }
372
373    #[test]
374    fn test_tx_execution_status_serde_roundtrip() {
375        for status in [
376            TxExecutionStatus::None,
377            TxExecutionStatus::Included,
378            TxExecutionStatus::ExecutedOptimistic,
379            TxExecutionStatus::IncludedFinal,
380            TxExecutionStatus::Executed,
381            TxExecutionStatus::Final,
382        ] {
383            let json = serde_json::to_string(&status).unwrap();
384            let parsed: TxExecutionStatus = serde_json::from_str(&json).unwrap();
385            assert_eq!(status, parsed);
386        }
387    }
388
389    #[test]
390    fn test_block_reference_clone_and_eq() {
391        let original = BlockReference::Height(12345);
392        let cloned = original.clone();
393        assert_eq!(original, cloned);
394    }
395
396    #[test]
397    fn test_finality_clone_and_copy() {
398        let f1 = Finality::Optimistic;
399        let f2 = f1; // Copy
400        #[allow(clippy::clone_on_copy)]
401        let f3 = f1.clone(); // Clone (intentionally testing Clone impl)
402        assert_eq!(f1, f2);
403        assert_eq!(f1, f3);
404    }
405
406    #[test]
407    fn test_sync_checkpoint_constructors() {
408        let genesis = BlockReference::genesis();
409        assert!(matches!(
410            genesis,
411            BlockReference::SyncCheckpoint(SyncCheckpoint::Genesis)
412        ));
413
414        let earliest = BlockReference::earliest_available();
415        assert!(matches!(
416            earliest,
417            BlockReference::SyncCheckpoint(SyncCheckpoint::EarliestAvailable)
418        ));
419    }
420
421    #[test]
422    fn test_sync_checkpoint_rpc_params() {
423        let genesis = BlockReference::genesis();
424        let params = genesis.to_rpc_params();
425        assert_eq!(params["sync_checkpoint"], "genesis");
426
427        let earliest = BlockReference::earliest_available();
428        let params = earliest.to_rpc_params();
429        assert_eq!(params["sync_checkpoint"], "earliest_available");
430    }
431
432    #[test]
433    fn test_sync_checkpoint_serde_roundtrip() {
434        for cp in [SyncCheckpoint::Genesis, SyncCheckpoint::EarliestAvailable] {
435            let json = serde_json::to_string(&cp).unwrap();
436            let parsed: SyncCheckpoint = serde_json::from_str(&json).unwrap();
437            assert_eq!(cp, parsed);
438        }
439    }
440
441    #[test]
442    fn test_tx_execution_status_is_executed() {
443        assert!(!TxExecutionStatus::None.is_executed());
444        assert!(!TxExecutionStatus::Included.is_executed());
445        assert!(TxExecutionStatus::ExecutedOptimistic.is_executed());
446        assert!(!TxExecutionStatus::IncludedFinal.is_executed());
447        assert!(TxExecutionStatus::Executed.is_executed());
448        assert!(TxExecutionStatus::Final.is_executed());
449    }
450
451    #[test]
452    fn test_tx_execution_status_is_block_final() {
453        assert!(!TxExecutionStatus::None.is_block_final());
454        assert!(!TxExecutionStatus::Included.is_block_final());
455        assert!(!TxExecutionStatus::ExecutedOptimistic.is_block_final());
456        assert!(TxExecutionStatus::IncludedFinal.is_block_final());
457        assert!(TxExecutionStatus::Executed.is_block_final());
458        assert!(TxExecutionStatus::Final.is_block_final());
459    }
460
461    #[test]
462    fn test_tx_execution_status_is_final() {
463        assert!(!TxExecutionStatus::None.is_final());
464        assert!(!TxExecutionStatus::Included.is_final());
465        assert!(!TxExecutionStatus::ExecutedOptimistic.is_final());
466        assert!(!TxExecutionStatus::IncludedFinal.is_final());
467        assert!(!TxExecutionStatus::Executed.is_final());
468        assert!(TxExecutionStatus::Final.is_final());
469    }
470
471    #[test]
472    fn test_tx_execution_status_partial_ord_linear() {
473        // Linear chain: None < Included < Executed < Final
474        assert!(TxExecutionStatus::None < TxExecutionStatus::Included);
475        assert!(TxExecutionStatus::Included < TxExecutionStatus::Executed);
476        assert!(TxExecutionStatus::Executed < TxExecutionStatus::Final);
477        assert!(TxExecutionStatus::None < TxExecutionStatus::Final);
478    }
479
480    #[test]
481    fn test_tx_execution_status_partial_ord_branches() {
482        // Both branches are greater than Included
483        assert!(TxExecutionStatus::ExecutedOptimistic > TxExecutionStatus::Included);
484        assert!(TxExecutionStatus::IncludedFinal > TxExecutionStatus::Included);
485        // Both branches are less than Executed
486        assert!(TxExecutionStatus::ExecutedOptimistic < TxExecutionStatus::Executed);
487        assert!(TxExecutionStatus::IncludedFinal < TxExecutionStatus::Executed);
488    }
489
490    #[test]
491    fn test_tx_execution_status_partial_ord_incomparable() {
492        // ExecutedOptimistic and IncludedFinal are incomparable
493        assert_eq!(
494            TxExecutionStatus::ExecutedOptimistic.partial_cmp(&TxExecutionStatus::IncludedFinal),
495            Option::None,
496        );
497        assert_eq!(
498            TxExecutionStatus::IncludedFinal.partial_cmp(&TxExecutionStatus::ExecutedOptimistic),
499            Option::None,
500        );
501        // Neither is >, <, ==
502        assert_ne!(
503            TxExecutionStatus::ExecutedOptimistic,
504            TxExecutionStatus::IncludedFinal
505        );
506    }
507}