From 0cff0ae31c2c59c793d7dfbad5638c767951a816 Mon Sep 17 00:00:00 2001 From: Pedro Date: Fri, 29 Oct 2021 16:34:49 -0300 Subject: [PATCH 1/5] add test for asserting the overflow error currently happens before adding the fix for this error, we first make add test with a situation which leads to the exact error we are expecting, in this case the "NFTs too large for change output" error --- rust/src/tx_builder.rs | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index b9af6f3d..0145c23d 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -1891,4 +1891,65 @@ mod tests { assert!(tx_builder.add_change_if_needed(&change_addr).is_err()) } + + #[test] + fn add_change_fails_when_nfts_too_large_for_output() { + let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); + let minimum_utxo_value = to_bignum(1); + let max_value_size = 100; // super low max output size to test with fewer assets + let mut tx_builder = TransactionBuilder::new( + &linear_fee, + &minimum_utxo_value, + &to_bignum(0), + &to_bignum(0), + max_value_size, + MAX_TX_SIZE + ); + + let policy_id = PolicyID::from([0u8; 28]); + let names = [ + AssetName::new(vec![99u8; 32]).unwrap(), + AssetName::new(vec![0u8, 1, 2, 3]).unwrap(), + AssetName::new(vec![4u8, 5, 6, 7]).unwrap(), + AssetName::new(vec![5u8, 5, 6, 7]).unwrap(), + AssetName::new(vec![6u8, 5, 6, 7]).unwrap(), + ]; + let assets = names + .iter() + .fold(Assets::new(), |mut a, name| { + a.insert(&name, &to_bignum(500)); + a + }); + let mut multiasset = MultiAsset::new(); + multiasset.insert(&policy_id, &assets); + + let mut input_value = Value::new(&to_bignum(10)); + input_value.set_multiasset(&multiasset); + + tx_builder.add_input( + &ByronAddress::from_base58("Ae2tdPwUPEZ5uzkzh1o2DHECiUi3iugvnnKHRisPgRRP3CTF4KCMvy54Xd3").unwrap().to_address(), + &TransactionInput::new( + &genesis_id(), + 0 + ), + &input_value + ); + + let output_addr = ByronAddress::from_base58("Ae2tdPwUPEZD9QQf2ZrcYV34pYJwxK4vqXaF8EXkup1eYH73zUScHReM42b").unwrap().to_address(); + let output_amount = Value::new(&to_bignum(1)); + + tx_builder + .add_output(&TransactionOutput::new(&output_addr, &output_amount)) + .unwrap(); + + let change_addr = ByronAddress::from_base58("Ae2tdPwUPEZGUEsuMAhvDcy94LKsZxDjCbgaiBBMgYpR8sKf96xJmit7Eho").unwrap().to_address(); + + let add_change_result = tx_builder.add_change_if_needed(&change_addr); + assert!(add_change_result.is_err()); + + let err: JsError = add_change_result.unwrap_err(); + let error_msg: String = err.as_string().unwrap(); + + assert!(error_msg == "NFTs too large for change output"); + } } From 5424a664e120cc6500f93e5ccb361ef0172ff9ef Mon Sep 17 00:00:00 2001 From: Pedro Date: Fri, 29 Oct 2021 18:41:18 -0300 Subject: [PATCH 2/5] Split into multiple outputs in case of overflow because of assets --- rust/src/tx_builder.rs | 96 +++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index 0145c23d..a1227801 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -418,9 +418,25 @@ impl TransactionBuilder { } let change_estimator = input_total.checked_sub(&output_total)?; if has_assets(change_estimator.multiasset()) { - fn pack_nfts_for_change(max_value_size: u32, change_address: &Address, change_estimator: &Value) -> Result { + fn will_adding_asset_make_output_overflow(output: &TransactionOutput, current_assets: &Assets, asset_to_add: (PolicyID, AssetName, BigNum), max_value_size: u32) -> bool { + let (policy, asset_name, value) = asset_to_add; + let mut current_assets_clone = current_assets.clone(); + current_assets_clone.insert(&asset_name, &value); + let mut amount_clone = output.amount.clone(); + let mut val = Value::new(&Coin::zero()); + let mut ma = MultiAsset::new(); + + ma.insert(&policy, ¤t_assets_clone); + val.set_multiasset(&ma); + amount_clone = amount_clone.checked_add(&val).unwrap(); + + amount_clone.to_bytes().len() > max_value_size as usize + } + fn pack_nfts_for_change(max_value_size: u32, change_address: &Address, change_estimator: &Value) -> Result, JsError> { // we insert the entire available ADA temporarily here since that could potentially impact the size // as it could be 1, 2 3 or 4 bytes for Coin. + let mut change_assets: Vec = Vec::new(); + let mut base_coin = Value::new(&change_estimator.coin()); base_coin.set_multiasset(&MultiAsset::new()); let mut output = TransactionOutput::new(change_address, &base_coin); @@ -448,18 +464,53 @@ impl TransactionBuilder { // performance becomes an issue. //let extra_bytes = policy.to_bytes().len() + assets.to_bytes().len() + 2 + cbor_len_diff; //if bytes_used + extra_bytes <= max_value_size as usize { - let old_amount = output.amount.clone(); + let mut old_amount = output.amount.clone(); let mut val = Value::new(&Coin::zero()); let mut next_nft = MultiAsset::new(); - next_nft.insert(policy, assets); + + let asset_names = assets.keys(); + let mut rebuilt_assets = Assets::new(); + for n in 0..asset_names.len() { + let asset_name = asset_names.get(n); + let value = assets.get(&asset_name).unwrap(); + + if will_adding_asset_make_output_overflow(&output, &rebuilt_assets, (policy.clone(), asset_name.clone(), value), max_value_size) { + // if we got here, this means we will run into a overflow error, + // so we want to split into multiple outputs, for that we... + + // 1. insert the current assets as they are, as this won't overflow + next_nft.insert(policy, &rebuilt_assets); + val.set_multiasset(&next_nft); + output.amount = output.amount.checked_add(&val)?; + change_assets.push(output.amount.multiasset().unwrap()); + + // 2. create a new output with the base coin value as zero + base_coin = Value::new(&Coin::zero()); + base_coin.set_multiasset(&MultiAsset::new()); + output = TransactionOutput::new(change_address, &base_coin); + + // 3. continue building the new output from the asset we stopped + old_amount = output.amount.clone(); + val = Value::new(&Coin::zero()); + next_nft = MultiAsset::new(); + + rebuilt_assets = Assets::new(); + } + + rebuilt_assets.insert(&asset_name, &value); + } + + next_nft.insert(policy, &rebuilt_assets); val.set_multiasset(&next_nft); output.amount = output.amount.checked_add(&val)?; + if output.amount.to_bytes().len() > max_value_size as usize { output.amount = old_amount; break; } } - Ok(output.amount.multiasset().unwrap()) + change_assets.push(output.amount.multiasset().unwrap()); + Ok(change_assets) } let mut change_left = input_total.checked_sub(&output_total)?; let mut new_fee = fee.clone(); @@ -467,25 +518,27 @@ impl TransactionBuilder { // which surpass the max UTXO size limit let minimum_utxo_val = self.minimum_utxo_val; while let Some(Ordering::Greater) = change_left.multiasset.as_ref().map_or_else(|| None, |ma| ma.partial_cmp(&MultiAsset::new())) { - let nft_change = pack_nfts_for_change(self.max_value_size, address, &change_left)?; - if nft_change.len() == 0 { + let nft_changes = pack_nfts_for_change(self.max_value_size, address, &change_left)?; + if nft_changes.len() == 0 { // this likely should never happen return Err(JsError::from_str("NFTs too large for change output")); } // we only add the minimum needed (for now) to cover this output let mut change_value = Value::new(&Coin::zero()); - change_value.set_multiasset(&nft_change); - let min_ada = min_ada_required(&change_value, &minimum_utxo_val); - change_value.set_coin(&min_ada); - let change_output = TransactionOutput::new(address, &change_value); - // increase fee - let fee_for_change = self.fee_for_output(&change_output)?; - new_fee = new_fee.checked_add(&fee_for_change)?; - if change_left.coin() < min_ada.checked_add(&new_fee)? { - return Err(JsError::from_str("Not enough ADA leftover to include non-ADA assets in a change address")); + for nft_change in nft_changes.iter() { + change_value.set_multiasset(&nft_change); + let min_ada = min_ada_required(&change_value, &minimum_utxo_val); + change_value.set_coin(&min_ada); + let change_output = TransactionOutput::new(address, &change_value); + // increase fee + let fee_for_change = self.fee_for_output(&change_output)?; + new_fee = new_fee.checked_add(&fee_for_change)?; + if change_left.coin() < min_ada.checked_add(&new_fee)? { + return Err(JsError::from_str("Not enough ADA leftover to include non-ADA assets in a change address")); + } + change_left = change_left.checked_sub(&change_value)?; + self.add_output(&change_output)?; } - change_left = change_left.checked_sub(&change_value)?; - self.add_output(&change_output)?; } change_left = change_left.checked_sub(&Value::new(&new_fee))?; // add potentially a separate pure ADA change output @@ -1893,7 +1946,7 @@ mod tests { } #[test] - fn add_change_fails_when_nfts_too_large_for_output() { + fn add_change_splits_change_into_multiple_outputs_when_nfts_overflow_output_size() { let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); let minimum_utxo_value = to_bignum(1); let max_value_size = 100; // super low max output size to test with fewer assets @@ -1945,11 +1998,6 @@ mod tests { let change_addr = ByronAddress::from_base58("Ae2tdPwUPEZGUEsuMAhvDcy94LKsZxDjCbgaiBBMgYpR8sKf96xJmit7Eho").unwrap().to_address(); let add_change_result = tx_builder.add_change_if_needed(&change_addr); - assert!(add_change_result.is_err()); - - let err: JsError = add_change_result.unwrap_err(); - let error_msg: String = err.as_string().unwrap(); - - assert!(error_msg == "NFTs too large for change output"); + assert!(add_change_result.is_ok()); } } From cb83dc2e38037618e6ad2bad37519f18dc2a20cf Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Mon, 8 Nov 2021 00:55:50 +0300 Subject: [PATCH 3/5] Fixed and extended the test a bit --- rust/src/tx_builder.rs | 45 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index 557e2617..bd679747 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -2378,16 +2378,16 @@ mod tests { #[test] fn add_change_splits_change_into_multiple_outputs_when_nfts_overflow_output_size() { let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); - let minimum_utxo_value = to_bignum(1); let max_value_size = 100; // super low max output size to test with fewer assets let mut tx_builder = TransactionBuilder::new( &linear_fee, - &minimum_utxo_value, &to_bignum(0), &to_bignum(0), max_value_size, - MAX_TX_SIZE + MAX_TX_SIZE, + &to_bignum(1), ); + tx_builder.set_prefer_pure_change(true); let policy_id = PolicyID::from([0u8; 28]); let names = [ @@ -2406,7 +2406,7 @@ mod tests { let mut multiasset = MultiAsset::new(); multiasset.insert(&policy_id, &assets); - let mut input_value = Value::new(&to_bignum(10)); + let mut input_value = Value::new(&to_bignum(300)); input_value.set_multiasset(&multiasset); tx_builder.add_input( @@ -2419,7 +2419,7 @@ mod tests { ); let output_addr = ByronAddress::from_base58("Ae2tdPwUPEZD9QQf2ZrcYV34pYJwxK4vqXaF8EXkup1eYH73zUScHReM42b").unwrap().to_address(); - let output_amount = Value::new(&to_bignum(1)); + let output_amount = Value::new(&to_bignum(50)); tx_builder .add_output(&TransactionOutput::new(&output_addr, &output_amount)) @@ -2429,5 +2429,40 @@ mod tests { let add_change_result = tx_builder.add_change_if_needed(&change_addr); assert!(add_change_result.is_ok()); + assert_eq!(tx_builder.outputs.len(), 4); + + let change1 = tx_builder.outputs.get(1); + let change2 = tx_builder.outputs.get(2); + let change3 = tx_builder.outputs.get(3); + + assert_eq!(change1.address, change_addr); + assert_eq!(change1.address, change2.address); + assert_eq!(change1.address, change3.address); + + assert_eq!(change1.amount.coin, to_bignum(45)); + assert_eq!(change2.amount.coin, to_bignum(42)); + assert_eq!(change3.amount.coin, to_bignum(162)); + + assert!(change1.amount.multiasset.is_some()); + assert!(change2.amount.multiasset.is_some()); + assert!(change3.amount.multiasset.is_none()); // purified + + let masset1 = change1.amount.multiasset.unwrap(); + let masset2 = change2.amount.multiasset.unwrap(); + + assert_eq!(masset1.keys().len(), 1); + assert_eq!(masset1.keys(), masset2.keys()); + + let asset1 = masset1.get(&policy_id).unwrap(); + let asset2 = masset2.get(&policy_id).unwrap(); + assert_eq!(asset1.len(), 4); + assert_eq!(asset2.len(), 1); + + names.iter().for_each(|name| { + let v1 = asset1.get(name); + let v2 = asset2.get(name); + assert_ne!(v1.is_some(), v2.is_some()); + assert_eq!(v1.or(v2).unwrap(), to_bignum(500)); + }); } } From 96b38371ad86d432ce0ed3217494f20fbd59b7de Mon Sep 17 00:00:00 2001 From: Pedro Date: Tue, 23 Nov 2021 16:48:14 -0300 Subject: [PATCH 4/5] Merge master --- .github/workflows/pr-checks.yml | 27 + package.json | 10 +- rust/pkg/cardano_serialization_lib.js.flow | 519 +++++-- rust/src/address.rs | 2 +- rust/src/crypto.rs | 3 +- rust/src/error.rs | 3 +- rust/src/lib.rs | 171 +++ rust/src/metadata.rs | 12 +- rust/src/plutus.rs | 35 +- rust/src/serialization.rs | 66 +- rust/src/tx_builder.rs | 1538 ++++++++++++++++---- rust/src/utils.rs | 119 +- 12 files changed, 2035 insertions(+), 470 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 41296296..87bc26ea 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -41,3 +41,30 @@ jobs: - name: rust:build-browser run: | npm run rust:build-browser + check-warns: + if: github.event.review && (github.event.review.state == 'approved' || contains(github.event.review.body, '/check')) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - uses: actions/setup-node@v1 + with: + node-version: '12.18.1' + - name: Cache node modules + uses: actions/cache@v1 + with: + path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: prepare-rust + run: | + rustup install stable + rustup target add wasm32-unknown-unknown --toolchain stable + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - name: rust:check-warnings + run: | + npm run rust:check-warnings diff --git a/package.json b/package.json index d2104c81..16b5cd5f 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,16 @@ "rust:build-asm": "(rimraf ./rust/pkg && cd rust; wasm-pack build --target=browser; wasm-pack pack) && npm run asm:build && npm run js:flowgen", "rust:publish": "cd rust && cargo publish && cd ../", "asm:build": "./binaryen/bin/wasm2js ./rust/pkg/cardano_serialization_lib_bg.wasm --output ./rust/pkg/cardano_serialization_lib.asm.js && node ./scripts/wasm-to-asm", + "rust:check-warnings": "(cd rust; RUSTFLAGS=\"-D warnings\" cargo +stable build)", "rust:test": "(cd rust; cargo test)", "js:flowgen": "flowgen ./rust/pkg/cardano_serialization_lib.d.ts -o ./rust/pkg/cardano_serialization_lib.js.flow --add-flow-header", "js:prepublish": "npm run rust:test && rimraf ./publish && cp -r ./rust/pkg ./publish && cp README.md publish/ && cp LICENSE publish/", - "js:publish-nodejs": "npm run rust:build-nodejs && npm run js:prepublish && node ./scripts/publish-helper -nodejs && cd publish && npm publish --access public", - "js:publish-browser": "npm run rust:build-browser && npm run js:prepublish && node ./scripts/publish-helper -browser && cd publish && npm publish --access public", - "js:publish-asm": "npm run rust:build-asm && npm run js:prepublish && node ./scripts/publish-helper -asmjs && cd publish && npm publish --access public", + "js:publish-nodejs:prod": "npm run rust:build-nodejs && npm run js:prepublish && node ./scripts/publish-helper -nodejs && cd publish && npm publish --access public", + "js:publish-nodejs:beta": "npm run rust:build-nodejs && npm run js:prepublish && node ./scripts/publish-helper -nodejs && cd publish && npm publish --tag beta --access public", + "js:publish-browser:prod": "npm run rust:build-browser && npm run js:prepublish && node ./scripts/publish-helper -browser && cd publish && npm publish --access public", + "js:publish-browser:beta": "npm run rust:build-browser && npm run js:prepublish && node ./scripts/publish-helper -browser && cd publish && npm publish --tag beta --access public", + "js:publish-asm:prod": "npm run rust:build-asm && npm run js:prepublish && node ./scripts/publish-helper -asmjs && cd publish && npm publish --access public", + "js:publish-asm:beta": "npm run rust:build-asm && npm run js:prepublish && node ./scripts/publish-helper -asmjs && cd publish && npm publish --tag beta --access public", "postinstall": "git submodule update --init --recursive && cd binaryen; cmake . && make" }, "husky": { diff --git a/rust/pkg/cardano_serialization_lib.js.flow b/rust/pkg/cardano_serialization_lib.js.flow index 56fdd2bc..e08a0aeb 100644 --- a/rust/pkg/cardano_serialization_lib.js.flow +++ b/rust/pkg/cardano_serialization_lib.js.flow @@ -5,73 +5,6 @@ * @flow */ -/** - * @param {Uint8Array} bytes - * @returns {TransactionMetadatum} - */ -declare export function encode_arbitrary_bytes_as_metadatum( - bytes: Uint8Array -): TransactionMetadatum; - -/** - * @param {TransactionMetadatum} metadata - * @returns {Uint8Array} - */ -declare export function decode_arbitrary_bytes_from_metadatum( - metadata: TransactionMetadatum -): Uint8Array; - -/** - * @param {string} json - * @param {number} schema - * @returns {TransactionMetadatum} - */ -declare export function encode_json_str_to_metadatum( - json: string, - schema: number -): TransactionMetadatum; - -/** - * @param {TransactionMetadatum} metadatum - * @param {number} schema - * @returns {string} - */ -declare export function decode_metadatum_to_json_str( - metadatum: TransactionMetadatum, - schema: number -): string; - -/** - * @param {string} password - * @param {string} salt - * @param {string} nonce - * @param {string} data - * @returns {string} - */ -declare export function encrypt_with_password( - password: string, - salt: string, - nonce: string, - data: string -): string; - -/** - * @param {string} password - * @param {string} data - * @returns {string} - */ -declare export function decrypt_with_password( - password: string, - data: string -): string; - -/** - * @param {Transaction} tx - * @param {LinearFee} linear_fee - * @returns {BigNum} - */ -declare export function min_fee(tx: Transaction, linear_fee: LinearFee): BigNum; - /** * @param {TransactionHash} tx_body_hash * @param {ByronAddress} addr @@ -166,12 +99,14 @@ declare export function get_deposit( /** * @param {Value} assets - * @param {BigNum} minimum_utxo_val + * @param {boolean} has_data_hash + * @param {BigNum} coins_per_utxo_word * @returns {BigNum} */ declare export function min_ada_required( assets: Value, - minimum_utxo_val: BigNum + has_data_hash: boolean, + coins_per_utxo_word: BigNum ): BigNum; /** @@ -179,22 +114,88 @@ declare export function min_ada_required( * and returns a NativeScript. * Cardano Wallet and Node styles are supported. * - * * wallet: https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/listSharedWallets - * * payment_script_template is the schema + * * wallet: https://github.com/input-output-hk/cardano-wallet/blob/master/specifications/api/swagger.yaml * * node: https://github.com/input-output-hk/cardano-node/blob/master/doc/reference/simple-scripts.md * - * self_address is expected to be a Bip32PublicKey as hex-encoded bytes + * self_xpub is expected to be a Bip32PublicKey as hex-encoded bytes * @param {string} json - * @param {string} self_address + * @param {string} self_xpub * @param {number} schema * @returns {NativeScript} */ declare export function encode_json_str_to_native_script( json: string, - self_address: string, + self_xpub: string, schema: number ): NativeScript; +/** + * @param {Uint8Array} bytes + * @returns {TransactionMetadatum} + */ +declare export function encode_arbitrary_bytes_as_metadatum( + bytes: Uint8Array +): TransactionMetadatum; + +/** + * @param {TransactionMetadatum} metadata + * @returns {Uint8Array} + */ +declare export function decode_arbitrary_bytes_from_metadatum( + metadata: TransactionMetadatum +): Uint8Array; + +/** + * @param {string} json + * @param {number} schema + * @returns {TransactionMetadatum} + */ +declare export function encode_json_str_to_metadatum( + json: string, + schema: number +): TransactionMetadatum; + +/** + * @param {TransactionMetadatum} metadatum + * @param {number} schema + * @returns {string} + */ +declare export function decode_metadatum_to_json_str( + metadatum: TransactionMetadatum, + schema: number +): string; + +/** + * @param {string} password + * @param {string} salt + * @param {string} nonce + * @param {string} data + * @returns {string} + */ +declare export function encrypt_with_password( + password: string, + salt: string, + nonce: string, + data: string +): string; + +/** + * @param {string} password + * @param {string} data + * @returns {string} + */ +declare export function decrypt_with_password( + password: string, + data: string +): string; + +/** + * @param {Transaction} tx + * @param {LinearFee} linear_fee + * @returns {BigNum} + */ +declare export function min_fee(tx: Transaction, linear_fee: LinearFee): BigNum; + /** */ @@ -267,21 +268,37 @@ declare export var NetworkIdKind: {| /** */ -declare export var TransactionMetadatumKind: {| - +MetadataMap: 0, // 0 - +MetadataList: 1, // 1 - +Int: 2, // 2 - +Bytes: 3, // 3 - +Text: 4, // 4 +declare export var LanguageKind: {| + +PlutusV1: 0, // 0 |}; /** */ -declare export var MetadataJsonSchema: {| - +NoConversions: 0, // 0 - +BasicConversions: 1, // 1 - +DetailedSchema: 2, // 2 +declare export var PlutusDataKind: {| + +ConstrPlutusData: 0, // 0 + +Map: 1, // 1 + +List: 2, // 2 + +Integer: 3, // 3 + +Bytes: 4, // 4 +|}; + +/** + */ + +declare export var RedeemerTagKind: {| + +Spend: 0, // 0 + +Mint: 1, // 1 + +Cert: 2, // 2 + +Reward: 3, // 3 +|}; + +/** + */ + +declare export var CoinSelectionStrategyCIP2: {| + +LargestFirst: 0, // 0 + +RandomImprove: 1, // 1 |}; /** @@ -296,34 +313,29 @@ declare export var ScriptSchema: {| /** */ -declare export var StakeCredKind: {| - +Key: 0, // 0 - +Script: 1, // 1 -|}; - -declare export var LanguageKind: {| - +PlutusV1: 0, // 0 +declare export var TransactionMetadatumKind: {| + +MetadataMap: 0, // 0 + +MetadataList: 1, // 1 + +Int: 2, // 2 + +Bytes: 3, // 3 + +Text: 4, // 4 |}; /** */ -declare export var PlutusDataKind: {| - +ConstrPlutusData: 0, // 0 - +Map: 1, // 1 - +List: 2, // 2 - +Integer: 3, // 3 - +Bytes: 4, // 4 +declare export var MetadataJsonSchema: {| + +NoConversions: 0, // 0 + +BasicConversions: 1, // 1 + +DetailedSchema: 2, // 2 |}; /** */ -declare export var RedeemerTagKind: {| - +Spend: 0, // 0 - +Mint: 1, // 1 - +Cert: 2, // 2 - +Reward: 3, // 3 +declare export var StakeCredKind: {| + +Key: 0, // 0 + +Script: 1, // 1 |}; /** @@ -1278,9 +1290,9 @@ declare export class ConstrPlutusData { static from_bytes(bytes: Uint8Array): ConstrPlutusData; /** - * @returns {Int} + * @returns {BigNum} */ - tag(): Int; + alternative(): BigNum; /** * @returns {PlutusList} @@ -1288,11 +1300,11 @@ declare export class ConstrPlutusData { data(): PlutusList; /** - * @param {Int} tag + * @param {BigNum} alternative * @param {PlutusList} data * @returns {ConstrPlutusData} */ - static new(tag: Int, data: PlutusList): ConstrPlutusData; + static new(alternative: BigNum, data: PlutusList): ConstrPlutusData; } /** */ @@ -1992,19 +2004,55 @@ declare export class Int { is_positive(): boolean; /** + * BigNum can only contain unsigned u64 values + * + * This function will return the BigNum representation + * only in case the underlying i128 value is positive. + * + * Otherwise nothing will be returned (undefined). * @returns {BigNum | void} */ as_positive(): BigNum | void; /** + * BigNum can only contain unsigned u64 values + * + * This function will return the *absolute* BigNum representation + * only in case the underlying i128 value is negative. + * + * Otherwise nothing will be returned (undefined). * @returns {BigNum | void} */ as_negative(): BigNum | void; /** + * !!! DEPRECATED !!! + * Returns an i32 value in case the underlying original i128 value is within the limits. + * Otherwise will just return an empty value (undefined). * @returns {number | void} */ as_i32(): number | void; + + /** + * Returns the underlying value converted to i32 if possible (within limits) + * Otherwise will just return an empty value (undefined). + * @returns {number | void} + */ + as_i32_or_nothing(): number | void; + + /** + * Returns the underlying value converted to i32 if possible (within limits) + * JsError in case of out of boundary overflow + * @returns {number} + */ + as_i32_or_fail(): number; + + /** + * Returns string representation of the underlying i128 value directly. + * Might contain the minus sign (-) in case of negative value. + * @returns {string} + */ + to_str(): string; } /** */ @@ -2386,6 +2434,13 @@ declare export class Mint { */ static new(): Mint; + /** + * @param {ScriptHash} key + * @param {MintAssets} value + * @returns {Mint} + */ + static new_from_entry(key: ScriptHash, value: MintAssets): Mint; + /** * @returns {number} */ @@ -2408,6 +2463,18 @@ declare export class Mint { * @returns {ScriptHashes} */ keys(): ScriptHashes; + + /** + * Returns the multiasset where only positive (minting) entries are present + * @returns {MultiAsset} + */ + as_positive_multiasset(): MultiAsset; + + /** + * Returns the multiasset where only negative (burning) entries are present + * @returns {MultiAsset} + */ + as_negative_multiasset(): MultiAsset; } /** */ @@ -2419,6 +2486,13 @@ declare export class MintAssets { */ static new(): MintAssets; + /** + * @param {AssetName} key + * @param {Int} value + * @returns {MintAssets} + */ + static new_from_entry(key: AssetName, value: Int): MintAssets; + /** * @returns {number} */ @@ -2620,7 +2694,7 @@ declare export class NativeScript { /** * @param {number} namespace - * @returns {Ed25519KeyHash} + * @returns {ScriptHash} */ hash(namespace: number): ScriptHash; @@ -3382,10 +3456,18 @@ declare export class PrivateKey { static generate_ed25519extended(): PrivateKey; /** - * @param {string} bech_str + * Get private key from its bech32 representation + * ```javascript + * PrivateKey.from_bech32('ed25519_sk1ahfetf02qwwg4dkq7mgp4a25lx5vh9920cr5wnxmpzz9906qvm8qwvlts0'); + * ``` + * For an extended 25519 key + * ```javascript + * PrivateKey.from_bech32('ed25519e_sk1gqwl4szuwwh6d0yk3nsqcc6xxc3fpvjlevgwvt60df59v8zd8f8prazt8ln3lmz096ux3xvhhvm3ca9wj2yctdh3pnw0szrma07rt5gl748fp'); + * ``` + * @param {string} bech32_str * @returns {PrivateKey} */ - static from_bech32(bech_str: string): PrivateKey; + static from_bech32(bech32_str: string): PrivateKey; /** * @returns {string} @@ -4902,6 +4984,22 @@ declare export class TransactionBuilder { free(): void; /** + * This automatically selects and adds inputs from {inputs} consisting of just enough to cover + * the outputs that have already been added. + * This should be called after adding all certs/outputs/etc and will be an error otherwise. + * Uses CIP2: https://github.com/cardano-foundation/CIPs/blob/master/CIP-0002/CIP-0002.md + * Adding a change output must be called after via TransactionBuilder::add_change_if_needed() + * This function, diverging from CIP2, takes into account fees and will attempt to add additional + * inputs to cover the minimum fees. This does not, however, set the txbuilder's fee. + * @param {TransactionUnspentOutputs} inputs + * @param {number} strategy + */ + add_inputs_from(inputs: TransactionUnspentOutputs, strategy: number): 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 + * 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 @@ -4955,6 +5053,45 @@ declare export class TransactionBuilder { ): BigNum; /** + * Add output by specifying the Address and Value + * @param {Address} address + * @param {Value} amount + */ + add_output_amount(address: Address, amount: Value): void; + + /** + * Add output by specifying the Address and Coin (BigNum) + * Output will have no additional assets + * @param {Address} address + * @param {BigNum} coin + */ + add_output_coin(address: Address, coin: BigNum): void; + + /** + * Add output by specifying the Address, the Coin (BigNum), and the MultiAsset + * @param {Address} address + * @param {BigNum} coin + * @param {MultiAsset} multiasset + */ + add_output_coin_and_asset( + address: Address, + coin: BigNum, + multiasset: MultiAsset + ): void; + + /** + * Add output by specifying the Address and the MultiAsset + * The output will be set to contain the minimum required amount of Coin + * @param {Address} address + * @param {MultiAsset} multiasset + */ + add_output_asset_and_min_required_coin( + address: Address, + multiasset: MultiAsset + ): void; + + /** + * Add explicit output via a TransactionOutput object * @param {TransactionOutput} output */ add_output(output: TransactionOutput): void; @@ -4992,31 +5129,143 @@ declare export class TransactionBuilder { set_withdrawals(withdrawals: Withdrawals): void; /** + * @returns {AuxiliaryData | void} + */ + get_auxiliary_data(): AuxiliaryData | void; + + /** + * Set explicit auxiliary data via an AuxiliaryData object + * It might contain some metadata plus native or Plutus scripts * @param {AuxiliaryData} auxiliary_data */ set_auxiliary_data(auxiliary_data: AuxiliaryData): void; /** + * Set metadata using a GeneralTransactionMetadata object + * It will be set to the existing or new auxiliary data in this builder + * @param {GeneralTransactionMetadata} metadata + */ + set_metadata(metadata: GeneralTransactionMetadata): void; + + /** + * Add a single metadatum using TransactionMetadatumLabel and TransactionMetadatum objects + * It will be securely added to existing or new metadata in this builder + * @param {BigNum} key + * @param {TransactionMetadatum} val + */ + add_metadatum(key: BigNum, val: TransactionMetadatum): void; + + /** + * Add a single JSON metadatum using a TransactionMetadatumLabel and a String + * It will be securely added to existing or new metadata in this builder + * @param {BigNum} key + * @param {string} val + */ + add_json_metadatum(key: BigNum, val: string): void; + + /** + * Add a single JSON metadatum using a TransactionMetadatumLabel, a String, and a MetadataJsonSchema object + * It will be securely added to existing or new metadata in this builder + * @param {BigNum} key + * @param {string} val + * @param {number} schema + */ + add_json_metadatum_with_schema( + key: BigNum, + val: string, + schema: number + ): void; + + /** + * Set explicit Mint object to this builder + * it will replace any previously existing mint + * @param {Mint} mint + */ + set_mint(mint: Mint): 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 {MintAssets} mint_assets + */ + set_mint_asset(policy_id: ScriptHash, 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 {AssetName} asset_name + * @param {Int} amount + */ + add_mint_asset( + policy_id: ScriptHash, + asset_name: AssetName, + amount: Int + ): void; + + /** + * Add a mint entry together with an output to this builder + * 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 {AssetName} asset_name + * @param {Int} amount + * @param {Address} address + * @param {BigNum} output_coin + */ + add_mint_asset_and_output( + policy_id: ScriptHash, + asset_name: AssetName, + amount: Int, + address: Address, + output_coin: BigNum + ): void; + + /** + * Add a mint entry together with an output to this builder + * Using a PolicyID, AssetName, Int for amount, and Address 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 and the minted asset + * The output will be set to contain the minimum required amount of Coin + * @param {ScriptHash} policy_id + * @param {AssetName} asset_name + * @param {Int} amount + * @param {Address} address + */ + add_mint_asset_and_output_min_required_coin( + policy_id: ScriptHash, + asset_name: AssetName, + amount: Int, + address: Address + ): void; + + /** + * If set to true, add_change_if_needed will try + * to put pure Coin in a separate output from assets * @param {boolean} prefer_pure_change */ set_prefer_pure_change(prefer_pure_change: boolean): void; /** * @param {LinearFee} linear_fee - * @param {BigNum} minimum_utxo_val * @param {BigNum} pool_deposit * @param {BigNum} key_deposit * @param {number} max_value_size * @param {number} max_tx_size + * @param {BigNum} coins_per_utxo_word * @returns {TransactionBuilder} */ static new( linear_fee: LinearFee, - minimum_utxo_val: BigNum, pool_deposit: BigNum, key_deposit: BigNum, max_value_size: number, - max_tx_size: number + max_tx_size: number, + coins_per_utxo_word: BigNum ): TransactionBuilder; /** @@ -5049,6 +5298,9 @@ declare export class TransactionBuilder { /** * Warning: this function will mutate the /fee/ field + * Make sure to call this function last after setting all other tx-body properties + * Editing inputs, outputs, mint, etc. after change been calculated + * might cause a mismatch in calculated fee versus the required fee * @param {Address} address * @returns {boolean} */ @@ -5065,10 +5317,21 @@ declare export class TransactionBuilder { output_sizes(): Uint32Array; /** + * Returns object the body of the new transaction + * Auxiliary data itself is not included + * You can use `get_auxiliary_date` 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: is_valid set to true + * @returns {Transaction} + */ + build_tx(): Transaction; + /** * warning: sum of all parts of a transaction must equal 0. You cannot just set the fee to the min value and forget about it * warning: min_fee may be slightly larger than the actual minimum fee (ex: a few lovelaces) @@ -5404,6 +5667,32 @@ declare export class TransactionUnspentOutput { */ output(): TransactionOutput; } +/** + */ +declare export class TransactionUnspentOutputs { + free(): void; + + /** + * @returns {TransactionUnspentOutputs} + */ + static new(): TransactionUnspentOutputs; + + /** + * @returns {number} + */ + len(): number; + + /** + * @param {number} index + * @returns {TransactionUnspentOutput} + */ + get(index: number): TransactionUnspentOutput; + + /** + * @param {TransactionUnspentOutput} elem + */ + add(elem: TransactionUnspentOutput): void; +} /** */ declare export class TransactionWitnessSet { @@ -5729,6 +6018,12 @@ declare export class Value { */ static new(coin: BigNum): Value; + /** + * @param {MultiAsset} multiasset + * @returns {Value} + */ + static new_from_assets(multiasset: MultiAsset): Value; + /** * @returns {Value} */ diff --git a/rust/src/address.rs b/rust/src/address.rs index 17021948..430930fd 100644 --- a/rust/src/address.rs +++ b/rust/src/address.rs @@ -440,7 +440,7 @@ impl Address { None => { // see CIP5 for bech32 prefix rules let prefix_header = match &self.0 { - AddrType::Reward(a) => "stake", + AddrType::Reward(_) => "stake", _ => "addr", }; let prefix_tail = match self.network_id()? { diff --git a/rust/src/crypto.rs b/rust/src/crypto.rs index 7b24c07c..93144db3 100644 --- a/rust/src/crypto.rs +++ b/rust/src/crypto.rs @@ -588,7 +588,7 @@ impl Deserialize for BootstrapWitness { } impl DeserializeEmbeddedGroup for BootstrapWitness { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let vkey = (|| -> Result<_, DeserializeError> { Ok(Vkey::deserialize(raw)?) })().map_err(|e| e.annotate("vkey"))?; @@ -953,7 +953,6 @@ impl cbor_event::se::Serialize for Nonce { impl Deserialize for Nonce { fn deserialize(raw: &mut Deserializer) -> Result { - use std::convert::TryInto; (|| -> Result { let len = raw.array()?; let hash = match raw.unsigned_integer()? { diff --git a/rust/src/error.rs b/rust/src/error.rs index bb165f3f..0557188b 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -1,5 +1,4 @@ -use cbor_event::{self, de::Deserializer, se::{Serialize, Serializer}}; -use std::io::{BufRead, Seek, Write}; +use cbor_event::{self}; use crate::chain_crypto; use super::*; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 68817968..600d91a4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2634,6 +2634,12 @@ impl MintAssets { Self(std::collections::BTreeMap::new()) } + pub fn new_from_entry(key: &AssetName, value: Int) -> Self { + let mut ma = MintAssets::new(); + ma.insert(key, value); + ma + } + pub fn len(&self) -> usize { self.0.len() } @@ -2663,6 +2669,12 @@ impl Mint { Self(std::collections::BTreeMap::new()) } + pub fn new_from_entry(key: &PolicyID, value: &MintAssets) -> Self { + let mut m = Mint::new(); + m.insert(key, value); + m + } + pub fn len(&self) -> usize { self.0.len() } @@ -2678,6 +2690,37 @@ impl Mint { pub fn keys(&self) -> PolicyIDs { ScriptHashes(self.0.iter().map(|(k, _v)| k.clone()).collect::>()) } + + fn as_multiasset(&self, is_positive: bool) -> MultiAsset { + self.0.iter().fold(MultiAsset::new(), | res, e | { + let assets: Assets = (e.1).0.iter().fold(Assets::new(), | res, e| { + let mut assets = res; + if e.1.is_positive() == is_positive { + let amount = match is_positive { + true => e.1.as_positive(), + false => e.1.as_negative(), + }; + assets.insert(e.0, &amount.unwrap()); + } + assets + }); + let mut ma = res; + if !assets.0.is_empty() { + ma.insert(e.0, &assets); + } + ma + }) + } + + /// Returns the multiasset where only positive (minting) entries are present + pub fn as_positive_multiasset(&self) -> MultiAsset { + self.as_multiasset(true) + } + + /// Returns the multiasset where only negative (burning) entries are present + pub fn as_negative_multiasset(&self) -> MultiAsset { + self.as_multiasset(false) + } } @@ -2762,4 +2805,132 @@ mod tests { assert_eq!(map2.keys(), AssetNames(vec![name33, name11, name22])); } + + #[test] + fn mint_to_multiasset() { + let policy_id1 = PolicyID::from([0u8; 28]); + let policy_id2 = PolicyID::from([1u8; 28]); + let name1 = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); + let name2 = AssetName::new(vec![0u8, 4, 5, 6]).unwrap(); + let amount1 = BigNum::from_str("1234").unwrap(); + let amount2 = BigNum::from_str("5678").unwrap(); + + let mut mass1 = MintAssets::new(); + mass1.insert(&name1, Int::new(&amount1)); + mass1.insert(&name2, Int::new(&amount2)); + + let mut mass2 = MintAssets::new(); + mass2.insert(&name1, Int::new(&amount2)); + mass2.insert(&name2, Int::new(&amount1)); + + let mut mint = Mint::new(); + mint.insert(&policy_id1, &mass1); + mint.insert(&policy_id2, &mass2); + + let multiasset = mint.as_positive_multiasset(); + assert_eq!(multiasset.len(), 2); + + let ass1 = multiasset.get(&policy_id1).unwrap(); + let ass2 = multiasset.get(&policy_id2).unwrap(); + + assert_eq!(ass1.len(), 2); + assert_eq!(ass2.len(), 2); + + assert_eq!(ass1.get(&name1).unwrap(), amount1); + assert_eq!(ass1.get(&name2).unwrap(), amount2); + + assert_eq!(ass2.get(&name1).unwrap(), amount2); + assert_eq!(ass2.get(&name2).unwrap(), amount1); + } + + #[test] + fn mint_to_negative_multiasset() { + let policy_id1 = PolicyID::from([0u8; 28]); + let policy_id2 = PolicyID::from([1u8; 28]); + let name1 = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); + let name2 = AssetName::new(vec![0u8, 4, 5, 6]).unwrap(); + let amount1 = BigNum::from_str("1234").unwrap(); + let amount2 = BigNum::from_str("5678").unwrap(); + + let mut mass1 = MintAssets::new(); + mass1.insert(&name1, Int::new(&amount1)); + mass1.insert(&name2, Int::new_negative(&amount2)); + + let mut mass2 = MintAssets::new(); + mass2.insert(&name1, Int::new_negative(&amount1)); + mass2.insert(&name2, Int::new(&amount2)); + + let mut mint = Mint::new(); + mint.insert(&policy_id1, &mass1); + mint.insert(&policy_id2, &mass2); + + let p_multiasset = mint.as_positive_multiasset(); + let n_multiasset = mint.as_negative_multiasset(); + + assert_eq!(p_multiasset.len(), 2); + assert_eq!(n_multiasset.len(), 2); + + let p_ass1 = p_multiasset.get(&policy_id1).unwrap(); + let p_ass2 = p_multiasset.get(&policy_id2).unwrap(); + + let n_ass1 = n_multiasset.get(&policy_id1).unwrap(); + let n_ass2 = n_multiasset.get(&policy_id2).unwrap(); + + assert_eq!(p_ass1.len(), 1); + assert_eq!(p_ass2.len(), 1); + assert_eq!(n_ass1.len(), 1); + assert_eq!(n_ass2.len(), 1); + + assert_eq!(p_ass1.get(&name1).unwrap(), amount1); + assert!(p_ass1.get(&name2).is_none()); + + assert!(p_ass2.get(&name1).is_none()); + assert_eq!(p_ass2.get(&name2).unwrap(), amount2); + + assert!(n_ass1.get(&name1).is_none()); + assert_eq!(n_ass1.get(&name2).unwrap(), amount2); + + assert_eq!(n_ass2.get(&name1).unwrap(), amount1); + assert!(n_ass2.get(&name2).is_none()); + } + + #[test] + fn mint_to_negative_multiasset_empty() { + let policy_id1 = PolicyID::from([0u8; 28]); + let name1 = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); + let amount1 = BigNum::from_str("1234").unwrap(); + + let mut mass1 = MintAssets::new(); + mass1.insert(&name1, Int::new(&amount1)); + + let mut mass2 = MintAssets::new(); + mass2.insert(&name1, Int::new_negative(&amount1)); + + let mut mint1 = Mint::new(); + mint1.insert(&policy_id1, &mass1); + + let mut mint2 = Mint::new(); + mint2.insert(&policy_id1, &mass2); + + let p_multiasset_some = mint1.as_positive_multiasset(); + let p_multiasset_none = mint2.as_positive_multiasset(); + + let n_multiasset_none = mint1.as_negative_multiasset(); + let n_multiasset_some = mint2.as_negative_multiasset(); + + assert_eq!(p_multiasset_some.len(), 1); + assert_eq!(p_multiasset_none.len(), 0); + + assert_eq!(n_multiasset_some.len(), 1); + assert_eq!(n_multiasset_none.len(), 0); + + let p_ass = p_multiasset_some.get(&policy_id1).unwrap(); + let n_ass = n_multiasset_some.get(&policy_id1).unwrap(); + + assert_eq!(p_ass.len(), 1); + assert_eq!(n_ass.len(), 1); + + assert_eq!(p_ass.get(&name1).unwrap(), amount1); + assert_eq!(n_ass.get(&name1).unwrap(), amount1); + } } diff --git a/rust/src/metadata.rs b/rust/src/metadata.rs index 23d83c07..658922d4 100644 --- a/rust/src/metadata.rs +++ b/rust/src/metadata.rs @@ -219,7 +219,7 @@ impl TransactionMetadatum { } } -type TransactionMetadatumLabel = BigNum; +pub type TransactionMetadatumLabel = BigNum; #[wasm_bindgen] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -930,7 +930,7 @@ mod tests { assert_eq!(map.get_str("sender_id").unwrap().as_text().unwrap(), "jkfdsufjdk34h3Sdfjdhfduf873"); assert_eq!(map.get_str("comment").unwrap().as_text().unwrap(), "happy birthday"); let tags = map.get_str("tags").unwrap().as_list().unwrap(); - let tags_i32 = tags.0.iter().map(|md| md.as_int().unwrap().as_i32().unwrap()).collect::>(); + let tags_i32 = tags.0.iter().map(|md| md.as_int().unwrap().as_i32_or_fail().unwrap()).collect::>(); assert_eq!(tags_i32, vec![0, 264, -1024, 32]); let output_str = decode_metadatum_to_json_str(&metadata, MetadataJsonSchema::NoConversions).expect("decode failed"); let input_json: serde_json::Value = serde_json::from_str(&input_str).unwrap(); @@ -991,11 +991,11 @@ mod tests { fn json_encoding_check_example_metadatum(metadata: &TransactionMetadatum) { let map = metadata.as_map().unwrap(); assert_eq!(map.get(&TransactionMetadatum::new_bytes(hex::decode("8badf00d").unwrap()).unwrap()).unwrap().as_bytes().unwrap(), hex::decode("deadbeef").unwrap()); - assert_eq!(map.get_i32(9).unwrap().as_int().unwrap().as_i32().unwrap(), 5); + assert_eq!(map.get_i32(9).unwrap().as_int().unwrap().as_i32_or_fail().unwrap(), 5); let inner_map = map.get_str("obj").unwrap().as_map().unwrap(); let a = inner_map.get_str("a").unwrap().as_list().unwrap(); let a1 = a.get(0).as_map().unwrap(); - assert_eq!(a1.get_i32(5).unwrap().as_int().unwrap().as_i32().unwrap(), 2); + assert_eq!(a1.get_i32(5).unwrap().as_int().unwrap().as_i32_or_fail().unwrap(), 2); let a2 = a.get(1).as_map().unwrap(); assert_eq!(a2.keys().len(), 0); } @@ -1025,11 +1025,11 @@ mod tests { let map = metadata.as_map().unwrap(); let key = map.keys().get(0); - assert_eq!(map.get(&key).unwrap().as_int().unwrap().as_i32().unwrap(), 5); + assert_eq!(map.get(&key).unwrap().as_int().unwrap().as_i32_or_fail().unwrap(), 5); let key_list = key.as_list().unwrap(); assert_eq!(key_list.len(), 2); let key_map = key_list.get(0).as_map().unwrap(); - assert_eq!(key_map.get_i32(5).unwrap().as_int().unwrap().as_i32().unwrap(), -7); + assert_eq!(key_map.get_i32(5).unwrap().as_int().unwrap().as_i32_or_fail().unwrap(), -7); assert_eq!(key_map.get_str("hello").unwrap().as_text().unwrap(), "world"); let key_bytes = key_list.get(1).as_bytes().unwrap(); assert_eq!(key_bytes, hex::decode("ff00ff00").unwrap()); diff --git a/rust/src/plutus.rs b/rust/src/plutus.rs index b8035bb2..8c67a573 100644 --- a/rust/src/plutus.rs +++ b/rust/src/plutus.rs @@ -988,10 +988,14 @@ impl Deserialize for PlutusData { impl cbor_event::se::Serialize for PlutusList { fn serialize<'se, W: Write>(&self, serializer: &'se mut Serializer) -> cbor_event::Result<&'se mut Serializer> { - serializer.write_array(cbor_event::Len::Len(self.0.len() as u64))?; + if self.0.len() == 0 { + return Ok(serializer.write_array(cbor_event::Len::Len(0))?); + } + serializer.write_array(cbor_event::Len::Indefinite)?; for element in &self.0 { element.serialize(serializer)?; } + serializer.write_special(cbor_event::Special::Break)?; Ok(serializer) } } @@ -1164,6 +1168,7 @@ impl Deserialize for Strings { #[cfg(test)] mod tests { use super::*; + use hex::*; #[test] pub fn plutus_constr_data() { @@ -1180,4 +1185,30 @@ mod tests { let constr_1854_roundtrip = PlutusData::from_bytes(constr_1854.to_bytes()).unwrap(); assert_eq!(constr_1854, constr_1854_roundtrip); } -} \ No newline at end of file + + #[test] + pub fn plutus_list_serialization_cli_compatibility() { + // mimic cardano-cli array encoding, see https://github.com/Emurgo/cardano-serialization-lib/issues/227 + let datum_cli = "d8799f4100d8799fd8799fd8799f581cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd8799fd8799fd8799f581cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd87a80ff1a002625a0d8799fd879801a000f4240d87a80ffff"; + let datum = PlutusData::from_bytes(Vec::from_hex(datum_cli).unwrap()).unwrap(); + assert_eq!(datum_cli, hex::encode(datum.to_bytes())); + + // encode empty arrays as fixed + assert_eq!("80", hex::encode(PlutusList::from_bytes(Vec::from_hex("9fff").unwrap()).unwrap().to_bytes())); + + // encode arrays as indefinite length array + let mut list = PlutusList::new(); + list.add(&PlutusData::new_integer(&BigInt::from_str("1").unwrap())); + assert_eq!("9f01ff", hex::encode(list.to_bytes())); + + // witness_set should have fixed length array + let mut witness_set = TransactionWitnessSet::new(); + witness_set.set_plutus_data(&list); + assert_eq!("a1048101", hex::encode(witness_set.to_bytes())); + + list = PlutusList::new(); + list.add(&datum); + witness_set.set_plutus_data(&list); + assert_eq!(format!("a10481{}", datum_cli), hex::encode(witness_set.to_bytes())); + } +} diff --git a/rust/src/serialization.rs b/rust/src/serialization.rs index 1d0ddb07..f8bbb8d5 100644 --- a/rust/src/serialization.rs +++ b/rust/src/serialization.rs @@ -40,7 +40,7 @@ impl Deserialize for UnitInterval { } impl DeserializeEmbeddedGroup for UnitInterval { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let numerator = (|| -> Result<_, DeserializeError> { Ok(BigNum::deserialize(raw)?) })().map_err(|e| e.annotate("numerator"))?; @@ -88,7 +88,7 @@ impl Deserialize for Transaction { } impl DeserializeEmbeddedGroup for Transaction { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let body = (|| -> Result<_, DeserializeError> { Ok(TransactionBody::deserialize(raw)?) })().map_err(|e| e.annotate("body"))?; @@ -121,7 +121,7 @@ impl DeserializeEmbeddedGroup for Transaction { } } })().map_err(|e| e.annotate("is_valid"))?; - if (!checked_auxiliary_data) { + if !checked_auxiliary_data { // this branch is reached, if the 3rd argument was a bool. then it simply follows the rules for checking auxiliary data auxiliary_data = (|| -> Result<_, DeserializeError> { Ok(match raw.cbor_type()? != CBORType::Special { @@ -232,7 +232,7 @@ impl Deserialize for Certificates { impl cbor_event::se::Serialize for TransactionBody { fn serialize<'se, W: Write>(&self, serializer: &'se mut Serializer) -> cbor_event::Result<&'se mut Serializer> { - serializer.write_map(cbor_event::Len::Len(3 + match &self.ttl { Some(x) => 1, None => 0 } + match &self.certs { Some(x) => 1, None => 0 } + match &self.withdrawals { Some(x) => 1, None => 0 } + match &self.update { Some(x) => 1, None => 0 } + match &self.auxiliary_data_hash { Some(x) => 1, None => 0 } + match &self.validity_start_interval { Some(x) => 1, None => 0 } + match &self.mint { Some(x) => 1, None => 0 } + match &self.script_data_hash { Some(x) => 1, None => 0 } + match &self.collateral { Some(x) => 1, None => 0 } + match &self.required_signers { Some(x) => 1, None => 0 } + match &self.network_id { Some(x) => 1, None => 0 }))?; + serializer.write_map(cbor_event::Len::Len(3 + match &self.ttl { Some(_) => 1, None => 0 } + match &self.certs { Some(_) => 1, None => 0 } + match &self.withdrawals { Some(_) => 1, None => 0 } + match &self.update { Some(_) => 1, None => 0 } + match &self.auxiliary_data_hash { Some(_) => 1, None => 0 } + match &self.validity_start_interval { Some(_) => 1, None => 0 } + match &self.mint { Some(_) => 1, None => 0 } + match &self.script_data_hash { Some(_) => 1, None => 0 } + match &self.collateral { Some(_) => 1, None => 0 } + match &self.required_signers { Some(_) => 1, None => 0 } + match &self.network_id { Some(_) => 1, None => 0 }))?; serializer.write_unsigned_integer(0)?; self.inputs.serialize(serializer)?; serializer.write_unsigned_integer(1)?; @@ -510,7 +510,7 @@ impl Deserialize for TransactionInput { } impl DeserializeEmbeddedGroup for TransactionInput { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let transaction_id = (|| -> Result<_, DeserializeError> { Ok(TransactionHash::deserialize(raw)?) })().map_err(|e| e.annotate("transaction_id"))?; @@ -561,8 +561,7 @@ impl Deserialize for TransactionOutput { // with array-encoded types with optional fields, due to the complexity. // This is made worse as this is a plain group... impl DeserializeEmbeddedGroup for TransactionOutput { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { - use std::convert::TryInto; + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let address = (|| -> Result<_, DeserializeError> { Ok(Address::deserialize(raw)?) })().map_err(|e| e.annotate("address"))?; @@ -635,7 +634,7 @@ impl Deserialize for StakeRegistration { } impl DeserializeEmbeddedGroup for StakeRegistration { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 0 { @@ -685,7 +684,7 @@ impl Deserialize for StakeDeregistration { } impl DeserializeEmbeddedGroup for StakeDeregistration { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 1 { @@ -736,7 +735,7 @@ impl Deserialize for StakeDelegation { } impl DeserializeEmbeddedGroup for StakeDelegation { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 2 { @@ -858,7 +857,7 @@ impl Deserialize for PoolParams { } impl DeserializeEmbeddedGroup for PoolParams { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let operator = (|| -> Result<_, DeserializeError> { Ok(Ed25519KeyHash::deserialize(raw)?) })().map_err(|e| e.annotate("operator"))?; @@ -994,7 +993,7 @@ impl Deserialize for PoolRetirement { } impl DeserializeEmbeddedGroup for PoolRetirement { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 4 { @@ -1050,7 +1049,7 @@ impl Deserialize for GenesisKeyDelegation { } impl DeserializeEmbeddedGroup for GenesisKeyDelegation { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 5 { @@ -1108,7 +1107,7 @@ impl Deserialize for MoveInstantaneousRewardsCert { } impl DeserializeEmbeddedGroup for MoveInstantaneousRewardsCert { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 6 { @@ -1441,7 +1440,7 @@ impl Deserialize for SingleHostAddr { } impl DeserializeEmbeddedGroup for SingleHostAddr { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 0 { @@ -1535,7 +1534,7 @@ impl Deserialize for SingleHostName { } impl DeserializeEmbeddedGroup for SingleHostName { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 1 { @@ -1599,7 +1598,7 @@ impl Deserialize for MultiHostName { } impl DeserializeEmbeddedGroup for MultiHostName { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 2 { @@ -1710,7 +1709,7 @@ impl Deserialize for PoolMetadata { } impl DeserializeEmbeddedGroup for PoolMetadata { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let url = (|| -> Result<_, DeserializeError> { Ok(URL::deserialize(raw)?) })().map_err(|e| e.annotate("url"))?; @@ -1788,7 +1787,7 @@ impl Deserialize for Withdrawals { impl cbor_event::se::Serialize for TransactionWitnessSet { fn serialize<'se, W: Write>(&self, serializer: &'se mut Serializer) -> cbor_event::Result<&'se mut Serializer> { - serializer.write_map(cbor_event::Len::Len(match &self.vkeys { Some(x) => 1, None => 0 } + match &self.native_scripts { Some(x) => 1, None => 0 } + match &self.bootstraps { Some(x) => 1, None => 0 } + match &self.plutus_scripts { Some(x) => 1, None => 0 } + match &self.plutus_data { Some(x) => 1, None => 0 } + match &self.redeemers { Some(x) => 1, None => 0 }))?; + serializer.write_map(cbor_event::Len::Len(match &self.vkeys { Some(_) => 1, None => 0 } + match &self.native_scripts { Some(_) => 1, None => 0 } + match &self.bootstraps { Some(_) => 1, None => 0 } + match &self.plutus_scripts { Some(_) => 1, None => 0 } + match &self.plutus_data { Some(_) => 1, None => 0 } + match &self.redeemers { Some(_) => 1, None => 0 }))?; if let Some(field) = &self.vkeys { serializer.write_unsigned_integer(0)?; field.serialize(serializer)?; @@ -1807,7 +1806,10 @@ impl cbor_event::se::Serialize for TransactionWitnessSet { } if let Some(field) = &self.plutus_data { serializer.write_unsigned_integer(4)?; - field.serialize(serializer)?; + serializer.write_array(cbor_event::Len::Len(field.len() as u64))?; + for i in 0..field.len() { + field.get(i).serialize(serializer)?; + } } if let Some(field) = &self.redeemers { serializer.write_unsigned_integer(5)?; @@ -1950,7 +1952,7 @@ impl Deserialize for ScriptPubkey { } impl DeserializeEmbeddedGroup for ScriptPubkey { - fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */_: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 0 { @@ -2002,7 +2004,7 @@ impl Deserialize for ScriptAll { } impl DeserializeEmbeddedGroup for ScriptAll { - fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */_: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 1 { @@ -2054,7 +2056,7 @@ impl Deserialize for ScriptAny { } impl DeserializeEmbeddedGroup for ScriptAny { - fn deserialize_as_embedded_group(raw: &mut Deserializer, /*/*read_len: &mut CBORReadLen, */*/len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, /*/*read_len: &mut CBORReadLen, */*/_: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 2 { @@ -2107,7 +2109,7 @@ impl Deserialize for ScriptNOfK { } impl DeserializeEmbeddedGroup for ScriptNOfK { - fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */_: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 3 { @@ -2163,7 +2165,7 @@ impl Deserialize for TimelockStart { } impl DeserializeEmbeddedGroup for TimelockStart { - fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */_: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 4 { @@ -2215,7 +2217,7 @@ impl Deserialize for TimelockExpiry { } impl DeserializeEmbeddedGroup for TimelockExpiry { - fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, /*read_len: &mut CBORReadLen, */_: cbor_event::Len) -> Result { (|| -> Result<_, DeserializeError> { let index_0_value = raw.unsigned_integer()?; if index_0_value != 5 { @@ -2372,7 +2374,7 @@ impl Deserialize for Update { } impl DeserializeEmbeddedGroup for Update { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let proposed_protocol_parameter_updates = (|| -> Result<_, DeserializeError> { Ok(ProposedProtocolParameterUpdates::deserialize(raw)?) })().map_err(|e| e.annotate("proposed_protocol_parameter_updates"))?; @@ -2508,7 +2510,7 @@ impl Deserialize for ProtocolVersion { } impl DeserializeEmbeddedGroup for ProtocolVersion { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let major = (|| -> Result<_, DeserializeError> { Ok(u32::deserialize(raw)?) })().map_err(|e| e.annotate("major"))?; @@ -2551,7 +2553,7 @@ impl Deserialize for ProtocolVersions { } impl cbor_event::se::Serialize for ProtocolParamUpdate { fn serialize<'se, W: Write>(&self, serializer: &'se mut Serializer) -> cbor_event::Result<&'se mut Serializer> { - serializer.write_map(cbor_event::Len::Len(match &self.minfee_a { Some(x) => 1, None => 0 } + match &self.minfee_b { Some(x) => 1, None => 0 } + match &self.max_block_body_size { Some(x) => 1, None => 0 } + match &self.max_tx_size { Some(x) => 1, None => 0 } + match &self.max_block_header_size { Some(x) => 1, None => 0 } + match &self.key_deposit { Some(x) => 1, None => 0 } + match &self.pool_deposit { Some(x) => 1, None => 0 } + match &self.max_epoch { Some(x) => 1, None => 0 } + match &self.n_opt { Some(x) => 1, None => 0 } + match &self.pool_pledge_influence { Some(x) => 1, None => 0 } + match &self.expansion_rate { Some(x) => 1, None => 0 } + match &self.treasury_growth_rate { Some(x) => 1, None => 0 } + match &self.d { Some(x) => 1, None => 0 } + match &self.extra_entropy { Some(x) => 1, None => 0 } + match &self.protocol_version { Some(x) => 1, None => 0 } + match &self.min_pool_cost { Some(x) => 1, None => 0 } + match &self.ada_per_utxo_byte { Some(x) => 1, None => 0 } + match &self.cost_models { Some(x) => 1, None => 0 } + match &self.execution_costs { Some(x) => 1, None => 0 } + match &self.max_tx_ex_units { Some(x) => 1, None => 0 } + match &self.max_block_ex_units { Some(x) => 1, None => 0 } + match &self.max_value_size { Some(x) => 1, None => 0 }))?; + serializer.write_map(cbor_event::Len::Len(match &self.minfee_a { Some(_) => 1, None => 0 } + match &self.minfee_b { Some(_) => 1, None => 0 } + match &self.max_block_body_size { Some(_) => 1, None => 0 } + match &self.max_tx_size { Some(_) => 1, None => 0 } + match &self.max_block_header_size { Some(_) => 1, None => 0 } + match &self.key_deposit { Some(_) => 1, None => 0 } + match &self.pool_deposit { Some(_) => 1, None => 0 } + match &self.max_epoch { Some(_) => 1, None => 0 } + match &self.n_opt { Some(_) => 1, None => 0 } + match &self.pool_pledge_influence { Some(_) => 1, None => 0 } + match &self.expansion_rate { Some(_) => 1, None => 0 } + match &self.treasury_growth_rate { Some(_) => 1, None => 0 } + match &self.d { Some(_) => 1, None => 0 } + match &self.extra_entropy { Some(_) => 1, None => 0 } + match &self.protocol_version { Some(_) => 1, None => 0 } + match &self.min_pool_cost { Some(_) => 1, None => 0 } + match &self.ada_per_utxo_byte { Some(_) => 1, None => 0 } + match &self.cost_models { Some(_) => 1, None => 0 } + match &self.execution_costs { Some(_) => 1, None => 0 } + match &self.max_tx_ex_units { Some(_) => 1, None => 0 } + match &self.max_block_ex_units { Some(_) => 1, None => 0 } + match &self.max_value_size { Some(_) => 1, None => 0 }))?; if let Some(field) = &self.minfee_a { serializer.write_unsigned_integer(0)?; field.serialize(serializer)?; @@ -3097,7 +3099,7 @@ impl Deserialize for Header { } impl DeserializeEmbeddedGroup for Header { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let header_body = (|| -> Result<_, DeserializeError> { Ok(HeaderBody::deserialize(raw)?) })().map_err(|e| e.annotate("header_body"))?; @@ -3146,7 +3148,7 @@ impl Deserialize for OperationalCert { } impl DeserializeEmbeddedGroup for OperationalCert { - fn deserialize_as_embedded_group(raw: &mut Deserializer, len: cbor_event::Len) -> Result { + fn deserialize_as_embedded_group(raw: &mut Deserializer, _: cbor_event::Len) -> Result { let hot_vkey = (|| -> Result<_, DeserializeError> { Ok(KESVKey::deserialize(raw)?) })().map_err(|e| e.annotate("hot_vkey"))?; @@ -3515,4 +3517,4 @@ mod tests { let block2 = Block::from_bytes(block.to_bytes()).unwrap(); assert_eq!(block.to_bytes(), block2.to_bytes()); } -} \ No newline at end of file +} diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index b441a8c9..7374443b 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -46,11 +46,18 @@ 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() +} + // tx_body must be the result of building from tx_builder // constructs the rest of the Transaction using fake witness data of the correct length // 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() { @@ -67,7 +74,7 @@ fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Resul Some(result) }, }; - let script_keys = match tx_builder.input_types.scripts.len() { + let script_keys: Option = match tx_builder.input_types.scripts.len() { 0 => None, _x => { // TODO: figure out how to populate fake witnesses for these @@ -89,9 +96,22 @@ fn fake_full_tx(tx_builder: &TransactionBuilder, body: TransactionBody) -> Resul Some(result) }, }; + let full_script_keys = match &tx_builder.mint { + None => script_keys, + Some(mint) => { + let mut ns = script_keys + .map(|sk| { sk.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)); + }); + Some(ns) + } + }; let witness_set = TransactionWitnessSet { vkeys: vkeys, - native_scripts: script_keys, + native_scripts: full_script_keys, bootstraps: bootstrap_keys, // TODO: plutus support? plutus_scripts: None, @@ -166,9 +186,7 @@ impl TransactionBuilder { /// inputs to cover the minimum fees. This does not, however, set the txbuilder's fee. pub fn add_inputs_from(&mut self, inputs: &TransactionUnspentOutputs, strategy: CoinSelectionStrategyCIP2) -> Result<(), JsError> { let mut available_inputs = inputs.0.clone(); - let mut input_total = self - .get_explicit_input()? - .checked_add(&self.get_implicit_input()?)?; + let mut input_total = self.get_total_input()?; let mut output_total = self .get_explicit_output()? .checked_add(&Value::new(&self.get_deposit()?))? @@ -192,6 +210,26 @@ impl TransactionBuilder { } }, CoinSelectionStrategyCIP2::RandomImprove => { + fn add_random_input( + s: &mut TransactionBuilder, + rng: &mut rand::rngs::ThreadRng, + available_inputs: &mut Vec, + input_total: &Value, + output_total: &Value, + added: &Value, + needed: &Value + ) -> Result<(Value, Value, Value, Value, TransactionUnspentOutput), JsError> { + let random_index = rng.gen_range(0..available_inputs.len()); + let input = available_inputs.swap_remove(random_index); + // differing from CIP2, we include the needed fees in the targets instead of just output values + let input_fee = s.fee_for_input(&input.output.address, &input.input, &input.output.amount)?; + s.add_input(&input.output.address, &input.input, &input.output.amount); + let new_input_total = input_total.checked_add(&input.output.amount)?; + let new_output_total = output_total.checked_add(&Value::new(&input_fee))?; + let new_needed = needed.checked_add(&Value::new(&input_fee))?; + let new_added = added.checked_add(&input.output.amount)?; + Ok((new_input_total, new_output_total, new_added, new_needed, input)) + } use rand::Rng; if self.outputs.0.iter().any(|output| output.amount.multiasset.is_some()) { return Err(JsError::from_str("Multiasset values not supported by RandomImprove. Please use LargestFirst")); @@ -208,15 +246,11 @@ impl TransactionBuilder { if available_inputs.is_empty() { return Err(JsError::from_str("UTxO Balance Insufficient")); } - let random_index = rng.gen_range(0..available_inputs.len()); - let input = available_inputs.swap_remove(random_index); - // differing from CIP2, we include the needed fees in the targets instead of just output values - let input_fee = self.fee_for_input(&input.output.address, &input.input, &input.output.amount)?; - self.add_input(&input.output.address, &input.input, &input.output.amount); - input_total = input_total.checked_add(&input.output.amount)?; - output_total = output_total.checked_add(&Value::new(&input_fee))?; - needed = needed.checked_add(&Value::new(&input_fee))?; - added = added.checked_add(&input.output.amount)?; + let (new_input_total, new_output_total, new_added, new_needed, input) = add_random_input(self, &mut rng, &mut available_inputs, &input_total, &output_total, &added, &needed)?; + input_total = new_input_total; + output_total = new_output_total; + added = new_added; + needed = new_needed; associated_inputs.entry(output.clone()).or_default().push(input); } } @@ -240,6 +274,23 @@ impl TransactionBuilder { } } } + // Phase 3: add extra inputs needed for fees + // We do this at the end because this new inputs won't be associated with + // a specific output, so the improvement algorithm we do above does not apply here. + if input_total < output_total { + let mut added = Value::new(&Coin::zero()); + let mut remaining_amount = output_total.checked_sub(&input_total)?; + while added < remaining_amount { + if available_inputs.is_empty() { + return Err(JsError::from_str("UTxO Balance Insufficient")); + } + let (new_input_total, new_output_total, new_added, new_remaining_amount, _) = add_random_input(self, &mut rng, &mut available_inputs, &input_total, &output_total, &added, &remaining_amount)?; + input_total = new_input_total; + output_total = new_output_total; + added = new_added; + remaining_amount = new_remaining_amount; + } + } }, } @@ -335,6 +386,45 @@ impl TransactionBuilder { fee_after.checked_sub(&fee_before) } + /// Add output by specifying the Address and Value + pub fn add_output_amount(&mut self, address: &Address, amount: &Value) -> Result<(), JsError> { + self.add_output(&TransactionOutput::new(address, amount)) + } + + /// Add output by specifying the Address and Coin (BigNum) + /// Output will have no additional assets + pub fn add_output_coin(&mut self, address: &Address, coin: &Coin) -> Result<(), JsError> { + self.add_output_amount(address, &Value::new(coin)) + } + + /// Add output by specifying the Address, the Coin (BigNum), and the MultiAsset + pub fn add_output_coin_and_asset( + &mut self, + address: &Address, + coin: &Coin, + multiasset: &MultiAsset, + ) -> Result<(), JsError> { + let mut val = Value::new(coin); + val.set_multiasset(multiasset); + self.add_output_amount(address, &val) + } + + /// Add output by specifying the Address and the MultiAsset + /// The output will be set to contain the minimum required amount of Coin + pub fn add_output_asset_and_min_required_coin( + &mut self, + address: &Address, + multiasset: &MultiAsset, + ) -> Result<(), JsError> { + let min_possible_coin = min_pure_ada(&self.coins_per_utxo_word)?; + let mut value = Value::new(&min_possible_coin); + value.set_multiasset(multiasset); + let required_coin = + min_ada_required(&value, false, &self.coins_per_utxo_word)?; + self.add_output_coin_and_asset(address, &required_coin, multiasset) + } + + /// Add explicit output via a TransactionOutput object pub fn add_output(&mut self, output: &TransactionOutput) -> Result<(), JsError> { let value_size = output.amount.to_bytes().len(); if value_size > self.max_value_size as usize { @@ -399,10 +489,133 @@ impl TransactionBuilder { }; } + pub fn get_auxiliary_data(&self) -> Option { + self.auxiliary_data.clone() + } + + /// Set explicit auxiliary data via an AuxiliaryData object + /// It might contain some metadata plus native or Plutus scripts pub fn set_auxiliary_data(&mut self, auxiliary_data: &AuxiliaryData) { self.auxiliary_data = Some(auxiliary_data.clone()) } + /// Set metadata using a GeneralTransactionMetadata object + /// It will be set to the existing or new auxiliary data in this builder + pub fn set_metadata(&mut self, metadata: &GeneralTransactionMetadata) { + let mut aux = self.auxiliary_data.as_ref().cloned().unwrap_or(AuxiliaryData::new()); + aux.set_metadata(metadata); + self.set_auxiliary_data(&aux); + } + + /// Add a single metadatum using TransactionMetadatumLabel and TransactionMetadatum objects + /// It will be securely added to existing or new metadata in this builder + pub fn add_metadatum(&mut self, key: &TransactionMetadatumLabel, val: &TransactionMetadatum) { + let mut metadata = self.auxiliary_data.as_ref() + .map(|aux| { aux.metadata().as_ref().cloned() }) + .unwrap_or(None) + .unwrap_or(GeneralTransactionMetadata::new()); + metadata.insert(key, val); + self.set_metadata(&metadata); + } + + /// Add a single JSON metadatum using a TransactionMetadatumLabel and a String + /// It will be securely added to existing or new metadata in this builder + pub fn add_json_metadatum( + &mut self, + key: &TransactionMetadatumLabel, + val: String, + ) -> Result<(), JsError> { + self.add_json_metadatum_with_schema(key, val, MetadataJsonSchema::NoConversions) + } + + /// Add a single JSON metadatum using a TransactionMetadatumLabel, a String, and a MetadataJsonSchema object + /// It will be securely added to existing or new metadata in this builder + pub fn add_json_metadatum_with_schema( + &mut self, + key: &TransactionMetadatumLabel, + val: String, + schema: MetadataJsonSchema, + ) -> Result<(), JsError> { + let metadatum = encode_json_str_to_metadatum(val, schema)?; + self.add_metadatum(key, &metadatum); + Ok(()) + } + + /// Set explicit Mint object to this builder + /// it will replace any previously existing mint + pub fn set_mint(&mut self, mint: &Mint) { + self.mint = Some(mint.clone()); + } + + /// 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); + } + + /// 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) { + let mut asset = self.mint.as_ref() + .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); + } + + /// Add a mint entry together with an output to this builder + /// 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 + pub fn add_mint_asset_and_output( + &mut self, + policy_id: &PolicyID, + asset_name: &AssetName, + amount: Int, + address: &Address, + output_coin: &Coin, + ) -> Result<(), JsError> { + 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 multiasset = Mint::new_from_entry( + policy_id, + &MintAssets::new_from_entry(asset_name, amount.clone()) + ).as_positive_multiasset(); + self.add_output_coin_and_asset(address, output_coin, &multiasset) + } + + /// Add a mint entry together with an output to this builder + /// Using a PolicyID, AssetName, Int for amount, and Address 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 and the minted asset + /// 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, + asset_name: &AssetName, + amount: Int, + address: &Address, + ) -> Result<(), JsError> { + 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 multiasset = Mint::new_from_entry( + policy_id, + &MintAssets::new_from_entry(asset_name, amount.clone()) + ).as_positive_multiasset(); + self.add_output_asset_and_min_required_coin(address, &multiasset) + } + + /// If set to true, add_change_if_needed will try + /// to put pure Coin in a separate output from assets pub fn set_prefer_pure_change(&mut self, prefer_pure_change: bool) { self.prefer_pure_change = prefer_pure_change; } @@ -445,10 +658,11 @@ impl TransactionBuilder { pub fn get_explicit_input(&self) -> Result { self.inputs .iter() - .try_fold(Value::new(&to_bignum(0)), |acc, ref tx_builder_input| { + .try_fold(Value::zero(), |acc, ref tx_builder_input| { acc.checked_add(&tx_builder_input.amount) }) } + /// withdrawals and refunds pub fn get_implicit_input(&self) -> Result { internal_get_implicit_input( @@ -459,6 +673,22 @@ impl TransactionBuilder { ) } + /// Returns mint as tuple of (mint_value, burn_value) or two zero values + fn get_mint_as_values(&self) -> (Value, Value) { + self.mint.as_ref().map(|m| { + (Value::new_from_assets(&m.as_positive_multiasset()), + Value::new_from_assets(&m.as_negative_multiasset())) + }).unwrap_or((Value::zero(), Value::zero())) + } + + fn get_total_input(&self) -> Result { + let (mint_value, burn_value) = self.get_mint_as_values(); + self.get_explicit_input()? + .checked_add(&self.get_implicit_input()?)? + .checked_add(&mint_value)? + .checked_sub(&burn_value) + } + /// does not include fee pub fn get_explicit_output(&self) -> Result { self.outputs @@ -482,6 +712,9 @@ impl TransactionBuilder { } /// Warning: this function will mutate the /fee/ field + /// Make sure to call this function last after setting all other tx-body properties + /// Editing inputs, outputs, mint, etc. after change been calculated + /// might cause a mismatch in calculated fee versus the required fee pub fn add_change_if_needed(&mut self, address: &Address) -> Result { let fee = match &self.fee { None => self.min_fee(), @@ -493,9 +726,7 @@ impl TransactionBuilder { } }?; - let input_total = self - .get_explicit_input()? - .checked_add(&self.get_implicit_input()?)?; + let input_total = self.get_total_input()?; let output_total = self .get_explicit_output()? @@ -648,7 +879,7 @@ impl TransactionBuilder { if potential_pure_above_minimum { new_fee = new_fee.checked_add(&additional_fee)?; change_left = Value::zero(); - self.add_output(&TransactionOutput::new(address, &potential_pure_value)); + self.add_output(&TransactionOutput::new(address, &potential_pure_value))?; } } self.set_fee(&new_fee); @@ -664,7 +895,7 @@ impl TransactionBuilder { // recall: min_fee assumed the fee was the maximum possible so we definitely have enough input to cover whatever fee it ends up being builder.set_fee(burn_amount); Ok(false) // not enough input to covert the extra fee from adding an output so we just burn whatever is left - }; + } match change_estimator.coin() >= min_ada { false => burn_extra(self, &change_estimator.coin()), true => { @@ -736,6 +967,9 @@ impl TransactionBuilder { return self.outputs.0.iter().map(|o| { o.to_bytes().len() }).collect(); } + /// Returns object the body of the new transaction + /// Auxiliary data itself is not included + /// You can use `get_auxiliary_date` or `build_tx` pub fn build(&self) -> Result { let (body, full_tx_size) = self.build_and_size()?; if full_tx_size > self.max_tx_size as usize { @@ -749,6 +983,18 @@ impl TransactionBuilder { } } + /// Returns full Transaction object with the body and the auxiliary data + /// NOTE: witness_set is set to just empty set + /// NOTE: is_valid set to true + pub fn build_tx(&self) -> Result { + Ok(Transaction { + body: self.build()?, + witness_set: TransactionWitnessSet::new(), + is_valid: true, + auxiliary_data: self.auxiliary_data.clone(), + }) + } + /// warning: sum of all parts of a transaction must equal 0. You cannot just set the fee to the min value and forget about it /// warning: min_fee may be slightly larger than the actual minimum fee (ex: a few lovelaces) /// this is done to simplify the library code, but can be fixed later @@ -796,17 +1042,72 @@ mod tests { ); } + fn byron_address() -> Address { + ByronAddress::from_base58("Ae2tdPwUPEZ5uzkzh1o2DHECiUi3iugvnnKHRisPgRRP3CTF4KCMvy54Xd3").unwrap().to_address() + } + + fn create_linear_fee(coefficient: u64, constant: u64) -> LinearFee { + LinearFee::new(&to_bignum(coefficient), &to_bignum(constant)) + } + + fn create_default_linear_fee() -> LinearFee { + create_linear_fee(500, 2) + } + + fn create_tx_builder_full( + linear_fee: &LinearFee, + pool_deposit: u64, + key_deposit: u64, + max_val_size: u32, + coins_per_utxo_word: u64, + ) -> TransactionBuilder { + TransactionBuilder::new( + linear_fee, + &to_bignum(pool_deposit), + &to_bignum(key_deposit), + max_val_size, + MAX_TX_SIZE, + &to_bignum(coins_per_utxo_word) + ) + } + + fn create_tx_builder( + linear_fee: &LinearFee, + coins_per_utxo_word: u64, + pool_deposit: u64, + key_deposit: u64, + ) -> TransactionBuilder { + create_tx_builder_full(linear_fee, pool_deposit, key_deposit, MAX_VALUE_SIZE, coins_per_utxo_word) + } + + fn create_reallistic_tx_builder() -> TransactionBuilder { + create_tx_builder( + &create_linear_fee(44, 155381), + COINS_PER_UTXO_WORD, + 500000000, + 2000000, + ) + } + + fn create_tx_builder_with_fee_and_val_size(linear_fee: &LinearFee, max_val_size: u32) -> TransactionBuilder { + create_tx_builder_full(linear_fee, 1, 1, max_val_size, 1) + } + + fn create_tx_builder_with_fee(linear_fee: &LinearFee) -> TransactionBuilder { + create_tx_builder(linear_fee, 1, 1, 1) + } + + fn create_tx_builder_with_key_deposit(deposit: u64) -> TransactionBuilder { + create_tx_builder(&create_default_linear_fee(), 1, 1, deposit) + } + + fn create_default_tx_builder() -> TransactionBuilder { + create_tx_builder_with_fee(&create_default_linear_fee()) + } + #[test] fn build_tx_with_change() { - let linear_fee = LinearFee::new(&to_bignum(500), &to_bignum(2)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(1), - &to_bignum(1), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1), - ); + let mut tx_builder = create_default_tx_builder(); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -861,15 +1162,7 @@ mod tests { #[test] fn build_tx_without_change() { - let linear_fee = LinearFee::new(&to_bignum(500), &to_bignum(2)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(1), - &to_bignum(1), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1), - ); + let mut tx_builder = create_default_tx_builder(); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -922,15 +1215,7 @@ mod tests { #[test] fn build_tx_with_certs() { - let linear_fee = LinearFee::new(&to_bignum(500), &to_bignum(2)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(1), - &to_bignum(1_000_000), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1), - ); + let mut tx_builder = create_tx_builder_with_key_deposit(1_000_000); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -991,15 +1276,7 @@ mod tests { #[test] fn build_tx_exact_amount() { // transactions where sum(input) == sum(output) exact should pass - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(0)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1), - ); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 0)); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -1048,15 +1325,7 @@ mod tests { #[test] fn build_tx_exact_change() { // transactions where we have exactly enough ADA to add change should pass - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(0)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1) - ); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 0)); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -1114,15 +1383,7 @@ mod tests { #[should_panic] fn build_tx_insufficient_deposit() { // transactions should fail with insufficient fees if a deposit is required - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(0)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(5), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1) - ); + let mut tx_builder = create_tx_builder_with_key_deposit(5); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -1185,15 +1446,7 @@ mod tests { #[test] fn build_tx_with_inputs() { - let linear_fee = LinearFee::new(&to_bignum(500), &to_bignum(2)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(1), - &to_bignum(1), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1) - ); + let mut tx_builder = create_default_tx_builder(); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -1264,17 +1517,8 @@ mod tests { } #[test] - fn build_tx_with_native_assets_change() { - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); - let coins_per_utxo_word = to_bignum(1); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &coins_per_utxo_word, - ); + fn build_tx_with_mint_all_sent() { + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 1)); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -1297,42 +1541,18 @@ mod tests { .derive(0) .to_public(); - let policy_id = &PolicyID::from([0u8; 28]); - let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); - - let ma_input1 = 100; - let ma_input2 = 200; - let ma_output1 = 60; - - let multiassets = [ma_input1, ma_input2, ma_output1] - .iter() - .map(|input| { - let mut multiasset = MultiAsset::new(); - multiasset.insert(policy_id, &{ - let mut assets = Assets::new(); - assets.insert(&name, &to_bignum(*input)); - assets - }); - multiasset - }) - .collect::>(); - - for (multiasset, ada) in multiassets - .iter() - .zip([100u64, 100].iter().cloned().map(to_bignum)) - { - 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), - &input_amount, - ); - } - - let stake_cred = StakeCredential::from_keyhash(&stake.to_raw_key().hash()); let spend_cred = StakeCredential::from_keyhash(&spend.to_raw_key().hash()); + let stake_cred = StakeCredential::from_keyhash(&stake.to_raw_key().hash()); + + // Input with 150 coins + tx_builder.add_input( + &EnterpriseAddress::new( + NetworkInfo::testnet().network_id(), + &spend_cred + ).to_address(), + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(150)) + ); let addr_net_0 = BaseAddress::new( NetworkInfo::testnet().network_id(), @@ -1341,8 +1561,21 @@ mod tests { ) .to_address(); - let mut output_amount = Value::new(&to_bignum(100)); - output_amount.set_multiasset(&multiassets[2]); + let policy_id = PolicyID::from([0u8; 28]); + 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)); + + let mut ass = Assets::new(); + ass.insert(&name, &amount); + let mut mass = MultiAsset::new(); + mass.insert(&policy_id, &ass); + + // One coin and the minted asset goes into the output + let mut output_amount = Value::new(&to_bignum(50)); + output_amount.set_multiasset(&mass); tx_builder .add_output(&TransactionOutput::new(&addr_net_0, &output_amount)) @@ -1357,43 +1590,18 @@ mod tests { .to_address(); let added_change = tx_builder.add_change_if_needed(&change_addr).unwrap(); - assert_eq!(added_change, true); - let final_tx = tx_builder.build().unwrap(); - assert_eq!(final_tx.outputs().len(), 2); - assert_eq!( - final_tx - .outputs() - .get(1) - .amount() - .multiasset() - .unwrap() - .get(policy_id) - .unwrap() - .get(&name) - .unwrap(), - to_bignum(ma_input1 + ma_input2 - ma_output1) - ); - assert_eq!( - final_tx.outputs().get(1).amount().coin(), - to_bignum(99) - ); + assert!(added_change); + assert_eq!(tx_builder.outputs.len(), 2); + + // Change must be one remaining coin because fee is one constant coin + let change = tx_builder.outputs.get(1).amount(); + assert_eq!(change.coin(), to_bignum(99)); + assert!(change.multiasset().is_none()); } #[test] - fn build_tx_with_native_assets_change_and_purification() { - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); - let minimum_utxo_value = to_bignum(1); - let coin_per_utxo_word = to_bignum(1); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &coin_per_utxo_word, - ); - // Prefer pure change! - tx_builder.set_prefer_pure_change(true); + fn build_tx_with_mint_in_change() { + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 1)); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -1416,35 +1624,240 @@ mod tests { .derive(0) .to_public(); - let policy_id = &PolicyID::from([0u8; 28]); + let spend_cred = StakeCredential::from_keyhash(&spend.to_raw_key().hash()); + let stake_cred = StakeCredential::from_keyhash(&stake.to_raw_key().hash()); + + // Input with 150 coins + tx_builder.add_input( + &EnterpriseAddress::new( + NetworkInfo::testnet().network_id(), + &spend_cred + ).to_address(), + &TransactionInput::new(&genesis_id(), 0), + &Value::new(&to_bignum(150)) + ); + + let addr_net_0 = BaseAddress::new( + NetworkInfo::testnet().network_id(), + &spend_cred, + &stake_cred, + ) + .to_address(); + + let policy_id = PolicyID::from([0u8; 28]); let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); - let ma_input1 = 100; - let ma_input2 = 200; - let ma_output1 = 60; + let amount_minted = to_bignum(1000); + let amount_sent = to_bignum(500); - let multiassets = [ma_input1, ma_input2, ma_output1] - .iter() - .map(|input| { - let mut multiasset = MultiAsset::new(); - multiasset.insert(policy_id, &{ - let mut assets = Assets::new(); - assets.insert(&name, &to_bignum(*input)); - assets - }); - multiasset - }) - .collect::>(); + // Adding mint of the asset - which should work as an input + tx_builder.add_mint_asset(&policy_id, &name, Int::new(&amount_minted)); - for (multiasset, ada) in multiassets - .iter() - .zip([100u64, 100].iter().cloned().map(to_bignum)) - { - let mut input_amount = Value::new(&ada); - input_amount.set_multiasset(multiasset); + let mut ass = Assets::new(); + ass.insert(&name, &amount_sent); + let mut mass = MultiAsset::new(); + mass.insert(&policy_id, &ass); - tx_builder.add_key_input( - &&spend.to_raw_key().hash(), + // One coin and the minted asset goes into the output + let mut output_amount = Value::new(&to_bignum(50)); + output_amount.set_multiasset(&mass); + + tx_builder + .add_output(&TransactionOutput::new(&addr_net_0, &output_amount)) + .unwrap(); + + let change_cred = StakeCredential::from_keyhash(&change_key.to_raw_key().hash()); + let change_addr = BaseAddress::new( + NetworkInfo::testnet().network_id(), + &change_cred, + &stake_cred, + ) + .to_address(); + + let added_change = tx_builder.add_change_if_needed(&change_addr).unwrap(); + assert!(added_change); + assert_eq!(tx_builder.outputs.len(), 2); + + // Change must be one remaining coin because fee is one constant coin + let change = tx_builder.outputs.get(1).amount(); + assert_eq!(change.coin(), to_bignum(99)); + assert!(change.multiasset().is_some()); + + let change_assets = change.multiasset().unwrap(); + let change_asset = change_assets.get(&policy_id).unwrap(); + assert_eq!( + change_asset.get(&name).unwrap(), + amount_minted.checked_sub(&amount_sent).unwrap(), + ); + } + + #[test] + fn build_tx_with_native_assets_change() { + let minimum_utxo_value = to_bignum(1); + let coins_per_utxo_word = to_bignum(1); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 1)); + let spend = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(0) + .derive(0) + .to_public(); + let change_key = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(1) + .derive(0) + .to_public(); + let stake = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(2) + .derive(0) + .to_public(); + + let policy_id = &PolicyID::from([0u8; 28]); + let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); + + let ma_input1 = 100; + let ma_input2 = 200; + let ma_output1 = 60; + + let multiassets = [ma_input1, ma_input2, ma_output1] + .iter() + .map(|input| { + let mut multiasset = MultiAsset::new(); + multiasset.insert(policy_id, &{ + let mut assets = Assets::new(); + assets.insert(&name, &to_bignum(*input)); + assets + }); + multiasset + }) + .collect::>(); + + for (multiasset, ada) in multiassets + .iter() + .zip([100u64, 100].iter().cloned().map(to_bignum)) + { + 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), + &input_amount, + ); + } + + let stake_cred = StakeCredential::from_keyhash(&stake.to_raw_key().hash()); + let spend_cred = StakeCredential::from_keyhash(&spend.to_raw_key().hash()); + + let addr_net_0 = BaseAddress::new( + NetworkInfo::testnet().network_id(), + &spend_cred, + &stake_cred, + ) + .to_address(); + + let mut output_amount = Value::new(&to_bignum(100)); + output_amount.set_multiasset(&multiassets[2]); + + tx_builder + .add_output(&TransactionOutput::new(&addr_net_0, &output_amount)) + .unwrap(); + + let change_cred = StakeCredential::from_keyhash(&change_key.to_raw_key().hash()); + let change_addr = BaseAddress::new( + NetworkInfo::testnet().network_id(), + &change_cred, + &stake_cred, + ) + .to_address(); + + let added_change = tx_builder.add_change_if_needed(&change_addr).unwrap(); + assert_eq!(added_change, true); + let final_tx = tx_builder.build().unwrap(); + assert_eq!(final_tx.outputs().len(), 2); + assert_eq!( + final_tx + .outputs() + .get(1) + .amount() + .multiasset() + .unwrap() + .get(policy_id) + .unwrap() + .get(&name) + .unwrap(), + to_bignum(ma_input1 + ma_input2 - ma_output1) + ); + assert_eq!( + final_tx.outputs().get(1).amount().coin(), + to_bignum(99) + ); + } + + #[test] + fn build_tx_with_native_assets_change_and_purification() { + let minimum_utxo_value = to_bignum(1); + let coin_per_utxo_word = to_bignum(1); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 1)); + // Prefer pure change! + tx_builder.set_prefer_pure_change(true); + let spend = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(0) + .derive(0) + .to_public(); + let change_key = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(1) + .derive(0) + .to_public(); + let stake = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(2) + .derive(0) + .to_public(); + + let policy_id = &PolicyID::from([0u8; 28]); + let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); + + let ma_input1 = 100; + let ma_input2 = 200; + let ma_output1 = 60; + + let multiassets = [ma_input1, ma_input2, ma_output1] + .iter() + .map(|input| { + let mut multiasset = MultiAsset::new(); + multiasset.insert(policy_id, &{ + let mut assets = Assets::new(); + assets.insert(&name, &to_bignum(*input)); + assets + }); + multiasset + }) + .collect::>(); + + for (multiasset, ada) in multiassets + .iter() + .zip([100u64, 100].iter().cloned().map(to_bignum)) + { + 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), &input_amount, ); @@ -1518,16 +1931,8 @@ mod tests { #[test] fn build_tx_with_native_assets_change_and_no_purification_cuz_not_enough_pure_coin() { - let linear_fee = LinearFee::new(&to_bignum(1), &to_bignum(1)); let minimum_utxo_value = to_bignum(10); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1), - ); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(1, 1)); // Prefer pure change! tx_builder.set_prefer_pure_change(true); let spend = root_key_15() @@ -1643,15 +2048,7 @@ mod tests { #[test] #[should_panic] fn build_tx_leftover_assets() { - let linear_fee = LinearFee::new(&to_bignum(500), &to_bignum(2)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(1), - &to_bignum(1), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(1) - ); + let mut tx_builder = create_default_tx_builder(); let spend = root_key_15() .derive(harden(1852)) .derive(harden(1815)) @@ -1717,16 +2114,8 @@ mod tests { #[test] fn build_tx_burn_less_than_min_ada() { - let linear_fee = LinearFee::new(&to_bignum(44), &to_bignum(155381)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(500000000), - &to_bignum(2000000), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - // with this mainnet value we should end up with a final min_ada_required of just under 1_000_000 - &to_bignum(COINS_PER_UTXO_WORD), - ); + // with this mainnet value we should end up with a final min_ada_required of just under 1_000_000 + let mut tx_builder = create_reallistic_tx_builder(); let output_addr = ByronAddress::from_base58("Ae2tdPwUPEZD9QQf2ZrcYV34pYJwxK4vqXaF8EXkup1eYH73zUScHReM42b").unwrap(); tx_builder.add_output(&TransactionOutput::new( @@ -1760,15 +2149,7 @@ mod tests { #[test] fn build_tx_burn_empty_assets() { - let linear_fee = LinearFee::new(&to_bignum(44), &to_bignum(155381)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(500000000), - &to_bignum(2000000), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(COINS_PER_UTXO_WORD) - ); + let mut tx_builder = create_reallistic_tx_builder(); let output_addr = ByronAddress::from_base58("Ae2tdPwUPEZD9QQf2ZrcYV34pYJwxK4vqXaF8EXkup1eYH73zUScHReM42b").unwrap(); tx_builder.add_output(&TransactionOutput::new( @@ -1804,16 +2185,7 @@ mod tests { #[test] fn build_tx_no_useless_multiasset() { - let linear_fee = LinearFee::new(&to_bignum(44), &to_bignum(155381)); - let mut tx_builder = - TransactionBuilder::new( - &linear_fee, - &to_bignum(500000000), - &to_bignum(2000000), - MAX_VALUE_SIZE, - MAX_TX_SIZE, - &to_bignum(COINS_PER_UTXO_WORD) - ); + let mut tx_builder = create_reallistic_tx_builder(); let policy_id = &PolicyID::from([0u8; 28]); let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); @@ -1897,15 +2269,10 @@ mod tests { #[test] fn build_tx_add_change_split_nfts() { - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); let max_value_size = 100; // super low max output size to test with fewer assets - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), + let mut tx_builder = create_tx_builder_with_fee_and_val_size( + &create_linear_fee(0, 1), max_value_size, - MAX_TX_SIZE, - &to_bignum(1) ); let (multiasset, policy_ids, names) = create_multiasset(); @@ -1954,14 +2321,9 @@ mod tests { #[test] fn build_tx_too_big_output() { - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), - 10, // super low max output size to test, - MAX_TX_SIZE, - &to_bignum(1) + let mut tx_builder = create_tx_builder_with_fee_and_val_size( + &create_linear_fee(0, 1), + 10, ); tx_builder.add_input( @@ -1970,11 +2332,11 @@ mod tests { &genesis_id(), 0 ), - &Value::new(&to_bignum(10)) + &Value::new(&to_bignum(500)) ); let output_addr = ByronAddress::from_base58("Ae2tdPwUPEZD9QQf2ZrcYV34pYJwxK4vqXaF8EXkup1eYH73zUScHReM42b").unwrap().to_address(); - let mut output_amount = Value::new(&to_bignum(1)); + let mut output_amount = Value::new(&to_bignum(50)); output_amount.set_multiasset(&create_multiasset().0); assert!(tx_builder.add_output(&TransactionOutput::new(&output_addr, &output_amount)).is_err()); @@ -1982,15 +2344,9 @@ mod tests { #[test] fn build_tx_add_change_nfts_not_enough_ada() { - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); - let max_value_size = 150; // super low max output size to test with fewer assets - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), - max_value_size, - MAX_TX_SIZE, - &to_bignum(1) + let mut tx_builder = create_tx_builder_with_fee_and_val_size( + &create_linear_fee(0, 1), + 150, // super low max output size to test with fewer assets ); let policy_ids = [ @@ -2029,7 +2385,7 @@ mod tests { ); let output_addr = ByronAddress::from_base58("Ae2tdPwUPEZD9QQf2ZrcYV34pYJwxK4vqXaF8EXkup1eYH73zUScHReM42b").unwrap().to_address(); - let output_amount = Value::new(&to_bignum(29)); + let output_amount = Value::new(&to_bignum(59)); tx_builder .add_output(&TransactionOutput::new(&output_addr, &output_amount)) @@ -2050,15 +2406,7 @@ mod tests { #[test] fn tx_builder_cip2_largest_first_increasing_fees() { // we have a = 1 to test increasing fees when more inputs are added - let linear_fee = LinearFee::new(&to_bignum(1), &to_bignum(0)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &Coin::zero(), - &to_bignum(0), - 9999, - 9999, - &to_bignum(0), - ); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(1, 0)); tx_builder.add_output(&TransactionOutput::new( &Address::from_bech32("addr1vyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmnqs6l44z").unwrap(), &Value::new(&to_bignum(1000)) @@ -2087,15 +2435,7 @@ mod tests { #[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 - let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(0)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &Coin::zero(), - &to_bignum(0), - 9999, - 9999, - &to_bignum(0), - ); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 0)); tx_builder.add_output(&TransactionOutput::new( &Address::from_bech32("addr1vyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmnqs6l44z").unwrap(), &Value::new(&to_bignum(1200)) @@ -2122,15 +2462,7 @@ mod tests { #[test] fn tx_builder_cip2_random_improve() { // we have a = 1 to test increasing fees when more inputs are added - let linear_fee = LinearFee::new(&to_bignum(1), &to_bignum(0)); - let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &Coin::zero(), - &to_bignum(0), - 9999, - 9999, - &to_bignum(0), - ); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(1, 0)); const COST: u64 = 10000; tx_builder.add_output(&TransactionOutput::new( &Address::from_bech32("addr1vyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmnqs6l44z").unwrap(), @@ -2196,10 +2528,40 @@ mod tests { assert!(add_inputs_res.is_ok(), "{:?}", add_inputs_res.err()); } - fn build_tx_pay_to_multisig() { - let linear_fee = LinearFee::new(&to_bignum(10), &to_bignum(2)); - let mut tx_builder = - TransactionBuilder::new(&linear_fee, &to_bignum(1), &to_bignum(1), MAX_VALUE_SIZE, MAX_TX_SIZE, &to_bignum(1)); + #[test] + fn tx_builder_cip2_random_improve_adds_enough_for_fees() { + // we have a = 1 to test increasing fees when more inputs are added + let linear_fee = LinearFee::new(&to_bignum(1), &to_bignum(0)); + let mut tx_builder = TransactionBuilder::new( + &linear_fee, + &Coin::zero(), + &to_bignum(0), + 9999, + 9999, + &to_bignum(0), + ); + const COST: u64 = 100; + tx_builder.add_output(&TransactionOutput::new( + &Address::from_bech32("addr1vyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmnqs6l44z").unwrap(), + &Value::new(&to_bignum(COST)) + )).unwrap(); + assert_eq!(tx_builder.min_fee().unwrap(), to_bignum(53)); + let mut available_inputs = TransactionUnspentOutputs::new(); + available_inputs.add(&make_input(1u8, Value::new(&to_bignum(150)))); + available_inputs.add(&make_input(2u8, Value::new(&to_bignum(150)))); + available_inputs.add(&make_input(3u8, Value::new(&to_bignum(150)))); + let add_inputs_res = + tx_builder.add_inputs_from(&available_inputs, CoinSelectionStrategyCIP2::RandomImprove); + assert!(add_inputs_res.is_ok(), "{:?}", add_inputs_res.err()); + assert_eq!(tx_builder.min_fee().unwrap(), to_bignum(264)); + let change_addr = ByronAddress::from_base58("Ae2tdPwUPEZGUEsuMAhvDcy94LKsZxDjCbgaiBBMgYpR8sKf96xJmit7Eho").unwrap().to_address(); + let add_change_res = tx_builder.add_change_if_needed(&change_addr); + assert!(add_change_res.is_ok(), "{:?}", add_change_res.err()); + } + + #[test] + fn build_tx_pay_to_multisig() { + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(10, 2)); let spend = root_key_15() .derive(harden(1854)) .derive(harden(1815)) @@ -2258,9 +2620,7 @@ mod tests { #[test] fn build_tx_multisig_spend_1on1_unsigned() { - let linear_fee = LinearFee::new(&to_bignum(10), &to_bignum(2)); - let mut tx_builder = - TransactionBuilder::new(&linear_fee, &to_bignum(1), &to_bignum(1), MAX_VALUE_SIZE, MAX_TX_SIZE, &to_bignum(1)); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(10, 2)); let spend = root_key_15()//multisig .derive(harden(1854)) @@ -2315,8 +2675,6 @@ mod tests { tx_builder.set_auxiliary_data(&auxiliary_data); - let body = tx_builder.build().unwrap(); - assert_eq!(tx_builder.outputs.len(), 1); assert_eq!( tx_builder.get_explicit_input().unwrap().checked_add(&tx_builder.get_implicit_input().unwrap()).unwrap(), @@ -2333,9 +2691,7 @@ mod tests { #[test] fn build_tx_multisig_1on1_signed() { - let linear_fee = LinearFee::new(&to_bignum(10), &to_bignum(2)); - let mut tx_builder = - TransactionBuilder::new(&linear_fee, &to_bignum(1), &to_bignum(1), MAX_VALUE_SIZE, MAX_TX_SIZE, &to_bignum(1)); + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(10, 2)); let spend = root_key_15() .derive(harden(1854))//multisig .derive(harden(1815)) @@ -2491,5 +2847,583 @@ mod tests { assert_eq!(v1.or(v2).unwrap(), to_bignum(500)); }); } + + fn create_json_metadatum_string() -> String { + String::from("{ \"qwe\": 123 }") + } + + fn create_json_metadatum() -> TransactionMetadatum { + encode_json_str_to_metadatum( + create_json_metadatum_string(), + MetadataJsonSchema::NoConversions, + ).unwrap() + } + + fn create_aux_with_metadata(metadatum_key: &TransactionMetadatumLabel) -> AuxiliaryData { + let mut metadata = GeneralTransactionMetadata::new(); + metadata.insert(metadatum_key, &create_json_metadatum()); + + let mut aux = AuxiliaryData::new(); + aux.set_metadata(&metadata); + + let mut nats = NativeScripts::new(); + nats.add( + &NativeScript::new_timelock_start( + &TimelockStart::new(123), + ), + ); + aux.set_native_scripts(&nats); + + return aux; + } + + fn assert_json_metadatum(dat: &TransactionMetadatum) { + let map = dat.as_map().unwrap(); + assert_eq!(map.len(), 1); + let key = TransactionMetadatum::new_text(String::from("qwe")).unwrap(); + let val = map.get(&key).unwrap(); + assert_eq!(val.as_int().unwrap(), Int::new_i32(123)); + } + + #[test] + fn set_metadata_with_empty_auxiliary() { + let mut tx_builder = create_default_tx_builder(); + + let num = to_bignum(42); + tx_builder.set_metadata(&create_aux_with_metadata(&num).metadata().unwrap()); + + assert!(tx_builder.auxiliary_data.is_some()); + + let aux = tx_builder.auxiliary_data.unwrap(); + assert!(aux.metadata().is_some()); + assert!(aux.native_scripts().is_none()); + assert!(aux.plutus_scripts().is_none()); + + let met = aux.metadata().unwrap(); + + assert_eq!(met.len(), 1); + assert_json_metadatum(&met.get(&num).unwrap()); + } + + #[test] + fn set_metadata_with_existing_auxiliary() { + let mut tx_builder = create_default_tx_builder(); + + let num1 = to_bignum(42); + tx_builder.set_auxiliary_data(&create_aux_with_metadata(&num1)); + + let num2 = to_bignum(84); + tx_builder.set_metadata(&create_aux_with_metadata(&num2).metadata().unwrap()); + + let aux = tx_builder.auxiliary_data.unwrap(); + assert!(aux.metadata().is_some()); + assert!(aux.native_scripts().is_some()); + assert!(aux.plutus_scripts().is_none()); + + let met = aux.metadata().unwrap(); + assert_eq!(met.len(), 1); + assert!(met.get(&num1).is_none()); + assert_json_metadatum(&met.get(&num2).unwrap()); + } + + #[test] + fn add_metadatum_with_empty_auxiliary() { + let mut tx_builder = create_default_tx_builder(); + + let num = to_bignum(42); + tx_builder.add_metadatum(&num, &create_json_metadatum()); + + assert!(tx_builder.auxiliary_data.is_some()); + + let aux = tx_builder.auxiliary_data.unwrap(); + assert!(aux.metadata().is_some()); + assert!(aux.native_scripts().is_none()); + assert!(aux.plutus_scripts().is_none()); + + let met = aux.metadata().unwrap(); + + assert_eq!(met.len(), 1); + assert_json_metadatum(&met.get(&num).unwrap()); + } + + #[test] + fn add_metadatum_with_existing_auxiliary() { + let mut tx_builder = create_default_tx_builder(); + + let num1 = to_bignum(42); + tx_builder.set_auxiliary_data(&create_aux_with_metadata(&num1)); + + let num2 = to_bignum(84); + tx_builder.add_metadatum(&num2, &create_json_metadatum()); + + let aux = tx_builder.auxiliary_data.unwrap(); + assert!(aux.metadata().is_some()); + assert!(aux.native_scripts().is_some()); + assert!(aux.plutus_scripts().is_none()); + + let met = aux.metadata().unwrap(); + assert_eq!(met.len(), 2); + assert_json_metadatum(&met.get(&num1).unwrap()); + assert_json_metadatum(&met.get(&num2).unwrap()); + } + + #[test] + fn add_json_metadatum_with_empty_auxiliary() { + let mut tx_builder = create_default_tx_builder(); + + let num = to_bignum(42); + tx_builder.add_json_metadatum(&num, create_json_metadatum_string()).unwrap(); + + assert!(tx_builder.auxiliary_data.is_some()); + + let aux = tx_builder.auxiliary_data.unwrap(); + assert!(aux.metadata().is_some()); + assert!(aux.native_scripts().is_none()); + assert!(aux.plutus_scripts().is_none()); + + let met = aux.metadata().unwrap(); + + assert_eq!(met.len(), 1); + assert_json_metadatum(&met.get(&num).unwrap()); + } + + #[test] + fn add_json_metadatum_with_existing_auxiliary() { + let mut tx_builder = create_default_tx_builder(); + + let num1 = to_bignum(42); + tx_builder.set_auxiliary_data(&create_aux_with_metadata(&num1)); + + let num2 = to_bignum(84); + tx_builder.add_json_metadatum(&num2, create_json_metadatum_string()).unwrap(); + + let aux = tx_builder.auxiliary_data.unwrap(); + assert!(aux.metadata().is_some()); + assert!(aux.native_scripts().is_some()); + assert!(aux.plutus_scripts().is_none()); + + let met = aux.metadata().unwrap(); + assert_eq!(met.len(), 2); + assert_json_metadatum(&met.get(&num1).unwrap()); + assert_json_metadatum(&met.get(&num2).unwrap()); + } + + fn create_asset_name() -> AssetName { + AssetName::new(vec![0u8, 1, 2, 3]).unwrap() + } + + fn create_mint_asset() -> MintAssets { + MintAssets::new_from_entry(&create_asset_name(), Int::new_i32(1234)) + } + + fn create_assets() -> Assets { + let mut assets = Assets::new(); + assets.insert(&create_asset_name(), &to_bignum(1234)); + return assets; + } + + fn create_mint_with_one_asset(policy_id: &PolicyID) -> Mint { + Mint::new_from_entry(policy_id, &create_mint_asset()) + } + + fn create_multiasset_one_asset(policy_id: &PolicyID) -> MultiAsset { + let mut mint = MultiAsset::new(); + mint.insert(policy_id, &create_assets()); + return mint; + } + + fn assert_mint_asset(mint: &Mint, policy_id: &PolicyID) { + assert!(mint.get(&policy_id).is_some()); + let result_asset = mint.get(&policy_id).unwrap(); + assert_eq!(result_asset.len(), 1); + assert_eq!(result_asset.get(&create_asset_name()).unwrap(), Int::new_i32(1234)); + } + + #[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()); + + assert!(tx_builder.mint.is_some()); + + let mint = tx_builder.mint.unwrap(); + + assert_eq!(mint.len(), 1); + assert_mint_asset(&mint, &policy_id); + } + + #[test] + fn set_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_id2 = PolicyID::from([1u8; 28]); + tx_builder.set_mint_asset(&policy_id2, &create_mint_asset()); + + assert!(tx_builder.mint.is_some()); + + let mint = tx_builder.mint.unwrap(); + + assert_eq!(mint.len(), 2); + assert_mint_asset(&mint, &policy_id1); + assert_mint_asset(&mint, &policy_id2); + } + + #[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)); + + assert!(tx_builder.mint.is_some()); + + let mint = tx_builder.mint.unwrap(); + + assert_eq!(mint.len(), 1); + assert_mint_asset(&mint, &policy_id); + } + + #[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_id2 = PolicyID::from([1u8; 28]); + tx_builder.add_mint_asset(&policy_id2, &create_asset_name(), Int::new_i32(1234)); + + assert!(tx_builder.mint.is_some()); + + let mint = tx_builder.mint.unwrap(); + + assert_eq!(mint.len(), 2); + assert_mint_asset(&mint, &policy_id1); + assert_mint_asset(&mint, &policy_id2); + } + + #[test] + fn add_output_amount() { + let mut tx_builder = create_default_tx_builder(); + + let policy_id1 = PolicyID::from([0u8; 28]); + let multiasset = create_multiasset_one_asset(&policy_id1); + let mut value = Value::new(&to_bignum(42)); + value.set_multiasset(&multiasset); + + let address = byron_address(); + tx_builder.add_output_amount(&address, &value).unwrap(); + + assert_eq!(tx_builder.outputs.len(), 1); + let out = tx_builder.outputs.get(0); + + assert_eq!(out.address.to_bytes(), address.to_bytes()); + assert_eq!(out.amount, value); + } + + #[test] + fn add_output_coin() { + let mut tx_builder = create_default_tx_builder(); + + let address = byron_address(); + let coin = to_bignum(43); + tx_builder.add_output_coin(&address, &coin).unwrap(); + + assert_eq!(tx_builder.outputs.len(), 1); + let out = tx_builder.outputs.get(0); + + assert_eq!(out.address.to_bytes(), address.to_bytes()); + assert_eq!(out.amount.coin, coin); + assert!(out.amount.multiasset.is_none()); + } + + #[test] + fn add_output_coin_and_multiasset() { + let mut tx_builder = create_default_tx_builder(); + + let policy_id1 = PolicyID::from([0u8; 28]); + let multiasset = create_multiasset_one_asset(&policy_id1); + + let address = byron_address(); + let coin = to_bignum(42); + + tx_builder.add_output_coin_and_asset(&address, &coin, &multiasset).unwrap(); + + assert_eq!(tx_builder.outputs.len(), 1); + let out = tx_builder.outputs.get(0); + + assert_eq!(out.address.to_bytes(), address.to_bytes()); + assert_eq!(out.amount.coin, coin); + assert_eq!(out.amount.multiasset.unwrap(), multiasset); + } + + #[test] + fn add_output_asset_and_min_required_coin() { + let mut tx_builder = create_reallistic_tx_builder(); + + let policy_id1 = PolicyID::from([0u8; 28]); + let multiasset = create_multiasset_one_asset(&policy_id1); + + let address = byron_address(); + tx_builder.add_output_asset_and_min_required_coin(&address, &multiasset).unwrap(); + + assert_eq!(tx_builder.outputs.len(), 1); + let out = tx_builder.outputs.get(0); + + assert_eq!(out.address.to_bytes(), address.to_bytes()); + assert_eq!(out.amount.multiasset.unwrap(), multiasset); + assert_eq!(out.amount.coin, to_bignum(1344798)); + } + + #[test] + 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 name = create_asset_name(); + let amount = Int::new_i32(1234); + + let address = byron_address(); + 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_and_output( + &policy_id1, + &name, + amount.clone(), + &address, + &coin, + ).unwrap(); + + assert!(tx_builder.mint.is_some()); + + let mint = tx_builder.mint.as_ref().unwrap(); + + // Mint contains two entries + assert_eq!(mint.len(), 2); + assert_mint_asset(mint, &policy_id0); + assert_mint_asset(mint, &policy_id1); + + // One new output is created + assert_eq!(tx_builder.outputs.len(), 1); + let out = tx_builder.outputs.get(0); + + assert_eq!(out.address.to_bytes(), address.to_bytes()); + assert_eq!(out.amount.coin, coin); + + let multiasset = out.amount.multiasset.unwrap(); + + // Only second mint entry was added to the output + assert_eq!(multiasset.len(), 1); + assert!(multiasset.get(&policy_id0).is_none()); + assert!(multiasset.get(&policy_id1).is_some()); + + let asset = multiasset.get(&policy_id1).unwrap(); + assert_eq!(asset.len(), 1); + assert_eq!(asset.get(&name).unwrap(), to_bignum(1234)); + } + + #[test] + 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 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_and_output_min_required_coin( + &policy_id1, + &name, + amount.clone(), + &address, + ).unwrap(); + + assert!(tx_builder.mint.is_some()); + + let mint = tx_builder.mint.as_ref().unwrap(); + + // Mint contains two entries + assert_eq!(mint.len(), 2); + assert_mint_asset(mint, &policy_id0); + assert_mint_asset(mint, &policy_id1); + + // One new output is created + assert_eq!(tx_builder.outputs.len(), 1); + let out = tx_builder.outputs.get(0); + + assert_eq!(out.address.to_bytes(), address.to_bytes()); + assert_eq!(out.amount.coin, to_bignum(1344798)); + + let multiasset = out.amount.multiasset.unwrap(); + + // Only second mint entry was added to the output + assert_eq!(multiasset.len(), 1); + assert!(multiasset.get(&policy_id0).is_none()); + assert!(multiasset.get(&policy_id1).is_some()); + + let asset = multiasset.get(&policy_id1).unwrap(); + assert_eq!(asset.len(), 1); + 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 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()), + ); + mint.insert( + &policy_id2, + &MintAssets::new_from_entry(&name2, amount.clone()), + ); + // 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); + + let mint_len = mint.to_bytes().len(); + let fee_coefficient = tx_builder.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)); + + tx_builder.set_mint(&mint); + + let new_tx_fee = tx_builder.min_fee().unwrap(); + + 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(); + + assert_eq!(witness_fee_increase, to_bignum(4356)); + + 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); + } + + #[test] + fn total_input_with_mint_and_burn() { + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 1)); + let spend = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(0) + .derive(0) + .to_public(); + let change_key = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(1) + .derive(0) + .to_public(); + let stake = root_key_15() + .derive(harden(1852)) + .derive(harden(1815)) + .derive(harden(0)) + .derive(2) + .derive(0) + .to_public(); + + let policy_id1 = &PolicyID::from([0u8; 28]); + let policy_id2 = &PolicyID::from([1u8; 28]); + let name = AssetName::new(vec![0u8, 1, 2, 3]).unwrap(); + + let ma_input1 = 100; + let ma_input2 = 200; + let ma_output1 = 60; + + let multiassets = [ma_input1, ma_input2, ma_output1] + .iter() + .map(|input| { + let mut multiasset = MultiAsset::new(); + multiasset.insert(policy_id1, &{ + let mut assets = Assets::new(); + assets.insert(&name, &to_bignum(*input)); + assets + }); + multiasset.insert(policy_id2, &{ + let mut assets = Assets::new(); + assets.insert(&name, &to_bignum(*input)); + assets + }); + multiasset + }) + .collect::>(); + + for (multiasset, ada) in multiassets + .iter() + .zip([100u64, 100, 100].iter().cloned().map(to_bignum)) + { + 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), + &input_amount, + ); + } + + let total_input_before_mint = tx_builder.get_total_input().unwrap(); + + 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)); + + tx_builder.add_mint_asset(policy_id1, &name, Int::new_i32(40)); + tx_builder.add_mint_asset(policy_id2, &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)); + } + } diff --git a/rust/src/utils.rs b/rust/src/utils.rs index 69e3b796..fd5dc13f 100644 --- a/rust/src/utils.rs +++ b/rust/src/utils.rs @@ -3,7 +3,6 @@ use hex::FromHex; use serde_json; use std::{collections::HashMap, io::{BufRead, Seek, Write}}; use itertools::Itertools; -use std::cmp; use std::ops::{Rem, Div, Sub}; use super::*; @@ -265,6 +264,7 @@ to_from_bytes!(Value); #[wasm_bindgen] impl Value { + pub fn new(coin: &Coin) -> Value { Self { coin: coin.clone(), @@ -272,8 +272,18 @@ impl Value { } } + pub fn new_from_assets(multiasset: &MultiAsset) -> Value { + match multiasset.0.is_empty() { + true => Value::zero(), + false => Self { + coin: Coin::zero(), + multiasset: Some(multiasset.clone()), + } + } + } + pub fn zero() -> Value { - Value::new(&to_bignum(0)) + Value::new(&Coin::zero()) } pub fn is_zero(&self) -> bool { @@ -346,7 +356,7 @@ impl Value { let coin = self.coin.checked_sub(&rhs_value.coin)?; let multiasset = match(&self.multiasset, &rhs_value.multiasset) { (Some(lhs_ma), Some(rhs_ma)) => { - match (lhs_ma.sub(rhs_ma).len()) { + match lhs_ma.sub(rhs_ma).len() { 0 => None, _ => Some(lhs_ma.sub(rhs_ma)) } @@ -363,7 +373,7 @@ impl Value { let coin = self.coin.clamped_sub(&rhs_value.coin); let multiasset = match(&self.multiasset, &rhs_value.multiasset) { (Some(lhs_ma), Some(rhs_ma)) => { - match (lhs_ma.sub(rhs_ma).len()) { + match lhs_ma.sub(rhs_ma).len() { 0 => None, _ => Some(lhs_ma.sub(rhs_ma)) } @@ -487,6 +497,12 @@ impl Int { return self.0 >= 0 } + /// BigNum can only contain unsigned u64 values + /// + /// This function will return the BigNum representation + /// only in case the underlying i128 value is positive. + /// + /// Otherwise nothing will be returned (undefined). pub fn as_positive(&self) -> Option { if self.is_positive() { Some(to_bignum(self.0 as u64)) @@ -495,6 +511,12 @@ impl Int { } } + /// BigNum can only contain unsigned u64 values + /// + /// This function will return the *absolute* BigNum representation + /// only in case the underlying i128 value is negative. + /// + /// Otherwise nothing will be returned (undefined). pub fn as_negative(&self) -> Option { if !self.is_positive() { Some(to_bignum((-self.0) as u64)) @@ -503,10 +525,37 @@ impl Int { } } + /// !!! DEPRECATED !!! + /// Returns an i32 value in case the underlying original i128 value is within the limits. + /// Otherwise will just return an empty value (undefined). + #[deprecated( + since = "10.0.0", + note = "Unsafe ignoring of possible boundary error and it's not clear from the function name. Use `as_i32_or_nothing`, `as_i32_or_fail`, or `to_str`" + )] pub fn as_i32(&self) -> Option { + self.as_i32_or_nothing() + } + + /// Returns the underlying value converted to i32 if possible (within limits) + /// Otherwise will just return an empty value (undefined). + pub fn as_i32_or_nothing(&self) -> Option { use std::convert::TryFrom; i32::try_from(self.0).ok() } + + /// Returns the underlying value converted to i32 if possible (within limits) + /// JsError in case of out of boundary overflow + pub fn as_i32_or_fail(&self) -> Result { + use std::convert::TryFrom; + i32::try_from(self.0) + .map_err(|e| JsError::from_str(&format!("{}", e))) + } + + /// Returns string representation of the underlying i128 value directly. + /// Might contain the minus sign (-) in case of negative value. + pub fn to_str(&self) -> String { + format!("{}", self.0) + } } impl cbor_event::se::Serialize for Int { @@ -987,7 +1036,7 @@ fn bundle_size( // converts bytes to 8-byte long words, rounding up fn roundup_bytes_to_words(b: usize) -> usize { quot(b + 7, 8) - }; + } constants.k0 + roundup_bytes_to_words( (num_assets * constants.k1) + sum_asset_name_lengths + (constants.k2 * sum_policy_id_lengths) @@ -1229,8 +1278,6 @@ fn encode_template_to_native_script( #[cfg(test)] mod tests { - use hex::FromHex; - use super::*; // this is what is used in mainnet @@ -2122,7 +2169,7 @@ mod tests { assert_eq!( hex::encode(script_data_hash.to_bytes()), - "57240d358f8ab6128c4a66340271e4fec39b4971232add308f01a5809313adcf" + "4415e6667e6d6bbd992af5092d48e3c2ba9825200d0234d2470068f7f0f178b3" ); } @@ -2192,4 +2239,60 @@ mod tests { Bip32PublicKey::from_bytes(&hex::decode(self_key_hex).unwrap()).unwrap().to_raw_key().hash() ); } + + #[test] + fn int_to_str() { + assert_eq!(Int::new(&BigNum(u64::max_value())).to_str(), u64::max_value().to_string()); + assert_eq!(Int::new(&BigNum(u64::min_value())).to_str(), u64::min_value().to_string()); + assert_eq!(Int::new_negative(&BigNum(u64::max_value())).to_str(), (-(u64::max_value() as i128)).to_string()); + assert_eq!(Int::new_negative(&BigNum(u64::min_value())).to_str(), (-(u64::min_value() as i128)).to_string()); + assert_eq!(Int::new_i32(142).to_str(), "142"); + assert_eq!(Int::new_i32(-142).to_str(), "-142"); + } + + #[test] + fn int_as_i32_or_nothing() { + + let over_pos_i32 = (i32::max_value() as i64) + 1; + assert!(Int::new(&BigNum(over_pos_i32 as u64)).as_i32_or_nothing().is_none()); + + let valid_pos_i32 = i32::max_value() as i64; + assert_eq!(Int::new(&BigNum(valid_pos_i32 as u64)).as_i32_or_nothing().unwrap(), i32::max_value()); + + let over_neg_i32 = (i32::min_value() as i64) - 1; + assert!(Int::new_negative(&BigNum((-over_neg_i32) as u64)).as_i32_or_nothing().is_none()); + + let valid_neg_i32 = i32::min_value() as i64; + assert_eq!(Int::new_negative(&BigNum((-valid_neg_i32) as u64)).as_i32_or_nothing().unwrap(), i32::min_value()); + + assert!(Int::new(&BigNum(u64::max_value())).as_i32_or_nothing().is_none()); + assert_eq!(Int::new(&BigNum(i32::max_value() as u64)).as_i32_or_nothing().unwrap(), i32::max_value()); + assert_eq!(Int::new_negative(&BigNum(i32::max_value() as u64)).as_i32_or_nothing().unwrap(), -i32::max_value()); + + assert_eq!(Int::new_i32(42).as_i32_or_nothing().unwrap(), 42); + assert_eq!(Int::new_i32(-42).as_i32_or_nothing().unwrap(), -42); + } + + #[test] + fn int_as_i32_or_fail() { + + let over_pos_i32 = (i32::max_value() as i64) + 1; + assert!(Int::new(&BigNum(over_pos_i32 as u64)).as_i32_or_fail().is_err()); + + let valid_pos_i32 = i32::max_value() as i64; + assert_eq!(Int::new(&BigNum(valid_pos_i32 as u64)).as_i32_or_fail().unwrap(), i32::max_value()); + + let over_neg_i32 = (i32::min_value() as i64) - 1; + assert!(Int::new_negative(&BigNum((-over_neg_i32) as u64)).as_i32_or_fail().is_err()); + + let valid_neg_i32 = i32::min_value() as i64; + assert_eq!(Int::new_negative(&BigNum((-valid_neg_i32) as u64)).as_i32_or_fail().unwrap(), i32::min_value()); + + assert!(Int::new(&BigNum(u64::max_value())).as_i32_or_fail().is_err()); + assert_eq!(Int::new(&BigNum(i32::max_value() as u64)).as_i32_or_fail().unwrap(), i32::max_value()); + assert_eq!(Int::new_negative(&BigNum(i32::max_value() as u64)).as_i32_or_fail().unwrap(), -i32::max_value()); + + assert_eq!(Int::new_i32(42).as_i32_or_fail().unwrap(), 42); + assert_eq!(Int::new_i32(-42).as_i32_or_fail().unwrap(), -42); + } } From de76ca8aac891528b816e2bb33a1c775b986a838 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Wed, 24 Nov 2021 11:58:29 +0300 Subject: [PATCH 5/5] Updating how tx-builder is used in tests (after merge) --- rust/src/tx_builder.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index 0f5a0711..7c81d149 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -2847,14 +2847,17 @@ mod tests { let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(1)); let max_value_size = 100; // super low max output size to test with fewer assets let mut tx_builder = TransactionBuilder::new( - &linear_fee, - &to_bignum(0), - &to_bignum(0), - max_value_size, - MAX_TX_SIZE, - &to_bignum(1), + &TransactionBuilderConfigBuilder::new() + .fee_algo(linear_fee) + .pool_deposit(to_bignum(0)) + .key_deposit(to_bignum(0)) + .max_value_size(max_value_size) + .max_tx_size(MAX_TX_SIZE) + .coins_per_utxo_word(to_bignum(1)) + .prefer_pure_change(true) + .build() + .unwrap() ); - tx_builder.set_prefer_pure_change(true); let policy_id = PolicyID::from([0u8; 28]); let names = [