From 7d7eb1052b07f9d1b65d8b323c1507e66c073f6e Mon Sep 17 00:00:00 2001 From: Joaquin Hoyos Date: Thu, 12 Sep 2024 00:45:51 +0200 Subject: [PATCH] fix: create multiple outputs if balance exceeds maxValueSize --- .changeset/thirty-years-cough.md | 5 ++ packages/blaze-tx/src/tx.ts | 50 ++++++++++++++++++ packages/blaze-tx/test/tx.test.ts | 87 +++++++++++++++++++++++++++++-- 3 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 .changeset/thirty-years-cough.md diff --git a/.changeset/thirty-years-cough.md b/.changeset/thirty-years-cough.md new file mode 100644 index 00000000..eda649e3 --- /dev/null +++ b/.changeset/thirty-years-cough.md @@ -0,0 +1,5 @@ +--- +"@blaze-cardano/tx": patch +--- + +Add balanceMultiAssetChange to avoid exceeding the max value size for an output diff --git a/packages/blaze-tx/src/tx.ts b/packages/blaze-tx/src/tx.ts index 9f0e87a0..54b48ac9 100644 --- a/packages/blaze-tx/src/tx.ts +++ b/packages/blaze-tx/src/tx.ts @@ -1320,6 +1320,54 @@ export class TxBuilder { this.body.setCollateralReturn(ret); } + /** + * Adjusts the balance of the transaction by creating or updating a change output. + * This method takes only the native assets from excess value from the transaction, removes any zero-valued + * tokens from the multiasset map, and then creates change outputs that don't exceed the minValueSize. + * + * Updates the changeOutputIndex to the index of the last change output. + * + * @param {Value} excessValue - The excess value that needs to be returned as change. + * returns {Value} The remaining excess value after creating change outputs. (Which should only be ADA) + */ + private balanceMultiAssetChange(excessValue: Value): Value { + const tokenMap = excessValue.multiasset(); + if (tokenMap) { + for (const key of tokenMap.keys()) { + if (tokenMap.get(key) == 0n) { + tokenMap.delete(key); + } + } + excessValue.setMultiasset(tokenMap); + } + let changeExcess = excessValue; + const multiAsset = excessValue.multiasset(); + if (!multiAsset || multiAsset.size == 0) return excessValue; + let output = new TransactionOutput(this.changeAddress!, value.zero()); + for (const [asset, qty] of Array.from(multiAsset.entries())) { + const newOutputValue = value.merge( + output.amount(), + value.makeValue(0n, [asset, qty]), + ); + const newOutputValueByteLength = newOutputValue.toCbor().length / 2; + //We need to check if the new output value is too large + //We leave a small buffer for the change ADA. Also we don't need such a big output so 10% is fine + if (newOutputValueByteLength > this.params.maxValueSize * 0.9) { + this.addOutput(output); + changeExcess = value.sub(changeExcess, output.amount()); + output = new TransactionOutput( + this.changeAddress!, + value.makeValue(0n, [asset, qty]), + ); + } else { + output = new TransactionOutput(this.changeAddress!, newOutputValue); + } + } + this.addOutput(output); + changeExcess = value.sub(changeExcess, output.amount()); + return changeExcess; + } + /** * Balances the collateral change by creating a transaction output that returns the excess collateral. * Throws an error if the change address is not set. @@ -1457,6 +1505,8 @@ export class TxBuilder { ); } + // We first balance the native assets to avoid issues with the max value size being exceeded + excessValue = this.balanceMultiAssetChange(excessValue); // Balance the change output with the updated excess value. this.balanceChange(excessValue); // Ensure a change output index has been set after balancing. diff --git a/packages/blaze-tx/test/tx.test.ts b/packages/blaze-tx/test/tx.test.ts index c68d16ba..12901dfe 100644 --- a/packages/blaze-tx/test/tx.test.ts +++ b/packages/blaze-tx/test/tx.test.ts @@ -22,12 +22,17 @@ function flatten(iterator: IterableIterator | undefined): U[] { return result; } -const ASSET_NAME_1 = "ab".repeat(56 / 2); -const ASSET_NAME_2 = "cd".repeat(56 / 2); -// const ASSET_NAME_3 = "ef".repeat(56/2) +const ASSETS = Array.from({ length: 1200 }, (_, i) => + i + .toString(16) + .padStart(2, "0") + .concat("ef".repeat(56 / 2)), +); describe("Transaction Building", () => { it("A complex transaction should balance correctly", async () => { + const ASSET_NAME_1 = ASSETS[0]!; + const ASSET_NAME_2 = ASSETS[1]!; // $hosky const testAddress = Address.fromBech32( "addr1q86ylp637q7hv7a9r387nz8d9zdhem2v06pjyg75fvcmen3rg8t4q3f80r56p93xqzhcup0w7e5heq7lnayjzqau3dfs7yrls5", @@ -88,4 +93,80 @@ describe("Transaction Building", () => { // console.dir(tx.toCore(), {depth: null}) expect(inputValue.toCbor()).toEqual(outputValue.toCbor()); }); + it("Should correctly balance change for a really big output change", async () => { + // $hosky + const testAddress = Address.fromBech32( + "addr1q86ylp637q7hv7a9r387nz8d9zdhem2v06pjyg75fvcmen3rg8t4q3f80r56p93xqzhcup0w7e5heq7lnayjzqau3dfs7yrls5", + ); + const utxo1Assets: [string, bigint][] = ASSETS.slice( + 0, + ASSETS.length / 2, + ).map((x) => [x, 1n]); + const utxo2Assets: [string, bigint][] = ASSETS.slice(ASSETS.length / 2).map( + (x) => [x, 1n], + ); + const utxos = [ + new TransactionUnspentOutput( + new TransactionInput(TransactionId("0".repeat(64)), 0n), + new TransactionOutput( + testAddress, + value.makeValue(10_000_000_000n, ...utxo1Assets), + ), + ), + new TransactionUnspentOutput( + new TransactionInput(TransactionId("1".padStart(64, "0")), 0n), + new TransactionOutput( + testAddress, + value.makeValue(10_000_000_000n, ...utxo2Assets), + ), + ), + ]; + const tx = await new TxBuilder(hardCodedProtocolParams) + .addUnspentOutputs(utxos) + .setNetworkId(NetworkId.Testnet) + .setChangeAddress(testAddress) + .payAssets( + testAddress, + value.makeValue(10_001_000_000n, [ASSETS[0]!, 1n]), + ) + .complete(); + + const inputValue = + // value.merge( + tx + .body() + .inputs() + .values() + .map((x) => + utxos + .find((y) => { + return y.input().toCbor() == x.toCbor(); + })! + .output() + .amount(), + ) + .reduce(value.merge, value.zero()); + // ,new Value( + // flatten(tx.body().withdrawals()?.values()).reduce((x, y) => x + y, 0n), + // ), + // ) + // + + const outputValue = value.merge( + Array.from(tx.body().outputs().values()) + .map((x) => x.amount()) + .reduce(value.merge, value.zero()), + new Value(tx.body().fee()), + ); + + console.log("Change: ", tx.body().outputs().at(1)?.amount().coin()); + + console.dir(inputValue.toCore(), { depth: null }); + console.dir(outputValue.toCore(), { depth: null }); + + expect(inputValue.multiasset()?.size).toEqual( + outputValue.multiasset()?.size, + ); + expect(inputValue.toCbor()).toEqual(outputValue.toCbor()); + }); });