From 2e523242ae20cb932ec971590247ca0b43f2e13e Mon Sep 17 00:00:00 2001 From: Gavin Harris Date: Mon, 25 Sep 2023 20:48:04 +1000 Subject: [PATCH 1/5] feat: Add support for maximum assets per change output This commit adds a new configuration option `MAX_ASSETS_PER_CHANGE_OUTPUT` to the `config.js` file. If the value is set, it limits the maximum number of assets per change output. The `Tx` class in `tx-builder.js` now checks this configuration and splits the change output into multiple outputs if the number of assets exceeds the limit. --- helios-internal.d.ts | 2 + helios.d.ts | 8 ++++ helios.js | 37 +++++++++++++++- src/config.js | 8 ++++ src/tx-builder.js | 29 ++++++++++++- test/tx-building.test.js | 92 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 171 insertions(+), 5 deletions(-) diff --git a/helios-internal.d.ts b/helios-internal.d.ts index 8f4f13f3..6b11be9c 100644 --- a/helios-internal.d.ts +++ b/helios-internal.d.ts @@ -863,6 +863,7 @@ declare module "helios" { VALIDITY_RANGE_END_OFFSET?: number | undefined; IGNORE_UNEVALUATED_CONSTANTS?: boolean | undefined; CHECK_CASTS?: boolean | undefined; + MAX_ASSETS_PER_CHANGE_OUTPUT: number; }): void; const DEBUG: boolean; const STRICT_BABBAGE: boolean; @@ -873,6 +874,7 @@ declare module "helios" { const VALIDITY_RANGE_END_OFFSET: number; const IGNORE_UNEVALUATED_CONSTANTS: boolean; const CHECK_CASTS: boolean; + const MAX_ASSETS_PER_CHANGE_OUTPUT: undefined; } /** * Read non-byte aligned numbers diff --git a/helios.d.ts b/helios.d.ts index 8e60413f..72712745 100644 --- a/helios.d.ts +++ b/helios.d.ts @@ -123,6 +123,7 @@ export namespace config { * VALIDITY_RANGE_END_OFFSET?: number * IGNORE_UNEVALUATED_CONSTANTS?: boolean * CHECK_CASTS?: boolean + * MAX_ASSETS_PER_CHANGE_OUTPUT: number * }} props */ function set(props: { @@ -135,6 +136,7 @@ export namespace config { VALIDITY_RANGE_END_OFFSET?: number | undefined; IGNORE_UNEVALUATED_CONSTANTS?: boolean | undefined; CHECK_CASTS?: boolean | undefined; + MAX_ASSETS_PER_CHANGE_OUTPUT: number; }): void; /** * Global debug flag. Currently unused. @@ -207,6 +209,12 @@ export namespace config { * @type {boolean} */ const CHECK_CASTS: boolean; + /** + * Maximum number of assets per change output. Used to break up very large asset outputs into multiple outputs. + * + * Default: `undefined` (no limit). + */ + const MAX_ASSETS_PER_CHANGE_OUTPUT: undefined; } /** * Function that generates a random number between 0 and 1 diff --git a/helios.js b/helios.js index 084e85bd..aae4bbeb 100644 --- a/helios.js +++ b/helios.js @@ -335,6 +335,7 @@ export const config = { * VALIDITY_RANGE_END_OFFSET?: number * IGNORE_UNEVALUATED_CONSTANTS?: boolean * CHECK_CASTS?: boolean + * MAX_ASSETS_PER_CHANGE_OUTPUT: number * }} props */ set: (props) => { @@ -424,6 +425,13 @@ export const config = { * @type {boolean} */ CHECK_CASTS: false, + + /** + * Maximum number of assets per change output. Used to break up very large asset outputs into multiple outputs. + * + * Default: `undefined` (no limit). + */ + MAX_ASSETS_PER_CHANGE_OUTPUT: undefined, } @@ -44802,9 +44810,34 @@ export class Tx extends CborData { } else { const diff = inputAssets.sub(outputAssets); - const changeOutput = new TxOutput(changeAddress, new Value(0n, diff)); + if (config.MAX_ASSETS_PER_CHANGE_OUTPUT) { + const maxAssetsPerOutput = config.MAX_ASSETS_PER_CHANGE_OUTPUT; + + let changeAssets = new Assets(); + let tokensAdded = 0; + + diff.mintingPolicies.forEach((mph) => { + const tokens = diff.getTokens(mph); + tokens.forEach(([token, quantity], i) => { + changeAssets.addComponent(mph, token, quantity); + tokensAdded += 1; + if (tokensAdded == maxAssetsPerOutput) { + this.#body.addOutput(new TxOutput(changeAddress, new Value(0n, changeAssets))); + changeAssets = new Assets(); + tokensAdded = 0; + } + }); + }); - this.#body.addOutput(changeOutput); + // If we are here and have No assets, they we're done + if (!changeAssets.isZero()) { + this.#body.addOutput(new TxOutput(changeAddress, new Value(0n, changeAssets))); + } + } else { + const changeOutput = new TxOutput(changeAddress, new Value(0n, diff)); + + this.#body.addOutput(changeOutput); + } } } diff --git a/src/config.js b/src/config.js index 456d7ec3..390d584a 100644 --- a/src/config.js +++ b/src/config.js @@ -33,6 +33,7 @@ export const config = { * VALIDITY_RANGE_END_OFFSET?: number * IGNORE_UNEVALUATED_CONSTANTS?: boolean * CHECK_CASTS?: boolean + * MAX_ASSETS_PER_CHANGE_OUTPUT: number * }} props */ set: (props) => { @@ -122,4 +123,11 @@ export const config = { * @type {boolean} */ CHECK_CASTS: false, + + /** + * Maximum number of assets per change output. Used to break up very large asset outputs into multiple outputs. + * + * Default: `undefined` (no limit). + */ + MAX_ASSETS_PER_CHANGE_OUTPUT: undefined, } diff --git a/src/tx-builder.js b/src/tx-builder.js index 0653c220..2f38306f 100644 --- a/src/tx-builder.js +++ b/src/tx-builder.js @@ -867,9 +867,34 @@ export class Tx extends CborData { } else { const diff = inputAssets.sub(outputAssets); - const changeOutput = new TxOutput(changeAddress, new Value(0n, diff)); + if (config.MAX_ASSETS_PER_CHANGE_OUTPUT) { + const maxAssetsPerOutput = config.MAX_ASSETS_PER_CHANGE_OUTPUT; + + let changeAssets = new Assets(); + let tokensAdded = 0; + + diff.mintingPolicies.forEach((mph) => { + const tokens = diff.getTokens(mph); + tokens.forEach(([token, quantity], i) => { + changeAssets.addComponent(mph, token, quantity); + tokensAdded += 1; + if (tokensAdded == maxAssetsPerOutput) { + this.#body.addOutput(new TxOutput(changeAddress, new Value(0n, changeAssets))); + changeAssets = new Assets(); + tokensAdded = 0; + } + }); + }); - this.#body.addOutput(changeOutput); + // If we are here and have No assets, they we're done + if (!changeAssets.isZero()) { + this.#body.addOutput(new TxOutput(changeAddress, new Value(0n, changeAssets))); + } + } else { + const changeOutput = new TxOutput(changeAddress, new Value(0n, diff)); + + this.#body.addOutput(changeOutput); + } } } diff --git a/test/tx-building.test.js b/test/tx-building.test.js index 36edaadd..5eef4e5d 100755 --- a/test/tx-building.test.js +++ b/test/tx-building.test.js @@ -23,7 +23,8 @@ import { assert, bytesToHex, hexToBytes, - textToBytes + textToBytes, + config } from "helios" const networkParams = new NetworkParams(JSON.parse(fs.readFileSync("./network-parameters-preview.json").toString())); @@ -621,6 +622,93 @@ async function sortInputs() { console.log(inputs.map(i => i.txId.hex)); } +async function testAssetSplitOnChangeOutput() { + + const inputClean = new TxInput( + new TxOutputId("a66564e90416a3c3ed89350108799ab122bdbfd098624d0f43f955207ace8eda#1"), + new TxOutput( + new Address("addr_test1wpcwnce7k66ldmduhkqdrgamxmnytekhr2hyp8ncsdcg0aqufrga4"), + new Value(12000000n) + )); + + const input = new TxInput( + new TxOutputId("fed1bb855c77efd1fa209a1b35c447b13d4b09671f7d682263b9f3af1089f58c#1"), + new TxOutput( + new Address("addr_test1qruk42fdnsvvyuha6z23dagxq5966h68ta8d42cdsa6e05muqmq4j86269r4ckhjsvmapapl24fazrtl22yg9sn9pvfsz4vr2h"), + new Value(14172320n, new Assets([[ + "5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7594", [ + ["48656C6C6F20776F726C642031", 1n], + ["48656C6C6F20776F726C642032", 1n], + ["48656C6C6F20776F726C642033", 1n], + ["48656C6C6F20776F726C642034", 1n], + ["48656C6C6F20776F726C642035", 1n], + ["48656C6C6F20776F726C642036", 1n], + ["48656C6C6F20776F726C642037", 1n], + ["48656C6C6F20776F726C642038", 1n], + ["48656C6C6F20776F726C642039", 1n], + ["48656C6C6F20776F726C642040", 1n], + ["48656C6C6F20776F726C642041", 1n], + ["48656C6C6F20776F726C642042", 1n], + ["48656C6C6F20776F726C642043", 1n], + ["48656C6C6F20776F726C642044", 1n], + ["48656C6C6F20776F726C642045", 1n], + ["48656C6C6F20776F726C642046", 1n], + ["48656C6C6F20776F726C642047", 1n], + ["48656C6C6F20776F726C642048", 1n], + ["48656C6C6F20776F726C642049", 1n], + ["48656C6C6F20776F726C642040", 1n], + ["48656C6C6F20776F726C642051", 1n], + ["48656C6C6F20776F726C642052", 1n], + ["48656C6C6F20776F726C642053", 1n], + ["48656C6C6F20776F726C642054", 1n], + ["48656C6C6F20776F726C642055", 1n], + ["48656C6C6F20776F726C642056", 1n], + ["48656C6C6F20776F726C642057", 1n], + ["48656C6C6F20776F726C642058", 1n], + ["48656C6C6F20776F726C642059", 1n], + ["48656C6C6F20776F726C642050", 1n], + ["48656C6C6F20776F726C642061", 1n], + ["48656C6C6F20776F726C642062", 1n], + ["48656C6C6F20776F726C642063", 1n], + ["48656C6C6F20776F726C642064", 1n], + ["48656C6C6F20776F726C642065", 1n], + ["48656C6C6F20776F726C642066", 1n], + ["48656C6C6F20776F726C642067", 1n], + ["48656C6C6F20776F726C642068", 1n], + ["48656C6C6F20776F726C642069", 1n], + ["48656C6C6F20776F726C642060", 1n], + ] + ]])) + ) + ); + + const changeAddress = Address.fromBech32('addr_test1vrk907u2q3tnakfwvwmdl89jhlzy7tfqaqxwzwsch3afw0qqarpt4'); + + let tx = await new Tx() + .addInput(input) + .finalize(networkParams, changeAddress, [inputClean]) + + console.log(tx.body.outputs.length); + let assetsInOutput = tx.body.outputs.map((o) => o.value.assets.getTokenNames(MintingPolicyHash.fromHex('5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7594')).length) + console.log( assetsInOutput.length === 2, assetsInOutput[0] == 39 ); + + config.set({ + MAX_ASSETS_PER_CHANGE_OUTPUT: 5 + }); + + tx = await new Tx() + .addInput(input) + .finalize(networkParams, changeAddress, [inputClean]); + + console.log(tx.body.outputs.length); + assetsInOutput = tx.body.outputs.map((o) => o.value.assets.getTokenNames(MintingPolicyHash.fromHex('5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7594')).length) + console.log( assetsInOutput.length === 9, [0, 1, 2, 3, 4,5,6].map((i) => assetsInOutput[i] === 5 ), assetsInOutput[7] === 4 ); + + config.set({ + MAX_ASSETS_PER_CHANGE_OUTPUT: undefined + }); +} + export default async function main() { await assetsCompare(); @@ -653,4 +741,6 @@ export default async function main() { await testEmulatorPrivateKeyGen(); await sortInputs(); + + await testAssetSplitOnChangeOutput(); } \ No newline at end of file From da74715d0a4d54412c42cfa31e871dba33161217 Mon Sep 17 00:00:00 2001 From: Gavin Harris Date: Mon, 25 Sep 2023 21:55:46 +1000 Subject: [PATCH 2/5] Made the MAX_ASSETS_PER_CHANGE_OUTPUT optional in the set method --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index 390d584a..7be76b0c 100644 --- a/src/config.js +++ b/src/config.js @@ -33,7 +33,7 @@ export const config = { * VALIDITY_RANGE_END_OFFSET?: number * IGNORE_UNEVALUATED_CONSTANTS?: boolean * CHECK_CASTS?: boolean - * MAX_ASSETS_PER_CHANGE_OUTPUT: number + * MAX_ASSETS_PER_CHANGE_OUTPUT?: number * }} props */ set: (props) => { From f5e634b24556fb955b33b86e7304f7155ab76bd7 Mon Sep 17 00:00:00 2001 From: Gavin Harris Date: Mon, 25 Sep 2023 21:57:34 +1000 Subject: [PATCH 3/5] Build to update the set property to be optional --- helios-internal.d.ts | 2 +- helios.d.ts | 4 ++-- helios.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/helios-internal.d.ts b/helios-internal.d.ts index 6b11be9c..ecc544b6 100644 --- a/helios-internal.d.ts +++ b/helios-internal.d.ts @@ -863,7 +863,7 @@ declare module "helios" { VALIDITY_RANGE_END_OFFSET?: number | undefined; IGNORE_UNEVALUATED_CONSTANTS?: boolean | undefined; CHECK_CASTS?: boolean | undefined; - MAX_ASSETS_PER_CHANGE_OUTPUT: number; + MAX_ASSETS_PER_CHANGE_OUTPUT?: number | undefined; }): void; const DEBUG: boolean; const STRICT_BABBAGE: boolean; diff --git a/helios.d.ts b/helios.d.ts index 72712745..e3383849 100644 --- a/helios.d.ts +++ b/helios.d.ts @@ -123,7 +123,7 @@ export namespace config { * VALIDITY_RANGE_END_OFFSET?: number * IGNORE_UNEVALUATED_CONSTANTS?: boolean * CHECK_CASTS?: boolean - * MAX_ASSETS_PER_CHANGE_OUTPUT: number + * MAX_ASSETS_PER_CHANGE_OUTPUT?: number * }} props */ function set(props: { @@ -136,7 +136,7 @@ export namespace config { VALIDITY_RANGE_END_OFFSET?: number | undefined; IGNORE_UNEVALUATED_CONSTANTS?: boolean | undefined; CHECK_CASTS?: boolean | undefined; - MAX_ASSETS_PER_CHANGE_OUTPUT: number; + MAX_ASSETS_PER_CHANGE_OUTPUT?: number | undefined; }): void; /** * Global debug flag. Currently unused. diff --git a/helios.js b/helios.js index aae4bbeb..948c32e1 100644 --- a/helios.js +++ b/helios.js @@ -335,7 +335,7 @@ export const config = { * VALIDITY_RANGE_END_OFFSET?: number * IGNORE_UNEVALUATED_CONSTANTS?: boolean * CHECK_CASTS?: boolean - * MAX_ASSETS_PER_CHANGE_OUTPUT: number + * MAX_ASSETS_PER_CHANGE_OUTPUT?: number * }} props */ set: (props) => { From 6f2c54a9214fdb1f7b76731c4cf1bbff63233cb7 Mon Sep 17 00:00:00 2001 From: Gavin Harris Date: Tue, 26 Sep 2023 13:52:54 +1000 Subject: [PATCH 4/5] Try to handle the UTxO is too fragmented issue --- helios.js | 13 ++- src/tx-builder.js | 13 ++- test/tx-building.test.js | 175 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 194 insertions(+), 7 deletions(-) diff --git a/helios.js b/helios.js index 948c32e1..91d121ae 100644 --- a/helios.js +++ b/helios.js @@ -44989,7 +44989,7 @@ export class Tx extends CborData { nonChangeOutputValue = feeValue.add(nonChangeOutputValue); // this is quite restrictive, but we really don't want to touch UTxOs containing assets just for balancing purposes - const spareAssetUTxOs = spareUtxos.some(utxo => !utxo.value.assets.isZero()); + const spareAssetUTxOs = spareUtxos.filter(utxo => !utxo.value.assets.isZero()); spareUtxos = spareUtxos.filter(utxo => utxo.value.assets.isZero()); // use some spareUtxos if the inputValue doesn't cover the outputs and fees @@ -44999,8 +44999,15 @@ export class Tx extends CborData { let spare = spareUtxos.pop(); if (spare === undefined) { - if (spareAssetUTxOs) { - throw new Error(`UTxOs too fragmented`); + if (spareAssetUTxOs.length > 0) { + spare = spareAssetUTxOs.pop(); + + if (!spare){ + throw new Error(`UTxOs too fragmented - or no Spare UTxOs available to fix this mess`); + } + + this.#body.addInput(spare); + this.balanceAssets(changeAddress); } else { throw new Error(`need ${totalOutputValue.lovelace} lovelace, but only have ${inputValue.lovelace}`); } diff --git a/src/tx-builder.js b/src/tx-builder.js index 2f38306f..5887637c 100644 --- a/src/tx-builder.js +++ b/src/tx-builder.js @@ -1046,7 +1046,7 @@ export class Tx extends CborData { nonChangeOutputValue = feeValue.add(nonChangeOutputValue); // this is quite restrictive, but we really don't want to touch UTxOs containing assets just for balancing purposes - const spareAssetUTxOs = spareUtxos.some(utxo => !utxo.value.assets.isZero()); + const spareAssetUTxOs = spareUtxos.filter(utxo => !utxo.value.assets.isZero()); spareUtxos = spareUtxos.filter(utxo => utxo.value.assets.isZero()); // use some spareUtxos if the inputValue doesn't cover the outputs and fees @@ -1056,8 +1056,15 @@ export class Tx extends CborData { let spare = spareUtxos.pop(); if (spare === undefined) { - if (spareAssetUTxOs) { - throw new Error(`UTxOs too fragmented`); + if (spareAssetUTxOs.length > 0) { + spare = spareAssetUTxOs.pop(); + + if (!spare){ + throw new Error(`UTxOs too fragmented - or no Spare UTxOs available to fix this mess`); + } + + this.#body.addInput(spare); + this.balanceAssets(changeAddress); } else { throw new Error(`need ${totalOutputValue.lovelace} lovelace, but only have ${inputValue.lovelace}`); } diff --git a/test/tx-building.test.js b/test/tx-building.test.js index 5eef4e5d..8b313491 100755 --- a/test/tx-building.test.js +++ b/test/tx-building.test.js @@ -656,7 +656,7 @@ async function testAssetSplitOnChangeOutput() { ["48656C6C6F20776F726C642047", 1n], ["48656C6C6F20776F726C642048", 1n], ["48656C6C6F20776F726C642049", 1n], - ["48656C6C6F20776F726C642040", 1n], + ["48656C6C6F20776F726C642050", 1n], ["48656C6C6F20776F726C642051", 1n], ["48656C6C6F20776F726C642052", 1n], ["48656C6C6F20776F726C642053", 1n], @@ -708,6 +708,177 @@ async function testAssetSplitOnChangeOutput() { MAX_ASSETS_PER_CHANGE_OUTPUT: undefined }); } +async function testAssetSplitOnChangeOutputMultiPolicy() { + + const inputClean = new TxInput( + new TxOutputId("a66564e90416a3c3ed89350108799ab122bdbfd098624d0f43f955207ace8eda#1"), + new TxOutput( + new Address("addr_test1wpcwnce7k66ldmduhkqdrgamxmnytekhr2hyp8ncsdcg0aqufrga4"), + new Value(12000000n) + )); + + const input = new TxInput( + new TxOutputId("fed1bb855c77efd1fa209a1b35c447b13d4b09671f7d682263b9f3af1089f58c#1"), + new TxOutput( + new Address("addr_test1qruk42fdnsvvyuha6z23dagxq5966h68ta8d42cdsa6e05muqmq4j86269r4ckhjsvmapapl24fazrtl22yg9sn9pvfsz4vr2h"), + new Value(14172320n, new Assets([[ + "5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7594", [ + ["48656C6C6F20776F726C642031", 1n], + ["48656C6C6F20776F726C642032", 1n], + ["48656C6C6F20776F726C642033", 1n], + ["48656C6C6F20776F726C642034", 1n], + ["48656C6C6F20776F726C642035", 1n], + ["48656C6C6F20776F726C642036", 1n], + ["48656C6C6F20776F726C642037", 1n], + ["48656C6C6F20776F726C642038", 1n], + ["48656C6C6F20776F726C642039", 1n], + ["48656C6C6F20776F726C642040", 1n], + ["48656C6C6F20776F726C642041", 1n], + ["48656C6C6F20776F726C642042", 1n], + ["48656C6C6F20776F726C642043", 1n], + ["48656C6C6F20776F726C642044", 1n], + ["48656C6C6F20776F726C642045", 1n], + ["48656C6C6F20776F726C642046", 1n], + ["48656C6C6F20776F726C642047", 1n], + ["48656C6C6F20776F726C642048", 1n], + ["48656C6C6F20776F726C642049", 1n], + ["48656C6C6F20776F726C642040", 1n], + ["48656C6C6F20776F726C642051", 1n], + ["48656C6C6F20776F726C642052", 1n], + ["48656C6C6F20776F726C642053", 1n], + ["48656C6C6F20776F726C642054", 1n], + ["48656C6C6F20776F726C642055", 1n], + ["48656C6C6F20776F726C642056", 1n], + ["48656C6C6F20776F726C642057", 1n], + ["48656C6C6F20776F726C642058", 1n], + ["48656C6C6F20776F726C642059", 1n], + ["48656C6C6F20776F726C642050", 1n], + ["48656C6C6F20776F726C642061", 1n], + ["48656C6C6F20776F726C642062", 1n], + ["48656C6C6F20776F726C642063", 1n], + ["48656C6C6F20776F726C642064", 1n], + ["48656C6C6F20776F726C642065", 1n], + ["48656C6C6F20776F726C642066", 1n], + ["48656C6C6F20776F726C642067", 1n], + ["48656C6C6F20776F726C642068", 1n], + ["48656C6C6F20776F726C642069", 1n], + ["48656C6C6F20776F726C642060", 1n], + ] + ], [ + "5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7595", [ + ["48656C6C6F20776F726C642031", 1n], + ["48656C6C6F20776F726C642032", 1n], + ["48656C6C6F20776F726C642033", 1n], + ["48656C6C6F20776F726C642034", 1n], + ["48656C6C6F20776F726C642035", 1n], + ["48656C6C6F20776F726C642036", 1n], + ["48656C6C6F20776F726C642037", 1n], + ["48656C6C6F20776F726C642038", 1n], + ["48656C6C6F20776F726C642039", 1n], + ["48656C6C6F20776F726C642040", 1n], + ["48656C6C6F20776F726C642041", 1n], + ["48656C6C6F20776F726C642042", 1n], + ["48656C6C6F20776F726C642043", 1n], + ["48656C6C6F20776F726C642044", 1n], + ["48656C6C6F20776F726C642045", 1n], + ["48656C6C6F20776F726C642046", 1n], + ["48656C6C6F20776F726C642047", 1n], + ["48656C6C6F20776F726C642048", 1n], + ["48656C6C6F20776F726C642049", 1n], + ["48656C6C6F20776F726C642040", 1n], + ["48656C6C6F20776F726C642051", 1n], + ["48656C6C6F20776F726C642052", 1n], + ["48656C6C6F20776F726C642053", 1n], + ["48656C6C6F20776F726C642054", 1n], + ["48656C6C6F20776F726C642055", 1n], + ["48656C6C6F20776F726C642056", 1n], + ["48656C6C6F20776F726C642057", 1n], + ["48656C6C6F20776F726C642058", 1n], + ["48656C6C6F20776F726C642059", 1n], + ["48656C6C6F20776F726C642050", 1n], + ["48656C6C6F20776F726C642061", 1n], + ["48656C6C6F20776F726C642062", 1n], + ["48656C6C6F20776F726C642063", 1n], + ["48656C6C6F20776F726C642064", 1n], + ["48656C6C6F20776F726C642065", 1n], + ["48656C6C6F20776F726C642066", 1n], + ["48656C6C6F20776F726C642067", 1n], + ["48656C6C6F20776F726C642068", 1n], + ["48656C6C6F20776F726C642069", 1n], + ["48656C6C6F20776F726C642060", 1n], + ] + ]])) + ) + ); + + const changeAddress = Address.fromBech32('addr_test1vrk907u2q3tnakfwvwmdl89jhlzy7tfqaqxwzwsch3afw0qqarpt4'); + + let tx = await new Tx() + .addInput(input) + .finalize(networkParams, changeAddress, [inputClean]) + + console.log(tx.body.outputs.length); + let assetsInOutput = tx.body.outputs.map((o) => o.value.assets.getTokenNames(MintingPolicyHash.fromHex('5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7594')).length) + console.log( assetsInOutput.length === 2, assetsInOutput[0] == 39 ); + + config.set({ + MAX_ASSETS_PER_CHANGE_OUTPUT: 5 + }); + + tx = await new Tx() + .addInput(input) + .finalize(networkParams, changeAddress, [inputClean]); + + console.log(tx.body.outputs.length); + let assetsInOutput2 = tx.body.outputs.map((o) => o.value.assets.getTokenNames(MintingPolicyHash.fromHex('5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7594'))) + let assetsInOutput3 = tx.body.outputs.map((o) => o.value.assets.getTokenNames(MintingPolicyHash.fromHex('5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7595'))) + console.log( assetsInOutput2.map((o) => o.map(a => a.hex) )); + console.log( assetsInOutput3.map((o) => o.map(a => a.hex) )); + + config.set({ + MAX_ASSETS_PER_CHANGE_OUTPUT: undefined + }); +} + + +async function testUTxOsTooFragmented() { + + const input1 = new TxInput( + new TxOutputId("a66564e90416a3c3ed89350108799ab122bdbfd098624d0f43f955207ace8eda#1"), + new TxOutput( + new Address("addr_test1wpcwnce7k66ldmduhkqdrgamxmnytekhr2hyp8ncsdcg0aqufrga4"), + new Value(52000000n, new Assets([["5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7500", [["48656C6C6F20776F726C642031", 1n]]]])) + )); + + const input = new TxInput( + new TxOutputId("fed1bb855c77efd1fa209a1b35c447b13d4b09671f7d682263b9f3af1089f58c#1"), + new TxOutput( + new Address("addr_test1qruk42fdnsvvyuha6z23dagxq5966h68ta8d42cdsa6e05muqmq4j86269r4ckhjsvmapapl24fazrtl22yg9sn9pvfsz4vr2h"), + new Value(24172320n, new Assets([[ + "5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7594", [ + ["48656C6C6F20776F726C642031", 1n], + ["48656C6C6F20776F726C642032", 1n], + ] + ]])) + ) + ); + + const changeAddress = Address.fromBech32('addr_test1vrk907u2q3tnakfwvwmdl89jhlzy7tfqaqxwzwsch3afw0qqarpt4'); + const receiveAddress = Address.fromBech32('addr_test1vr8xz7jf77ve6qdjategas0wj2e4wx7szlqgzhy4k3esgng649jwl'); + + let tx = await new Tx() + .addInput(input) + .addOutput(new TxOutput(receiveAddress, new Value(10000000n, + new Assets([[ + "5e2f416b455dc4e5f9a7c7e58919e9a12a1db15e14ed08f8776b7594", [ + ["48656C6C6F20776F726C642031", 1n] + ] + ]])))) + .finalize(networkParams, changeAddress, [input1]); + + console.log(JSON.stringify(tx.dump())); + console.log('8113e38071edbfe02eda73a7fe7b32a1e4ffbe957b1ac0ec1caa05d121a329ea' === bytesToHex(tx.bodyHash)); +} export default async function main() { await assetsCompare(); @@ -743,4 +914,6 @@ export default async function main() { await sortInputs(); await testAssetSplitOnChangeOutput(); + + await testUTxOsTooFragmented(); } \ No newline at end of file From 0f6a442aea45ad0a0cba3733e9cf51fefd828a36 Mon Sep 17 00:00:00 2001 From: Gavin Harris Date: Wed, 27 Sep 2023 11:51:48 +1000 Subject: [PATCH 5/5] Sort the TxInput by asset count before we try adding it. --- helios.js | 2 +- src/tx-builder.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helios.js b/helios.js index 91d121ae..d9cf04b1 100644 --- a/helios.js +++ b/helios.js @@ -45000,7 +45000,7 @@ export class Tx extends CborData { if (spare === undefined) { if (spareAssetUTxOs.length > 0) { - spare = spareAssetUTxOs.pop(); + spare = spareAssetUTxOs.sort((a, b) => a.output.value.assets.nTokenTypes - b.output.value.assets.nTokenTypes ).pop(); // Should sort so that we get the UTxO with the 'least' number of Assets if (!spare){ throw new Error(`UTxOs too fragmented - or no Spare UTxOs available to fix this mess`); diff --git a/src/tx-builder.js b/src/tx-builder.js index 5887637c..1d808a59 100644 --- a/src/tx-builder.js +++ b/src/tx-builder.js @@ -1057,7 +1057,7 @@ export class Tx extends CborData { if (spare === undefined) { if (spareAssetUTxOs.length > 0) { - spare = spareAssetUTxOs.pop(); + spare = spareAssetUTxOs.sort((a, b) => a.output.value.assets.nTokenTypes - b.output.value.assets.nTokenTypes ).pop(); // Should sort so that we get the UTxO with the 'least' number of Assets if (!spare){ throw new Error(`UTxOs too fragmented - or no Spare UTxOs available to fix this mess`);