@@ -168,6 +168,23 @@ impl TransactionBuilder {
168
168
}
169
169
}
170
170
171
+ /// calculates how much the fee would increase if you added a given output
172
+ pub fn fee_for_output ( & mut self , output : & TransactionOutput ) -> Result < Coin , JsValue > {
173
+ let mut self_copy = self . clone ( ) ;
174
+
175
+ // we need some value for these for it to be a a valid transaction
176
+ // but since we're only calculating the different between the fee of two transactions
177
+ // it doesn't matter what these are set as, since it cancels out
178
+ self_copy. set_ttl ( 0 ) ;
179
+ self_copy. set_fee ( & Coin :: new ( 0 ) ) ;
180
+
181
+ let fee_before = min_fee ( & self_copy) ?;
182
+
183
+ self_copy. add_output ( & output) ?;
184
+ let fee_after = min_fee ( & self_copy) ?;
185
+ fee_after. checked_sub ( & fee_before)
186
+ }
187
+
171
188
pub fn set_fee ( & mut self , fee : & Coin ) {
172
189
self . fee = Some ( fee. clone ( ) )
173
190
}
@@ -272,32 +289,31 @@ impl TransactionBuilder {
272
289
Some ( _x) => return Err ( JsValue :: from_str ( "Cannot calculate change if fee was explicitly specified" ) ) ,
273
290
} ?;
274
291
let input_total = self . get_explicit_input ( ) ?. checked_add ( & self . get_implicit_input ( ) ?) ?;
275
- let output_total = self . get_explicit_output ( ) ?;
276
- let deposit = self . get_deposit ( ) ?;
277
- match input_total. unwrap ( ) > output_total. checked_add ( & fee) ?. unwrap ( ) {
292
+ let output_total = self . get_explicit_output ( ) ?. checked_add ( & self . get_deposit ( ) ?) ?;
293
+ match input_total. unwrap ( ) >= output_total. checked_add ( & fee) ?. unwrap ( ) {
278
294
false => return Err ( JsValue :: from_str ( "Insufficient input in transaction" ) ) ,
279
295
true => {
280
- let mut copy = self . clone ( ) ;
281
- copy . add_output ( & TransactionOutput {
296
+ // check how much the fee would increase if we added a change output
297
+ let fee_for_change = self . fee_for_output ( & TransactionOutput {
282
298
address : address. clone ( ) ,
283
299
// maximum possible output to maximize fee from adding this output
284
300
// this may over-estimate the fee by a few bytes but that's okay
285
301
amount : Coin :: new ( 0x1_00_00_00_00 ) ,
286
302
} ) ?;
287
- let new_fee = copy . min_fee ( ) ?;
303
+ let new_fee = fee . checked_add ( & fee_for_change ) ?;
288
304
// needs to have at least minimum_utxo_val leftover for the change to be a valid UTXO entry
289
- match input_total > output_total. checked_add ( & deposit ) ? . checked_add ( & new_fee) ?. checked_add ( & self . minimum_utxo_val ) ? {
305
+ match input_total >= output_total. checked_add ( & new_fee) ?. checked_add ( & self . minimum_utxo_val ) ? {
290
306
false => {
291
307
// recall: we originally assumed the fee was the maximum possible so we definitely have enough input to cover whatever fee it ends up being
292
- self . set_fee ( & input_total. checked_sub ( & output_total) ?. checked_sub ( & deposit ) ? ) ;
308
+ self . set_fee ( & input_total. checked_sub ( & output_total) ?) ;
293
309
return Ok ( false ) // not enough input to covert the extra fee from adding an output so we just burn whatever is left
294
310
} ,
295
311
true => {
296
312
// recall: we originally assumed the fee was the maximum possible so we definitely have enough input to cover whatever fee it ends up being
297
313
self . set_fee ( & new_fee) ;
298
314
self . add_output ( & TransactionOutput {
299
315
address : address. clone ( ) ,
300
- amount : input_total. checked_sub ( & output_total) ?. checked_sub ( & new_fee) ?. checked_sub ( & deposit ) ? ,
316
+ amount : input_total. checked_sub ( & output_total) ?. checked_sub ( & new_fee) ?,
301
317
} ) ?;
302
318
} ,
303
319
} ;
@@ -487,7 +503,6 @@ mod tests {
487
503
. derive ( 0 )
488
504
. to_public ( ) ;
489
505
490
- let spend_cred = StakeCredential :: from_keyhash ( & spend. to_raw_key ( ) . hash ( ) ) ;
491
506
let stake_cred = StakeCredential :: from_keyhash ( & stake. to_raw_key ( ) . hash ( ) ) ;
492
507
tx_builder. add_key_input (
493
508
& spend. to_raw_key ( ) . hash ( ) ,
@@ -522,4 +537,159 @@ mod tests {
522
537
) ;
523
538
let _final_tx = tx_builder. build ( ) ; // just test that it doesn't throw
524
539
}
540
+
541
+ #[ test]
542
+ fn build_tx_exact_amount ( ) {
543
+ // transactions where sum(input) == sum(output) exact should pass
544
+ let linear_fee = LinearFee :: new ( & Coin :: new ( 0 ) , & Coin :: new ( 0 ) ) ;
545
+ let mut tx_builder = TransactionBuilder :: new ( & linear_fee, & Coin :: new ( 1 ) , & Coin :: new ( 0 ) , & Coin :: new ( 0 ) ) ;
546
+ let spend = root_key_15 ( )
547
+ . derive ( harden ( 1852 ) )
548
+ . derive ( harden ( 1815 ) )
549
+ . derive ( harden ( 0 ) )
550
+ . derive ( 0 )
551
+ . derive ( 0 )
552
+ . to_public ( ) ;
553
+ let change_key = root_key_15 ( )
554
+ . derive ( harden ( 1852 ) )
555
+ . derive ( harden ( 1815 ) )
556
+ . derive ( harden ( 0 ) )
557
+ . derive ( 1 )
558
+ . derive ( 0 )
559
+ . to_public ( ) ;
560
+ let stake = root_key_15 ( )
561
+ . derive ( harden ( 1852 ) )
562
+ . derive ( harden ( 1815 ) )
563
+ . derive ( harden ( 0 ) )
564
+ . derive ( 2 )
565
+ . derive ( 0 )
566
+ . to_public ( ) ;
567
+ tx_builder. add_key_input (
568
+ & & spend. to_raw_key ( ) . hash ( ) ,
569
+ & TransactionInput :: new ( & genesis_id ( ) , 0 ) ,
570
+ & Coin :: new ( 5 )
571
+ ) ;
572
+ let spend_cred = StakeCredential :: from_keyhash ( & spend. to_raw_key ( ) . hash ( ) ) ;
573
+ let stake_cred = StakeCredential :: from_keyhash ( & stake. to_raw_key ( ) . hash ( ) ) ;
574
+ let addr_net_0 = BaseAddress :: new ( 0 , & spend_cred, & stake_cred) . to_address ( ) ;
575
+ tx_builder. add_output ( & TransactionOutput :: new (
576
+ & addr_net_0,
577
+ & Coin :: new ( 5 )
578
+ ) ) . unwrap ( ) ;
579
+ tx_builder. set_ttl ( 0 ) ;
580
+
581
+ let change_cred = StakeCredential :: from_keyhash ( & change_key. to_raw_key ( ) . hash ( ) ) ;
582
+ let change_addr = BaseAddress :: new ( 0 , & change_cred, & stake_cred) . to_address ( ) ;
583
+ let added_change = tx_builder. add_change_if_needed (
584
+ & change_addr
585
+ ) . unwrap ( ) ;
586
+ assert_eq ! ( added_change, false ) ;
587
+ let final_tx = tx_builder. build ( ) . unwrap ( ) ;
588
+ assert_eq ! ( final_tx. outputs( ) . len( ) , 1 ) ;
589
+ }
590
+
591
+ #[ test]
592
+ fn build_tx_exact_change ( ) {
593
+ // transactions where we have exactly enough ADA to add change should pass
594
+ let linear_fee = LinearFee :: new ( & Coin :: new ( 0 ) , & Coin :: new ( 0 ) ) ;
595
+ let mut tx_builder = TransactionBuilder :: new ( & linear_fee, & Coin :: new ( 1 ) , & Coin :: new ( 0 ) , & Coin :: new ( 0 ) ) ;
596
+ let spend = root_key_15 ( )
597
+ . derive ( harden ( 1852 ) )
598
+ . derive ( harden ( 1815 ) )
599
+ . derive ( harden ( 0 ) )
600
+ . derive ( 0 )
601
+ . derive ( 0 )
602
+ . to_public ( ) ;
603
+ let change_key = root_key_15 ( )
604
+ . derive ( harden ( 1852 ) )
605
+ . derive ( harden ( 1815 ) )
606
+ . derive ( harden ( 0 ) )
607
+ . derive ( 1 )
608
+ . derive ( 0 )
609
+ . to_public ( ) ;
610
+ let stake = root_key_15 ( )
611
+ . derive ( harden ( 1852 ) )
612
+ . derive ( harden ( 1815 ) )
613
+ . derive ( harden ( 0 ) )
614
+ . derive ( 2 )
615
+ . derive ( 0 )
616
+ . to_public ( ) ;
617
+ tx_builder. add_key_input (
618
+ & & spend. to_raw_key ( ) . hash ( ) ,
619
+ & TransactionInput :: new ( & genesis_id ( ) , 0 ) ,
620
+ & Coin :: new ( 6 )
621
+ ) ;
622
+ let spend_cred = StakeCredential :: from_keyhash ( & spend. to_raw_key ( ) . hash ( ) ) ;
623
+ let stake_cred = StakeCredential :: from_keyhash ( & stake. to_raw_key ( ) . hash ( ) ) ;
624
+ let addr_net_0 = BaseAddress :: new ( 0 , & spend_cred, & stake_cred) . to_address ( ) ;
625
+ tx_builder. add_output ( & TransactionOutput :: new (
626
+ & addr_net_0,
627
+ & Coin :: new ( 5 )
628
+ ) ) . unwrap ( ) ;
629
+ tx_builder. set_ttl ( 0 ) ;
630
+
631
+ let change_cred = StakeCredential :: from_keyhash ( & change_key. to_raw_key ( ) . hash ( ) ) ;
632
+ let change_addr = BaseAddress :: new ( 0 , & change_cred, & stake_cred) . to_address ( ) ;
633
+ let added_change = tx_builder. add_change_if_needed (
634
+ & change_addr
635
+ ) . unwrap ( ) ;
636
+ assert_eq ! ( added_change, true ) ;
637
+ let final_tx = tx_builder. build ( ) . unwrap ( ) ;
638
+ assert_eq ! ( final_tx. outputs( ) . len( ) , 2 ) ;
639
+ assert_eq ! ( final_tx. outputs( ) . get( 1 ) . amount( ) . to_str( ) , "1" ) ;
640
+ }
641
+
642
+ #[ test]
643
+ #[ should_panic]
644
+ fn build_tx_insufficient_deposit ( ) {
645
+ // transactions should fail with insufficient fees if a deposit is required
646
+ let linear_fee = LinearFee :: new ( & Coin :: new ( 0 ) , & Coin :: new ( 0 ) ) ;
647
+ let mut tx_builder = TransactionBuilder :: new ( & linear_fee, & Coin :: new ( 1 ) , & Coin :: new ( 0 ) , & Coin :: new ( 5 ) ) ;
648
+ let spend = root_key_15 ( )
649
+ . derive ( harden ( 1852 ) )
650
+ . derive ( harden ( 1815 ) )
651
+ . derive ( harden ( 0 ) )
652
+ . derive ( 0 )
653
+ . derive ( 0 )
654
+ . to_public ( ) ;
655
+ let change_key = root_key_15 ( )
656
+ . derive ( harden ( 1852 ) )
657
+ . derive ( harden ( 1815 ) )
658
+ . derive ( harden ( 0 ) )
659
+ . derive ( 1 )
660
+ . derive ( 0 )
661
+ . to_public ( ) ;
662
+ let stake = root_key_15 ( )
663
+ . derive ( harden ( 1852 ) )
664
+ . derive ( harden ( 1815 ) )
665
+ . derive ( harden ( 0 ) )
666
+ . derive ( 2 )
667
+ . derive ( 0 )
668
+ . to_public ( ) ;
669
+ tx_builder. add_key_input (
670
+ & & spend. to_raw_key ( ) . hash ( ) ,
671
+ & TransactionInput :: new ( & genesis_id ( ) , 0 ) ,
672
+ & Coin :: new ( 5 )
673
+ ) ;
674
+ let spend_cred = StakeCredential :: from_keyhash ( & spend. to_raw_key ( ) . hash ( ) ) ;
675
+ let stake_cred = StakeCredential :: from_keyhash ( & stake. to_raw_key ( ) . hash ( ) ) ;
676
+ let addr_net_0 = BaseAddress :: new ( 0 , & spend_cred, & stake_cred) . to_address ( ) ;
677
+ tx_builder. add_output ( & TransactionOutput :: new (
678
+ & addr_net_0,
679
+ & Coin :: new ( 5 )
680
+ ) ) . unwrap ( ) ;
681
+ tx_builder. set_ttl ( 0 ) ;
682
+
683
+ // add a cert which requires a deposit
684
+ let mut certs = Certificates :: new ( ) ;
685
+ certs. add ( & Certificate :: new_stake_registration ( & StakeRegistration :: new ( & stake_cred) ) ) ;
686
+ tx_builder. set_certs ( & certs) ;
687
+
688
+ let change_cred = StakeCredential :: from_keyhash ( & change_key. to_raw_key ( ) . hash ( ) ) ;
689
+ let change_addr = BaseAddress :: new ( 0 , & change_cred, & stake_cred) . to_address ( ) ;
690
+
691
+ tx_builder. add_change_if_needed (
692
+ & change_addr
693
+ ) . unwrap ( ) ;
694
+ }
525
695
}
0 commit comments