dig_chainsource_interface/record.rs
1//! [`CoinRecord`] — a coin plus the chain metadata a consumer needs to reason about it: where it
2//! was confirmed, whether/when it was spent, its block timestamp, and whether it is a coinbase.
3
4use chia_protocol::{Coin, CoinState};
5
6/// A coin together with its on-chain lifecycle metadata, as read from a [`ChainSource`](crate::ChainSource).
7///
8/// This is the canonical result shape for coin reads across the ecosystem. Heights and the
9/// timestamp are `Option` because a light source may know a coin exists without knowing its block
10/// context — `None` means "not known by this source", never "does not exist".
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct CoinRecord {
13 /// The coin itself (parent, puzzle hash, amount).
14 pub coin: Coin,
15 /// The block height at which the coin was created/confirmed, if known.
16 pub confirmed_height: Option<u32>,
17 /// The block height at which the coin was spent, if it has been spent and the source knows it.
18 pub spent_height: Option<u32>,
19 /// The Unix timestamp of the confirming block, if the source resolves timestamps.
20 pub timestamp: Option<u64>,
21 /// Whether the coin is a coinbase (farmer/pool reward) coin.
22 pub coinbase: bool,
23}
24
25impl CoinRecord {
26 /// Whether the coin has been spent (i.e. a spent height is known).
27 pub fn is_spent(&self) -> bool {
28 self.spent_height.is_some()
29 }
30
31 /// Builds a [`CoinRecord`] from a wallet-protocol [`CoinState`], mapping
32 /// `created_height -> confirmed_height` and preserving `spent_height`.
33 ///
34 /// A `CoinState` carries no timestamp or coinbase flag, so those become `None`/`false` — a
35 /// source that resolves them fills them in via the fuller read path.
36 pub fn from_coin_state(state: CoinState) -> Self {
37 Self {
38 coin: state.coin,
39 confirmed_height: state.created_height,
40 spent_height: state.spent_height,
41 timestamp: None,
42 coinbase: false,
43 }
44 }
45}
46
47impl From<CoinState> for CoinRecord {
48 fn from(state: CoinState) -> Self {
49 Self::from_coin_state(state)
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use chia_protocol::Bytes32;
57
58 fn sample_coin() -> Coin {
59 Coin::new(Bytes32::new([1u8; 32]), Bytes32::new([2u8; 32]), 7)
60 }
61
62 #[test]
63 fn from_coin_state_maps_created_to_confirmed_and_defaults() {
64 let state = CoinState {
65 coin: sample_coin(),
66 spent_height: Some(200),
67 created_height: Some(100),
68 };
69 let record = CoinRecord::from(state);
70
71 assert_eq!(record.coin, sample_coin());
72 assert_eq!(record.confirmed_height, Some(100));
73 assert_eq!(record.spent_height, Some(200));
74 assert_eq!(record.timestamp, None);
75 assert!(!record.coinbase);
76 assert!(record.is_spent());
77 }
78
79 #[test]
80 fn unspent_record_reports_not_spent() {
81 let state = CoinState {
82 coin: sample_coin(),
83 spent_height: None,
84 created_height: Some(100),
85 };
86 let record = CoinRecord::from_coin_state(state);
87 assert!(!record.is_spent());
88 }
89}