Files
routstrd/docs/wallet-mint-recovery.md
T
redshift 9c75d2e202 feat(wallet): add live health checks to wallet doctor
wire the doctor helpers from the previous commit into a read-only health
check surfaced by the daemon and the CLI:

- coco-client: runWalletDoctor orchestrates the four checks against an
  injected structural source (unit-testable like runMintQuoteRecovery);
  diagnoseWallet() wires it to coco - trusted mints, pending/in-flight
  mint ops, and failed ops + inflight proofs via coco's private
  repositories, resolved fail-closed so a coco upgrade that renames them
  surfaces immediately. Quote checks are plain NUT-04 reads (never coco's
  observe-and-persist path), bounded by an overall budget and a
  concurrency limit; uncheckable quotes are counted, not dropped.
- http: GET /wallet/doctor returns the report (501 when the wallet client
  has no doctor support).
- cli: wallet doctor now prints the health section first (mint
  reachability, recent unpaid quotes, paid-but-not-issued with recover
  remediation, stuck melts), then the existing offline migration
  diagnosis. NO_COLOR/TTY-aware colors, --json for scripting, exit 1 when
  money is provably at risk or the wallets conflict. When the daemon is
  down the live section is skipped rather than auto-starting the daemon.
- tests: HTTP route tests, runWalletDoctor unit tests with a fake source,
  and real-Manager + fake-mint integration tests covering the full
  unpaid -> paid-unissued -> recovered -> clean arc and unreachable-mint
  degradation.
- docs: wallet doctor section in wallet-mint-recovery.md.
2026-10-04 20:52:16 +08:00

93 lines
4.5 KiB
Markdown

# Mint quote recovery: scope and troubleshooting
`routstrd wallet recover` explicitly retries mint operations through coco using
**their existing stored outputs**. It can restore signatures when a quote is
already issued, and reopen failed operations when explicitly requested:
```sh
routstrd history --json
routstrd wallet recover --op <operationId> --include-failed
```
Failed operations require explicit IDs over both HTTP and the CLI. A successful
re-run on an already finalized operation is a no-op. Requests that exceed their
wait budget are not cancelled; explicit retries skip the operation while the
underlying work is outstanding.
## What this fixes—and what it does not
Coco already checks pending quotes on startup and the daemon periodically
refreshes them. A quote paid while the daemon was offline does not, by itself,
require a new issuance implementation.
This change makes normal cleanup confirm UNPAID with the mint before failing an
expired quote. PAID, ISSUED and unverified quotes remain pending. It also gives
operators a recovery path for operations previously marked failed.
It does **not** replace rejected outputs with fresh outputs on an active keyset.
An inactive-keyset rejection can therefore remain retryable with zero recovery.
Recovery reports coco's persisted mint error when available, rather than only a
generic “remains pending” error.
Do not infer that the production incidents were caused by keyset retirement.
Before claiming those incidents are fixed, collect:
- The affected operation IDs, quote IDs, state and persisted `error`.
- A fresh remote quote state and, where provided, paid/issued amounts.
- The keyset IDs in the stored outputs and the mint's current keyset metadata.
- A reproduction showing existing recovery fails and the proposed fix succeeds.
Inspect persisted operation data through a read-only database copy; do not edit
rows or run recovery scripts concurrently with a daemon against the same wallet.
Never share the mnemonic, output secrets, or full wallet database in a PR.
A future fresh-output path must preserve original outputs for uncertain issuance
and NUT-09 restore, allocate fresh deterministic counters safely, and coordinate
with coco's watcher/processor. It needs its own integration tests before handling
real funds.
## Wallet doctor
`routstrd wallet doctor` runs read-only health checks before the legacy
migration diagnosis:
1. **Mint reachability** — a NUT-06 probe against every trusted mint.
2. **Recent unpaid quotes** — pending quotes from the last hour the mint
still reports UNPAID (an invoice awaiting payment; informational).
3. **Paid but not issued** — the stuck scenario this recovery feature
fixes. Each finding prints its `wallet recover --op <id>` remediation,
with `--include-failed` when the operation must be re-opened first.
4. **Stuck melts** — prepared melts holding reserved proofs, in-flight
melts, and failed melts whose input proofs were never released.
The doctor never mutates wallet state: quote checks are plain NUT-04 reads,
not coco's observe-and-persist path, and remediation is always left to the
operator. It exits non-zero when money is provably at risk (unreachable
mint, paid-but-unissued quote, failed melt with locked proofs) or the
migration diagnosis finds a conflict, so it can gate scripts. When the
daemon is down only the offline migration section runs.
## Cleanup preview and force
`wallet cleanup --dry-run` is local-only: it reports `mintQuoteCandidates`, not
confirmed failures. `failedMintQuotes` and `leftForRecovery` are zero because no
mint check or cleanup transition was performed. Send/melt counts remain planned
cleanup counts in dry-run mode.
`--force` deliberately bypasses mint confirmation and can strand paid sats in a
failed operation. Prefer normal cleanup. Forced operations can be retried with
`--op <operationId> --include-failed`, but recovery still depends on the mint
accepting their stored outputs or restoring their signatures.
## Integration and release notes
The reopen helper uses private coco-core 1.0.1 methods. Retain real-Manager and
HTTP fake-mint coverage, use frozen dependency installs, and re-run integration
tests on coco upgrades. A controlled low-value live-mint smoke test remains
recommended before release.
PR #118 removes `cocod-client.ts`. When integrating that change, move recovery
and cleanup contracts into its replacement `wallet-client.ts`, rename HTTP error
references accordingly, and make recovery mandatory for the in-process client.
This follow-up does not pull in #118's unrelated removal.