@shieldedtech/moth-wallet 0.14.1 → 0.15.0-next.20260925125759-5ffbb70

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 (74) hide show
  1. package/CHANGELOG.md +1485 -0
  2. package/dist/contract/call.d.ts.map +1 -1
  3. package/dist/contract/call.js +11 -49
  4. package/dist/contract/call.js.map +1 -1
  5. package/dist/contract/deploy.d.ts.map +1 -1
  6. package/dist/contract/deploy.js +11 -49
  7. package/dist/contract/deploy.js.map +1 -1
  8. package/dist/contract/maintenance.d.ts.map +1 -1
  9. package/dist/contract/maintenance.js +21 -92
  10. package/dist/contract/maintenance.js.map +1 -1
  11. package/dist/contract/wallet-provider.d.ts +30 -0
  12. package/dist/contract/wallet-provider.d.ts.map +1 -0
  13. package/dist/contract/wallet-provider.js +48 -0
  14. package/dist/contract/wallet-provider.js.map +1 -0
  15. package/dist/daemon/protocol.d.ts +1 -1
  16. package/dist/daemon/protocol.d.ts.map +1 -1
  17. package/dist/daemon/protocol.js.map +1 -1
  18. package/dist/daemon/wallet-handlers.d.ts.map +1 -1
  19. package/dist/daemon/wallet-handlers.js +30 -6
  20. package/dist/daemon/wallet-handlers.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/proof/provider.d.ts +10 -2
  26. package/dist/proof/provider.d.ts.map +1 -1
  27. package/dist/proof/provider.js +35 -14
  28. package/dist/proof/provider.js.map +1 -1
  29. package/dist/sync/activity.d.ts +1 -1
  30. package/dist/sync/activity.d.ts.map +1 -1
  31. package/dist/sync/activity.js +13 -2
  32. package/dist/sync/activity.js.map +1 -1
  33. package/dist/sync/dust-transacting.d.ts +29 -3
  34. package/dist/sync/dust-transacting.d.ts.map +1 -1
  35. package/dist/sync/dust-transacting.js +87 -28
  36. package/dist/sync/dust-transacting.js.map +1 -1
  37. package/dist/sync/ledger-routing.d.ts +80 -0
  38. package/dist/sync/ledger-routing.d.ts.map +1 -0
  39. package/dist/sync/ledger-routing.js +130 -0
  40. package/dist/sync/ledger-routing.js.map +1 -0
  41. package/dist/sync/operations.d.ts +24 -9
  42. package/dist/sync/operations.d.ts.map +1 -1
  43. package/dist/sync/operations.js +46 -35
  44. package/dist/sync/operations.js.map +1 -1
  45. package/dist/sync/preseed.d.ts +4 -0
  46. package/dist/sync/preseed.d.ts.map +1 -1
  47. package/dist/sync/preseed.js +14 -14
  48. package/dist/sync/preseed.js.map +1 -1
  49. package/dist/sync/tx-summary.d.ts +21 -7
  50. package/dist/sync/tx-summary.d.ts.map +1 -1
  51. package/dist/sync/tx-summary.js +41 -10
  52. package/dist/sync/tx-summary.js.map +1 -1
  53. package/dist/sync/wallet-sync.d.ts +10 -0
  54. package/dist/sync/wallet-sync.d.ts.map +1 -1
  55. package/dist/sync/wallet-sync.js +66 -82
  56. package/dist/sync/wallet-sync.js.map +1 -1
  57. package/dist/types/wallet.d.ts +19 -15
  58. package/dist/types/wallet.d.ts.map +1 -1
  59. package/dist/wallet/address.d.ts +20 -5
  60. package/dist/wallet/address.d.ts.map +1 -1
  61. package/dist/wallet/address.js +25 -9
  62. package/dist/wallet/address.js.map +1 -1
  63. package/dist/wallet/manager.d.ts.map +1 -1
  64. package/dist/wallet/manager.js +6 -17
  65. package/dist/wallet/manager.js.map +1 -1
  66. package/dist/wallet/sign-message.d.ts +4 -3
  67. package/dist/wallet/sign-message.d.ts.map +1 -1
  68. package/dist/wallet/sign-message.js +3 -3
  69. package/dist/wallet/sign-message.js.map +1 -1
  70. package/package.json +3 -3
  71. package/dist/sync/sdk-dedup.d.ts +0 -69
  72. package/dist/sync/sdk-dedup.d.ts.map +0 -1
  73. package/dist/sync/sdk-dedup.js +0 -181
  74. package/dist/sync/sdk-dedup.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,1485 @@
1
+ # @shieldedtech/moth-wallet
2
+
3
+ ## 0.15.0-next.20260925125759-5ffbb70
4
+
5
+ ### Minor Changes
6
+
7
+ - 773443b: The daemon's `submitTransaction` answers `PROTOCOL_VERSION_MISMATCH` instead of `INVALID_PARAMS`
8
+ when the hex is a transaction built for the other side of the ledger fork, or when the wallets
9
+ cross the fork before it is submitted. Hex that no ledger reads still gets `INVALID_PARAMS`.
10
+ `RpcErrorCode` gains the new member, and the CLI renders it as `INVALID_INPUT` with a "Protocol
11
+ version mismatch" prefix.
12
+ - d07668a: Settings → Network shows where the account's wallets stand on the protocol version line, as the
13
+ wallet SDK reports it: the version transactions are built for, the ledger it puts the wallet on
14
+ (ledger-v8 below the v9 fork, ledger-v9 from it) and whether the three wallets have settled on it
15
+ or are still crossing a fork. Core exposes it as `protocolStatus(facade)`.
16
+ - fa2a009: Upgrade to `@midnightntwrk/wallet-sdk` 2.0.0-rc.0, which brings `@midnightntwrk/ledger-v9`
17
+ 1.0.0-rc.5. The SDK runs ledger-v8 below the chain's v9 fork and ledger-v9 from it, and its
18
+ wallets follow a live chain across the boundary, so moth reaches networks on either side with one
19
+ build.
20
+
21
+ - Wallets start from per-role seeds: `WalletKeys` is now `{shielded, unshielded, dust}` seeds
22
+ rather than ledger key objects, and the wallet holds the keys it derives from them for as long
23
+ as it runs. Transaction-building methods no longer take key material.
24
+ - Transactions travel as version-stamped handles (`FinalizedTransaction`). Hex payloads on the
25
+ daemon socket and dApp-supplied transactions are read with the ledger the facade is acting at,
26
+ through the SDK's `WalletFacade.adoptTransaction`; `transactionHashOf` and
27
+ `activeProtocolVersion` read hashes and versions off the handles.
28
+ - Proving routes by protocol version: one proof server serves both sides, and the in-process
29
+ prover, including the extension's in-worker one, is registered per ledger version.
30
+ - The client-side dedup of re-sent boundary events (`sync/sdk-dedup.ts`) is removed; the fix
31
+ landed upstream in both wallet variants.
32
+ - DUST fee balancing keeps its two fixes, largest-first coin selection and a loop that terminates,
33
+ on both sides of the fork: SDK 2.0 still ships the unbounded loop in both dust variants, so the
34
+ dust wallet is `DustWallet` rebuilt through `CustomForkingDustWallet` with both fixes on each.
35
+ - Contract deploy/call/maintenance keep authoring ledger-v8 transactions through midnight-js
36
+ 4.1.1 and refuse a network that has crossed to ledger-v9.
37
+ - Message signing returns the schnorr hex value of ledger-v9's tagged signature, so the wire
38
+ format is unchanged for connectors.
39
+ - Browser bundles now carry both ledgers' WASM, as every SDK 2.0 consumer does.
40
+ - The wait for the first facade emission no longer reaches for its own subscription: the 2.0
41
+ facade replays state synchronously on subscribe, which turned that into a startup crash.
42
+ - The SDK's unshielded subscription now reads `protocolVersion` off the indexer's progress frame,
43
+ so unshielded sync needs an indexer that exposes it (midnight-indexer 4.4.0-rc.2 / `25da0487`
44
+ or later). Against an older indexer the unshielded wallet never connects.
45
+
46
+ ## 0.14.1
47
+
48
+ ### Patch Changes
49
+
50
+ - 0f0afa3: Stop the dedup filter from punching holes in a replay, and say what a non-linear
51
+ insert actually means.
52
+
53
+ `partitionByAppliedIndex` filtered the whole batch by predicate, dropping every
54
+ event with `id <= appliedIndex` wherever it sat. For an ascending batch that is
55
+ the intended boundary-duplicate filter. For an out-of-order one it removes an
56
+ already-applied event from the *middle* and hands the SDK a replay with a gap —
57
+ and the ledger tree then rejects the insert:
58
+
59
+ ```
60
+ values inserted non-linearly into dust generation tree;
61
+ expected to insert index 337423, but received 337429.
62
+ ```
63
+
64
+ Note the direction: the SDK bug this wrapper exists to work around re-sends a
65
+ boundary event, so the tree receives an index *lower* than it expects. This is
66
+ the mirror image — an index *higher* than expected, because events the tree
67
+ needed never arrived. The workaround for one could produce the other, and only
68
+ for the part whose cursor and tree had drifted apart, which is why a single
69
+ sub-wallet would stall while the others finished.
70
+
71
+ The filter now drops only a leading contiguous run of already-applied events.
72
+ When an already-applied id appears after a fresh one the batch is not the
73
+ ascending stream the filter assumes, so nothing is dropped and the SDK's own
74
+ (more conservative) handling decides. The existing test named "drops only the
75
+ already-applied prefix" described this behaviour already; the implementation now
76
+ matches it.
77
+
78
+ Non-linear insert failures are also no longer bare WASM strings surfacing four
79
+ layers down with no mention of sync state. They now carry the cursor and batch
80
+ that produced them and say that retrying cannot help — the same batch replays
81
+ from the same cursor forever — and that the fix is to clear that part's cache:
82
+
83
+ ```
84
+ appliedIndex=1291234, replayed 2 event(s) 1291235..1291240,
85
+ 0 dropped as already applied. Clear this wallet part's sync cache and resync.
86
+ ```
87
+
88
+ The original error is preserved as `cause`. Unrelated errors pass through
89
+ untouched.
90
+
91
+ ## 0.14.0
92
+
93
+ ### Minor Changes
94
+
95
+ - 55a878c: Add a `proveTransaction` daemon verb, and honour `MOTH_HOME`.
96
+
97
+ `proveTransaction` builds, balances, proves and signs a transfer, then returns
98
+ the finalized transaction as hex instead of submitting it. Programmatic clients
99
+ that need to hold a proof and submit it later had no verb for the prove half;
100
+ `submitTransaction` already covered the other. The returned hex binds fee-side
101
+ DUST UTXOs by nullifier, so the wallet must stay quiet until submission — the
102
+ L3 modal says so, and `--max-spend` applies as it does to `transferTokens`.
103
+
104
+ `MOTH_HOME` now overrides the `~/.moth` root for both the filesystem storage
105
+ adapter and the daemon socket path, so several isolated instances can run from
106
+ one account. A relative value is rejected rather than resolved against the
107
+ working directory, which would otherwise put a wallet and its socket under
108
+ different roots.
109
+
110
+ ### Patch Changes
111
+
112
+ - 3b39fb6: Report the real error when a contract artifact fails to load.
113
+
114
+ `loadContractArtifact` wrapped its whole `managed/contract/` branch in a
115
+ `try`/`catch` that discarded the error, so a module that existed but failed to
116
+ load was reported as `No contract module found in <path>`. The common cause is a
117
+ compact runtime version mismatch, whose own message names both versions and
118
+ points straight at the fix. Only a missing `contract/` directory now falls
119
+ through to the remaining strategies.
120
+ - 3b39fb6: Stop waiting for two equal DUST balances before building a contract transaction.
121
+
122
+ Before building a contract transaction the contract paths waited for the wallet
123
+ to be strictly synced, and then for two consecutive emissions carrying an
124
+ identical DUST balance. `facade.state()` emits roughly every 30 seconds, and a
125
+ resident daemon has just spent DUST on the previous call, so the pair never
126
+ matched on the first try and every call after the first paid a full emission
127
+ cycle.
128
+
129
+ Measured with the wait instrumented, this was 39 s of a 60 s daemon call on a
130
+ local stack and 16-30 s of a ~36 s call on preprod, against 1.7 s of real work.
131
+ Comparing both snapshots at a single instant does not help, because the balance
132
+ genuinely differs after a spend.
133
+
134
+ The pair check is removed. The strict-completion filter stays: that is what
135
+ guards against a stale dust tree root (InvalidDustSpendProof, error 170), and it
136
+ is all the transfer path has ever done. Per call this takes preprod from ~36 s
137
+ to ~27 s and a local stack from 60 s to ~29 s.
138
+ - 3b39fb6: Sign contract transactions through `facade.signRecipe`, so unshielded inputs reach the node signed.
139
+
140
+ The contract paths signed by hand, ending each intent with
141
+ `tx.intents.set(segment, signedIntent)` and discarding the result.
142
+ `Transaction.intents` is a WASM getter that builds a fresh JS Map on every read,
143
+ so this mutated a throwaway copy and the signature never reached WASM. Any
144
+ circuit producing unshielded UTxO inputs — anything calling `receiveUnshielded`
145
+ — was submitted with those inputs and zero signatures, and the node rejected it
146
+ with `1010 Invalid Transaction: Custom error: 192`
147
+ (InputsSignaturesLengthMismatch).
148
+
149
+ All four sites (call, deploy, and both maintenance paths) now call
150
+ `facade.signRecipe`, which returns a new recipe rather than mutating in place —
151
+ the same call the transfer path has always made. It also selects the right
152
+ signer per transaction type and stamps DUST registration signatures, neither of
153
+ which the hand-rolled helper did. The three copies of that helper are deleted.
154
+
155
+ `deploy` was never affected, because it spends only DUST on fees, which is why
156
+ the failure looked specific to `call`.
157
+ - 77edf22: Replace the wallet SDK's DUST fee-balancing loop with one that terminates.
158
+
159
+ The SDK's `computeBalancingRecipe` (`wallet-sdk-dust-wallet` 4.2.0) re-selects
160
+ dust coins until they cover the fee of the transaction they produce, with no
161
+ iteration cap and no progress check. It also seeds its first pass with a
162
+ negative dust imbalance and every later pass with a positive fee; the balancer
163
+ reads the positive seed as a surplus, adds an output and selects nothing, so
164
+ only the first pass can ever converge. A wallet holding several part-drained
165
+ dust coins under-covers on that first pass and the loop then spins forever,
166
+ building and proof-erasing a WASM transaction on the calling thread each time
167
+ until the process runs out of memory.
168
+
169
+ Moth now supplies its own transacting capability through the SDK's documented
170
+ `V1Builder.withTransacting` seam (`sync/dust-transacting.ts`). It keeps the
171
+ SDK's fee arithmetic — `dryRunFee` and `calculateFee`, the WASM parts — and
172
+ replaces only the control flow: each pass covers the outstanding deficit from
173
+ coins not yet selected, then re-prices the transaction with everything selected
174
+ so far. A pass that does not converge has strictly grown the input set, so the
175
+ loop is bounded by the number of coins; running out surfaces as the SDK's own
176
+ `InsufficientFundsError`. Both `estimateFee` and `balanceTransactions` route
177
+ through the replaced method, so the fee preview and the real spend take the
178
+ same path. Coin selection order is unchanged.
179
+
180
+ Trade-off: this couples Moth to the SDK's exported implementation class and
181
+ three of its methods. The test suite pins that surface so an SDK upgrade that
182
+ changes it fails in CI rather than silently reverting to the non-terminating
183
+ loop. `effect` becomes a direct dependency of `@shieldedtech/moth-wallet` (it
184
+ was already in the tree via the SDK) because the capability returns the SDK's
185
+ `Either` values. The same loop is proposed upstream in
186
+ `docs/upstream-issues/dust-fee-balancing-nontermination.md`.
187
+ - aa3c276: Pay DUST fees from the largest coin first, so fee balancing terminates.
188
+
189
+ The wallet SDK's dust fee balancer (`wallet-sdk-dust-wallet` 4.2.0,
190
+ `computeBalancingRecipe`) loops until the coins it selected cover the fee that
191
+ selecting them produced, with no iteration cap and no progress check. Its
192
+ default selector takes the smallest coin first, so a wallet holding several
193
+ part-drained DUST coins spends a handful of them, which enlarges the
194
+ transaction, which raises the fee past what those coins cover — and the loop
195
+ never exits. It also never fails: only the first pass can converge, because
196
+ that pass is seeded with a negative dust imbalance while every later pass is
197
+ seeded with a positive fee, which sends the balancer down its add-an-output
198
+ branch and selects no inputs at all. Each pass deserialises and erases proofs
199
+ on a fresh WASM transaction while holding the thread, so the wallet stops
200
+ responding and its WASM heap grows until the process dies.
201
+
202
+ Moth now sets largest-first selection on the dust wallet through the SDK's
203
+ documented `V1Builder.withCoinSelection` extension point
204
+ (`sync/dust-coin-selection.ts`), on both the restore-from-cache and
205
+ start-from-secret-key paths. One coin near its generation cap covers a fee
206
+ outright, so the first pass converges — which is the only pass that can.
207
+
208
+ This costs nothing in DUST fragmentation: a dust spend is one-in-one-out (the
209
+ ledger nullifies the coin and mints a successor worth the remainder), coin
210
+ count is pinned to the number of registered NIGHT UTXOs, and a coin's value
211
+ regenerates toward its cap. Draining the fullest coin therefore rotates across
212
+ backing UTXOs on its own as the drained ones refill, and produces a smaller
213
+ transaction than spending eight coins to reach the same fee.
214
+
215
+ **This is a mitigation, not a fix.** A wallet whose dust is spread evenly
216
+ across coins that are all far below fee size still spins, because no single
217
+ coin covers the fee — pinned as a test, and filed upstream with the iteration
218
+ traces in `docs/upstream-issues/dust-fee-balancing-nontermination.md`, which
219
+ asks for the progress check that would close it. Transaction construction,
220
+ signing, and proving are unchanged.
221
+ - 19a1a23: Pre-seed the DUST state of restored wallets when the indexer proves it safe.
222
+
223
+ Pre-seeding only ran for a wallet whose birthday was at or after the reference
224
+ height — the birthday being the wallet's only local proof that it has no earlier
225
+ history to skip. A wallet restored from a mnemonic or hex seed has no birthday,
226
+ and a wallet whose cache was cleared has one that predates the reference, so
227
+ both walked DUST from genesis: 1.4M events at ~293 events/s, about 78 minutes on
228
+ preprod, of which dust is 99%.
229
+
230
+ For the dust part there is a second proof. All of a wallet's DUST descends from
231
+ generation entries owned by its dust key, and the indexer can say whether that
232
+ key owned any entry at the reference height: `Block.dustGenerationEndIndex`
233
+ gives the generation tree's size `N` at that height, and the bounded
234
+ `dustGenerations(dustAddress, 0, N − 1)` subscription ends with `complete` — no
235
+ owned entry before it is a positive "none" (~1.1 s on preprod). When the
236
+ birthday rule fails and the dust cache is missing, `startWalletSync` runs that
237
+ probe and on "none" seeds dust alone from the reference; shielded and
238
+ unshielded still scan from genesis, which is quick. The hour becomes the
239
+ reference's deserialize plus catch-up.
240
+
241
+ Fails closed: an owned entry, an indexer without the field (pre-4.2), a timeout
242
+ or an error all keep the genesis walk, with the reason in the sync progress
243
+ line. The decision is the pure `preSeedPlan` (`sync/preseed-parts.ts`); the
244
+ probe is `sync/dust-history.ts`; `IndexerClient.getDustGenerationEndIndex` and
245
+ `dustAddressForKey` (a dust address from the typed key, no seed needed) support
246
+ it. ADR 0003 records the exception.
247
+
248
+ ## 0.13.0
249
+
250
+ ### Minor Changes
251
+
252
+ - a17b719: Show what a dApp transaction takes from the wallet before the user approves it.
253
+
254
+ When a connected dApp calls `balanceSealedTransaction` or
255
+ `balanceUnsealedTransaction`, the wallet is asked to cover whatever the dApp's
256
+ transaction is short of — and the approval screen said only "Network fee: paid
257
+ in DUST". The user was approving a spend without being told the amount or the
258
+ token. The screen now lists, per token, what the wallet has to supply ("You
259
+ pay") and any surplus it collects back ("You get back"), plus the number of
260
+ contract calls when there are any. If the transaction cannot be decoded, it
261
+ says so in a visible warning rather than showing nothing.
262
+
263
+ The amounts come from the transaction itself, before anything is balanced,
264
+ booked or spent: core gains `summarizeTransaction` /
265
+ `summarizeConnectorTransaction` (`sync/tx-summary.ts`), which sums the ledger's
266
+ `Transaction.imbalances(segment)` over the guaranteed section and every intent.
267
+ A negative imbalance is what the wallet must put in; a positive one is change.
268
+ The sign convention is pinned by a test against the real ledger. Fees are not
269
+ included — they are only known once the wallet has balanced and proven its own
270
+ segment, and are always paid in DUST.
271
+ - b49c96d: Give the CLI and TUI the pre-seed, timings and DUST-registration behaviour the
272
+ extension already had.
273
+
274
+ **Birthdays.** `chainTip` moves from the extension's background handlers into
275
+ core, and `wallet generate` on both surfaces records the chain tip as the new
276
+ wallet's birthday. Without one the `reference.height <= birthday` guard can never
277
+ pass, so no CLI or TUI wallet could ever be pre-seeded — the difference between
278
+ 29.3s and 78.6 min on preprod. Imports still get none, deliberately: a restored
279
+ wallet may hold funds at any height, and seeding it past its own history would
280
+ lose them silently.
281
+
282
+ **`moth preseed status|refresh|build`.** Thin wrappers over core functions
283
+ that already existed but had no caller outside the extension. `refresh` is the
284
+ one worth having — 9.1s to catch a reference up, against 53.6 min to rebuild it
285
+ from genesis, which is what someone does by hand when the command is missing.
286
+ `build` says how long it will take before starting, because an unattended command
287
+ that appears to hang for an hour is indistinguishable from a broken one.
288
+
289
+ **Phase timings on disk.** `createFileTimingStore` backs the existing
290
+ storage-agnostic recorder with `~/.moth/timings.json` — the path
291
+ `docs/BENCHMARKING.md` already documented and nothing wrote. `moth diagnostics
292
+ timings` shows the timeline as deltas, and recording stays off until switched on.
293
+ A headless sync is where this matters most: it is the surface with no other
294
+ signal about where the time went.
295
+
296
+ **DUST registration in the TUI.** `DustRegistrationNotYetError` is now caught
297
+ distinctly, so a wallet whose NIGHT is too new to cover the registration fee is
298
+ told "not yet" instead of shown the raw SDK error as a failure. The panel and the
299
+ CLI already did this; the TUI was the surface still reporting it as a defect.
300
+ - 0508a38: Four CLI fixes found by a manual test pass, none of which the test suite could see.
301
+
302
+ **Amounts are parsed strictly (#63).** `moth transfer` used `parseFloat` and
303
+ `Math.round`, and `parseFloat` keeps whatever prefix it understands: `1,5` was
304
+ accepted as **1 NIGHT**, losing a third of the value with no warning; `0.0000001`
305
+ rounded to **zero base units** and was submitted as a transfer that moved nothing
306
+ and still paid a fee; `1e3` became 1000 NIGHT; `1abc` became 1. A shared
307
+ `parseNightAmount` in core now refuses all of them, in BigInt, with a message
308
+ naming what is wrong — and `daemon transfer` already did it this way, so the two
309
+ paths finally agree.
310
+
311
+ **`moth transfer` can select a token (#62).** It hardcoded `NIGHT_TOKEN_ID`, so a
312
+ wallet holding anything else could spend it only through `daemon transfer
313
+ --token-id`. Same flag name and default here. The positional amount stays a NIGHT
314
+ decimal and is refused for other tokens, directing to `--amount` in raw base units
315
+ — mis-scaling a token by NIGHT's 10⁶ would be worse than refusing.
316
+
317
+ **`moth wallet export-phrase` exists (#59).** `WalletManager.exportPhrase` and
318
+ `exportSeedHex` have always been in core, and the extension exposes them, but no
319
+ CLI command did. So the CLI had no backup path — a phrase was shown once at
320
+ `wallet generate` and never again — no way to move a wallet between machines, and
321
+ no way to recover a seed for a keystore you hold the passphrase to. Confirmed by
322
+ default and refused non-interactively without `--yes`, following `wallet remove`.
323
+ A wallet imported from a hex seed says so rather than presenting a seed as a
324
+ phrase.
325
+
326
+ **`wallet address` takes `--wallet` (#60).** It was the only command in the CLI
327
+ requiring `--name`, which made it the only one that could not act on the active
328
+ wallet: `wallet use w1` then `wallet address` failed with "Missing required flag
329
+ name". `--name` remains as an alias.
330
+
331
+ **`dust register` distinguishes empty from done (#58).** `designateForDust`
332
+ returns `null` both when every UTXO is already designated and when there are no
333
+ NIGHT UTXOs at all, and the command reported the first for both — vacuously true
334
+ of an empty wallet, and read as success. An unfunded wallet now gets "No NIGHT to
335
+ designate" and a non-zero exit.
336
+ - 426b757: Detect and steer users away from the devnet dust-ledger wedge (docs/bugs-found #15-style defect).
337
+
338
+ Some devnets (midnight-node 2.0.0-rc.4 / midnight-ledger 9.1.0.0-rc.3) can
339
+ enter a state where a DUST registration leaves the node's dust ledger unable
340
+ to reconcile with any wallet's, permanently — every subsequent dust spend,
341
+ from every wallet, fails with the same `InvalidDustSpendProof` /
342
+ `Custom error: 170` signature that a normal transient race also produces.
343
+ Only a chain reset clears it; retrying does not. This is not a Moth defect —
344
+ see `docs/upstream-issues/dust-ledger-wedge-invalid-dust-spend-proof.md` for
345
+ the evidence and the draft issue against `midnightntwrk/midnight-node` /
346
+ `midnight-ledger` — but Moth users hit it on shared and local devnets, so
347
+ Moth now detects and steers around it rather than retrying forever.
348
+
349
+ **Detection** (`@shieldedtech/moth-wallet`, `sync/dust-ledger-health.ts`): a
350
+ run of consecutive, independently built submissions rejected with the same
351
+ ambiguous signature — with the chain confirmed still producing new blocks
352
+ meanwhile — is
353
+ surfaced as `DustLedgerWedgedError` instead of a generic failure. Wired into
354
+ every fee-paying submission path: the extension's offscreen host, `moth
355
+ transfer` / `moth dust register`, and the daemon's `transferTokens` /
356
+ `dustRegister` / `dustDeregister` RPCs (used by both the headless CLI daemon
357
+ and the TUI).
358
+
359
+ **Registration UX** (`@shieldedtech/moth-extension`): the "Register for
360
+ DUST" flow now warns before registering a NIGHT coin that has sat
361
+ unregistered for more than a minute — the only pattern with zero known
362
+ failures across four documented occurrences is registering within seconds of
363
+ funding — and a wallet that has never registered is nudged to do so as soon
364
+ as new NIGHT is observed, rather than waiting for the user to find the Dust
365
+ screen on their own.
366
+
367
+ **Repro harness**: `packages/cli/tests/integration/daemon/dust-wedge-repro.test.ts`
368
+ isolates the fund-to-register delay as the one variable prior occurrences
369
+ didn't control for, holding UTXO size fixed at the size every known success
370
+ used (10,000,000 NIGHT).
371
+
372
+ No change to transaction construction, signing, or proving — every
373
+ registration involved in the underlying defect was accepted by the ledger,
374
+ so this only classifies what a rejection means after the fact.
375
+ - eb4d56a: **The extension can restore an account from a raw hex seed.** Previously it
376
+ accepted a 24-word phrase and nothing else, which made accounts created from a
377
+ seed permanently unreachable there.
378
+
379
+ That was not a policy — a wallet created from a hex seed **has no mnemonic and
380
+ can never be given one**, because BIP-39's phrase-to-seed step is a one-way KDF.
381
+ There was nothing to type into the word grid. Meanwhile the TUI
382
+ (`tui/src/app.tsx`) and the CLI (`moth wallet import --seed-hex`) both had the
383
+ path, and the extension's own session model is seed-based end to end: it derives
384
+ `seedHex` at unlock and carries it through every operation. A mnemonic was only
385
+ ever a transport format for the seed, and the extension accepted just that one
386
+ format at the door.
387
+
388
+ The plumbing was the only thing missing. `wallets` in the browser facade *is* a
389
+ `WalletManager`, so `importFromSeed` was already callable from the offscreen
390
+ document; `unlock` already handled a `seed:` keystore. `ImportWalletRequest` is
391
+ now a union — exactly one of `mnemonic` / `seed`, enforced by the type rather
392
+ than a runtime check — threaded through the background/offscreen RPC, and the
393
+ offscreen host routes to whichever core call fits. The two stay separate calls,
394
+ not one with a branch: `import` runs the BIP-39 checksum, `importFromSeed`
395
+ shape-checks the hex.
396
+
397
+ The restore screen now offers both artifacts as tabs on one page. No
398
+ "which do you have?" step first: anyone restoring already knows which they hold,
399
+ so that step would cost a click and gather nothing.
400
+
401
+ **New in core: `wallet/hex-seed.ts`, and `importFromSeed` now validates.** It had
402
+ no validation whatsoever — a malformed seed reached the SDK and surfaced as a
403
+ bare `Invalid seed`, and a merely wrong-*length* seed was accepted outright,
404
+ silently producing a different wallet. `checkHexSeed` returns a machine-readable
405
+ problem so each surface can word it itself (the extension localises it; the CLI
406
+ and TUI use `describeHexSeedProblem`). The TUI and CLI inherit the fix.
407
+
408
+ **The validation is shaped around the fact that a hex seed has no checksum.**
409
+ Measured against the wallet SDK, `HDWallet.fromSeed` accepts any 16–64 byte
410
+ seed and refuses 15 or 65 — and every accepted length derives a *different*
411
+ wallet. So:
412
+
413
+ - change one character and there is **no error**, just a different, valid, empty
414
+ wallet;
415
+ - truncate a paste and the same is true;
416
+ - whereas one wrong word in a phrase fails `validateMnemonic`.
417
+
418
+ The bounds therefore match what the SDK actually accepts rather than a rule of
419
+ our own, with a test that fails if an SDK bump moves them. Lengths other than 32
420
+ or 64 bytes — the two sizes real tooling emits — are **warned about, not
421
+ refused**: a truncated paste looks exactly like a seed genuinely created at that
422
+ length, and refusing would lock such a wallet out. And the field is deliberately
423
+ **not** a password input: reading the seed back against a backup is the only
424
+ check available, so masking it would remove the sole defence.
425
+
426
+ Note those two sizes are not interchangeable. A 32-byte seed is what the Midnight
427
+ node toolkit and `moth wallet import --seed-hex` deal in; the 64-byte one is the
428
+ BIP-39 seed a phrase expands to, and what `exportSeedHex` returns for a
429
+ phrase-backed wallet. Truncating the latter to the former gives a different
430
+ wallet.
431
+
432
+ Restored accounts keep `createdHere: false` and no birthday, so they scan from
433
+ genesis — they may hold funds at any height, and seeding one past its own history
434
+ would hide them (ADR 0003, rule 4).
435
+
436
+ **The TUI's hex import was that validation.** `SeedEntryScreen` matched
437
+ `/^[0-9a-fA-F]{64}$/` and did it *before* `importFromSeed`, so core's new check
438
+ never ran and the screen refused every length the SDK accepts bar one — including
439
+ the 128-character seed this release teaches the extension to reveal. Round-trip
440
+ a phrase-backed account (extension → Accounts → Reveal → Hex seed) into the
441
+ TUI's hex import and it answered "Invalid hex seed. Must be exactly 64 hex
442
+ characters", while the CLI took the same string. The check now lives in
443
+ `tui/src/screens/onboarding/seed-input.ts` and delegates to `checkHexSeed`, so
444
+ the TUI inherits the bounds and the unusual-length warning instead of
445
+ re-deciding them; a test sweeps 16..64 bytes so the screen cannot silently drift
446
+ tighter than core again.
447
+
448
+ Also corrected: three places documented a BIP-39 seed as 64 hex characters. It is
449
+ 128 — 64 hex characters is a 32-byte seed, a different artifact that derives a
450
+ different wallet. `core/src/sync/operations.ts` and two spots in
451
+ `docs/spec/wallet-service/05-key-management.md`. That claim is precisely what
452
+ would lead someone to write `length === 64` validation and reject the seeds this
453
+ app exports (#99).
454
+
455
+ Closes #98.
456
+ - 3e131e2: Detect an indexer renumbering instead of silently syncing from the wrong place.
457
+
458
+ Sync cursors are indexer-assigned event sequence numbers, and nothing ties an id
459
+ to a block — `DustLedgerEvent` carries only `id`, `raw`, `maxId` and
460
+ `protocolVersion`. So when the same URL starts serving a differently-numbered
461
+ stream, a stored cursor names a different event and the sync resumes at the wrong
462
+ point without erroring. The only guard was a string comparison of `indexerUrl`,
463
+ which by definition cannot see a backend swap behind an unchanged name — and it
464
+ lived in the extension's background, so the CLI and TUI had no check at all.
465
+
466
+ This has already happened on preprod. The default indexer had a 22-wide hole in
467
+ its dust id space; the host now serving that name numbers contiguously, so cursors
468
+ written before the change sit 22 events too high. The pre-seed reference committed
469
+ for the extension stores dust cursor `1431375`, which under the current numbering
470
+ is 22 events beyond the state the snapshot holds — verified against the live
471
+ indexer, where that id now yields digest `3f3576deb45ad350` while the event the
472
+ reference actually stopped at yields `11c8cf9fd5a736f2`.
473
+
474
+ A cursor is now stored with a **witness**: the hash of the event found at that id.
475
+ On resume the id is re-read and compared. Same event means the numbering is
476
+ unchanged; a different event means it moved and the cursor is refused, failing
477
+ closed to a genesis sync — the direction ADR-0003 already establishes as always
478
+ safe.
479
+
480
+ A witness rather than one global indexer fingerprint, because a fingerprint has to
481
+ be sampled at a fixed id and any id below the point where two numberings diverge
482
+ returns the *same* event from both. Sampled at preprod's hole (989781), old and
483
+ new both return the event new calls 989781 — old's first existing id at or above
484
+ that probe was 989803, the same event — so a fingerprint there would have matched
485
+ across the exact cutover it existed to detect. The divergence point is not
486
+ knowable in advance; a witness has no such blind spot, because it asks only about
487
+ the id the cache actually depends on.
488
+
489
+ Three paths are gated: the warm read verifies before handing a reference to any
490
+ wallet, a build records witnesses for the cursors it stops at, and
491
+ `refreshEmptyRefCache` refuses to resume across a mismatch — resuming would carry
492
+ the old numbering forward into a reference that then looks freshly built, which
493
+ destroys the evidence.
494
+
495
+ Scope. Only shielded and dust are witnessed: they ride the global ledger-event
496
+ numbering and are the two the preprod change moved, where unshielded is keyed by
497
+ address. Per-wallet caches are not yet gated — normal sync persistence is written
498
+ by the SDK's own serialization rather than through `saveCachedState`, so covering
499
+ those needs a separate seam. A reference with no witness is treated as
500
+ unverifiable rather than invalid, so upgrading does not force a chain walk on
501
+ everyone at once; new references carry witnesses and the population converges.
502
+ - e31eaf8: Record a witness per cursor in exported references, and refuse a bundle without one.
503
+
504
+ A published reference records cursors that are indexer-assigned event sequence
505
+ numbers, so its correctness depends on an indexer that the bundle says nothing
506
+ about. That is how the preprod bundle stayed in use after the numbering underneath
507
+ it moved: the bytes were intact, the checksums matched, and the cursors had
508
+ quietly stopped naming the events they were written for.
509
+
510
+ `export-preseed.mjs` now reads the event at each cursor and records its hash in
511
+ the manifest under `witnesses`, alongside `height` and the per-part sizes. It
512
+ refuses to export at all if a cursor cannot be witnessed — including the case
513
+ where the indexer returns no event at or after the cursor, which means the
514
+ reference is *ahead* of the indexer it is being exported against and is itself the
515
+ renumbering signal.
516
+
517
+ The extension's installer requires them. A manifest without a witness for shielded
518
+ and dust is rejected, and the witnesses are written to the store before the height
519
+ — the height is what marks a reference usable, so a reference that reads as usable
520
+ without its witnesses is one that skips verification.
521
+
522
+ The asymmetry with local references is deliberate. A witnessless reference already
523
+ on disk is treated as unverifiable rather than invalid, because the alternative
524
+ forces every existing user into a chain walk on upgrade. A witnessless *bundle* is
525
+ an artefact we control and can re-cut, so refusing it costs one slower first sync
526
+ and trusting it costs a wallet silently resuming at the wrong event. The bundle
527
+ currently in the repository has no witnesses and will therefore no longer install;
528
+ a reference rebuilt from genesis against the current indexer replaces it.
529
+ - b49c96d: Move a pre-seed reference between machines: `moth preseed export` / `import`.
530
+
531
+ Building a reference IS the chain walk — tens of minutes, once per network per
532
+ machine. That cost is identical for everyone, because a reference holds public
533
+ chain state and nothing else, so it is work that should be done once and shared
534
+ rather than repeated by every developer who clones the repo. ADR 0005 called for
535
+ these two actions; the rest of the command shipped without them.
536
+
537
+ The on-disk shape is the one `scripts/export-preseed.mjs` already writes and CI
538
+ already publishes: gzipped state per sub-wallet plus a manifest. One format, so a
539
+ reference exported here can be dropped into the extension package, and one
540
+ downloaded from a release can be imported here.
541
+
542
+ `export` never writes the reference wallet's mnemonic. That is the only secret in
543
+ the arrangement — the state blobs are public chain data, but the mnemonic
544
+ controls the wallet they were built from, and a published reference is meant to
545
+ be safe to hand to strangers. The command says so in its own output.
546
+
547
+ `import` refuses rather than guesses. A bundle for another network would seed
548
+ wallets from a chain they have never been on, and the mismatch is silent
549
+ afterwards. A bundle older than what is already present is a downgrade that costs
550
+ catch-up time on every wallet created from then on; `--force` allows it, for
551
+ replacing a corrupt newer reference with a known-good older one.
552
+
553
+ Every part is decompressed before any part is written. Unpacking as it went left
554
+ the store holding new shielded and unshielded state beside an old dust state when
555
+ a later part turned out to be corrupt — a mixture that never existed on chain,
556
+ with a height key that still looked consistent. The height is written last, since
557
+ it is what marks a reference usable.
558
+ - c1c462e: **Reveal no longer offers a recovery phrase for accounts that do not have one.**
559
+
560
+ The first version accepted either choice and then answered with the seed for a
561
+ seed-restored account, because that account genuinely has no phrase — BIP-39's
562
+ phrase-to-seed step is one-way, so one cannot be worked back out. Correct
563
+ values, but it read as the selection being ignored: pick "recovery phrase", get
564
+ a hex seed, with the explanation arriving only after the password was entered.
565
+
566
+ The phrase option is now disabled for those accounts, with the reason shown
567
+ next to it, and the dialog opens on the seed instead.
568
+
569
+ To grey it out *before* asking for a password, the UI has to know which artifact
570
+ an account holds. `WalletInfo.backupKind` (`'mnemonic' | 'seed'`) records it:
571
+ `generate` and `import` write `'mnemonic'`, `importFromSeed` writes `'seed'`.
572
+
573
+ `undefined` on accounts written before the field, and deliberately not defaulted
574
+ — guessing `'mnemonic'` would tell a seed-imported account it has a phrase,
575
+ which is the bug this exists to prevent. Unknown leaves the option open and lets
576
+ the revealed value explain itself as before. **`unlock` backfills the field**,
577
+ since the keystore payload (`seed:<hex>` versus a mnemonic) is what distinguishes
578
+ them and the password is the only thing that reveals it — so an account someone
579
+ actually opens stops being unknown after one unlock. The write happens only when
580
+ the stored value is missing or wrong, alongside the address backfill already
581
+ there, so a normal unlock does not touch storage.
582
+ - 04f1aa4: **`undeployed` replaces `local` as the local devnet network.** Every interface now
583
+ offers it, including the extension, which could not select it before.
584
+
585
+ The two were duplicate presets for the same thing. `undeployed` is the id the
586
+ Midnight tooling, `docs/TESTING.md`, and this repo's own README instructions all
587
+ use for the local stack, and it points at the node port that stack listens on —
588
+ `9944`. `local` pointed at `9933`, which nothing in the documented stack serves,
589
+ so selecting **Local** in the extension connected to a closed port. It has been
590
+ that way since the first commit.
591
+
592
+ `local` was kept out of the extension's picker by a comment claiming the wallet
593
+ could not derive addresses for `undeployed`. That was false: `mn_addr_undeployed1…`,
594
+ `mn_dust_undeployed1…` and `mn_shield-addr_undeployed1…` have always derived, and
595
+ `address-parity`'s network loop stopping at `qanet` is why nothing contradicted it.
596
+ The loop now covers `undeployed` and `stagenet`, and a new test holds
597
+ `SUPPORTED_NETWORKS` equal to the keys of `DEFAULT_NETWORKS`, so a preset no
598
+ interface can reach — or an offered network with no preset — fails the suite.
599
+
600
+ **Breaking, with a migration.** `local` is gone from `SUPPORTED_NETWORKS` and
601
+ `DEFAULT_NETWORKS`, and the extension rejects it on save. Read paths resolve it via
602
+ `canonicalNetworkId`, exported from core: the extension's stored selection, a
603
+ wallet's meta record, the TUI's `lastNetwork`, `--network local`, and
604
+ `createMothBrowser({ network: 'local' })` all continue to work and land on
605
+ `undeployed`. Per-network birthdays and endpoint overrides move across with it, so
606
+ a migrated wallet keeps its pre-seed shortcut instead of resyncing from genesis.
607
+ Stored records are rewritten lazily, only when something else is already saving
608
+ them. `local` stays in `ALL_NETWORKS` so addresses already handed out still resolve.
609
+
610
+ Sync caches are keyed by network, so a migrated account resyncs once under the new
611
+ key — correct, since the node URL genuinely changes — and its old entries are left
612
+ behind rather than cleaned up.
613
+
614
+ Also fixed alongside: the four localhost indexer fallbacks pointed at
615
+ `http://localhost:8088` without the `/api/v4/graphql` path the indexer client posts
616
+ queries to, and the README's network table listed `devnet` as localhost, omitted
617
+ `undeployed`, and gave qanet hostnames (`rpc.qanet.dev.midnight.network`) that do
618
+ not resolve. Mainnet stays out of both the table and the `--network` reference:
619
+ the CLI refuses it and the extension keeps it out of the picker, so documenting
620
+ its endpoints only invites someone to try.
621
+
622
+ ### Patch Changes
623
+
624
+ - b49c96d: Bound the sync-engine teardown, and stop a wedged one from pinning everything
625
+ behind it.
626
+
627
+ `facade.stop()` closes the wallet SDK's submission service, which first awaits
628
+ the Polkadot client the facade was built with. That client is created with
629
+ `ApiPromise.create({throwOnConnect: false})`, so against a node that never
630
+ answers it neither resolves nor rejects — WsProvider simply keeps retrying. The
631
+ stop therefore had no failure path, and every caller inherited the hang. The
632
+ trigger is a node URL that does not answer, which is exactly the state a user is
633
+ in while editing one, Local network being the common case.
634
+
635
+ Three consequences, all fixed here:
636
+
637
+ - **Settings → Network's Save button spun for ever and discarded the edit.**
638
+ `saveNetworkConfig` awaited the stop before persisting, so the save neither
639
+ completed nor failed and the next attempt started from the same broken
640
+ endpoint. Settings are now written before the engine is touched and rolled back
641
+ if the switch fails, and a stop the offscreen document never answers closes that
642
+ document — Chrome destroys it without its cooperation, which is the only
643
+ teardown that reaches a wedged one.
644
+ - **The idle teardown never closed the offscreen document,** so an idle wallet
645
+ kept its worker and WASM heap alive instead of letting the service worker
646
+ suspend.
647
+ - **Locking never freed the worker holding key material,** because `lockNow`'s
648
+ forced teardown waited on the same stop.
649
+
650
+ `facade.stop()` is now raced against a 5s bound. The offscreen `syncStop` no
651
+ longer waits unboundedly on a start that may never finish, and still stops that
652
+ engine whenever it does come up, so an abandoned start cannot keep syncing behind
653
+ a new one.
654
+ - f736ebd: Settings → Network gains "Clear cache and resync".
655
+
656
+ A local network that goes down and comes back as a new chain from genesis
657
+ leaves the wallet holding state for a chain that no longer exists: the
658
+ account's serialized sync state, its pending submissions, and the network's
659
+ pre-seed reference that every fresh sync is seeded from. The engine cannot
660
+ tell, and the only ways out were switching indexer URLs or removing the
661
+ account. The new action, behind a confirmation, stops sync, drops all of that
662
+ for the active account on its network, clears the cached balance snapshot so
663
+ the loading screen shows, and starts syncing again from the start of the
664
+ chain. Nothing is spent.
665
+
666
+ Core gains `clearEmptyRefCache(networkId, store)` (`sync/preseed.ts`), which
667
+ removes the reference's state parts, height, cursor witnesses and mnemonic and
668
+ forgets the in-process memo — without the last, a worker that had already
669
+ verified the reference would keep handing the stale one out.
670
+ - 2dabc50: Make `moth config` usable, and add smoke coverage for the class of bug it was.
671
+
672
+ `config` declared an optional positional argument (`action`) ahead of a required
673
+ one (`key`). @oclif/core rejects that outright — with `action` absent, a single
674
+ value is ambiguous between an action and a key — so every invocation failed at
675
+ spec validation and the command body never ran. `action` is now required, which
676
+ changes no working behaviour because nothing worked.
677
+
678
+ Nothing caught this because no test invoked the command. Two probes now cover the
679
+ class:
680
+
681
+ - **Positional order, checked statically from source.** This is the one that
682
+ bites: reverting the fix produces `config: required "key" follows optional
683
+ "action"`.
684
+ - **A `--help` sweep over all 35 commands**, which catches a broken flag
685
+ definition, a bad example, or an import that throws on load.
686
+
687
+ Worth recording why it takes two. `--help` does not validate positional-argument
688
+ order: with the bad spec in place, `moth config --help` prints help perfectly
689
+ happily while bare `moth config` reports "Invalid argument spec". So the help
690
+ sweep would not have caught the bug that prompted it, and the order rule has to be
691
+ checked separately. Invoking every command bare would catch it, but would also
692
+ run them.
693
+ - 9afd580: Refuse mainnet at the `--network` flag, not in one of its consumers.
694
+
695
+ The refusal lived inside `BaseCommand.getNetworkConfig`, and twelve commands never
696
+ call it — including both that create wallets. `moth wallet generate --network
697
+ mainnet` derived mainnet addresses, wrote a keystore, printed a recovery phrase
698
+ and exited 0, with no warning shown. A guard in one consumer is not a guard; it is
699
+ a convention that holds wherever someone remembered it.
700
+
701
+ It now hangs off the `--network` flag that every command inherits through
702
+ `baseFlags`, so no command can take a network id without it. `getNetworkConfig`
703
+ keeps the check as defence in depth, for an id arriving from stored config or from
704
+ a caller assembling flags itself, and both now route through one
705
+ `assertNotMainnet`.
706
+
707
+ Verified across the paths the issue did not cover: `wallet generate`, `wallet
708
+ import`, `wallet use` and `tui` all now print the warning and exit 1 without
709
+ writing a keystore, while `--network preprod` is untouched.
710
+
711
+ Also guards `config set default-network mainnet`, which is the second way a
712
+ network id enters the CLI — `WalletManager` falls back to `config.defaultNetwork`
713
+ for a wallet with no network of its own, so a stored value reaches the same code
714
+ paths without `--network` ever being used. Note that path is currently unreachable
715
+ for an unrelated reason: `moth config` declares an optional argument before a
716
+ required one, which oclif rejects, so every invocation of that command fails
717
+ before it runs. Filed separately.
718
+ - b49c96d: Read the birthday back, so a CLI or TUI wallet can actually pre-seed.
719
+
720
+ The birthday was written and never read. `startWalletSync`'s pre-seed gate is
721
+ `(isNewWallet || birthday)`, and no CLI command passed either — eleven of them
722
+ stopped at `walletName`, and the TUI hook passed `isNewWallet` but no birthday,
723
+ so the guard `emptyRef.height <= birthday` could never be reached. The effect was
724
+ silent: `moth balance -n preprod -v` showed no pre-seed line at all and dust began
725
+ at 0%, with the reference sitting unused.
726
+
727
+ Every sync call site now passes it, resolved through a new
728
+ `WalletManager.birthdayOn(name, networkId)`. Per network on purpose: `list()`
729
+ resolves against the wallet's own `meta.network`, so a sync driven by `--network`
730
+ was reading a height belonging to a different chain, or nothing at all. It never
731
+ throws — a wallet with no meta asserts nothing, and "no claim" means scan from
732
+ genesis, which is slow but never wrong.
733
+
734
+ Guarded by a test that walks the AST of every `startWalletSync` call in the CLI
735
+ and TUI and fails any that omits the birthday, since nothing else would notice
736
+ this regressing. Verified by deliberately dropping the argument.
737
+ - c2f8b73: Re-cut the pre-seed bundles from genesis, and add one for qanet.
738
+
739
+ The preprod bundle recorded dust cursor `1431375`, written under the indexer's old
740
+ numbering. Under the numbering now served, that id names an event 22 positions
741
+ later than the state the snapshot holds, so every wallet seeded from it resumed
742
+ past 22 dust events — no error, just missing generation history (#40).
743
+
744
+ All three references were rebuilt from genesis rather than refreshed, because a
745
+ refresh resumes from the stored cursor and would have carried the old numbering
746
+ forward into a bundle that then looked freshly built:
747
+
748
+ | Network | Height | Build | Dust cursor |
749
+ | --- | --- | --- | --- |
750
+ | preprod | 2,203,416 | 55 min | 1,449,958 (was 1,431,375) |
751
+ | preview | 519,470 | 3 min | 141,062 |
752
+ | qanet | 2,314,786 | 14 min | 346,693 |
753
+
754
+ Each manifest now carries a witness per cursor, so a consumer can tell whether the
755
+ numbering it was written under still holds — these are the first bundles that can
756
+ be verified rather than trusted, and the first that the installer will accept.
757
+
758
+ qanet ships for the first time. It costs 140 KB, not the several megabytes preprod
759
+ does: its chain is longer but has far fewer dust events, and dust is what makes a
760
+ reference large. The control that offers on-device warming probes which networks
761
+ ship a reference rather than listing them, so no code changed to add it.
762
+ - b49c96d: Move the pre-seed commands from `moth dust preseed` to `moth preseed`.
763
+
764
+ DUST is why the pre-seed matters — the 4.9 MB blob, the ~1.4M events, the tens of
765
+ minutes, where shielded and unshielded take seconds — which is what put it under
766
+ `dust`. But that describes the motivation, not the thing: the pre-seed writes all
767
+ three sub-wallet caches, and a reference is per-network machine state in `~/.moth`
768
+ shared by every wallet there, whereas `moth dust` groups per-wallet token
769
+ operations. A command tree should say what a thing is, and someone whose first
770
+ sync is crawling searches for "preseed" rather than reasoning their way to DUST.
771
+
772
+ Settled while the command had not shipped, so the rename costs no compatibility.
773
+
774
+ Each action is now a real subcommand — `preseed status|import|refresh|build|export`
775
+ — instead of one command taking an action argument. `--timeout` therefore belongs
776
+ to `build` and `--force` to `import`, rather than every flag hanging off the group
777
+ with "(build only)" in its description, and each gets its own `--help`. The group
778
+ carries an oclif topic description; without one the help listed the whole group
779
+ under whichever subcommand sorted first.
780
+ - b49c96d: Require all three parts of a pre-seed reference, and drop `node:zlib` from core.
781
+
782
+ Two findings from review on the CLI/TUI parity work.
783
+
784
+ **A missing part was as damaging as a corrupt one.** Both `exportReference` and
785
+ `importReference` checked only dust, so a bundle without shielded state imported
786
+ the other two over an existing reference and moved the height key with them. The
787
+ store was left holding shielded at the old height and the rest at the new one — a
788
+ mixture that never existed on chain, reported as ready by
789
+ `loadUsableRefStates` because the height key still looked consistent, and that
790
+ inflated height then feeding the `emptyRef.height <= birthday` guard, seeding
791
+ wallets whose birthday fell between the two. Both functions now require every
792
+ part: export returns null, import refuses and names each missing file.
793
+
794
+ **`node:zlib` had no business in core.** Nothing in the browser or extension
795
+ packages imported `preseed-portable` yet, but it is re-exported from core's
796
+ barrel — and that barrel reaches 36 Node builtins where the browser package's
797
+ walked graph reaches none, so one careless import would have carried zlib into
798
+ every dependent DApp bundle. Compression now goes through `CompressionStream` and
799
+ `DecompressionStream`, which Node 18+ and every current browser provide, so the
800
+ module is genuinely isomorphic rather than allow-listed as an exception. Gzip
801
+ level is not selectable through that API, so bundles written here compress
802
+ slightly less than `scripts/export-preseed.mjs` does at level 9; sizes are
803
+ recorded in the manifest either way, and decompression is level-agnostic.
804
+ - 9be5669: Show what a transfer can actually spend.
805
+
806
+ A synced wallet reported 500 NIGHT and refused a 10 NIGHT transfer with
807
+ `Insufficient funds`. Both figures were true and neither was reconcilable from
808
+ outside the wallet.
809
+
810
+ The displayed balance counts coins reserved by transactions in flight, and does
811
+ so deliberately — dropping them flashes the balance to zero mid-send. But the SDK
812
+ spends from `availableUtxos` alone, so the number shown was never the number that
813
+ could be spent, and nothing surfaced the difference.
814
+
815
+ `moth balance` now prints the split when anything is reserved, and stays quiet
816
+ otherwise:
817
+
818
+ ```
819
+ NIGHT:
820
+ unshielded: 500.000000 (500000000 STARS)
821
+ available: 0.000000 ← what a transfer can use
822
+ reserved: 500.000000 (a transaction in flight holds these)
823
+ ```
824
+
825
+ JSON gains `unshieldedAvailable` and `unshieldedReserved` beside the existing
826
+ fields, so nothing reading it today breaks. The transfer's insufficient-funds
827
+ path names the number that blocked it, and says nothing when a reservation was
828
+ not the cause.
829
+
830
+ Nothing new is computed — `WalletBalances.coins` already carried the split.
831
+
832
+ This makes the state visible; it does not stop reservations outliving their
833
+ transactions. A wallet already in that state still needs its sync cache cleared.
834
+ - ea1793d: Report sync progress that is neither invented nor erased.
835
+
836
+ Two defects in how progress reached the surfaces, close enough together in
837
+ `extractBalancesPartial` that fixing them apart would mean resolving the same
838
+ twenty lines twice.
839
+
840
+ **A partial emission erased a sub-wallet.** Each sub-wallet's coins and its
841
+ progress were read inside a single `try`, and the coin loops reached into the
842
+ state without the optional chaining used one line above on the balances:
843
+
844
+ ```ts
845
+ const sb = state.shielded?.balances; // guarded
846
+ for (const c of state.shielded.availableCoins) { // not guarded — throws here
847
+ …
848
+ subProgress.shielded = {applied, total}; // never reached
849
+ ```
850
+
851
+ An emission carrying no slice for a part threw in the loop and skipped the
852
+ progress assignment, leaving `{applied: 0, total: 0}` — which `fraction()` treats
853
+ as **complete**, correctly for a sub-wallet with genuinely nothing to apply and
854
+ catastrophically for one whose slice was simply absent. The TUI alternated about
855
+ once a second between real figures and `synced · 0 / 0` with no balance. Coins
856
+ and progress now read separately, all six coin loops are guarded, and each part
857
+ carries its previous value forward when an emission says nothing about it;
858
+ progress does not go backwards inside a session. A genuinely 0/0 part still
859
+ counts as complete rather than stalling the overall figure.
860
+
861
+ **The ETA assumed every sync starts at zero.** `etaSeconds` was
862
+ `elapsedMs / percentage - elapsedMs`, which treats cumulative progress as this
863
+ session's work. Dust resumes from cache constantly, so a run that restored at
864
+ ~65% and then ran 152s was read as "67% in 152s" — fifteen times the real rate.
865
+ Measured on preprod it promised 1m15s at 67% and 2m23s at 81% against a true
866
+ ~10m, and the estimate *climbed* as elapsed time corrected the fiction. The rate
867
+ now comes from a per-session baseline: the same inputs give 41m and 12m19s,
868
+ falling as the run proceeds. Below 0.2 points of movement it reports nothing,
869
+ since an admitted unknown beats a number derived from noise.
870
+
871
+ Both bugs predate the CLI/TUI parity work, which only made the first visible by
872
+ putting per-sub-wallet counters on screen. The daemon and extension read the same
873
+ balances.
874
+ - 316ca82: Document `moth transfer` as it actually works.
875
+
876
+ The README showed `moth transfer <amount> NIGHT --to <addr>` on two rows. That form
877
+ does not parse — `transfer` declares one positional and rejects the second with
878
+ `Unexpected argument: NIGHT`. It was the documented invocation, so it was the first
879
+ thing a new user would type.
880
+
881
+ Corrected to `moth transfer [<amount>] [--to <addr>]`, and the rows now say what
882
+ was previously stated nowhere: the in-process command is NIGHT-only, with the
883
+ token hardcoded and no flag to change it. A row for `moth daemon transfer` covers
884
+ the path that *can* move other tokens, including the distinction between
885
+ `--amount` (raw smallest units, any token) and `--night` (a decimal converted at
886
+ 10⁶ STARS, refused for anything but NIGHT).
887
+
888
+ Docs only. Whether the in-process command should grow token selection is the open
889
+ half of #62.
890
+ - 89f34aa: Stop the sync before freeing the keys when the TUI quits.
891
+
892
+ Quitting printed a wall of WASM errors over the exiting terminal, once per live
893
+ sync:
894
+
895
+ ```
896
+ Wallet.Other: Error while applying sync update
897
+ cause: Error: Dust secret key was cleared
898
+ at DustLocalState.replayEventsWithChanges
899
+ ```
900
+
901
+ The quit handler called `lockAll()` and then `exit()`, zeroing the
902
+ `DustSecretKey` in the WASM heap while the dust sync was still mid-batch. The
903
+ only `stop()` lived in an unmount cleanup, ran after `exit()`, and was
904
+ fire-and-forget, so the sync's next batch reached for a key that no longer
905
+ existed.
906
+
907
+ `useBalance` now exposes an awaited `stop()` — unsubscribe, await the facade's
908
+ stop, bounded by a timeout so a sync that will not settle cannot keep the TUI
909
+ open — and both quit paths await it before locking.
910
+
911
+ Nothing was corrupted: the wallet was exiting and its state was already
912
+ persisted. It simply looked like a crash every time, and would have buried a real
913
+ error.
914
+
915
+ The non-quit unmount path (Ctrl-C, a crash, the process ending) now stops before
916
+ locking as well, which narrows the window rather than closing it — a React
917
+ cleanup cannot await, so a batch already inside the WASM call can still find the
918
+ key gone.
919
+
920
+ ## 0.12.1
921
+
922
+ ## 0.12.0
923
+
924
+ ### Minor Changes
925
+
926
+ - ea15676: Coordinate the first public package release under the Moth package names.
927
+
928
+ This is a minor bump because no package has previously been published under the
929
+ new names. The release remains experimental and unsupported, so it stays below
930
+ 1.0.0. Package names, CLI identifiers, environment variables, connector identity,
931
+ and local state paths are aligned before publication; no compatibility shim is
932
+ required for an unpublished package.
933
+
934
+ ### Patch Changes
935
+
936
+ - be98f55: Measure test coverage in CI, and share the core test fixtures.
937
+
938
+ Adds a second job to `.github/workflows/ci.yml` that runs `yarn test:coverage`
939
+ and archives `lcov.info`. The measurement step is advisory: V8 instrumentation
940
+ slows the scrypt-backed keystore and seed-export suites enough to trip vitest's
941
+ internal worker RPC timeout, so the run exits non-zero even though every test
942
+ passes and the report is written. `continue-on-error` is scoped to that step
943
+ rather than the job, so a run that produces no report at all still fails on the
944
+ upload. The strict gate stays the `test` job. Baseline at the time of writing:
945
+ 62.54% lines, 83.76% branch over 686 tests, with entrypoints and screens excluded
946
+ as shells the E2E tier covers.
947
+
948
+ The three `exportSeedHex` tests no longer carry a per-test 15s timeout. A
949
+ literal there overrides the project config — which already raises the timeout
950
+ precisely because these derive keys at the v2 scrypt parameters — and made them
951
+ the only thing failing a `--coverage` run.
952
+
953
+ Shared test fixtures now live in `packages/core/tests/helpers/`. Every core test
954
+ that needs an in-memory `StorageAdapter` or the reference mnemonic takes it from
955
+ there — four hand-rolled `MemoryStorage` classes and four copies of the mnemonic
956
+ are gone, so a change to `StorageAdapter` now breaks compilation at every call
957
+ site, and no test can drift from the seed the address-parity fixtures pin. The
958
+ keystore suite shares one encrypted keystore across its shape and tamper
959
+ assertions while keeping an independent full-strength round trip, cutting its
960
+ scrypt derivations from thirteen to nine.
961
+
962
+ The extension's network picker test now asserts that every network in
963
+ `SUPPORTED_NETWORKS` is both named and described in the rendered markup, with
964
+ developer mode on so the gated mainnet is covered too. The radio count derives
965
+ from the state's `available` list rather than any literal, so gating a second
966
+ network cannot fail a correct picker; a hardcoded count disagreed with the
967
+ picker for twelve days after `local` was added without label entries.
968
+ `NETWORK_LABELS` and `NETWORK_DESCRIPTIONS` are exported for that assertion.
969
+
970
+ Root vitest config: `projects` is now globbed rather than listed, so a new
971
+ package cannot be gated by CI while staying invisible to the coverage number,
972
+ and `coverage.exclude` extends vitest's defaults rather than replacing them.
973
+
974
+ Removes the root `lint` script and turbo's `lint` task. No package defined a
975
+ `lint` script, so `turbo run lint` linted nothing and reported success, which
976
+ read as a passing gate — worse than having none. `CONTRIBUTING.md` no longer
977
+ claims a linter config lives in the repo.
978
+
979
+ The root `vitest` range now matches `@vitest/coverage-v8`, which declares an
980
+ exact peer on the vitest it ships with, so the two cannot drift onto an
981
+ unsupported pairing.
982
+
983
+ ## 0.2.0
984
+
985
+ ### Minor Changes
986
+
987
+ - b157dd2: Add the browser extension wallet and dApp connector workflow.
988
+
989
+ The wallet core now supports browser-safe persisted sync state, activity history,
990
+ message signing, staged transfer construction and submission, fee estimation,
991
+ and DUST and intent operations. Add the WXT side-panel extension and Connector
992
+ Lab mock dApp, expose the new APIs through the browser adapter, and keep the CLI
993
+ and TUI integrations compatible with asynchronous sync cache handling and the
994
+ Ink runtime.
995
+
996
+ - 36cb067: Add selectable proof-server and local WASM proving across the core wallet, CLI,
997
+ terminal dashboard, browser extension, and dApp connector API.
998
+ - fc93b31: Add the opt-in daemon for sharing one live wallet between a long-running host
999
+ (the TUI dashboard or `moth daemon serve`) and CLI clients over an authenticated
1000
+ Unix-socket or TCP RPC. The daemon owns the sync engine and spending keys;
1001
+ clients route the build/balance/prove/sign/submit pipeline through it, so keys
1002
+ never leave the host process. Adds the `moth daemon serve|transfer|call|deploy|
1003
+ submit-tx|dust register|dust deregister|key gen|key list|key revoke|maintenance`
1004
+ subcommands, API-key authentication with read/write scopes, an append-only audit
1005
+ log, and an L3 confirmation queue (interactive modal, or headless auto-approve
1006
+ gated behind an explicit flag + env var).
1007
+ - fc93b31: Adopt a single derive-and-drop key model. `deriveWalletKeys(seedHex)` produces a
1008
+ typed `WalletKeys` bundle once at unlock and the raw BIP-39 seed is dropped;
1009
+ `UnlockedWallet` no longer exposes it. Every Midnight write path accepts
1010
+ `WalletKeys` directly (`sendTokensWithKeys`, `designateForDustWithKeys`,
1011
+ `dedesignateFromDustWithKeys`, and the contract call/deploy/maintenance paths),
1012
+ with the seed-based functions kept as thin wrappers so both API shapes remain
1013
+ available. `WalletManager.exportSeedHex` is the one deliberate exception, for a
1014
+ key-holder that must re-supply a serializable secret across a process/document
1015
+ boundary (the extension's offscreen document, where WASM keys can't cross the
1016
+ message channel). See docs/spec/wallet-service/05-key-management.md (D-KM-3).
1017
+ - fc93b31: Report the NIGHT actually registered for DUST generation.
1018
+
1019
+ `DustGeneration` gains `registeredNight` — the sum of the NIGHT UTXOs flagged
1020
+ as registered. Registration binds the NIGHT key, but each UTXO generates via
1021
+ its own on-chain record, so three amounts can differ: the balance, the
1022
+ registered NIGHT, and the NIGHT generating right now (`designated`). The
1023
+ extension's DUST screen now attributes "Total possible" to the generating
1024
+ amount instead of the whole balance, quantifies NIGHT that is registered but
1025
+ not yet generating, and offers registration whenever unregistered NIGHT
1026
+ exists (previously the CTA vanished after the first registration).
1027
+
1028
+ `DustGeneration` also gains `newestRegisteredAt`, and `clearDustSyncCache`
1029
+ evicts only the dust sub-wallet's cached state. Together they power the
1030
+ extension's automatic dust-view repair: when registered NIGHT older than the
1031
+ grace period still has no generation records, the sync host rebuilds just the
1032
+ dust view (transaction-free, cooldown-guarded) instead of requiring a manual
1033
+ deregister + full resync.
1034
+
1035
+ `designateForDust` now rejects an invalid DUST receiver instead of silently
1036
+ falling back to the wallet's own address, `dedesignateFromDust` is exported
1037
+ through the browser adapter, and the extension's DUST screen gains a "Stop
1038
+ generating" flow plus a receiver-address field on registration (prefilled with
1039
+ the wallet's own DUST address; any valid DUST address is accepted).
1040
+
1041
+ - 1c597ff: Add an optional auth header for the node, for endpoints that rate-limit.
1042
+
1043
+ preprod's node answers 403 to unauthenticated callers and accepts requests
1044
+ carrying an operator-issued bypass header. Settings → Network gains two fields
1045
+ under the endpoint URLs: a header name (pre-filled with
1046
+ `x-shielded-ratelimit-bypass`, editable so a different operator or a renamed
1047
+ header needs no code change) and a masked value.
1048
+
1049
+ **This needs `declarativeNetRequestWithHostAccess`, and that is not incidental.**
1050
+ A browser cannot set headers on a WebSocket handshake — `new WebSocket(url,
1051
+ protocols)` takes no header argument — and the node connection is a WebSocket, so
1052
+ no JavaScript reaches it. declarativeNetRequest's `modifyHeaders` action does,
1053
+ because it rewrites the request before it leaves the browser and its resource
1054
+ types include `websocket`. The `WithHostAccess` variant reuses the
1055
+ `https://*.midnight.network/*` host permission already declared rather than
1056
+ widening access; it grants header rewriting on those hosts and nothing else.
1057
+
1058
+ Scoped to the node host only. The indexer is not rate-limited today, and a
1059
+ credential should reach as few destinations as possible. The rule is dynamic
1060
+ rather than a static ruleset — a static one would ship in the package — and is
1061
+ removed when the header is cleared or the node URL changes, so a credential is
1062
+ never left pointing at a host the wallet no longer uses.
1063
+
1064
+ The value is a secret and is treated as one: masked in the UI, never logged,
1065
+ excluded from developer mode (which shows endpoint, HTTP status and attempt
1066
+ count), and excluded from the diagnostics export, which promises "labels,
1067
+ durations and sizes only". It ships with an empty value — only the header _name_
1068
+ has a default — so no credential can enter the package.
1069
+
1070
+ Worth stating plainly: it is stored in `storage.local`, **not** in the encrypted
1071
+ keystore, so unlike seeds it is not protected by the wallet passphrase and is
1072
+ readable by anything with access to the browser profile. The help text says so.
1073
+
1074
+ Both the name and value are re-validated at the background boundary rather than
1075
+ trusted from the panel: names must match the RFC 7230 token characters and values
1076
+ must contain no CR or LF, so neither can smuggle a second header. Surrounding
1077
+ whitespace is trimmed, since pasting a token reliably introduces it.
1078
+
1079
+ Two related fixes this exposed. `endpointOverridesFor` collapsed to `null`
1080
+ whenever the URLs matched the preset, which would have silently discarded a
1081
+ header set against default endpoints — the common case, since the endpoint
1082
+ needing the header is the preset one. And `getNetworkConfig` did not carry the
1083
+ header through, so nothing downstream could see it.
1084
+
1085
+ - 24cb16c: Keep each network's synced state and birthday, so switching networks stops costing a full rescan.
1086
+
1087
+ Switching a wallet between networks used to be doubly expensive, and the second
1088
+ cost was the larger one.
1089
+
1090
+ `setNetwork` discarded the wallet's `birthday` on every move. That is defensible
1091
+ in isolation — a height on preprod means nothing on preview — but `syncEnsure`
1092
+ passes `isNewWallet: false`, so the pre-seed block is entered only when a birthday
1093
+ exists. A switched wallet therefore had no birthday anywhere, could never satisfy
1094
+ `reference.height <= birthday`, and walked from genesis on every network it
1095
+ touched, no matter how many references were built or shipped.
1096
+
1097
+ `walletSetNetwork` also cleared the sync cache for BOTH sides of the move, on the
1098
+ grounds that "switching back must also perform a fresh scan rather than revive
1099
+ state the user explicitly reset" — which conflated switching networks with
1100
+ resetting sync state. Resetting is its own deliberate action on the DUST screen.
1101
+ The keys were already per-network (`sync/<networkId>/<wallet>/<part>.dat`), so
1102
+ the wipe was a choice, not a constraint, and it grew expensive as dust came to
1103
+ dominate: a return trip meant 78.6 min on preprod.
1104
+
1105
+ Birthdays are now per network — `birthdays: Record<string, number>` — recorded on
1106
+ first arrival and never overwritten on return, because a wallet may have
1107
+ transacted on a network before leaving and a later tip would skip that history.
1108
+ The legacy single value is folded into the map under the network it belonged to
1109
+ rather than dropped.
1110
+
1111
+ Recording is gated on a new explicit `createdHere` flag. An imported wallet may
1112
+ hold funds on any chain at any height, so it never gets a birthday and keeps
1113
+ scanning from genesis. That distinction used to be implied by `birthday` being
1114
+ present at all (generate set it, import did not); splitting birthdays per network
1115
+ dissolved that signal, so it is now stored outright. Wallets written before the
1116
+ field exists read as imported — the conservative direction, since a slow sync
1117
+ costs time whereas the opposite error hides funds.
1118
+
1119
+ Sync caches are no longer cleared on a network switch. Both networks' state
1120
+ coexists and a return trip resumes from the last known good state, exactly as an
1121
+ ordinary restart does. Indexer changes still clear, since a different indexer can
1122
+ disagree about history.
1123
+
1124
+ The Settings copy said "Changing the network or indexer clears local sync data";
1125
+ that is now only true of the indexer, and the switch dialog no longer warns about
1126
+ losing state it keeps.
1127
+
1128
+ - 1f69f66: Add `refreshEmptyRefCache` — sync an existing reference forward instead of rebuilding it.
1129
+
1130
+ `ensureEmptyRefCache` short-circuits on a warm reference, so updating one appeared
1131
+ to require deleting it and walking the chain again: 53.6 min on preprod. That was
1132
+ never a limitation of the mechanism, only of the entry points.
1133
+ `buildEmptyRefCache` already resumed from whatever reference state the store held
1134
+ — it syncs under `EMPTY_REF_WALLET`, and `startWalletSync` restores that wallet's
1135
+ cache like any other. The only thing in the way was the early return.
1136
+
1137
+ `refreshEmptyRefCache` bypasses it, sharing the same in-flight dedup so a refresh
1138
+ cannot start a second chain walk beside a build already running.
1139
+
1140
+ Measured on preview: **9.1s to advance 25,660 blocks to zero stale**, against 96s
1141
+ to rebuild the same reference from genesis — and against 53.6 min for preprod's,
1142
+ which is the rebuild this would have avoided.
1143
+
1144
+ A stale reference is safe to use; it only means more catch-up for the wallets it
1145
+ seeds, at roughly half a second per hour of age. So this is an optimisation rather
1146
+ than a repair — run it before cutting a release, or on a schedule.
1147
+
1148
+ ADR 0004 was written on the premise that no refresh path existed and made building
1149
+ one the precondition for the CI and distribution work. That premise is corrected
1150
+ there, along with everything it gated.
1151
+
1152
+ - fc93b31: Add account display labels and local token names.
1153
+
1154
+ `WalletManager.setLabel` stores a user-chosen display label in the wallet's
1155
+ metadata (surfaced on `WalletInfo`/`UnlockedWallet`); the storage name stays the
1156
+ immutable key, so keystores, sync caches and sessions survive renames. The
1157
+ extension exposes this as "Rename account" and also lets shielded token rows be
1158
+ given local display names.
1159
+
1160
+ - 6766583: Make the phase-timings recorder storage-agnostic, so the CLI, TUI and daemon can use it.
1161
+
1162
+ The recorder was extension-only by construction: it imported `wxt/browser` and
1163
+ wrote to `storage.local`, so the three Node surfaces had no equivalent even though
1164
+ they run the same core sync engine and emit the same progress stream.
1165
+
1166
+ The arithmetic and the policy are identical everywhere — delta from the previous
1167
+ entry, a bounded history, an enabled cache so the hot path does not hit storage,
1168
+ and a never-throw guarantee so an instrument can never break the path it measures.
1169
+ Only persistence differs. So persistence is now a four-method `TimingStore`
1170
+ interface and everything else lives in `diagnostics/timings.ts`, isomorphic and
1171
+ dependency-free: no node builtins, no extension APIs, no WASM. Same split as
1172
+ `storage/adapter.ts` versus `storage/fs-adapter.ts`.
1173
+
1174
+ Three stores ship: `FilesystemTimingStore` (`~/.moth/timings.json`, for CLI/TUI/
1175
+ daemon), the extension's `storage.local` adapter, and an in-memory one for tests
1176
+ and short-lived processes. Because the shape is shared, a timeline captured from
1177
+ the CLI is directly comparable with one from the panel.
1178
+
1179
+ Two behaviours are deliberate and pinned by tests. A store that cannot answer is
1180
+ treated as disabled rather than raising, since recording is the optional
1181
+ behaviour and the failure mode must be "no data", never "broken wallet". But
1182
+ `setEnabled` and `clear` still propagate: those are deliberate user actions with
1183
+ UI behind them, and silently doing nothing would leave a toggle lying about its
1184
+ own state. `clear()` drops entries while keeping the enabled flag, which is how
1185
+ one phase gets isolated mid-session without losing what follows.
1186
+
1187
+ The extension's public API is unchanged — `record`, `getTimings`, `clearTimings`,
1188
+ `setTimingsEnabled`, `timingsEnabled`, `MAX_ENTRIES` and `TimingEntry` all keep
1189
+ their names and behaviour, so no call site moved.
1190
+
1191
+ docs/BENCHMARKING.md gains a CLI/TUI/daemon section, documents
1192
+ `dust-proving-check.mjs` alongside the other two instruments, corrects the setup
1193
+ command (the repo pins yarn; corepack rejects npm), notes that a slow `submitting`
1194
+ row is usually the relay backoff rather than the wallet, and flags that
1195
+ `sync-benchmark --json`'s `percentage` field changed meaning when progress moved
1196
+ to the slowest sub-wallet.
1197
+
1198
+ ### Patch Changes
1199
+
1200
+ - fc93b31: Expose the outgoing transfer count on activity entries.
1201
+
1202
+ `ActivityEntry` gains `outputs` — the number of external destination outputs a
1203
+ (possibly batched) transaction carried, counted from the external unshielded
1204
+ created UTxOs the wallet can see. This lets the UI represent a multi-transfer
1205
+ send as one entry that reads "3 transfers" instead of silently showing only the
1206
+ first output. Shielded recipients are unknowable (their notes can't be
1207
+ decrypted), so a shielded-only batch reports 0 and the UI falls back to the
1208
+ count of distinct tokens moved.
1209
+
1210
+ - fc93b31: Count booked (pending) unshielded inputs in the reported balance.
1211
+
1212
+ A send or DUST registration reserves its own NIGHT UTxOs (moved from available
1213
+ to pending) while the transaction is in flight, settling them back to the
1214
+ wallet on apply. The balance previously counted only available coins, so a
1215
+ full-balance registration flashed the displayed balance down to zero until the
1216
+ transaction applied. Unshielded pending holds only these booked inputs — never
1217
+ incoming coins — so folding them into the balance is safe and never
1218
+ over-counts receipts. Shielded is unchanged (its pending includes incoming).
1219
+
1220
+ - 12881a3: Make sync-benchmark actually measure the pre-seeded path.
1221
+
1222
+ The script could not measure the thing it exists to measure, and said nothing
1223
+ about it. Two independent faults, either of which was sufficient:
1224
+
1225
+ - The measured wallet was given a bare `InMemorySyncStateStore`, but
1226
+ `ensureEmptyRefCache` looks for the reference in whatever store the wallet was
1227
+ given. So it searched an empty store, found nothing, and measured the unseeded
1228
+ path. A run in the same process as `--warm-reference` only found the reference
1229
+ through the module-level `refCache`, never from disk.
1230
+ - The birthday was read before warming. Warming takes minutes to an hour and the
1231
+ chain moves under it, so the birthday came out older than the reference the run
1232
+ had just built, `reference.height <= birthday` failed, and the guard correctly
1233
+ refused to seed — while the run announced "reference ready … should start at
1234
+ tip".
1235
+
1236
+ Both are fixed. The birthday is now read after any warm, and the measured wallet
1237
+ gets an overlay store that reads the reference keys through to disk while keeping
1238
+ every write in memory — so a run still never leaves wallet state in `~/.moth`,
1239
+ and never mutates the reference it is measuring against.
1240
+
1241
+ Verified on preview: 94.5s unseeded, **2.2s** seeded, 96.0s to build the
1242
+ reference. Previously both modes reported 94.5s.
1243
+
1244
+ `emptyRefHeightKey` is now exported from the core barrel; the overlay needs it to
1245
+ know which keys belong to the reference.
1246
+
1247
+ This also means any previously recorded "warm reference" figure came from a run
1248
+ that measured the unseeded path, and should be re-measured rather than trusted.
1249
+ docs/BENCHMARKING.md now says so, documents the `Pre-seed complete` line to check
1250
+ for, and records the preview numbers.
1251
+
1252
+ - c7d1ef7: Apply the SDK console-noise filter in the browser, not just in Node.
1253
+
1254
+ `SDK_NOISE` has always listed the strings the wallet SDK and @polkadot emit on
1255
+ every reconnect — `API-WS`, `disconnected from`, `Abnormal Closure`,
1256
+ `RPC-CORE`, `subscribeRuntimeVersion`. But the installer bailed out when
1257
+ `process` was undefined, so the CLI, TUI and daemon got a clean console while the
1258
+ extension — the surface most users actually see — got all of it.
1259
+
1260
+ The console patch now runs everywhere. The stdout/stderr interception stays
1261
+ Node-only, since @polkadot's logger writes directly to those streams there and a
1262
+ browser worker has no equivalent; so does the `unhandledRejection` handler,
1263
+ deliberately, because a swallowed rejection in the worker would hide failures the
1264
+ extension has no other channel to report.
1265
+
1266
+ The curated list is unchanged, and the rule behind it still holds: specific
1267
+ strings only, never broad patterns that could swallow a security-relevant error.
1268
+
1269
+ One thing this cannot reach: `WebSocket connection to '…' failed: 403`. Chrome
1270
+ emits that from its network stack rather than from JavaScript, so no console
1271
+ patch touches it. That is the right outcome — it is not benign. It means the node
1272
+ is genuinely unreachable, and the relay banner now says so in the UI while the
1273
+ backoff keeps the repetition to roughly one line a minute instead of twenty-four.
1274
+
1275
+ - 6f914fc: Clear nine Dependabot advisories in transitive build dependencies.
1276
+
1277
+ All ten open alerts are transitive, and every one arrives through build or dev
1278
+ tooling — `web-ext` (via WXT's Firefox support), `changesets`, `vite`,
1279
+ `node-notifier`. None is reachable from the shipped extension bundle, checked
1280
+ against `.output/chrome-mv3`. That lowers the severity in practice but does not
1281
+ make them worth leaving.
1282
+
1283
+ Resolved via `resolutions`, which this repo already uses for the same purpose:
1284
+
1285
+ | package | was | now | severity |
1286
+ | --------------- | ------ | ------ | -------- |
1287
+ | shell-quote | 1.7.3 | 1.10.0 | critical |
1288
+ | adm-zip | 0.5.18 | 0.6.0 | high |
1289
+ | brace-expansion | 5.0.7 | 5.0.9 | high |
1290
+ | js-yaml | 4.3.0 | 4.3.1 | high |
1291
+ | tmp | 0.2.5 | 0.2.7 | high |
1292
+ | uuid | 8.3.2 | 11.1.1 | medium |
1293
+ | esbuild | — | 0.28.2 | low |
1294
+
1295
+ Pinned to the lowest patched major rather than `>=`. An open-ended range let
1296
+ yarn take js-yaml 5.2.3 and uuid 14.0.1 — several majors beyond what the
1297
+ advisories require, and a far larger change than the fix warrants.
1298
+
1299
+ `elliptic` (low, via `browserify-sign`) has no patched version published and is
1300
+ left open. It is a transitive dependency of a build-time crypto shim, not of the
1301
+ wallet's own cryptography, which runs through the Midnight ledger WASM.
1302
+
1303
+ - 771338d: Tell the user when registration needs time, instead of failing and blaming the
1304
+ proof server.
1305
+
1306
+ Registering NIGHT for DUST generation is self-funding: a `DustRegistration`
1307
+ carries `allow_fee_payment`, and the ledger lets the transaction pay its own fee
1308
+ from the DUST its NIGHT _would have_ generated had it been registered all along.
1309
+ That is what stops the obvious deadlock — DUST pays fees, and registering is how
1310
+ you get DUST.
1311
+
1312
+ Self-funding is not free. `generationless_fee_availability` caps the backdated
1313
+ amount at `elapsed × night_value × generation_decay_rate`, which starts at zero.
1314
+ So a freshly funded wallet cannot cover the fee yet, and the wait is _inversely
1315
+ proportional to the balance_: at the ledger's defaults a 0.3 DUST fee needs ~36s
1316
+ at 1,000 NIGHT, ~6 min at 100, ~1 hour at 10, and ~10 hours at 1.
1317
+
1318
+ Reported from preprod as a red failure card reading "That didn't go through",
1319
+ with the raw SDK message and a footnote suggesting the proof server. Two of those
1320
+ three were wrong: nothing went wrong, and proving never happened — the SDK refuses
1321
+ before building.
1322
+
1323
+ `estimateRegistrationAffordability` (core, pure, WASM-free) turns the SDK's
1324
+ per-UTxO figures into an answer: affordable now, affordable in N seconds, or never
1325
+ at this holding. That last case matters — when the ceiling is below the fee,
1326
+ "wait" is the wrong advice and "hold more NIGHT" is the right one.
1327
+
1328
+ `designateForDust` throws `DustRegistrationNotYetError` carrying that estimate.
1329
+ Because the guard sits in core, it reaches every run mode at once — extension,
1330
+ CLI, TUI and daemon RPC all route through the same function.
1331
+
1332
+ Two decisions worth recording. The estimate is computed only on the failure path,
1333
+ so a registration that was always going to succeed pays nothing for it. And
1334
+ whether to raise the typed error is decided by the affordability numbers, not by
1335
+ matching the SDK's message text — string-matching would need re-matching on every
1336
+ SDK release, and would fail silently when it drifted.
1337
+
1338
+ The panel now shows "Not quite yet", says nothing was spent, and gives a localized
1339
+ wait ("Ready in about 8 hours"). `mayBeProvingFailure` suppresses the proof-server
1340
+ footnote for every outcome decided before proving.
1341
+
1342
+ `moth dust register` gains a pre-flight and `--wait` (with `--wait-timeout`). The
1343
+ pre-flight matters more on the CLI than in the panel: without it the only way to
1344
+ learn the wait is to fail, and re-running means paying for a full sync first.
1345
+ `--wait` polls rather than sleeping the predicted duration blind, since the
1346
+ estimate moves if the wallet's NIGHT changes underneath it.
1347
+
1348
+ Also corrects the documentation. Four files stated that the ledger imposes a 3h
1349
+ grace period before DUST appears. `dust_grace_period` is 3 hours, but it bounds how
1350
+ stale a transaction's declared `ctime` may be — it is not a delay before
1351
+ generation starts, which is linear from the UTxO's creation with a time-to-cap of
1352
+ about a week. The observation behind the claim was real; the mechanism was
1353
+ invented to fit it. The guides are corrected in place; ADR 0003 is annotated
1354
+ rather than rewritten, since it is a dated record of what was decided.
1355
+
1356
+ - fc93b31: Fix contract circuit calls being rejected with "expected proof-preimage-versioned".
1357
+
1358
+ `callCircuit` previously used a hand-rolled proof provider that POSTed the bare
1359
+ proof-preimage, producing an unversioned `midnight:proof-preimage:` payload that
1360
+ ledger-v8's `check` rejects. Circuit calls now generate proofs through the
1361
+ selectable proof provider: for server proving it routes through the SDK's
1362
+ proving provider and the ledger's versioned `createProvingPayload`/
1363
+ `createCheckPayload` (attaching the circuit's wrapped-IR from the ZK config),
1364
+ and it also supports local WASM proving. Verified with an on-chain preprod mint.
1365
+
1366
+ - ba86b72: Bump ws from 8.20.1 to 8.21.0.
1367
+ - fc93b31: Fix the transparent keystore KDF upgrade writing to a phantom storage key
1368
+ (`wallets/<name>` instead of `wallets/<name>.keystore`). The re-encryption to
1369
+ stronger scrypt parameters never persisted, so it re-ran on every unlock and v1
1370
+ keystores were never actually upgraded. It now writes back to the real keystore
1371
+ path and upgrades once.
1372
+ - b7e2f00: add local network support
1373
+ - 0f9369f: Pre-seed each sub-wallet independently, so a DUST rebuild stops walking from genesis.
1374
+
1375
+ The pre-seed gate tested the SHIELDED cache alone, as a proxy for "this wallet has
1376
+ no state yet". That proxy failed exactly where it mattered most.
1377
+
1378
+ `clearDustSyncCache` evicts the dust cache and nothing else, which is what the DUST
1379
+ screen's "Rebuild records" does. Shielded was therefore still present, the gate
1380
+ stayed shut, and dust walked all 1.4M events from genesis — 78.6 min on preprod —
1381
+ with a perfectly usable reference sitting in the store untouched. "Rebuild
1382
+ records" is precisely what a user reaches for when dust looks wrong, so the
1383
+ narrow, careful-looking operation was the slowest to recover, while a full
1384
+ indexer-change wipe (which clears all four parts) re-seeded and finished in
1385
+ seconds.
1386
+
1387
+ The gate now opens when ANY seedable part is missing, and each part is written
1388
+ only where absent — a part that already has a cache is at least as far along as
1389
+ the reference, so seeding over it would discard progress.
1390
+
1391
+ Mixed heights are coherent, and were verified rather than assumed. The
1392
+ sub-wallets carry independent cursors, so dust can restore at the reference's
1393
+ height while shielded and unshielded resume at tip, each catching up on its own
1394
+ stream. Two measurements on preview:
1395
+
1396
+ - dust rewound to the reference (64,771) with shielded at tip (64,982): fully
1397
+ synced in 1.0s, balances identical.
1398
+ - a real DUST rebuild on a funded, dust-registered wallet: `Pre-seed complete —
1399
+ dust at chain tip`, dust resumed at 64,771 instead of 0, synced in 1.0s, with
1400
+ NIGHT, the DUST registration and the DUST balance all preserved.
1401
+
1402
+ The decision moves to `sync/preseed-parts.ts`, WASM-free so it is unit-testable
1403
+ without loading the ledger — the same split as `sync/progress.ts`. Its tests pin
1404
+ the case that was broken: shielded and unshielded cached, dust absent, must seed
1405
+ dust.
1406
+
1407
+ - bf49ced: Sync the pre-seed reference wallet to chain tip before serializing it.
1408
+
1409
+ `buildEmptyRefCache` started the reference wallet and stopped it immediately.
1410
+ `startWalletSync` resolves on the first balance emission (or a 5s timeout), so
1411
+ `stop()` serialized a wallet that had applied nothing: every sub-wallet snapshot
1412
+ was written with `offset: 0`, which the SDK reads back as `appliedIndex: 0n` —
1413
+ its "stream from genesis" sentinel. The pre-seed then reported "shielded +
1414
+ unshielded + dust at chain tip" while seeding genesis, and had done so for as
1415
+ long as the cache had existed. Shielded and unshielded hid it because their
1416
+ genesis scan is cheap; dust made it visible as an hour of syncing.
1417
+
1418
+ Measured on preprod, brand-new empty wallet, cold cache:
1419
+
1420
+ - before: dust synced at 4715.8s, total 78.6 min (99.2% of it dust,
1421
+ 1,382,732 events at ~293/s)
1422
+ - after: total 49.2s, of which 46.7s is one DustLocalState.deserialize
1423
+
1424
+ Building the reference now costs one full chain walk (71.3 min on preprod) per
1425
+ network per machine, so it must not sit on the wallet-startup path — waiting there
1426
+ would block the user's own wallet for over an hour. `ensureEmptyRefCache` therefore
1427
+ no longer builds by default: it returns a reference already at tip, or nothing.
1428
+ Deliberate builds go through the new `warmEmptyRefCache()`, intended for a
1429
+ background task or an explicit command. A warm on-disk reference is picked up in
1430
+ 0.02s, and a build that times out leaves its partial state for the next attempt to
1431
+ resume rather than handing out a useless snapshot.
1432
+
1433
+ `loadUsableRefStates` gates on the serialized cursor, so a reference at offset 0
1434
+ is never again treated as warm. Also corrects the docblock claiming dust cannot be
1435
+ pre-seeded: dust ledger events are global (the indexer streams `dustLedgerEvents`
1436
+ keyed by a global id) and an empty wallet has no designations of its own, so the
1437
+ reference's generation tree and cursor do transfer — now verified end to end.
1438
+
1439
+ Also adds the birthday guard that making this work turned from latent into live.
1440
+ The pre-seed condition is `(isNewWallet || birthday) && no shielded cache`, which
1441
+ admits any wallet merely missing a cache — including a funded one after a cache
1442
+ reset, a storage eviction, or a restore from mnemonic. Seeding such a wallet from
1443
+ a reference newer than its own first activity starts it past its own history and
1444
+ drops funds from view. Harmless while the reference sat at offset 0 (seeding
1445
+ genesis is always safe); a real hazard once it carries a tip cursor.
1446
+
1447
+ Pre-seeding now requires `birthday !== undefined && reference.height <= birthday`.
1448
+ The height is recorded separately at build time (`emptyRefHeightKey`) because the
1449
+ snapshots' `offset` is an event index, not a block height, and the two are not
1450
+ comparable — 1,382,805 against 1,977,245 on preprod. It is read after the sync
1451
+ completes, which can only overstate it and therefore only make the check stricter,
1452
+ and a reference with no recorded height is treated as unusable rather than
1453
+ guessed at. Existing callers are unaffected: the TUI passes `isNewWallet` without
1454
+ a birthday and the CLI passes neither, so both keep the slow path.
1455
+
1456
+ Nothing calls `warmEmptyRefCache()` yet, so no shipped surface changes behaviour:
1457
+ new wallets still sync from genesis until a caller warms the reference.
1458
+
1459
+ - 0f197e2: Preserve transaction identities in activity entries and submitted transaction records so applied transactions replace their pending rows instead of appearing as duplicates.
1460
+ - fc93b31: Fix transfer submission to return `transactionHash()` instead of the facade's
1461
+ last intent identifier. Tx history, indexer status queries and explorers are all
1462
+ keyed by the transaction hash, so the old value matched nothing — leaving the
1463
+ extension's activity feed stuck on "Pending" after a transfer had applied.
1464
+ - 2fde86f: Report sync progress from the slowest sub-wallet, not the shielded one.
1465
+
1466
+ Progress read shielded indices only, on the stated assumption that shielded was
1467
+ the slowest sub-wallet. It is not — dust is, by two orders of magnitude: a full
1468
+ dust walk is ~1.4M events at a few hundred per second, where shielded covers the
1469
+ same range in under a minute.
1470
+
1471
+ Observed on preprod: a wallet reporting "100% (0s remaining)" with dust at
1472
+ 178,029/1,395,558 and roughly 69 minutes of work left. That is worse than
1473
+ reporting nothing, because it stops the user waiting.
1474
+
1475
+ Progress is now the minimum across all three sub-wallets. A sub-wallet with
1476
+ nothing relevant to apply (total 0) counts as complete rather than stalled — a
1477
+ fresh wallet's unshielded progress is legitimately 0/0 and must not drag the
1478
+ minimum to zero. The figure never rounds up to 100% while the facade still says
1479
+ unsynced, since rendering a near-complete fraction as "100% (0s remaining)" is
1480
+ the specific lie this change exists to remove. The ETA follows the same fraction,
1481
+ so it reflects whichever sub-wallet is actually behind rather than one that
1482
+ finished a minute in.
1483
+
1484
+ The arithmetic moved to sync/progress.ts so it can be unit-tested without loading
1485
+ WASM — the same split as types/tokens.ts and the extension's dust-heal.ts.