fix(frontend): friendly toast when chain-fusion signer is unavailable#13145
fix(frontend): friendly toast when chain-fusion signer is unavailable#13145sbpublic wants to merge 19 commits into
Conversation
A generic, calm message for chain-fusion signing outages, in a new top-level `sign` namespace (distinct from the dApp-signer `signer` namespace). Reused across sends and message-signing flows. Locale files synced (translations follow); en.json is the source of truth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Centralises the signer-outage detection: shows the calm sign.error.unavailable toast (without appending the raw ledger error) when the error is a SignerCanisterPaymentError, otherwise defers to the caller's fallback. The raw error is still logged to the console. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route the signer-error catch sites through toastsSignerUnavailableOr so the backend-out-of-cycles outage shows the calm sign.error.unavailable message instead of the raw 'Ledger error: …' text. Covers ETH/BTC/SOL sends, ETH NFT send, WalletConnect signing (central execute() wrapper, all chains), and the OpenCryptoPay inline failure. Genuine user errors keep their specific toasts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A distinct message for the per-user signing-limit case (exhausted ICRC-2 allowance towards the signer), separate from the wallet-wide outage message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An exhausted ICRC-2 spender allowance is a per-user signing limit, not the wallet-wide cycles outage, so the generic 'working on a fix' message is wrong for it. Detect the nested InsufficientAllowance via a new isSignerCanisterAllowanceError guard and show sign.error.limit_reached for that case, keeping sign.error.unavailable for other payment failures. Applied in the toast helper and the OpenCryptoPay inline failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ent errors getSchnorrPublicKey/signWithSchnorr threw the raw candid Err (stale TODO). The signer returns EthAddressError for Schnorr too, so route it through the existing mapSignerCanisterGetEthAddressError: a PaymentError now becomes a SignerCanisterPaymentError, so the signer-unavailable / per-user-limit toast reaches SOL signing like the ETH/BTC flows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…simulator Adds the 'allowance' mode (exhausted per-user ICRC-2 allowance) so the new sign.error.limit_reached message can be reproduced alongside payment/internal/ signing. Merged latest PR #13145 so the limit_reached path is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the spec in line with what shipped: the per-user allowance distinction (sign.error.limit_reached + isSignerCanisterAllowanceError), the as-shipped catch-site wiring (central WC execute(), OpenCryptoPay; swaps/WalletConnectReview deliberately not wired), the Schnorr error-mapping fix (new A5), and updated acceptance criteria + tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…blocker An exhausted-allowance payment error is a per-user limit working as intended, so tagging it 'blocker' (like a real cycles outage) over-reports it on dashboards. trackCfsSign now sets result_error_severity = major for the allowance case, blocker for other backend payment failures, critical otherwise. Also expands the spec Goal to capture the 'our fault' (cycles) vs 'as intended' (allowance) distinction, and syncs PRODUCT.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review — ✅ LGTM (toast half), and base #13144Reviewed both PRs in this stack against the spec. Base PR #13144 (tracking) — ✅ LGTM: all 11 paid signer calls wrapped, enums + severity added, This PR #13145 (toast) — ✅ LGTM. Faithful to Part A, with two well-judged extensions. Matches the spec:
Extensions beyond the original spec — both good calls:
Verified the one deliberate skip: swaps/EIP-2612 permits show a static Non-blocking follow-up: during an outage a swap shows "swap failed unexpectedly" rather than the nicer "signing unavailable" message. Not a leak, just less specific — could route swaps through |
…imit On an exhausted-allowance error, fire a best-effort allow_signing re-grant (replenishSignerAllowance) so the next signing attempt can succeed without a page reload. It mirrors the boot loader's allow_signing call but is non-fatal (never signs the user out) and single-flight. No auto-retry of the original operation; the limit_reached message still shows. The allow_signing rate limit remains the real per-user cap. Wired into the toast helper (sends + WalletConnect) and the OpenCryptoPay catch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an always-present event_code that encodes the full outcome semantics, so 'all payment failures' is filterable from one property (shared cfs_payment_failed_ prefix) instead of OR-ing severities: success, cfs_payment_failed_backend_out_of_cycles, cfs_payment_failed_user_allowance_exhausted, cfs_generic_error. event_code (cause) and result_error_severity (alerting weight) are derived from one classification so they never disagree. Also move the event to a dedicated event_context=cfs (not the dApp-signer's 'signer'), settling the old naming collision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…toast' into fix/frontend/signer-unavailable-toast
unavailable: 'Signing is temporarily unavailable. Please try again shortly.' limit_reached: 'You've reached your signing limit. Please try again shortly.' Drops the chain list and the 'we're working on a fix' admission so the outage message reads neutrally (not like an incident on our side); the two now share a cadence and closing. Synced the quoted copy in spec + PRODUCT.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Manual translations of the two signer toast strings (unavailable, limit_reached) into every language in the Languages enum (cs, de, es, fr, hi, it, ja, ko-KR, pl, pt, ru, vi, zh-CN). Arabic (ar.json) is not in the supported set, so it's left to fall back to English. Formality matches each locale's existing tone (Sie/vous/ usted/você/Вы formal; it informal). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up — final design (Part A)Two refinements since the review above — both good:
Still ✅ LGTM. |
…igner-unavailable-toast
…igner-unavailable-toast
Motivation
Second of two PRs from the spec. During a recent incident the chain-fusion signer could not sign for any user because OISY's backend ran out of TCycles. Users saw a raw, alarming toast:
That number is OISY's backend cycles balance, not the user's token balance — but it reads like a wallet error. This PR replaces that raw leak with a calm message across every affected flow, and is stacked on #13144 (it reuses the
isSignerCanisterPaymentErrorguard added there).It also distinguishes a second payment failure mode: an exhausted per-user ICRC-2 allowance towards the signer. That is a per-user signing limit, not a wallet-wide outage, so an outage-style "it's on us, we're fixing it" tone would be wrong — it gets its own message.
Spec:
docs/ai/spec-driven-development/specs/2026-06-18-impr-cfs-signing-error-toast-and-cfs-sign-event.md(Part A).Changes
signnamespace i18n (distinct from the dApp-signersignernamespace), translated into all supported languages (the 13 in theLanguagesenum;aris unused → falls back to English). Two keys:sign.error.unavailable— wallet-wide outage (e.g. backend out of cycles).sign.error.limit_reached— per-user signing limit (exhausted allowance).signer.errors.ts:SignerCanisterPaymentErrornow records whether the underlying ledger error was a nestedInsufficientAllowance, exposed via a newisSignerCanisterAllowanceErrorguard (alongsideisSignerCanisterPaymentError).toastsSignerUnavailableOr({ err, fallback })intoasts.store.ts: for aSignerCanisterPaymentErrorit showssign.error.limit_reached(allowance) orsign.error.unavailable(any other payment failure) — without appending the raw ledger text (stillconsoleError-logged); otherwise it defers to the caller's fallback.mapSolanaErrorMsgfallback).execute()wrapper inlib/services/wallet-connect.services.ts— covers WCpersonal_sign/eth_sign/ sends /signPsbtfor all chains in one place.failedPaymentError(friendly/limit message for the payment cases; tracking keeps the raw message).getSchnorrPublicKey/signWithSchnorr): they previously threw the raw candidErr(staleTODO). The signer returnsEthAddressErrorfor Schnorr too, so they now route through the existingmapSignerCanisterGetEthAddressError— aPaymentErrorbecomes aSignerCanisterPaymentError, so the new messages reach SOL signing like the ETH/BTC flows.event_code(success,cfs_payment_failed_backend_out_of_cycles,cfs_payment_failed_user_allowance_exhausted,cfs_generic_error) so "all payment failures" is filterable from one property (thecfs_payment_failed_prefix), and move the event to a dedicatedevent_context=cfs(not the dApp-signer'ssigner).event_code(cause) andresult_error_severity(alerting) are derived from one classification.allow_signingre-grant (replenishSignerAllowance, newsigner-allowance.services.ts) so the next attempt can succeed without a page reload -- non-fatal (never signs out), single-flight, no auto-retry. Theallow_signingrate limiter stays the real per-user cap.cfs_signevent severity for the allowance case:result_error_severity = major(a per-user limit working as intended) instead ofblocker, so it is not over-reported as an incident on dashboards (backend payment failures stayblocker, other signer errorscritical).PRODUCT.md.Tests
signer.errors.spec.ts:isSignerCanisterAllowanceErroris true forLedger{Withdraw,Transfer}FromError → InsufficientAllowanceand false for the funds/other payment variants and non-payment errors; an allowance error is still anisSignerCanisterPaymentError.toasts.store.spec.ts: showssign.error.unavailablefor a non-allowance payment error,sign.error.limit_reachedfor an allowance error (neither leaks the raw ledger detail), and defers to the fallback for non-payment / nullish errors.signer.canister.spec.ts:getSchnorrPublicKey/signWithSchnorrmap a returnedPaymentErrorto aSignerCanisterPaymentError.EthSendTokenWizard/BtcSendTokenWizard/wallet-connect.servicesspecs still pass.prettier,eslint --max-warnings 0,i18n:types+i18n:keys, andsvelte-check(no problems in changed files) pass.Divergence from the spec
The spec (
docs/ai/spec-driven-development/specs/2026-06-18-impr-cfs-signing-error-toast-and-cfs-sign-event.md) has been updated in this PR to match what shipped; the items below summarise the notable deviations from the original draft.InsufficientAllowancepayment error is a per-user signing limit (the caller's ICRC-2 allowance towards the signer is exhausted), distinct from the wallet-wide cycles outage, so it getssign.error.limit_reachedrather than the outage message. The distinction is frontend-only — the variant is already present in the signer's candid, so no CFS change is needed.SwapEthWizard(and siblings) already surface a genericswap.error.failed_unexpectedlymessage — the raw error only goes to tracking, never the UI — so there was no raw-text leak to fix. EIP-2612 permits surface through these same swap/approve catches, so they're covered by the existing generic message.WalletConnectReview.sveltenot touched: its catch handles session approve/reject, which does not call a signer method, so it can't receive aSignerCanisterPaymentError.🤖 Generated with Claude Code — model: Claude Opus 4.8 (claude-opus-4-8)