Skip to content

Commit

Permalink
fix: create multiple outputs if balance exceeds maxValueSize (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
joacohoyos authored Sep 13, 2024
1 parent da29f02 commit 73e34a6
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/thirty-years-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blaze-cardano/tx": patch
---

Add balanceMultiAssetChange to avoid exceeding the max value size for an output
50 changes: 50 additions & 0 deletions packages/blaze-tx/src/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
87 changes: 84 additions & 3 deletions packages/blaze-tx/test/tx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ function flatten<U>(iterator: IterableIterator<U> | 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",
Expand Down Expand Up @@ -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());
});
});

0 comments on commit 73e34a6

Please sign in to comment.