@shieldedtech/moth-wallet 0.14.1 → 0.14.2-canary.20260928125650-b5fd114

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