1
- use std:: collections:: HashSet ;
1
+ use std:: collections:: { HashMap , HashSet } ;
2
2
3
3
use bitcoin:: { Address , Network , ScriptBuf } ;
4
4
use chainhook_sdk:: utils:: Context ;
@@ -52,10 +52,12 @@ pub async fn augment_block_with_transfers(
52
52
ctx : & Context ,
53
53
) -> Result < ( ) , String > {
54
54
let network = get_bitcoin_network ( & block. metadata . network ) ;
55
+ let mut block_transferred_satpoints = HashMap :: new ( ) ;
55
56
for ( tx_index, tx) in block. transactions . iter_mut ( ) . enumerate ( ) {
56
- let _ = augment_transaction_with_ordinal_transfers (
57
+ augment_transaction_with_ordinal_transfers (
57
58
tx,
58
59
tx_index,
60
+ & mut block_transferred_satpoints,
59
61
& block. block_identifier ,
60
62
& network,
61
63
db_tx,
@@ -146,13 +148,12 @@ pub fn compute_satpoint_post_transfer(
146
148
pub async fn augment_transaction_with_ordinal_transfers (
147
149
tx : & mut BitcoinTransactionData ,
148
150
tx_index : usize ,
151
+ block_transferred_satpoints : & mut HashMap < String , Vec < WatchedSatpoint > > ,
149
152
block_identifier : & BlockIdentifier ,
150
153
network : & Network ,
151
154
db_tx : & Transaction < ' _ > ,
152
155
ctx : & Context ,
153
- ) -> Result < Vec < OrdinalInscriptionTransferData > , String > {
154
- let mut transfers = vec ! [ ] ;
155
-
156
+ ) -> Result < ( ) , String > {
156
157
// The transfers are inserted in storage after the inscriptions.
157
158
// We have a unicity constraing, and can only have 1 ordinals per satpoint.
158
159
let mut updated_sats = HashSet :: new ( ) ;
@@ -162,11 +163,33 @@ pub async fn augment_transaction_with_ordinal_transfers(
162
163
}
163
164
}
164
165
165
- // For each satpoint inscribed retrieved, we need to compute the next outpoint to watch
166
- let input_entries =
167
- ordinals_pg:: get_inscribed_satpoints_at_tx_inputs ( & tx. metadata . inputs , db_tx) . await ?;
166
+ // Load all sats that will be transferred with this transaction i.e. loop through all tx inputs and look for previous
167
+ // satpoints we need to move.
168
+ //
169
+ // Since the DB state is currently at the end of the previous block, and there may be multiple transfers for the same sat in
170
+ // this new block, we'll use a memory cache to keep all sats that have been transferred but have not yet been written into the
171
+ // DB.
172
+ let mut cached_satpoints = HashMap :: new ( ) ;
173
+ let mut inputs_for_db_lookup = vec ! [ ] ;
174
+ for ( vin, input) in tx. metadata . inputs . iter ( ) . enumerate ( ) {
175
+ let output_key = format_outpoint_to_watch (
176
+ & input. previous_output . txid ,
177
+ input. previous_output . vout as usize ,
178
+ ) ;
179
+ // Look in memory cache, or save for a batched DB lookup later.
180
+ if let Some ( watched_satpoints) = block_transferred_satpoints. remove ( & output_key) {
181
+ cached_satpoints. insert ( vin, watched_satpoints) ;
182
+ } else {
183
+ inputs_for_db_lookup. push ( ( vin, output_key) ) ;
184
+ }
185
+ }
186
+ let mut input_satpoints =
187
+ ordinals_pg:: get_inscribed_satpoints_at_tx_inputs ( & inputs_for_db_lookup, db_tx) . await ?;
188
+ input_satpoints. extend ( cached_satpoints) ;
189
+
190
+ // Process all transfers across all inputs.
168
191
for ( input_index, input) in tx. metadata . inputs . iter ( ) . enumerate ( ) {
169
- let Some ( entries) = input_entries . get ( & input_index) else {
192
+ let Some ( entries) = input_satpoints . get ( & input_index) else {
170
193
continue ;
171
194
} ;
172
195
for watched_satpoint in entries. into_iter ( ) {
@@ -199,6 +222,12 @@ pub async fn augment_transaction_with_ordinal_transfers(
199
222
satpoint_post_transfer : satpoint_post_transfer. clone ( ) ,
200
223
post_transfer_output_value,
201
224
} ;
225
+ // Keep an in-memory copy of this watchpoint at its new tx output for later retrieval.
226
+ let ( output, _) = parse_output_and_offset_from_satpoint ( & satpoint_post_transfer) ?;
227
+ let entry = block_transferred_satpoints
228
+ . entry ( output)
229
+ . or_insert ( vec ! [ ] ) ;
230
+ entry. push ( watched_satpoint. clone ( ) ) ;
202
231
203
232
try_info ! (
204
233
ctx,
@@ -208,26 +237,188 @@ pub async fn augment_transaction_with_ordinal_transfers(
208
237
satpoint_post_transfer,
209
238
block_identifier. index
210
239
) ;
211
- transfers. push ( transfer_data. clone ( ) ) ;
212
240
tx. metadata
213
241
. ordinal_operations
214
242
. push ( OrdinalOperation :: InscriptionTransferred ( transfer_data) ) ;
215
243
}
216
244
}
217
245
218
- Ok ( transfers )
246
+ Ok ( ( ) )
219
247
}
220
248
221
249
#[ cfg( test) ]
222
250
mod test {
223
251
use bitcoin:: Network ;
252
+ use chainhook_postgres:: { pg_begin, pg_pool_client} ;
224
253
use chainhook_sdk:: utils:: Context ;
225
- use chainhook_types:: OrdinalInscriptionTransferDestination ;
254
+ use chainhook_types:: {
255
+ OrdinalInscriptionNumber , OrdinalInscriptionRevealData , OrdinalInscriptionTransferData ,
256
+ OrdinalInscriptionTransferDestination , OrdinalOperation ,
257
+ } ;
226
258
227
- use crate :: core:: test_builders:: { TestTransactionBuilder , TestTxInBuilder , TestTxOutBuilder } ;
259
+ use crate :: {
260
+ core:: {
261
+ protocol:: satoshi_tracking:: augment_block_with_transfers,
262
+ test_builders:: {
263
+ TestBlockBuilder , TestTransactionBuilder , TestTxInBuilder , TestTxOutBuilder ,
264
+ } ,
265
+ } ,
266
+ db:: { ordinals_pg, pg_reset_db, pg_test_connection, pg_test_connection_pool} ,
267
+ } ;
228
268
229
269
use super :: compute_satpoint_post_transfer;
230
270
271
+ #[ tokio:: test]
272
+ async fn tracks_chained_satoshi_transfers_in_block ( ) -> Result < ( ) , String > {
273
+ let ordinal_number: u64 = 283888212016616 ;
274
+ let inscription_id =
275
+ "cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8adi1245" . to_string ( ) ;
276
+ let block_height_1: u64 = 874387 ;
277
+ let block_height_2: u64 = 875364 ;
278
+
279
+ let ctx = Context :: empty ( ) ;
280
+ let mut pg_client = pg_test_connection ( ) . await ;
281
+ ordinals_pg:: migrate ( & mut pg_client) . await ?;
282
+ let result = {
283
+ let mut ord_client = pg_pool_client ( & pg_test_connection_pool ( ) ) . await ?;
284
+ let client = pg_begin ( & mut ord_client) . await ?;
285
+
286
+ // 1. Insert inscription in a previous block first
287
+ let block = TestBlockBuilder :: new ( )
288
+ . height ( block_height_1)
289
+ . hash ( "0x000000000000000000021668d82e096a1aad3934b5a6f8f707ad29ade2505580" . into ( ) )
290
+ . add_transaction (
291
+ TestTransactionBuilder :: new ( )
292
+ . hash (
293
+ "0xcbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad"
294
+ . into ( ) ,
295
+ )
296
+ . add_ordinal_operation (
297
+ OrdinalOperation :: InscriptionRevealed (
298
+ OrdinalInscriptionRevealData {
299
+ content_bytes : "0x" . into ( ) ,
300
+ content_type : "" . into ( ) ,
301
+ content_length : 0 ,
302
+ inscription_number : OrdinalInscriptionNumber { classic : 79754112 , jubilee : 79754112 } ,
303
+ inscription_fee : 1161069 ,
304
+ inscription_output_value : 546 ,
305
+ inscription_id,
306
+ inscription_input_index : 0 ,
307
+ inscription_pointer : Some ( 0 ) ,
308
+ inscriber_address : Some ( "bc1p3qus9j7ucg0c4s2pf7k70nlpkk7r3ddt4u2ek54wn6nuwkzm9twqfenmjm" . into ( ) ) ,
309
+ delegate : None ,
310
+ metaprotocol : None ,
311
+ metadata : None ,
312
+ parents : vec ! [ ] ,
313
+ ordinal_number,
314
+ ordinal_block_height : 56777 ,
315
+ ordinal_offset : 0 ,
316
+ tx_index : 0 ,
317
+ transfers_pre_inscription : 0 ,
318
+ satpoint_post_inscription : "cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad:0:0" . into ( ) ,
319
+ curse_type : None ,
320
+ charms : 0 ,
321
+ unbound_sequence : None ,
322
+ } ,
323
+ ) ,
324
+ )
325
+ . build ( ) ,
326
+ )
327
+ . build ( ) ;
328
+ ordinals_pg:: insert_block ( & block, & client) . await ?;
329
+
330
+ // 2. Simulate a new block which transfers that same inscription back and forth across 2 transactions
331
+ let mut block = TestBlockBuilder :: new ( )
332
+ . height ( block_height_2)
333
+ . hash ( "0x00000000000000000001efc5fba69f0ebd5645a18258ec3cf109ca3636327242" . into ( ) )
334
+ . add_transaction ( TestTransactionBuilder :: new ( ) . build ( ) )
335
+ . add_transaction (
336
+ TestTransactionBuilder :: new ( )
337
+ . hash (
338
+ "0x30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd"
339
+ . into ( ) ,
340
+ )
341
+ . add_input (
342
+ TestTxInBuilder :: new ( )
343
+ . prev_out_block_height ( block_height_1)
344
+ . prev_out_tx_hash ( "0xcbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad" . into ( ) )
345
+ . value ( 546 )
346
+ . build ( )
347
+ )
348
+ . add_output (
349
+ TestTxOutBuilder :: new ( )
350
+ . value ( 546 )
351
+ . script_pubkey ( "0x51200944f1eef1a8f34ef4d0b58286a51115878abddbec2a3d3d8c581b71ff1c4bbc" . into ( ) )
352
+ . build ( )
353
+ )
354
+ . build ( ) ,
355
+ )
356
+ . add_transaction (
357
+ TestTransactionBuilder :: new ( )
358
+ . hash (
359
+ "0x0029b328fee7ab916ba98c194f21a084a4a781170610644de518dd0733c0d5d2"
360
+ . into ( ) ,
361
+ )
362
+ . add_input (
363
+ TestTxInBuilder :: new ( )
364
+ . prev_out_block_height ( block_height_2)
365
+ . prev_out_tx_hash ( "0x30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd" . into ( ) )
366
+ . value ( 546 )
367
+ . build ( )
368
+ )
369
+ . add_output (
370
+ TestTxOutBuilder :: new ( )
371
+ . value ( 546 )
372
+ . script_pubkey ( "0x5120883902cbdcc21f8ac1414fade7cfe1b5bc38b5abaf159b52ae9ea7c7585b2adc" . into ( ) )
373
+ . build ( )
374
+ )
375
+ . build ( )
376
+ )
377
+ . build ( ) ;
378
+ augment_block_with_transfers ( & mut block, & client, & ctx) . await ?;
379
+
380
+ // 3. Make sure the correct transfers were produced
381
+ assert_eq ! (
382
+ & block. transactions[ 1 ] . metadata. ordinal_operations[ 0 ] ,
383
+ & OrdinalOperation :: InscriptionTransferred ( OrdinalInscriptionTransferData {
384
+ ordinal_number,
385
+ destination: OrdinalInscriptionTransferDestination :: Transferred (
386
+ "bc1pp9z0rmh34re5aaxskkpgdfg3zkrc40wmas4r60vvtqdhrlcufw7qmgufuz" . into( )
387
+ ) ,
388
+ satpoint_pre_transfer:
389
+ "cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad:0:0"
390
+ . into( ) ,
391
+ satpoint_post_transfer:
392
+ "30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd:0:0"
393
+ . into( ) ,
394
+ post_transfer_output_value: Some ( 546 ) ,
395
+ tx_index: 1 ,
396
+ } )
397
+ ) ;
398
+ assert_eq ! (
399
+ & block. transactions[ 2 ] . metadata. ordinal_operations[ 0 ] ,
400
+ & OrdinalOperation :: InscriptionTransferred ( OrdinalInscriptionTransferData {
401
+ ordinal_number,
402
+ destination: OrdinalInscriptionTransferDestination :: Transferred (
403
+ "bc1p3qus9j7ucg0c4s2pf7k70nlpkk7r3ddt4u2ek54wn6nuwkzm9twqfenmjm" . into( )
404
+ ) ,
405
+ satpoint_pre_transfer:
406
+ "30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd:0:0"
407
+ . into( ) ,
408
+ satpoint_post_transfer:
409
+ "0029b328fee7ab916ba98c194f21a084a4a781170610644de518dd0733c0d5d2:0:0"
410
+ . into( ) ,
411
+ post_transfer_output_value: Some ( 546 ) ,
412
+ tx_index: 2 ,
413
+ } )
414
+ ) ;
415
+
416
+ Ok ( ( ) )
417
+ } ;
418
+ pg_reset_db ( & mut pg_client) . await ?;
419
+ result
420
+ }
421
+
231
422
#[ test]
232
423
fn computes_satpoint_spent_as_fee ( ) {
233
424
let ctx = Context :: empty ( ) ;
0 commit comments