@@ -168,6 +168,23 @@ impl TransactionBuilder {
168168 }
169169 }
170170
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+
171188 pub fn set_fee ( & mut self , fee : & Coin ) {
172189 self . fee = Some ( fee. clone ( ) )
173190 }
@@ -272,32 +289,31 @@ impl TransactionBuilder {
272289 Some ( _x) => return Err ( JsValue :: from_str ( "Cannot calculate change if fee was explicitly specified" ) ) ,
273290 } ?;
274291 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 ( ) {
278294 false => return Err ( JsValue :: from_str ( "Insufficient input in transaction" ) ) ,
279295 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 {
282298 address : address. clone ( ) ,
283299 // maximum possible output to maximize fee from adding this output
284300 // this may over-estimate the fee by a few bytes but that's okay
285301 amount : Coin :: new ( 0x1_00_00_00_00 ) ,
286302 } ) ?;
287- let new_fee = copy . min_fee ( ) ?;
303+ let new_fee = fee . checked_add ( & fee_for_change ) ?;
288304 // 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 ) ? {
290306 false => {
291307 // 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) ?) ;
293309 return Ok ( false ) // not enough input to covert the extra fee from adding an output so we just burn whatever is left
294310 } ,
295311 true => {
296312 // recall: we originally assumed the fee was the maximum possible so we definitely have enough input to cover whatever fee it ends up being
297313 self . set_fee ( & new_fee) ;
298314 self . add_output ( & TransactionOutput {
299315 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) ?,
301317 } ) ?;
302318 } ,
303319 } ;
@@ -487,7 +503,6 @@ mod tests {
487503 . derive ( 0 )
488504 . to_public ( ) ;
489505
490- let spend_cred = StakeCredential :: from_keyhash ( & spend. to_raw_key ( ) . hash ( ) ) ;
491506 let stake_cred = StakeCredential :: from_keyhash ( & stake. to_raw_key ( ) . hash ( ) ) ;
492507 tx_builder. add_key_input (
493508 & spend. to_raw_key ( ) . hash ( ) ,
@@ -522,4 +537,159 @@ mod tests {
522537 ) ;
523538 let _final_tx = tx_builder. build ( ) ; // just test that it doesn't throw
524539 }
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+ }
525695}
0 commit comments