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