Skip to content

Silent access-key send fallback: swallowed errors, double-send risk #644

Description

@deodad

Summary

Every send variant in Provider.ts wraps the entire access-key transaction path in a bare catch {} and falls back to the adapter account. Any transient error silently reroutes that one send through the wallet (on mobile: a surprise browser round trip to the root wallet), the cause is undiagnosable because the error is discarded — and if the failure happened after broadcast, the fallback builds a second transaction and both can land.

Problem

sendTransactionSync_ (and the other send paths — three catch { } sites in Provider.ts):

if (transaction)
  try {
    const prepared = await transaction.prepare(...)   // eth_fillTransaction → relay
    return await prepared.sendSync()                  // sign → eth_sendRawTransactionSync → waits for receipt
  } catch { }                                         // ← anything, swallowed
// falls through to the adapter account (wallet round trip)

Observed in the field (wallet-mobile, device): three consecutive sends — first and third signed with the access key, the middle one unexpectedly opened the wallet browser flow. Plausible transients include a relay/network blip on the receipt-holding sync call, a nonce race between overlapping sends, or a keystore sign refusal — but the catch discards the error, so there is no way to tell, in development or production.

Two distinct defects:

  1. No diagnostics. A degraded send (worse UX, different trust surface) is indistinguishable from a healthy one in logs. Apps can instrument their own keystore signs, but fill/broadcast/receipt errors — the most likely transients — are invisible.
  2. Double-send risk. sendSync() throws both before broadcast (fill, sign — safe to fall back) and after (eth_sendRawTransactionSync dispatched, e.g. receipt-wait timeout — the transaction may already be in flight). The fallback can't tell these apart, and the wallet path then constructs a new transaction with its own nonce. Both can land: a payment sent twice because of a timeout.

Solution

  1. Make the fallback observable: log the swallowed error in development (console.warn('[accounts] access-key send failed; falling back to wallet:', error)), and/or expose it via a debug/event hook so apps can report it in production.
  2. Never fall back after broadcast: have the transaction object tag errors thrown after eth_sendRawTransactionSync/eth_sendRawTransaction is dispatched (the raw tx hash is known at that point and can ride on the error). The send paths rethrow those instead of falling back; the caller then surfaces "transaction may be pending" rather than silently re-sending.
  3. Apply to all three catch { } sites.

Acceptance criteria

  • A failed access-key send logs (dev) / surfaces (hook) the underlying error before any fallback
  • An error thrown after raw-tx dispatch propagates to the caller — no adapter fallback, no second transaction
  • Pre-broadcast failures (fill, sign, key selection) keep the existing graceful fallback
  • Localnet test: receipt-wait failure after broadcast does not produce a duplicate transaction

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