[LWDM] fix(coin-framework): improve nonce check to avoid error at broadcast - #20289
[LWDM] fix(coin-framework): improve nonce check to avoid error at broadcast#20289qperrot wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Improves nonce/sequence handling for rapid consecutive sends and for explicit nonce intents (e.g., WalletConnect / dapps), aiming to prevent “nonce too low” failures and surface nonce issues earlier in the flow.
Changes:
- Add
nextSequenceWithPending()to compute the next nonce frommax(networkNonce, highestPendingNonce + 1). - Use the new pending-aware nonce computation during signing when the intent does not carry an explicit sequence.
- Add a pre-sign EVM intent nonce validation against the live on-chain nonce (best-effort).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| libs/ledger-live-common/src/bridge/generic-coin-framework/utils.ts | Adds nextSequenceWithPending() helper for pending-aware nonce derivation. |
| libs/ledger-live-common/src/bridge/generic-coin-framework/utils.test.ts | Adds unit tests for nextSequenceWithPending(). |
| libs/ledger-live-common/src/bridge/generic-coin-framework/signOperation.ts | Uses pending-aware nonce derivation when intent nonce isn’t explicitly set. |
| libs/coin-modules/coin-evm/src/logic/validateIntent.ts | Adds validateNonce() to detect “nonce too low” before signing (best-effort). |
| libs/coin-modules/coin-evm/src/logic/validateIntent.test.ts | Adds test coverage for nonce validation behavior (error/skip/best-effort). |
| .changeset/quick-nonces-heal.md | Declares minor bumps for live-common and coin-evm and documents the behavioral change. |
4b7504e to
683c459
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
libs/ledger-live-common/src/bridge/generic-coin-framework/utils.ts:166
nextSequenceWithPendingconvertstransactionSequenceNumberviaBigInt(op.transactionSequenceNumber.toString()), which can throw at runtime (e.g., if the BigNumber is NaN, non-integer, or rendered in exponential notation like1e+21). That would crash signing when an account has any malformed/edge pending op sequence. Consider guarding the BigNumber and converting viatoFixed(0)like other code does (e.g. coin-multiversx safeStakeToBigInt).
if (op.transactionSequenceNumber === undefined || op.transactionSequenceNumber === null) {
continue;
}
const seq = BigInt(op.transactionSequenceNumber.toString());
if (seq > highestPending) highestPending = seq;
Web Tools Build Status
|
Rsdoctor Bundle Diff AnalysisFound 7 projects in monorepo, 2 projects with changes. 📊 Quick Summary
📋 Detailed Reports (Click to expand)📁 desktop-rendererPath:
📦 Download Diff Report: desktop-renderer Bundle Diff 📁 mobilePath:
📦 Download Diff Report: mobile Bundle Diff Generated by Rsdoctor GitHub Action |
683c459 to
d94edd8
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
libs/ledger-live-common/src/bridge/generic-coin-framework/utils.test.ts:1382
- This test uses
pendingOp(1e21)as a JS number;1e21is outside the safe integer range, so the BigNumber constructed from it can be imprecise and make the assertion flaky. Use a string literal for the large nonce instead.
it("handles a large pending sequence without throwing (fixed-point, not exponential)", () => {
// BigNumber(1e21).toString() is "1e+21", which BigInt() cannot parse; .toFixed() must be used.
expect(nextSequenceWithPending([pendingOp(1e21)], 5n)).toBe(1000000000000000000001n);
});
libs/coin-modules/coin-evm/src/logic/validateIntent.ts:135
validateNonceusesgetNextSequence(), which is backed byNodeApi.getTransactionCount(). For RPC nodes this uses the"pending"tag (libs/coin-modules/coin-evm/src/network/node/rpc.common.ts:299-305), socurrentNonceis the next available nonce including pending txs. That means an explicit nonce meant to replace/speed-up an existing pending tx (same nonce as the pending one) will be< currentNonceand will be rejected as "nonce is too low", contradicting the stated support for speed-up flows.
try {
const currentNonce = await getNextSequence(currency, intent.sender);
if (intent.sequence < currentNonce) {
return {
errors: { transaction: new InvalidTransactionError("nonce is too low") },
warnings: {},
};
}
libs/ledger-live-common/src/bridge/generic-coin-framework/utils.test.ts:1351
- The
pendingOphelper takes a JSnumberand feeds it tonew BigNumber(seq). For large values (e.g. nonces beyondNumber.MAX_SAFE_INTEGER) this risks precision loss in the test setup; letting it acceptBigNumber.Valueenables passing a string for large nonces.
This issue also appears on line 1379 of the same file.
const pendingOp = (seq: number | null): Operation =>
({
transactionSequenceNumber: seq === null ? undefined : new BigNumber(seq),
}) as Operation;
d94edd8 to
a144cc3
Compare
|
4f3959f to
e50980f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (1)
libs/ledger-live-common/src/bridge/generic-coin-framework/signOperation.ts:72
- PR description states a new pre-sign
validateNonce()guard was added inlibs/coin-modules/coin-evm/src/logic/validateIntent.ts, but this PR only changes the generic coin-framework (no EVM validateIntent changes are included). Either add the missing EVM nonce validation changes, or update the PR description/scope so it matches what’s actually being shipped.
if (typeof transactionIntent.sequence !== "bigint" || transactionIntent.sequence < 0n) {
// The network sequence source lags behind a just-broadcast tx, so combine it with
// locally-tracked pending operations to avoid reusing a nonce on rapid consecutive sends.
const networkSequence = await coinModuleApi.getNextSequence(transactionIntent.sender);
transactionIntent.sequence = nextSequenceWithPending(
account.pendingOperations ?? [],
networkSequence,
);
|



📝 Description
When two transactions are sent in quick succession, the second one can be
rejected by the node with "nonce too low" (or trigger an unintended
replacement). This happens because the sequence/nonce source (indexer or a
load-balanced RPC) lags behind a just-broadcast transaction and briefly returns
a nonce that was already consumed by the first send.
Separately, when an intent carries an explicit nonce (dapp / WalletConnect /
speed-up flows), a stale value was only rejected after signing — the user went
through the whole device flow before hitting the error.
What this PR does
nextSequenceWithPending(generic-coin-framework) — at sign time, derivethe next sequence from both the network source and the locally-tracked
pending operations:
max(networkSequence, highestPendingSequence + 1).Pending ops are tracked optimistically right after broadcast and carry their
transactionSequenceNumber, so the next send no longer reuses a nonce. It isdeterministic and self-corrects: once the network source catches up,
networkSequencewins again.validateNonce(coin-evmvalidateIntent) — when an intent carries anexplicit sequence, validate it against the sender's on-chain nonce before
signing and surface
"nonce is too low"up front. Best-effort: a failed noncefetch never blocks the user. When no explicit sequence is set, nothing is
checked (crafting fetches a fresh nonce).
Changes
generic-coin-framework/utils.ts— addnextSequenceWithPending()generic-coin-framework/signOperation.ts— use it when the intent has no explicit sequencecoin-evm/logic/validateIntent.ts— add the pre-signvalidateNonce()guard🔗 Context