diff --git a/package-lock.json b/package-lock.json index 30ba3b23..041eb50a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cardano-serialization-lib", - "version": "10.1.0", + "version": "10.2.0-beta.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3e48fe20..a20a8056 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cardano-serialization-lib", - "version": "10.1.0", + "version": "10.2.0-beta.5", "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 847ce662..e2928dca 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -52,7 +52,7 @@ checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" [[package]] name = "cardano-serialization-lib" -version = "10.1.0" +version = "10.2.0-beta.5" dependencies = [ "bech32", "cbor_event", @@ -68,6 +68,7 @@ dependencies = [ "linked-hash-map", "noop_proc_macro", "num-bigint", + "num-integer", "quickcheck", "quickcheck_macros", "rand 0.8.4", @@ -288,9 +289,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg 1.0.1", "num-traits", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 970c93ac..54cb4b96 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardano-serialization-lib" -version = "10.1.0" +version = "10.2.0-beta.5" edition = "2018" authors = ["EMURGO"] license = "MIT" @@ -27,6 +27,7 @@ cfg-if = "1" linked-hash-map = "0.5.3" serde_json = "1.0.57" num-bigint = "0.4.0" +num-integer = "0.1.45" # The default can't be compiled to wasm, so it's necessary to use either the 'nightly' # feature or this one clear_on_drop = { version = "0.2", features = ["no_cc"] } diff --git a/rust/pkg/cardano_serialization_lib.js.flow b/rust/pkg/cardano_serialization_lib.js.flow index 9955fb1f..9b0de798 100644 --- a/rust/pkg/cardano_serialization_lib.js.flow +++ b/rust/pkg/cardano_serialization_lib.js.flow @@ -172,6 +172,26 @@ declare export function decode_metadatum_to_json_str( */ declare export function min_fee(tx: Transaction, linear_fee: LinearFee): BigNum; +/** + * @param {ExUnits} ex_units + * @param {ExUnitPrices} ex_unit_prices + * @returns {BigNum} + */ +declare export function calculate_ex_units_ceil_cost( + ex_units: ExUnits, + ex_unit_prices: ExUnitPrices +): BigNum; + +/** + * @param {Transaction} tx + * @param {ExUnitPrices} ex_unit_prices + * @returns {BigNum} + */ +declare export function min_script_fee( + tx: Transaction, + ex_unit_prices: ExUnitPrices +): BigNum; + /** * @param {string} password * @param {string} salt @@ -648,6 +668,11 @@ declare export class BigInt { */ static from_bytes(bytes: Uint8Array): BigInt; + /** + * @returns {boolean} + */ + is_zero(): boolean; + /** * @returns {BigNum | void} */ @@ -663,6 +688,34 @@ declare export class BigInt { * @returns {string} */ to_str(): string; + + /** + * @param {BigInt} other + * @returns {BigInt} + */ + add(other: BigInt): BigInt; + + /** + * @param {BigInt} other + * @returns {BigInt} + */ + mul(other: BigInt): BigInt; + + /** + * @returns {BigInt} + */ + static one(): BigInt; + + /** + * @returns {BigInt} + */ + increment(): BigInt; + + /** + * @param {BigInt} other + * @returns {BigInt} + */ + div_ceil(other: BigInt): BigInt; } /** */ @@ -696,6 +749,11 @@ declare export class BigNum { */ static zero(): BigNum; + /** + * @returns {BigNum} + */ + static one(): BigNum; + /** * @returns {boolean} */ @@ -1533,6 +1591,11 @@ declare export class Ed25519KeyHashes { * @param {Ed25519KeyHash} elem */ add(elem: Ed25519KeyHash): void; + + /** + * @returns {Ed25519KeyHashes | void} + */ + to_option(): Ed25519KeyHashes | void; } /** */ @@ -3052,6 +3115,13 @@ declare export class PlutusData { constr_plutus_data: ConstrPlutusData ): PlutusData; + /** + * Same as `.new_constr_plutus_data` but creates constr with empty data list + * @param {BigNum} alternative + * @returns {PlutusData} + */ + static new_empty_constr_plutus_data(alternative: BigNum): PlutusData; + /** * @param {PlutusMap} map * @returns {PlutusData} @@ -4258,6 +4328,11 @@ declare export class Redeemers { * @param {Redeemer} elem */ add(elem: Redeemer): void; + + /** + * @returns {ExUnits} + */ + total_ex_units(): ExUnits; } /** */ @@ -5292,6 +5367,16 @@ declare export class TransactionBuilder { */ add_inputs_from(inputs: TransactionUnspentOutputs, strategy: number): void; + /** + * @param {TxInputsBuilder} inputs + */ + set_inputs(inputs: TxInputsBuilder): void; + + /** + * @param {TxInputsBuilder} collateral + */ + set_collateral(collateral: TxInputsBuilder): void; + /** * We have to know what kind of inputs these are to know what kind of mock witnesses to create since * 1) mock witnesses have different lengths depending on the type which changes the expecting fee @@ -5684,6 +5769,11 @@ declare export class TransactionBuilder { */ remove_script_data_hash(): void; + /** + * @param {Ed25519KeyHash} key + */ + add_required_signer(key: Ed25519KeyHash): void; + /** * @returns {number} */ @@ -5754,6 +5844,12 @@ declare export class TransactionBuilderConfigBuilder { coins_per_utxo_word: BigNum ): TransactionBuilderConfigBuilder; + /** + * @param {ExUnitPrices} ex_unit_prices + * @returns {TransactionBuilderConfigBuilder} + */ + ex_unit_prices(ex_unit_prices: ExUnitPrices): TransactionBuilderConfigBuilder; + /** * @param {BigNum} pool_deposit * @returns {TransactionBuilderConfigBuilder} @@ -6346,6 +6442,158 @@ declare export class TxBuilderConstants { */ static plutus_default_cost_models(): Costmdls; } +/** + */ +declare export class TxInputsBuilder { + free(): void; + + /** + * @returns {TxInputsBuilder} + */ + static new(): TxInputsBuilder; + + /** + * We have to know what kind of inputs these are to know what kind of mock witnesses to create since + * 1) mock witnesses have different lengths depending on the type which changes the expecting fee + * 2) Witnesses are a set so we need to get rid of duplicates to avoid over-estimating the fee + * @param {Ed25519KeyHash} hash + * @param {TransactionInput} input + * @param {Value} amount + */ + add_key_input( + hash: Ed25519KeyHash, + input: TransactionInput, + amount: Value + ): void; + + /** + * This method adds the input to the builder BUT leaves a missing spot for the witness native script + * + * After adding the input with this method, use `.add_required_native_input_scripts` + * and `.add_required_plutus_input_scripts` to add the witness scripts + * + * Or instead use `.add_native_script_input` and `.add_plutus_script_input` + * to add inputs right along with the script, instead of the script hash + * @param {ScriptHash} hash + * @param {TransactionInput} input + * @param {Value} amount + */ + add_script_input( + hash: ScriptHash, + input: TransactionInput, + amount: Value + ): void; + + /** + * This method will add the input to the builder and also register the required native script witness + * @param {NativeScript} script + * @param {TransactionInput} input + * @param {Value} amount + */ + add_native_script_input( + script: NativeScript, + input: TransactionInput, + amount: Value + ): void; + + /** + * This method will add the input to the builder and also register the required plutus witness + * @param {PlutusWitness} witness + * @param {TransactionInput} input + * @param {Value} amount + */ + add_plutus_script_input( + witness: PlutusWitness, + input: TransactionInput, + amount: Value + ): void; + + /** + * @param {ByronAddress} hash + * @param {TransactionInput} input + * @param {Value} amount + */ + add_bootstrap_input( + hash: ByronAddress, + input: TransactionInput, + amount: Value + ): void; + + /** + * Note that for script inputs this method will use underlying generic `.add_script_input` + * which leaves a required empty spot for the script witness (or witnesses in case of Plutus). + * You can use `.add_native_script_input` or `.add_plutus_script_input` directly to register the input along with the witness. + * @param {Address} address + * @param {TransactionInput} input + * @param {Value} amount + */ + add_input(address: Address, input: TransactionInput, amount: Value): void; + + /** + * Returns the number of still missing input scripts (either native or plutus) + * Use `.add_required_native_input_scripts` or `.add_required_plutus_input_scripts` to add the missing scripts + * @returns {number} + */ + count_missing_input_scripts(): number; + + /** + * Try adding the specified scripts as witnesses for ALREADY ADDED script inputs + * Any scripts that don't match any of the previously added inputs will be ignored + * Returns the number of remaining required missing witness scripts + * Use `.count_missing_input_scripts` to find the number of still missing scripts + * @param {NativeScripts} scripts + * @returns {number} + */ + add_required_native_input_scripts(scripts: NativeScripts): number; + + /** + * Try adding the specified scripts as witnesses for ALREADY ADDED script inputs + * Any scripts that don't match any of the previously added inputs will be ignored + * Returns the number of remaining required missing witness scripts + * Use `.count_missing_input_scripts` to find the number of still missing scripts + * @param {PlutusWitnesses} scripts + * @returns {number} + */ + add_required_plutus_input_scripts(scripts: PlutusWitnesses): number; + + /** + * Returns a copy of the current script input witness scripts in the builder + * @returns {NativeScripts | void} + */ + get_native_input_scripts(): NativeScripts | void; + + /** + * Returns a copy of the current plutus input witness scripts in the builder. + * NOTE: each plutus witness will be cloned with a specific corresponding input index + * @returns {PlutusWitnesses | void} + */ + get_plutus_input_scripts(): PlutusWitnesses | void; + + /** + * @returns {number} + */ + len(): number; + + /** + * @param {Ed25519KeyHash} key + */ + add_required_signer(key: Ed25519KeyHash): void; + + /** + * @param {Ed25519KeyHashes} keys + */ + add_required_signers(keys: Ed25519KeyHashes): void; + + /** + * @returns {TransactionInputs} + */ + inputs(): TransactionInputs; + + /** + * @returns {TransactionInputs | void} + */ + inputs_option(): TransactionInputs | void; +} /** */ declare export class URL { diff --git a/rust/src/fees.rs b/rust/src/fees.rs index 41dd0a25..9760aafe 100644 --- a/rust/src/fees.rs +++ b/rust/src/fees.rs @@ -33,6 +33,56 @@ pub fn min_fee(tx: &Transaction, linear_fee: &LinearFee) -> Result Result { + type Ratio = (BigInt, BigInt); + fn mult(sc: &SubCoin, x: &BigNum) -> Result { + let n: BigInt = BigInt::from_str(&sc.numerator.to_str())?; + let d: BigInt = BigInt::from_str(&sc.denominator.to_str())?; + let m: BigInt = BigInt::from_str(&x.to_str())?; + Ok((n.mul(&m), d)) + } + fn sum(a: &Ratio, b: &Ratio) -> Ratio { + // Ratio Addition: a/x + b/y = ((a*y) + (b*x))/(x*y) + let (a_num, a_denum) = &a; + let (b_num, b_denum) = &b; + if a_num.is_zero() { + return b.clone(); + } + if b_num.is_zero() { + return a.clone(); + } + let a_num_fixed = &a_num.mul(b_denum); + let b_num_fixed = &b_num.mul(a_denum); + let a_b_num_sum = a_num_fixed.add(b_num_fixed); + let common_denum = a_denum.mul(b_denum); + (a_b_num_sum, common_denum) + } + let mem_ratio: Ratio = mult(&ex_unit_prices.mem_price(), &ex_units.mem())?; + let steps_ratio: Ratio = mult(&ex_unit_prices.step_price(), &ex_units.steps())?; + let (total_num, total_denum) = sum(&mem_ratio, &steps_ratio); + match total_num.div_ceil(&total_denum).as_u64() { + Some(coin) => Ok(coin), + _ => Err(JsError::from_str(&format!( + "Failed to calculate ceil from ratio {}/{}", + total_num.to_str(), + total_denum.to_str(), + ))) + } +} + +#[wasm_bindgen] +pub fn min_script_fee(tx: &Transaction, ex_unit_prices: &ExUnitPrices) -> Result { + if let Some(redeemers) = &tx.witness_set.redeemers { + let total_ex_units: ExUnits = redeemers.total_ex_units()?; + return calculate_ex_units_ceil_cost(&total_ex_units, ex_unit_prices); + } + Ok(Coin::zero()) +} + #[cfg(test)] mod tests { use super::*; @@ -517,4 +567,49 @@ mod tests { "163002" // todo: compare to Haskell fee to make sure the diff is not too big ); } + + fn exunits(mem: u64, steps: u64) -> ExUnits { + ExUnits::new(&to_bignum(mem), &to_bignum(steps)) + } + + fn subcoin(num: u64, denum: u64) -> SubCoin { + SubCoin::new(&to_bignum(num), &to_bignum(denum)) + } + + fn exunit_prices(mem_prices: (u64, u64), step_prices: (u64, u64)) -> ExUnitPrices { + ExUnitPrices::new( + &subcoin(mem_prices.0, mem_prices.1), + &subcoin(step_prices.0, step_prices.1), + ) + } + + fn _calculate_ex_units_ceil_cost(mem: u64, steps: u64, mem_prices: (u64, u64), step_prices: (u64, u64)) -> Coin { + let ex_units = exunits(mem, steps); + let ex_unit_prices = exunit_prices(mem_prices, step_prices); + calculate_ex_units_ceil_cost(&ex_units, &ex_unit_prices).unwrap() + } + + #[test] + fn test_calc_ex_units_cost() { + // 10 * (2/1) + 20 * (3/1) = 10 * 2 + 20 * 3 = 20 + 60 + assert_eq!( + _calculate_ex_units_ceil_cost(10, 20, (2, 1), (3, 1)), + to_bignum(80), + ); + // 22 * (12/6) + 33 * (33/11) = 22 * 2 + 33 * 3 = 44 + 99 = 143 + assert_eq!( + _calculate_ex_units_ceil_cost(22, 33, (12, 6), (33, 11)), + to_bignum(143), + ); + // 10 * (5/7) + 20 * (9/13) = 50/7 + 180/13 = 650/91 + 1260/91 = 1910/91 = ceil(20.98) = 21 + assert_eq!( + _calculate_ex_units_ceil_cost(10, 20, (5, 7), (9, 13)), + to_bignum(21), + ); + // 22 * (7/5) + 33 * (13/9) = 154/5 + 429/9 = 1386/45 + 2145/45 = 3531/45 = ceil(78.46) = 79 + assert_eq!( + _calculate_ex_units_ceil_cost(22, 33, (7, 5), (13, 9)), + to_bignum(79), + ); + } } \ No newline at end of file diff --git a/rust/src/lib.rs b/rust/src/lib.rs index cb9475d2..fa5dba8e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,4 +1,5 @@ #![cfg_attr(feature = "with-bench", feature(test))] +#![allow(deprecated)] #[macro_use] extern crate cfg_if; @@ -229,6 +230,15 @@ impl Certificates { pub type RequiredSigners = Ed25519KeyHashes; pub type RequiredSignersSet = BTreeSet; +impl From<&Ed25519KeyHashes> for RequiredSignersSet { + fn from(keys: &Ed25519KeyHashes) -> Self { + keys.0.iter().fold(BTreeSet::new(), |mut set, k| { + set.insert(k.clone()); + set + }) + } +} + #[wasm_bindgen] #[derive(Clone)] pub struct TransactionBody { @@ -613,6 +623,7 @@ to_from_bytes!(Ed25519KeyHashes); #[wasm_bindgen] impl Ed25519KeyHashes { + pub fn new() -> Self { Self(Vec::new()) } @@ -628,6 +639,10 @@ impl Ed25519KeyHashes { pub fn add(&mut self, elem: &Ed25519KeyHash) { self.0.push(elem.clone()); } + + pub fn to_option(&self) -> Option { + if self.len() > 0 { Some(self.clone()) } else { None } + } } #[wasm_bindgen] diff --git a/rust/src/plutus.rs b/rust/src/plutus.rs index 601bccf3..0d514d2d 100644 --- a/rust/src/plutus.rs +++ b/rust/src/plutus.rs @@ -253,7 +253,6 @@ impl ExUnitPrices { #[wasm_bindgen] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct ExUnits { - // TODO: should these be u32 or BigNum? mem: BigNum, steps: BigNum, } @@ -388,6 +387,7 @@ to_from_bytes!(PlutusData); #[wasm_bindgen] impl PlutusData { + pub fn new_constr_plutus_data(constr_plutus_data: &ConstrPlutusData) -> Self { Self { datum: PlutusDataEnum::ConstrPlutusData(constr_plutus_data.clone()), @@ -395,6 +395,16 @@ impl PlutusData { } } + /// Same as `.new_constr_plutus_data` but creates constr with empty data list + pub fn new_empty_constr_plutus_data(alternative: &BigNum) -> Self { + Self::new_constr_plutus_data( + &ConstrPlutusData::new( + alternative, + &PlutusList::new(), + ), + ) + } + pub fn new_map(map: &PlutusMap) -> Self { Self { datum: PlutusDataEnum::Map(map.clone()), @@ -619,6 +629,17 @@ impl Redeemers { pub fn add(&mut self, elem: &Redeemer) { self.0.push(elem.clone()); } + + pub fn total_ex_units(&self) -> Result { + let mut tot_mem = BigNum::zero(); + let mut tot_steps = BigNum::zero(); + for i in 0..self.0.len() { + let r: &Redeemer = &self.0[i]; + tot_mem = tot_mem.checked_add(&r.ex_units().mem())?; + tot_steps = tot_steps.checked_add(&r.ex_units().steps())?; + } + Ok(ExUnits::new(&tot_mem, &tot_steps)) + } } impl From> for Redeemers { @@ -1280,14 +1301,14 @@ mod tests { ); let constr_0_hash = hex::encode(hash_plutus_data(&constr_0).to_bytes()); assert_eq!(constr_0_hash, "923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec"); - let constr_0_roundtrip = PlutusData::from_bytes(constr_0.to_bytes()).unwrap(); + // let constr_0_roundtrip = PlutusData::from_bytes(constr_0.to_bytes()).unwrap(); // TODO: do we want semantic equality or bytewise equality? - //assert_eq!(constr_0, constr_0_roundtrip); - let constr_1854 = PlutusData::new_constr_plutus_data( - &ConstrPlutusData::new(&to_bignum(1854), &PlutusList::new()) - ); - let constr_1854_roundtrip = PlutusData::from_bytes(constr_1854.to_bytes()).unwrap(); - //assert_eq!(constr_1854, constr_1854_roundtrip); + // assert_eq!(constr_0, constr_0_roundtrip); + // let constr_1854 = PlutusData::new_constr_plutus_data( + // &ConstrPlutusData::new(&to_bignum(1854), &PlutusList::new()) + // ); + // let constr_1854_roundtrip = PlutusData::from_bytes(constr_1854.to_bytes()).unwrap(); + // assert_eq!(constr_1854, constr_1854_roundtrip); } #[test] @@ -1342,7 +1363,7 @@ mod tests { 32, 150000, 32, 3345831, 1, 1, ]; let cm = arr.iter().fold((CostModel::new(), 0), |(mut cm, i), x| { - cm.set(i, &Int::new_i32(x.clone())); + cm.set(i, &Int::new_i32(x.clone())).unwrap(); (cm, i + 1) }).0; let mut cms = Costmdls::new(); @@ -1363,4 +1384,43 @@ mod tests { ).unwrap(); assert_eq!(script.hash(), hash); } + + fn redeemer_with_ex_units(mem: &BigNum, steps: &BigNum) -> Redeemer { + Redeemer::new( + &RedeemerTag::new_spend(), + &BigNum::zero(), + &PlutusData::new_integer(&BigInt::from_str("0").unwrap()), + &ExUnits::new(mem, steps), + ) + } + + #[test] + fn test_total_ex_units() { + let mut r = Redeemers::new(); + + fn assert_ex_units(eu: &ExUnits, exp_mem: u64, exp_steps: u64) { + assert_eq!(eu.mem, to_bignum(exp_mem)); + assert_eq!(eu.steps, to_bignum(exp_steps)); + } + + r.add(&redeemer_with_ex_units(&to_bignum(10), &to_bignum(100))); + assert_ex_units(&r.total_ex_units().unwrap(), 10, 100); + r.add(&redeemer_with_ex_units(&to_bignum(20), &to_bignum(200))); + assert_ex_units(&r.total_ex_units().unwrap(), 30, 300); + r.add(&redeemer_with_ex_units(&to_bignum(30), &to_bignum(300))); + assert_ex_units(&r.total_ex_units().unwrap(), 60, 600); + } + + #[test] + fn test_empty_constr_data() { + assert_eq!( + PlutusData::new_empty_constr_plutus_data(&BigNum::one()), + PlutusData::new_constr_plutus_data( + &ConstrPlutusData::new( + &BigNum::from_str("1").unwrap(), + &PlutusList::new(), + ), + ), + ) + } } diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index 43b9db57..a0e72ae0 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -1,42 +1,50 @@ +#![allow(deprecated)] + +mod tx_inputs_builder; + use super::*; use super::fees; use super::utils; -use super::output_builder::{TransactionOutputAmountBuilder}; +use super::output_builder::TransactionOutputAmountBuilder; use std::collections::{BTreeMap, BTreeSet, HashSet}; use linked_hash_map::LinkedHashMap; +use tx_inputs_builder::{PlutusWitness, PlutusWitnesses}; +use crate::tx_builder::tx_inputs_builder::{get_bootstraps, TxInputsBuilder}; // comes from witsVKeyNeeded in the Ledger spec -fn witness_keys_for_cert(cert_enum: &Certificate, keys: &mut BTreeSet) { +fn witness_keys_for_cert(cert_enum: &Certificate) -> RequiredSigners { + let mut set = RequiredSigners::new(); match &cert_enum.0 { // stake key registrations do not require a witness CertificateEnum::StakeRegistration(_cert) => {}, CertificateEnum::StakeDeregistration(cert) => { - keys.insert(cert.stake_credential().to_keyhash().unwrap()); + set.add(&cert.stake_credential().to_keyhash().unwrap()); }, CertificateEnum::StakeDelegation(cert) => { - keys.insert(cert.stake_credential().to_keyhash().unwrap()); + set.add(&cert.stake_credential().to_keyhash().unwrap()); }, CertificateEnum::PoolRegistration(cert) => { for owner in &cert.pool_params().pool_owners().0 { - keys.insert(owner.clone()); + set.add(&owner.clone()); } - keys.insert( - Ed25519KeyHash::from_bytes(cert.pool_params().operator().to_bytes()).unwrap() + set.add( + &Ed25519KeyHash::from_bytes(cert.pool_params().operator().to_bytes()).unwrap() ); }, CertificateEnum::PoolRetirement(cert) => { - keys.insert( - Ed25519KeyHash::from_bytes(cert.pool_keyhash().to_bytes()).unwrap() + set.add( + &Ed25519KeyHash::from_bytes(cert.pool_keyhash().to_bytes()).unwrap() ); }, CertificateEnum::GenesisKeyDelegation(cert) => { - keys.insert( - Ed25519KeyHash::from_bytes(cert.genesis_delegate_hash().to_bytes()).unwrap() + set.add( + &Ed25519KeyHash::from_bytes(cert.genesis_delegate_hash().to_bytes()).unwrap() ); }, // not witness as there is no single core node or genesis key that posts the certificate CertificateEnum::MoveInstantaneousRewardsCert(_cert) => {}, } + set } fn fake_private_key() -> Bip32PrivateKey { @@ -61,14 +69,13 @@ fn fake_raw_key_public() -> PublicKey { } 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(&RequiredSignersSet::from(scripts)).count() - } + let mut input_hashes: RequiredSignersSet = RequiredSignersSet::from(&tx_builder.inputs); + input_hashes.extend(RequiredSignersSet::from(&tx_builder.collateral)); + input_hashes.extend(RequiredSignersSet::from(&tx_builder.required_signers)); + if let Some(scripts) = &tx_builder.mint_scripts { + input_hashes.extend(RequiredSignersSet::from(scripts)); } + input_hashes.len() } // tx_body must be the result of building from tx_builder @@ -94,11 +101,12 @@ fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Resul Some(result) }, }; - let bootstrap_keys = match tx_builder.input_types.bootstraps.len() { + let bootstraps = get_bootstraps(&tx_builder.inputs); + let bootstrap_keys = match bootstraps.len() { 0 => None, _x => { let mut result = BootstrapWitnesses::new(); - for addr in &tx_builder.input_types.bootstraps { + for addr in bootstraps { // picking icarus over daedalus for fake witness generation shouldn't matter result.add(&make_icarus_bootstrap_witness( &TransactionHash::from([0u8; TransactionHash::BYTE_COUNT]), @@ -110,7 +118,7 @@ fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Resul }, }; let (plutus_scripts, plutus_data, redeemers) = { - if let Some(s) = tx_builder.get_plutus_input_scripts() { + if let Some(s) = tx_builder.get_combined_plutus_scripts() { let (s, d, r) = s.collect(); (Some(s), Some(d), Some(r)) } else { @@ -164,112 +172,19 @@ fn min_fee(tx_builder: &TransactionBuilder) -> Result { // 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) -} - - -#[wasm_bindgen] -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct PlutusWitnesses(Vec); - -#[wasm_bindgen] -impl PlutusWitnesses { - pub fn new() -> Self { - Self(Vec::new()) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn get(&self, index: usize) -> PlutusWitness { - self.0[index].clone() - } - - pub fn add(&mut self, elem: &PlutusWitness) { - self.0.push(elem.clone()); - } - - fn collect(&self) -> (PlutusScripts, PlutusList, Redeemers) { - let mut s = PlutusScripts::new(); - let mut d = PlutusList::new(); - let mut r = Redeemers::new(); - self.0.iter().for_each(|w| { - s.add(&w.script); - d.add(&w.datum); - r.add(&w.redeemer); - }); - (s, d, r) - } -} - -impl From> for PlutusWitnesses { - fn from(scripts: Vec) -> Self { - scripts.iter().fold(PlutusWitnesses::new(), |mut scripts, s| { - scripts.add(s); - scripts - }) - } -} - -#[wasm_bindgen] -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct PlutusWitness { - script: PlutusScript, - datum: PlutusData, - redeemer: Redeemer, -} - -#[wasm_bindgen] -impl PlutusWitness { - - pub fn new(script: &PlutusScript, datum: &PlutusData, redeemer: &Redeemer) -> Self { - Self { - script: script.clone(), - datum: datum.clone(), - redeemer: redeemer.clone(), - } - } - - pub fn script(&self) -> PlutusScript { - self.script.clone() - } - - pub fn datum(&self) -> PlutusData { - self.datum.clone() - } - - pub fn redeemer(&self) -> Redeemer { - self.redeemer.clone() - } - - fn clone_with_redeemer_index(&self, index: &BigNum) -> Self { - Self { - script: self.script.clone(), - datum: self.datum.clone(), - redeemer: self.redeemer.clone_with_index(index), - } + let fee: Coin = fees::min_fee(&full_tx, &tx_builder.config.fee_algo)?; + if let Some(ex_unit_prices) = &tx_builder.config.ex_unit_prices { + let script_fee: Coin = fees::min_script_fee(&full_tx, &ex_unit_prices)?; + return fee.checked_add(&script_fee); + } + if tx_builder.has_plutus_inputs() { + return Err( + JsError::from_str( + "Plutus inputs are present but ex unit prices are missing in the config!" + ) + ); } -} - -#[derive(Clone, Debug)] -enum ScriptWitnessType { - NativeScriptWitness(NativeScript), - PlutusScriptWitness(PlutusWitness), -} - -// We need to know how many of each type of witness will be in the transaction so we can calculate the tx fee -#[derive(Clone, Debug)] -struct MockWitnessSet { - vkeys: BTreeSet, - scripts: LinkedHashMap>, - bootstraps: BTreeSet>, -} - -#[derive(Clone, Debug)] -struct TxBuilderInput { - input: TransactionInput, - amount: Value, // we need to keep track of the amount in the inputs for input selection + Ok(fee) } #[wasm_bindgen] @@ -293,6 +208,7 @@ pub struct TransactionBuilderConfig { max_value_size: u32, // protocol parameter max_tx_size: u32, // protocol parameter coins_per_utxo_word: Coin, // protocol parameter + ex_unit_prices: Option, // protocol parameter prefer_pure_change: bool, } @@ -305,6 +221,7 @@ pub struct TransactionBuilderConfigBuilder { max_value_size: Option, // protocol parameter max_tx_size: Option, // protocol parameter coins_per_utxo_word: Option, // protocol parameter + ex_unit_prices: Option, // protocol parameter prefer_pure_change: bool, } @@ -318,6 +235,7 @@ impl TransactionBuilderConfigBuilder { max_value_size: None, max_tx_size: None, coins_per_utxo_word: None, + ex_unit_prices: None, prefer_pure_change: false, } } @@ -334,6 +252,12 @@ impl TransactionBuilderConfigBuilder { cfg } + pub fn ex_unit_prices(&self, ex_unit_prices: &ExUnitPrices) -> Self { + let mut cfg = self.clone(); + cfg.ex_unit_prices = Some(ex_unit_prices.clone()); + cfg + } + pub fn pool_deposit(&self, pool_deposit: &BigNum) -> Self { let mut cfg = self.clone(); cfg.pool_deposit = Some(pool_deposit.clone()); @@ -365,7 +289,7 @@ impl TransactionBuilderConfigBuilder { } pub fn build(&self) -> Result { - let cfg = self.clone(); + let cfg: Self = self.clone(); Ok(TransactionBuilderConfig { fee_algo: cfg.fee_algo.ok_or(JsError::from_str("uninitialized field: fee_algo"))?, pool_deposit: cfg.pool_deposit.ok_or(JsError::from_str("uninitialized field: pool_deposit"))?, @@ -373,6 +297,7 @@ impl TransactionBuilderConfigBuilder { max_value_size: cfg.max_value_size.ok_or(JsError::from_str("uninitialized field: max_value_size"))?, max_tx_size: cfg.max_tx_size.ok_or(JsError::from_str("uninitialized field: max_tx_size"))?, coins_per_utxo_word: cfg.coins_per_utxo_word.ok_or(JsError::from_str("uninitialized field: coins_per_utxo_word"))?, + ex_unit_prices: cfg.ex_unit_prices, prefer_pure_change: cfg.prefer_pure_change, }) } @@ -382,7 +307,8 @@ impl TransactionBuilderConfigBuilder { #[derive(Clone, Debug)] pub struct TransactionBuilder { config: TransactionBuilderConfig, - inputs: Vec<(TxBuilderInput, Option)>, + inputs: TxInputsBuilder, + collateral: TxInputsBuilder, outputs: TransactionOutputs, fee: Option, ttl: Option, // absolute slot number @@ -390,10 +316,10 @@ pub struct TransactionBuilder { withdrawals: Option, auxiliary_data: Option, validity_start_interval: Option, - input_types: MockWitnessSet, mint: Option, mint_scripts: Option, script_data_hash: Option, + required_signers: Ed25519KeyHashes, } #[wasm_bindgen] @@ -649,16 +575,20 @@ impl TransactionBuilder { Ok(()) } + pub fn set_inputs(&mut self, inputs: &TxInputsBuilder) { + self.inputs = inputs.clone(); + } + + pub fn set_collateral(&mut self, collateral: &TxInputsBuilder) { + self.collateral = collateral.clone(); + } + /// We have to know what kind of inputs these are to know what kind of mock witnesses to create since /// 1) mock witnesses have different lengths depending on the type which changes the expecting fee /// 2) Witnesses are a set so we need to get rid of duplicates to avoid over-estimating the fee + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn add_key_input(&mut self, hash: &Ed25519KeyHash, input: &TransactionInput, amount: &Value) { - let inp = TxBuilderInput { - input: input.clone(), - amount: amount.clone(), - }; - self.inputs.push((inp, None)); - self.input_types.vkeys.insert(hash.clone()); + self.inputs.add_key_input(hash, input, amount); } /// This method adds the input to the builder BUT leaves a missing spot for the witness native script @@ -668,178 +598,72 @@ impl TransactionBuilder { /// /// Or instead use `.add_native_script_input` and `.add_plutus_script_input` /// to add inputs right along with the script, instead of the script hash + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn add_script_input(&mut self, hash: &ScriptHash, input: &TransactionInput, amount: &Value) { - let inp = TxBuilderInput { - input: input.clone(), - amount: amount.clone(), - }; - self.inputs.push((inp, Some(hash.clone()))); - if !self.input_types.scripts.contains_key(hash) { - self.input_types.scripts.insert(hash.clone(), None); - } + self.inputs.add_script_input(hash, input, amount); } /// This method will add the input to the builder and also register the required native script witness + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn add_native_script_input(&mut self, script: &NativeScript, input: &TransactionInput, amount: &Value) { - let hash = script.hash(); - self.add_script_input(&hash, input, amount); - self.input_types.scripts.insert( - hash, - Some(ScriptWitnessType::NativeScriptWitness(script.clone())), - ); + self.inputs.add_native_script_input(script, input, amount); } /// This method will add the input to the builder and also register the required plutus witness + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn add_plutus_script_input(&mut self, witness: &PlutusWitness, input: &TransactionInput, amount: &Value) { - let hash = witness.script.hash(); - self.add_script_input(&hash, input, amount); - self.input_types.scripts.insert( - hash, - Some(ScriptWitnessType::PlutusScriptWitness(witness.clone())), - ); + self.inputs.add_plutus_script_input(witness, input, amount); } + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn add_bootstrap_input(&mut self, hash: &ByronAddress, input: &TransactionInput, amount: &Value) { - let inp = TxBuilderInput { - input: input.clone(), - amount: amount.clone(), - }; - self.inputs.push((inp, None)); - self.input_types.bootstraps.insert(hash.to_bytes()); + self.inputs.add_bootstrap_input(hash, input, amount); } /// Note that for script inputs this method will use underlying generic `.add_script_input` /// which leaves a required empty spot for the script witness (or witnesses in case of Plutus). /// You can use `.add_native_script_input` or `.add_plutus_script_input` directly to register the input along with the witness. + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn add_input(&mut self, address: &Address, input: &TransactionInput, amount: &Value) { - match &BaseAddress::from_address(address) { - Some(addr) => { - match &addr.payment_cred().to_keyhash() { - Some(hash) => return self.add_key_input(hash, input, amount), - None => (), - } - match &addr.payment_cred().to_scripthash() { - Some(hash) => return self.add_script_input(hash, input, amount), - None => (), - } - }, - None => (), - } - match &EnterpriseAddress::from_address(address) { - Some(addr) => { - match &addr.payment_cred().to_keyhash() { - Some(hash) => return self.add_key_input(hash, input, amount), - None => (), - } - match &addr.payment_cred().to_scripthash() { - Some(hash) => return self.add_script_input(hash, input, amount), - None => (), - } - }, - None => (), - } - match &PointerAddress::from_address(address) { - Some(addr) => { - match &addr.payment_cred().to_keyhash() { - Some(hash) => return self.add_key_input(hash, input, amount), - None => (), - } - match &addr.payment_cred().to_scripthash() { - Some(hash) => return self.add_script_input(hash, input, amount), - None => (), - } - }, - None => (), - } - match &ByronAddress::from_address(address) { - Some(addr) => { - return self.add_bootstrap_input(addr, input, amount); - }, - None => (), - } + self.inputs.add_input(address, input, amount); } /// Returns the number of still missing input scripts (either native or plutus) /// Use `.add_required_native_input_scripts` or `.add_required_plutus_input_scripts` to add the missing scripts + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn count_missing_input_scripts(&self) -> usize { - self.input_types.scripts.values().filter(|s| { s.is_none() }).count() + self.inputs.count_missing_input_scripts() } /// Try adding the specified scripts as witnesses for ALREADY ADDED script inputs /// Any scripts that don't match any of the previously added inputs will be ignored /// Returns the number of remaining required missing witness scripts /// Use `.count_missing_input_scripts` to find the number of still missing scripts + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn add_required_native_input_scripts(&mut self, scripts: &NativeScripts) -> usize { - scripts.0.iter().for_each(|s: &NativeScript| { - let hash = s.hash(); - if self.input_types.scripts.contains_key(&hash) { - self.input_types.scripts.insert( - hash, - Some(ScriptWitnessType::NativeScriptWitness(s.clone())), - ); - } - }); - self.count_missing_input_scripts() + self.inputs.add_required_native_input_scripts(scripts) } /// Try adding the specified scripts as witnesses for ALREADY ADDED script inputs /// Any scripts that don't match any of the previously added inputs will be ignored /// Returns the number of remaining required missing witness scripts /// Use `.count_missing_input_scripts` to find the number of still missing scripts + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn add_required_plutus_input_scripts(&mut self, scripts: &PlutusWitnesses) -> usize { - scripts.0.iter().for_each(|s: &PlutusWitness| { - let hash = s.script.hash(); - if self.input_types.scripts.contains_key(&hash) { - self.input_types.scripts.insert( - hash, - Some(ScriptWitnessType::PlutusScriptWitness(s.clone())), - ); - } - }); - self.count_missing_input_scripts() + self.inputs.add_required_plutus_input_scripts(scripts) } /// Returns a copy of the current script input witness scripts in the builder + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn get_native_input_scripts(&self) -> Option { - let mut scripts = NativeScripts::new(); - self.input_types.scripts.values().for_each(|option| { - if let Some(ScriptWitnessType::NativeScriptWitness(s)) = option { - scripts.add(&s); - } - }); - if scripts.len() > 0 { Some(scripts) } else { None } + self.inputs.get_native_input_scripts() } /// Returns a copy of the current plutus input witness scripts in the builder. /// NOTE: each plutus witness will be cloned with a specific corresponding input index + #[deprecated(since = "10.2.0", note="Use `.set_inputs`")] pub fn get_plutus_input_scripts(&self) -> Option { - /* - * === EXPLANATION === - * The `Redeemer` object contains the `.index` field which is supposed to point - * exactly to the index of the corresponding input in the inputs array. We want to - * simplify and automate this as much as possible for the user to not have to care about it. - * - * For this we store the script hash along with the input, when it was registered, and - * now we create a map of script hashes to their input indexes. - * - * The registered witnesses are then each cloned with the new correct redeemer input index. - */ - let script_hash_index_map: BTreeMap<&ScriptHash, BigNum> = self.inputs.iter().enumerate() - .fold(BTreeMap::new(), |mut m, (i, (_, hash_option))| { - if let Some(hash) = hash_option { - m.insert(hash, to_bignum(i as u64)); - } - m - }); - let mut scripts = PlutusWitnesses::new(); - self.input_types.scripts.iter().for_each(|(hash, option)| { - if let Some(ScriptWitnessType::PlutusScriptWitness(s)) = option { - if let Some(idx) = script_hash_index_map.get(&hash) { - scripts.add(&s.clone_with_redeemer_index(&idx)); - } - } - }); - if scripts.len() > 0 { Some(scripts) } else { None } + self.inputs.get_plutus_input_scripts() } /// calculates how much the fee would increase if you added a given output @@ -937,14 +761,14 @@ impl TransactionBuilder { pub fn set_certs(&mut self, certs: &Certificates) { self.certs = Some(certs.clone()); for cert in &certs.0 { - witness_keys_for_cert(cert, &mut self.input_types.vkeys); + self.inputs.add_required_signers(&witness_keys_for_cert(cert)) }; } pub fn set_withdrawals(&mut self, withdrawals: &Withdrawals) { self.withdrawals = Some(withdrawals.clone()); for (withdrawal, _coin) in &withdrawals.0 { - self.input_types.vkeys.insert(withdrawal.payment_cred().to_keyhash().unwrap().clone()); + self.inputs.add_required_signer(&withdrawal.payment_cred().to_keyhash().unwrap()) }; } @@ -1120,22 +944,19 @@ impl TransactionBuilder { pub fn new(cfg: &TransactionBuilderConfig) -> Self { Self { config: cfg.clone(), - inputs: Vec::new(), + inputs: TxInputsBuilder::new(), + collateral: TxInputsBuilder::new(), outputs: TransactionOutputs::new(), fee: None, ttl: None, certs: None, withdrawals: None, auxiliary_data: None, - input_types: MockWitnessSet { - vkeys: BTreeSet::new(), - scripts: LinkedHashMap::new(), - bootstraps: BTreeSet::new(), - }, validity_start_interval: None, mint: None, mint_scripts: None, script_data_hash: None, + required_signers: Ed25519KeyHashes::new(), } } @@ -1143,7 +964,7 @@ impl TransactionBuilder { pub fn get_explicit_input(&self) -> Result { self.inputs .iter() - .try_fold(Value::zero(), |acc, (ref tx_builder_input, _)| { + .try_fold(Value::zero(), |acc, ref tx_builder_input| { acc.checked_add(&tx_builder_input.amount) }) } @@ -1464,7 +1285,7 @@ impl TransactionBuilder { /// In case there are no plutus input witnesses present - nothing will change /// You can set specific hash value using `.set_script_data_hash` pub fn calc_script_data_hash(&mut self, cost_models: &Costmdls) { - if let Some(pw) = self.get_plutus_input_scripts() { + if let Some(pw) = self.inputs.get_plutus_input_scripts() { let (_, datums, redeemers) = pw.collect(); self.script_data_hash = Some(hash_script_data(&redeemers, cost_models, Some(datums))); @@ -1484,12 +1305,14 @@ impl TransactionBuilder { self.script_data_hash = None; } + pub fn add_required_signer(&mut self, key: &Ed25519KeyHash) { + self.required_signers.add(key); + } + fn build_and_size(&self) -> Result<(TransactionBody, usize), JsError> { let fee = self.fee.ok_or_else(|| JsError::from_str("Fee not specified"))?; - let inputs = self.inputs.iter() - .map(|(ref tx_builder_input, _)| tx_builder_input.input.clone()).collect(); let built = TransactionBody { - inputs: TransactionInputs(inputs), + inputs: self.inputs.inputs(), outputs: self.outputs.clone(), fee, ttl: self.ttl, @@ -1503,8 +1326,8 @@ impl TransactionBuilder { validity_start_interval: self.validity_start_interval, mint: self.mint.clone(), script_data_hash: self.script_data_hash.clone(), - collateral: None, - required_signers: None, + collateral: self.collateral.inputs_option(), + required_signers: self.required_signers.to_option(), network_id: None, }; // we must build a tx with fake data (of correct size) to check the final Transaction size @@ -1539,7 +1362,10 @@ impl TransactionBuilder { fn get_combined_native_scripts(&self) -> Option { let mut ns = NativeScripts::new(); - if let Some(input_scripts) = self.get_native_input_scripts() { + if let Some(input_scripts) = self.inputs.get_native_input_scripts() { + input_scripts.0.iter().for_each(|s| { ns.add(s); }); + } + if let Some(input_scripts) = self.collateral.get_native_input_scripts() { input_scripts.0.iter().for_each(|s| { ns.add(s); }); } if let Some(mint_scripts) = &self.mint_scripts { @@ -1548,6 +1374,17 @@ impl TransactionBuilder { if ns.len() > 0 { Some(ns) } else { None } } + fn get_combined_plutus_scripts(&self) -> Option { + let mut res = PlutusWitnesses::new(); + if let Some(scripts) = self.inputs.get_plutus_input_scripts() { + scripts.0.iter().for_each(|s| { res.add(s); }) + } + if let Some(scripts) = self.collateral.get_plutus_input_scripts() { + scripts.0.iter().for_each(|s| { res.add(s); }) + } + if res.len() > 0 { Some(res) } else { None } + } + // This function should be producing the total witness-set // that is created by the tx-builder itself, // before the transaction is getting signed by the actual wallet. @@ -1557,7 +1394,7 @@ impl TransactionBuilder { if let Some(scripts) = self.get_combined_native_scripts() { wit.set_native_scripts(&scripts); } - if let Some(pw) = self.get_plutus_input_scripts() { + if let Some(pw) = self.get_combined_plutus_scripts() { let (scripts, datums, redeemers) = pw.collect(); wit.set_plutus_scripts(&scripts); wit.set_plutus_data(&datums); @@ -1566,6 +1403,10 @@ impl TransactionBuilder { wit } + fn has_plutus_inputs(&self) -> bool { + self.inputs.get_plutus_input_scripts().is_some() + } + /// Returns full Transaction object with the body and the auxiliary data /// NOTE: witness_set will contain all mint_scripts if any been added or set /// NOTE: is_valid set to true @@ -1576,10 +1417,17 @@ impl TransactionBuilder { "There are some script inputs added that don't have the corresponding script provided as a witness!", )); } - if self.script_data_hash.is_none() && self.get_plutus_input_scripts().is_some() { - return Err(JsError::from_str( - "Plutus inputs are present, but script data hash is not specified", - )); + if self.has_plutus_inputs() { + if self.script_data_hash.is_none() { + return Err(JsError::from_str( + "Plutus inputs are present, but script data hash is not specified", + )); + } + if self.collateral.len() == 0 { + return Err(JsError::from_str( + "Plutus inputs are present, but no collateral inputs are added", + )); + } } self.build_tx_unsafe() } @@ -1609,7 +1457,7 @@ mod tests { use super::*; use fees::*; use crate::tx_builder_constants::TxBuilderConstants; - use super::output_builder::{TransactionOutputBuilder}; + use super::output_builder::TransactionOutputBuilder; const MAX_VALUE_SIZE: u32 = 4000; const MAX_TX_SIZE: u32 = 8000; // might be out of date but suffices for our tests @@ -1677,6 +1525,12 @@ mod tests { .max_value_size(max_val_size) .max_tx_size(MAX_TX_SIZE) .coins_per_utxo_word(&to_bignum(coins_per_utxo_word)) + .ex_unit_prices( + &ExUnitPrices::new( + &SubCoin::new(&to_bignum(577), &to_bignum(10000)), + &SubCoin::new(&to_bignum(721), &to_bignum(10000000)), + ), + ) .build() .unwrap(); TransactionBuilder::new(&cfg) @@ -2124,7 +1978,7 @@ mod tests { &spend_cred, &stake_cred ).to_address(), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 1), &Value::new(&to_bignum(1_000_000)) ); tx_builder.add_input( @@ -2137,14 +1991,14 @@ mod tests { &to_bignum(0) ) ).to_address(), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 2), &Value::new(&to_bignum(1_000_000)) ); tx_builder.add_input( &ByronAddress::icarus_from_key( &spend, NetworkInfo::testnet().protocol_magic() ).to_address(), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 3), &Value::new(&to_bignum(1_000_000)) ); @@ -2379,16 +2233,17 @@ mod tests { }) .collect::>(); - for (multiasset, ada) in multiassets + for (i, (multiasset, ada)) in multiassets .iter() .zip([100u64, 100].iter().cloned().map(to_bignum)) + .enumerate() { let mut input_amount = Value::new(&ada); input_amount.set_multiasset(multiasset); tx_builder.add_key_input( &&spend.to_raw_key().hash(), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), i as u32), &input_amount, ); } @@ -2492,16 +2347,17 @@ mod tests { }) .collect::>(); - for (multiasset, ada) in multiassets + for (i, (multiasset, ada)) in multiassets .iter() .zip([100u64, 100].iter().cloned().map(to_bignum)) + .enumerate() { let mut input_amount = Value::new(&ada); input_amount.set_multiasset(multiasset); tx_builder.add_key_input( &&spend.to_raw_key().hash(), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), i as u32), &input_amount, ); } @@ -2622,16 +2478,17 @@ mod tests { }) .collect::>(); - for (multiasset, ada) in multiassets + for (i, (multiasset, ada)) in multiassets .iter() .zip([300u64, 300].iter().cloned().map(to_bignum)) + .enumerate() { let mut input_amount = Value::new(&ada); input_amount.set_multiasset(multiasset); tx_builder.add_key_input( &&spend.to_raw_key().hash(), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), i as u32), &input_amount, ); } @@ -3071,9 +2928,13 @@ mod tests { assert!(tx_builder.add_change_if_needed(&change_addr).is_err()) } + fn fake_tx_hash(input_hash_byte: u8) -> TransactionHash { + TransactionHash::from([input_hash_byte; 32]) + } + fn make_input(input_hash_byte: u8, value: Value) -> TransactionUnspentOutput { TransactionUnspentOutput::new( - &TransactionInput::new(&TransactionHash::from([input_hash_byte; 32]), 0), + &TransactionInput::new(&fake_tx_hash(input_hash_byte), 0), &TransactionOutputBuilder::new() .with_address(&Address::from_bech32("addr1vyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmnqs6l44z").unwrap()) .next().unwrap() @@ -3109,12 +2970,11 @@ mod tests { assert_eq!(2, tx.outputs().len()); assert_eq!(3, tx.inputs().len()); // confirm order of only what is necessary - assert_eq!(2u8, tx.inputs().get(0).transaction_id().0[0]); - assert_eq!(3u8, tx.inputs().get(1).transaction_id().0[0]); - assert_eq!(1u8, tx.inputs().get(2).transaction_id().0[0]); + assert_eq!(1u8, tx.inputs().get(0).transaction_id().0[0]); + assert_eq!(2u8, tx.inputs().get(1).transaction_id().0[0]); + assert_eq!(3u8, tx.inputs().get(2).transaction_id().0[0]); } - #[test] fn tx_builder_cip2_largest_first_static_fees() { // we have a = 0 so we know adding inputs/outputs doesn't change the fee so we can analyze more @@ -3148,7 +3008,6 @@ mod tests { #[test] fn tx_builder_cip2_largest_first_multiasset() { // we have a = 0 so we know adding inputs/outputs doesn't change the fee so we can analyze more - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(0)); let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 0)); let pid1 = PolicyID::from([1u8; 28]); let pid2 = PolicyID::from([2u8; 28]); @@ -3253,7 +3112,6 @@ mod tests { #[test] fn tx_builder_cip2_random_improve_multiasset() { - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(0)); let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 0)); let pid1 = PolicyID::from([1u8; 28]); let pid2 = PolicyID::from([2u8; 28]); @@ -4306,7 +4164,7 @@ mod tests { // One input from same address as mint tx_builder.add_key_input( &hash1, - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 1), &Value::new(&to_bignum(10_000_000)), ); @@ -4464,16 +4322,17 @@ mod tests { }) .collect::>(); - for (multiasset, ada) in multiassets + for (i, (multiasset, ada)) in multiassets .iter() .zip([100u64, 100, 100].iter().cloned().map(to_bignum)) + .enumerate() { let mut input_amount = Value::new(&ada); input_amount.set_multiasset(multiasset); tx_builder.add_key_input( &&spend.to_raw_key().hash(), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), i as u32), &input_amount, ); } @@ -4515,6 +4374,14 @@ mod tests { assert_eq!(ma2_output.get(&policy_id2).unwrap().get(&name).unwrap(), to_bignum(40)); } + fn create_base_address(x: u8) -> Address { + BaseAddress::new( + NetworkInfo::testnet().network_id(), + &StakeCredential::from_keyhash(&fake_key_hash(x)), + &StakeCredential::from_keyhash(&fake_key_hash(0)) + ).to_address() + } + fn create_base_address_from_script_hash(sh: &ScriptHash) -> Address { BaseAddress::new( NetworkInfo::testnet().network_id(), @@ -4639,7 +4506,7 @@ mod tests { let tx_len_before_new_script_input = unsafe_tx_len(&tx_builder); tx_builder.add_input( &create_base_address_from_script_hash(&hash2), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 1), &Value::new(&to_bignum(1_000_000)) ); let tx_len_after_new_script_input = unsafe_tx_len(&tx_builder); @@ -4734,7 +4601,7 @@ mod tests { tx_builder.set_fee(&to_bignum(42)); let (script1, hash1) = plutus_script_and_hash(0); let (script2, hash2) = plutus_script_and_hash(1); - let (script3, hash3) = plutus_script_and_hash(3); + let (script3, _hash3) = plutus_script_and_hash(3); let datum1 = PlutusData::new_bytes(fake_bytes(10)); let datum2 = PlutusData::new_bytes(fake_bytes(11)); let redeemer1 = Redeemer::new( @@ -4756,7 +4623,7 @@ mod tests { ); tx_builder.add_input( &create_base_address_from_script_hash(&hash2), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 1), &Value::new(&to_bignum(1_000_000)), ); // There are TWO missing script witnesses @@ -4799,10 +4666,21 @@ mod tests { assert_eq!(redeems.get(1), redeemer2); } + fn create_collateral() -> TxInputsBuilder { + let mut collateral_builder = TxInputsBuilder::new(); + collateral_builder.add_input( + &byron_address(), + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(1_000_000)), + ); + collateral_builder + } + #[test] fn test_existing_plutus_scripts_require_data_hash() { let mut tx_builder = create_reallistic_tx_builder(); tx_builder.set_fee(&to_bignum(42)); + tx_builder.set_collateral(&create_collateral()); let (script1, _) = plutus_script_and_hash(0); let datum = PlutusData::new_bytes(fake_bytes(1)); let redeemer_datum = PlutusData::new_bytes(fake_bytes(2)); @@ -4844,6 +4722,8 @@ mod tests { fn test_calc_script_hash_data() { let mut tx_builder = create_reallistic_tx_builder(); tx_builder.set_fee(&to_bignum(42)); + tx_builder.set_collateral(&create_collateral()); + let (script1, _) = plutus_script_and_hash(0); let datum = PlutusData::new_bytes(fake_bytes(1)); let redeemer_datum = PlutusData::new_bytes(fake_bytes(2)); @@ -4880,6 +4760,7 @@ mod tests { fn test_plutus_witness_redeemer_index_auto_changing() { let mut tx_builder = create_reallistic_tx_builder(); tx_builder.set_fee(&to_bignum(42)); + tx_builder.set_collateral(&create_collateral()); let (script1, _) = plutus_script_and_hash(0); let (script2, _) = plutus_script_and_hash(1); let datum1 = PlutusData::new_bytes(fake_bytes(10)); @@ -4910,12 +4791,12 @@ mod tests { // both have redeemers with index ZERO tx_builder.add_plutus_script_input( &PlutusWitness::new(&script1, &datum1, &redeemer1), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 1), &Value::new(&to_bignum(1_000_000)), ); tx_builder.add_plutus_script_input( &PlutusWitness::new(&script2, &datum2, &redeemer2), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 2), &Value::new(&to_bignum(1_000_000)), ); @@ -4949,6 +4830,7 @@ mod tests { fn test_native_and_plutus_scripts_together() { let mut tx_builder = create_reallistic_tx_builder(); tx_builder.set_fee(&to_bignum(42)); + tx_builder.set_collateral(&create_collateral()); let (pscript1, _) = plutus_script_and_hash(0); let (pscript2, phash2) = plutus_script_and_hash(1); let (nscript1, _) = mint_script_and_policy(0); @@ -4978,19 +4860,19 @@ mod tests { // Add one native input directly with witness tx_builder.add_native_script_input( &nscript1, - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 1), &Value::new(&to_bignum(1_000_000)), ); // Add one plutus input generically without witness tx_builder.add_input( &create_base_address_from_script_hash(&phash2), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 2), &Value::new(&to_bignum(1_000_000)), ); // Add one native input generically without witness tx_builder.add_input( &create_base_address_from_script_hash(&nhash2), - &TransactionInput::new(&genesis_id(), 0), + &TransactionInput::new(&genesis_id(), 3), &Value::new(&to_bignum(1_000_000)), ); @@ -5050,5 +4932,339 @@ mod tests { // because it was added on the third position assert_eq!(redeems.get(1), redeemer2.clone_with_index(&to_bignum(2))); } + + #[test] + fn test_regular_and_collateral_inputs_same_keyhash() { + + let mut input_builder = TxInputsBuilder::new(); + let mut collateral_builder = TxInputsBuilder::new(); + + // Add a single input of both kinds with the SAME keyhash + input_builder.add_input( + &create_base_address(0), + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(1_000_000)), + ); + collateral_builder.add_input( + &create_base_address(0), + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(1_000_000)), + ); + + fn get_fake_vkeys_count(i: &TxInputsBuilder, c: &TxInputsBuilder) -> usize { + let mut tx_builder = create_reallistic_tx_builder(); + tx_builder.set_fee(&to_bignum(42)); + tx_builder.set_inputs(i); + tx_builder.set_collateral(c); + let tx: Transaction = fake_full_tx(&tx_builder, tx_builder.build().unwrap()).unwrap(); + tx.witness_set.vkeys.unwrap().len() + } + + // There's only one fake witness in the builder + // because a regular and a collateral inputs both use the same keyhash + assert_eq!(get_fake_vkeys_count(&input_builder, &collateral_builder), 1); + + // Add a new input of each kind with DIFFERENT keyhashes + input_builder.add_input( + &create_base_address(1), + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(1_000_000)), + ); + collateral_builder.add_input( + &create_base_address(2), + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(1_000_000)), + ); + + + // There are now three fake witnesses in the builder + // because all three unique keyhashes got combined + assert_eq!(get_fake_vkeys_count(&input_builder, &collateral_builder), 3); + } + + #[test] + fn test_regular_and_collateral_inputs_together() { + let mut tx_builder = create_reallistic_tx_builder(); + tx_builder.set_fee(&to_bignum(42)); + let (pscript1, _) = plutus_script_and_hash(0); + let (pscript2, _) = plutus_script_and_hash(1); + let (nscript1, _) = mint_script_and_policy(0); + let (nscript2, _) = mint_script_and_policy(1); + let datum1 = PlutusData::new_bytes(fake_bytes(10)); + let datum2 = PlutusData::new_bytes(fake_bytes(11)); + // Creating redeemers with indexes ZERO + let redeemer1 = Redeemer::new( + &RedeemerTag::new_spend(), + &to_bignum(0), + &PlutusData::new_bytes(fake_bytes(20)), + &ExUnits::new(&to_bignum(1), &to_bignum(2)), + ); + let redeemer2 = Redeemer::new( + &RedeemerTag::new_spend(), + &to_bignum(0), + &PlutusData::new_bytes(fake_bytes(21)), + &ExUnits::new(&to_bignum(1), &to_bignum(2)), + ); + + let mut input_builder = TxInputsBuilder::new(); + let mut collateral_builder = TxInputsBuilder::new(); + + input_builder.add_native_script_input( + &nscript1, + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(1_000_000)), + ); + collateral_builder.add_native_script_input( + &nscript2, + &TransactionInput::new(&genesis_id(), 1), + &Value::new(&to_bignum(1_000_000)), + ); + + input_builder.add_plutus_script_input( + &PlutusWitness::new( + &pscript1, + &datum1, + &redeemer1, + ), + &TransactionInput::new(&genesis_id(), 2), + &Value::new(&to_bignum(1_000_000)), + ); + collateral_builder.add_plutus_script_input( + &PlutusWitness::new( + &pscript2, + &datum2, + &redeemer2, + ), + &TransactionInput::new(&genesis_id(), 3), + &Value::new(&to_bignum(1_000_000)), + ); + + tx_builder.set_inputs(&input_builder); + tx_builder.set_collateral(&collateral_builder); + + tx_builder.calc_script_data_hash( + &TxBuilderConstants::plutus_default_cost_models(), + ); + + let w: &TransactionWitnessSet = &tx_builder.build_tx().unwrap().witness_set; + + assert!(w.native_scripts.is_some()); + let nscripts = w.native_scripts.as_ref().unwrap(); + assert_eq!(nscripts.len(), 2); + assert_eq!(nscripts.get(0), nscript1); + assert_eq!(nscripts.get(1), nscript2); + + assert!(w.plutus_scripts.is_some()); + let pscripts = w.plutus_scripts.as_ref().unwrap(); + assert_eq!(pscripts.len(), 2); + assert_eq!(pscripts.get(0), pscript1); + assert_eq!(pscripts.get(1), pscript2); + + assert!(w.plutus_data.is_some()); + let datums = w.plutus_data.as_ref().unwrap(); + assert_eq!(datums.len(), 2); + assert_eq!(datums.get(0), datum1); + assert_eq!(datums.get(1), datum2); + + assert!(w.redeemers.is_some()); + let redeemers = w.redeemers.as_ref().unwrap(); + assert_eq!(redeemers.len(), 2); + assert_eq!(redeemers.get(0), redeemer1.clone_with_index(&to_bignum(1))); + assert_eq!(redeemers.get(1), redeemer2.clone_with_index(&to_bignum(1))); + } + + #[test] + fn test_ex_unit_costs_are_added_to_the_fees() { + + fn calc_fee_with_ex_units(mem: u64, step: u64) -> Coin { + let mut input_builder = TxInputsBuilder::new(); + let mut collateral_builder = TxInputsBuilder::new(); + + // Add a single input of both kinds with the SAME keyhash + input_builder.add_input( + &create_base_address(0), + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(1_000_000)), + ); + collateral_builder.add_input( + &create_base_address(0), + &TransactionInput::new(&genesis_id(), 1), + &Value::new(&to_bignum(1_000_000)), + ); + + let (pscript1, _) = plutus_script_and_hash(0); + let datum1 = PlutusData::new_bytes(fake_bytes(10)); + let redeemer1 = Redeemer::new( + &RedeemerTag::new_spend(), + &to_bignum(0), + &PlutusData::new_bytes(fake_bytes(20)), + &ExUnits::new(&to_bignum(mem), &to_bignum(step)), + ); + input_builder.add_plutus_script_input( + &PlutusWitness::new( + &pscript1, + &datum1, + &redeemer1, + ), + &TransactionInput::new(&genesis_id(), 2), + &Value::new(&to_bignum(1_000_000)), + ); + + let mut tx_builder = create_reallistic_tx_builder(); + tx_builder.set_inputs(&input_builder); + tx_builder.set_collateral(&collateral_builder); + + tx_builder.add_change_if_needed( + &create_base_address(42), + ).unwrap(); + + tx_builder.get_fee_if_set().unwrap() + } + + assert_eq!(calc_fee_with_ex_units(0, 0), to_bignum(173509)); + assert_eq!(calc_fee_with_ex_units(10000, 0), to_bignum(174174)); + assert_eq!(calc_fee_with_ex_units(0, 10000000), to_bignum(174406)); + assert_eq!(calc_fee_with_ex_units(10000, 10000000), to_bignum(175071)); + } + + #[test] + fn test_script_inputs_ordering() { + let mut tx_builder = create_reallistic_tx_builder(); + tx_builder.set_fee(&to_bignum(42)); + let (nscript1, _) = mint_script_and_policy(0); + let (pscript1, _) = plutus_script_and_hash(0); + let (pscript2, _) = plutus_script_and_hash(1); + let datum1 = PlutusData::new_bytes(fake_bytes(10)); + let datum2 = PlutusData::new_bytes(fake_bytes(11)); + // Creating redeemers with indexes ZERO + let pdata1 = PlutusData::new_bytes(fake_bytes(20)); + let pdata2 = PlutusData::new_bytes(fake_bytes(21)); + let redeemer1 = Redeemer::new( + &RedeemerTag::new_spend(), + &to_bignum(0), + &pdata1, + &ExUnits::new(&to_bignum(1), &to_bignum(2)), + ); + let redeemer2 = Redeemer::new( + &RedeemerTag::new_spend(), + &to_bignum(0), + &pdata2, + &ExUnits::new(&to_bignum(1), &to_bignum(2)), + ); + + tx_builder.add_plutus_script_input( + &PlutusWitness::new( + &pscript1, + &datum1, + &redeemer1, + ), + &TransactionInput::new(&fake_tx_hash(2), 1), + &Value::new(&to_bignum(1_000_000)), + ); + tx_builder.add_native_script_input( + &nscript1, + &TransactionInput::new(&fake_tx_hash(1), 0), + &Value::new(&to_bignum(1_000_000)), + ); + tx_builder.add_plutus_script_input( + &PlutusWitness::new( + &pscript2, + &datum2, + &redeemer2, + ), + &TransactionInput::new(&fake_tx_hash(2), 0), + &Value::new(&to_bignum(1_000_000)), + ); + + let tx: Transaction = tx_builder.build_tx_unsafe().unwrap(); + + let ins = tx.body.inputs; + assert_eq!(ins.len(), 3); + assert_eq!(ins.get(0).transaction_id.0[0], 1); + assert_eq!(ins.get(1).transaction_id.0[0], 2); + assert_eq!(ins.get(1).index, 0); + assert_eq!(ins.get(2).transaction_id.0[0], 2); + assert_eq!(ins.get(2).index, 1); + + let r: Redeemers = tx.witness_set.redeemers.unwrap(); + assert_eq!(r.len(), 2); + + // Redeemer1 now has the index 2 even tho the input was added first + assert_eq!(r.get(0).data(), pdata1); + assert_eq!(r.get(0).index(), to_bignum(2)); + + // Redeemer1 now has the index 1 even tho the input was added last + assert_eq!(r.get(1).data(), pdata2); + assert_eq!(r.get(1).index(), to_bignum(1)); + } + + #[test] + fn test_required_signers() { + let mut tx_builder = create_reallistic_tx_builder(); + tx_builder.set_fee(&to_bignum(42)); + let tx1: TransactionBody = tx_builder.build().unwrap(); + assert!(tx1.required_signers.is_none()); + + let s1 = fake_key_hash(1); + let s2 = fake_key_hash(22); + let s3 = fake_key_hash(133); + + tx_builder.add_required_signer(&s1); + tx_builder.add_required_signer(&s3); + tx_builder.add_required_signer(&s2); + + let tx1: TransactionBody = tx_builder.build().unwrap(); + assert!(tx1.required_signers.is_some()); + + let rs: RequiredSigners = tx1.required_signers.unwrap(); + assert_eq!(rs.len(), 3); + assert_eq!(rs.get(0), s1); + assert_eq!(rs.get(1), s3); + assert_eq!(rs.get(2), s2); + } + + #[test] + fn test_required_signers_are_added_to_the_witness_estimate() { + + fn count_fake_witnesses_with_required_signers(keys: &Ed25519KeyHashes) -> usize { + let mut tx_builder = create_reallistic_tx_builder(); + tx_builder.set_fee(&to_bignum(42)); + tx_builder.add_input( + &create_base_address(0), + &TransactionInput::new(&fake_tx_hash(0), 0), + &Value::new(&to_bignum(10_000_000)), + ); + + keys.0.iter().for_each(|k| { + tx_builder.add_required_signer(k); + }); + + let tx: Transaction = fake_full_tx(&tx_builder, tx_builder.build().unwrap()).unwrap(); + tx.witness_set.vkeys.unwrap().len() + } + + assert_eq!(count_fake_witnesses_with_required_signers( + &Ed25519KeyHashes::new(), + ), 1); + + assert_eq!(count_fake_witnesses_with_required_signers( + &Ed25519KeyHashes(vec![fake_key_hash(1)]), + ), 2); + + assert_eq!(count_fake_witnesses_with_required_signers( + &Ed25519KeyHashes(vec![fake_key_hash(1), fake_key_hash(2)]), + ), 3); + + // This case still produces only 3 fake signatures, because the same key is already used in the input address + assert_eq!(count_fake_witnesses_with_required_signers( + &Ed25519KeyHashes(vec![fake_key_hash(1), fake_key_hash(2), fake_key_hash(0)]), + ), 3); + + // When a different key is used - 4 fake witnesses are produced + assert_eq!(count_fake_witnesses_with_required_signers( + &Ed25519KeyHashes(vec![fake_key_hash(1), fake_key_hash(2), fake_key_hash(3)]), + ), 4); + } + } diff --git a/rust/src/tx_builder/tx_inputs_builder.rs b/rust/src/tx_builder/tx_inputs_builder.rs new file mode 100644 index 00000000..0762802e --- /dev/null +++ b/rust/src/tx_builder/tx_inputs_builder.rs @@ -0,0 +1,369 @@ +use super::*; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Clone, Debug)] +pub(crate) struct TxBuilderInput { + pub(crate) input: TransactionInput, + pub(crate) amount: Value, // we need to keep track of the amount in the inputs for input selection +} + +#[wasm_bindgen] +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct PlutusWitness { + script: PlutusScript, + datum: PlutusData, + redeemer: Redeemer, +} + +#[wasm_bindgen] +impl PlutusWitness { + + pub fn new(script: &PlutusScript, datum: &PlutusData, redeemer: &Redeemer) -> Self { + Self { + script: script.clone(), + datum: datum.clone(), + redeemer: redeemer.clone(), + } + } + + pub fn script(&self) -> PlutusScript { + self.script.clone() + } + + pub fn datum(&self) -> PlutusData { + self.datum.clone() + } + + pub fn redeemer(&self) -> Redeemer { + self.redeemer.clone() + } + + fn clone_with_redeemer_index(&self, index: &BigNum) -> Self { + Self { + script: self.script.clone(), + datum: self.datum.clone(), + redeemer: self.redeemer.clone_with_index(index), + } + } +} + + +#[wasm_bindgen] +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct PlutusWitnesses(pub(crate) Vec); + +#[wasm_bindgen] +impl PlutusWitnesses { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn get(&self, index: usize) -> PlutusWitness { + self.0[index].clone() + } + + pub fn add(&mut self, elem: &PlutusWitness) { + self.0.push(elem.clone()); + } + + pub(crate) fn collect(&self) -> (PlutusScripts, PlutusList, Redeemers) { + let mut s = PlutusScripts::new(); + let mut d = PlutusList::new(); + let mut r = Redeemers::new(); + self.0.iter().for_each(|w| { + s.add(&w.script); + d.add(&w.datum); + r.add(&w.redeemer); + }); + (s, d, r) + } +} + +impl From> for PlutusWitnesses { + fn from(scripts: Vec) -> Self { + scripts.iter().fold(PlutusWitnesses::new(), |mut scripts, s| { + scripts.add(s); + scripts + }) + } +} + +#[derive(Clone, Debug)] +pub enum ScriptWitnessType { + NativeScriptWitness(NativeScript), + PlutusScriptWitness(PlutusWitness), +} + +// We need to know how many of each type of witness will be in the transaction so we can calculate the tx fee +#[derive(Clone, Debug)] +pub struct MockWitnessSet { + vkeys: RequiredSignersSet, + scripts: LinkedHashMap>, + bootstraps: BTreeSet>, +} + +#[wasm_bindgen] +#[derive(Clone, Debug)] +pub struct TxInputsBuilder { + inputs: BTreeMap)>, + input_types: MockWitnessSet, +} + +pub(crate) fn get_bootstraps(inputs: &TxInputsBuilder) -> BTreeSet> { + inputs.input_types.bootstraps.clone() +} + +#[wasm_bindgen] +impl TxInputsBuilder { + + pub fn new() -> Self { + Self { + inputs: BTreeMap::new(), + input_types: MockWitnessSet { + vkeys: BTreeSet::new(), + scripts: LinkedHashMap::new(), + bootstraps: BTreeSet::new(), + }, + } + } + + fn push_input(&mut self, e: (TxBuilderInput, Option)) { + self.inputs.insert(e.0.input.clone(), e); + } + + /// We have to know what kind of inputs these are to know what kind of mock witnesses to create since + /// 1) mock witnesses have different lengths depending on the type which changes the expecting fee + /// 2) Witnesses are a set so we need to get rid of duplicates to avoid over-estimating the fee + pub fn add_key_input(&mut self, hash: &Ed25519KeyHash, input: &TransactionInput, amount: &Value) { + let inp = TxBuilderInput { + input: input.clone(), + amount: amount.clone(), + }; + self.push_input((inp, None)); + self.input_types.vkeys.insert(hash.clone()); + } + + /// This method adds the input to the builder BUT leaves a missing spot for the witness native script + /// + /// After adding the input with this method, use `.add_required_native_input_scripts` + /// and `.add_required_plutus_input_scripts` to add the witness scripts + /// + /// Or instead use `.add_native_script_input` and `.add_plutus_script_input` + /// to add inputs right along with the script, instead of the script hash + pub fn add_script_input(&mut self, hash: &ScriptHash, input: &TransactionInput, amount: &Value) { + let inp = TxBuilderInput { + input: input.clone(), + amount: amount.clone(), + }; + self.push_input((inp, Some(hash.clone()))); + if !self.input_types.scripts.contains_key(hash) { + self.input_types.scripts.insert(hash.clone(), None); + } + } + + /// This method will add the input to the builder and also register the required native script witness + pub fn add_native_script_input(&mut self, script: &NativeScript, input: &TransactionInput, amount: &Value) { + let hash = script.hash(); + self.add_script_input(&hash, input, amount); + self.input_types.scripts.insert( + hash, + Some(ScriptWitnessType::NativeScriptWitness(script.clone())), + ); + } + + /// This method will add the input to the builder and also register the required plutus witness + pub fn add_plutus_script_input(&mut self, witness: &PlutusWitness, input: &TransactionInput, amount: &Value) { + let hash = witness.script.hash(); + self.add_script_input(&hash, input, amount); + self.input_types.scripts.insert( + hash, + Some(ScriptWitnessType::PlutusScriptWitness(witness.clone())), + ); + } + + pub fn add_bootstrap_input(&mut self, hash: &ByronAddress, input: &TransactionInput, amount: &Value) { + let inp = TxBuilderInput { + input: input.clone(), + amount: amount.clone(), + }; + self.push_input((inp, None)); + self.input_types.bootstraps.insert(hash.to_bytes()); + } + + /// Note that for script inputs this method will use underlying generic `.add_script_input` + /// which leaves a required empty spot for the script witness (or witnesses in case of Plutus). + /// You can use `.add_native_script_input` or `.add_plutus_script_input` directly to register the input along with the witness. + pub fn add_input(&mut self, address: &Address, input: &TransactionInput, amount: &Value) { + match &BaseAddress::from_address(address) { + Some(addr) => { + match &addr.payment_cred().to_keyhash() { + Some(hash) => return self.add_key_input(hash, input, amount), + None => (), + } + match &addr.payment_cred().to_scripthash() { + Some(hash) => return self.add_script_input(hash, input, amount), + None => (), + } + }, + None => (), + } + match &EnterpriseAddress::from_address(address) { + Some(addr) => { + match &addr.payment_cred().to_keyhash() { + Some(hash) => return self.add_key_input(hash, input, amount), + None => (), + } + match &addr.payment_cred().to_scripthash() { + Some(hash) => return self.add_script_input(hash, input, amount), + None => (), + } + }, + None => (), + } + match &PointerAddress::from_address(address) { + Some(addr) => { + match &addr.payment_cred().to_keyhash() { + Some(hash) => return self.add_key_input(hash, input, amount), + None => (), + } + match &addr.payment_cred().to_scripthash() { + Some(hash) => return self.add_script_input(hash, input, amount), + None => (), + } + }, + None => (), + } + match &ByronAddress::from_address(address) { + Some(addr) => { + return self.add_bootstrap_input(addr, input, amount); + }, + None => (), + } + } + + /// Returns the number of still missing input scripts (either native or plutus) + /// Use `.add_required_native_input_scripts` or `.add_required_plutus_input_scripts` to add the missing scripts + pub fn count_missing_input_scripts(&self) -> usize { + self.input_types.scripts.values().filter(|s| { s.is_none() }).count() + } + + /// Try adding the specified scripts as witnesses for ALREADY ADDED script inputs + /// Any scripts that don't match any of the previously added inputs will be ignored + /// Returns the number of remaining required missing witness scripts + /// Use `.count_missing_input_scripts` to find the number of still missing scripts + pub fn add_required_native_input_scripts(&mut self, scripts: &NativeScripts) -> usize { + scripts.0.iter().for_each(|s: &NativeScript| { + let hash = s.hash(); + if self.input_types.scripts.contains_key(&hash) { + self.input_types.scripts.insert( + hash, + Some(ScriptWitnessType::NativeScriptWitness(s.clone())), + ); + } + }); + self.count_missing_input_scripts() + } + + /// Try adding the specified scripts as witnesses for ALREADY ADDED script inputs + /// Any scripts that don't match any of the previously added inputs will be ignored + /// Returns the number of remaining required missing witness scripts + /// Use `.count_missing_input_scripts` to find the number of still missing scripts + pub fn add_required_plutus_input_scripts(&mut self, scripts: &PlutusWitnesses) -> usize { + scripts.0.iter().for_each(|s: &PlutusWitness| { + let hash = s.script.hash(); + if self.input_types.scripts.contains_key(&hash) { + self.input_types.scripts.insert( + hash, + Some(ScriptWitnessType::PlutusScriptWitness(s.clone())), + ); + } + }); + self.count_missing_input_scripts() + } + + /// Returns a copy of the current script input witness scripts in the builder + pub fn get_native_input_scripts(&self) -> Option { + let mut scripts = NativeScripts::new(); + self.input_types.scripts.values().for_each(|option| { + if let Some(ScriptWitnessType::NativeScriptWitness(s)) = option { + scripts.add(&s); + } + }); + if scripts.len() > 0 { Some(scripts) } else { None } + } + + /// Returns a copy of the current plutus input witness scripts in the builder. + /// NOTE: each plutus witness will be cloned with a specific corresponding input index + pub fn get_plutus_input_scripts(&self) -> Option { + /* + * === EXPLANATION === + * The `Redeemer` object contains the `.index` field which is supposed to point + * exactly to the index of the corresponding input in the inputs array. We want to + * simplify and automate this as much as possible for the user to not have to care about it. + * + * For this we store the script hash along with the input, when it was registered, and + * now we create a map of script hashes to their input indexes. + * + * The registered witnesses are then each cloned with the new correct redeemer input index. + */ + let script_hash_index_map: BTreeMap<&ScriptHash, BigNum> = self.inputs.values().enumerate() + .fold(BTreeMap::new(), |mut m, (i, (_, hash_option))| { + if let Some(hash) = hash_option { + m.insert(hash, to_bignum(i as u64)); + } + m + }); + let mut scripts = PlutusWitnesses::new(); + self.input_types.scripts.iter().for_each(|(hash, option)| { + if let Some(ScriptWitnessType::PlutusScriptWitness(s)) = option { + if let Some(idx) = script_hash_index_map.get(&hash) { + scripts.add(&s.clone_with_redeemer_index(&idx)); + } + } + }); + if scripts.len() > 0 { Some(scripts) } else { None } + } + + pub(crate) fn iter(&self) -> impl std::iter::Iterator + '_ { + self.inputs.values().map(|(i, _)| i) + } + + pub fn len(&self) -> usize { + self.inputs.len() + } + + pub fn add_required_signer(&mut self, key: &Ed25519KeyHash) { + self.input_types.vkeys.insert(key.clone()); + } + + pub fn add_required_signers(&mut self, keys: &RequiredSigners) { + keys.0.iter().for_each(|k| self.add_required_signer(k)); + } + + pub fn inputs(&self) -> TransactionInputs { + TransactionInputs( + self.inputs.values() + .map(|(ref tx_builder_input, _)| tx_builder_input.input.clone()).collect() + ) + } + + pub fn inputs_option(&self) -> Option { + if self.len() > 0 { Some(self.inputs()) } else { None } + } +} + +impl From<&TxInputsBuilder> for RequiredSignersSet { + fn from(inputs: &TxInputsBuilder) -> Self { + let mut set = inputs.input_types.vkeys.clone(); + inputs.input_types.scripts.values().for_each(|swt: &Option| { + if let Some(ScriptWitnessType::NativeScriptWitness(s)) = swt { + RequiredSignersSet::from(s).iter().for_each(|k| { set.insert(k.clone()); }); + } + }); + set + } +} diff --git a/rust/src/utils.rs b/rust/src/utils.rs index 1ea74e8a..7705e860 100644 --- a/rust/src/utils.rs +++ b/rust/src/utils.rs @@ -4,6 +4,7 @@ use serde_json; use std::{collections::HashMap, io::{BufRead, Seek, Write}}; use std::convert::{TryFrom}; use itertools::Itertools; +use num_bigint::Sign; use std::ops::{Rem, Div, Sub}; use super::*; @@ -187,6 +188,10 @@ impl BigNum { Self(0) } + pub fn one() -> Self { + Self(1) + } + pub fn is_zero(&self) -> bool { self.0 == 0 } @@ -275,6 +280,11 @@ pub fn from_bignum(val: &BigNum) -> u64 { val.0 } + +pub fn to_bigint(val: u64) -> BigInt { + BigInt::from_str(&val.to_string()).unwrap() +} + // Specifies an amount of ADA in terms of lovelace pub type Coin = BigNum; @@ -713,12 +723,18 @@ to_from_bytes!(BigInt); #[wasm_bindgen] impl BigInt { + + pub fn is_zero(&self) -> bool { + self.0.sign() == Sign::NoSign + } + pub fn as_u64(&self) -> Option { let (sign, u64_digits) = self.0.to_u64_digits(); if sign == num_bigint::Sign::Minus { return None; } match u64_digits.len() { + 0 => Some(to_bignum(0)), 1 => Some(to_bignum(*u64_digits.first().unwrap())), _ => None, } @@ -728,12 +744,36 @@ impl BigInt { use std::str::FromStr; num_bigint::BigInt::from_str(text) .map_err(|e| JsError::from_str(&format! {"{:?}", e})) - .map(BigInt) + .map(Self) } pub fn to_str(&self) -> String { self.0.to_string() } + + pub fn add(&self, other: &BigInt) -> BigInt { + Self(&self.0 + &other.0) + } + + pub fn mul(&self, other: &BigInt) -> BigInt { + Self(&self.0 * &other.0) + } + + pub fn one() -> BigInt { + use std::str::FromStr; + Self(num_bigint::BigInt::from_str("1").unwrap()) + } + + pub fn increment(&self) -> BigInt { + self.add(&Self::one()) + } + + pub fn div_ceil(&self, other: &BigInt) -> BigInt { + use num_integer::Integer; + let (res, rem) = self.0.div_rem(&other.0); + let result = Self(res); + if Self(rem).is_zero() { result } else { result.increment() } + } } impl cbor_event::se::Serialize for BigInt { @@ -2366,4 +2406,60 @@ mod tests { assert_eq!(y.to_str(), "-18446744073709551616"); assert_eq!(bytes_y, y.to_bytes()); } + + #[test] + fn test_bigint_add() { + assert_eq!( + to_bigint(10).add(&to_bigint(20)), + to_bigint(30), + ); + assert_eq!( + to_bigint(500).add(&to_bigint(800)), + to_bigint(1300), + ); + } + + #[test] + fn test_bigint_mul() { + assert_eq!( + to_bigint(10).mul(&to_bigint(20)), + to_bigint(200), + ); + assert_eq!( + to_bigint(500).mul(&to_bigint(800)), + to_bigint(400000), + ); + assert_eq!( + to_bigint(12).mul(&to_bigint(22)), + to_bigint(264), + ); + } + + #[test] + fn test_bigint_div_ceil() { + assert_eq!( + to_bigint(20).div_ceil(&to_bigint(10)), + to_bigint(2), + ); + assert_eq!( + to_bigint(20).div_ceil(&to_bigint(2)), + to_bigint(10), + ); + assert_eq!( + to_bigint(21).div_ceil(&to_bigint(2)), + to_bigint(11), + ); + assert_eq!( + to_bigint(6).div_ceil(&to_bigint(3)), + to_bigint(2), + ); + assert_eq!( + to_bigint(5).div_ceil(&to_bigint(3)), + to_bigint(2), + ); + assert_eq!( + to_bigint(7).div_ceil(&to_bigint(3)), + to_bigint(3), + ); + } }