Skip to main content

chia_query/provider_registry/
providers.rs

1//! The four provider-kind wrappers the registry composes: [`CoinsetProvider`] (public oracle),
2//! [`LocalNodeProvider`] (the operator's node), [`DigPeersProvider`] (the DIG peer network), and
3//! [`CustomProvider`] (an operator override).
4//!
5//! Each wraps ANY [`ChainSource`] and labels it with the right [`ProviderKind`], so the same
6//! wrapper composes a live [`ChiaQueryProvider`](crate::provider_registry::ChiaQueryProvider) in
7//! production and a `MockChainSource` in tests. The wrappers add identity + kind only; they do NOT
8//! grant trust โ€” the registry's operator-assigned [`TrustLevel`](super::TrustLevel) does that.
9
10use std::borrow::Cow;
11
12use chia_protocol::{Bytes32, CoinSpend};
13use dig_chainsource_interface::{
14    ChainSource, ChainSourceProvider, CoinRecord, ProviderId, ProviderInfo, ProviderKind,
15    SingletonLineage,
16};
17
18/// Defines a provider-kind wrapper over an arbitrary [`ChainSource`], delegating every read to the
19/// inner source and reporting a fixed [`ProviderKind`] via [`ChainSourceProvider`].
20macro_rules! kinded_provider {
21    ($(#[$meta:meta])* $name:ident, $kind:expr) => {
22        $(#[$meta])*
23        pub struct $name<S> {
24            inner: S,
25            info: ProviderInfo,
26        }
27
28        impl<S> $name<S> {
29            /// Wraps `source` with a stable `id`, a try-order `priority` (lower = tried first), and
30            /// this wrapper's fixed [`ProviderKind`].
31            pub fn new(id: impl Into<Cow<'static, str>>, priority: i32, source: S) -> Self {
32                Self {
33                    inner: source,
34                    info: ProviderInfo {
35                        id: ProviderId(id.into()),
36                        kind: $kind,
37                        priority,
38                        // A wrapper never self-declares trustlessness; the registry assigns trust.
39                        trustless: false,
40                    },
41                }
42            }
43        }
44
45        impl<S> ChainSource for $name<S>
46        where
47            S: ChainSource,
48        {
49            type Error = S::Error;
50
51            fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
52                self.inner.coin_record(coin_id)
53            }
54
55            fn coin_records_by_puzzle_hash(
56                &self,
57                puzzle_hash: Bytes32,
58                include_spent: bool,
59            ) -> Result<Vec<CoinRecord>, Self::Error> {
60                self.inner.coin_records_by_puzzle_hash(puzzle_hash, include_spent)
61            }
62
63            fn coin_records_by_parent(
64                &self,
65                parent_coin_id: Bytes32,
66            ) -> Result<Vec<CoinRecord>, Self::Error> {
67                self.inner.coin_records_by_parent(parent_coin_id)
68            }
69
70            fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
71                self.inner.coin_spend(coin_id)
72            }
73
74            fn parent_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
75                self.inner.parent_spend(coin_id)
76            }
77
78            fn resolve_singleton_lineage(
79                &self,
80                launcher_id: Bytes32,
81            ) -> Result<Option<SingletonLineage>, Self::Error> {
82                self.inner.resolve_singleton_lineage(launcher_id)
83            }
84
85            fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
86                self.inner.peak_height()
87            }
88
89            fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
90                self.inner.block_timestamp(height)
91            }
92        }
93
94        impl<S> ChainSourceProvider for $name<S>
95        where
96            S: ChainSource,
97        {
98            fn provider_info(&self) -> ProviderInfo {
99                self.info.clone()
100            }
101        }
102    };
103}
104
105kinded_provider!(
106    /// A public oracle/gateway (e.g. coinset.org): convenient, but `Untrusted` for custody by
107    /// default โ€” only ever a quorum member unless the operator explicitly vouches for it.
108    CoinsetProvider,
109    ProviderKind::PublicOracle
110);
111
112kinded_provider!(
113    /// The operator's own full/wallet node โ€” the most trustworthy source, `Trusted` for custody by
114    /// default. Compose it over the ยง5.3 `dig.local` -> `localhost` node ladder.
115    LocalNodeProvider,
116    ProviderKind::LocalNode
117);
118
119kinded_provider!(
120    /// Chain data served over the DIG peer network: `Untrusted` for custody by default.
121    DigPeersProvider,
122    ProviderKind::DigPeers
123);
124
125kinded_provider!(
126    /// An operator-supplied override not covered by the other kinds: `Untrusted` for custody by
127    /// default (the operator may raise it to `Trusted` at registration).
128    CustomProvider,
129    ProviderKind::Custom
130);
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use chia_protocol::Coin;
136    use dig_chainsource_interface::{
137        ChainSourceError, CoinRecord as IfaceCoinRecord, MockChainSource, SingletonLineage,
138    };
139
140    #[test]
141    fn wrapper_delegates_every_read_to_the_inner_source() {
142        let id = Coin::new(Bytes32::new([0x01; 32]), Bytes32::new([0x22; 32]), 1).coin_id();
143        let record = IfaceCoinRecord {
144            coin: Coin::new(id, Bytes32::new([0x22; 32]), 1),
145            confirmed_height: Some(5),
146            spent_height: None,
147            timestamp: Some(9),
148            coinbase: false,
149        };
150        let launcher = Bytes32::new([0x33; 32]);
151        let mock = MockChainSource::new()
152            .with_coin(id, record.clone())
153            .with_lineage(launcher, SingletonLineage::single(launcher))
154            .with_timestamp(5, 1_000)
155            .with_peak(42);
156
157        let provider = LocalNodeProvider::new("local", 0, mock);
158
159        assert_eq!(provider.coin_record(id).unwrap(), Some(record.clone()));
160        assert_eq!(
161            provider
162                .coin_records_by_puzzle_hash(Bytes32::new([0x22; 32]), true)
163                .unwrap(),
164            vec![record.clone()]
165        );
166        // `record`'s coin has parent == id, so by-parent finds it.
167        assert_eq!(
168            provider.coin_records_by_parent(id).unwrap(),
169            vec![record.clone()]
170        );
171        assert_eq!(
172            provider
173                .coin_records_by_parent(Bytes32::new([0xEE; 32]))
174                .unwrap(),
175            vec![]
176        );
177        assert_eq!(provider.coin_spend(id).unwrap(), None);
178        assert_eq!(provider.parent_spend(id).unwrap(), None);
179        assert_eq!(
180            provider.resolve_singleton_lineage(launcher).unwrap(),
181            Some(SingletonLineage::single(launcher))
182        );
183        assert_eq!(provider.peak_height().unwrap(), Some(42));
184        assert_eq!(provider.block_timestamp(5).unwrap(), Some(1_000));
185    }
186
187    #[test]
188    fn wrapper_propagates_inner_errors() {
189        let provider = CoinsetProvider::new(
190            "coinset",
191            0,
192            MockChainSource::new().fail_with(ChainSourceError::Timeout),
193        );
194        assert_eq!(
195            provider.coin_record(Bytes32::new([0x01; 32])),
196            Err(ChainSourceError::Timeout)
197        );
198    }
199
200    #[test]
201    fn wrappers_report_their_kind_and_identity() {
202        let coinset = CoinsetProvider::new("coinset.org", 10, MockChainSource::new());
203        assert_eq!(coinset.provider_info().kind, ProviderKind::PublicOracle);
204        assert_eq!(coinset.provider_info().priority, 10);
205        assert!(!coinset.provider_info().trustless);
206
207        let local = LocalNodeProvider::new("local", 0, MockChainSource::new());
208        assert_eq!(local.provider_info().kind, ProviderKind::LocalNode);
209
210        let peers = DigPeersProvider::new("dig-peers", 20, MockChainSource::new());
211        assert_eq!(peers.provider_info().kind, ProviderKind::DigPeers);
212
213        let custom = CustomProvider::new("custom", 30, MockChainSource::new());
214        assert_eq!(custom.provider_info().kind, ProviderKind::Custom);
215    }
216}