SIGN IN SIGN UP

fix: classify wallet errors by code, not by prose

Round 6 replaced one string-match with another and made things worse in the
opposite direction. `userRefused` used \b after the code alternation, and \b
does not match between D and _, so it missed USER_REFUSED_OP - the actual
Wallet API code, number 113, declared in @starknet-io/starknet-types-0103 in
this repo's own node_modules. An ordinary Reject therefore left the payer's
invoice branded maybe-paid, behind a ten-minute probe, with the double-spend
button as the only remaining control. Round 6 fixed the bug only for phrasings
no wallet emits.

The same labelling made every wallet-side error "may have reached the network",
including INSUFFICIENT_PRIVATE_BALANCE (119), so the shield-then-pay branch was
dead code with the shipped adapter: the payer was refused and then locked out.
The one test covering it passed for a wallet that does not exist.

Errors are now classified on the JSON-RPC code, which is a fact the wallet
asserts rather than prose it happens to write. Six of the seven documented
codes are pre-submission; UNKNOWN_ERROR is not, because unknown means unknown.
The message remains a narrow fallback for wallets that send no code.

Also:

- A settled invoice no longer mints a receipt. `confirm` answers "is this
  invoice paid?" and ignores the hash, so on a shared link one payer's money
  minted a receipt for another who had broadcast nothing, and fired onPaid so
  the merchant handed over goods. It now ends with a distinct error saying the
  merchant records the invoice as paid and this page cannot attribute that
  payment to this wallet.
- The unreconciled hold was applied by passing a doctored copy of the invoice,
  and evaluateInvoice returns a spread of what it is given - so the copy was
  written back and persisted with expiresAt gone. The deadline was destroyed
  permanently and later payments reported paid instead of paid_late. It is a
  parameter now, and the row is never edited.
- expiresAt is out of the idempotency fingerprint: a dashboard recomputes it on
  every click, so a retry 409'd forever for a create that had succeeded.
- A known token label must carry that token's address, the mirror of the check
  that already refused a known address with the wrong label.
- persist() is serialised and writes temp-then-rename, and a corrupt store is
  quarantined and logged rather than refusing to boot - refusing took down the
  only routes that could repair it.
- New write-off route: a stranger sending 1 wei to a published invoice address
  made it underpaid, which is neither deletable nor cancellable, pinning the
  invoice and its address forever. Write-off retires the row and keeps the
  address claimed, which is the part that must not change.

159 tests. All seven guards above verified by breaking them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
B
bongbongcrypto committed
5e353b2eac37be48e486ef5a481ddb6ccd7badb3
Parent: 20ebfb3