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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions