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:
- 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.
- 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
- 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.
- 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.
- Apply to all three
catch { } sites.
Acceptance criteria
Summary
Every send variant in
Provider.tswraps the entire access-key transaction path in a barecatch {}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 — threecatch { }sites inProvider.ts):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:
sendSync()throws both before broadcast (fill, sign — safe to fall back) and after (eth_sendRawTransactionSyncdispatched, 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
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.eth_sendRawTransactionSync/eth_sendRawTransactionis 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.catch { }sites.Acceptance criteria