routstrd 0.3.11 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/COCO-2.0.0-MIGRATION-PLAN.md +1191 -0
  2. package/IMPLEMENTATION.md +253 -0
  3. package/README.md +46 -0
  4. package/SECURITY.md +23 -0
  5. package/SKILL.md +13 -3
  6. package/bun.lock +68 -41
  7. package/dist/daemon/index.js +48837 -11383
  8. package/dist/index.js +5027 -421
  9. package/package.json +8 -3
  10. package/src/cli.test.ts +55 -0
  11. package/src/cli.ts +601 -217
  12. package/src/daemon/args.ts +8 -1
  13. package/src/daemon/fatal-error.test.ts +104 -0
  14. package/src/daemon/fatal-error.ts +48 -0
  15. package/src/daemon/models.ts +119 -39
  16. package/src/daemon/wallet/auto-refill.ts +4 -6
  17. package/src/daemon/wallet/cleanup.test.ts +168 -0
  18. package/src/daemon/wallet/cleanup.ts +104 -0
  19. package/src/daemon/wallet/coco-client.npc.test.ts +252 -0
  20. package/src/daemon/wallet/coco-client.test.ts +669 -0
  21. package/src/daemon/wallet/coco-client.ts +1628 -0
  22. package/src/daemon/wallet/cocod-client.ts +135 -3
  23. package/src/daemon/wallet/diagnostics.test.ts +378 -0
  24. package/src/daemon/wallet/diagnostics.ts +471 -0
  25. package/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz +0 -0
  26. package/src/daemon/wallet/migration.test.ts +171 -0
  27. package/src/daemon/wallet/migration.ts +154 -0
  28. package/src/daemon/wallet/paths.ts +33 -0
  29. package/src/daemon/wallet/receive-dedup.test.ts +238 -0
  30. package/src/daemon/wallet/receive-dedup.ts +384 -0
  31. package/src/daemon/wallet/wallet-state.ts +75 -0
  32. package/src/integrations/claudecode.ts +2 -1
  33. package/src/integrations/hermes.ts +46 -6
  34. package/src/integrations/openclaw.ts +3 -2
  35. package/src/integrations/opencode.ts +3 -2
  36. package/src/integrations/pi.ts +3 -2
  37. package/src/integrations/registry.ts +7 -5
  38. package/src/start-daemon.ts +189 -23
  39. package/src/utils/clients.ts +21 -0
  40. package/src/utils/config.ts +11 -0
  41. package/src/utils/daemon-client.ts +104 -25
  42. package/src/utils/logger.ts +42 -28
  43. package/tests/integrations/hermes.test.ts +42 -5
  44. package/tests/utils/daemon-client.test.ts +61 -0
  45. package/tests/wallet/short-keyset-token.test.ts +106 -0
@@ -0,0 +1,1191 @@
1
+ # Coco 2.0.0 migration plan
2
+
3
+ ## Objectives
4
+
5
+ The migration must accomplish four things safely:
6
+
7
+ 1. Upgrade Routstrd from Coco `1.0.1` to `2.0.0`.
8
+ 2. Prevent old receive operations from causing a multi-hour first startup.
9
+ 3. Prevent repeated submission of the same Cashu proofs from creating duplicate receive operations.
10
+ 4. Preserve a reliable rollback path for legacy cocod wallets and existing Routstrd wallets.
11
+
12
+ The migration should **not** rely solely on Coco 2.0's normal receive recovery. Coco 2.0 correctly rolls back spent inputs whose restore returns no outputs, but it still processes receive rows individually. A wallet with 1,924 duplicate rows could therefore still experience one final long recovery before the rows become terminal.
13
+
14
+ ---
15
+
16
+ # 1. Target dependency set
17
+
18
+ Upgrade these as a coordinated set:
19
+
20
+ ```text
21
+ @cashu/coco-core 1.0.1 → 2.0.0
22
+ @cashu/coco-sqlite-bun 1.0.1 → 2.0.0
23
+ @cashu/cashu-ts → 5.0.0-rc.4-compatible version
24
+ ```
25
+
26
+ Do not upgrade only `@cashu/coco-core`. The SQLite adapter has an exact peer relationship with Coco 2.0.0 and Cashu TS 5.0.0-rc.4.
27
+
28
+ ## NPC compatibility gate
29
+
30
+ The existing NPC integration is the largest dependency risk:
31
+
32
+ ```text
33
+ coco-cashu-plugin-npc@2.4.1
34
+ └─ peer: coco-cashu-core ^1.1.2-rc.50
35
+ ```
36
+
37
+ Routstrd currently bridges the old and new Coco package families using a structural cast. Before the migration can ship, verify that the plugin still works with Coco 2.0's:
38
+
39
+ - plugin registration contract,
40
+ - service map,
41
+ - mint service,
42
+ - mint operation service,
43
+ - quote APIs,
44
+ - manager lifecycle,
45
+ - shutdown lifecycle.
46
+
47
+ If it is not compatible, choose one of these before release:
48
+
49
+ 1. Upgrade to a Coco 2-compatible NPC plugin.
50
+ 2. Patch/fork the plugin temporarily.
51
+ 3. Put NPC behind a compatibility feature flag and refuse the migration for NPC-enabled wallets.
52
+ 4. Disable NPC only with explicit user notice—not silently.
53
+
54
+ ---
55
+
56
+ # 2. Required startup architecture
57
+
58
+ The startup order is critical.
59
+
60
+ Today, constructing `SqliteRepositories` and calling `repo.init()` applies adapter migrations. The new receive preflight must therefore happen **before** Coco 2.0's repository initialization.
61
+
62
+ The intended startup pipeline should be:
63
+
64
+ ```text
65
+ Acquire wallet and legacy cocod locks
66
+ │
67
+ ├─ Locate wallet source
68
+ │ ├─ fresh wallet
69
+ │ ├─ legacy cocod wallet
70
+ │ ├─ current Routstrd Coco 1 wallet
71
+ │ └─ already-migrated Coco 2 wallet
72
+ │
73
+ ├─ Create committed SQLite snapshot
74
+ │
75
+ ├─ Validate snapshot and write pre-upgrade manifest
76
+ │
77
+ ├─ Run legacy receive-operation preflight
78
+ │ ├─ inspect receive operations
79
+ │ ├─ fingerprint proof sets
80
+ │ ├─ collapse provable duplicates
81
+ │ └─ reconcile ambiguous groups with bounded mint requests
82
+ │
83
+ ├─ Verify preflight invariants
84
+ │
85
+ ├─ Open staged DB using Coco 2 adapter
86
+ │ └─ repo.init() applies Coco 2 schema migrations
87
+ │
88
+ ├─ Verify post-schema-migration invariants
89
+ │
90
+ ├─ Initialize complete Coco 2 Manager lifecycle
91
+ │
92
+ ├─ Run bounded recovery over remaining unique operations
93
+ │
94
+ ├─ Verify wallet balances/proofs
95
+ │
96
+ ├─ Commit staged wallet atomically
97
+ │
98
+ └─ Start value-moving services
99
+ ```
100
+
101
+ The source database must remain unchanged until every staging check succeeds.
102
+
103
+ ---
104
+
105
+ # 3. Wallet version detection
106
+
107
+ Add an explicit wallet-format detector that works using raw SQLite, without importing or initializing a Coco repository.
108
+
109
+ It should identify:
110
+
111
+ ```text
112
+ fresh
113
+ legacy-cocod
114
+ coco-v1
115
+ coco-v2
116
+ unknown-or-partially-migrated
117
+ ```
118
+
119
+ Detection should use:
120
+
121
+ - presence of `config.json`,
122
+ - presence of `coco.db`,
123
+ - SQLite migration table contents,
124
+ - table and column signatures,
125
+ - WAL/SHM presence,
126
+ - a Routstrd-owned migration marker,
127
+ - Coco adapter migration IDs.
128
+
129
+ Do not infer wallet format only from the installed application version.
130
+
131
+ ## Refusal states
132
+
133
+ Startup must stop without modifying the wallet if:
134
+
135
+ - `coco.db` exists without its matching `config.json`,
136
+ - source and destination wallets both exist and differ,
137
+ - the SQLite database fails `PRAGMA quick_check`,
138
+ - the schema is unknown,
139
+ - a previous migration is partially committed,
140
+ - required sidecar files cannot be included in the snapshot,
141
+ - another wallet process holds the database,
142
+ - there is insufficient disk space for source + backup + staging database.
143
+
144
+ ---
145
+
146
+ # 4. Snapshot and rollback strategy
147
+
148
+ ## Always migrate a snapshot
149
+
150
+ For both legacy cocod and existing Routstrd wallets:
151
+
152
+ 1. Stop or fence all wallet engines.
153
+ 2. Acquire the wallet lock.
154
+ 3. Open the source database using raw SQLite.
155
+ 4. Run `PRAGMA quick_check`.
156
+ 5. Create a standalone committed snapshot using `VACUUM INTO`.
157
+ 6. Verify the snapshot independently.
158
+ 7. Perform preflight and Coco 2 migration only on the snapshot.
159
+
160
+ This includes committed WAL frames and avoids copying an incomplete main database file.
161
+
162
+ ## Pre-upgrade manifest
163
+
164
+ Before changing the staged snapshot, record a privacy-safe manifest containing:
165
+
166
+ - migration ID,
167
+ - source format,
168
+ - source database hash,
169
+ - SQLite migration IDs,
170
+ - proof counts by mint/unit/state,
171
+ - proof amounts by mint/unit/state,
172
+ - counter values by mint/keyset/unit,
173
+ - receive counts by state,
174
+ - receive unique-proof-set counts,
175
+ - send/melt/mint operation counts by state,
176
+ - trusted mints,
177
+ - history row count,
178
+ - database file size,
179
+ - backup path,
180
+ - timestamp.
181
+
182
+ Do not record:
183
+
184
+ - proof secrets,
185
+ - encoded tokens,
186
+ - output blinding data,
187
+ - mnemonic material.
188
+
189
+ ## Backup retention
190
+
191
+ Keep:
192
+
193
+ ```text
194
+ wallet-backups/
195
+ └─ pre-coco-2-<timestamp>/
196
+ ├─ config.json
197
+ ├─ coco.db
198
+ ├─ manifest.json
199
+ └─ README-restore.txt
200
+ ```
201
+
202
+ A Coco 2 database must not be opened by Coco 1 after migration. Downgrade means restoring the entire pre-upgrade snapshot, not reinstalling the old binary over the new database.
203
+
204
+ ---
205
+
206
+ # 5. Legacy receive-operation preflight checker
207
+
208
+ ## Purpose
209
+
210
+ The preflight checker must ensure users do not encounter hours of network recovery on their first Coco 2 startup.
211
+
212
+ It runs:
213
+
214
+ - after a committed snapshot has been created,
215
+ - against the staged Coco 1-format database,
216
+ - before `SqliteRepositories.init()` applies Coco 2 schema migrations.
217
+
218
+ It should be idempotent and safe to rerun after a crash.
219
+
220
+ ## Preflight modes
221
+
222
+ Provide two modes:
223
+
224
+ ```text
225
+ inspect
226
+ reconcile
227
+ ```
228
+
229
+ ### Inspect mode
230
+
231
+ Read-only. It reports:
232
+
233
+ - executing receive rows,
234
+ - unique input-proof sets,
235
+ - duplicate count,
236
+ - counts by mint,
237
+ - locally finalized siblings,
238
+ - groups with locally persisted output proofs,
239
+ - groups requiring mint reconciliation,
240
+ - estimated worst-case mint calls,
241
+ - estimated recovery duration.
242
+
243
+ ### Reconcile mode
244
+
245
+ Mutates only the staged snapshot and creates a complete audit record of every state transition.
246
+
247
+ Production migration uses `reconcile`; a diagnostic CLI command should expose `inspect`.
248
+
249
+ ---
250
+
251
+ # 6. Receive fingerprint design
252
+
253
+ Every receive operation must get a canonical, privacy-safe fingerprint representing the actual input proof set.
254
+
255
+ Conceptually:
256
+
257
+ ```text
258
+ fingerprint = SHA-256(
259
+ version
260
+ + normalized mint URL
261
+ + unit
262
+ + sorted proof identifiers
263
+ )
264
+ ```
265
+
266
+ The proof identifier should be Cashu's public `Y` derived from each proof secret, not the raw secret.
267
+
268
+ Requirements:
269
+
270
+ - normalize mint URLs consistently with Coco,
271
+ - include the Cashu unit,
272
+ - sort proof identifiers so proof order does not matter,
273
+ - include proof count,
274
+ - use domain separation/versioning,
275
+ - never log raw proof secrets,
276
+ - never use only the encoded token string.
277
+
278
+ Equivalent tokens containing the same proofs in different orders must produce the same fingerprint.
279
+
280
+ A fingerprint identifies a proof set, not a receive operation's deterministic outputs. Multiple duplicate operations can share one fingerprint while having different `outputData`.
281
+
282
+ ---
283
+
284
+ # 7. Preflight classification algorithm
285
+
286
+ Group every legacy receive operation by fingerprint.
287
+
288
+ For each group, inspect:
289
+
290
+ - operation states,
291
+ - operation timestamps,
292
+ - `outputData`,
293
+ - local proofs linked by `createdByOperationId`,
294
+ - local proof secrets matching deterministic outputs,
295
+ - local history,
296
+ - local input-proof states,
297
+ - input/output operation linkage.
298
+
299
+ ## Class A: finalized operation or outputs already saved locally
300
+
301
+ If one operation has locally persisted outputs:
302
+
303
+ - preserve/finalize that operation,
304
+ - mark duplicate nonterminal operations `rolled_back`,
305
+ - do not contact the mint.
306
+
307
+ Reason:
308
+
309
+ ```text
310
+ The wallet already has the receive outputs.
311
+ Other operations using the same inputs cannot represent additional value.
312
+ ```
313
+
314
+ If multiple operations appear finalized for the same fingerprint, flag the wallet for deeper invariant checking. This may be legitimate history duplication, but amounts must not be counted twice.
315
+
316
+ ## Class B: all inputs locally known spent by a finalized local operation
317
+
318
+ If input proofs are linked to a known successful local operation and no candidate has missing outputs:
319
+
320
+ - roll back duplicate executing rows locally,
321
+ - do not contact the mint.
322
+
323
+ The audit reason should distinguish this from an empty restore:
324
+
325
+ ```text
326
+ Preflight: duplicate receive inputs already consumed by finalized local operation
327
+ ```
328
+
329
+ ## Class C: duplicate group with inputs confirmed UNSPENT at the mint
330
+
331
+ Make one proof-state request for the fingerprint, not one per operation.
332
+
333
+ If every input is `UNSPENT`:
334
+
335
+ - retain one canonical operation for retry,
336
+ - mark duplicate operations `rolled_back`,
337
+ - let Coco 2 re-execute only the canonical operation.
338
+
339
+ Canonical selection should be deterministic:
340
+
341
+ 1. operation with the most complete valid prepared data,
342
+ 2. oldest operation if completeness is equal,
343
+ 3. stable operation-ID tie-breaker.
344
+
345
+ The retained operation must have valid `outputData`. If none does, preserve the group for manual recovery rather than guessing.
346
+
347
+ ## Class D: inputs confirmed SPENT at the mint
348
+
349
+ This is the important stuck-wallet case.
350
+
351
+ Because duplicate operations have different deterministic outputs, a successful swap may correspond to any one candidate's `outputData`. It is unsafe to select an arbitrary operation before checking restore results.
352
+
353
+ The checker should:
354
+
355
+ 1. Generate all expected blinded outputs for the group from stored `outputData`.
356
+ 2. Submit them to `/v1/restore` in bounded batches.
357
+ 3. Map returned output signatures back to the owning operation.
358
+ 4. Classify candidates:
359
+
360
+ ```text
361
+ matching restored outputs
362
+ ├─ preserve matching operation for Coco 2 finalization
363
+ └─ roll back nonmatching duplicates
364
+
365
+ no matching restored outputs
366
+ └─ roll back all operations in the group
367
+
368
+ partial/inconsistent restored outputs
369
+ └─ stop automatic migration and require recovery review
370
+ ```
371
+
372
+ A successful empty restore response is authoritative for the supplied output set and should result in:
373
+
374
+ ```text
375
+ rolled_back
376
+ error = "Preflight: input proofs spent without recoverable outputs"
377
+ ```
378
+
379
+ The checker does not need to save/unblind restored proofs itself if that would duplicate Coco internals. It can preserve the matching operation as the sole `executing` row and allow Coco 2 to recover it normally.
380
+
381
+ ## Class E: mixed proof states
382
+
383
+ If inputs are a mixture of:
384
+
385
+ - `SPENT`,
386
+ - `UNSPENT`,
387
+ - `PENDING`,
388
+ - unknown states,
389
+
390
+ do not automatically roll back the group.
391
+
392
+ Attempt bounded restore matching. If no conclusive outcome exists:
393
+
394
+ - preserve the minimum necessary candidate set,
395
+ - flag the group as unresolved,
396
+ - expose it clearly in migration status,
397
+ - do not silently launch an unbounded recovery sweep.
398
+
399
+ ## Class F: mint unavailable or timeout
400
+
401
+ An unavailable mint is not evidence that funds are unrecoverable.
402
+
403
+ The checker must:
404
+
405
+ - retain the canonical unresolved operation,
406
+ - avoid retaining hundreds of obvious duplicates where safety can be established locally,
407
+ - stop retrying when the migration network budget is exhausted,
408
+ - report the wallet as "migration pending mint availability."
409
+
410
+ Policy options should be configurable:
411
+
412
+ ```text
413
+ fail closed default; migration pauses and explains why
414
+ defer recovery daemon starts read-only, value-moving operations remain blocked
415
+ operator override explicit CLI action only
416
+ ```
417
+
418
+ Never automatically roll back an unresolved unique proof set only because the mint is offline.
419
+
420
+ ---
421
+
422
+ # 8. Bounded and deduplicated mint requests
423
+
424
+ The preflight must not merely replace a two-hour Coco sweep with another two-hour custom sweep.
425
+
426
+ ## Request strategy
427
+
428
+ - Check proof states once per unique fingerprint.
429
+ - Group requests by normalized mint URL.
430
+ - Batch proof-state checks according to protocol/mint limits.
431
+ - Batch restore outputs across duplicate candidates where supported.
432
+ - Map responses by `Y` or blinded output identifier.
433
+ - Apply per-mint rate limits.
434
+ - Apply request timeouts.
435
+ - Use bounded retries with exponential backoff.
436
+ - Persist progress after each completed batch.
437
+ - Resume from the last completed batch after restart.
438
+
439
+ ## Migration budget
440
+
441
+ Define explicit limits, for example:
442
+
443
+ ```text
444
+ per-request timeout
445
+ per-mint wall-clock budget
446
+ global preflight wall-clock budget
447
+ maximum retry count
448
+ maximum outputs per restore batch
449
+ ```
450
+
451
+ Do not hard-code the exact values until tested against common mints.
452
+
453
+ If the budget is exhausted:
454
+
455
+ - stop network reconciliation,
456
+ - save the checkpoint,
457
+ - keep the original wallet untouched,
458
+ - show exact unresolved counts,
459
+ - allow the user to resume later.
460
+
461
+ ## Expected effect on the observed wallet
462
+
463
+ Instead of processing 1,924 rows independently:
464
+
465
+ ```text
466
+ 1,924 receive rows
467
+ └─ 47 unique input sets
468
+ ```
469
+
470
+ Proof-state work becomes approximately one check per unique set, and restore work becomes batched by mint/output count. Even without aggressive batching, the cost should be based on 47 groups rather than 1,924 rows.
471
+
472
+ ---
473
+
474
+ # 9. Safe preflight database transitions
475
+
476
+ All preflight mutations must happen in explicit SQLite transactions.
477
+
478
+ For each batch:
479
+
480
+ ```text
481
+ BEGIN IMMEDIATE
482
+
483
+ ├─ verify candidate rows still have expected state/data
484
+ ├─ insert audit records
485
+ ├─ mark duplicates/terminal rows rolled_back
486
+ ├─ update timestamps using Unix seconds
487
+ ├─ write preflight checkpoint
488
+ └─ COMMIT
489
+ ```
490
+
491
+ Coco SQLite stores these timestamps in Unix seconds, so direct SQL updates must use:
492
+
493
+ ```sql
494
+ updatedAt = unixepoch()
495
+ ```
496
+
497
+ Do not write millisecond timestamps directly.
498
+
499
+ ## Audit data
500
+
501
+ Use a Routstrd-owned migration audit table or sidecar manifest containing:
502
+
503
+ - operation ID,
504
+ - fingerprint,
505
+ - original state,
506
+ - resulting state,
507
+ - reason code,
508
+ - evidence category,
509
+ - mint URL hash or normalized URL,
510
+ - reconciliation timestamp,
511
+ - restore/check batch ID.
512
+
513
+ Reason codes should be machine-readable, for example:
514
+
515
+ ```text
516
+ duplicate_of_local_finalized
517
+ duplicate_unspent_canonical_retained
518
+ spent_no_recoverable_outputs
519
+ recoverable_outputs_owned_by_sibling
520
+ mixed_state_unresolved
521
+ mint_unreachable
522
+ invalid_legacy_operation
523
+ ```
524
+
525
+ No secrets or full token material should be stored in the audit log.
526
+
527
+ ---
528
+
529
+ # 10. Post-preflight invariants
530
+
531
+ Before allowing Coco 2 schema migration:
532
+
533
+ 1. `PRAGMA quick_check` returns `ok`.
534
+ 2. Proof rows are unchanged unless the preflight explicitly recovered proofs.
535
+ 3. Proof totals by mint/unit/state are unchanged.
536
+ 4. Counters never decrease.
537
+ 5. Trusted mint records are unchanged.
538
+ 6. Every rolled-back receive has an audit record.
539
+ 7. At most one unresolved operation remains per fingerprint unless explicitly classified ambiguous.
540
+ 8. No finalized operation was demoted.
541
+ 9. No operation with locally persisted outputs was discarded.
542
+ 10. Remaining executing receive count is small and explained.
543
+ 11. The original source snapshot remains unchanged.
544
+
545
+ If any invariant fails, discard the staging database and retain the source wallet.
546
+
547
+ ---
548
+
549
+ # 11. Coco 2 schema migration
550
+
551
+ After receive preflight succeeds:
552
+
553
+ 1. Open the staged database.
554
+ 2. Construct the Coco 2 `SqliteRepositories`.
555
+ 3. Call `repo.init()` exactly once.
556
+ 4. Allow the official adapter to perform schema migrations.
557
+ 5. Do not manually reproduce Coco's general schema migration SQL.
558
+ 6. Close and reopen the migrated database.
559
+ 7. Verify the new schema and migration IDs.
560
+
561
+ ## Post-schema verification
562
+
563
+ Compare the post-migration database against the pre-upgrade manifest:
564
+
565
+ - proof counts and amounts,
566
+ - proof states,
567
+ - counters,
568
+ - receive states,
569
+ - send/melt/mint operation states,
570
+ - trusted mints,
571
+ - history row count,
572
+ - units,
573
+ - operation IDs.
574
+
575
+ Account for legitimate Coco 2 transformations such as amount representation and added columns, but the economic totals must remain equal.
576
+
577
+ Any amount conversion discrepancy is a release blocker.
578
+
579
+ ---
580
+
581
+ # 12. Coco 2 Manager lifecycle migration
582
+
583
+ Routstrd manually initializes Coco to avoid blocking HTTP startup on recovery. That initialization must be reviewed against Coco 2.
584
+
585
+ Coco 2 introduces or changes lifecycle pieces including:
586
+
587
+ - payment-request receive recovery,
588
+ - melt quote watcher,
589
+ - melt settlement processor,
590
+ - canonical quote handling,
591
+ - updated mint operation behavior,
592
+ - updated plugin surface,
593
+ - disposal semantics.
594
+
595
+ Create one Routstrd initialization function that mirrors Coco 2's official `initializeCoco()` sequence except where recovery is intentionally deferred.
596
+
597
+ Document every difference from upstream.
598
+
599
+ Conceptually:
600
+
601
+ ```text
602
+ construct Manager
603
+ ├─ initialize plugins
604
+ ├─ reconcile legacy/canonical quote state
605
+ ├─ enable required watchers
606
+ ├─ enable required processors
607
+ ├─ register NPC only after compatibility validation
608
+ ├─ expose read-only status
609
+ └─ start controlled recovery
610
+ ```
611
+
612
+ Do not accidentally omit new Coco 2 watchers/processors just because the old custom bootstrap did not know about them.
613
+
614
+ ---
615
+
616
+ # 13. Remaining recovery after migration
617
+
618
+ After preflight, Coco 2 recovery should see:
619
+
620
+ - no obvious duplicate receive rows,
621
+ - no known spent-and-unrestorable rows,
622
+ - at most one canonical operation per unresolved proof set,
623
+ - any operation with recoverable outputs preserved.
624
+
625
+ Run recovery in this order:
626
+
627
+ ```text
628
+ receive preflight verification
629
+ send recovery
630
+ melt recovery
631
+ receive recovery
632
+ payment-request receive recovery
633
+ mint recovery
634
+ ```
635
+
636
+ The final order should track Coco 2's documented requirements.
637
+
638
+ ## Recovery gating
639
+
640
+ Continue allowing safe reads during recovery, but block all value-moving actions:
641
+
642
+ - receive,
643
+ - send,
644
+ - melt,
645
+ - mint,
646
+ - mint trust changes,
647
+ - NPC actions that can move funds.
648
+
649
+ Expose recovery state through health/status:
650
+
651
+ ```json
652
+ {
653
+ "state": "MIGRATING",
654
+ "phase": "receive-preflight",
655
+ "legacyReceiveRows": 1924,
656
+ "uniqueReceiveSets": 47,
657
+ "groupsResolved": 42,
658
+ "groupsRemaining": 5,
659
+ "networkRequests": 61,
660
+ "elapsedMs": 42000
661
+ }
662
+ ```
663
+
664
+ The generic `/health` endpoint should distinguish "HTTP process alive" from "wallet ready for value movement."
665
+
666
+ ---
667
+
668
+ # 14. Preventing future duplicate receive operations
669
+
670
+ Coco 2 fixes terminal recovery, but ordinary `wallet.receive(token)` still does not appear to provide a general proof-set uniqueness guarantee. Routstrd needs its own receive coordinator.
671
+
672
+ ## Persistent receive guard
673
+
674
+ Create a Routstrd-owned receive guard keyed by the canonical fingerprint.
675
+
676
+ Suggested fields:
677
+
678
+ ```text
679
+ fingerprint PRIMARY KEY
680
+ mintUrl
681
+ unit
682
+ proofCount
683
+ operationId
684
+ state
685
+ terminalReason
686
+ attemptGeneration
687
+ createdAt
688
+ updatedAt
689
+ ```
690
+
691
+ Possible states:
692
+
693
+ ```text
694
+ reserved
695
+ executing
696
+ finalized
697
+ rolled_back_retryable
698
+ rolled_back_terminal
699
+ unknown
700
+ ```
701
+
702
+ ## Receive flow
703
+
704
+ Replace the direct call:
705
+
706
+ ```text
707
+ coco.wallet.receive(token)
708
+ ```
709
+
710
+ with:
711
+
712
+ ```text
713
+ decode and validate token
714
+ └─ compute fingerprint
715
+ └─ acquire per-fingerprint mutex
716
+ ├─ reconcile guard with Coco operation records
717
+ ├─ inspect existing guard state
718
+ ├─ reserve fingerprint persistently
719
+ ├─ create/execute one Coco receive operation
720
+ ├─ persist resulting operation ID/state
721
+ └─ release mutex
722
+ ```
723
+
724
+ ## Duplicate behavior
725
+
726
+ ### Existing finalized receive
727
+
728
+ Return an idempotent terminal result:
729
+
730
+ ```json
731
+ {
732
+ "success": true,
733
+ "deduplicated": true,
734
+ "credited": false,
735
+ "status": "already_received"
736
+ }
737
+ ```
738
+
739
+ This lets refund systems remove the token without implying that the wallet was credited twice.
740
+
741
+ ### Existing executing receive
742
+
743
+ Do not create another operation.
744
+
745
+ Return:
746
+
747
+ ```json
748
+ {
749
+ "success": false,
750
+ "retryable": true,
751
+ "status": "receive_in_progress",
752
+ "operationId": "..."
753
+ }
754
+ ```
755
+
756
+ Optionally trigger one controlled refresh of the existing operation.
757
+
758
+ ### Existing terminal rolled-back receive
759
+
760
+ Return a terminal result such as:
761
+
762
+ ```json
763
+ {
764
+ "success": false,
765
+ "retryable": false,
766
+ "status": "already_spent_or_unrecoverable"
767
+ }
768
+ ```
769
+
770
+ The caller should stop retrying the token.
771
+
772
+ ### Retryable rolled-back receive
773
+
774
+ Allow a new generation only after confirming the inputs remain unspent or the prior reason was explicitly transient.
775
+
776
+ Do not treat every `rolled_back` operation as retryable.
777
+
778
+ ---
779
+
780
+ # 15. Crash consistency of the receive guard
781
+
782
+ A process can crash between:
783
+
784
+ ```text
785
+ guard reservation
786
+ Coco operation creation
787
+ Coco operation completion
788
+ guard update
789
+ ```
790
+
791
+ Therefore startup must reconcile the guard table with Coco receive operations.
792
+
793
+ Cases:
794
+
795
+ ```text
796
+ guard reserved, no Coco operation
797
+ └─ clear or retry after stale-age validation
798
+
799
+ guard reserved, matching Coco operation exists
800
+ └─ attach operation ID and derive state
801
+
802
+ Coco operation exists, no guard row
803
+ └─ backfill guard from fingerprint
804
+
805
+ guard finalized, Coco executing
806
+ └─ verify local outputs before correcting either side
807
+
808
+ guard executing, Coco rolled_back
809
+ └─ copy terminal/retryable classification
810
+
811
+ multiple Coco operations for one fingerprint
812
+ └─ invoke duplicate reconciliation
813
+ ```
814
+
815
+ The process-level wallet lock prevents multiple Routstrd daemons from receiving concurrently, while the fingerprint mutex prevents concurrent submissions inside one process. The persistent unique key protects against race conditions that escape in-memory locking.
816
+
817
+ ---
818
+
819
+ # 16. SDK retry behavior
820
+
821
+ Deduplication in Routstrd prevents database growth, but the upstream caller should also stop repeatedly sending terminal tokens.
822
+
823
+ Update the wallet adapter contract to return structured outcomes:
824
+
825
+ ```ts
826
+ type ReceiveResult =
827
+ | { status: "received"; amount: number; unit: Unit }
828
+ | { status: "already_received"; amount: number; unit: Unit }
829
+ | { status: "in_progress"; retryAfterMs?: number }
830
+ | { status: "mint_unreachable"; retryAfterMs?: number }
831
+ | { status: "already_spent"; terminal: true }
832
+ | { status: "invalid_token"; terminal: true }
833
+ | { status: "unsupported"; terminal: true }
834
+ | { status: "unknown"; retryable: boolean };
835
+ ```
836
+
837
+ Avoid classifying behavior by matching strings in error messages.
838
+
839
+ ## Cached refund policy
840
+
841
+ - `received`: remove cached refund token.
842
+ - `already_received`: remove cached refund token.
843
+ - `already_spent`: remove or quarantine it; do not retry.
844
+ - `invalid_token`: quarantine; do not retry.
845
+ - `in_progress`: keep but do not create another receive.
846
+ - `mint_unreachable`: retry with exponential backoff.
847
+ - `unknown`: bounded retry, then require operator review.
848
+
849
+ Every retry record should include:
850
+
851
+ - fingerprint,
852
+ - first attempt time,
853
+ - last attempt time,
854
+ - next attempt time,
855
+ - attempt count,
856
+ - last structured status.
857
+
858
+ Set an upper retry age and attempt count. A token must not be retried forever.
859
+
860
+ ---
861
+
862
+ # 17. Legacy migration paths
863
+
864
+ ## Fresh wallet
865
+
866
+ ```text
867
+ No database
868
+ └─ initialize directly with Coco 2
869
+ └─ create empty receive guard
870
+ ```
871
+
872
+ No preflight is needed.
873
+
874
+ ## Legacy cocod wallet
875
+
876
+ ```text
877
+ legacy ~/.cocod
878
+ ├─ stop and fence cocod
879
+ ├─ snapshot into staging
880
+ ├─ run receive preflight on staged legacy DB
881
+ ├─ apply Coco 2 migration
882
+ ├─ verify
883
+ ├─ install canonical Routstrd wallet
884
+ └─ archive untouched legacy source
885
+ ```
886
+
887
+ The existing legacy archive behavior should be retained.
888
+
889
+ ## Existing Routstrd Coco 1 wallet
890
+
891
+ ```text
892
+ canonical wallet exists
893
+ ├─ acquire wallet lock
894
+ ├─ snapshot to upgrade staging
895
+ ├─ run preflight
896
+ ├─ apply Coco 2 migration
897
+ ├─ verify
898
+ └─ atomically replace active database
899
+ ```
900
+
901
+ ## Existing Coco 2 wallet without Routstrd receive guard
902
+
903
+ ```text
904
+ detect Coco 2 schema
905
+ ├─ do not rerun Coco schema migration
906
+ ├─ scan historical receive operations
907
+ ├─ backfill guard fingerprints
908
+ ├─ reconcile duplicates
909
+ └─ mark Routstrd migration complete
910
+ ```
911
+
912
+ ## Already migrated wallet
913
+
914
+ Check the Routstrd migration marker and proceed normally. The operation must be idempotent.
915
+
916
+ ---
917
+
918
+ # 18. Atomic commit strategy
919
+
920
+ Do not overwrite the active database in place while testing the migration.
921
+
922
+ Recommended commit sequence:
923
+
924
+ 1. Stop all wallet access.
925
+ 2. Close the staged Coco Manager and SQLite connection.
926
+ 3. Run final integrity and manifest checks.
927
+ 4. Rename active database to a rollback name.
928
+ 5. Rename staged database to `coco.db`.
929
+ 6. Fsync the containing directory where supported.
930
+ 7. Start Coco 2.
931
+ 8. Perform a read-only smoke check.
932
+ 9. Mark migration committed.
933
+ 10. Keep the rollback copy until a configured retention period passes.
934
+
935
+ If startup fails after the rename but before commit:
936
+
937
+ - stop Coco 2,
938
+ - preserve the failed staged database for diagnosis,
939
+ - restore the original database,
940
+ - restore the old application version only as part of the documented rollback procedure.
941
+
942
+ ---
943
+
944
+ # 19. User and operator experience
945
+
946
+ ## Automatic migration output
947
+
948
+ Example:
949
+
950
+ ```text
951
+ Preparing Cashu wallet upgrade to Coco 2.0...
952
+ Created rollback snapshot.
953
+ Checking 1,924 unfinished receive operations...
954
+ Found 47 unique input proof sets and 1,877 duplicates.
955
+ Resolved 44 sets locally.
956
+ Checking 3 unresolved sets with their mints...
957
+ Receive preflight complete:
958
+ 1 recoverable operation retained
959
+ 1,923 stale operations retired
960
+ Applying Coco 2.0 database migration...
961
+ Verifying proofs, counters, balances, and operations...
962
+ Wallet upgrade complete.
963
+ ```
964
+
965
+ ## Unavailable mint
966
+
967
+ Example:
968
+
969
+ ```text
970
+ Wallet upgrade paused safely.
971
+
972
+ 2 unique receive operations at https://mint.example could not be verified.
973
+ No wallet data was replaced and no operations were discarded.
974
+
975
+ Run:
976
+ routstrd wallet migration status
977
+ routstrd wallet migration resume
978
+ ```
979
+
980
+ Do not present an apparently healthy wallet while all value-moving calls are silently waiting on migration.
981
+
982
+ ## Suggested CLI
983
+
984
+ ```text
985
+ routstrd wallet migration inspect
986
+ routstrd wallet migration start
987
+ routstrd wallet migration status
988
+ routstrd wallet migration resume
989
+ routstrd wallet migration rollback
990
+ ```
991
+
992
+ Potential expert-only command:
993
+
994
+ ```text
995
+ routstrd wallet migration export-report
996
+ ```
997
+
998
+ The report must remain privacy-safe.
999
+
1000
+ ---
1001
+
1002
+ # 20. Testing plan
1003
+
1004
+ ## A. Coco 2 compatibility tests
1005
+
1006
+ - Manager construction.
1007
+ - Plugin initialization.
1008
+ - NPC address retrieval.
1009
+ - NPC username setup.
1010
+ - NPC synchronization.
1011
+ - Mint addition and default mint persistence.
1012
+ - Mint quote creation and recovery.
1013
+ - Send, melt, and receive operations.
1014
+ - Watcher startup and shutdown.
1015
+ - Manager disposal.
1016
+ - Background recovery gating.
1017
+
1018
+ ## B. Schema migration fixtures
1019
+
1020
+ Maintain frozen fixtures for:
1021
+
1022
+ - cocod `0.0.24`,
1023
+ - current Routstrd/Coco `1.0.1`,
1024
+ - Coco 1 wallet with WAL frames,
1025
+ - Coco 1 wallet without receive table,
1026
+ - Coco 1 wallet with all receive states,
1027
+ - Coco 2 wallet,
1028
+ - interrupted/partially migrated database.
1029
+
1030
+ For every fixture, compare pre/post economic manifests.
1031
+
1032
+ ## C. Stuck receive fixture
1033
+
1034
+ Create a sanitized fixture shaped like production:
1035
+
1036
+ ```text
1037
+ 1,924 executing rows
1038
+ 47 unique input sets
1039
+ multiple mints
1040
+ large duplicate groups
1041
+ some finalized siblings
1042
+ some empty restores
1043
+ some recoverable outputs
1044
+ some unavailable mints
1045
+ ```
1046
+
1047
+ Assertions:
1048
+
1049
+ - network work scales with 47 groups, not 1,924 rows,
1050
+ - no startup requires hours,
1051
+ - empty restores become rolled back,
1052
+ - recoverable output owners are preserved,
1053
+ - duplicate rows are retired,
1054
+ - second startup performs no receive recovery for terminal groups.
1055
+
1056
+ ## D. Deduplication tests
1057
+
1058
+ 1. Submit identical token twice sequentially.
1059
+ 2. Submit identical token concurrently.
1060
+ 3. Submit equivalent token with reordered proofs.
1061
+ 4. Submit equivalent token with different encoding.
1062
+ 5. Restart between guard reservation and Coco operation creation.
1063
+ 6. Restart between operation creation and guard update.
1064
+ 7. Submit a token already finalized before guard-table introduction.
1065
+ 8. Submit a terminally spent token repeatedly.
1066
+ 9. Submit a token while its first receive is still executing.
1067
+ 10. Confirm only one active Coco receive operation exists per fingerprint.
1068
+
1069
+ ## E. Preflight safety tests
1070
+
1071
+ - Locally finalized sibling.
1072
+ - Inputs all unspent.
1073
+ - Inputs all spent, empty restore.
1074
+ - Inputs all spent, one candidate restore match.
1075
+ - Inputs all spent, multiple inconsistent matches.
1076
+ - Mixed proof states.
1077
+ - Mint timeout.
1078
+ - Malformed output data.
1079
+ - Missing operation output data.
1080
+ - Database write failure mid-batch.
1081
+ - Crash after audit insertion but before operation update.
1082
+ - Resume after checkpoint.
1083
+ - Timestamp units remain Unix seconds.
1084
+
1085
+ ## F. Rollback tests
1086
+
1087
+ - Failure before preflight mutation.
1088
+ - Failure during preflight transaction.
1089
+ - Failure during Coco schema migration.
1090
+ - Failure during post-migration verification.
1091
+ - Failure after atomic rename.
1092
+ - Restore pre-upgrade snapshot and run old binary.
1093
+ - Ensure the old binary never opens the Coco 2 database.
1094
+
1095
+ ---
1096
+
1097
+ # 21. Release strategy
1098
+
1099
+ ## Stage 1: Compatibility branch
1100
+
1101
+ - Upgrade dependencies.
1102
+ - Adapt Manager lifecycle.
1103
+ - Resolve NPC compatibility.
1104
+ - Run existing tests.
1105
+ - No production migration yet.
1106
+
1107
+ ## Stage 2: Offline migration tool
1108
+
1109
+ - Implement scanner, manifest, preflight and staging.
1110
+ - Test against copied production databases.
1111
+ - Do not integrate automatic startup migration yet.
1112
+
1113
+ ## Stage 3: Receive coordinator
1114
+
1115
+ - Add fingerprint guard.
1116
+ - Add structured receive results.
1117
+ - Update SDK retry handling.
1118
+ - Verify no new duplicates can be created.
1119
+
1120
+ This should land before automatic Coco 2 migration so migrated users are protected immediately.
1121
+
1122
+ ## Stage 4: Canary
1123
+
1124
+ Use copied production wallets first, then a small opt-in user cohort.
1125
+
1126
+ Canary gates:
1127
+
1128
+ - exact proof totals preserved,
1129
+ - exact spendable balances preserved,
1130
+ - recovery duration within target,
1131
+ - no duplicate receive growth,
1132
+ - no NPC regressions,
1133
+ - no migration rollback events.
1134
+
1135
+ ## Stage 5: General release
1136
+
1137
+ Automatic migration may be enabled only after:
1138
+
1139
+ - fixture suite passes,
1140
+ - real wallet copies pass,
1141
+ - canary succeeds,
1142
+ - rollback is tested,
1143
+ - operational documentation is published.
1144
+
1145
+ Keep a feature flag such as:
1146
+
1147
+ ```text
1148
+ ROUTSTRD_COCO2_AUTO_MIGRATE=0
1149
+ ```
1150
+
1151
+ during the initial rollout so operators can require explicit migration.
1152
+
1153
+ ---
1154
+
1155
+ # 22. Acceptance criteria
1156
+
1157
+ The migration is complete when all of the following are true:
1158
+
1159
+ 1. Coco core and SQLite adapter both run at `2.0.0`.
1160
+ 2. Existing Coco 1 and legacy cocod wallets migrate without balance changes.
1161
+ 3. The preflight runs before Coco 2 schema migration and recovery.
1162
+ 4. Recovery work scales with unique proof sets rather than operation rows.
1163
+ 5. The 1,924-row production-shaped fixture does not cause a multi-hour startup.
1164
+ 6. Successful empty restore results become `rolled_back`.
1165
+ 7. Recoverable outputs are never discarded.
1166
+ 8. Unreachable mints do not cause unsafe local rollback.
1167
+ 9. At most one active receive operation exists per fingerprint.
1168
+ 10. Repeated tokens return structured idempotent/terminal results.
1169
+ 11. SDK refund logic stops retrying terminal tokens.
1170
+ 12. Interrupted migrations resume or roll back safely.
1171
+ 13. Pre-upgrade databases remain available for full rollback.
1172
+ 14. NPC functionality is verified or explicitly gated.
1173
+ 15. A second startup after migration performs no redundant receive sweep.
1174
+ 16. Every automatic receive state change is auditable without exposing wallet secrets.
1175
+
1176
+ ## Recommended implementation priority
1177
+
1178
+ ```text
1179
+ 1. Dependency and NPC compatibility spike
1180
+ 2. Receive fingerprint specification
1181
+ 3. Read-only preflight inspector
1182
+ 4. Staged migration and rollback framework
1183
+ 5. Deduplicated preflight reconciler
1184
+ 6. Persistent receive guard
1185
+ 7. Structured SDK receive outcomes
1186
+ 8. Coco 2 Manager integration
1187
+ 9. Fixture/canary validation
1188
+ 10. Automatic migration rollout
1189
+ ```
1190
+
1191
+ The most important design rule is: **never let Coco 2 open and migrate an old wallet until the raw legacy receive preflight has inspected and reduced its unfinished receive set.**