Skip to content

Rewrite transaction balance / fee calculation logic #241

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

Merged
merged 54 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c033c5e
chore: adds hvf coin selection and test coverage
cjkoepke Jan 9, 2025
4b089c5
chore: fmt
cjkoepke Jan 9, 2025
031dad6
chore: expose coin selectors
cjkoepke Jan 9, 2025
d658d46
wip: test case
cjkoepke Jan 10, 2025
e376078
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Jan 21, 2025
e495f26
fix: fmt
cjkoepke Jan 21, 2025
2829122
chore: fix test case
cjkoepke Jan 21, 2025
5c81060
chore: changeset
cjkoepke Jan 21, 2025
b44dc08
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Jan 21, 2025
bbb76c2
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Jan 24, 2025
593dc58
chore: more tests
cjkoepke Jan 27, 2025
ae6e780
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Jan 31, 2025
e5fedde
wip: use coin selection to determine collateral
cjkoepke Jan 31, 2025
a89f667
chore: update prepareCollateral to use coinSelector
cjkoepke Feb 5, 2025
c7fe2f9
chore: cleanup
cjkoepke Feb 5, 2025
5a138ee
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Feb 5, 2025
697c0f7
fix: test
cjkoepke Feb 5, 2025
47ddfc1
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Feb 6, 2025
29ccb85
chore: cleanup
cjkoepke Feb 6, 2025
50c7652
chore: review comments
cjkoepke Feb 6, 2025
024e93d
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Feb 6, 2025
f2fffe4
wip: reconfigure tx completion
cjkoepke Feb 7, 2025
a1e7402
wip: more progress on transaction balancing, etc
cjkoepke Feb 10, 2025
d436a89
chore: enable tracing
cjkoepke Feb 10, 2025
8a0af38
chore: fix prepareCollateral tests
cjkoepke Feb 11, 2025
017243d
chore: passing tests
cjkoepke Feb 14, 2025
cf980b2
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Feb 18, 2025
99e771b
chore: add donation support
cjkoepke Feb 18, 2025
bdf5d56
chore: comment for fee calculation
cjkoepke Feb 19, 2025
1c3e8cc
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Feb 20, 2025
f608146
ci/cd: snapshot releases
micahkendall Feb 21, 2025
9a75940
fix: use release command
micahkendall Feb 21, 2025
bb2c8de
correct token
micahkendall Feb 21, 2025
3fda65a
simple snapshot names
micahkendall Feb 21, 2025
bce8739
dev release command with tag
micahkendall Feb 21, 2025
2d957ee
--no-git-tag
micahkendall Feb 21, 2025
cdfd54b
name snapshots
micahkendall Feb 21, 2025
900d968
Merge branch 'feat/collateral' of github.com:butaneprotocol/blaze-car…
cjkoepke Feb 24, 2025
4af073a
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Feb 24, 2025
53f2360
Merge branch 'main' of github.com:butaneprotocol/blaze-cardano into f…
cjkoepke Feb 28, 2025
ce41d40
chore: abstract some utilities out of TxBuilder
cjkoepke Feb 28, 2025
1ad9ccd
chore: just a bit more cleanup
cjkoepke Feb 28, 2025
71448c5
chore: abstract out required witnesses logic
cjkoepke Mar 4, 2025
d110e01
fmt
cjkoepke Mar 5, 2025
2a3bb74
fix: tests
cjkoepke Mar 12, 2025
d1e7fcd
chore: remove console log
cjkoepke Mar 12, 2025
0fa0748
chore: review comments
cjkoepke Mar 12, 2025
47f03b8
chore: fmt
cjkoepke Mar 12, 2025
15a04e2
chore: throw error instead of early return
cjkoepke Mar 12, 2025
f2d95e0
chore: throw error instead of early return
cjkoepke Mar 12, 2025
5994313
chore: revert committed blueprint change
cjkoepke Mar 12, 2025
de6d86b
chore: review comments
cjkoepke Mar 12, 2025
1a1f633
chore: fmt
cjkoepke Mar 12, 2025
4e44cf0
chore: review comments and changeset
cjkoepke Mar 12, 2025
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/smart-avocados-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blaze-cardano/tx": patch
---

Adds a new Coin Selection algorithm as an export (while keeping the default selection algo intact for now).
4 changes: 3 additions & 1 deletion packages/blaze-tx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"dev": "tsup src/index.ts --format esm,cjs --watch --dts --external rxjs",
"lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"test": "jest"
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"@blaze-cardano/eslint-config": "workspace:*",
Expand Down
203 changes: 203 additions & 0 deletions packages/blaze-tx/src/coinSelectors/hvfSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import {
type Address,
AssetId,
hardCodedProtocolParams,
type TokenMap,
TransactionOutput,
type TransactionUnspentOutput,
Value,
} from "@blaze-cardano/core";
import * as value from "../value";
import type { CoinSelectionFunc, SelectionResult } from "../types";
import { calculateMinAda, isEqualUTxO, stringifyBigint } from "../utils";

/**
* Returns a list of UTxOs whose total assets are equal to or greater than the asset value provided
* @link https://github.com/Anastasia-Labs/lucid-evolution/blob/main/packages/utils/src/utxo.ts#L112
* @param utxos list of available utxos
* @param assetsRequired minimum total assets required
* @param includeUTxOsWithScriptRef Whether to include UTxOs with scriptRef or not. default = false
*/
export const selectUTxOs = (
utxos: TransactionUnspentOutput[],
totalRequired: Value,
includeUTxOsWithScriptRef: boolean = false,
): TransactionUnspentOutput[] => {
const selectedUtxos: TransactionUnspentOutput[] = [];
let isSelected = false;
isSelected = false;
const assetsRequired = new Map<string, bigint>([
...Object.entries({
lovelace: totalRequired.coin(),
}),
...(totalRequired.multiasset()?.entries() || []),
]);

for (const utxo of utxos) {
if (!includeUTxOsWithScriptRef && utxo.output().scriptRef()) continue;
isSelected = false;
for (const [unit, amount] of assetsRequired) {
const assetAmount =
unit === "lovelace"
? utxo.output().amount().coin()
: utxo.output().amount().multiasset()?.get(AssetId(unit));
if (assetAmount) {
if (assetAmount >= amount) {
assetsRequired.delete(unit);
} else {
assetsRequired.set(unit, amount - assetAmount);
}
isSelected = true;
}
}
if (isSelected) {
selectedUtxos.push(utxo);
}
if (assetsRequired.size == 0) {
break;
}
}
if (assetsRequired.size > 0) return [];
return selectedUtxos;
};

const calculateExtraLovelace = (
leftoverAssets: Value,
address: Address,
coinsPerUtxoByte: number,
): Value => {
const output = new TransactionOutput(address, leftoverAssets);
const minLovelace = calculateMinAda(output, coinsPerUtxoByte);
const currentLovelace = leftoverAssets.coin();
return currentLovelace >= minLovelace
? new Value(0n)
: new Value(minLovelace - currentLovelace);
};

/**
* Performs coin selection to obtain the "requiredAssets" and then carries out
* recursive coin selection to ensure that leftover assets (selectedAssets + externalAssets - requiredAssets)
* have enough ADA to satisfy minimum ADA requirement for them to be sent as change output.
* If "requiredAssets" is empty, it still checks for minimum ADA requirement of "externalAssets"
* and does coin selection if required.
* @param inputs
* @param requiredAssets
* @param coinsPerUtxoByte
* @param externalAssets
* @param error
* @returns
*/
export const recursive = (
inputs: TransactionUnspentOutput[],
requiredAssets: Value,
externalAssets: Value = new Value(0n),
coinsPerUtxoByte: number = hardCodedProtocolParams.coinsPerUtxoByte,
): SelectionResult => {
const firstInput = inputs[0];
let selected: TransactionUnspentOutput[] = selectUTxOs(
inputs,
requiredAssets,
);
if (!firstInput || selected.length === 0) {
throw new Error(
`Your wallet does not have enough funds to cover the required assets: ${stringifyBigint(requiredAssets.toCore())}. Or it contains UTxOs with reference scripts; which are excluded from coin selection.`,
);
}

const selectedAssets: Value = value.sum(
selected.map((s) => s.output().amount()),
);

let availableAssets = value.sub(selectedAssets, requiredAssets);
availableAssets = value.sum([availableAssets, externalAssets]);

let extraLovelace: Value | undefined = calculateExtraLovelace(
availableAssets,
firstInput.output().address(),
coinsPerUtxoByte,
);

let remainingInputs = inputs;

while (extraLovelace.coin() > 0n) {
remainingInputs = remainingInputs.filter(
(i, index) => !selected[index] || !isEqualUTxO(i, selected[index]),
);

const extraSelected = selectUTxOs(remainingInputs, extraLovelace);
if (extraSelected.length === 0) {
throw new Error(
`Your wallet does not have enough funds to cover required minimum ADA for change output: ${stringifyBigint(extraLovelace.toCore())}. Or it contains UTxOs with reference scripts; which are excluded from coin selection.`,
);
}

const extraSelectedAssets: Value = value.sum(
extraSelected.map((v) => v.output().amount()),
);
selected = [...selected, ...extraSelected];
availableAssets = value.sum([availableAssets, extraSelectedAssets]);

extraLovelace = calculateExtraLovelace(
availableAssets,
firstInput.output().address(),
coinsPerUtxoByte,
);
}

const leftoverInputs = inputs.filter((i) => !selected.includes(i));

return {
selectedInputs: selected,
leftoverInputs,
selectedValue: selectedAssets,
};
};

/**
* This coin selection algorithm follows a Highest Value First (HVF) function, taking into consideration things like fee estimation.
* Inspiration taken from Lucid Evolution: https://github.com/Anastasia-Labs/lucid-evolution/blob/main/packages/lucid/src/tx-builder/internal/CompleteTxBuilder.ts#L789
* @param inputs
* @param collectedAssets
* @param preliminaryFee
* @param externalAssets
* @param coinsPerUtxoByte
* @returns
*/
export const hvfSelector: CoinSelectionFunc = (
inputs,
collectedAssets,
preliminaryFee = 0,
externalAssets = new Value(0n),
coinsPerUtxoByte = hardCodedProtocolParams.coinsPerUtxoByte,
) => {
const requiredAssets = new Value(
collectedAssets.coin() + externalAssets.coin() + BigInt(preliminaryFee),
);
const nonRequiredAssets = new Value(0n);

externalAssets.multiasset()?.forEach((amount, id) => {
const requiredMultiAsset = requiredAssets.multiasset()?.get(id);
const nonRequiredMultiAsset = nonRequiredAssets.multiasset()?.get(id);
if (amount < 0n) {
if (nonRequiredMultiAsset) {
(nonRequiredAssets.multiasset() as TokenMap).set(
id,
nonRequiredMultiAsset + amount,
);
} else {
nonRequiredAssets.setMultiasset(new Map([[AssetId(id), amount]]));
}
} else {
if (requiredMultiAsset) {
(requiredAssets.multiasset() as TokenMap).set(
id,
requiredMultiAsset + amount,
);
} else {
requiredAssets.setMultiasset(new Map([[AssetId(id), amount]]));
}
}
});

return recursive(inputs, requiredAssets, nonRequiredAssets, coinsPerUtxoByte);
};
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import type { TransactionUnspentOutput } from "@blaze-cardano/core";
import { Value, UTxOSelectionError } from "@blaze-cardano/core";
import * as value from "./value";

/**
* The result of a coin selection operation.
* It includes the selected inputs, the total value of the selected inputs, and the remaining inputs.
*/
export type SelectionResult = {
selectedInputs: TransactionUnspentOutput[];
selectedValue: Value;
inputs: TransactionUnspentOutput[];
};
import * as value from "../value";
import type { CoinSelectionFunc, SelectionResult } from "../types";

/**
* The wide selection algorithm is a multiasset coin selector that doesn't care about magnitude.
Expand Down Expand Up @@ -59,7 +50,11 @@ function wideSelection(
acc = value.merge(acc, bestStep[1]);
}
}
return { selectedInputs, selectedValue: acc, inputs: availableInputs };
return {
selectedInputs,
selectedValue: acc,
leftoverInputs: availableInputs,
};
}

/**
Expand Down Expand Up @@ -110,7 +105,11 @@ function deepSelection(
acc = value.merge(acc, bestStep[1]);
}
}
return { selectedInputs, selectedValue: acc, inputs: availableInputs };
return {
selectedInputs,
selectedValue: acc,
leftoverInputs: availableInputs,
};
}

/**
Expand All @@ -120,15 +119,15 @@ function deepSelection(
* @param {Value} dearth - The value to be covered by the selected inputs.
* @returns {SelectionResult} The result of the coin selection operation.
*/
export function micahsSelector(
inputs: TransactionUnspentOutput[],
dearth: Value,
): SelectionResult {
export const micahsSelector: CoinSelectionFunc = (
inputs,
dearth,
): SelectionResult => {
const wideResult = wideSelection(inputs, dearth);
const remainingDearth = value.positives(
value.sub(dearth, wideResult.selectedValue),
);
const deepResult = deepSelection(wideResult.inputs, remainingDearth);
const deepResult = deepSelection(wideResult.leftoverInputs, remainingDearth);
const finalDearth = value.positives(
value.sub(remainingDearth, deepResult.selectedValue),
);
Expand All @@ -145,6 +144,6 @@ export function micahsSelector(
wideResult.selectedValue,
deepResult.selectedValue,
),
inputs: deepResult.inputs,
leftoverInputs: deepResult.leftoverInputs,
};
}
};
1 change: 1 addition & 0 deletions packages/blaze-tx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * as Value from "./value";
export { makeValue } from "./value";
export * from "./data";
export * from "./utils";
export * as CoinSelector from "./coinSelectors/hvfSelector";
Loading
Loading