Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: create multiple outputs if balance exceeds maxValueSize #169

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
});
});