Skip to content

w3sper: Transaction building & confirmation: surprising nonce semantics, misleading fee field, session-locked confirmation #4072

Description

@ichbindas

Summary

Three transaction-lifecycle issues that each cost us real debugging time because the SDK's behavior didn't match what its naming/shape implied.

1. .nonce(N) silently becomes N + 1 on-chain

Problem: .nonce(N) on a transaction builder sets a base nonce; .build() adds 1 internally. To land a transaction with an intended on-chain nonce of N, callers must call .nonce(N - 1n). Nothing about the method name suggests this offset.

Example:

// intent: land with on-chain nonce N
tx.nonce(N);       // actually lands as N + 1
tx.nonce(N - 1n);  // this is what actually lands as N

We built transactions expecting .nonce(N) to mean on-chain nonce N; they landed at N + 1, only caught via cross-checking against externally-tracked expected nonces.

Current workaround / suggested fix: We call .nonce(N - 1n) everywhere we want on-chain nonce N, with a regression test pinned to this exact offset so an SDK update (or a future "fix") doesn't silently reintroduce the mismatch. Ideally .nonce(N) would mean "the nonce that ends up on-chain" (absorbing the +1 internally), or at minimum the docstring/type would state explicitly that the input is a base value and the actual on-chain nonce is input + 1.

2. gasPaid on the confirmation event is a fee, not a unit count

Problem: gasPaid on the live once.executed() event is already the lux fee paid (gas price × units consumed), not a raw unit count — despite reading, next to fields like gasLimit, as if it should be unit-denominated.

Example:

// assumed gasPaid was a unit count, like gasLimit:
const fee = event.gasPaid * gasPrice;  // doubles the actual fee — gasPaid is already the fee

Current workaround / suggested fix: We stopped multiplying and treat gasPaid as the final lux fee directly, with a comment at the call site recording what it actually represents. Renaming the field (e.g. feePaid/luxPaid, reserving gasPaid/gasUsed for a unit count) — or documenting its unit explicitly in the event schema — would prevent this being re-introduced by the next person who reads the name at face value.

3. execute() confirmation is locked to the node it was broadcast to

Problem: Broadcasting (execute()) and confirming (once.executed()) appear coupled to the same RUES session/node. If that node goes unresponsive right after accepting the broadcast, there's no supported way to confirm via a different, healthy connection — undermining redundancy for apps that maintain multiple node connections specifically for that purpose.

Example: We route submission through a pool of connections to route around flaky per-node TCP connect behavior. When the node used for one execute() call degraded right after accepting the broadcast, confirmation had no path through any of our other healthy connections.

Current workaround / suggested fix: We track which node a transaction was broadcast to, and if that connection degrades post-broadcast, fall back to polling transaction status via general chain-state queries on a different connection instead of the once.executed() subscription. If confirmation events are inherently sourced from the originating node's session, exposing either (a) a way to attach a confirmation subscription for a given tx ID from a different Network instance, or (b) a first-class polling-based "confirm by tx ID against any node" API, would remove the need for consumers to build this fallback against internal chain-state APIs themselves.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions