1+ #![ allow( clippy:: print_stdout, clippy:: print_stderr) ]
2+ use std:: time:: Instant ;
3+
4+ use anyhow:: Context ;
5+ use bdk_chain:: bitcoin:: {
6+ bip158:: BlockFilter , secp256k1:: Secp256k1 , Block , BlockHash ,
7+ Network , ScriptBuf ,
8+ } ;
9+ use bdk_chain:: indexer:: keychain_txout:: KeychainTxOutIndex ;
10+ use bdk_chain:: miniscript:: Descriptor ;
11+ use bdk_chain:: {
12+ Anchor , BlockId , CanonicalizationParams , CanonicalizationTask , ChainOracle ,
13+ ConfirmationBlockTime , IndexedTxGraph , SpkIterator ,
14+ } ;
15+ use bdk_testenv:: anyhow;
16+ use bitcoincore_rpc:: json:: GetBlockHeaderResult ;
17+ use bitcoincore_rpc:: { Client , RpcApi } ;
18+
19+ // This example shows how to use a CoreOracle that implements ChainOracle trait
20+ // to handle canonicalization with bitcoind RPC, without needing LocalChain.
21+
22+ const EXTERNAL : & str = "tr([83737d5e/86'/1'/0']tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/0/*)" ;
23+ const INTERNAL : & str = "tr([83737d5e/86'/1'/0']tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/1/*)" ;
24+ const SPK_COUNT : u32 = 25 ;
25+ const NETWORK : Network = Network :: Signet ;
26+
27+ const START_HEIGHT : u32 = 205_000 ;
28+ const START_HASH : & str = "0000002bd0f82f8c0c0f1e19128f84c938763641dba85c44bdb6aed1678d16cb" ;
29+
30+ /// Error types for CoreOracle and FilterIterV2
31+ #[ derive( Debug ) ]
32+ pub enum Error {
33+ /// RPC error
34+ Rpc ( bitcoincore_rpc:: Error ) ,
35+ /// `bitcoin::bip158` error
36+ Bip158 ( bdk_chain:: bitcoin:: bip158:: Error ) ,
37+ /// Max reorg depth exceeded
38+ ReorgDepthExceeded ,
39+ /// Error converting an integer
40+ TryFromInt ( core:: num:: TryFromIntError ) ,
41+ }
42+
43+ impl core:: fmt:: Display for Error {
44+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
45+ match self {
46+ Self :: Rpc ( e) => write ! ( f, "{e}" ) ,
47+ Self :: Bip158 ( e) => write ! ( f, "{e}" ) ,
48+ Self :: ReorgDepthExceeded => write ! ( f, "maximum reorg depth exceeded" ) ,
49+ Self :: TryFromInt ( e) => write ! ( f, "{e}" ) ,
50+ }
51+ }
52+ }
53+
54+ impl std:: error:: Error for Error { }
55+
56+ impl From < bitcoincore_rpc:: Error > for Error {
57+ fn from ( e : bitcoincore_rpc:: Error ) -> Self {
58+ Self :: Rpc ( e)
59+ }
60+ }
61+
62+ impl From < core:: num:: TryFromIntError > for Error {
63+ fn from ( e : core:: num:: TryFromIntError ) -> Self {
64+ Self :: TryFromInt ( e)
65+ }
66+ }
67+
68+ impl From < bdk_chain:: bitcoin:: bip158:: Error > for Error {
69+ fn from ( e : bdk_chain:: bitcoin:: bip158:: Error ) -> Self {
70+ Self :: Bip158 ( e)
71+ }
72+ }
73+
74+ /// Whether the RPC error is a "not found" error (code: `-5`)
75+ fn is_not_found ( e : & bitcoincore_rpc:: Error ) -> bool {
76+ matches ! (
77+ e,
78+ bitcoincore_rpc:: Error :: JsonRpc ( bitcoincore_rpc:: jsonrpc:: Error :: Rpc ( e) )
79+ if e. code == -5
80+ )
81+ }
82+
83+ /// CoreOracle implements ChainOracle using bitcoind RPC
84+ pub struct CoreOracle {
85+ client : Client ,
86+ cached_tip : Option < BlockId > ,
87+ }
88+
89+ impl CoreOracle {
90+ pub fn new ( client : Client ) -> Self {
91+ Self {
92+ client,
93+ cached_tip : None ,
94+ }
95+ }
96+
97+ /// Refresh and return the chain tip
98+ pub fn refresh_tip ( & mut self ) -> Result < BlockId , Error > {
99+ let height = self . client . get_block_count ( ) ? as u32 ;
100+ let hash = self . client . get_block_hash ( height as u64 ) ?;
101+ let tip = BlockId { height, hash } ;
102+ self . cached_tip = Some ( tip) ;
103+ Ok ( tip)
104+ }
105+
106+ /// Canonicalize a transaction graph using this oracle
107+ pub fn canonicalize < A : Anchor > (
108+ & self ,
109+ mut task : CanonicalizationTask < ' _ , A > ,
110+ chain_tip : BlockId ,
111+ ) -> bdk_chain:: CanonicalView < A > {
112+ // Process all queries from the task
113+ while let Some ( request) = task. next_query ( ) {
114+ // Check each anchor against the chain
115+ let mut best_anchor = None ;
116+
117+ for anchor in & request. anchors {
118+ let block_id = anchor. anchor_block ( ) ;
119+
120+ // Check if block is in chain
121+ match self . is_block_in_chain ( block_id, chain_tip) {
122+ Ok ( Some ( true ) ) => {
123+ best_anchor = Some ( anchor. clone ( ) ) ;
124+ break ; // Found a confirmed anchor
125+ }
126+ _ => continue , // Not confirmed or error, check next
127+ }
128+ }
129+
130+ task. resolve_query ( best_anchor) ;
131+ }
132+
133+ // Finish and return the canonical view
134+ task. finish ( chain_tip)
135+ }
136+ }
137+
138+ impl ChainOracle for CoreOracle {
139+ type Error = Error ;
140+
141+ fn is_block_in_chain (
142+ & self ,
143+ block : BlockId ,
144+ chain_tip : BlockId ,
145+ ) -> Result < Option < bool > , Self :: Error > {
146+ // Check if the requested block height is within range
147+ if block. height > chain_tip. height {
148+ return Ok ( Some ( false ) ) ;
149+ }
150+
151+ // Get the block hash at the requested height
152+ match self . client . get_block_hash ( block. height as u64 ) {
153+ Ok ( hash_at_height) => Ok ( Some ( hash_at_height == block. hash ) ) ,
154+ Err ( e) if is_not_found ( & e) => Ok ( Some ( false ) ) ,
155+ Err ( _) => Ok ( None ) , // Can't determine, return None
156+ }
157+ }
158+
159+ fn get_chain_tip ( & self ) -> Result < BlockId , Self :: Error > {
160+ if let Some ( tip) = self . cached_tip {
161+ Ok ( tip)
162+ } else {
163+ let height = self . client . get_block_count ( ) ? as u32 ;
164+ let hash = self . client . get_block_hash ( height as u64 ) ?;
165+ Ok ( BlockId { height, hash } )
166+ }
167+ }
168+ }
169+
170+ /// FilterIterV2: Similar to FilterIter but doesn't manage CheckPoints
171+ pub struct FilterIterV2 < ' a > {
172+ client : & ' a Client ,
173+ spks : Vec < ScriptBuf > ,
174+ current_height : u32 ,
175+ current_hash : BlockHash ,
176+ header : Option < GetBlockHeaderResult > ,
177+ }
178+
179+ impl < ' a > FilterIterV2 < ' a > {
180+ pub fn new (
181+ client : & ' a Client ,
182+ start_height : u32 ,
183+ start_hash : BlockHash ,
184+ spks : impl IntoIterator < Item = ScriptBuf > ,
185+ ) -> Self {
186+ Self {
187+ client,
188+ spks : spks. into_iter ( ) . collect ( ) ,
189+ current_height : start_height,
190+ current_hash : start_hash,
191+ header : None ,
192+ }
193+ }
194+
195+ /// Find the starting point for iteration
196+ fn find_base ( & self ) -> Result < GetBlockHeaderResult , Error > {
197+ match self . client . get_block_header_info ( & self . current_hash ) {
198+ Ok ( header) if header. confirmations > 0 => Ok ( header) ,
199+ _ => {
200+ // If we can't find the starting hash, try to get the header at the height
201+ let hash = self . client . get_block_hash ( self . current_height as u64 ) ?;
202+ Ok ( self . client . get_block_header_info ( & hash) ?)
203+ }
204+ }
205+ }
206+ }
207+
208+ /// Event returned by FilterIterV2
209+ #[ derive( Debug , Clone ) ]
210+ pub struct EventV2 {
211+ pub height : u32 ,
212+ pub hash : BlockHash ,
213+ pub block : Option < Block > ,
214+ }
215+
216+ impl Iterator for FilterIterV2 < ' _ > {
217+ type Item = Result < EventV2 , Error > ;
218+
219+ fn next ( & mut self ) -> Option < Self :: Item > {
220+ let result = ( || -> Result < Option < EventV2 > , Error > {
221+ let header = match self . header . take ( ) {
222+ Some ( header) => header,
223+ None => self . find_base ( ) ?,
224+ } ;
225+
226+ let next_hash = match header. next_block_hash {
227+ Some ( hash) => hash,
228+ None => return Ok ( None ) , // Reached chain tip
229+ } ;
230+
231+ let mut next_header = self . client . get_block_header_info ( & next_hash) ?;
232+
233+ // Handle reorgs
234+ while next_header. confirmations < 0 {
235+ let prev_hash = next_header
236+ . previous_block_hash
237+ . ok_or ( Error :: ReorgDepthExceeded ) ?;
238+ next_header = self . client . get_block_header_info ( & prev_hash) ?;
239+ }
240+
241+ let height = next_header. height . try_into ( ) ?;
242+ let hash = next_header. hash ;
243+
244+ // Check if block matches our filters
245+ let mut block = None ;
246+ let filter = BlockFilter :: new (
247+ self . client
248+ . get_block_filter ( & hash) ?
249+ . filter
250+ . as_slice ( ) ,
251+ ) ;
252+
253+ if filter. match_any ( & hash, self . spks . iter ( ) . map ( ScriptBuf :: as_ref) ) ? {
254+ block = Some ( self . client . get_block ( & hash) ?) ;
255+ }
256+
257+ // Update state
258+ self . current_height = height;
259+ self . current_hash = hash;
260+ self . header = Some ( next_header) ;
261+
262+ Ok ( Some ( EventV2 {
263+ height,
264+ hash,
265+ block,
266+ } ) )
267+ } ) ( ) ;
268+
269+ result. transpose ( )
270+ }
271+ }
272+
273+ fn main ( ) -> anyhow:: Result < ( ) > {
274+ // Setup descriptors and graph
275+ let secp = Secp256k1 :: new ( ) ;
276+ let ( descriptor, _) = Descriptor :: parse_descriptor ( & secp, EXTERNAL ) ?;
277+ let ( change_descriptor, _) = Descriptor :: parse_descriptor ( & secp, INTERNAL ) ?;
278+
279+ let mut graph = IndexedTxGraph :: < ConfirmationBlockTime , KeychainTxOutIndex < & str > > :: new ( {
280+ let mut index = KeychainTxOutIndex :: default ( ) ;
281+ index. insert_descriptor ( "external" , descriptor. clone ( ) ) ?;
282+ index. insert_descriptor ( "internal" , change_descriptor. clone ( ) ) ?;
283+ index
284+ } ) ;
285+
286+ // Configure RPC client
287+ let url = std:: env:: var ( "RPC_URL" ) . context ( "must set RPC_URL" ) ?;
288+ let cookie = std:: env:: var ( "RPC_COOKIE" ) . context ( "must set RPC_COOKIE" ) ?;
289+ let client = Client :: new ( & url, bitcoincore_rpc:: Auth :: CookieFile ( cookie. into ( ) ) ) ?;
290+
291+ // Generate SPKs to watch
292+ let mut spks = vec ! [ ] ;
293+ for ( _, desc) in graph. index . keychains ( ) {
294+ spks. extend ( SpkIterator :: new_with_range ( desc, 0 ..SPK_COUNT ) . map ( |( _, s) | s) ) ;
295+ }
296+
297+ // Create FilterIterV2
298+ let iter = FilterIterV2 :: new (
299+ & client,
300+ START_HEIGHT ,
301+ START_HASH . parse ( ) ?,
302+ spks,
303+ ) ;
304+
305+ let start = Instant :: now ( ) ;
306+ let mut last_height = START_HEIGHT ;
307+
308+ // Scan blocks
309+ println ! ( "Scanning blocks..." ) ;
310+ for res in iter {
311+ let event = res?;
312+ last_height = event. height ;
313+
314+ if let Some ( block) = event. block {
315+ let _ = graph. apply_block_relevant ( & block, event. height ) ;
316+ println ! ( "Matched block at height {}" , event. height) ;
317+ }
318+ }
319+
320+ println ! ( "\n Scan took: {}s" , start. elapsed( ) . as_secs( ) ) ;
321+ println ! ( "Scanned up to height: {}" , last_height) ;
322+
323+ // Create CoreOracle
324+ let mut oracle = CoreOracle :: new ( client) ;
325+
326+ // Get current chain tip from oracle
327+ let chain_tip = oracle. refresh_tip ( ) ?;
328+ println ! ( "Chain tip: height={}, hash={}" , chain_tip. height, chain_tip. hash) ;
329+
330+ // Perform canonicalization using CoreOracle
331+ println ! ( "\n Performing canonicalization using CoreOracle..." ) ;
332+ let task = graph. canonicalization_task ( CanonicalizationParams :: default ( ) ) ;
333+ let canonical_view = oracle. canonicalize ( task, chain_tip) ;
334+
335+ println ! ( "Canonical view created:" ) ;
336+ // println!(" Chain tip: {:?}", canonical_view.chain_tip());
337+
338+ // Calculate balance
339+ let balance = canonical_view. balance (
340+ graph. index . outpoints ( ) . iter ( ) . cloned ( ) ,
341+ |( k, _) , _| k == & "external" || k == & "internal" ,
342+ 0 ,
343+ ) ;
344+
345+ println ! ( "\n Balance:" ) ;
346+ println ! ( " Confirmed: {} sats" , balance. confirmed) ;
347+ // println!(" Unconfirmed: {} sats", balance.unconfirmed);
348+ println ! ( " Total: {} sats" , balance. total( ) ) ;
349+
350+ // Display unspent outputs
351+ let unspent: Vec < _ > = canonical_view
352+ . filter_unspent_outpoints ( graph. index . outpoints ( ) . clone ( ) )
353+ . collect ( ) ;
354+
355+ if !unspent. is_empty ( ) {
356+ println ! ( "\n Unspent outputs:" ) ;
357+ for ( index, utxo) in unspent {
358+ println ! ( " {:?} | {} sats | {}" , index, utxo. txout. value, utxo. outpoint) ;
359+ }
360+ }
361+
362+ // Display canonical transactions
363+ let canonical_txs: Vec < _ > = canonical_view. txs ( ) . collect ( ) ;
364+ println ! ( "\n Canonical transactions: {}" , canonical_txs. len( ) ) ;
365+
366+ for tx in & canonical_txs {
367+ match & tx. pos {
368+ bdk_chain:: ChainPosition :: Confirmed { anchor, .. } => {
369+ let block_id = anchor. anchor_block ( ) ;
370+ println ! ( " {} - Confirmed at height {}" , tx. txid, block_id. height) ;
371+ }
372+ bdk_chain:: ChainPosition :: Unconfirmed { last_seen, .. } => {
373+ println ! ( " {} - Unconfirmed (last seen: {:?})" , tx. txid, last_seen) ;
374+ }
375+ }
376+ }
377+
378+ Ok ( ( ) )
379+ }
0 commit comments