Skip to content

feat(evm): wallet-declared privatePending hint for nonce + gas routing (backs blockbook#1639) - #30212

Closed
pragmaxim wants to merge 3 commits into
developfrom
feat/evm-privatepending-hint
Closed

feat(evm): wallet-declared privatePending hint for nonce + gas routing (backs blockbook#1639)#30212
pragmaxim wants to merge 3 commits into
developfrom
feat/evm-privatepending-hint

Conversation

@pragmaxim

@pragmaxim pragmaxim commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Populates the wallet-declared privatePending hint that blockbook trezor/blockbook#1639 consumes, so Blockbook can route EVM nonce lookups (getAccountInfo) and gas estimates (estimateFee) deterministically from authoritative wallet state, instead of inferring "did this instance recently accept a private send" via its per-instance recentSenders heuristic — which is fragile across restarts, load-balanced replicas or different websocket connections on different replica.

Draft — proposal, backs the design note: https://gist.github.com/pragmaxim/f71b03a4ed4c0030b6cba7fb0a302e96
Requires the blockbook side (trezor/blockbook#1639) to be deployed to have any effect; until then every field transmits undefined and behavior is unchanged.

Why this needed real design (the two original blockers)

  1. Suite couldn't cleanly populate the field. The private-vs-public decision is transient — it lives only at broadcast time in getMevProtectedTxData (drop disableAlternativeRPC ⇒ relay-routed) and is stored on neither the fake pending tx nor the real tx. Worse, the reducer whole-object-overwrites the fake pending tx with blockbook's real (unmarked) tx mid-flight, so any per-tx marker is wiped exactly when it's still needed. And the staking / WalletConnect / Merkl-claim / stablecoin-yield flows create no fake tx at all.
  2. Plumbing asymmetry. The nonce hint goes on getAccountInfo (validateParams whitelist + the explicit forward object); the estimate hint is hand-built at ~8 independent specific call sites with no choke point.

Design decisions

  • Dedicated durable slice keyed by (accountKey, nonce) (suite-common/wallet-core/src/privatePending) is the sole source of truth. Nonce is invariant across the fake→real swap; the slice is independent of the tx list, so it also covers the no-fake-tx flows. A tx-object marker or a getOwnEvmNonceSets-derived set both provably fail (marker wiped mid-flight; derivation can't tell private from public → would over-declare public txs to the rate-limited relay and under-declare the no-tx-list flows).
  • Delivery rides the unconditional basic getAccountInfo call in fetchAndUpdateAccountThunk, not the gated details:'txs' call. A blockbook-invisible private tx does not make the account "outdated", so the txs call is skipped for exactly the flows that need the hint. Sent only when the account has an in-flight private nonce (also the over-declaration guard).
  • Instance-agnostic STOP: prune entries with nonce < confirmedNonce (mined) or past a TTL backstop, on every basic refresh — so it self-heals after reconnecting to a different (load-balanced) blockbook instance, and a relay-dropped tx that never confirms can't be declared forever (bounds the eth: every gas estimate is sent to the alternative provider, burning its rate-limit quota blockbook#1629 quota concern).
  • buildEvmEstimateSpecific() attaches the hint to an EVM estimate specific only when nonces.length > 0 — the presence-only routing means a keystroke with nothing in flight never hits the relay.

Commits

  1. feat(blockchain-link) — plumbing, no behavior change: shared PrivatePendingHint type on AccountInfoParams + presence-only {nonces} on EstimateFeeParams.specific; forward through connect getAccountInfo (mirroring confirmedNonce); doc-mirror on WsAccountInfoReq; buildEvmEstimateSpecific helper + unit tests.
  2. feat(wallet-core) — durable Redux slice + delivery on the basic call + confirmedNonce/TTL prune. Still a no-op (no producer writes yet).
  3. feat(wallet-core) — first observable behavior: isMevPrivateSend predicate; record private sends in the send + WalletConnect flows (signed nonce already in scope); adopt buildEvmEstimateSpecific at the send + allowance estimate sites.

Scope / deferred

  • Deferred to a follow-up: the staking / Merkl-claim / stablecoin-yield writes. Those flows have no resolved signed nonce in scope (PrecomposedTransactionFinal carries none), so recording them faithfully needs signed-nonce capture (RLP-decode the serialized tx, or thread the computed nonce through compose/sign) — declaring them with a guessed nonce would be worse than the fallback. Until then they fall back to blockbook's heuristic, which Various fixes #1639 keeps as the fallback, so partial coverage never regresses below today.
  • Deferred: IndexedDB persistence of the slice. Redux alone already covers the reconnect-to-a-different-instance case (Redux isn't reset on WS disconnect); persistence only matters for an app restart mid-private-tx, which matches today's heuristic fallback.

Related

🤖 Generated with Claude Code

🌐 Preview deployments

🌐 Suite Web preview: https://dev.suite.sldev.cz/suite-web/feat/evm-privatepending-hint/web/

🔍 Currents Test Results

🔍 Suite desktop test results: View in Currents

🔍 Suite web test results: View in Currents

🔍 Suite native android test results: View in Currents

🔒 Quarantined E2E Tests

Trezor Suite (web) — 3 test(s)
Test Type
Trading - Swap inputs > Swap form inputs validation 🤖 auto
Quarantine test: "TrezorConnect webextension -> Suite Web,second call after popup was closed by user should work" 🙋 manual
Quarantine test: "Recovery T2T1 - dry run,Recovery after partial recovery" 🙋 manual

Updated: 2026-07-23T09:21:48.175Z • 3 test(s) total

Trezor Suite (desktop) — 2 test(s)
Test Type
Quarantine test: "Trading POC - passthru swap,Swap SOL to BTC with live quotes and blocked send" 🙋 manual
Quarantine test: "Trading POC - passthru swap ETH,Swap ETH to BTC with live quotes and blocked send" 🙋 manual

Updated: 2026-07-23T09:23:55.528Z • 2 test(s) total

pragmaxim and others added 3 commits July 23, 2026 08:02
… + estimate helper)

First of three stacked changes wiring the wallet-declared privatePending hint that
blockbook trezor/blockbook#1639 consumes to route EVM nonce lookups and gas estimates
deterministically instead of via its per-instance recentSenders heuristic.

This PR is pure plumbing with no behavior change (all fields transmit undefined):
- shared PrivatePendingHint type on AccountInfoParams (top-level, drives blockbook's
  declaredFloor) and a presence-only {nonces} on EstimateFeeParams.specific;
- forward privatePending through connect getAccountInfo (validateParams whitelist +
  the explicit forward object), mirroring confirmedNonce; the blockchain-link worker
  already forwards the payload untouched, so no worker change is needed;
- doc-mirror the field on WsAccountInfoReq in the generated blockbook-api.ts;
- buildEvmEstimateSpecific() helper (wallet-utils) that attaches the hint to an EVM
  estimate `specific` ONLY when there is at least one in-flight private nonce - the
  length>0 guard is the trezor/blockbook#1629 over-declaration guard - with unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…delivery + prune)

Second of three stacked changes for the blockbook privatePending hint
(trezor/blockbook#1639). Adds the durable model and the delivery/prune machinery;
no producer writes yet, so the slice stays empty and this is still a functional
no-op end to end.

Why a dedicated slice: the private/relay bit is transient (computed only at
broadcast in getMevProtectedTxData) and lives on neither the fake nor the real tx
object, and blockbook overwrites the fake pending tx with its real (unmarked) tx
mid-flight - so a tx-object marker cannot survive and a tx-list-derived set cannot
tell private from public. A small Redux slice keyed by (accountKey, nonce) is the
sole authoritative source; nonce is invariant across the fake->real swap.

- new suite-common/wallet-core/src/privatePending slice (Redux only for now;
  IndexedDB persistence deferred - Redux already survives WS reconnect, only an
  app restart mid-tx regresses, matching today's heuristic fallback);
- DELIVERY: attach the hint (and confirmedNonce:true) to the UNCONDITIONAL basic
  getAccountInfo call in fetchAndUpdateAccountThunk, gated on there being an
  in-flight private nonce. It must ride the basic call, not the gated details:'txs'
  one, because a blockbook-invisible private tx does not make the account
  "outdated" so the txs call is skipped for exactly the flows that need the hint;
- PRUNE (STOP): drop entries with nonce < confirmedNonce (mined) or past a TTL
  backstop, on every basic refresh - instance-agnostic, so it self-heals across
  load-balanced blockbook replicas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… privatePending

Third of three stacked changes for the blockbook privatePending hint
(trezor/blockbook#1639). First change with observable behavior: it fills the slice
from the two flows whose signed nonce is already in scope, and routes their gas
estimates.

- isMevPrivateSend(symbol, isMevProtectionEnabled) predicate (wallet-utils) - the
  exact condition under which getMevProtectedTxData broadcasts through the relay;
- record the private nonce+txid after a successful pushTransaction in the send flow
  (pushSendFormTransactionThunk) and in WalletConnect eth_sendTransaction, gated by
  isMevPrivateSend so public sends never declare;
- adopt buildEvmEstimateSpecific at the send and allowance estimate sites, passing
  the account's private-pending nonces so the estimate simulates against the relay's
  pending-private state - presence-only, so a keystroke with nothing in flight never
  routes to the relay.

Deferred (see PR description): staking / Merkl-claim / stablecoin-yield writes need
signed-nonce capture first (PrecomposedTransactionFinal carries no nonce); until
then those private flows fall back to blockbook's recentSenders heuristic, which is
acceptable because #1639 keeps that heuristic as the fallback. IndexedDB persistence
also deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pragmaxim
pragmaxim force-pushed the feat/evm-privatepending-hint branch from a39c8b4 to 315b63a Compare July 23, 2026 09:00
@pragmaxim
pragmaxim marked this pull request as ready for review July 27, 2026 10:17
@pragmaxim

Copy link
Copy Markdown
Contributor Author

Solved in #30534

@pragmaxim pragmaxim closed this Jul 30, 2026
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