diff --git a/package-lock.json b/package-lock.json index c130067d..11d70945 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cardano-serialization-lib", - "version": "9.1.2", + "version": "10.0.0-beta.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 16b5cd5f..4c1dd14a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cardano-serialization-lib", - "version": "9.1.2", + "version": "10.0.0-beta.8", "description": "(De)serialization functions for the Cardano blockchain along with related utility functions", "scripts": { "rust:build-nodejs": "(rimraf ./rust/pkg && cd rust; wasm-pack build --target=nodejs; wasm-pack pack) && npm run js:flowgen", diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 17de1a4b..ab208d1f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -50,7 +50,7 @@ checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" [[package]] name = "cardano-serialization-lib" -version = "9.1.2" +version = "10.0.0-beta.8" dependencies = [ "bech32", "cbor_event", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9db068ba..eaed2c5e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardano-serialization-lib" -version = "9.1.2" +version = "10.0.0-beta.8" edition = "2018" authors = ["EMURGO"] license = "MIT" diff --git a/rust/pkg/cardano_serialization_lib.js.flow b/rust/pkg/cardano_serialization_lib.js.flow index 2a66c8ec..b7652eab 100644 --- a/rust/pkg/cardano_serialization_lib.js.flow +++ b/rust/pkg/cardano_serialization_lib.js.flow @@ -5179,29 +5179,52 @@ declare export class TransactionBuilder { /** * Set explicit Mint object to this builder * it will replace any previously existing mint + * NOTE! If you use `set_mint` manually - you must use `set_mint_scripts` + * to provide matching policy scripts or min-fee calculation will be rejected! * @param {Mint} mint */ set_mint(mint: Mint): void; + /** + * Returns a copy of the current mint state in the builder + * @returns {Mint | void} + */ + get_mint(): Mint | void; + + /** + * Set explicit witness set to this builder + * It will replace any previously existing witnesses + * NOTE! Use carefully! If you are using `set_mint` - then you must be using + * this setter as well to be able to calculate fee automatically! + * @param {NativeScripts} mint_scripts + */ + set_mint_scripts(mint_scripts: NativeScripts): void; + + /** + * Returns a copy of the current mint witness scripts in the builder + * @returns {NativeScripts | void} + */ + get_mint_scripts(): NativeScripts | void; + /** * Add a mint entry to this builder using a PolicyID and MintAssets object * It will be securely added to existing or new Mint in this builder * It will replace any existing mint assets with the same PolicyID - * @param {ScriptHash} policy_id + * @param {NativeScript} policy_script * @param {MintAssets} mint_assets */ - set_mint_asset(policy_id: ScriptHash, mint_assets: MintAssets): void; + set_mint_asset(policy_script: NativeScript, mint_assets: MintAssets): void; /** * Add a mint entry to this builder using a PolicyID, AssetName, and Int object for amount * It will be securely added to existing or new Mint in this builder * It will replace any previous existing amount same PolicyID and AssetName - * @param {ScriptHash} policy_id + * @param {NativeScript} policy_script * @param {AssetName} asset_name * @param {Int} amount */ add_mint_asset( - policy_id: ScriptHash, + policy_script: NativeScript, asset_name: AssetName, amount: Int ): void; @@ -5211,14 +5234,14 @@ declare export class TransactionBuilder { * Using a PolicyID, AssetName, Int for amount, Address, and Coin (BigNum) objects * The asset will be securely added to existing or new Mint in this builder * A new output will be added with the specified Address, the Coin value, and the minted asset - * @param {ScriptHash} policy_id + * @param {NativeScript} policy_script * @param {AssetName} asset_name * @param {Int} amount * @param {Address} address * @param {BigNum} output_coin */ add_mint_asset_and_output( - policy_id: ScriptHash, + policy_script: NativeScript, asset_name: AssetName, amount: Int, address: Address, @@ -5231,13 +5254,13 @@ declare export class TransactionBuilder { * The asset will be securely added to existing or new Mint in this builder * A new output will be added with the specified Address and the minted asset * The output will be set to contain the minimum required amount of Coin - * @param {ScriptHash} policy_id + * @param {NativeScript} policy_script * @param {AssetName} asset_name * @param {Int} amount * @param {Address} address */ add_mint_asset_and_output_min_required_coin( - policy_id: ScriptHash, + policy_script: NativeScript, asset_name: AssetName, amount: Int, address: Address @@ -5300,14 +5323,14 @@ declare export class TransactionBuilder { /** * Returns object the body of the new transaction * Auxiliary data itself is not included - * You can use `get_auxiliary_date` or `build_tx` + * You can use `get_auxiliary_data` or `build_tx` * @returns {TransactionBody} */ build(): TransactionBody; /** * Returns full Transaction object with the body and the auxiliary data - * NOTE: witness_set is set to just empty set + * NOTE: witness_set will contain all mint_scripts if any been added or set * NOTE: is_valid set to true * @returns {Transaction} */ diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 600d91a4..248f355a 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -58,6 +58,7 @@ use plutus::*; use metadata::*; use utils::*; use std::cmp::Ordering; +use std::collections::BTreeSet; type DeltaCoin = Int; @@ -2752,6 +2753,35 @@ impl NetworkId { } } +pub fn get_all_pubkeys_from_script(script: &NativeScript) -> BTreeSet { + match &script.0 { + NativeScriptEnum::ScriptPubkey(spk) => { + let mut set = BTreeSet::new(); + set.insert(spk.addr_keyhash()); + set + }, + NativeScriptEnum::ScriptAll(all) => { + get_all_pubkeys_from_scripts(&all.native_scripts) + }, + NativeScriptEnum::ScriptAny(any) => { + get_all_pubkeys_from_scripts(&any.native_scripts) + }, + NativeScriptEnum::ScriptNOfK(ofk) => { + get_all_pubkeys_from_scripts(&ofk.native_scripts) + }, + _ => BTreeSet::new(), + } +} + +pub fn get_all_pubkeys_from_scripts(scripts: &NativeScripts) -> BTreeSet { + scripts.0.iter().fold(BTreeSet::new(), |mut set, s| { + get_all_pubkeys_from_script(s).iter().for_each(|pk| { + set.insert(pk.clone()); + }); + set + }) +} + #[cfg(test)] mod tests { use super::*; @@ -2933,4 +2963,77 @@ mod tests { assert_eq!(p_ass.get(&name1).unwrap(), amount1); assert_eq!(n_ass.get(&name1).unwrap(), amount1); } + + fn keyhash(x: u8) -> Ed25519KeyHash { + Ed25519KeyHash::from_bytes(vec![x, 180, 186, 93, 223, 42, 243, 7, 81, 98, 86, 125, 97, 69, 110, 52, 130, 243, 244, 98, 246, 13, 33, 212, 128, 168, 136, 40]).unwrap() + } + + fn pkscript(pk: &Ed25519KeyHash) -> NativeScript { + NativeScript::new_script_pubkey(&ScriptPubkey::new(pk)) + } + + fn scripts_vec(scripts: Vec<&NativeScript>) -> NativeScripts { + NativeScripts(scripts.iter().map(|s| { (*s).clone() }).collect()) + } + + #[test] + fn native_scripts_get_pubkeys() { + let keyhash1 = keyhash(1); + let keyhash2 = keyhash(2); + let keyhash3 = keyhash(3); + + let pks1 = get_all_pubkeys_from_script(&pkscript(&keyhash1)); + assert_eq!(pks1.len(), 1); + assert!(pks1.contains(&keyhash1)); + + let pks2 = get_all_pubkeys_from_script( + &NativeScript::new_timelock_start( + &TimelockStart::new(123), + ), + ); + assert_eq!(pks2.len(), 0); + + let pks3 = get_all_pubkeys_from_script( + &NativeScript::new_script_all( + &ScriptAll::new(&scripts_vec(vec![ + &pkscript(&keyhash1), + &pkscript(&keyhash2), + ])) + ), + ); + assert_eq!(pks3.len(), 2); + assert!(pks3.contains(&keyhash1)); + assert!(pks3.contains(&keyhash2)); + + let pks4 = get_all_pubkeys_from_script( + &NativeScript::new_script_any( + &ScriptAny::new(&scripts_vec(vec![ + &NativeScript::new_script_n_of_k(&ScriptNOfK::new( + 1, + &scripts_vec(vec![ + &NativeScript::new_timelock_start(&TimelockStart::new(132)), + &pkscript(&keyhash3), + ]), + )), + &NativeScript::new_script_all(&ScriptAll::new( + &scripts_vec(vec![ + &NativeScript::new_timelock_expiry(&TimelockExpiry::new(132)), + &pkscript(&keyhash1), + ]), + )), + &NativeScript::new_script_any(&ScriptAny::new( + &scripts_vec(vec![ + &pkscript(&keyhash1), + &pkscript(&keyhash2), + &pkscript(&keyhash3), + ]), + )), + ])) + ), + ); + assert_eq!(pks4.len(), 3); + assert!(pks4.contains(&keyhash1)); + assert!(pks4.contains(&keyhash2)); + assert!(pks4.contains(&keyhash3)); + } } diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index 0a7d99eb..9285f99d 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -1,7 +1,7 @@ use super::*; use super::fees; use super::utils; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; // comes from witsVKeyNeeded in the Ledger spec fn witness_keys_for_cert(cert_enum: &Certificate, keys: &mut BTreeSet) { @@ -46,10 +46,15 @@ fn fake_private_key() -> Bip32PrivateKey { ).unwrap() } -fn fake_key_hash() -> Ed25519KeyHash { - Ed25519KeyHash::from_bytes( - vec![142, 239, 181, 120, 142, 135, 19, 200, 68, 223, 211, 43, 46, 145, 222, 30, 48, 159, 239, 255, 213, 85, 248, 39, 204, 158, 225, 100] - ).unwrap() +fn count_needed_vkeys(tx_builder: &TransactionBuilder) -> usize { + let input_hashes = &tx_builder.input_types.vkeys; + match &tx_builder.mint_scripts { + None => input_hashes.len(), + Some(scripts) => { + // Union all input keys with minting keys + input_hashes.union(&get_all_pubkeys_from_scripts(scripts)).count() + } + } } // tx_body must be the result of building from tx_builder @@ -57,19 +62,18 @@ fn fake_key_hash() -> Ed25519KeyHash { // for use in calculating the size of the final Transaction fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Result { let fake_key_root = fake_private_key(); - let fake_key_hash = fake_key_hash(); - // recall: this includes keys for input, certs and withdrawals - let vkeys = match tx_builder.input_types.vkeys.len() { + let vkeys = match count_needed_vkeys(tx_builder) { 0 => None, x => { - let mut result = Vkeywitnesses::new(); let raw_key = fake_key_root.to_raw_key(); + let fake_vkey_witness = Vkeywitness::new( + &Vkey::new(&raw_key.to_public()), + &raw_key.sign([1u8; 100].as_ref()) + ); + let mut result = Vkeywitnesses::new(); for _i in 0..x { - result.add(&Vkeywitness::new( - &Vkey::new(&raw_key.to_public()), - &raw_key.sign([1u8; 100].as_ref()) - )); + result.add(&fake_vkey_witness.clone()); } Some(result) }, @@ -96,21 +100,20 @@ fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Resul Some(result) }, }; - let full_script_keys = match &tx_builder.mint { + let full_script_keys = match &tx_builder.mint_scripts { None => script_keys, - Some(mint) => { + Some(witness_scripts) => { let mut ns = script_keys - .map(|sk| { sk.clone() }) + .map(|x| { x.clone() }) .unwrap_or(NativeScripts::new()); - let spk = ScriptPubkey::new(&fake_key_hash); - mint.keys().0.iter().for_each(|_p| { - ns.add(&NativeScript::new_script_pubkey(&spk)); + witness_scripts.0.iter().for_each(|s| { + ns.add(s); }); Some(ns) } }; let witness_set = TransactionWitnessSet { - vkeys: vkeys, + vkeys, native_scripts: full_script_keys, bootstraps: bootstrap_keys, // TODO: plutus support? @@ -126,7 +129,33 @@ fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Resul }) } +fn assert_required_mint_scripts(mint: &Mint, maybe_mint_scripts: Option<&NativeScripts>) -> Result<(), JsError> { + if let Some(mint_scripts) = maybe_mint_scripts { + let witness_hashes: HashSet = witness_scripts.0.iter().map(|script| { + script.hash(ScriptHashNamespace::NativeScript) + }).collect(); + for mint_hash in mint.keys().0.iter() { + if !witness_hashes.contains(mint_hash) { + return Err(JsError::from_str( + &format!( + "No witness script is found for mint policy '{:?}'! Script is required!", + hex::encode(mint_hash.to_bytes()), + )) + ); + } + } + } else { + return Err(JsError::from_str( + "Mint is present in the builder, but witness scripts are not provided!", + )); + } + Ok(()) +} + fn min_fee(tx_builder: &TransactionBuilder) -> Result { + if let Some(mint) = tx_builder.mint.as_ref() { + assert_required_mint_scripts(mint, tx_builder.mint_scripts.as_ref())?; + } let full_tx = fake_full_tx(tx_builder, tx_builder.build()?)?; fees::min_fee(&full_tx, &tx_builder.config.fee_algo) } @@ -260,6 +289,7 @@ pub struct TransactionBuilder { validity_start_interval: Option, input_types: MockWitnessSet, mint: Option, + mint_scripts: Option, inputs_auto_added: bool, } @@ -636,31 +666,71 @@ impl TransactionBuilder { Ok(()) } - /// Set explicit Mint object to this builder - /// it will replace any previously existing mint - pub fn set_mint(&mut self, mint: &Mint) { + /// Set explicit Mint object and the required witnesses to this builder + /// it will replace any previously existing mint and mint scripts + /// NOTE! Error will be returned in case a mint policy does not have a matching script + pub fn set_mint(&mut self, mint: &Mint, mint_scripts: &NativeScripts) -> Result<(), JsError> { + assert_required_mint_scripts(mint, Some(mint_scripts))?; self.mint = Some(mint.clone()); + self.mint_scripts = Some(mint_scripts.clone()); + Ok(()) + } + + /// Returns a copy of the current mint state in the builder + pub fn get_mint(&self) -> Option { + self.mint.clone() + } + + /// Returns a copy of the current mint witness scripts in the builder + pub fn get_mint_scripts(&self) -> Option { + self.mint_scripts.clone() + } + + fn _set_mint_asset(&mut self, policy_id: &PolicyID, policy_script: &NativeScript, mint_assets: &MintAssets) { + let mut mint = self.mint.as_ref().cloned().unwrap_or(Mint::new()); + let is_new_policy = mint.insert(&policy_id, mint_assets).is_none(); + let mint_scripts = { + let mut witness_scripts = self.mint_scripts.as_ref().cloned() + .unwrap_or(NativeScripts::new()); + if is_new_policy { + // If policy has not been encountered before - insert the script into witnesses + witness_scripts.add(&policy_script.clone()); + } + witness_scripts + }; + self.set_mint(&mint, &mint_scripts); + if is_new_policy { + // If policy has not been encountered before - insert the script into witnesses + let mut witness_scripts = self.mint_scripts.as_ref().cloned() + .unwrap_or(NativeScripts::new()); + witness_scripts.add(&policy_script.clone()); + self.set_mint_scripts(&witness_scripts); + } } /// Add a mint entry to this builder using a PolicyID and MintAssets object /// It will be securely added to existing or new Mint in this builder /// It will replace any existing mint assets with the same PolicyID - pub fn set_mint_asset(&mut self, policy_id: &PolicyID, mint_assets: &MintAssets) { - let mut mint = self.mint.as_ref().cloned().unwrap_or(Mint::new()); - mint.insert(policy_id, mint_assets); - self.set_mint(&mint); + pub fn set_mint_asset(&mut self, policy_script: &NativeScript, mint_assets: &MintAssets) { + let policy_id: PolicyID = policy_script.hash(ScriptHashNamespace::NativeScript); + self._set_mint_asset(&policy_id, policy_script, mint_assets); } - /// Add a mint entry to this builder using a PolicyID, AssetName, and Int object for amount - /// It will be securely added to existing or new Mint in this builder - /// It will replace any previous existing amount same PolicyID and AssetName - pub fn add_mint_asset(&mut self, policy_id: &PolicyID, asset_name: &AssetName, amount: Int) { + fn _add_mint_asset(&mut self, policy_id: &PolicyID, policy_script: &NativeScript, asset_name: &AssetName, amount: Int) { let mut asset = self.mint.as_ref() - .map(|m| { m.get(policy_id).as_ref().cloned() }) + .map(|m| { m.get(&policy_id).as_ref().cloned() }) .unwrap_or(None) .unwrap_or(MintAssets::new()); asset.insert(asset_name, amount); - self.set_mint_asset(policy_id, &asset); + self._set_mint_asset(&policy_id, policy_script, &asset); + } + + /// Add a mint entry to this builder using a PolicyID, AssetName, and Int object for amount + /// It will be securely added to existing or new Mint in this builder + /// It will replace any previous existing amount same PolicyID and AssetName + pub fn add_mint_asset(&mut self, policy_script: &NativeScript, asset_name: &AssetName, amount: Int) { + let policy_id: PolicyID = policy_script.hash(ScriptHashNamespace::NativeScript); + self._add_mint_asset(&policy_id, policy_script, asset_name, amount); } /// Add a mint entry together with an output to this builder @@ -669,7 +739,7 @@ impl TransactionBuilder { /// A new output will be added with the specified Address, the Coin value, and the minted asset pub fn add_mint_asset_and_output( &mut self, - policy_id: &PolicyID, + policy_script: &NativeScript, asset_name: &AssetName, amount: Int, address: &Address, @@ -678,9 +748,10 @@ impl TransactionBuilder { if !amount.is_positive() { return Err(JsError::from_str("Output value must be positive!")); } - self.add_mint_asset(policy_id, asset_name, amount.clone()); + let policy_id: PolicyID = policy_script.hash(ScriptHashNamespace::NativeScript); + self._add_mint_asset(&policy_id, policy_script, asset_name, amount.clone()); let multiasset = Mint::new_from_entry( - policy_id, + &policy_id, &MintAssets::new_from_entry(asset_name, amount.clone()) ).as_positive_multiasset(); self.add_output_coin_and_asset(address, output_coin, &multiasset) @@ -693,7 +764,7 @@ impl TransactionBuilder { /// The output will be set to contain the minimum required amount of Coin pub fn add_mint_asset_and_output_min_required_coin( &mut self, - policy_id: &PolicyID, + policy_script: &NativeScript, asset_name: &AssetName, amount: Int, address: &Address, @@ -701,9 +772,10 @@ impl TransactionBuilder { if !amount.is_positive() { return Err(JsError::from_str("Output value must be positive!")); } - self.add_mint_asset(policy_id, asset_name, amount.clone()); + let policy_id: PolicyID = policy_script.hash(ScriptHashNamespace::NativeScript); + self._add_mint_asset(&policy_id, policy_script, asset_name, amount.clone()); let multiasset = Mint::new_from_entry( - policy_id, + &policy_id, &MintAssets::new_from_entry(asset_name, amount.clone()) ).as_positive_multiasset(); self.add_output_asset_and_min_required_coin(address, &multiasset) @@ -726,6 +798,7 @@ impl TransactionBuilder { }, validity_start_interval: None, mint: None, + mint_scripts: None, inputs_auto_added: false, } } @@ -1049,7 +1122,7 @@ impl TransactionBuilder { /// Returns object the body of the new transaction /// Auxiliary data itself is not included - /// You can use `get_auxiliary_date` or `build_tx` + /// You can use `get_auxiliary_data` or `build_tx` pub fn build(&self) -> Result { let (body, full_tx_size) = self.build_and_size()?; if full_tx_size > self.config.max_tx_size as usize { @@ -1063,13 +1136,21 @@ impl TransactionBuilder { } } + fn get_witness_set(&self) -> TransactionWitnessSet { + let mut wit = TransactionWitnessSet::new(); + if let Some(scripts) = self.mint_scripts.as_ref() { + wit.set_native_scripts(scripts); + } + wit + } + /// Returns full Transaction object with the body and the auxiliary data - /// NOTE: witness_set is set to just empty set + /// NOTE: witness_set will contain all mint_scripts if any been added or set /// NOTE: is_valid set to true pub fn build_tx(&self) -> Result { Ok(Transaction { body: self.build()?, - witness_set: TransactionWitnessSet::new(), + witness_set: self.get_witness_set(), is_valid: true, auxiliary_data: self.auxiliary_data.clone(), }) @@ -1105,6 +1186,12 @@ mod tests { Bip32PrivateKey::from_bip39_entropy(&entropy, &[]) } + fn fake_key_hash(x: u8) -> Ed25519KeyHash { + Ed25519KeyHash::from_bytes( + vec![x, 239, 181, 120, 142, 135, 19, 200, 68, 223, 211, 43, 46, 145, 222, 30, 48, 159, 239, 255, 213, 85, 248, 39, 204, 158, 225, 100] + ).unwrap() + } + fn harden(index: u32) -> u32 { index | 0x80_00_00_00 } @@ -1656,12 +1743,12 @@ mod tests { ) .to_address(); - let policy_id = PolicyID::from([0u8; 28]); + let (min_script, policy_id) = mint_script_and_policy(0); let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); let amount = to_bignum(1234); // Adding mint of the asset - which should work as an input - tx_builder.add_mint_asset(&policy_id, &name, Int::new(&amount)); + tx_builder.add_mint_asset(&min_script, &name, Int::new(&amount)); let mut ass = Assets::new(); ass.insert(&name, &amount); @@ -1739,14 +1826,14 @@ mod tests { ) .to_address(); - let policy_id = PolicyID::from([0u8; 28]); + let (min_script, policy_id) = mint_script_and_policy(0); let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); let amount_minted = to_bignum(1000); let amount_sent = to_bignum(500); // Adding mint of the asset - which should work as an input - tx_builder.add_mint_asset(&policy_id, &name, Int::new(&amount_minted)); + tx_builder.add_mint_asset(&min_script, &name, Int::new(&amount_minted)); let mut ass = Assets::new(); ass.insert(&name, &amount_sent); @@ -3135,72 +3222,110 @@ mod tests { assert_eq!(result_asset.get(&create_asset_name()).unwrap(), Int::new_i32(1234)); } + fn mint_script_and_policy_and_hash(x: u8) -> (NativeScript, PolicyID, Ed25519KeyHash) { + let hash = fake_key_hash(x); + let mint_script = NativeScript::new_script_pubkey( + &ScriptPubkey::new(&hash) + ); + let policy_id = mint_script.hash(ScriptHashNamespace::NativeScript); + (mint_script, policy_id, hash) + } + + fn mint_script_and_policy(x: u8) -> (NativeScript, PolicyID) { + let (m, p, _) = mint_script_and_policy_and_hash(x); + (m, p) + } + #[test] fn set_mint_asset_with_empty_mint() { let mut tx_builder = create_default_tx_builder(); - let policy_id = PolicyID::from([0u8; 28]); - tx_builder.set_mint_asset(&policy_id, &create_mint_asset()); + let (mint_script, policy_id) = mint_script_and_policy(0); + tx_builder.set_mint_asset(&mint_script, &create_mint_asset()); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.unwrap(); + let mint_scripts = tx_builder.mint_scripts.unwrap(); assert_eq!(mint.len(), 1); assert_mint_asset(&mint, &policy_id); + + assert_eq!(mint_scripts.len(), 1); + assert_eq!(mint_scripts.get(0), mint_script); } #[test] fn set_mint_asset_with_existing_mint() { let mut tx_builder = create_default_tx_builder(); - let policy_id1 = PolicyID::from([0u8; 28]); + let (_, policy_id1) = mint_script_and_policy(0); + let (mint_script2, policy_id2) = mint_script_and_policy(1); + tx_builder.set_mint(&create_mint_with_one_asset(&policy_id1)); - let policy_id2 = PolicyID::from([1u8; 28]); - tx_builder.set_mint_asset(&policy_id2, &create_mint_asset()); + tx_builder.set_mint_asset(&mint_script2, &create_mint_asset()); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.unwrap(); + let mint_scripts = tx_builder.mint_scripts.unwrap(); assert_eq!(mint.len(), 2); assert_mint_asset(&mint, &policy_id1); assert_mint_asset(&mint, &policy_id2); + + // Only second script is present in the scripts + assert_eq!(mint_scripts.len(), 1); + assert_eq!(mint_scripts.get(0), mint_script2); } #[test] fn add_mint_asset_with_empty_mint() { let mut tx_builder = create_default_tx_builder(); - let policy_id = PolicyID::from([0u8; 28]); - tx_builder.add_mint_asset(&policy_id, &create_asset_name(), Int::new_i32(1234)); + let (mint_script, policy_id) = mint_script_and_policy(0); + + tx_builder.add_mint_asset(&mint_script, &create_asset_name(), Int::new_i32(1234)); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.unwrap(); + let mint_scripts = tx_builder.mint_scripts.unwrap(); assert_eq!(mint.len(), 1); assert_mint_asset(&mint, &policy_id); + + assert_eq!(mint_scripts.len(), 1); + assert_eq!(mint_scripts.get(0), mint_script); } #[test] fn add_mint_asset_with_existing_mint() { let mut tx_builder = create_default_tx_builder(); - let policy_id1 = PolicyID::from([0u8; 28]); - tx_builder.set_mint(&create_mint_with_one_asset(&policy_id1)); + let (_, policy_id1) = mint_script_and_policy(0); + let (mint_script2, policy_id2) = mint_script_and_policy(1); - let policy_id2 = PolicyID::from([1u8; 28]); - tx_builder.add_mint_asset(&policy_id2, &create_asset_name(), Int::new_i32(1234)); + tx_builder.set_mint(&create_mint_with_one_asset(&policy_id1)); + tx_builder.add_mint_asset(&mint_script2, &create_asset_name(), Int::new_i32(1234)); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.unwrap(); + let mint_scripts = tx_builder.mint_scripts.unwrap(); assert_eq!(mint.len(), 2); assert_mint_asset(&mint, &policy_id1); assert_mint_asset(&mint, &policy_id2); + + // Only second script is present in the scripts + assert_eq!(mint_scripts.len(), 1); + assert_eq!(mint_scripts.get(0), mint_script2); } #[test] @@ -3280,8 +3405,9 @@ mod tests { fn add_mint_asset_and_output() { let mut tx_builder = create_default_tx_builder(); - let policy_id0 = PolicyID::from([0u8; 28]); - let policy_id1 = PolicyID::from([1u8; 28]); + let (mint_script0, policy_id0) = mint_script_and_policy(0); + let (mint_script1, policy_id1) = mint_script_and_policy(1); + let name = create_asset_name(); let amount = Int::new_i32(1234); @@ -3289,10 +3415,10 @@ mod tests { let coin = to_bignum(100); // Add unrelated mint first to check it is NOT added to output later - tx_builder.add_mint_asset(&policy_id0, &name, amount.clone()); + tx_builder.add_mint_asset(&mint_script0, &name, amount.clone()); tx_builder.add_mint_asset_and_output( - &policy_id1, + &mint_script1, &name, amount.clone(), &address, @@ -3300,14 +3426,20 @@ mod tests { ).unwrap(); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.as_ref().unwrap(); + let mint_scripts = tx_builder.mint_scripts.as_ref().unwrap(); // Mint contains two entries assert_eq!(mint.len(), 2); assert_mint_asset(mint, &policy_id0); assert_mint_asset(mint, &policy_id1); + assert_eq!(mint_scripts.len(), 2); + assert_eq!(mint_scripts.get(0), mint_script0); + assert_eq!(mint_scripts.get(1), mint_script1); + // One new output is created assert_eq!(tx_builder.outputs.len(), 1); let out = tx_builder.outputs.get(0); @@ -3331,32 +3463,40 @@ mod tests { fn add_mint_asset_and_min_required_coin() { let mut tx_builder = create_reallistic_tx_builder(); - let policy_id0 = PolicyID::from([0u8; 28]); - let policy_id1 = PolicyID::from([1u8; 28]); + let (mint_script0, policy_id0) = mint_script_and_policy(0); + let (mint_script1, policy_id1) = mint_script_and_policy(1); + let name = create_asset_name(); let amount = Int::new_i32(1234); let address = byron_address(); // Add unrelated mint first to check it is NOT added to output later - tx_builder.add_mint_asset(&policy_id0, &name, amount.clone()); + tx_builder.add_mint_asset(&mint_script0, &name, amount.clone()); tx_builder.add_mint_asset_and_output_min_required_coin( - &policy_id1, + &mint_script1, &name, amount.clone(), &address, ).unwrap(); assert!(tx_builder.mint.is_some()); + assert!(tx_builder.mint_scripts.is_some()); let mint = tx_builder.mint.as_ref().unwrap(); + let mint_scripts = tx_builder.mint_scripts.as_ref().unwrap(); // Mint contains two entries assert_eq!(mint.len(), 2); assert_mint_asset(mint, &policy_id0); assert_mint_asset(mint, &policy_id1); + + assert_eq!(mint_scripts.len(), 2); + assert_eq!(mint_scripts.get(0), mint_script0); + assert_eq!(mint_scripts.get(1), mint_script1); + // One new output is created assert_eq!(tx_builder.outputs.len(), 1); let out = tx_builder.outputs.get(0); @@ -3376,66 +3516,130 @@ mod tests { assert_eq!(asset.get(&name).unwrap(), to_bignum(1234)); } - #[test] fn add_mint_includes_witnesses_into_fee_estimation() { + let mut tx_builder = create_reallistic_tx_builder(); - let original_tx_fee = tx_builder.min_fee().unwrap(); - assert_eq!(original_tx_fee, to_bignum(156217)); + let hash0 = fake_key_hash(0); + + let (mint_script1, _, hash1) = mint_script_and_policy_and_hash(1); + let (mint_script2, _, _) = mint_script_and_policy_and_hash(2); + let (mint_script3, _, _) = mint_script_and_policy_and_hash(3); - let policy_id1 = PolicyID::from([0u8; 28]); - let policy_id2 = PolicyID::from([1u8; 28]); - let policy_id3 = PolicyID::from([2u8; 28]); let name1 = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); let name2 = AssetName::new(vec![1u8, 1, 2, 3]).unwrap(); let name3 = AssetName::new(vec![2u8, 1, 2, 3]).unwrap(); let name4 = AssetName::new(vec![3u8, 1, 2, 3]).unwrap(); let amount = Int::new_i32(1234); - let mut mint = Mint::new(); - mint.insert( - &policy_id1, - &MintAssets::new_from_entry(&name1, amount.clone()), + // One input from unrelated address + tx_builder.add_key_input( + &hash0, + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(10_000_000)), ); - mint.insert( - &policy_id2, - &MintAssets::new_from_entry(&name2, amount.clone()), + + // One input from same address as mint + tx_builder.add_key_input( + &hash1, + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(10_000_000)), ); - // Third policy with two asset names - let mut mass = MintAssets::new_from_entry(&name3, amount.clone()); - mass.insert(&name4, amount.clone()); - mint.insert(&policy_id3, &mass); + // Original tx fee now assumes two VKey signatures for two inputs + let original_tx_fee = tx_builder.min_fee().unwrap(); + assert_eq!(original_tx_fee, to_bignum(168361)); + + // Add minting four assets from three different policies + tx_builder.add_mint_asset(&mint_script1, &name1, amount.clone()); + tx_builder.add_mint_asset(&mint_script2, &name2, amount.clone()); + tx_builder.add_mint_asset(&mint_script3, &name3, amount.clone()); + tx_builder.add_mint_asset(&mint_script3, &name4, amount.clone()); + + let mint = tx_builder.get_mint().unwrap(); let mint_len = mint.to_bytes().len(); + + let mint_scripts = tx_builder.get_witness_set(); + let mint_scripts_len = mint_scripts.to_bytes().len() + - TransactionWitnessSet::new().to_bytes().len(); + let fee_coefficient = tx_builder.config.fee_algo.coefficient(); let raw_mint_fee = fee_coefficient .checked_mul(&to_bignum(mint_len as u64)) .unwrap(); - assert_eq!(raw_mint_fee, to_bignum(5544)); + let raw_mint_script_fee = fee_coefficient + .checked_mul(&to_bignum(mint_scripts_len as u64)) + .unwrap(); - tx_builder.set_mint(&mint); + assert_eq!(raw_mint_fee, to_bignum(5544)); + assert_eq!(raw_mint_script_fee, to_bignum(4312)); let new_tx_fee = tx_builder.min_fee().unwrap(); - let fee_diff_from_adding_mint = - new_tx_fee.checked_sub(&original_tx_fee) + let fee_diff_from_adding_mint = new_tx_fee + .checked_sub(&original_tx_fee) .unwrap(); - let witness_fee_increase = - fee_diff_from_adding_mint.checked_sub(&raw_mint_fee) - .unwrap(); + let witness_fee_increase = fee_diff_from_adding_mint + .checked_sub(&raw_mint_fee).unwrap() + .checked_sub(&raw_mint_script_fee).unwrap(); - assert_eq!(witness_fee_increase, to_bignum(4356)); + assert_eq!(witness_fee_increase, to_bignum(8932)); let fee_increase_bytes = from_bignum(&witness_fee_increase) .checked_div(from_bignum(&fee_coefficient)) .unwrap(); - // Three policy IDs of 32 bytes each + 3 byte overhead - assert_eq!(fee_increase_bytes, 99); + // Two vkey witnesses 96 bytes each (32 byte pubkey + 64 byte signature) + // Plus 11 bytes overhead for CBOR wrappers + // This is happening because we have three different minting policies + // but the same key-hash from one of them is already also used in inputs + // so no suplicate witness signature is require for that one + assert_eq!(fee_increase_bytes, 203); + } + + #[test] + fn fee_estimation_fails_on_missing_mint_scripts() { + let mut tx_builder = create_reallistic_tx_builder(); + + // No error estimating fee without mint + assert!(tx_builder.min_fee().is_ok()); + + let (mint_script1, policy_id1) = mint_script_and_policy(0); + let (mint_script2, _) = mint_script_and_policy(1); + + let name1 = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); + let amount = Int::new_i32(1234); + + let mut mint = Mint::new(); + mint.insert( + &policy_id1, + &MintAssets::new_from_entry(&name1, amount.clone()), + ); + + tx_builder.set_mint(&mint); + + // Mint exists but no witness scripts at all present + let est1 = tx_builder.min_fee(); + assert!(est1.is_err()); + assert!(est1.err().unwrap().to_string().contains("witness scripts are not provided")); + + tx_builder.add_mint_asset(&mint_script2, &name1, amount.clone()); + + // Now two different policies are minted but only one witness script is present + let est2 = tx_builder.min_fee(); + assert!(est2.is_err()); + assert!(est2.err().unwrap().to_string().contains(&format!("{:?}", hex::encode(policy_id1.to_bytes())))); + + let mut scripts = tx_builder.get_mint_scripts().unwrap(); + scripts.add(&mint_script1); + tx_builder.set_mint_scripts(&scripts); + + let est3 = tx_builder.min_fee(); + assert!(est3.is_ok()) } #[test] @@ -3449,8 +3653,9 @@ mod tests { .derive(0) .to_public(); - let policy_id1 = &PolicyID::from([0u8; 28]); - let policy_id2 = &PolicyID::from([1u8; 28]); + let (mint_script1, policy_id1) = mint_script_and_policy(0); + let (mint_script2, policy_id2) = mint_script_and_policy(1); + let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); let ma_input1 = 100; @@ -3461,12 +3666,12 @@ mod tests { .iter() .map(|input| { let mut multiasset = MultiAsset::new(); - multiasset.insert(policy_id1, &{ + multiasset.insert(&policy_id1, &{ let mut assets = Assets::new(); assets.insert(&name, &to_bignum(*input)); assets }); - multiasset.insert(policy_id2, &{ + multiasset.insert(&policy_id2, &{ let mut assets = Assets::new(); assets.insert(&name, &to_bignum(*input)); assets @@ -3493,18 +3698,18 @@ mod tests { assert_eq!(total_input_before_mint.coin, to_bignum(300)); let ma1 = total_input_before_mint.multiasset.unwrap(); - assert_eq!(ma1.get(policy_id1).unwrap().get(&name).unwrap(), to_bignum(360)); - assert_eq!(ma1.get(policy_id2).unwrap().get(&name).unwrap(), to_bignum(360)); + assert_eq!(ma1.get(&policy_id1).unwrap().get(&name).unwrap(), to_bignum(360)); + assert_eq!(ma1.get(&policy_id2).unwrap().get(&name).unwrap(), to_bignum(360)); - tx_builder.add_mint_asset(policy_id1, &name, Int::new_i32(40)); - tx_builder.add_mint_asset(policy_id2, &name, Int::new_i32(-40)); + tx_builder.add_mint_asset(&mint_script1, &name, Int::new_i32(40)); + tx_builder.add_mint_asset(&mint_script2, &name, Int::new_i32(-40)); let total_input_after_mint = tx_builder.get_total_input().unwrap(); assert_eq!(total_input_after_mint.coin, to_bignum(300)); let ma2 = total_input_after_mint.multiasset.unwrap(); - assert_eq!(ma2.get(policy_id1).unwrap().get(&name).unwrap(), to_bignum(400)); - assert_eq!(ma2.get(policy_id2).unwrap().get(&name).unwrap(), to_bignum(320)); + assert_eq!(ma2.get(&policy_id1).unwrap().get(&name).unwrap(), to_bignum(400)); + assert_eq!(ma2.get(&policy_id2).unwrap().get(&name).unwrap(), to_bignum(320)); } }