@@ -27,29 +27,49 @@ enum ProcessReceiptError {
27
27
Both ( anyhow:: Error , anyhow:: Error ) ,
28
28
}
29
29
30
+ /// Indicates which versions of Receipts where processed
31
+ /// It's intended to be used for migration tests
32
+ #[ derive( Debug , PartialEq , Eq ) ]
33
+ pub enum ProcessedReceipt {
34
+ V1 ,
35
+ V2 ,
36
+ Both ,
37
+ None ,
38
+ }
39
+
30
40
impl InnerContext {
31
41
async fn process_db_receipts (
32
42
& self ,
33
43
buffer : Vec < DatabaseReceipt > ,
34
- ) -> Result < ( ) , ProcessReceiptError > {
44
+ ) -> Result < ProcessedReceipt , ProcessReceiptError > {
35
45
let ( v1_receipts, v2_receipts) : ( Vec < _ > , Vec < _ > ) =
36
46
buffer. into_iter ( ) . partition_map ( |r| match r {
37
47
DatabaseReceipt :: V1 ( db_receipt_v1) => Either :: Left ( db_receipt_v1) ,
38
48
DatabaseReceipt :: V2 ( db_receipt_v2) => Either :: Right ( db_receipt_v2) ,
39
49
} ) ;
50
+
40
51
let ( insert_v1, insert_v2) = tokio:: join!(
41
52
self . store_receipts_v1( v1_receipts) ,
42
- self . store_receipts_v2( v2_receipts)
53
+ self . store_receipts_v2( v2_receipts) ,
43
54
) ;
55
+
44
56
match ( insert_v1, insert_v2) {
45
57
( Err ( e1) , Err ( e2) ) => Err ( ProcessReceiptError :: Both ( e1. into ( ) , e2. into ( ) ) ) ,
46
- ( Err ( e1) , _) => Err ( ProcessReceiptError :: V1 ( e1. into ( ) ) ) ,
47
- ( _, Err ( e2) ) => Err ( ProcessReceiptError :: V2 ( e2. into ( ) ) ) ,
48
- _ => Ok ( ( ) ) ,
58
+
59
+ ( Err ( e1) , Ok ( _) ) => Err ( ProcessReceiptError :: V1 ( e1. into ( ) ) ) ,
60
+ ( Ok ( _) , Err ( e2) ) => Err ( ProcessReceiptError :: V2 ( e2. into ( ) ) ) ,
61
+
62
+ ( Ok ( 0 ) , Ok ( 0 ) ) => Ok ( ProcessedReceipt :: None ) ,
63
+ ( Ok ( _) , Ok ( 0 ) ) => Ok ( ProcessedReceipt :: V1 ) ,
64
+ ( Ok ( 0 ) , Ok ( _) ) => Ok ( ProcessedReceipt :: V2 ) ,
65
+ ( Ok ( _) , Ok ( _) ) => Ok ( ProcessedReceipt :: Both ) ,
49
66
}
50
67
}
51
68
52
- async fn store_receipts_v1 ( & self , receipts : Vec < DbReceiptV1 > ) -> Result < ( ) , AdapterError > {
69
+ async fn store_receipts_v1 ( & self , receipts : Vec < DbReceiptV1 > ) -> Result < u64 , AdapterError > {
70
+ if receipts. is_empty ( ) {
71
+ return Ok ( 0 ) ;
72
+ }
53
73
let receipts_len = receipts. len ( ) ;
54
74
let mut signers = Vec :: with_capacity ( receipts_len) ;
55
75
let mut signatures = Vec :: with_capacity ( receipts_len) ;
@@ -66,7 +86,7 @@ impl InnerContext {
66
86
nonces. push ( receipt. nonce ) ;
67
87
values. push ( receipt. value ) ;
68
88
}
69
- sqlx:: query!(
89
+ let query_res = sqlx:: query!(
70
90
r#"INSERT INTO scalar_tap_receipts (
71
91
signer_address,
72
92
signature,
@@ -96,10 +116,13 @@ impl InnerContext {
96
116
anyhow ! ( e)
97
117
} ) ?;
98
118
99
- Ok ( ( ) )
119
+ Ok ( query_res . rows_affected ( ) )
100
120
}
101
121
102
- async fn store_receipts_v2 ( & self , receipts : Vec < DbReceiptV2 > ) -> Result < ( ) , AdapterError > {
122
+ async fn store_receipts_v2 ( & self , receipts : Vec < DbReceiptV2 > ) -> Result < u64 , AdapterError > {
123
+ if receipts. is_empty ( ) {
124
+ return Ok ( 0 ) ;
125
+ }
103
126
let receipts_len = receipts. len ( ) ;
104
127
let mut signers = Vec :: with_capacity ( receipts_len) ;
105
128
let mut signatures = Vec :: with_capacity ( receipts_len) ;
@@ -122,7 +145,7 @@ impl InnerContext {
122
145
nonces. push ( receipt. nonce ) ;
123
146
values. push ( receipt. value ) ;
124
147
}
125
- sqlx:: query!(
148
+ let query_res = sqlx:: query!(
126
149
r#"INSERT INTO tap_horizon_receipts (
127
150
signer_address,
128
151
signature,
@@ -161,7 +184,7 @@ impl InnerContext {
161
184
anyhow ! ( e)
162
185
} ) ?;
163
186
164
- Ok ( ( ) )
187
+ Ok ( query_res . rows_affected ( ) )
165
188
}
166
189
}
167
190
@@ -305,3 +328,175 @@ impl DbReceiptV2 {
305
328
} )
306
329
}
307
330
}
331
+
332
+ #[ cfg( test) ]
333
+ mod tests {
334
+ use std:: { path:: PathBuf , sync:: LazyLock } ;
335
+
336
+ use futures:: future:: BoxFuture ;
337
+ use sqlx:: {
338
+ migrate:: { MigrationSource , Migrator } ,
339
+ PgPool ,
340
+ } ;
341
+ use test_assets:: {
342
+ create_signed_receipt, create_signed_receipt_v2, SignedReceiptRequest , INDEXER_ALLOCATIONS ,
343
+ TAP_EIP712_DOMAIN ,
344
+ } ;
345
+
346
+ use crate :: tap:: {
347
+ receipt_store:: {
348
+ DatabaseReceipt , DbReceiptV1 , DbReceiptV2 , InnerContext , ProcessReceiptError ,
349
+ ProcessedReceipt ,
350
+ } ,
351
+ AdapterError ,
352
+ } ;
353
+
354
+ async fn create_v1 ( ) -> DatabaseReceipt {
355
+ let alloc = INDEXER_ALLOCATIONS . values ( ) . next ( ) . unwrap ( ) . clone ( ) ;
356
+ let v1 = create_signed_receipt (
357
+ SignedReceiptRequest :: builder ( )
358
+ . allocation_id ( alloc. id )
359
+ . value ( 100 )
360
+ . build ( ) ,
361
+ )
362
+ . await ;
363
+ DatabaseReceipt :: V1 ( DbReceiptV1 :: from_receipt ( & v1, & TAP_EIP712_DOMAIN ) . unwrap ( ) )
364
+ }
365
+
366
+ async fn create_v2 ( ) -> DatabaseReceipt {
367
+ let v2 = create_signed_receipt_v2 ( ) . call ( ) . await ;
368
+ DatabaseReceipt :: V2 ( DbReceiptV2 :: from_receipt ( & v2, & TAP_EIP712_DOMAIN ) . unwrap ( ) )
369
+ }
370
+
371
+ mod when_all_migrations_are_run {
372
+ use super :: * ;
373
+
374
+ #[ rstest:: rstest]
375
+ #[ case( ProcessedReceipt :: None , async { vec![ ] } ) ]
376
+ #[ case( ProcessedReceipt :: V1 , async { vec![ create_v1( ) . await ] } ) ]
377
+ #[ case( ProcessedReceipt :: V2 , async { vec![ create_v2( ) . await ] } ) ]
378
+ #[ case( ProcessedReceipt :: Both , async { vec![ create_v2( ) . await , create_v1( ) . await ] } ) ]
379
+ #[ sqlx:: test( migrations = "../../migrations" ) ]
380
+ async fn v1_and_v2_are_processed_successfully (
381
+ #[ ignore] pgpool : PgPool ,
382
+ #[ case] expected : ProcessedReceipt ,
383
+ #[ future( awt) ]
384
+ #[ case]
385
+ receipts : Vec < DatabaseReceipt > ,
386
+ ) {
387
+ let context = InnerContext { pgpool } ;
388
+
389
+ let res = context. process_db_receipts ( receipts) . await . unwrap ( ) ;
390
+
391
+ assert_eq ! ( res, expected) ;
392
+ }
393
+ }
394
+
395
+ mod when_horizon_migrations_are_ignored {
396
+ use super :: * ;
397
+
398
+ #[ sqlx:: test( migrator = "WITHOUT_HORIZON_MIGRATIONS" ) ]
399
+ async fn test_empty_receipts_are_processed_successfully ( pgpool : PgPool ) {
400
+ let context = InnerContext { pgpool } ;
401
+
402
+ let res = context. process_db_receipts ( vec ! [ ] ) . await . unwrap ( ) ;
403
+
404
+ assert_eq ! ( res, ProcessedReceipt :: None ) ;
405
+ }
406
+
407
+ #[ sqlx:: test( migrator = "WITHOUT_HORIZON_MIGRATIONS" ) ]
408
+ async fn test_v1_receipts_are_processed_successfully ( pgpool : PgPool ) {
409
+ let context = InnerContext { pgpool } ;
410
+
411
+ let v1 = create_v1 ( ) . await ;
412
+ let receipts = vec ! [ v1] ;
413
+
414
+ let res = context. process_db_receipts ( receipts) . await . unwrap ( ) ;
415
+
416
+ assert_eq ! ( res, ProcessedReceipt :: V1 ) ;
417
+ }
418
+
419
+ #[ rstest:: rstest]
420
+ #[ case( async { vec![ create_v2( ) . await ] } ) ]
421
+ #[ case( async { vec![ create_v2( ) . await , create_v1( ) . await ] } ) ]
422
+ #[ sqlx:: test( migrator = "WITHOUT_HORIZON_MIGRATIONS" ) ]
423
+ async fn test_cases_with_v2_receipts_fails_to_process (
424
+ #[ ignore] pgpool : PgPool ,
425
+ #[ future( awt) ]
426
+ #[ case]
427
+ receipts : Vec < DatabaseReceipt > ,
428
+ ) {
429
+ let context = InnerContext { pgpool } ;
430
+
431
+ let error = context. process_db_receipts ( receipts) . await . unwrap_err ( ) ;
432
+
433
+ let ProcessReceiptError :: V2 ( error) = error else {
434
+ panic ! ( )
435
+ } ;
436
+ let d = error. downcast_ref :: < AdapterError > ( ) . unwrap ( ) . to_string ( ) ;
437
+
438
+ assert_eq ! (
439
+ d,
440
+ "error returned from database: relation \" tap_horizon_receipts\" does not exist"
441
+ ) ;
442
+ }
443
+
444
+ pub static WITHOUT_HORIZON_MIGRATIONS : LazyLock < Migrator > = LazyLock :: new ( create_migrator) ;
445
+
446
+ pub fn create_migrator ( ) -> Migrator {
447
+ futures:: executor:: block_on ( Migrator :: new ( MigrationRunner :: new (
448
+ "../../migrations" ,
449
+ [ "horizon" ] ,
450
+ ) ) )
451
+ . unwrap ( )
452
+ }
453
+
454
+ #[ derive( Debug ) ]
455
+ pub struct MigrationRunner {
456
+ migration_path : PathBuf ,
457
+ ignored_migrations : Vec < String > ,
458
+ }
459
+
460
+ impl MigrationRunner {
461
+ /// Construct a new MigrationRunner that does not apply the given migrations.
462
+ ///
463
+ /// `ignored_migrations` is any iterable of strings that describes which
464
+ /// migrations to be ignored.
465
+ pub fn new < I > ( path : impl Into < PathBuf > , ignored_migrations : I ) -> Self
466
+ where
467
+ I : IntoIterator ,
468
+ I :: Item : Into < String > ,
469
+ {
470
+ Self {
471
+ migration_path : path. into ( ) ,
472
+ ignored_migrations : ignored_migrations. into_iter ( ) . map ( Into :: into) . collect ( ) ,
473
+ }
474
+ }
475
+ }
476
+
477
+ impl MigrationSource < ' static > for MigrationRunner {
478
+ fn resolve (
479
+ self ,
480
+ ) -> BoxFuture < ' static , Result < Vec < sqlx:: migrate:: Migration > , sqlx:: error:: BoxDynError > >
481
+ {
482
+ Box :: pin ( async move {
483
+ let canonical = self . migration_path . canonicalize ( ) ?;
484
+ let migrations_with_paths =
485
+ sqlx:: migrate:: resolve_blocking ( & canonical) . unwrap ( ) ;
486
+
487
+ let migrations_with_paths = migrations_with_paths
488
+ . into_iter ( )
489
+ . filter ( |( _, p) | {
490
+ let path = p. to_str ( ) . unwrap ( ) ;
491
+ self . ignored_migrations
492
+ . iter ( )
493
+ . any ( |ignored| !path. contains ( ignored) )
494
+ } )
495
+ . collect :: < Vec < _ > > ( ) ;
496
+
497
+ Ok ( migrations_with_paths. into_iter ( ) . map ( |( m, _p) | m) . collect ( ) )
498
+ } )
499
+ }
500
+ }
501
+ }
502
+ }
0 commit comments