Skip to main content

bsv_wallet_toolbox/services/chaintracker/
chaintracks_chain_tracker.rs

1//! ChaintracksChainTracker implementing bsv-sdk ChainTracker trait.
2//!
3//! Ported from wallet-toolbox/src/services/chaintracker/ChaintracksChainTracker.ts.
4//! Caches merkle roots by block height to avoid redundant API calls.
5//!
6//! Supports two backends:
7//! - Remote: delegates to a `ChaintracksServiceClient` over HTTP.
8//! - Local: delegates to an in-process `Chaintracks` instance.
9
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use bsv::transaction::chain_tracker::ChainTracker;
15use bsv::transaction::error::TransactionError;
16use tokio::sync::Mutex;
17
18use crate::chaintracks::{Chaintracks, ChaintracksClient as LocalChaintracksClient};
19use crate::error::{WalletError, WalletResult};
20use crate::services::types::BlockHeader;
21
22use super::chaintracks_service_client::ChaintracksServiceClient;
23
24// ---------------------------------------------------------------------------
25// Backend enum
26// ---------------------------------------------------------------------------
27
28enum ChaintracksBackend {
29    Remote(ChaintracksServiceClient),
30    Local(Arc<Chaintracks>),
31}
32
33// ---------------------------------------------------------------------------
34// BlockHeader conversion
35// ---------------------------------------------------------------------------
36
37/// Convert the chaintracks-layer BlockHeader into the services-layer BlockHeader.
38///
39/// Both structs have identical fields; this is a field-by-field copy.
40impl From<crate::chaintracks::BlockHeader> for BlockHeader {
41    fn from(h: crate::chaintracks::BlockHeader) -> Self {
42        BlockHeader {
43            version: h.version,
44            previous_hash: h.previous_hash,
45            merkle_root: h.merkle_root,
46            time: h.time,
47            bits: h.bits,
48            nonce: h.nonce,
49            height: h.height,
50            hash: h.hash,
51        }
52    }
53}
54
55// ---------------------------------------------------------------------------
56// ChaintracksChainTracker
57// ---------------------------------------------------------------------------
58
59/// Chain tracker backed by the Chaintracks service (remote) or a local instance.
60///
61/// Implements the bsv-sdk `ChainTracker` trait for merkle root validation.
62/// Caches previously looked-up merkle roots by block height to avoid
63/// redundant network or storage calls.
64pub struct ChaintracksChainTracker {
65    backend: ChaintracksBackend,
66    root_cache: Mutex<HashMap<u32, String>>,
67}
68
69impl ChaintracksChainTracker {
70    /// Create a new ChaintracksChainTracker wrapping the given remote service client.
71    ///
72    /// This preserves the existing constructor signature; callers are unaffected.
73    pub fn new(service_client: ChaintracksServiceClient) -> Self {
74        Self {
75            backend: ChaintracksBackend::Remote(service_client),
76            root_cache: Mutex::new(HashMap::new()),
77        }
78    }
79
80    /// Create a ChaintracksChainTracker backed by a local Chaintracks instance.
81    pub fn with_local(chaintracks: Arc<Chaintracks>) -> Self {
82        Self {
83            backend: ChaintracksBackend::Local(chaintracks),
84            root_cache: Mutex::new(HashMap::new()),
85        }
86    }
87
88    /// Delegate to the backend: get a block header by hash.
89    pub async fn hash_to_header(&self, hash: &str) -> WalletResult<BlockHeader> {
90        match &self.backend {
91            ChaintracksBackend::Remote(client) => client
92                .get_header_for_block_hash(hash)
93                .await?
94                .ok_or_else(|| {
95                    WalletError::Internal(format!("No header found for block hash {}", hash))
96                }),
97            ChaintracksBackend::Local(ct) => ct
98                .find_header_for_block_hash(hash)
99                .await?
100                .map(BlockHeader::from)
101                .ok_or_else(|| {
102                    WalletError::Internal(format!("No header found for block hash {}", hash))
103                }),
104        }
105    }
106
107    /// Delegate to the backend: get a block header by height.
108    pub async fn get_header_for_height(&self, height: u32) -> WalletResult<BlockHeader> {
109        match &self.backend {
110            ChaintracksBackend::Remote(client) => client
111                .get_header_for_height(height)
112                .await?
113                .ok_or_else(|| {
114                    WalletError::Internal(format!("No header found for height {}", height))
115                }),
116            ChaintracksBackend::Local(ct) => ct
117                .find_header_for_height(height)
118                .await?
119                .map(BlockHeader::from)
120                .ok_or_else(|| {
121                    WalletError::Internal(format!("No header found for height {}", height))
122                }),
123        }
124    }
125
126    /// Fetch the header at `height` from whichever backend is configured.
127    ///
128    /// Returns `None` if the header is not found; propagates other errors.
129    async fn fetch_header_for_height(
130        &self,
131        height: u32,
132    ) -> Result<Option<BlockHeader>, TransactionError> {
133        match &self.backend {
134            ChaintracksBackend::Remote(client) => client
135                .get_header_for_height(height)
136                .await
137                .map_err(|e| {
138                    TransactionError::InvalidFormat(format!("ChainTracker error: {}", e))
139                }),
140            ChaintracksBackend::Local(ct) => ct
141                .find_header_for_height(height)
142                .await
143                .map(|opt| opt.map(BlockHeader::from))
144                .map_err(|e| {
145                    TransactionError::InvalidFormat(format!("ChainTracker error: {}", e))
146                }),
147        }
148    }
149
150    /// Insert a value directly into the merkle root cache.
151    ///
152    /// Primarily for testing purposes.
153    pub async fn insert_cache(&self, height: u32, merkle_root: String) {
154        let mut cache = self.root_cache.lock().await;
155        cache.insert(height, merkle_root);
156    }
157}
158
159// ---------------------------------------------------------------------------
160// ChainTracker trait impl
161// ---------------------------------------------------------------------------
162
163#[async_trait]
164impl ChainTracker for ChaintracksChainTracker {
165    async fn is_valid_root_for_height(
166        &self,
167        root: &str,
168        height: u32,
169    ) -> Result<bool, TransactionError> {
170        // Check cache first
171        {
172            let cache = self.root_cache.lock().await;
173            if let Some(cached_root) = cache.get(&height) {
174                return Ok(cached_root == root);
175            }
176        }
177
178        // Cache miss -- fetch from backend
179        let header = self.fetch_header_for_height(height).await?;
180
181        match header {
182            None => Ok(false),
183            Some(h) => {
184                // Store in cache
185                let mut cache = self.root_cache.lock().await;
186                cache.insert(height, h.merkle_root.clone());
187                Ok(h.merkle_root == root)
188            }
189        }
190    }
191
192    async fn current_height(&self) -> Result<u32, TransactionError> {
193        match &self.backend {
194            ChaintracksBackend::Remote(client) => client
195                .get_present_height()
196                .await
197                .map_err(|e| {
198                    TransactionError::InvalidFormat(format!("ChainTracker error: {}", e))
199                }),
200            ChaintracksBackend::Local(ct) => ct
201                .current_height()
202                .await
203                .map_err(|e| {
204                    TransactionError::InvalidFormat(format!("ChainTracker error: {}", e))
205                }),
206        }
207    }
208}