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