@shieldedtech/moth-wallet 0.12.1 → 0.13.0-canary.20260903151454-e3385c4

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 (64) hide show
  1. package/CHANGELOG.md +1061 -0
  2. package/dist/diagnostics/file-timing-store.d.ts +11 -0
  3. package/dist/diagnostics/file-timing-store.d.ts.map +1 -0
  4. package/dist/diagnostics/file-timing-store.js +76 -0
  5. package/dist/diagnostics/file-timing-store.js.map +1 -0
  6. package/dist/index.d.ts +8 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +8 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/sync/chain-tip.d.ts +16 -0
  11. package/dist/sync/chain-tip.d.ts.map +1 -0
  12. package/dist/sync/chain-tip.js +37 -0
  13. package/dist/sync/chain-tip.js.map +1 -0
  14. package/dist/sync/cursor-witness.d.ts +89 -0
  15. package/dist/sync/cursor-witness.d.ts.map +1 -0
  16. package/dist/sync/cursor-witness.js +159 -0
  17. package/dist/sync/cursor-witness.js.map +1 -0
  18. package/dist/sync/preseed-portable.d.ts +66 -0
  19. package/dist/sync/preseed-portable.d.ts.map +1 -0
  20. package/dist/sync/preseed-portable.js +213 -0
  21. package/dist/sync/preseed-portable.js.map +1 -0
  22. package/dist/sync/preseed.d.ts.map +1 -1
  23. package/dist/sync/preseed.js +117 -1
  24. package/dist/sync/preseed.js.map +1 -1
  25. package/dist/sync/progress.d.ts +15 -0
  26. package/dist/sync/progress.d.ts.map +1 -1
  27. package/dist/sync/progress.js +17 -1
  28. package/dist/sync/progress.js.map +1 -1
  29. package/dist/sync/sync-store.d.ts +11 -0
  30. package/dist/sync/sync-store.d.ts.map +1 -1
  31. package/dist/sync/sync-store.js +13 -0
  32. package/dist/sync/sync-store.js.map +1 -1
  33. package/dist/sync/wallet-sync.d.ts +15 -0
  34. package/dist/sync/wallet-sync.d.ts.map +1 -1
  35. package/dist/sync/wallet-sync.js +81 -14
  36. package/dist/sync/wallet-sync.js.map +1 -1
  37. package/dist/types/network.d.ts +24 -6
  38. package/dist/types/network.d.ts.map +1 -1
  39. package/dist/types/network.js +36 -12
  40. package/dist/types/network.js.map +1 -1
  41. package/dist/wallet/address.d.ts.map +1 -1
  42. package/dist/wallet/address.js +10 -0
  43. package/dist/wallet/address.js.map +1 -1
  44. package/dist/wallet/batch-transfer.d.ts +12 -1
  45. package/dist/wallet/batch-transfer.d.ts.map +1 -1
  46. package/dist/wallet/batch-transfer.js +38 -7
  47. package/dist/wallet/batch-transfer.js.map +1 -1
  48. package/dist/wallet/manager.d.ts +22 -0
  49. package/dist/wallet/manager.d.ts.map +1 -1
  50. package/dist/wallet/manager.js +93 -2
  51. package/dist/wallet/manager.js.map +1 -1
  52. package/dist/wallet/night-amount.d.ts +35 -0
  53. package/dist/wallet/night-amount.d.ts.map +1 -0
  54. package/dist/wallet/night-amount.js +58 -0
  55. package/dist/wallet/night-amount.js.map +1 -0
  56. package/dist/wallet/spendable.d.ts +19 -0
  57. package/dist/wallet/spendable.d.ts.map +1 -0
  58. package/dist/wallet/spendable.js +38 -0
  59. package/dist/wallet/spendable.js.map +1 -0
  60. package/package.json +4 -3
  61. package/dist/diagnostics/fs-timing-store.d.ts +0 -29
  62. package/dist/diagnostics/fs-timing-store.d.ts.map +0 -1
  63. package/dist/diagnostics/fs-timing-store.js +0 -81
  64. package/dist/diagnostics/fs-timing-store.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,1061 @@
1
+ # @shieldedtech/moth-wallet
2
+
3
+ ## 0.13.0-canary.20260903151454-e3385c4
4
+
5
+ ### Minor Changes
6
+
7
+ - b49c96d: Give the CLI and TUI the pre-seed, timings and DUST-registration behaviour the
8
+ extension already had.
9
+
10
+ **Birthdays.** `chainTip` moves from the extension's background handlers into
11
+ core, and `wallet generate` on both surfaces records the chain tip as the new
12
+ wallet's birthday. Without one the `reference.height <= birthday` guard can never
13
+ pass, so no CLI or TUI wallet could ever be pre-seeded — the difference between
14
+ 29.3s and 78.6 min on preprod. Imports still get none, deliberately: a restored
15
+ wallet may hold funds at any height, and seeding it past its own history would
16
+ lose them silently.
17
+
18
+ **`moth preseed status|refresh|build`.** Thin wrappers over core functions
19
+ that already existed but had no caller outside the extension. `refresh` is the
20
+ one worth having — 9.1s to catch a reference up, against 53.6 min to rebuild it
21
+ from genesis, which is what someone does by hand when the command is missing.
22
+ `build` says how long it will take before starting, because an unattended command
23
+ that appears to hang for an hour is indistinguishable from a broken one.
24
+
25
+ **Phase timings on disk.** `createFileTimingStore` backs the existing
26
+ storage-agnostic recorder with `~/.moth/timings.json` — the path
27
+ `docs/BENCHMARKING.md` already documented and nothing wrote. `moth diagnostics
28
+ timings` shows the timeline as deltas, and recording stays off until switched on.
29
+ A headless sync is where this matters most: it is the surface with no other
30
+ signal about where the time went.
31
+
32
+ **DUST registration in the TUI.** `DustRegistrationNotYetError` is now caught
33
+ distinctly, so a wallet whose NIGHT is too new to cover the registration fee is
34
+ told "not yet" instead of shown the raw SDK error as a failure. The panel and the
35
+ CLI already did this; the TUI was the surface still reporting it as a defect.
36
+ - 0508a38: Four CLI fixes found by a manual test pass, none of which the test suite could see.
37
+
38
+ **Amounts are parsed strictly (#63).** `moth transfer` used `parseFloat` and
39
+ `Math.round`, and `parseFloat` keeps whatever prefix it understands: `1,5` was
40
+ accepted as **1 NIGHT**, losing a third of the value with no warning; `0.0000001`
41
+ rounded to **zero base units** and was submitted as a transfer that moved nothing
42
+ and still paid a fee; `1e3` became 1000 NIGHT; `1abc` became 1. A shared
43
+ `parseNightAmount` in core now refuses all of them, in BigInt, with a message
44
+ naming what is wrong — and `daemon transfer` already did it this way, so the two
45
+ paths finally agree.
46
+
47
+ **`moth transfer` can select a token (#62).** It hardcoded `NIGHT_TOKEN_ID`, so a
48
+ wallet holding anything else could spend it only through `daemon transfer
49
+ --token-id`. Same flag name and default here. The positional amount stays a NIGHT
50
+ decimal and is refused for other tokens, directing to `--amount` in raw base units
51
+ — mis-scaling a token by NIGHT's 10⁶ would be worse than refusing.
52
+
53
+ **`moth wallet export-phrase` exists (#59).** `WalletManager.exportPhrase` and
54
+ `exportSeedHex` have always been in core, and the extension exposes them, but no
55
+ CLI command did. So the CLI had no backup path — a phrase was shown once at
56
+ `wallet generate` and never again — no way to move a wallet between machines, and
57
+ no way to recover a seed for a keystore you hold the passphrase to. Confirmed by
58
+ default and refused non-interactively without `--yes`, following `wallet remove`.
59
+ A wallet imported from a hex seed says so rather than presenting a seed as a
60
+ phrase.
61
+
62
+ **`wallet address` takes `--wallet` (#60).** It was the only command in the CLI
63
+ requiring `--name`, which made it the only one that could not act on the active
64
+ wallet: `wallet use w1` then `wallet address` failed with "Missing required flag
65
+ name". `--name` remains as an alias.
66
+
67
+ **`dust register` distinguishes empty from done (#58).** `designateForDust`
68
+ returns `null` both when every UTXO is already designated and when there are no
69
+ NIGHT UTXOs at all, and the command reported the first for both — vacuously true
70
+ of an empty wallet, and read as success. An unfunded wallet now gets "No NIGHT to
71
+ designate" and a non-zero exit.
72
+ - 3e131e2: Detect an indexer renumbering instead of silently syncing from the wrong place.
73
+
74
+ Sync cursors are indexer-assigned event sequence numbers, and nothing ties an id
75
+ to a block — `DustLedgerEvent` carries only `id`, `raw`, `maxId` and
76
+ `protocolVersion`. So when the same URL starts serving a differently-numbered
77
+ stream, a stored cursor names a different event and the sync resumes at the wrong
78
+ point without erroring. The only guard was a string comparison of `indexerUrl`,
79
+ which by definition cannot see a backend swap behind an unchanged name — and it
80
+ lived in the extension's background, so the CLI and TUI had no check at all.
81
+
82
+ This has already happened on preprod. The default indexer had a 22-wide hole in
83
+ its dust id space; the host now serving that name numbers contiguously, so cursors
84
+ written before the change sit 22 events too high. The pre-seed reference committed
85
+ for the extension stores dust cursor `1431375`, which under the current numbering
86
+ is 22 events beyond the state the snapshot holds — verified against the live
87
+ indexer, where that id now yields digest `3f3576deb45ad350` while the event the
88
+ reference actually stopped at yields `11c8cf9fd5a736f2`.
89
+
90
+ A cursor is now stored with a **witness**: the hash of the event found at that id.
91
+ On resume the id is re-read and compared. Same event means the numbering is
92
+ unchanged; a different event means it moved and the cursor is refused, failing
93
+ closed to a genesis sync — the direction ADR-0003 already establishes as always
94
+ safe.
95
+
96
+ A witness rather than one global indexer fingerprint, because a fingerprint has to
97
+ be sampled at a fixed id and any id below the point where two numberings diverge
98
+ returns the *same* event from both. Sampled at preprod's hole (989781), old and
99
+ new both return the event new calls 989781 — old's first existing id at or above
100
+ that probe was 989803, the same event — so a fingerprint there would have matched
101
+ across the exact cutover it existed to detect. The divergence point is not
102
+ knowable in advance; a witness has no such blind spot, because it asks only about
103
+ the id the cache actually depends on.
104
+
105
+ Three paths are gated: the warm read verifies before handing a reference to any
106
+ wallet, a build records witnesses for the cursors it stops at, and
107
+ `refreshEmptyRefCache` refuses to resume across a mismatch — resuming would carry
108
+ the old numbering forward into a reference that then looks freshly built, which
109
+ destroys the evidence.
110
+
111
+ Scope. Only shielded and dust are witnessed: they ride the global ledger-event
112
+ numbering and are the two the preprod change moved, where unshielded is keyed by
113
+ address. Per-wallet caches are not yet gated — normal sync persistence is written
114
+ by the SDK's own serialization rather than through `saveCachedState`, so covering
115
+ those needs a separate seam. A reference with no witness is treated as
116
+ unverifiable rather than invalid, so upgrading does not force a chain walk on
117
+ everyone at once; new references carry witnesses and the population converges.
118
+ - e31eaf8: Record a witness per cursor in exported references, and refuse a bundle without one.
119
+
120
+ A published reference records cursors that are indexer-assigned event sequence
121
+ numbers, so its correctness depends on an indexer that the bundle says nothing
122
+ about. That is how the preprod bundle stayed in use after the numbering underneath
123
+ it moved: the bytes were intact, the checksums matched, and the cursors had
124
+ quietly stopped naming the events they were written for.
125
+
126
+ `export-preseed.mjs` now reads the event at each cursor and records its hash in
127
+ the manifest under `witnesses`, alongside `height` and the per-part sizes. It
128
+ refuses to export at all if a cursor cannot be witnessed — including the case
129
+ where the indexer returns no event at or after the cursor, which means the
130
+ reference is *ahead* of the indexer it is being exported against and is itself the
131
+ renumbering signal.
132
+
133
+ The extension's installer requires them. A manifest without a witness for shielded
134
+ and dust is rejected, and the witnesses are written to the store before the height
135
+ — the height is what marks a reference usable, so a reference that reads as usable
136
+ without its witnesses is one that skips verification.
137
+
138
+ The asymmetry with local references is deliberate. A witnessless reference already
139
+ on disk is treated as unverifiable rather than invalid, because the alternative
140
+ forces every existing user into a chain walk on upgrade. A witnessless *bundle* is
141
+ an artefact we control and can re-cut, so refusing it costs one slower first sync
142
+ and trusting it costs a wallet silently resuming at the wrong event. The bundle
143
+ currently in the repository has no witnesses and will therefore no longer install;
144
+ a reference rebuilt from genesis against the current indexer replaces it.
145
+ - b49c96d: Move a pre-seed reference between machines: `moth preseed export` / `import`.
146
+
147
+ Building a reference IS the chain walk — tens of minutes, once per network per
148
+ machine. That cost is identical for everyone, because a reference holds public
149
+ chain state and nothing else, so it is work that should be done once and shared
150
+ rather than repeated by every developer who clones the repo. ADR 0005 called for
151
+ these two actions; the rest of the command shipped without them.
152
+
153
+ The on-disk shape is the one `scripts/export-preseed.mjs` already writes and CI
154
+ already publishes: gzipped state per sub-wallet plus a manifest. One format, so a
155
+ reference exported here can be dropped into the extension package, and one
156
+ downloaded from a release can be imported here.
157
+
158
+ `export` never writes the reference wallet's mnemonic. That is the only secret in
159
+ the arrangement — the state blobs are public chain data, but the mnemonic
160
+ controls the wallet they were built from, and a published reference is meant to
161
+ be safe to hand to strangers. The command says so in its own output.
162
+
163
+ `import` refuses rather than guesses. A bundle for another network would seed
164
+ wallets from a chain they have never been on, and the mismatch is silent
165
+ afterwards. A bundle older than what is already present is a downgrade that costs
166
+ catch-up time on every wallet created from then on; `--force` allows it, for
167
+ replacing a corrupt newer reference with a known-good older one.
168
+
169
+ Every part is decompressed before any part is written. Unpacking as it went left
170
+ the store holding new shielded and unshielded state beside an old dust state when
171
+ a later part turned out to be corrupt — a mixture that never existed on chain,
172
+ with a height key that still looked consistent. The height is written last, since
173
+ it is what marks a reference usable.
174
+ - 04f1aa4: **`undeployed` replaces `local` as the local devnet network.** Every interface now
175
+ offers it, including the extension, which could not select it before.
176
+
177
+ The two were duplicate presets for the same thing. `undeployed` is the id the
178
+ Midnight tooling, `docs/TESTING.md`, and this repo's own README instructions all
179
+ use for the local stack, and it points at the node port that stack listens on —
180
+ `9944`. `local` pointed at `9933`, which nothing in the documented stack serves,
181
+ so selecting **Local** in the extension connected to a closed port. It has been
182
+ that way since the first commit.
183
+
184
+ `local` was kept out of the extension's picker by a comment claiming the wallet
185
+ could not derive addresses for `undeployed`. That was false: `mn_addr_undeployed1…`,
186
+ `mn_dust_undeployed1…` and `mn_shield-addr_undeployed1…` have always derived, and
187
+ `address-parity`'s network loop stopping at `qanet` is why nothing contradicted it.
188
+ The loop now covers `undeployed` and `stagenet`, and a new test holds
189
+ `SUPPORTED_NETWORKS` equal to the keys of `DEFAULT_NETWORKS`, so a preset no
190
+ interface can reach — or an offered network with no preset — fails the suite.
191
+
192
+ **Breaking, with a migration.** `local` is gone from `SUPPORTED_NETWORKS` and
193
+ `DEFAULT_NETWORKS`, and the extension rejects it on save. Read paths resolve it via
194
+ `canonicalNetworkId`, exported from core: the extension's stored selection, a
195
+ wallet's meta record, the TUI's `lastNetwork`, `--network local`, and
196
+ `createMothBrowser({ network: 'local' })` all continue to work and land on
197
+ `undeployed`. Per-network birthdays and endpoint overrides move across with it, so
198
+ a migrated wallet keeps its pre-seed shortcut instead of resyncing from genesis.
199
+ Stored records are rewritten lazily, only when something else is already saving
200
+ them. `local` stays in `ALL_NETWORKS` so addresses already handed out still resolve.
201
+
202
+ Sync caches are keyed by network, so a migrated account resyncs once under the new
203
+ key — correct, since the node URL genuinely changes — and its old entries are left
204
+ behind rather than cleaned up.
205
+
206
+ Also fixed alongside: the four localhost indexer fallbacks pointed at
207
+ `http://localhost:8088` without the `/api/v4/graphql` path the indexer client posts
208
+ queries to, and the README's network table listed `devnet` as localhost, omitted
209
+ `undeployed`, and gave qanet hostnames (`rpc.qanet.dev.midnight.network`) that do
210
+ not resolve. Mainnet stays out of both the table and the `--network` reference:
211
+ the CLI refuses it and the extension keeps it out of the picker, so documenting
212
+ its endpoints only invites someone to try.
213
+
214
+ ### Patch Changes
215
+
216
+ - b49c96d: Bound the sync-engine teardown, and stop a wedged one from pinning everything
217
+ behind it.
218
+
219
+ `facade.stop()` closes the wallet SDK's submission service, which first awaits
220
+ the Polkadot client the facade was built with. That client is created with
221
+ `ApiPromise.create({throwOnConnect: false})`, so against a node that never
222
+ answers it neither resolves nor rejects — WsProvider simply keeps retrying. The
223
+ stop therefore had no failure path, and every caller inherited the hang. The
224
+ trigger is a node URL that does not answer, which is exactly the state a user is
225
+ in while editing one, Local network being the common case.
226
+
227
+ Three consequences, all fixed here:
228
+
229
+ - **Settings → Network's Save button spun for ever and discarded the edit.**
230
+ `saveNetworkConfig` awaited the stop before persisting, so the save neither
231
+ completed nor failed and the next attempt started from the same broken
232
+ endpoint. Settings are now written before the engine is touched and rolled back
233
+ if the switch fails, and a stop the offscreen document never answers closes that
234
+ document — Chrome destroys it without its cooperation, which is the only
235
+ teardown that reaches a wedged one.
236
+ - **The idle teardown never closed the offscreen document,** so an idle wallet
237
+ kept its worker and WASM heap alive instead of letting the service worker
238
+ suspend.
239
+ - **Locking never freed the worker holding key material,** because `lockNow`'s
240
+ forced teardown waited on the same stop.
241
+
242
+ `facade.stop()` is now raced against a 5s bound. The offscreen `syncStop` no
243
+ longer waits unboundedly on a start that may never finish, and still stops that
244
+ engine whenever it does come up, so an abandoned start cannot keep syncing behind
245
+ a new one.
246
+ - 2dabc50: Make `moth config` usable, and add smoke coverage for the class of bug it was.
247
+
248
+ `config` declared an optional positional argument (`action`) ahead of a required
249
+ one (`key`). @oclif/core rejects that outright — with `action` absent, a single
250
+ value is ambiguous between an action and a key — so every invocation failed at
251
+ spec validation and the command body never ran. `action` is now required, which
252
+ changes no working behaviour because nothing worked.
253
+
254
+ Nothing caught this because no test invoked the command. Two probes now cover the
255
+ class:
256
+
257
+ - **Positional order, checked statically from source.** This is the one that
258
+ bites: reverting the fix produces `config: required "key" follows optional
259
+ "action"`.
260
+ - **A `--help` sweep over all 35 commands**, which catches a broken flag
261
+ definition, a bad example, or an import that throws on load.
262
+
263
+ Worth recording why it takes two. `--help` does not validate positional-argument
264
+ order: with the bad spec in place, `moth config --help` prints help perfectly
265
+ happily while bare `moth config` reports "Invalid argument spec". So the help
266
+ sweep would not have caught the bug that prompted it, and the order rule has to be
267
+ checked separately. Invoking every command bare would catch it, but would also
268
+ run them.
269
+ - 9afd580: Refuse mainnet at the `--network` flag, not in one of its consumers.
270
+
271
+ The refusal lived inside `BaseCommand.getNetworkConfig`, and twelve commands never
272
+ call it — including both that create wallets. `moth wallet generate --network
273
+ mainnet` derived mainnet addresses, wrote a keystore, printed a recovery phrase
274
+ and exited 0, with no warning shown. A guard in one consumer is not a guard; it is
275
+ a convention that holds wherever someone remembered it.
276
+
277
+ It now hangs off the `--network` flag that every command inherits through
278
+ `baseFlags`, so no command can take a network id without it. `getNetworkConfig`
279
+ keeps the check as defence in depth, for an id arriving from stored config or from
280
+ a caller assembling flags itself, and both now route through one
281
+ `assertNotMainnet`.
282
+
283
+ Verified across the paths the issue did not cover: `wallet generate`, `wallet
284
+ import`, `wallet use` and `tui` all now print the warning and exit 1 without
285
+ writing a keystore, while `--network preprod` is untouched.
286
+
287
+ Also guards `config set default-network mainnet`, which is the second way a
288
+ network id enters the CLI — `WalletManager` falls back to `config.defaultNetwork`
289
+ for a wallet with no network of its own, so a stored value reaches the same code
290
+ paths without `--network` ever being used. Note that path is currently unreachable
291
+ for an unrelated reason: `moth config` declares an optional argument before a
292
+ required one, which oclif rejects, so every invocation of that command fails
293
+ before it runs. Filed separately.
294
+ - b49c96d: Read the birthday back, so a CLI or TUI wallet can actually pre-seed.
295
+
296
+ The birthday was written and never read. `startWalletSync`'s pre-seed gate is
297
+ `(isNewWallet || birthday)`, and no CLI command passed either — eleven of them
298
+ stopped at `walletName`, and the TUI hook passed `isNewWallet` but no birthday,
299
+ so the guard `emptyRef.height <= birthday` could never be reached. The effect was
300
+ silent: `moth balance -n preprod -v` showed no pre-seed line at all and dust began
301
+ at 0%, with the reference sitting unused.
302
+
303
+ Every sync call site now passes it, resolved through a new
304
+ `WalletManager.birthdayOn(name, networkId)`. Per network on purpose: `list()`
305
+ resolves against the wallet's own `meta.network`, so a sync driven by `--network`
306
+ was reading a height belonging to a different chain, or nothing at all. It never
307
+ throws — a wallet with no meta asserts nothing, and "no claim" means scan from
308
+ genesis, which is slow but never wrong.
309
+
310
+ Guarded by a test that walks the AST of every `startWalletSync` call in the CLI
311
+ and TUI and fails any that omits the birthday, since nothing else would notice
312
+ this regressing. Verified by deliberately dropping the argument.
313
+ - c2f8b73: Re-cut the pre-seed bundles from genesis, and add one for qanet.
314
+
315
+ The preprod bundle recorded dust cursor `1431375`, written under the indexer's old
316
+ numbering. Under the numbering now served, that id names an event 22 positions
317
+ later than the state the snapshot holds, so every wallet seeded from it resumed
318
+ past 22 dust events — no error, just missing generation history (#40).
319
+
320
+ All three references were rebuilt from genesis rather than refreshed, because a
321
+ refresh resumes from the stored cursor and would have carried the old numbering
322
+ forward into a bundle that then looked freshly built:
323
+
324
+ | Network | Height | Build | Dust cursor |
325
+ | --- | --- | --- | --- |
326
+ | preprod | 2,203,416 | 55 min | 1,449,958 (was 1,431,375) |
327
+ | preview | 519,470 | 3 min | 141,062 |
328
+ | qanet | 2,314,786 | 14 min | 346,693 |
329
+
330
+ Each manifest now carries a witness per cursor, so a consumer can tell whether the
331
+ numbering it was written under still holds — these are the first bundles that can
332
+ be verified rather than trusted, and the first that the installer will accept.
333
+
334
+ qanet ships for the first time. It costs 140 KB, not the several megabytes preprod
335
+ does: its chain is longer but has far fewer dust events, and dust is what makes a
336
+ reference large. The control that offers on-device warming probes which networks
337
+ ship a reference rather than listing them, so no code changed to add it.
338
+ - b49c96d: Move the pre-seed commands from `moth dust preseed` to `moth preseed`.
339
+
340
+ DUST is why the pre-seed matters — the 4.9 MB blob, the ~1.4M events, the tens of
341
+ minutes, where shielded and unshielded take seconds — which is what put it under
342
+ `dust`. But that describes the motivation, not the thing: the pre-seed writes all
343
+ three sub-wallet caches, and a reference is per-network machine state in `~/.moth`
344
+ shared by every wallet there, whereas `moth dust` groups per-wallet token
345
+ operations. A command tree should say what a thing is, and someone whose first
346
+ sync is crawling searches for "preseed" rather than reasoning their way to DUST.
347
+
348
+ Settled while the command had not shipped, so the rename costs no compatibility.
349
+
350
+ Each action is now a real subcommand — `preseed status|import|refresh|build|export`
351
+ — instead of one command taking an action argument. `--timeout` therefore belongs
352
+ to `build` and `--force` to `import`, rather than every flag hanging off the group
353
+ with "(build only)" in its description, and each gets its own `--help`. The group
354
+ carries an oclif topic description; without one the help listed the whole group
355
+ under whichever subcommand sorted first.
356
+ - b49c96d: Require all three parts of a pre-seed reference, and drop `node:zlib` from core.
357
+
358
+ Two findings from review on the CLI/TUI parity work.
359
+
360
+ **A missing part was as damaging as a corrupt one.** Both `exportReference` and
361
+ `importReference` checked only dust, so a bundle without shielded state imported
362
+ the other two over an existing reference and moved the height key with them. The
363
+ store was left holding shielded at the old height and the rest at the new one — a
364
+ mixture that never existed on chain, reported as ready by
365
+ `loadUsableRefStates` because the height key still looked consistent, and that
366
+ inflated height then feeding the `emptyRef.height <= birthday` guard, seeding
367
+ wallets whose birthday fell between the two. Both functions now require every
368
+ part: export returns null, import refuses and names each missing file.
369
+
370
+ **`node:zlib` had no business in core.** Nothing in the browser or extension
371
+ packages imported `preseed-portable` yet, but it is re-exported from core's
372
+ barrel — and that barrel reaches 36 Node builtins where the browser package's
373
+ walked graph reaches none, so one careless import would have carried zlib into
374
+ every dependent DApp bundle. Compression now goes through `CompressionStream` and
375
+ `DecompressionStream`, which Node 18+ and every current browser provide, so the
376
+ module is genuinely isomorphic rather than allow-listed as an exception. Gzip
377
+ level is not selectable through that API, so bundles written here compress
378
+ slightly less than `scripts/export-preseed.mjs` does at level 9; sizes are
379
+ recorded in the manifest either way, and decompression is level-agnostic.
380
+ - 9be5669: Show what a transfer can actually spend.
381
+
382
+ A synced wallet reported 500 NIGHT and refused a 10 NIGHT transfer with
383
+ `Insufficient funds`. Both figures were true and neither was reconcilable from
384
+ outside the wallet.
385
+
386
+ The displayed balance counts coins reserved by transactions in flight, and does
387
+ so deliberately — dropping them flashes the balance to zero mid-send. But the SDK
388
+ spends from `availableUtxos` alone, so the number shown was never the number that
389
+ could be spent, and nothing surfaced the difference.
390
+
391
+ `moth balance` now prints the split when anything is reserved, and stays quiet
392
+ otherwise:
393
+
394
+ ```
395
+ NIGHT:
396
+ unshielded: 500.000000 (500000000 STARS)
397
+ available: 0.000000 ← what a transfer can use
398
+ reserved: 500.000000 (a transaction in flight holds these)
399
+ ```
400
+
401
+ JSON gains `unshieldedAvailable` and `unshieldedReserved` beside the existing
402
+ fields, so nothing reading it today breaks. The transfer's insufficient-funds
403
+ path names the number that blocked it, and says nothing when a reservation was
404
+ not the cause.
405
+
406
+ Nothing new is computed — `WalletBalances.coins` already carried the split.
407
+
408
+ This makes the state visible; it does not stop reservations outliving their
409
+ transactions. A wallet already in that state still needs its sync cache cleared.
410
+ - ea1793d: Report sync progress that is neither invented nor erased.
411
+
412
+ Two defects in how progress reached the surfaces, close enough together in
413
+ `extractBalancesPartial` that fixing them apart would mean resolving the same
414
+ twenty lines twice.
415
+
416
+ **A partial emission erased a sub-wallet.** Each sub-wallet's coins and its
417
+ progress were read inside a single `try`, and the coin loops reached into the
418
+ state without the optional chaining used one line above on the balances:
419
+
420
+ ```ts
421
+ const sb = state.shielded?.balances; // guarded
422
+ for (const c of state.shielded.availableCoins) { // not guarded — throws here
423
+ …
424
+ subProgress.shielded = {applied, total}; // never reached
425
+ ```
426
+
427
+ An emission carrying no slice for a part threw in the loop and skipped the
428
+ progress assignment, leaving `{applied: 0, total: 0}` — which `fraction()` treats
429
+ as **complete**, correctly for a sub-wallet with genuinely nothing to apply and
430
+ catastrophically for one whose slice was simply absent. The TUI alternated about
431
+ once a second between real figures and `synced · 0 / 0` with no balance. Coins
432
+ and progress now read separately, all six coin loops are guarded, and each part
433
+ carries its previous value forward when an emission says nothing about it;
434
+ progress does not go backwards inside a session. A genuinely 0/0 part still
435
+ counts as complete rather than stalling the overall figure.
436
+
437
+ **The ETA assumed every sync starts at zero.** `etaSeconds` was
438
+ `elapsedMs / percentage - elapsedMs`, which treats cumulative progress as this
439
+ session's work. Dust resumes from cache constantly, so a run that restored at
440
+ ~65% and then ran 152s was read as "67% in 152s" — fifteen times the real rate.
441
+ Measured on preprod it promised 1m15s at 67% and 2m23s at 81% against a true
442
+ ~10m, and the estimate *climbed* as elapsed time corrected the fiction. The rate
443
+ now comes from a per-session baseline: the same inputs give 41m and 12m19s,
444
+ falling as the run proceeds. Below 0.2 points of movement it reports nothing,
445
+ since an admitted unknown beats a number derived from noise.
446
+
447
+ Both bugs predate the CLI/TUI parity work, which only made the first visible by
448
+ putting per-sub-wallet counters on screen. The daemon and extension read the same
449
+ balances.
450
+ - 316ca82: Document `moth transfer` as it actually works.
451
+
452
+ The README showed `moth transfer <amount> NIGHT --to <addr>` on two rows. That form
453
+ does not parse — `transfer` declares one positional and rejects the second with
454
+ `Unexpected argument: NIGHT`. It was the documented invocation, so it was the first
455
+ thing a new user would type.
456
+
457
+ Corrected to `moth transfer [<amount>] [--to <addr>]`, and the rows now say what
458
+ was previously stated nowhere: the in-process command is NIGHT-only, with the
459
+ token hardcoded and no flag to change it. A row for `moth daemon transfer` covers
460
+ the path that *can* move other tokens, including the distinction between
461
+ `--amount` (raw smallest units, any token) and `--night` (a decimal converted at
462
+ 10⁶ STARS, refused for anything but NIGHT).
463
+
464
+ Docs only. Whether the in-process command should grow token selection is the open
465
+ half of #62.
466
+ - 89f34aa: Stop the sync before freeing the keys when the TUI quits.
467
+
468
+ Quitting printed a wall of WASM errors over the exiting terminal, once per live
469
+ sync:
470
+
471
+ ```
472
+ Wallet.Other: Error while applying sync update
473
+ cause: Error: Dust secret key was cleared
474
+ at DustLocalState.replayEventsWithChanges
475
+ ```
476
+
477
+ The quit handler called `lockAll()` and then `exit()`, zeroing the
478
+ `DustSecretKey` in the WASM heap while the dust sync was still mid-batch. The
479
+ only `stop()` lived in an unmount cleanup, ran after `exit()`, and was
480
+ fire-and-forget, so the sync's next batch reached for a key that no longer
481
+ existed.
482
+
483
+ `useBalance` now exposes an awaited `stop()` — unsubscribe, await the facade's
484
+ stop, bounded by a timeout so a sync that will not settle cannot keep the TUI
485
+ open — and both quit paths await it before locking.
486
+
487
+ Nothing was corrupted: the wallet was exiting and its state was already
488
+ persisted. It simply looked like a crash every time, and would have buried a real
489
+ error.
490
+
491
+ The non-quit unmount path (Ctrl-C, a crash, the process ending) now stops before
492
+ locking as well, which narrows the window rather than closing it — a React
493
+ cleanup cannot await, so a batch already inside the WASM call can still find the
494
+ key gone.
495
+
496
+ ## 0.12.1
497
+
498
+ ## 0.12.0
499
+
500
+ ### Minor Changes
501
+
502
+ - ea15676: Coordinate the first public package release under the Moth package names.
503
+
504
+ This is a minor bump because no package has previously been published under the
505
+ new names. The release remains experimental and unsupported, so it stays below
506
+ 1.0.0. Package names, CLI identifiers, environment variables, connector identity,
507
+ and local state paths are aligned before publication; no compatibility shim is
508
+ required for an unpublished package.
509
+
510
+ ### Patch Changes
511
+
512
+ - be98f55: Measure test coverage in CI, and share the core test fixtures.
513
+
514
+ Adds a second job to `.github/workflows/ci.yml` that runs `yarn test:coverage`
515
+ and archives `lcov.info`. The measurement step is advisory: V8 instrumentation
516
+ slows the scrypt-backed keystore and seed-export suites enough to trip vitest's
517
+ internal worker RPC timeout, so the run exits non-zero even though every test
518
+ passes and the report is written. `continue-on-error` is scoped to that step
519
+ rather than the job, so a run that produces no report at all still fails on the
520
+ upload. The strict gate stays the `test` job. Baseline at the time of writing:
521
+ 62.54% lines, 83.76% branch over 686 tests, with entrypoints and screens excluded
522
+ as shells the E2E tier covers.
523
+
524
+ The three `exportSeedHex` tests no longer carry a per-test 15s timeout. A
525
+ literal there overrides the project config — which already raises the timeout
526
+ precisely because these derive keys at the v2 scrypt parameters — and made them
527
+ the only thing failing a `--coverage` run.
528
+
529
+ Shared test fixtures now live in `packages/core/tests/helpers/`. Every core test
530
+ that needs an in-memory `StorageAdapter` or the reference mnemonic takes it from
531
+ there — four hand-rolled `MemoryStorage` classes and four copies of the mnemonic
532
+ are gone, so a change to `StorageAdapter` now breaks compilation at every call
533
+ site, and no test can drift from the seed the address-parity fixtures pin. The
534
+ keystore suite shares one encrypted keystore across its shape and tamper
535
+ assertions while keeping an independent full-strength round trip, cutting its
536
+ scrypt derivations from thirteen to nine.
537
+
538
+ The extension's network picker test now asserts that every network in
539
+ `SUPPORTED_NETWORKS` is both named and described in the rendered markup, with
540
+ developer mode on so the gated mainnet is covered too. The radio count derives
541
+ from the state's `available` list rather than any literal, so gating a second
542
+ network cannot fail a correct picker; a hardcoded count disagreed with the
543
+ picker for twelve days after `local` was added without label entries.
544
+ `NETWORK_LABELS` and `NETWORK_DESCRIPTIONS` are exported for that assertion.
545
+
546
+ Root vitest config: `projects` is now globbed rather than listed, so a new
547
+ package cannot be gated by CI while staying invisible to the coverage number,
548
+ and `coverage.exclude` extends vitest's defaults rather than replacing them.
549
+
550
+ Removes the root `lint` script and turbo's `lint` task. No package defined a
551
+ `lint` script, so `turbo run lint` linted nothing and reported success, which
552
+ read as a passing gate — worse than having none. `CONTRIBUTING.md` no longer
553
+ claims a linter config lives in the repo.
554
+
555
+ The root `vitest` range now matches `@vitest/coverage-v8`, which declares an
556
+ exact peer on the vitest it ships with, so the two cannot drift onto an
557
+ unsupported pairing.
558
+
559
+ ## 0.2.0
560
+
561
+ ### Minor Changes
562
+
563
+ - b157dd2: Add the browser extension wallet and dApp connector workflow.
564
+
565
+ The wallet core now supports browser-safe persisted sync state, activity history,
566
+ message signing, staged transfer construction and submission, fee estimation,
567
+ and DUST and intent operations. Add the WXT side-panel extension and Connector
568
+ Lab mock dApp, expose the new APIs through the browser adapter, and keep the CLI
569
+ and TUI integrations compatible with asynchronous sync cache handling and the
570
+ Ink runtime.
571
+
572
+ - 36cb067: Add selectable proof-server and local WASM proving across the core wallet, CLI,
573
+ terminal dashboard, browser extension, and dApp connector API.
574
+ - fc93b31: Add the opt-in daemon for sharing one live wallet between a long-running host
575
+ (the TUI dashboard or `moth daemon serve`) and CLI clients over an authenticated
576
+ Unix-socket or TCP RPC. The daemon owns the sync engine and spending keys;
577
+ clients route the build/balance/prove/sign/submit pipeline through it, so keys
578
+ never leave the host process. Adds the `moth daemon serve|transfer|call|deploy|
579
+ submit-tx|dust register|dust deregister|key gen|key list|key revoke|maintenance`
580
+ subcommands, API-key authentication with read/write scopes, an append-only audit
581
+ log, and an L3 confirmation queue (interactive modal, or headless auto-approve
582
+ gated behind an explicit flag + env var).
583
+ - fc93b31: Adopt a single derive-and-drop key model. `deriveWalletKeys(seedHex)` produces a
584
+ typed `WalletKeys` bundle once at unlock and the raw BIP-39 seed is dropped;
585
+ `UnlockedWallet` no longer exposes it. Every Midnight write path accepts
586
+ `WalletKeys` directly (`sendTokensWithKeys`, `designateForDustWithKeys`,
587
+ `dedesignateFromDustWithKeys`, and the contract call/deploy/maintenance paths),
588
+ with the seed-based functions kept as thin wrappers so both API shapes remain
589
+ available. `WalletManager.exportSeedHex` is the one deliberate exception, for a
590
+ key-holder that must re-supply a serializable secret across a process/document
591
+ boundary (the extension's offscreen document, where WASM keys can't cross the
592
+ message channel). See docs/spec/wallet-service/05-key-management.md (D-KM-3).
593
+ - fc93b31: Report the NIGHT actually registered for DUST generation.
594
+
595
+ `DustGeneration` gains `registeredNight` — the sum of the NIGHT UTXOs flagged
596
+ as registered. Registration binds the NIGHT key, but each UTXO generates via
597
+ its own on-chain record, so three amounts can differ: the balance, the
598
+ registered NIGHT, and the NIGHT generating right now (`designated`). The
599
+ extension's DUST screen now attributes "Total possible" to the generating
600
+ amount instead of the whole balance, quantifies NIGHT that is registered but
601
+ not yet generating, and offers registration whenever unregistered NIGHT
602
+ exists (previously the CTA vanished after the first registration).
603
+
604
+ `DustGeneration` also gains `newestRegisteredAt`, and `clearDustSyncCache`
605
+ evicts only the dust sub-wallet's cached state. Together they power the
606
+ extension's automatic dust-view repair: when registered NIGHT older than the
607
+ grace period still has no generation records, the sync host rebuilds just the
608
+ dust view (transaction-free, cooldown-guarded) instead of requiring a manual
609
+ deregister + full resync.
610
+
611
+ `designateForDust` now rejects an invalid DUST receiver instead of silently
612
+ falling back to the wallet's own address, `dedesignateFromDust` is exported
613
+ through the browser adapter, and the extension's DUST screen gains a "Stop
614
+ generating" flow plus a receiver-address field on registration (prefilled with
615
+ the wallet's own DUST address; any valid DUST address is accepted).
616
+
617
+ - 1c597ff: Add an optional auth header for the node, for endpoints that rate-limit.
618
+
619
+ preprod's node answers 403 to unauthenticated callers and accepts requests
620
+ carrying an operator-issued bypass header. Settings → Network gains two fields
621
+ under the endpoint URLs: a header name (pre-filled with
622
+ `x-shielded-ratelimit-bypass`, editable so a different operator or a renamed
623
+ header needs no code change) and a masked value.
624
+
625
+ **This needs `declarativeNetRequestWithHostAccess`, and that is not incidental.**
626
+ A browser cannot set headers on a WebSocket handshake — `new WebSocket(url,
627
+ protocols)` takes no header argument — and the node connection is a WebSocket, so
628
+ no JavaScript reaches it. declarativeNetRequest's `modifyHeaders` action does,
629
+ because it rewrites the request before it leaves the browser and its resource
630
+ types include `websocket`. The `WithHostAccess` variant reuses the
631
+ `https://*.midnight.network/*` host permission already declared rather than
632
+ widening access; it grants header rewriting on those hosts and nothing else.
633
+
634
+ Scoped to the node host only. The indexer is not rate-limited today, and a
635
+ credential should reach as few destinations as possible. The rule is dynamic
636
+ rather than a static ruleset — a static one would ship in the package — and is
637
+ removed when the header is cleared or the node URL changes, so a credential is
638
+ never left pointing at a host the wallet no longer uses.
639
+
640
+ The value is a secret and is treated as one: masked in the UI, never logged,
641
+ excluded from developer mode (which shows endpoint, HTTP status and attempt
642
+ count), and excluded from the diagnostics export, which promises "labels,
643
+ durations and sizes only". It ships with an empty value — only the header _name_
644
+ has a default — so no credential can enter the package.
645
+
646
+ Worth stating plainly: it is stored in `storage.local`, **not** in the encrypted
647
+ keystore, so unlike seeds it is not protected by the wallet passphrase and is
648
+ readable by anything with access to the browser profile. The help text says so.
649
+
650
+ Both the name and value are re-validated at the background boundary rather than
651
+ trusted from the panel: names must match the RFC 7230 token characters and values
652
+ must contain no CR or LF, so neither can smuggle a second header. Surrounding
653
+ whitespace is trimmed, since pasting a token reliably introduces it.
654
+
655
+ Two related fixes this exposed. `endpointOverridesFor` collapsed to `null`
656
+ whenever the URLs matched the preset, which would have silently discarded a
657
+ header set against default endpoints — the common case, since the endpoint
658
+ needing the header is the preset one. And `getNetworkConfig` did not carry the
659
+ header through, so nothing downstream could see it.
660
+
661
+ - 24cb16c: Keep each network's synced state and birthday, so switching networks stops costing a full rescan.
662
+
663
+ Switching a wallet between networks used to be doubly expensive, and the second
664
+ cost was the larger one.
665
+
666
+ `setNetwork` discarded the wallet's `birthday` on every move. That is defensible
667
+ in isolation — a height on preprod means nothing on preview — but `syncEnsure`
668
+ passes `isNewWallet: false`, so the pre-seed block is entered only when a birthday
669
+ exists. A switched wallet therefore had no birthday anywhere, could never satisfy
670
+ `reference.height <= birthday`, and walked from genesis on every network it
671
+ touched, no matter how many references were built or shipped.
672
+
673
+ `walletSetNetwork` also cleared the sync cache for BOTH sides of the move, on the
674
+ grounds that "switching back must also perform a fresh scan rather than revive
675
+ state the user explicitly reset" — which conflated switching networks with
676
+ resetting sync state. Resetting is its own deliberate action on the DUST screen.
677
+ The keys were already per-network (`sync/<networkId>/<wallet>/<part>.dat`), so
678
+ the wipe was a choice, not a constraint, and it grew expensive as dust came to
679
+ dominate: a return trip meant 78.6 min on preprod.
680
+
681
+ Birthdays are now per network — `birthdays: Record<string, number>` — recorded on
682
+ first arrival and never overwritten on return, because a wallet may have
683
+ transacted on a network before leaving and a later tip would skip that history.
684
+ The legacy single value is folded into the map under the network it belonged to
685
+ rather than dropped.
686
+
687
+ Recording is gated on a new explicit `createdHere` flag. An imported wallet may
688
+ hold funds on any chain at any height, so it never gets a birthday and keeps
689
+ scanning from genesis. That distinction used to be implied by `birthday` being
690
+ present at all (generate set it, import did not); splitting birthdays per network
691
+ dissolved that signal, so it is now stored outright. Wallets written before the
692
+ field exists read as imported — the conservative direction, since a slow sync
693
+ costs time whereas the opposite error hides funds.
694
+
695
+ Sync caches are no longer cleared on a network switch. Both networks' state
696
+ coexists and a return trip resumes from the last known good state, exactly as an
697
+ ordinary restart does. Indexer changes still clear, since a different indexer can
698
+ disagree about history.
699
+
700
+ The Settings copy said "Changing the network or indexer clears local sync data";
701
+ that is now only true of the indexer, and the switch dialog no longer warns about
702
+ losing state it keeps.
703
+
704
+ - 1f69f66: Add `refreshEmptyRefCache` — sync an existing reference forward instead of rebuilding it.
705
+
706
+ `ensureEmptyRefCache` short-circuits on a warm reference, so updating one appeared
707
+ to require deleting it and walking the chain again: 53.6 min on preprod. That was
708
+ never a limitation of the mechanism, only of the entry points.
709
+ `buildEmptyRefCache` already resumed from whatever reference state the store held
710
+ — it syncs under `EMPTY_REF_WALLET`, and `startWalletSync` restores that wallet's
711
+ cache like any other. The only thing in the way was the early return.
712
+
713
+ `refreshEmptyRefCache` bypasses it, sharing the same in-flight dedup so a refresh
714
+ cannot start a second chain walk beside a build already running.
715
+
716
+ Measured on preview: **9.1s to advance 25,660 blocks to zero stale**, against 96s
717
+ to rebuild the same reference from genesis — and against 53.6 min for preprod's,
718
+ which is the rebuild this would have avoided.
719
+
720
+ A stale reference is safe to use; it only means more catch-up for the wallets it
721
+ seeds, at roughly half a second per hour of age. So this is an optimisation rather
722
+ than a repair — run it before cutting a release, or on a schedule.
723
+
724
+ ADR 0004 was written on the premise that no refresh path existed and made building
725
+ one the precondition for the CI and distribution work. That premise is corrected
726
+ there, along with everything it gated.
727
+
728
+ - fc93b31: Add account display labels and local token names.
729
+
730
+ `WalletManager.setLabel` stores a user-chosen display label in the wallet's
731
+ metadata (surfaced on `WalletInfo`/`UnlockedWallet`); the storage name stays the
732
+ immutable key, so keystores, sync caches and sessions survive renames. The
733
+ extension exposes this as "Rename account" and also lets shielded token rows be
734
+ given local display names.
735
+
736
+ - 6766583: Make the phase-timings recorder storage-agnostic, so the CLI, TUI and daemon can use it.
737
+
738
+ The recorder was extension-only by construction: it imported `wxt/browser` and
739
+ wrote to `storage.local`, so the three Node surfaces had no equivalent even though
740
+ they run the same core sync engine and emit the same progress stream.
741
+
742
+ The arithmetic and the policy are identical everywhere — delta from the previous
743
+ entry, a bounded history, an enabled cache so the hot path does not hit storage,
744
+ and a never-throw guarantee so an instrument can never break the path it measures.
745
+ Only persistence differs. So persistence is now a four-method `TimingStore`
746
+ interface and everything else lives in `diagnostics/timings.ts`, isomorphic and
747
+ dependency-free: no node builtins, no extension APIs, no WASM. Same split as
748
+ `storage/adapter.ts` versus `storage/fs-adapter.ts`.
749
+
750
+ Three stores ship: `FilesystemTimingStore` (`~/.moth/timings.json`, for CLI/TUI/
751
+ daemon), the extension's `storage.local` adapter, and an in-memory one for tests
752
+ and short-lived processes. Because the shape is shared, a timeline captured from
753
+ the CLI is directly comparable with one from the panel.
754
+
755
+ Two behaviours are deliberate and pinned by tests. A store that cannot answer is
756
+ treated as disabled rather than raising, since recording is the optional
757
+ behaviour and the failure mode must be "no data", never "broken wallet". But
758
+ `setEnabled` and `clear` still propagate: those are deliberate user actions with
759
+ UI behind them, and silently doing nothing would leave a toggle lying about its
760
+ own state. `clear()` drops entries while keeping the enabled flag, which is how
761
+ one phase gets isolated mid-session without losing what follows.
762
+
763
+ The extension's public API is unchanged — `record`, `getTimings`, `clearTimings`,
764
+ `setTimingsEnabled`, `timingsEnabled`, `MAX_ENTRIES` and `TimingEntry` all keep
765
+ their names and behaviour, so no call site moved.
766
+
767
+ docs/BENCHMARKING.md gains a CLI/TUI/daemon section, documents
768
+ `dust-proving-check.mjs` alongside the other two instruments, corrects the setup
769
+ command (the repo pins yarn; corepack rejects npm), notes that a slow `submitting`
770
+ row is usually the relay backoff rather than the wallet, and flags that
771
+ `sync-benchmark --json`'s `percentage` field changed meaning when progress moved
772
+ to the slowest sub-wallet.
773
+
774
+ ### Patch Changes
775
+
776
+ - fc93b31: Expose the outgoing transfer count on activity entries.
777
+
778
+ `ActivityEntry` gains `outputs` — the number of external destination outputs a
779
+ (possibly batched) transaction carried, counted from the external unshielded
780
+ created UTxOs the wallet can see. This lets the UI represent a multi-transfer
781
+ send as one entry that reads "3 transfers" instead of silently showing only the
782
+ first output. Shielded recipients are unknowable (their notes can't be
783
+ decrypted), so a shielded-only batch reports 0 and the UI falls back to the
784
+ count of distinct tokens moved.
785
+
786
+ - fc93b31: Count booked (pending) unshielded inputs in the reported balance.
787
+
788
+ A send or DUST registration reserves its own NIGHT UTxOs (moved from available
789
+ to pending) while the transaction is in flight, settling them back to the
790
+ wallet on apply. The balance previously counted only available coins, so a
791
+ full-balance registration flashed the displayed balance down to zero until the
792
+ transaction applied. Unshielded pending holds only these booked inputs — never
793
+ incoming coins — so folding them into the balance is safe and never
794
+ over-counts receipts. Shielded is unchanged (its pending includes incoming).
795
+
796
+ - 12881a3: Make sync-benchmark actually measure the pre-seeded path.
797
+
798
+ The script could not measure the thing it exists to measure, and said nothing
799
+ about it. Two independent faults, either of which was sufficient:
800
+
801
+ - The measured wallet was given a bare `InMemorySyncStateStore`, but
802
+ `ensureEmptyRefCache` looks for the reference in whatever store the wallet was
803
+ given. So it searched an empty store, found nothing, and measured the unseeded
804
+ path. A run in the same process as `--warm-reference` only found the reference
805
+ through the module-level `refCache`, never from disk.
806
+ - The birthday was read before warming. Warming takes minutes to an hour and the
807
+ chain moves under it, so the birthday came out older than the reference the run
808
+ had just built, `reference.height <= birthday` failed, and the guard correctly
809
+ refused to seed — while the run announced "reference ready … should start at
810
+ tip".
811
+
812
+ Both are fixed. The birthday is now read after any warm, and the measured wallet
813
+ gets an overlay store that reads the reference keys through to disk while keeping
814
+ every write in memory — so a run still never leaves wallet state in `~/.moth`,
815
+ and never mutates the reference it is measuring against.
816
+
817
+ Verified on preview: 94.5s unseeded, **2.2s** seeded, 96.0s to build the
818
+ reference. Previously both modes reported 94.5s.
819
+
820
+ `emptyRefHeightKey` is now exported from the core barrel; the overlay needs it to
821
+ know which keys belong to the reference.
822
+
823
+ This also means any previously recorded "warm reference" figure came from a run
824
+ that measured the unseeded path, and should be re-measured rather than trusted.
825
+ docs/BENCHMARKING.md now says so, documents the `Pre-seed complete` line to check
826
+ for, and records the preview numbers.
827
+
828
+ - c7d1ef7: Apply the SDK console-noise filter in the browser, not just in Node.
829
+
830
+ `SDK_NOISE` has always listed the strings the wallet SDK and @polkadot emit on
831
+ every reconnect — `API-WS`, `disconnected from`, `Abnormal Closure`,
832
+ `RPC-CORE`, `subscribeRuntimeVersion`. But the installer bailed out when
833
+ `process` was undefined, so the CLI, TUI and daemon got a clean console while the
834
+ extension — the surface most users actually see — got all of it.
835
+
836
+ The console patch now runs everywhere. The stdout/stderr interception stays
837
+ Node-only, since @polkadot's logger writes directly to those streams there and a
838
+ browser worker has no equivalent; so does the `unhandledRejection` handler,
839
+ deliberately, because a swallowed rejection in the worker would hide failures the
840
+ extension has no other channel to report.
841
+
842
+ The curated list is unchanged, and the rule behind it still holds: specific
843
+ strings only, never broad patterns that could swallow a security-relevant error.
844
+
845
+ One thing this cannot reach: `WebSocket connection to '…' failed: 403`. Chrome
846
+ emits that from its network stack rather than from JavaScript, so no console
847
+ patch touches it. That is the right outcome — it is not benign. It means the node
848
+ is genuinely unreachable, and the relay banner now says so in the UI while the
849
+ backoff keeps the repetition to roughly one line a minute instead of twenty-four.
850
+
851
+ - 6f914fc: Clear nine Dependabot advisories in transitive build dependencies.
852
+
853
+ All ten open alerts are transitive, and every one arrives through build or dev
854
+ tooling — `web-ext` (via WXT's Firefox support), `changesets`, `vite`,
855
+ `node-notifier`. None is reachable from the shipped extension bundle, checked
856
+ against `.output/chrome-mv3`. That lowers the severity in practice but does not
857
+ make them worth leaving.
858
+
859
+ Resolved via `resolutions`, which this repo already uses for the same purpose:
860
+
861
+ | package | was | now | severity |
862
+ | --------------- | ------ | ------ | -------- |
863
+ | shell-quote | 1.7.3 | 1.10.0 | critical |
864
+ | adm-zip | 0.5.18 | 0.6.0 | high |
865
+ | brace-expansion | 5.0.7 | 5.0.9 | high |
866
+ | js-yaml | 4.3.0 | 4.3.1 | high |
867
+ | tmp | 0.2.5 | 0.2.7 | high |
868
+ | uuid | 8.3.2 | 11.1.1 | medium |
869
+ | esbuild | — | 0.28.2 | low |
870
+
871
+ Pinned to the lowest patched major rather than `>=`. An open-ended range let
872
+ yarn take js-yaml 5.2.3 and uuid 14.0.1 — several majors beyond what the
873
+ advisories require, and a far larger change than the fix warrants.
874
+
875
+ `elliptic` (low, via `browserify-sign`) has no patched version published and is
876
+ left open. It is a transitive dependency of a build-time crypto shim, not of the
877
+ wallet's own cryptography, which runs through the Midnight ledger WASM.
878
+
879
+ - 771338d: Tell the user when registration needs time, instead of failing and blaming the
880
+ proof server.
881
+
882
+ Registering NIGHT for DUST generation is self-funding: a `DustRegistration`
883
+ carries `allow_fee_payment`, and the ledger lets the transaction pay its own fee
884
+ from the DUST its NIGHT _would have_ generated had it been registered all along.
885
+ That is what stops the obvious deadlock — DUST pays fees, and registering is how
886
+ you get DUST.
887
+
888
+ Self-funding is not free. `generationless_fee_availability` caps the backdated
889
+ amount at `elapsed × night_value × generation_decay_rate`, which starts at zero.
890
+ So a freshly funded wallet cannot cover the fee yet, and the wait is _inversely
891
+ proportional to the balance_: at the ledger's defaults a 0.3 DUST fee needs ~36s
892
+ at 1,000 NIGHT, ~6 min at 100, ~1 hour at 10, and ~10 hours at 1.
893
+
894
+ Reported from preprod as a red failure card reading "That didn't go through",
895
+ with the raw SDK message and a footnote suggesting the proof server. Two of those
896
+ three were wrong: nothing went wrong, and proving never happened — the SDK refuses
897
+ before building.
898
+
899
+ `estimateRegistrationAffordability` (core, pure, WASM-free) turns the SDK's
900
+ per-UTxO figures into an answer: affordable now, affordable in N seconds, or never
901
+ at this holding. That last case matters — when the ceiling is below the fee,
902
+ "wait" is the wrong advice and "hold more NIGHT" is the right one.
903
+
904
+ `designateForDust` throws `DustRegistrationNotYetError` carrying that estimate.
905
+ Because the guard sits in core, it reaches every run mode at once — extension,
906
+ CLI, TUI and daemon RPC all route through the same function.
907
+
908
+ Two decisions worth recording. The estimate is computed only on the failure path,
909
+ so a registration that was always going to succeed pays nothing for it. And
910
+ whether to raise the typed error is decided by the affordability numbers, not by
911
+ matching the SDK's message text — string-matching would need re-matching on every
912
+ SDK release, and would fail silently when it drifted.
913
+
914
+ The panel now shows "Not quite yet", says nothing was spent, and gives a localized
915
+ wait ("Ready in about 8 hours"). `mayBeProvingFailure` suppresses the proof-server
916
+ footnote for every outcome decided before proving.
917
+
918
+ `moth dust register` gains a pre-flight and `--wait` (with `--wait-timeout`). The
919
+ pre-flight matters more on the CLI than in the panel: without it the only way to
920
+ learn the wait is to fail, and re-running means paying for a full sync first.
921
+ `--wait` polls rather than sleeping the predicted duration blind, since the
922
+ estimate moves if the wallet's NIGHT changes underneath it.
923
+
924
+ Also corrects the documentation. Four files stated that the ledger imposes a 3h
925
+ grace period before DUST appears. `dust_grace_period` is 3 hours, but it bounds how
926
+ stale a transaction's declared `ctime` may be — it is not a delay before
927
+ generation starts, which is linear from the UTxO's creation with a time-to-cap of
928
+ about a week. The observation behind the claim was real; the mechanism was
929
+ invented to fit it. The guides are corrected in place; ADR 0003 is annotated
930
+ rather than rewritten, since it is a dated record of what was decided.
931
+
932
+ - fc93b31: Fix contract circuit calls being rejected with "expected proof-preimage-versioned".
933
+
934
+ `callCircuit` previously used a hand-rolled proof provider that POSTed the bare
935
+ proof-preimage, producing an unversioned `midnight:proof-preimage:` payload that
936
+ ledger-v8's `check` rejects. Circuit calls now generate proofs through the
937
+ selectable proof provider: for server proving it routes through the SDK's
938
+ proving provider and the ledger's versioned `createProvingPayload`/
939
+ `createCheckPayload` (attaching the circuit's wrapped-IR from the ZK config),
940
+ and it also supports local WASM proving. Verified with an on-chain preprod mint.
941
+
942
+ - ba86b72: Bump ws from 8.20.1 to 8.21.0.
943
+ - fc93b31: Fix the transparent keystore KDF upgrade writing to a phantom storage key
944
+ (`wallets/<name>` instead of `wallets/<name>.keystore`). The re-encryption to
945
+ stronger scrypt parameters never persisted, so it re-ran on every unlock and v1
946
+ keystores were never actually upgraded. It now writes back to the real keystore
947
+ path and upgrades once.
948
+ - b7e2f00: add local network support
949
+ - 0f9369f: Pre-seed each sub-wallet independently, so a DUST rebuild stops walking from genesis.
950
+
951
+ The pre-seed gate tested the SHIELDED cache alone, as a proxy for "this wallet has
952
+ no state yet". That proxy failed exactly where it mattered most.
953
+
954
+ `clearDustSyncCache` evicts the dust cache and nothing else, which is what the DUST
955
+ screen's "Rebuild records" does. Shielded was therefore still present, the gate
956
+ stayed shut, and dust walked all 1.4M events from genesis — 78.6 min on preprod —
957
+ with a perfectly usable reference sitting in the store untouched. "Rebuild
958
+ records" is precisely what a user reaches for when dust looks wrong, so the
959
+ narrow, careful-looking operation was the slowest to recover, while a full
960
+ indexer-change wipe (which clears all four parts) re-seeded and finished in
961
+ seconds.
962
+
963
+ The gate now opens when ANY seedable part is missing, and each part is written
964
+ only where absent — a part that already has a cache is at least as far along as
965
+ the reference, so seeding over it would discard progress.
966
+
967
+ Mixed heights are coherent, and were verified rather than assumed. The
968
+ sub-wallets carry independent cursors, so dust can restore at the reference's
969
+ height while shielded and unshielded resume at tip, each catching up on its own
970
+ stream. Two measurements on preview:
971
+
972
+ - dust rewound to the reference (64,771) with shielded at tip (64,982): fully
973
+ synced in 1.0s, balances identical.
974
+ - a real DUST rebuild on a funded, dust-registered wallet: `Pre-seed complete —
975
+ dust at chain tip`, dust resumed at 64,771 instead of 0, synced in 1.0s, with
976
+ NIGHT, the DUST registration and the DUST balance all preserved.
977
+
978
+ The decision moves to `sync/preseed-parts.ts`, WASM-free so it is unit-testable
979
+ without loading the ledger — the same split as `sync/progress.ts`. Its tests pin
980
+ the case that was broken: shielded and unshielded cached, dust absent, must seed
981
+ dust.
982
+
983
+ - bf49ced: Sync the pre-seed reference wallet to chain tip before serializing it.
984
+
985
+ `buildEmptyRefCache` started the reference wallet and stopped it immediately.
986
+ `startWalletSync` resolves on the first balance emission (or a 5s timeout), so
987
+ `stop()` serialized a wallet that had applied nothing: every sub-wallet snapshot
988
+ was written with `offset: 0`, which the SDK reads back as `appliedIndex: 0n` —
989
+ its "stream from genesis" sentinel. The pre-seed then reported "shielded +
990
+ unshielded + dust at chain tip" while seeding genesis, and had done so for as
991
+ long as the cache had existed. Shielded and unshielded hid it because their
992
+ genesis scan is cheap; dust made it visible as an hour of syncing.
993
+
994
+ Measured on preprod, brand-new empty wallet, cold cache:
995
+
996
+ - before: dust synced at 4715.8s, total 78.6 min (99.2% of it dust,
997
+ 1,382,732 events at ~293/s)
998
+ - after: total 49.2s, of which 46.7s is one DustLocalState.deserialize
999
+
1000
+ Building the reference now costs one full chain walk (71.3 min on preprod) per
1001
+ network per machine, so it must not sit on the wallet-startup path — waiting there
1002
+ would block the user's own wallet for over an hour. `ensureEmptyRefCache` therefore
1003
+ no longer builds by default: it returns a reference already at tip, or nothing.
1004
+ Deliberate builds go through the new `warmEmptyRefCache()`, intended for a
1005
+ background task or an explicit command. A warm on-disk reference is picked up in
1006
+ 0.02s, and a build that times out leaves its partial state for the next attempt to
1007
+ resume rather than handing out a useless snapshot.
1008
+
1009
+ `loadUsableRefStates` gates on the serialized cursor, so a reference at offset 0
1010
+ is never again treated as warm. Also corrects the docblock claiming dust cannot be
1011
+ pre-seeded: dust ledger events are global (the indexer streams `dustLedgerEvents`
1012
+ keyed by a global id) and an empty wallet has no designations of its own, so the
1013
+ reference's generation tree and cursor do transfer — now verified end to end.
1014
+
1015
+ Also adds the birthday guard that making this work turned from latent into live.
1016
+ The pre-seed condition is `(isNewWallet || birthday) && no shielded cache`, which
1017
+ admits any wallet merely missing a cache — including a funded one after a cache
1018
+ reset, a storage eviction, or a restore from mnemonic. Seeding such a wallet from
1019
+ a reference newer than its own first activity starts it past its own history and
1020
+ drops funds from view. Harmless while the reference sat at offset 0 (seeding
1021
+ genesis is always safe); a real hazard once it carries a tip cursor.
1022
+
1023
+ Pre-seeding now requires `birthday !== undefined && reference.height <= birthday`.
1024
+ The height is recorded separately at build time (`emptyRefHeightKey`) because the
1025
+ snapshots' `offset` is an event index, not a block height, and the two are not
1026
+ comparable — 1,382,805 against 1,977,245 on preprod. It is read after the sync
1027
+ completes, which can only overstate it and therefore only make the check stricter,
1028
+ and a reference with no recorded height is treated as unusable rather than
1029
+ guessed at. Existing callers are unaffected: the TUI passes `isNewWallet` without
1030
+ a birthday and the CLI passes neither, so both keep the slow path.
1031
+
1032
+ Nothing calls `warmEmptyRefCache()` yet, so no shipped surface changes behaviour:
1033
+ new wallets still sync from genesis until a caller warms the reference.
1034
+
1035
+ - 0f197e2: Preserve transaction identities in activity entries and submitted transaction records so applied transactions replace their pending rows instead of appearing as duplicates.
1036
+ - fc93b31: Fix transfer submission to return `transactionHash()` instead of the facade's
1037
+ last intent identifier. Tx history, indexer status queries and explorers are all
1038
+ keyed by the transaction hash, so the old value matched nothing — leaving the
1039
+ extension's activity feed stuck on "Pending" after a transfer had applied.
1040
+ - 2fde86f: Report sync progress from the slowest sub-wallet, not the shielded one.
1041
+
1042
+ Progress read shielded indices only, on the stated assumption that shielded was
1043
+ the slowest sub-wallet. It is not — dust is, by two orders of magnitude: a full
1044
+ dust walk is ~1.4M events at a few hundred per second, where shielded covers the
1045
+ same range in under a minute.
1046
+
1047
+ Observed on preprod: a wallet reporting "100% (0s remaining)" with dust at
1048
+ 178,029/1,395,558 and roughly 69 minutes of work left. That is worse than
1049
+ reporting nothing, because it stops the user waiting.
1050
+
1051
+ Progress is now the minimum across all three sub-wallets. A sub-wallet with
1052
+ nothing relevant to apply (total 0) counts as complete rather than stalled — a
1053
+ fresh wallet's unshielded progress is legitimately 0/0 and must not drag the
1054
+ minimum to zero. The figure never rounds up to 100% while the facade still says
1055
+ unsynced, since rendering a near-complete fraction as "100% (0s remaining)" is
1056
+ the specific lie this change exists to remove. The ETA follows the same fraction,
1057
+ so it reflects whichever sub-wallet is actually behind rather than one that
1058
+ finished a minute in.
1059
+
1060
+ The arithmetic moved to sync/progress.ts so it can be unit-tested without loading
1061
+ WASM — the same split as types/tokens.ts and the extension's dust-heal.ts.