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.
4.5 KiB
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:
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:
- Mint reachability — a NUT-06 probe against every trusted mint.
- Recent unpaid quotes — pending quotes from the last hour the mint still reports UNPAID (an invoice awaiting payment; informational).
- Paid but not issued — the stuck scenario this recovery feature
fixes. Each finding prints its
wallet recover --op <id>remediation, with--include-failedwhen the operation must be re-opened first. - 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.