Skip to main content

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}