From 763bc5a86a03cbc5c758742e569c59717261ef0a Mon Sep 17 00:00:00 2001 From: lisicky Date: Mon, 11 Nov 2024 05:19:54 +0800 Subject: [PATCH 1/9] changed set_fee, added min_fee --- rust/src/builders/tx_builder.rs | 72 +++++++++++-- rust/src/tests/builders/tx_builder.rs | 102 ++++++++++++++++-- rust/src/tests/builders/voting_builder.rs | 12 +-- .../tests/builders/voting_proposal_builder.rs | 14 +-- 4 files changed, 170 insertions(+), 30 deletions(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index f885c195..7b781906 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -348,6 +348,13 @@ impl ChangeConfig { } } +#[derive(Clone, Debug)] +pub(crate) enum TxBuilderFee { + Unspecified, + NotLess(Coin), + Exactly(Coin), +} + #[wasm_bindgen] #[derive(Clone, Debug)] pub struct TransactionBuilder { @@ -355,7 +362,8 @@ pub struct TransactionBuilder { pub(crate) inputs: TxInputsBuilder, pub(crate) collateral: TxInputsBuilder, pub(crate) outputs: TransactionOutputs, - pub(crate) fee: Option, + pub(crate) fee_request: TxBuilderFee, + pub(crate) fee: Option, pub(crate) ttl: Option, // absolute slot number pub(crate) certs: Option, pub(crate) withdrawals: Option, @@ -910,7 +918,7 @@ impl TransactionBuilder { self.add_inputs_from(inputs, strategy)?; if self.fee.is_some() { return Err(JsError::from_str( - "Cannot calculate change if fee was explicitly specified", + "Cannot calculate change if it was calculated before", )) } let mut add_change_result = self @@ -1054,7 +1062,7 @@ impl TransactionBuilder { // we need some value for these for it to be a a valid transaction // but since we're only calculating the difference between the fee of two transactions // it doesn't matter what these are set as, since it cancels out - self_copy.set_fee(&BigNum::zero()); + self_copy.set_final_fee(BigNum::zero()); let fee_before = min_fee(&self_copy)?; @@ -1092,7 +1100,7 @@ impl TransactionBuilder { // we need some value for these for it to be a a valid transaction // but since we're only calculating the different between the fee of two transactions // it doesn't matter what these are set as, since it cancels out - self_copy.set_fee(&BigNum::zero()); + self_copy.set_final_fee(BigNum::zero()); let fee_before = min_fee(&self_copy)?; @@ -1102,7 +1110,22 @@ impl TransactionBuilder { } pub fn set_fee(&mut self, fee: &Coin) { - self.fee = Some(fee.clone()) + self.fee_request = TxBuilderFee::Exactly(fee.clone()); + } + + pub fn set_min_fee(&mut self, fee: &Coin) { + self.fee_request = TxBuilderFee::NotLess(fee.clone()); + } + + fn set_final_fee(&mut self, fee: Coin) { + self.fee = match &self.fee_request { + TxBuilderFee::Exactly(exact_fee) => Some(exact_fee.clone()), + TxBuilderFee::NotLess(not_less) => { + if &fee >= not_less + { Some(fee) } else { Some(not_less.clone()) } + }, + TxBuilderFee::Unspecified => Some(fee), + } } /// !!! DEPRECATED !!! @@ -1491,6 +1514,7 @@ impl TransactionBuilder { inputs: TxInputsBuilder::new(), collateral: TxInputsBuilder::new(), outputs: TransactionOutputs::new(), + fee_request: TxBuilderFee::Unspecified, fee: None, ttl: None, certs: None, @@ -1573,6 +1597,22 @@ impl TransactionBuilder { Ok(()) } + fn validate_fee(&self) -> Result<(), JsError> { + if let Some(fee) = &self.fee { + let min_fee = min_fee(&self)?; + if fee < &min_fee { + Err(JsError::from_str(&format!( + "Fee is less than the minimum fee. Min fee: {}, Fee: {}", + min_fee, fee + ))) + } else { + Ok(()) + } + } else { + Err(JsError::from_str("Fee is not set")) + } + } + fn get_total_ref_scripts_size(&self) -> Result { let mut sizes_map = HashMap::new(); fn add_to_map<'a>( @@ -1719,7 +1759,15 @@ impl TransactionBuilder { } pub fn get_fee_if_set(&self) -> Option { - self.fee.clone() + if let Some(fee) = &self.fee { + return Some(fee.clone()) + }; + + match self.fee_request { + TxBuilderFee::Exactly(fee) => Some(fee), + TxBuilderFee::NotLess(fee) => Some(fee), + TxBuilderFee::Unspecified => None + } } /// Warning: this function will mutate the /fee/ field @@ -1773,7 +1821,7 @@ impl TransactionBuilder { match &input_total.partial_cmp(&output_total.checked_add(&Value::new(&fee))?) { Some(Ordering::Equal) => { // recall: min_fee assumed the fee was the maximum possible so we definitely have enough input to cover whatever fee it ends up being - self.set_fee(&input_total.checked_sub(&output_total)?.coin()); + self.set_final_fee(input_total.checked_sub(&output_total)?.coin()); Ok(false) } Some(Ordering::Less) => Err(JsError::from_str("Insufficient input in transaction")), @@ -2022,7 +2070,7 @@ impl TransactionBuilder { })?; } } - self.set_fee(&new_fee); + self.set_final_fee(new_fee); // add in the rest of the ADA if !change_left.is_zero() { self.outputs.0.last_mut().unwrap().amount = self @@ -2054,7 +2102,7 @@ impl TransactionBuilder { burn_amount: &BigNum, ) -> Result { // recall: min_fee assumed the fee was the maximum possible so we definitely have enough input to cover whatever fee it ends up being - builder.set_fee(burn_amount); + builder.set_final_fee(burn_amount.clone()); Ok(false) // not enough input to covert the extra fee from adding an output so we just burn whatever is left } match change_estimator.coin() >= min_ada { @@ -2076,7 +2124,7 @@ impl TransactionBuilder { false => burn_extra(self, &change_estimator.coin()), true => { // recall: min_fee assumed the fee was the maximum possible so we definitely have enough input to cover whatever fee it ends up being - self.set_fee(&new_fee); + self.set_final_fee(new_fee); self.add_output(&TransactionOutput { address: address.clone(), @@ -2208,6 +2256,7 @@ impl TransactionBuilder { fn build_and_size(&self) -> Result<(TransactionBody, usize), JsError> { let fee = self .fee + .or(self.get_fee_if_set()) .ok_or_else(|| JsError::from_str("Fee not specified"))?; let built = TransactionBody { @@ -2466,6 +2515,7 @@ impl TransactionBuilder { } } self.validate_inputs_intersection()?; + self.validate_fee()?; self.build_tx_unsafe() } @@ -2484,7 +2534,7 @@ impl TransactionBuilder { /// this is done to simplify the library code, but can be fixed later pub fn min_fee(&self) -> Result { let mut self_copy = self.clone(); - self_copy.set_fee(&(0x1_00_00_00_00u64).into()); + self_copy.set_final_fee((0x1_00_00_00_00u64).into()); min_fee(&self_copy) } } diff --git a/rust/src/tests/builders/tx_builder.rs b/rust/src/tests/builders/tx_builder.rs index 6035c607..43eb3a07 100644 --- a/rust/src/tests/builders/tx_builder.rs +++ b/rust/src/tests/builders/tx_builder.rs @@ -3893,7 +3893,6 @@ fn create_collateral() -> TxInputsBuilder { #[test] fn test_existing_plutus_scripts_require_data_hash() { let mut tx_builder = fake_reallistic_tx_builder(); - tx_builder.set_fee(&BigNum(42)); tx_builder.set_collateral(&create_collateral()); let (script1, _) = fake_plutus_script_and_hash(0); let datum = PlutusData::new_bytes(fake_bytes_32(1)); @@ -3917,6 +3916,8 @@ fn test_existing_plutus_scripts_require_data_hash() { assert!(e.as_string().unwrap().contains("script data hash")); } + tx_builder.add_change_if_needed(&fake_base_address(1)).unwrap(); + // Setting script data hash removes the error tx_builder.set_script_data_hash(&ScriptDataHash::from_bytes(fake_bytes_32(42)).unwrap()); // Using SAFE `.build_tx` @@ -3933,7 +3934,6 @@ fn test_existing_plutus_scripts_require_data_hash() { #[test] fn test_calc_script_hash_data() { let mut tx_builder = fake_reallistic_tx_builder(); - tx_builder.set_fee(&BigNum(42)); tx_builder.set_collateral(&create_collateral()); let (script1, _) = fake_plutus_script_and_hash(0); @@ -3956,6 +3956,8 @@ fn test_calc_script_hash_data() { .calc_script_data_hash(&TxBuilderConstants::plutus_default_cost_models()) .unwrap(); + tx_builder.add_change_if_needed(&fake_base_address(1)).unwrap(); + // Using SAFE `.build_tx` let res2 = tx_builder.build_tx(); assert!(res2.is_ok()); @@ -3974,7 +3976,6 @@ fn test_calc_script_hash_data() { #[test] fn test_plutus_witness_redeemer_index_auto_changing() { let mut tx_builder = fake_reallistic_tx_builder(); - tx_builder.set_fee(&BigNum(42)); tx_builder.set_collateral(&create_collateral()); let (script1, _) = fake_plutus_script_and_hash(0); let (script2, _) = fake_plutus_script_and_hash(1); @@ -4020,6 +4021,8 @@ fn test_plutus_witness_redeemer_index_auto_changing() { .calc_script_data_hash(&TxBuilderConstants::plutus_default_cost_models()) .unwrap(); + tx_builder.add_change_if_needed(&fake_base_address(1)).expect("Failed to add change"); + let tx: Transaction = tx_builder.build_tx().unwrap(); assert!(tx.witness_set.redeemers.is_some()); let redeems = tx.witness_set.redeemers.unwrap(); @@ -4044,7 +4047,6 @@ fn test_plutus_witness_redeemer_index_auto_changing() { #[test] fn test_native_and_plutus_scripts_together() { let mut tx_builder = fake_reallistic_tx_builder(); - tx_builder.set_fee(&BigNum(42)); tx_builder.set_collateral(&create_collateral()); let (pscript1, _) = fake_plutus_script_and_hash(0); let (pscript2, _phash2) = fake_plutus_script_and_hash(1); @@ -4095,6 +4097,8 @@ fn test_native_and_plutus_scripts_together() { .calc_script_data_hash(&TxBuilderConstants::plutus_default_cost_models()) .unwrap(); + tx_builder.add_change_if_needed(&fake_base_address(1)).expect("Failed to add change"); + let tx: Transaction = tx_builder.build_tx().unwrap(); let wits = tx.witness_set; @@ -4130,7 +4134,6 @@ fn test_native_and_plutus_scripts_together() { #[test] fn test_json_serialization_native_and_plutus_scripts_together() { let mut tx_builder = fake_reallistic_tx_builder(); - tx_builder.set_fee(&BigNum(42)); tx_builder.set_collateral(&create_collateral()); let (pscript1, _) = fake_plutus_script_and_hash(0); let (pscript2, _phash2) = fake_plutus_script_and_hash(1); @@ -4179,6 +4182,8 @@ fn test_json_serialization_native_and_plutus_scripts_together() { tx_builder.calc_script_data_hash(&TxBuilderConstants::plutus_default_cost_models()).expect("Failed to calc script data hash"); + tx_builder.add_change_if_needed(&fake_base_address(1)).expect("Failed to add change"); + let tx: Transaction = tx_builder.build_tx().unwrap(); let json_tx = tx.to_json().unwrap(); @@ -4238,7 +4243,6 @@ fn test_regular_and_collateral_inputs_same_keyhash() { #[test] fn test_regular_and_collateral_inputs_together() { let mut tx_builder = fake_reallistic_tx_builder(); - tx_builder.set_fee(&BigNum(42)); let (pscript1, _) = fake_plutus_script_and_hash(0); let (pscript2, _) = fake_plutus_script_and_hash(1); let (nscript1, _) = mint_script_and_policy(0); @@ -4291,6 +4295,8 @@ fn test_regular_and_collateral_inputs_together() { .calc_script_data_hash(&TxBuilderConstants::plutus_default_cost_models()) .unwrap(); + tx_builder.add_change_if_needed(&fake_base_address(1)).expect("Failed to add change"); + let w: &TransactionWitnessSet = &tx_builder.build_tx().unwrap().witness_set; assert!(w.native_scripts.is_some()); @@ -4939,7 +4945,6 @@ fn test_auto_calc_collateral_return_fails_on_no_collateral() { #[test] fn test_costmodel_retaining_for_v1() { let mut tx_builder = fake_reallistic_tx_builder(); - tx_builder.set_fee(&BigNum(42)); tx_builder.set_collateral(&create_collateral()); let (script1, _) = fake_plutus_script_and_hash(0); @@ -4956,6 +4961,7 @@ fn test_costmodel_retaining_for_v1() { &Value::new(&BigNum(1_000_000)), ); + tx_builder.add_change_if_needed(&fake_base_address(42)).unwrap(); // Setting script data hash removes the error tx_builder .calc_script_data_hash(&TxBuilderConstants::plutus_vasil_cost_models()) @@ -6485,4 +6491,86 @@ fn multiple_boostrap_witnesses() { let min_fee = min_fee_for_size(tx_len, &fake_linear_fee(44, 155381)).unwrap(); assert!(total_tx_fee >= min_fee); +} + +#[test] +fn tx_builder_exact_fee_test() { + let mut tx_builder = fake_reallistic_tx_builder(); + let change_address = fake_base_address(1); + + let manual_fee = BigNum(1000000u64); + + let input= fake_tx_input(1); + let utxo_address = fake_base_address(2); + let utxo_value = Value::new(&Coin::from(10000000u64)); + tx_builder.add_regular_input(&utxo_address, &input, &utxo_value).unwrap(); + tx_builder.set_fee(&manual_fee); + tx_builder.add_inputs_from_and_change(&TransactionUnspentOutputs::new(), CoinSelectionStrategyCIP2::LargestFirstMultiAsset, &ChangeConfig::new(&change_address)).unwrap(); + + let res = tx_builder.build_tx(); + assert!(res.is_ok()); + + let fee = res.unwrap().body().fee(); + assert_eq!(fee, manual_fee); +} + +#[test] +fn tx_builder_exact_fee_error_test() { + let mut tx_builder = fake_reallistic_tx_builder(); + let change_address = fake_base_address(1); + + let manual_fee = BigNum(1064); + + let input= fake_tx_input(1); + let utxo_address = fake_base_address(2); + let utxo_value = Value::new(&Coin::from(10000000u64)); + tx_builder.add_regular_input(&utxo_address, &input, &utxo_value).unwrap(); + tx_builder.set_fee(&manual_fee); + tx_builder.add_inputs_from_and_change(&TransactionUnspentOutputs::new(), CoinSelectionStrategyCIP2::LargestFirstMultiAsset, &ChangeConfig::new(&change_address)).unwrap(); + + let res = tx_builder.build_tx(); + assert!(res.is_err()); +} + + +#[test] +fn tx_builder_min_fee_small_automatic_fee_test() { + let mut tx_builder = fake_reallistic_tx_builder(); + let change_address = fake_base_address(1); + + let manual_fee = BigNum(1000000u64); + + let input= fake_tx_input(1); + let utxo_address = fake_base_address(2); + let utxo_value = Value::new(&Coin::from(10000000u64)); + tx_builder.add_regular_input(&utxo_address, &input, &utxo_value).unwrap(); + tx_builder.set_min_fee(&manual_fee); + tx_builder.add_inputs_from_and_change(&TransactionUnspentOutputs::new(), CoinSelectionStrategyCIP2::LargestFirstMultiAsset, &ChangeConfig::new(&change_address)).unwrap(); + + let res = tx_builder.build_tx(); + assert!(res.is_ok()); + + let fee = res.unwrap().body().fee(); + assert_eq!(fee, manual_fee); +} + +#[test] +fn tx_builder_min_fee_big_automatic_fee_test() { + let mut tx_builder = fake_reallistic_tx_builder(); + let change_address = fake_base_address(1); + + let manual_fee = BigNum(100u64); + + let input= fake_tx_input(1); + let utxo_address = fake_base_address(2); + let utxo_value = Value::new(&Coin::from(10000000u64)); + tx_builder.add_regular_input(&utxo_address, &input, &utxo_value).unwrap(); + tx_builder.set_min_fee(&manual_fee); + tx_builder.add_inputs_from_and_change(&TransactionUnspentOutputs::new(), CoinSelectionStrategyCIP2::LargestFirstMultiAsset, &ChangeConfig::new(&change_address)).unwrap(); + + let res = tx_builder.build_tx(); + assert!(res.is_ok()); + + let fee = res.unwrap().body().fee(); + assert!(fee > manual_fee); } \ No newline at end of file diff --git a/rust/src/tests/builders/voting_builder.rs b/rust/src/tests/builders/voting_builder.rs index df627bf7..8bfd29f5 100644 --- a/rust/src/tests/builders/voting_builder.rs +++ b/rust/src/tests/builders/voting_builder.rs @@ -125,15 +125,16 @@ fn voting_builder_plutus_witness() { let mut tx_builder = fake_rich_tx_builder(true); tx_builder.set_voting_builder(&builder); - tx_builder - .add_change_if_needed(&fake_change_address()) - .unwrap(); let mut cost_models = TxBuilderConstants::plutus_default_cost_models(); cost_models = cost_models.retain_language_versions(&Languages(vec![Language::new_plutus_v2()])); tx_builder.calc_script_data_hash(&cost_models).unwrap(); + tx_builder + .add_change_if_needed(&fake_change_address()) + .unwrap(); + let tx = tx_builder.build_tx().unwrap(); let tx_witnesses = tx.witness_set(); @@ -221,15 +222,14 @@ fn voting_builder_plutus_ref_witness() { let mut tx_builder = fake_rich_tx_builder(true); tx_builder.set_voting_builder(&builder); - tx_builder - .add_change_if_needed(&fake_change_address()) - .unwrap(); let mut cost_models = TxBuilderConstants::plutus_default_cost_models(); cost_models = cost_models.retain_language_versions(&Languages(vec![Language::new_plutus_v2()])); tx_builder.calc_script_data_hash(&cost_models).unwrap(); + tx_builder.add_change_if_needed(&fake_change_address()).unwrap(); + let tx = tx_builder.build_tx().unwrap(); let tx_witnesses = tx.witness_set(); diff --git a/rust/src/tests/builders/voting_proposal_builder.rs b/rust/src/tests/builders/voting_proposal_builder.rs index 6a48f17b..85316bcc 100644 --- a/rust/src/tests/builders/voting_proposal_builder.rs +++ b/rust/src/tests/builders/voting_proposal_builder.rs @@ -249,15 +249,16 @@ fn voting_proposal_builder_with_plutus_script_witness() { fake_tx_builder_with_amount_and_deposit_params(initial_amount, 500, 500, true); tx_builder.set_voting_proposal_builder(&builder); - tx_builder - .add_change_if_needed(&fake_change_address()) - .unwrap(); let mut cost_models = TxBuilderConstants::plutus_default_cost_models(); cost_models = cost_models.retain_language_versions(&Languages(vec![Language::new_plutus_v2()])); tx_builder.calc_script_data_hash(&cost_models).unwrap(); + tx_builder + .add_change_if_needed(&fake_change_address()) + .unwrap(); + let tx = tx_builder.build_tx().unwrap(); let voting_proposals = tx.body().voting_proposals().unwrap(); @@ -357,15 +358,16 @@ fn voting_proposal_builder_with_ref_plutus_script_witness() { fake_tx_builder_with_amount_and_deposit_params(initial_amount, 500, 500, true); tx_builder.set_voting_proposal_builder(&builder); - tx_builder - .add_change_if_needed(&fake_change_address()) - .unwrap(); let mut cost_models = TxBuilderConstants::plutus_default_cost_models(); cost_models = cost_models.retain_language_versions(&Languages(vec![Language::new_plutus_v2()])); tx_builder.calc_script_data_hash(&cost_models).unwrap(); + tx_builder + .add_change_if_needed(&fake_change_address()) + .unwrap(); + let tx = tx_builder.build_tx().unwrap(); let voting_proposals = tx.body().voting_proposals().unwrap(); From 6d9a474a89b5a71251774ebd73d42e853e81495d Mon Sep 17 00:00:00 2001 From: lisicky Date: Mon, 11 Nov 2024 05:20:08 +0800 Subject: [PATCH 2/9] flow update --- rust/pkg/cardano_serialization_lib.js.flow | 327 +++++++++++---------- 1 file changed, 166 insertions(+), 161 deletions(-) diff --git a/rust/pkg/cardano_serialization_lib.js.flow b/rust/pkg/cardano_serialization_lib.js.flow index c0d602bc..a8a0d9a9 100644 --- a/rust/pkg/cardano_serialization_lib.js.flow +++ b/rust/pkg/cardano_serialization_lib.js.flow @@ -94,6 +94,30 @@ declare export function min_ref_script_fee( ref_script_coins_per_byte: UnitInterval ): BigNum; +/** + * @param {string} password + * @param {string} salt + * @param {string} nonce + * @param {string} data + * @returns {string} + */ +declare export function encrypt_with_password( + password: string, + salt: string, + nonce: string, + data: string +): string; + +/** + * @param {string} password + * @param {string} data + * @returns {string} + */ +declare export function decrypt_with_password( + password: string, + data: string +): string; + /** * @param {string} json * @param {$Values< @@ -253,41 +277,23 @@ declare export function has_transaction_set_tag( ): $Values; /** - * @param {string} password - * @param {string} salt - * @param {string} nonce - * @param {string} data - * @returns {string} */ -declare export function encrypt_with_password( - password: string, - salt: string, - nonce: string, - data: string -): string; -/** - * @param {string} password - * @param {string} data - * @returns {string} - */ -declare export function decrypt_with_password( - password: string, - data: string -): string; +declare export var MIRPot: {| + +Reserves: 0, // 0 + +Treasury: 1, // 1 +|}; /** - * Each new language uses a different namespace for hashing its script - * This is because you could have a language where the same bytes have different semantics - * So this avoids scripts in different languages mapping to the same hash - * Note that the enum value here is different than the enum value for deciding the cost model of a script */ -declare export var ScriptHashNamespace: {| - +NativeScript: 0, // 0 - +PlutusScript: 1, // 1 - +PlutusScriptV2: 2, // 2 - +PlutusScriptV3: 3, // 3 +declare export var RedeemerTagKind: {| + +Spend: 0, // 0 + +Mint: 1, // 1 + +Cert: 2, // 2 + +Reward: 3, // 3 + +Vote: 4, // 4 + +VotingProposal: 5, // 5 |}; /** @@ -302,13 +308,9 @@ declare export var VoteKind: {| /** */ -declare export var NativeScriptKind: {| - +ScriptPubkey: 0, // 0 - +ScriptAll: 1, // 1 - +ScriptAny: 2, // 2 - +ScriptNOfK: 3, // 3 - +TimelockStart: 4, // 4 - +TimelockExpiry: 5, // 5 +declare export var CborSetType: {| + +Tagged: 0, // 0 + +Untagged: 1, // 1 |}; /** @@ -321,93 +323,107 @@ declare export var RelayKind: {| |}; /** - * Used to choosed the schema for a script JSON string */ -declare export var ScriptSchema: {| - +Wallet: 0, // 0 - +Node: 1, // 1 -|}; - -/** - */ - -declare export var CoinSelectionStrategyCIP2: {| - +LargestFirst: 0, // 0 - +RandomImprove: 1, // 1 - +LargestFirstMultiAsset: 2, // 2 - +RandomImproveMultiAsset: 3, // 3 +declare export var AddressKind: {| + +Base: 0, // 0 + +Pointer: 1, // 1 + +Enterprise: 2, // 2 + +Reward: 3, // 3 + +Byron: 4, // 4 + +Malformed: 5, // 5 |}; /** */ -declare export var GovernanceActionKind: {| - +ParameterChangeAction: 0, // 0 - +HardForkInitiationAction: 1, // 1 - +TreasuryWithdrawalsAction: 2, // 2 - +NoConfidenceAction: 3, // 3 - +UpdateCommitteeAction: 4, // 4 - +NewConstitutionAction: 5, // 5 - +InfoAction: 6, // 6 +declare export var CertificateKind: {| + +StakeRegistration: 0, // 0 + +StakeDeregistration: 1, // 1 + +StakeDelegation: 2, // 2 + +PoolRegistration: 3, // 3 + +PoolRetirement: 4, // 4 + +GenesisKeyDelegation: 5, // 5 + +MoveInstantaneousRewardsCert: 6, // 6 + +CommitteeHotAuth: 7, // 7 + +CommitteeColdResign: 8, // 8 + +DRepDeregistration: 9, // 9 + +DRepRegistration: 10, // 10 + +DRepUpdate: 11, // 11 + +StakeAndVoteDelegation: 12, // 12 + +StakeRegistrationAndDelegation: 13, // 13 + +StakeVoteRegistrationAndDelegation: 14, // 14 + +VoteDelegation: 15, // 15 + +VoteRegistrationAndDelegation: 16, // 16 |}; /** */ -declare export var CborContainerType: {| - +Array: 0, // 0 - +Map: 1, // 1 +declare export var LanguageKind: {| + +PlutusV1: 0, // 0 + +PlutusV2: 1, // 1 + +PlutusV3: 2, // 2 |}; /** + * Each new language uses a different namespace for hashing its script + * This is because you could have a language where the same bytes have different semantics + * So this avoids scripts in different languages mapping to the same hash + * Note that the enum value here is different than the enum value for deciding the cost model of a script */ -declare export var AddressKind: {| - +Base: 0, // 0 - +Pointer: 1, // 1 - +Enterprise: 2, // 2 - +Reward: 3, // 3 - +Byron: 4, // 4 - +Malformed: 5, // 5 +declare export var ScriptHashNamespace: {| + +NativeScript: 0, // 0 + +PlutusScript: 1, // 1 + +PlutusScriptV2: 2, // 2 + +PlutusScriptV3: 3, // 3 |}; /** */ -declare export var MetadataJsonSchema: {| - +NoConversions: 0, // 0 - +BasicConversions: 1, // 1 - +DetailedSchema: 2, // 2 +declare export var BlockEra: {| + +Byron: 0, // 0 + +Shelley: 1, // 1 + +Allegra: 2, // 2 + +Mary: 3, // 3 + +Alonzo: 4, // 4 + +Babbage: 5, // 5 + +Conway: 6, // 6 + +Unknown: 7, // 7 |}; /** */ -declare export var MIRKind: {| - +ToOtherPot: 0, // 0 - +ToStakeCredentials: 1, // 1 +declare export var VoterKind: {| + +ConstitutionalCommitteeHotKeyHash: 0, // 0 + +ConstitutionalCommitteeHotScriptHash: 1, // 1 + +DRepKeyHash: 2, // 2 + +DRepScriptHash: 3, // 3 + +StakingPoolKeyHash: 4, // 4 |}; /** */ -declare export var DRepKind: {| - +KeyHash: 0, // 0 - +ScriptHash: 1, // 1 - +AlwaysAbstain: 2, // 2 - +AlwaysNoConfidence: 3, // 3 +declare export var TransactionSetsState: {| + +AllSetsHaveTag: 0, // 0 + +AllSetsHaveNoTag: 1, // 1 + +MixedSets: 2, // 2 |}; /** */ -declare export var TransactionMetadatumKind: {| - +MetadataMap: 0, // 0 - +MetadataList: 1, // 1 - +Int: 2, // 2 - +Bytes: 3, // 3 - +Text: 4, // 4 +declare export var NativeScriptKind: {| + +ScriptPubkey: 0, // 0 + +ScriptAll: 1, // 1 + +ScriptAny: 2, // 2 + +ScriptNOfK: 3, // 3 + +TimelockStart: 4, // 4 + +TimelockExpiry: 5, // 5 |}; /** @@ -421,131 +437,115 @@ declare export var NetworkIdKind: {| /** */ -declare export var CredKind: {| - +Key: 0, // 0 - +Script: 1, // 1 +declare export var GovernanceActionKind: {| + +ParameterChangeAction: 0, // 0 + +HardForkInitiationAction: 1, // 1 + +TreasuryWithdrawalsAction: 2, // 2 + +NoConfidenceAction: 3, // 3 + +UpdateCommitteeAction: 4, // 4 + +NewConstitutionAction: 5, // 5 + +InfoAction: 6, // 6 |}; /** */ -declare export var PlutusDataKind: {| - +ConstrPlutusData: 0, // 0 - +Map: 1, // 1 - +List: 2, // 2 - +Integer: 3, // 3 - +Bytes: 4, // 4 +declare export var CredKind: {| + +Key: 0, // 0 + +Script: 1, // 1 |}; /** + * Used to choosed the schema for a script JSON string */ -declare export var MIRPot: {| - +Reserves: 0, // 0 - +Treasury: 1, // 1 +declare export var ScriptSchema: {| + +Wallet: 0, // 0 + +Node: 1, // 1 |}; /** */ -declare export var CertificateKind: {| - +StakeRegistration: 0, // 0 - +StakeDeregistration: 1, // 1 - +StakeDelegation: 2, // 2 - +PoolRegistration: 3, // 3 - +PoolRetirement: 4, // 4 - +GenesisKeyDelegation: 5, // 5 - +MoveInstantaneousRewardsCert: 6, // 6 - +CommitteeHotAuth: 7, // 7 - +CommitteeColdResign: 8, // 8 - +DRepDeregistration: 9, // 9 - +DRepRegistration: 10, // 10 - +DRepUpdate: 11, // 11 - +StakeAndVoteDelegation: 12, // 12 - +StakeRegistrationAndDelegation: 13, // 13 - +StakeVoteRegistrationAndDelegation: 14, // 14 - +VoteDelegation: 15, // 15 - +VoteRegistrationAndDelegation: 16, // 16 +declare export var MetadataJsonSchema: {| + +NoConversions: 0, // 0 + +BasicConversions: 1, // 1 + +DetailedSchema: 2, // 2 |}; /** + * JSON <-> PlutusData conversion schemas. + * Follows ScriptDataJsonSchema in cardano-cli defined at: + * https://github.com/input-output-hk/cardano-node/blob/master/cardano-api/src/Cardano/Api/ScriptData.hs#L254 + * + * All methods here have the following restrictions due to limitations on dependencies: + * * JSON numbers above u64::MAX (positive) or below i64::MIN (negative) will throw errors + * * Hex strings for bytes don't accept odd-length (half-byte) strings. + * cardano-cli seems to support these however but it seems to be different than just 0-padding + * on either side when tested so proceed with caution */ -declare export var TransactionSetsState: {| - +AllSetsHaveTag: 0, // 0 - +AllSetsHaveNoTag: 1, // 1 - +MixedSets: 2, // 2 +declare export var PlutusDatumSchema: {| + +BasicConversions: 0, // 0 + +DetailedSchema: 1, // 1 |}; /** */ -declare export var BlockEra: {| - +Byron: 0, // 0 - +Shelley: 1, // 1 - +Allegra: 2, // 2 - +Mary: 3, // 3 - +Alonzo: 4, // 4 - +Babbage: 5, // 5 - +Conway: 6, // 6 - +Unknown: 7, // 7 +declare export var CoinSelectionStrategyCIP2: {| + +LargestFirst: 0, // 0 + +RandomImprove: 1, // 1 + +LargestFirstMultiAsset: 2, // 2 + +RandomImproveMultiAsset: 3, // 3 |}; /** */ -declare export var RedeemerTagKind: {| - +Spend: 0, // 0 - +Mint: 1, // 1 - +Cert: 2, // 2 - +Reward: 3, // 3 - +Vote: 4, // 4 - +VotingProposal: 5, // 5 +declare export var DRepKind: {| + +KeyHash: 0, // 0 + +ScriptHash: 1, // 1 + +AlwaysAbstain: 2, // 2 + +AlwaysNoConfidence: 3, // 3 |}; /** */ -declare export var LanguageKind: {| - +PlutusV1: 0, // 0 - +PlutusV2: 1, // 1 - +PlutusV3: 2, // 2 +declare export var CborContainerType: {| + +Array: 0, // 0 + +Map: 1, // 1 |}; /** */ -declare export var VoterKind: {| - +ConstitutionalCommitteeHotKeyHash: 0, // 0 - +ConstitutionalCommitteeHotScriptHash: 1, // 1 - +DRepKeyHash: 2, // 2 - +DRepScriptHash: 3, // 3 - +StakingPoolKeyHash: 4, // 4 +declare export var MIRKind: {| + +ToOtherPot: 0, // 0 + +ToStakeCredentials: 1, // 1 |}; /** */ -declare export var CborSetType: {| - +Tagged: 0, // 0 - +Untagged: 1, // 1 +declare export var PlutusDataKind: {| + +ConstrPlutusData: 0, // 0 + +Map: 1, // 1 + +List: 2, // 2 + +Integer: 3, // 3 + +Bytes: 4, // 4 |}; /** - * JSON <-> PlutusData conversion schemas. - * Follows ScriptDataJsonSchema in cardano-cli defined at: - * https://github.com/input-output-hk/cardano-node/blob/master/cardano-api/src/Cardano/Api/ScriptData.hs#L254 - * - * All methods here have the following restrictions due to limitations on dependencies: - * * JSON numbers above u64::MAX (positive) or below i64::MIN (negative) will throw errors - * * Hex strings for bytes don't accept odd-length (half-byte) strings. - * cardano-cli seems to support these however but it seems to be different than just 0-padding - * on either side when tested so proceed with caution */ -declare export var PlutusDatumSchema: {| - +BasicConversions: 0, // 0 - +DetailedSchema: 1, // 1 +declare export var TransactionMetadatumKind: {| + +MetadataMap: 0, // 0 + +MetadataList: 1, // 1 + +Int: 2, // 2 + +Bytes: 3, // 3 + +Text: 4, // 4 |}; /** @@ -11207,6 +11207,11 @@ declare export class TransactionBuilder { */ set_fee(fee: BigNum): void; + /** + * @param {BigNum} fee + */ + set_min_fee(fee: BigNum): void; + /** * !!! DEPRECATED !!! * Set ttl value. From e4af66a0e7c75e79d9d1c617e8de022a8a8edfda Mon Sep 17 00:00:00 2001 From: lisicky Date: Mon, 11 Nov 2024 15:34:06 +0800 Subject: [PATCH 3/9] fix usage of fee getter --- rust/src/builders/tx_builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index 7b781906..a2152ba6 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -1598,7 +1598,7 @@ impl TransactionBuilder { } fn validate_fee(&self) -> Result<(), JsError> { - if let Some(fee) = &self.fee { + if let Some(fee) = &self.get_fee_if_set() { let min_fee = min_fee(&self)?; if fee < &min_fee { Err(JsError::from_str(&format!( @@ -2255,7 +2255,7 @@ impl TransactionBuilder { fn build_and_size(&self) -> Result<(TransactionBody, usize), JsError> { let fee = self - .fee + .get_fee_if_set() .or(self.get_fee_if_set()) .ok_or_else(|| JsError::from_str("Fee not specified"))?; From e32a65a76df116db7e76e681578f6ec3fde82dc0 Mon Sep 17 00:00:00 2001 From: lisicky Date: Tue, 12 Nov 2024 01:49:52 +0800 Subject: [PATCH 4/9] fix fee calculation --- rust/src/builders/tx_builder.rs | 36 ++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index a2152ba6..973ed8fd 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -355,6 +355,24 @@ pub(crate) enum TxBuilderFee { Exactly(Coin), } +impl TxBuilderFee { + fn get_new_fee(&self, new_fee: Coin) -> Coin { + match self { + TxBuilderFee::Unspecified => new_fee, + TxBuilderFee::NotLess(old_fee) => { + if &new_fee < old_fee { + old_fee.clone() + } else { + new_fee + } + } + TxBuilderFee::Exactly(old_fee) => { + old_fee.clone() + } + } + } +} + #[wasm_bindgen] #[derive(Clone, Debug)] pub struct TransactionBuilder { @@ -1103,10 +1121,13 @@ impl TransactionBuilder { self_copy.set_final_fee(BigNum::zero()); let fee_before = min_fee(&self_copy)?; + let aligned_fee_before = self.fee_request.get_new_fee(fee_before); self_copy.add_output(&output)?; let fee_after = min_fee(&self_copy)?; - fee_after.checked_sub(&fee_before) + let aligned_fee_after = self.fee_request.get_new_fee(fee_after); + + aligned_fee_after.checked_sub(&aligned_fee_before) } pub fn set_fee(&mut self, fee: &Coin) { @@ -2002,9 +2023,9 @@ impl TransactionBuilder { // this likely should never happen return Err(JsError::from_str("NFTs too large for change output")); } - // we only add the minimum needed (for now) to cover this output - let mut change_value = Value::new(&Coin::zero()); for nft_change in nft_changes.iter() { + // we only add the minimum needed (for now) to cover this output + let mut change_value = Value::new(&Coin::zero()); change_value.set_multiasset(&nft_change); let mut calc = MinOutputAdaCalculator::new_empty(&utxo_cost)?; //TODO add precise calculation @@ -2101,7 +2122,16 @@ impl TransactionBuilder { builder: &mut TransactionBuilder, burn_amount: &BigNum, ) -> Result { + let fee_request = &builder.fee_request; // recall: min_fee assumed the fee was the maximum possible so we definitely have enough input to cover whatever fee it ends up being + match fee_request { + TxBuilderFee::Exactly(fee) => { + if burn_amount > fee { + return Err(JsError::from_str("Not enough ADA leftover to include a new change output and. But leftovers is more than fee restriction")); + } + } + _ => {} + } builder.set_final_fee(burn_amount.clone()); Ok(false) // not enough input to covert the extra fee from adding an output so we just burn whatever is left } From bb5c198ccf05d61c0e2ad1b57a4421ba188c5331 Mon Sep 17 00:00:00 2001 From: lisicky Date: Tue, 12 Nov 2024 02:13:09 +0800 Subject: [PATCH 5/9] fix fee calculation, add balance validation --- rust/src/builders/tx_builder.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index 973ed8fd..ca385f7c 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -1634,6 +1634,24 @@ impl TransactionBuilder { } } + fn validate_balance(&self) -> Result<(), JsError> { + let total_input = self.get_total_input()?; + let mut total_output = self.get_total_output()?; + let fee = self.get_fee_if_set(); + if let Some(fee) = fee { + let out_coin = total_output.coin().checked_add(&fee)?; + total_output.set_coin(&out_coin); + } + if total_input != total_output { + Err(JsError::from_str(&format!( + "Total input and total output are not equal. Total input: {}, Total output: {}", + total_input.to_json()?, total_output.to_json()? + ))) + } else { + Ok(()) + } + } + fn get_total_ref_scripts_size(&self) -> Result { let mut sizes_map = HashMap::new(); fn add_to_map<'a>( @@ -2546,6 +2564,7 @@ impl TransactionBuilder { } self.validate_inputs_intersection()?; self.validate_fee()?; + self.validate_balance()?; self.build_tx_unsafe() } @@ -2565,6 +2584,6 @@ impl TransactionBuilder { pub fn min_fee(&self) -> Result { let mut self_copy = self.clone(); self_copy.set_final_fee((0x1_00_00_00_00u64).into()); - min_fee(&self_copy) + Ok(self.fee_request.get_new_fee(min_fee(&self_copy)?)) } } From 025c46efb4c4624b39d2870c2dadd6b78c6e32bf Mon Sep 17 00:00:00 2001 From: lisicky Date: Tue, 12 Nov 2024 02:17:23 +0800 Subject: [PATCH 6/9] add test --- rust/src/builders/tx_builder.rs | 2 +- rust/src/tests/builders/tx_builder.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index ca385f7c..e8c57953 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -2145,7 +2145,7 @@ impl TransactionBuilder { match fee_request { TxBuilderFee::Exactly(fee) => { if burn_amount > fee { - return Err(JsError::from_str("Not enough ADA leftover to include a new change output and. But leftovers is more than fee restriction")); + return Err(JsError::from_str("Not enough ADA leftover to include a new change output. And leftovers is bigger than fee upper bound")); } } _ => {} diff --git a/rust/src/tests/builders/tx_builder.rs b/rust/src/tests/builders/tx_builder.rs index 43eb3a07..585f28d2 100644 --- a/rust/src/tests/builders/tx_builder.rs +++ b/rust/src/tests/builders/tx_builder.rs @@ -6573,4 +6573,21 @@ fn tx_builder_min_fee_big_automatic_fee_test() { let fee = res.unwrap().body().fee(); assert!(fee > manual_fee); +} + +#[test] +fn tx_builder_exact_fee_burn_extra_issue() { + let mut tx_builder = fake_reallistic_tx_builder(); + let change_address = fake_base_address(1); + + let manual_fee = BigNum(100u64); + + let input= fake_tx_input(1); + let utxo_address = fake_base_address(2); + let utxo_value = Value::new(&Coin::from(1000u64)); + tx_builder.add_regular_input(&utxo_address, &input, &utxo_value).unwrap(); + tx_builder.set_fee(&manual_fee); + let change_res = tx_builder.add_inputs_from_and_change(&TransactionUnspentOutputs::new(), CoinSelectionStrategyCIP2::LargestFirstMultiAsset, &ChangeConfig::new(&change_address)); + + assert!(change_res.is_err()); } \ No newline at end of file From 6c25e5ee1cb180de4bf9a0f08be317f718a40b4e Mon Sep 17 00:00:00 2001 From: lisicky Date: Tue, 12 Nov 2024 15:24:16 +0800 Subject: [PATCH 7/9] fix fee_for_input --- rust/src/builders/tx_builder.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index e8c57953..7c6583f0 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -1083,10 +1083,13 @@ impl TransactionBuilder { self_copy.set_final_fee(BigNum::zero()); let fee_before = min_fee(&self_copy)?; + let aligned_fee_before = self.fee_request.get_new_fee(fee_before); self_copy.add_regular_input(&address, &input, &amount)?; let fee_after = min_fee(&self_copy)?; - fee_after.checked_sub(&fee_before) + let aligned_fee_after = self.fee_request.get_new_fee(fee_after); + + aligned_fee_after.checked_sub(&aligned_fee_before) } /// Add explicit output via a TransactionOutput object From d47da7b0a09d15172617d0bf88fe60d4e9c4fdb0 Mon Sep 17 00:00:00 2001 From: lisicky Date: Tue, 12 Nov 2024 16:32:17 +0800 Subject: [PATCH 8/9] add useful error for collateral return --- rust/src/builders/tx_builder.rs | 7 +++++++ rust/src/tests/builders/tx_builder.rs | 29 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index f885c195..8269886d 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -803,6 +803,13 @@ impl TransactionBuilder { )); } let col_input_value: Value = collateral.total_value()?; + let col_input_coin = col_input_value.coin; + if col_input_coin < *total_collateral { + return Err(JsError::from_str( + "Total collateral value cannot exceed the sum of collateral inputs", + )); + } + let col_return: Value = col_input_value.checked_sub(&Value::new(&total_collateral))?; if col_return.multiasset.is_some() || col_return.coin > BigNum::zero() { let return_output = TransactionOutput::new(return_address, &col_return); diff --git a/rust/src/tests/builders/tx_builder.rs b/rust/src/tests/builders/tx_builder.rs index 6035c607..52cb339c 100644 --- a/rust/src/tests/builders/tx_builder.rs +++ b/rust/src/tests/builders/tx_builder.rs @@ -4923,6 +4923,35 @@ fn test_add_collateral_return_fails_no_enough_ada() { assert!(tx_builder.collateral_return.is_none()); } +#[test] +fn test_add_collateral_return_fails_no_enough_ada_to_cover_return() { + let mut tx_builder = fake_reallistic_tx_builder(); + tx_builder.set_fee(&BigNum(123456)); + + let masset = fake_multiasset(123); + + let mut inp = TxInputsBuilder::new(); + let collateral_input_value = 2_000_000; + inp.add_regular_input( + &fake_base_address(0), + &fake_tx_input(0), + &Value::new_with_assets(&BigNum(collateral_input_value.clone()), &masset), + ).expect("Failed to add input"); + + tx_builder.set_collateral(&inp); + + let collateral_return_address = fake_base_address(1); + + let total_collateral = BigNum(collateral_input_value + 1); + + let coll_add_res = tx_builder + .set_total_collateral_and_return(&total_collateral, &collateral_return_address); + + assert!(coll_add_res.is_err()); + assert!(tx_builder.total_collateral.is_none()); + assert!(tx_builder.collateral_return.is_none()); +} + #[test] fn test_auto_calc_collateral_return_fails_on_no_collateral() { let mut tx_builder = fake_reallistic_tx_builder(); From e832b5e0fe236d6307d30ae9d5b0777fde6fa25b Mon Sep 17 00:00:00 2001 From: lisicky Date: Thu, 14 Nov 2024 19:40:29 +0800 Subject: [PATCH 9/9] fix fee getter --- rust/src/builders/tx_builder.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index 7c6583f0..b53f1bbc 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -2307,7 +2307,6 @@ impl TransactionBuilder { fn build_and_size(&self) -> Result<(TransactionBody, usize), JsError> { let fee = self .get_fee_if_set() - .or(self.get_fee_if_set()) .ok_or_else(|| JsError::from_str("Fee not specified"))?; let built = TransactionBody {