From 2e523242ae20cb932ec971590247ca0b43f2e13e Mon Sep 17 00:00:00 2001 From: Gavin Harris Date: Mon, 25 Sep 2023 20:48:04 +1000 Subject: [PATCH 1/3] 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/3] 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/3] 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) => {