Skip to content

Commit 35e4ee2

Browse files
add fee_for_output to txbuilder (#60)
1 parent 0c063c3 commit 35e4ee2

File tree

2 files changed

+194
-10
lines changed

2 files changed

+194
-10
lines changed

rust/pkg/cardano_serialization_lib.js.flow

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,13 @@ declare export class ByronAddress {
574574
*/
575575
static from_bytes(bytes: Uint8Array): ByronAddress;
576576

577+
/**
578+
* returns the byron protocol magic embedded in the address, or mainnet id if none is present
579+
* note: for bech32 addresses, you need to use network_id instead
580+
* @returns {number}
581+
*/
582+
byron_protocol_magic(): number;
583+
577584
/**
578585
* @returns {number}
579586
*/
@@ -3041,6 +3048,13 @@ declare export class TransactionBuilder {
30413048
*/
30423049
add_output(output: TransactionOutput): void;
30433050

3051+
/**
3052+
* calculates how much the fee would increase if you added a given output
3053+
* @param {TransactionOutput} output
3054+
* @returns {BigNum}
3055+
*/
3056+
fee_for_output(output: TransactionOutput): BigNum;
3057+
30443058
/**
30453059
* @param {BigNum} fee
30463060
*/

rust/src/tx_builder.rs

Lines changed: 180 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)