Skip to content
Open
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
31 changes: 20 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions packages/mesh-common/src/interfaces/balancer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Protocol } from "../types";
import { UTxO } from "../types/utxo";

/**
* Settles the numbers of a built transaction that depend on each other: the execution units of
* every redeemer, the fee, and the lovelace of the change output.
*
* `MeshTxBuilder` evaluates scripts before it adds the change output, so the units it declares can
* be wrong for the transaction it finally builds: a script sees the whole transaction, and an extra
* output can change what it costs, which changes the fee, which changes the change. A balancer runs
* that as a loop until the three agree.
*
* Coin selection and change placement stay with the builder. An implementation is given the output
* that should absorb the difference and may edit only its lovelace, the fee, and the redeemers.
*/
export interface ITxBalancer {
/**
* @param tx - the serialized transaction, as CBOR hex
* @param utxos - every UTxO the transaction's inputs, collateral and reference inputs name
* @param params - the protocol parameters the transaction is built against
* @param changeOutputIndex - which output absorbs the difference, counting from 0
* @returns the balanced transaction as CBOR hex
*/
balanceTx(
tx: string,
utxos: UTxO[],
params: Protocol,
changeOutputIndex: number,
): Promise<string>;
}
1 change: 1 addition & 0 deletions packages/mesh-common/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from "./submitter";
export * from "./serializer";
export * from "./signer";
export * from "./evaluator";
export * from "./balancer";
export * from "./wallet";
6 changes: 5 additions & 1 deletion packages/mesh-core-cst/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { Config } from "jest";

const jestConfig: Config = {
clearMocks: true,
testPathIgnorePatterns: ["/offline-evaluator-scalus.test.ts$"],
testPathIgnorePatterns: [
"/offline-evaluator-scalus.test.ts$",
"/balance-tx-scalus.test.ts$",
"/scalus-tx-balancer.test.ts$",
],
maxWorkers: 1,
testEnvironment: "node",
testMatch: ["**/packages/**/*.test.ts"],
Expand Down
2 changes: 1 addition & 1 deletion packages/mesh-core-cst/jest.scalus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import base from "./jest.config";
export default {
...base,
testPathIgnorePatterns: [],
testMatch: ["**/offline-evaluator-scalus.test.ts"],
testMatch: ["**/offline-evaluator-scalus.test.ts", "**/balance-tx-scalus.test.ts", "**/scalus-tx-balancer.test.ts"],
preset: "ts-jest/presets/default-esm",
extensionsToTreatAsEsm: [".ts"],
transform: { "^.+\\.[jt]s?$": ["ts-jest", { useESM: true }] },
Expand Down
2 changes: 1 addition & 1 deletion packages/mesh-core-cst/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"blakejs": "^1.2.1",
"bn.js": "^5.2.0",
"hash.js": "^1.1.7",
"scalus": "^1.2.1"
"scalus": "^1.3.0"
},
"overrides": {
"@cardano-sdk/crypto": {
Expand Down
101 changes: 101 additions & 0 deletions packages/mesh-core-cst/src/offline-providers/balance-tx-scalus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
DEFAULT_V1_COST_MODEL_LIST,
DEFAULT_V2_COST_MODEL_LIST,
DEFAULT_V3_COST_MODEL_LIST,
Network,
Protocol,
SLOT_CONFIG_NETWORK,
UTxO,
} from "@meshsdk/common";

import { toTxUnspentOutput } from "../utils";

/**
* Balances a transaction with Scalus: sets every redeemer's execution units, the fee, and the
* lovelace of one change output, until the three agree.
*
* Mesh evaluates scripts before it adds change outputs, so the units it declares can be wrong for
* the transaction it finally builds: a script sees the whole transaction, and an extra output can
* change what it costs. This runs the loop instead. Coin selection and change placement stay with
* `MeshTxBuilder`; only the numbers move, in the output you name.
*
* @param tx - the built transaction, as CBOR hex
* @param utxos - every UTxO the transaction's inputs, collateral and reference inputs name
* @param params - the protocol parameters, as `fetchProtocolParameters` returns them
* @param network - which network, for slot arithmetic
* @param changeOutputIndex - which output absorbs the difference, counting from 0
* @param protocolMajorVersion - the ledger protocol version to cost against
* @param costModels - cost models by Plutus version, defaulting to the mainnet ones
* @param extraSigners - hex key hashes for signatures the transaction does not name, such as the
* keys a native script requires. Inferred signers are always included as well
* @returns the balanced transaction as CBOR hex, ready to sign
*/
export const balanceTxWithScalus = async (
tx: string,
utxos: UTxO[],
params: Protocol,
network: Network,
changeOutputIndex: number,
protocolMajorVersion: number,
costModels: number[][] = [
DEFAULT_V1_COST_MODEL_LIST,
DEFAULT_V2_COST_MODEL_LIST,
DEFAULT_V3_COST_MODEL_LIST,
],
extraSigners: string[] = [],
): Promise<string> => {
// Keep native import() in the CJS build: Scalus is ESM-only.
const { balancer } = await import("scalus");

// CIP-30 `transaction_unspent_output` is `[input, output]`, which is what Scalus takes.
const pairs = utxos.map((utxo) => toTxUnspentOutput(utxo).toCbor().toString());

const slots = SLOT_CONFIG_NETWORK[network];

const balanced = balancer.balanceTx(
tx,
pairs,
{
zeroTime: slots.zeroTime,
zeroSlot: slots.zeroSlot,
slotLength: slots.slotLength,
},
scalusParams(params, protocolMajorVersion, costModels),
changeOutputIndex,
extraSigners,
);
return Buffer.from(balanced).toString("hex");
};

/**
* Mesh's `Protocol` as the record Scalus reads. Every number is one Mesh already holds, so there is
* no JSON round trip: the deposits Scalus does not consult while balancing stay at zero.
*/
const scalusParams = (
p: Protocol,
protocolMajorVersion: number,
costModels: number[][],
) => ({
txFeePerByte: p.minFeeA,
txFeeFixed: p.minFeeB,
maxTxSize: p.maxTxSize,
maxValueSize: p.maxValSize,
stakeAddressDeposit: p.keyDeposit,
stakePoolDeposit: p.poolDeposit,
dRepDeposit: 0,
govActionDeposit: 0,
utxoCostPerByte: p.coinsPerUtxoSize,
priceMemory: p.priceMem,
priceSteps: p.priceStep,
maxTxExecutionMemory: Number(p.maxTxExMem),
maxTxExecutionSteps: Number(p.maxTxExSteps),
collateralPercentage: p.collateralPercent,
maxCollateralInputs: p.maxCollateralInputs,
minFeeRefScriptCostPerByte: p.minFeeRefScriptCostPerByte,
protocolMajorVersion,
costModels: {
PlutusV1: costModels[0],
PlutusV2: costModels[1],
PlutusV3: costModels[2],
},
});
2 changes: 2 additions & 0 deletions packages/mesh-core-cst/src/offline-providers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "./offline-evaluator-scalus";
export * from "./balance-tx-scalus";
export * from "./scalus-tx-balancer";
62 changes: 62 additions & 0 deletions packages/mesh-core-cst/src/offline-providers/scalus-tx-balancer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
DEFAULT_V1_COST_MODEL_LIST,
DEFAULT_V2_COST_MODEL_LIST,
DEFAULT_V3_COST_MODEL_LIST,
ITxBalancer,
Network,
Protocol,
UTxO,
} from "@meshsdk/common";

import { balanceTxWithScalus } from "./balance-tx-scalus";

/**
* An {@link ITxBalancer} backed by Scalus, for `new MeshTxBuilder({ balancer })`.
*
* With one configured, `complete()` no longer hands back a transaction whose execution units were
* computed before the change output existed. The builder still chooses the inputs and places the
* change; this settles the fee, the units and that output's lovelace against each other.
*
* ```ts
* const balancer = new ScalusTxBalancer("preprod");
* const txHex = await new MeshTxBuilder({ fetcher, evaluator, balancer })
* .txOut(bob, [{ unit: "lovelace", quantity: "25000000" }])
* .changeAddress(alice)
* .selectUtxosFrom(utxos)
* .complete();
* ```
*/
export class ScalusTxBalancer implements ITxBalancer {
constructor(
private readonly network: Network,
/** The ledger protocol version to cost against. Mainnet runs 11 (van Rossem). */
private readonly protocolMajorVersion: number = 11,
private readonly costModels: number[][] = [
DEFAULT_V1_COST_MODEL_LIST,
DEFAULT_V2_COST_MODEL_LIST,
DEFAULT_V3_COST_MODEL_LIST,
],
/**
* Hex key hashes for signatures the transaction does not name, such as the keys a native
* script requires. Inferred signers are always included as well.
*/
private readonly extraSigners: string[] = [],
) {}

balanceTx = async (
tx: string,
utxos: UTxO[],
params: Protocol,
changeOutputIndex: number,
): Promise<string> =>
balanceTxWithScalus(
tx,
utxos,
params,
this.network,
changeOutputIndex,
this.protocolMajorVersion,
this.costModels,
this.extraSigners,
);
}
Loading
Loading