Skip to content

Settle fee, execution units and change with a Scalus balancer - #851

Open
nau wants to merge 2 commits into
MeshJS:mainfrom
nau:pr2/scalus-balancer
Open

nau wants to merge 2 commits into
MeshJS:mainfrom
nau:pr2/scalus-balancer

Conversation

@nau

@nau nau commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

MeshTxBuilder evaluates scripts before coin selection adds the change output. A script sees the
whole transaction, so its execution units can be wrong for the transaction finally built, and the
fee and the change both depend on those units. This adds an opt-in balancer that settles the three
together.

  • ITxBalancer in @meshsdk/common: balanceTx(tx, utxos, params, changeOutputIndex). Coin
    selection and change placement stay with the builder. A balancer edits only the change output's
    lovelace, the fee and the redeemers.
  • ScalusTxBalancer in @meshsdk/core-cst, through Scalus's balancer.balanceTx. Keys a native
    script needs, which cannot be inferred from the inputs, go in extraSigners.
  • new MeshTxBuilder({ balancer }) runs it at the end of complete(). Without one, nothing
    changes.

Builds on #850 (Scalus 1.3.0).

Tests

@meshsdk/core-cst         179 + 8
@meshsdk/scalus-emulator  24
test:scalus-packages      ESM, CJS, .d.ts, browser
@meshsdk/transaction      same results as main

The balancer tests start from a draft with the fee set to zero, so the fee they check is one the
balancer computed. The MeshTxBuilder test checks what complete() hands the balancer: the change
output's index, and the selected UTxO with its address and value.

Node

Scalus is loaded with import(), so the CJS build works on Node 18. Checked there: the CJS build
loads and the balancer reaches Scalus.

nau added 2 commits September 29, 2026 15:36
1.3.0 adds `balancer.balanceTx`, which settles a transaction's fee, execution
units and change output against each other. Nothing in this repo uses it yet; the
bump is separate so the version change and the code that depends on it can be
reviewed apart.

Everything already here is unaffected: OfflineEvaluatorScalus and
@meshsdk/scalus-emulator use APIs 1.2.1 already had, and 1.3.0 removes nothing.

The lockfile needs regenerating once 1.3.0 is published:
npm install --package-lock-only
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. Today that is worked around by hard-coding
ExUnits or applying a multiplier.

This adds an optional balancer that runs the loop instead:

  const txHex = await new MeshTxBuilder({ fetcher, balancer: new ScalusTxBalancer("preprod") })
    .txOut(bob, [{ unit: "lovelace", quantity: "25000000" }])
    .changeAddress(alice)
    .selectUtxosFrom(utxos)
    .complete();

ITxBalancer sits in @meshsdk/common beside IEvaluator, so the mechanism is not
tied to Scalus. ScalusTxBalancer implements it. Coin selection and change
placement stay with the builder, which passes the index of the output that should
absorb the difference; an implementation may edit only that output's lovelace,
the fee and the redeemers.

Opt-in: a builder with no balancer serializes exactly as before, which a test
pins.

Mesh's own Protocol maps onto the record Scalus reads field for field, so there
is no Blockfrost JSON round trip, and UTxOs go over as CIP-30 [input, output]
pairs that toTxUnspentOutput already produces.

Needs scalus 1.3.0, whose balancer.balanceTx is marked experimental: it may change
shape in any release. Nothing else in the repo depends on it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant