@@ -34,7 +34,7 @@ use tokio_retry::{strategy::ExponentialBackoff, Retry};
34
34
use url:: Url ;
35
35
use uuid:: Uuid ;
36
36
37
- use crate :: stores:: redis_ethereum_ledger:: * ;
37
+ use crate :: stores:: { redis_ethereum_ledger:: * , LeftoversStore } ;
38
38
use crate :: { ApiResponse , CreateAccount , SettlementEngine , SettlementEngineApi } ;
39
39
use interledger_settlement:: { Convert , ConvertDetails , Quantity } ;
40
40
@@ -100,7 +100,12 @@ pub struct EthereumLedgerSettlementEngineBuilder<'a, S, Si, A> {
100
100
101
101
impl < ' a , S , Si , A > EthereumLedgerSettlementEngineBuilder < ' a , S , Si , A >
102
102
where
103
- S : EthereumStore < Account = A > + Clone + Send + Sync + ' static ,
103
+ S : EthereumStore < Account = A >
104
+ + LeftoversStore < AssetType = BigUint >
105
+ + Clone
106
+ + Send
107
+ + Sync
108
+ + ' static ,
104
109
Si : EthereumLedgerTxSigner + Clone + Send + Sync + ' static ,
105
110
A : EthereumAccount + Send + Sync + ' static ,
106
111
{
@@ -222,7 +227,12 @@ where
222
227
223
228
impl < S , Si , A > EthereumLedgerSettlementEngine < S , Si , A >
224
229
where
225
- S : EthereumStore < Account = A > + Clone + Send + Sync + ' static ,
230
+ S : EthereumStore < Account = A >
231
+ + LeftoversStore < AssetType = BigUint >
232
+ + Clone
233
+ + Send
234
+ + Sync
235
+ + ' static ,
226
236
Si : EthereumLedgerTxSigner + Clone + Send + Sync + ' static ,
227
237
A : EthereumAccount + Send + Sync + ' static ,
228
238
{
@@ -283,6 +293,9 @@ where
283
293
let our_address = self . address . own_address ;
284
294
let token_address = self . address . token_address ;
285
295
296
+ // We `Box` futures in these functions due to
297
+ // https://github.com/rust-lang/rust/issues/54540#issuecomment-494749912.
298
+ // Otherwise, we get `type_length_limit` errors.
286
299
// get the current block number
287
300
web3. eth ( )
288
301
. block_number ( )
@@ -358,7 +371,7 @@ where
358
371
& self ,
359
372
transfer : ERC20Transfer ,
360
373
token_address : Address ,
361
- ) -> impl Future < Item = ( ) , Error = ( ) > {
374
+ ) -> Box < dyn Future < Item = ( ) , Error = ( ) > + Send > {
362
375
let store = self . store . clone ( ) ;
363
376
let tx_hash = transfer. tx_hash ;
364
377
let self_clone = self . clone ( ) ;
@@ -367,7 +380,7 @@ where
367
380
token_address : Some ( token_address) ,
368
381
} ;
369
382
let amount = transfer. amount ;
370
- store
383
+ Box :: new ( store
371
384
. check_if_tx_processed ( tx_hash)
372
385
. map_err ( move |_| error ! ( "Error when querying store about transaction: {:?}" , tx_hash) )
373
386
. and_then ( move |processed| {
@@ -377,7 +390,7 @@ where
377
390
. load_account_id_from_address ( addr)
378
391
. and_then ( move |id| {
379
392
debug ! ( "Notifying connector about incoming ERC20 transaction for account {} for amount: {} (tx hash: {})" , id, amount, tx_hash) ;
380
- self_clone. notify_connector ( id. to_string ( ) , amount, tx_hash)
393
+ self_clone. notify_connector ( id. to_string ( ) , amount. to_string ( ) , tx_hash)
381
394
} )
382
395
. and_then ( move |_| {
383
396
// only save the transaction hash if the connector
@@ -388,7 +401,7 @@ where
388
401
} else {
389
402
Either :: B ( ok ( ( ) ) ) // return an empty future otherwise since we want to skip this transaction
390
403
}
391
- } )
404
+ } ) )
392
405
}
393
406
394
407
fn notify_eth_txs_in_block ( & self , block_number : u64 ) -> impl Future < Item = ( ) , Error = ( ) > {
@@ -422,18 +435,17 @@ where
422
435
. and_then ( |_| Ok ( ( ) ) )
423
436
}
424
437
425
- fn notify_eth_transfer ( & self , tx_hash : H256 ) -> impl Future < Item = ( ) , Error = ( ) > {
438
+ fn notify_eth_transfer ( & self , tx_hash : H256 ) -> Box < dyn Future < Item = ( ) , Error = ( ) > + Send > {
426
439
let our_address = self . address . own_address ;
427
440
let web3 = self . web3 . clone ( ) ;
428
441
let store = self . store . clone ( ) ;
429
442
let self_clone = self . clone ( ) ;
430
443
// Skip transactions which have already been processed by the connector
431
- store. check_if_tx_processed ( tx_hash)
444
+ Box :: new ( store. check_if_tx_processed ( tx_hash)
432
445
. map_err ( move |_| error ! ( "Error when querying store about transaction: {:?}" , tx_hash) )
433
446
. and_then ( move |processed| {
434
447
if !processed {
435
- Either :: A (
436
- web3. eth ( ) . transaction ( TransactionId :: Hash ( tx_hash) )
448
+ Either :: A ( web3. eth ( ) . transaction ( TransactionId :: Hash ( tx_hash) )
437
449
. map_err ( move |err| error ! ( "Could not fetch transaction data from transaction hash: {:?}. Got error: {:?}" , tx_hash, err) )
438
450
. and_then ( move |maybe_tx| {
439
451
// Unlikely to error out since we only call this on
@@ -451,10 +463,10 @@ where
451
463
own_address : from,
452
464
token_address : None ,
453
465
} ;
454
- return Either :: A ( store. load_account_id_from_address ( addr)
466
+
467
+ return Either :: A ( store. load_account_id_from_address ( addr)
455
468
. and_then ( move |id| {
456
- debug ! ( "Notifying connector about incoming ETH transaction for account {} for amount: {} (tx hash: {})" , id, amount, tx_hash) ;
457
- self_clone. notify_connector ( id. to_string ( ) , amount, tx_hash)
469
+ self_clone. notify_connector ( id. to_string ( ) , amount. to_string ( ) , tx_hash)
458
470
} )
459
471
. and_then ( move |_| {
460
472
// only save the transaction hash if the connector
@@ -470,54 +482,161 @@ where
470
482
} else {
471
483
Either :: B ( ok ( ( ) ) ) // return an empty future otherwise since we want to skip this transaction
472
484
}
473
- } )
485
+ } ) )
474
486
}
475
487
476
488
fn notify_connector (
477
489
& self ,
478
490
account_id : String ,
479
- amount : U256 ,
491
+ amount : String ,
480
492
tx_hash : H256 ,
481
493
) -> impl Future < Item = ( ) , Error = ( ) > {
482
- let mut url = self . connector_url . clone ( ) ;
483
- let account_id_clone = account_id. clone ( ) ;
484
494
let engine_scale = self . asset_scale ;
495
+ let mut url = self . connector_url . clone ( ) ;
485
496
url. path_segments_mut ( )
486
497
. expect ( "Invalid connector URL" )
487
498
. push ( "accounts" )
488
499
. push ( & account_id. clone ( ) )
489
500
. push ( "settlements" ) ;
490
- let client = Client :: new ( ) ;
491
501
debug ! ( "Making POST to {:?} {:?} about {:?}" , url, amount, tx_hash) ;
492
- let action = move || {
493
- let account_id = account_id. clone ( ) ;
494
- client
495
- . post ( url. clone ( ) )
496
- . header ( "Idempotency-Key" , tx_hash. to_string ( ) )
497
- . json ( & json ! ( { "amount" : amount. to_string( ) , "scale" : engine_scale } ) )
498
- . send ( )
499
- . map_err ( move |err| {
500
- error ! (
501
- "Error notifying Accounting System's account: {:?}, amount: {:?}: {:?}" ,
502
- account_id, amount, err
503
- )
502
+
503
+ // settle for amount + uncredited_settlement_amount
504
+ let account_id_clone = account_id. clone ( ) ;
505
+ let full_amount_fut = self
506
+ . store
507
+ . load_uncredited_settlement_amount ( account_id. clone ( ) )
508
+ . and_then ( move |uncredited_settlement_amount| {
509
+ let full_amount_fut2 = result ( BigUint :: from_str ( & amount) . map_err ( move |err| {
510
+ let error_msg = format ! ( "Error converting to BigUint {:?}" , err) ;
511
+ error ! ( "{:?}" , error_msg) ;
512
+ } ) )
513
+ . and_then ( move |amount| {
514
+ debug ! ( "Got uncredited amount {}" , amount) ;
515
+ let full_amount = amount + uncredited_settlement_amount;
516
+ debug ! (
517
+ "Notifying accounting system about full amount: {}" ,
518
+ full_amount
519
+ ) ;
520
+ ok ( full_amount)
521
+ } ) ;
522
+ ok ( full_amount_fut2)
523
+ } )
524
+ . flatten ( ) ;
525
+
526
+ let self_clone = self . clone ( ) ;
527
+ let ping_connector_fut = full_amount_fut. and_then ( move |full_amount| {
528
+ let url = url. clone ( ) ;
529
+ let account_id = account_id_clone. clone ( ) ;
530
+ let account_id2 = account_id_clone. clone ( ) ;
531
+ let full_amount2 = full_amount. clone ( ) ;
532
+
533
+ let action = move || {
534
+ let client = Client :: new ( ) ;
535
+ let account_id = account_id. clone ( ) ;
536
+ let full_amount = full_amount. clone ( ) ;
537
+ let full_amount_clone = full_amount. clone ( ) ;
538
+ client
539
+ . post ( url. clone ( ) )
540
+ . header ( "Idempotency-Key" , tx_hash. to_string ( ) )
541
+ . json ( & json ! ( Quantity :: new( full_amount. clone( ) , engine_scale) ) )
542
+ . send ( )
543
+ . map_err ( move |err| {
544
+ error ! (
545
+ "Error notifying Accounting System's account: {:?}, amount: {:?}: {:?}" ,
546
+ account_id, full_amount_clone, err
547
+ ) ;
548
+ } )
549
+ . and_then ( move |ret| {
550
+ ok ( ( ret, full_amount) )
551
+ } )
552
+ } ;
553
+ Retry :: spawn (
554
+ ExponentialBackoff :: from_millis ( 10 ) . take ( MAX_RETRIES ) ,
555
+ action,
556
+ )
557
+ . map_err ( move |_| {
558
+ error ! ( "Exceeded max retries when notifying connector about account {:?} for amount {:?} and transaction hash {:?}. Please check your API." , account_id2, full_amount2, tx_hash)
559
+ } )
560
+ } ) ;
561
+
562
+ ping_connector_fut. and_then ( move |ret| {
563
+ trace ! ( "Accounting system responded with {:?}" , ret. 0 ) ;
564
+ self_clone. process_connector_response ( account_id, ret. 0 , ret. 1 )
565
+ } )
566
+ }
567
+
568
+ /// Parses a response from a connector into a Quantity type and calls a
569
+ /// function to further process the parsed data to check if the store's
570
+ /// uncredited settlement amount should be updated.
571
+ fn process_connector_response (
572
+ & self ,
573
+ account_id : String ,
574
+ response : HttpResponse ,
575
+ engine_amount : BigUint ,
576
+ ) -> Box < dyn Future < Item = ( ) , Error = ( ) > + Send > {
577
+ let self_clone = self . clone ( ) ;
578
+ if !response. status ( ) . is_success ( ) {
579
+ return Box :: new ( err ( ( ) ) ) ;
580
+ }
581
+ Box :: new (
582
+ response
583
+ . into_body ( )
584
+ . concat2 ( )
585
+ . map_err ( |err| {
586
+ let err = format ! ( "Couldn't retrieve body {:?}" , err) ;
587
+ error ! ( "{}" , err) ;
504
588
} )
505
- . and_then ( move |response | {
506
- trace ! ( "Accounting system responded with {:?}" , response ) ;
507
- if response . status ( ) . is_success ( ) {
508
- Ok ( ( ) )
509
- } else {
510
- Err ( ( ) )
511
- }
589
+ . and_then ( move |body | {
590
+ // Get the amount stored by the connector and
591
+ // check for any precision loss / overflow
592
+ serde_json :: from_slice :: < Quantity > ( & body ) . map_err ( |err| {
593
+ let err = format ! ( "Couldn't parse body {:?} into Quantity {:?}" , body , err ) ;
594
+ error ! ( "{}" , err ) ;
595
+ } )
512
596
} )
513
- } ;
514
- Retry :: spawn (
515
- ExponentialBackoff :: from_millis ( 10 ) . take ( MAX_RETRIES ) ,
516
- action,
597
+ . and_then ( move |quantity : Quantity | {
598
+ self_clone. process_received_quantity ( account_id, quantity, engine_amount)
599
+ } ) ,
600
+ )
601
+ }
602
+
603
+ // Normalizes a received Quantity object against the local engine scale, and
604
+ // if the normalized value is less than what the engine originally sent, it
605
+ // stores it as uncredited settlement amount in the store.
606
+ fn process_received_quantity (
607
+ & self ,
608
+ account_id : String ,
609
+ quantity : Quantity ,
610
+ engine_amount : BigUint ,
611
+ ) -> Box < dyn Future < Item = ( ) , Error = ( ) > + Send > {
612
+ let store = self . store . clone ( ) ;
613
+ let engine_scale = self . asset_scale ;
614
+ Box :: new (
615
+ result ( BigUint :: from_str ( & quantity. amount ) )
616
+ . map_err ( |err| {
617
+ let error_msg = format ! ( "Error converting to BigUint {:?}" , err) ;
618
+ error ! ( "{:?}" , error_msg) ;
619
+ } )
620
+ . and_then ( move |connector_amount : BigUint | {
621
+ // Scale the amount settled by the
622
+ // connector back up to our scale
623
+ result ( connector_amount. normalize_scale ( ConvertDetails {
624
+ from : quantity. scale ,
625
+ to : engine_scale,
626
+ } ) )
627
+ . and_then ( move |scaled_connector_amount| {
628
+ if engine_amount > scaled_connector_amount {
629
+ let diff = engine_amount - scaled_connector_amount;
630
+ // connector settled less than we
631
+ // instructed it to, so we must save
632
+ // the difference
633
+ store. save_uncredited_settlement_amount ( account_id, diff)
634
+ } else {
635
+ Box :: new ( ok ( ( ) ) )
636
+ }
637
+ } )
638
+ } ) ,
517
639
)
518
- . map_err ( move |_| {
519
- error ! ( "Exceeded max retries when notifying connector about account {:?} for amount {:?} and transaction hash {:?}. Please check your API." , account_id_clone, amount, tx_hash)
520
- } )
521
640
}
522
641
523
642
/// Helper function which submits an Ethereum ledger transaction to `to` for `amount`.
@@ -624,7 +743,12 @@ where
624
743
625
744
impl < S , Si , A > SettlementEngine for EthereumLedgerSettlementEngine < S , Si , A >
626
745
where
627
- S : EthereumStore < Account = A > + Clone + Send + Sync + ' static ,
746
+ S : EthereumStore < Account = A >
747
+ + LeftoversStore < AssetType = BigUint >
748
+ + Clone
749
+ + Send
750
+ + Sync
751
+ + ' static ,
628
752
Si : EthereumLedgerTxSigner + Clone + Send + Sync + ' static ,
629
753
A : EthereumAccount + Send + Sync + ' static ,
630
754
{
@@ -980,7 +1104,7 @@ mod tests {
980
1104
"{\" amount\" : \" 100000000000\" , \" scale\" : 18 }" . to_string ( ) ,
981
1105
) )
982
1106
. with_status ( 200 )
983
- . with_body ( "OK " . to_string ( ) )
1107
+ . with_body ( "{ \" amount \" : \" 100000000000 \" , \" scale \" : 9 } " . to_string ( ) )
984
1108
. create ( ) ;
985
1109
986
1110
let bob_connector_url = mockito:: server_url ( ) ;
0 commit comments