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

feat: Add support for maximum assets per change output #103

Merged
merged 3 commits into from
Oct 6, 2023
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
2 changes: 2 additions & 0 deletions helios-internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 | undefined;
}): void;
const DEBUG: boolean;
const STRICT_BABBAGE: boolean;
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions helios.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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 | undefined;
}): void;
/**
* Global debug flag. Currently unused.
Expand Down Expand Up @@ -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
Expand Down
37 changes: 35 additions & 2 deletions helios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
}


Expand Down Expand Up @@ -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);
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
}
29 changes: 27 additions & 2 deletions src/tx-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
92 changes: 91 additions & 1 deletion test/tx-building.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -653,4 +741,6 @@ export default async function main() {
await testEmulatorPrivateKeyGen();

await sortInputs();

await testAssetSplitOnChangeOutput();
}