Skip to main content

Routstr/Cashu safety

Routstr/Cashu code is money-path code. Treat changes here differently from ordinary UI work: a small bug can strand a user’s node balance, overwrite a recovery token, or make a browser wallet look empty after an upgrade. Use this page before changing any of:
  • js/cashu-wallet.ts
  • js/provider-wallet-runtime.ts
  • js/provider-wallet-panels.ts
  • js/provider-wallet-funding-recovery.ts
  • js/nostr-discovery.ts
  • Routstr provider key, model, or balance handling in js/api.ts
  • wallet IndexedDB schema or proof storage
  • cashu-ts upgrades or migration code
  • Cashu token receive, send, export, or restore
  • Lightning funding or withdrawal
  • Routstr node deposit or refund logic

Safety invariants

Do not leak bearer material

Never print, persist to test logs, or include in screenshots:
  • wallet seed phrases;
  • Cashu tokens;
  • Routstr sk-... session keys.
The only value a real-funds canary may print is the Lightning invoice that an operator needs to pay.

Preserve pending recovery state

  • pendingDeposit must be saved before sending a node deposit token.
  • pendingDeposit must survive node rejection or network failure until the token is recovered or explicitly cleared.
  • Node refunds must be journaled before the request, with the original node/session identity; returned tokens are saved before wallet receive.
  • Refund recovery is completed only after the receive and durable proof reconciliation succeed.
  • A new refund must not overwrite another session’s saved recovery record. Wallet outgoing-token and Lightning-withdrawal journals remain separate from node refunds.

Do not replace existing Routstr sessions

Deposit routing must be: Do not fall back from top-up failure to create. That can create a new key and strand the old node balance.

Keep the wallet facade stable

js/cashu-wallet.ts is the app’s compatibility facade. Library upgrades should happen behind this facade without forcing a wallet reset. Upgrades must preserve:
  • existing wallet seed;
  • existing mint URL;
  • existing proof rows;
  • pending funding quotes;
  • pending deposit token;
  • pending withdraw token;
  • Routstr node key and selected node;
  • JSON-safe proof rows, even if the Cashu library uses richer Amount objects internally.

Mint ownership and durable recovery

  • Keep proofs, pending quotes and recovery records associated with their originating mint. Switching the active mint must not hide or spend another mint’s proofs as if they belonged to the new one.
  • Show mint-specific balances and retain inactive-mint funding recovery. A paid invoice is not a credited balance until verified proofs are durably saved.
  • Journal outbound transfers, receives, minting and melts before irreversible calls. Persist recovered proofs and cleanup together so a lost response or interrupted browser can resume without losing the recovery record.
  • Coordinate wallet mutations and funding checks across tabs. Shared receipts and rate-limit cooldowns prevent duplicated work; notification subscriptions use paced polling as a fallback.
  • Stop background funding checks when their UI is inactive. Rejected quotes pause automatic retries without discarding recoverable state; an explicit recheck may resume them.
The facade delegates persistence to cashu-wallet-store.ts and transfer operations to cashu-wallet-transfers.ts. Keep failure-injection coverage for lost responses, failed storage writes, mint switching and concurrent tabs when touching these paths.

Node-session refund ownership

routstr-node-payments.ts stores node refunds under session-scoped recovery identities, with the original node URL, session key, generation, and token. Retry must validate that identity rather than use whichever node is currently selected. provider-wallet-refund-recovery.ts displays saved refunds separately, including their token mint. Receive and proof reconciliation use the token’s mint. Old refunds do not block unrelated node accounts or a later session, but an unresolved operation within the same session must still be recovered before another refund request. Switching mints preserves mint-bound receipts and quotes; checking them must use their recorded mint, not the active picker value. A spent-proof response alone is not evidence that recovered value was durably credited: retain the receipt until reconciliation proves completion.

Required test layers

Run tests in this order. Do not run real-funds tests until the no-money layers pass.

1. Runtime wallet tests

This covers the core wallet and recovery invariants without browser UI or real money. Run the focused test files for the changed wallet modules as well. Failures in the affected runtime tests block real-funds testing; a historical test count is not an acceptance criterion.

2. Browser Routstr wallet test

Run against the active dev server. If getbased is running on port 8180:
This covers the Settings → AI → Routstr UI contract without real money:
  • seed gate and restore UI;
  • mint switching for node requirements;
  • Lightning invoice funding UI;
  • pending Lightning funding recovery UI;
  • node deposit failure recovery UI;
  • node refund recovery path;
  • backup/export token action;
  • send-as-token action;
  • Lightning invoice withdrawal quote and execute;
  • Lightning address withdrawal with amount.
If Playwright tries the wrong port, set PORT to the actual getbased dev-server port. Verify the app identity if another service owns the default port.

3. Guarded tiny-sats canary

Only run this after the first two layers pass and after the developer intentionally chooses a tiny real amount. The app repo script is:
It refuses to run without this guard:
Setup phase:
Pay the printed PAY_THIS_LIGHTNING_INVOICE=... invoice, then run:
The canary uses isolated browser profiles under /tmp. Do not use a user’s normal browser profile for test funds. Expected canary checks:
  1. paid Lightning invoice recovers into the app wallet;
  2. first node deposit uses /v1/balance/create;
  3. second deposit with existing key uses /v1/balance/topup;
  4. a tiny model call works;
  5. node refund returns a Cashu token;
  6. refund token is saved before receive;
  7. refund token receives into the wallet;
  8. pending withdraw clears only after receive;
  9. Routstr key is cleared after refund;
  10. Cashu token export/import works in a second profile;
  11. token send-back recovers funds to the main profile;
  12. seed restore works in a third throwaway profile;
  13. final state has no pending deposit, no pending withdraw, and no Routstr key.
Expected final report shape:

Mint selection

Do not assume the default mint is reachable or accepted by the node. Before a real-funds canary, use a mint that is both:
  1. reachable from the test environment, and
  2. listed by the selected Routstr node’s /v1/info as accepted.
A prior baseline found mint.cubabitcoin.org reachable and accepted by api.routstr.com, while mint.minibits.cash was unreachable or refused from the VM path. Re-check live state before relying on either.

What blocks shipping

A Routstr/Cashu change should not ship if any of these are true:
  • existing wallet balance disappears after reload or upgrade;
  • pending funding quotes are lost;
  • pending deposit token is lost after a failed node deposit;
  • pending withdraw token can be overwritten;
  • existing Routstr key causes /create instead of /topup;
  • refund token is imported before being saved as pending;
  • seed restore no longer finds issued proofs;
  • real-funds canary leaves a node key with unrecovered balance;
  • real-funds canary leaves unexplained pending state;
  • test logs contain seed phrases, Cashu tokens, or sk-... keys.

Historical baseline before Cashu TS upgrade

The following historical baseline was tested with a 1000 sat canary before the Cashu TS migration work:
  • wallet funding recovered: 1000 sats;
  • first node deposit: /v1/balance/create;
  • second node deposit: /v1/balance/topup;
  • model call: 200 OK;
  • node refund: received back into wallet;
  • token export/import: passed;
  • seed restore: passed;
  • final wallet: 993 sats;
  • pending deposit: false;
  • pending withdraw: false;
  • Routstr key present: false.
This is historical migration evidence, not a claim that the current release has just passed a real-funds canary. Repeat an explicitly authorized canary for the version under review when required.

User docs

The user-facing Routstr guide is Routstr AI provider.