SIGN IN SIGN UP

fix: the Wallet API's errors are plain objects, and every reader stringified them

The spec declares each error as `{ code, message }`, not an Error. Reading it
with `err instanceof Error ? err.message : String(err)` produced the literal
text "[object Object]" - which the payer was shown for most codes, and which
killed every prose branch beneath it at once, including the insufficient-funds
recovery that exists to shield and retry.

Round 7's `didNotSubmit` also returned set membership for ANY numeric code, so
an unrecognised one suppressed the message fallback that used to catch it: a
wallet sending EIP-1193's 4001 was read as "may have been submitted" while
`explainWalletError`, one branch away, told the payer they had dismissed the
prompt. That is the lock-out the previous round said it removed. And the code
walk followed one `??` chain, so a JSON-RPC envelope's transport code hid the
wallet's real code in `data`.

Classification is now: every code found breadth-first, a known one decides, and
an unknown one falls through to the message. `abort` is out of the decision
list - it is what a transport says when a request is cut off, and Chrome's own
AbortError reads "The user aborted a request.", so treating it as a refusal
cleared the marker and paid the invoice twice on a lost response.

Also:

- The payment record was keyed by invoice id alone, and `?id=` is chosen by
  whoever writes the link. A second invoice reusing one erased the first's
  record and the first link paid again. Keyed by terms now.
- The data-borne event readers could never run: a node drops events whose key
  list is shorter than the filter, and both filters are longer. They looked
  alive only because the test's fake node skipped its own filter. Removed, the
  fake node made faithful, and the real limitation documented - event
  accounting covers keyed tokens, and a data-borne token falls back to balance
  accounting, which cannot see money already swept.
- write-off asked no chain and accepted any state: it could bury a payment the
  poller had not seen, into a state never polled again, and could convert a
  deletable row into a permanent lock. It now asks, and applies only to
  underpaid.
- The unrecognised-invoice refusal rested on a /healthz probe that CORS blocks
  in exactly the split-host deployment it was for, so it was silently off
  there and wrongly on when the server erred. It rests on the lookup's own
  status code now: 404 refuses, anything else is not evidence.
- `holdOpen` suppressed the paid_late label as well as expiry, so a late
  payment on a held row reported paid.
- The create path had no changed-under-us guard, so a DELETE inside its window
  returned 200 and was then undone.
- A corrupt store is renamed aside once rather than copied every boot, and the
  process refuses to start if the evidence cannot be kept.

164 tests. All seven guards verified by breaking them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
B
bongbongcrypto committed
54d30116155eae7757e6068e511fbcf10f5dd6c8
Parent: f92428f