@shieldedtech/moth-wallet 0.2.0
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/README.md +62 -0
- package/dist/contract/args-parser.d.ts +29 -0
- package/dist/contract/args-parser.d.ts.map +1 -0
- package/dist/contract/args-parser.js +123 -0
- package/dist/contract/args-parser.js.map +1 -0
- package/dist/contract/artifact-loader.d.ts +26 -0
- package/dist/contract/artifact-loader.d.ts.map +1 -0
- package/dist/contract/artifact-loader.js +129 -0
- package/dist/contract/artifact-loader.js.map +1 -0
- package/dist/contract/call.d.ts +32 -0
- package/dist/contract/call.d.ts.map +1 -0
- package/dist/contract/call.js +245 -0
- package/dist/contract/call.js.map +1 -0
- package/dist/contract/deploy.d.ts +36 -0
- package/dist/contract/deploy.d.ts.map +1 -0
- package/dist/contract/deploy.js +388 -0
- package/dist/contract/deploy.js.map +1 -0
- package/dist/contract/fungible-token.d.ts +40 -0
- package/dist/contract/fungible-token.d.ts.map +1 -0
- package/dist/contract/fungible-token.js +88 -0
- package/dist/contract/fungible-token.js.map +1 -0
- package/dist/contract/initial-private-state.d.ts +18 -0
- package/dist/contract/initial-private-state.d.ts.map +1 -0
- package/dist/contract/initial-private-state.js +44 -0
- package/dist/contract/initial-private-state.js.map +1 -0
- package/dist/contract/maintenance.d.ts +72 -0
- package/dist/contract/maintenance.d.ts.map +1 -0
- package/dist/contract/maintenance.js +430 -0
- package/dist/contract/maintenance.js.map +1 -0
- package/dist/contract/state.d.ts +16 -0
- package/dist/contract/state.d.ts.map +1 -0
- package/dist/contract/state.js +18 -0
- package/dist/contract/state.js.map +1 -0
- package/dist/contract/witness-loader.d.ts +3 -0
- package/dist/contract/witness-loader.d.ts.map +1 -0
- package/dist/contract/witness-loader.js +27 -0
- package/dist/contract/witness-loader.js.map +1 -0
- package/dist/daemon/api-keys.d.ts +66 -0
- package/dist/daemon/api-keys.d.ts.map +1 -0
- package/dist/daemon/api-keys.js +222 -0
- package/dist/daemon/api-keys.js.map +1 -0
- package/dist/daemon/audit-log.d.ts +65 -0
- package/dist/daemon/audit-log.d.ts.map +1 -0
- package/dist/daemon/audit-log.js +73 -0
- package/dist/daemon/audit-log.js.map +1 -0
- package/dist/daemon/client.d.ts +63 -0
- package/dist/daemon/client.d.ts.map +1 -0
- package/dist/daemon/client.js +214 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/confirmation-queue.d.ts +46 -0
- package/dist/daemon/confirmation-queue.d.ts.map +1 -0
- package/dist/daemon/confirmation-queue.js +92 -0
- package/dist/daemon/confirmation-queue.js.map +1 -0
- package/dist/daemon/index.d.ts +23 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +26 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/protocol.d.ts +40 -0
- package/dist/daemon/protocol.d.ts.map +1 -0
- package/dist/daemon/protocol.js +104 -0
- package/dist/daemon/protocol.js.map +1 -0
- package/dist/daemon/server.d.ts +158 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +297 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/wallet-handlers.d.ts +37 -0
- package/dist/daemon/wallet-handlers.d.ts.map +1 -0
- package/dist/daemon/wallet-handlers.js +533 -0
- package/dist/daemon/wallet-handlers.js.map +1 -0
- package/dist/daemon/wallet-rpc-parsers.d.ts +14 -0
- package/dist/daemon/wallet-rpc-parsers.d.ts.map +1 -0
- package/dist/daemon/wallet-rpc-parsers.js +259 -0
- package/dist/daemon/wallet-rpc-parsers.js.map +1 -0
- package/dist/daemon/wallet-rpc-types.d.ts +138 -0
- package/dist/daemon/wallet-rpc-types.d.ts.map +1 -0
- package/dist/daemon/wallet-rpc-types.js +11 -0
- package/dist/daemon/wallet-rpc-types.js.map +1 -0
- package/dist/diagnostics/fs-timing-store.d.ts +29 -0
- package/dist/diagnostics/fs-timing-store.d.ts.map +1 -0
- package/dist/diagnostics/fs-timing-store.js +81 -0
- package/dist/diagnostics/fs-timing-store.js.map +1 -0
- package/dist/diagnostics/timings.d.ts +64 -0
- package/dist/diagnostics/timings.d.ts.map +1 -0
- package/dist/diagnostics/timings.js +100 -0
- package/dist/diagnostics/timings.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/network/indexer-client.d.ts +66 -0
- package/dist/network/indexer-client.d.ts.map +1 -0
- package/dist/network/indexer-client.js +112 -0
- package/dist/network/indexer-client.js.map +1 -0
- package/dist/network/node-client.d.ts +27 -0
- package/dist/network/node-client.d.ts.map +1 -0
- package/dist/network/node-client.js +94 -0
- package/dist/network/node-client.js.map +1 -0
- package/dist/proof/client.d.ts +15 -0
- package/dist/proof/client.d.ts.map +1 -0
- package/dist/proof/client.js +77 -0
- package/dist/proof/client.js.map +1 -0
- package/dist/proof/provider.d.ts +12 -0
- package/dist/proof/provider.d.ts.map +1 -0
- package/dist/proof/provider.js +96 -0
- package/dist/proof/provider.js.map +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +2 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/midnight-provider.d.ts +11 -0
- package/dist/providers/midnight-provider.d.ts.map +1 -0
- package/dist/providers/midnight-provider.js +30 -0
- package/dist/providers/midnight-provider.js.map +1 -0
- package/dist/storage/adapter.d.ts +8 -0
- package/dist/storage/adapter.d.ts.map +1 -0
- package/dist/storage/adapter.js +2 -0
- package/dist/storage/adapter.js.map +1 -0
- package/dist/storage/fs-adapter.d.ts +14 -0
- package/dist/storage/fs-adapter.d.ts.map +1 -0
- package/dist/storage/fs-adapter.js +126 -0
- package/dist/storage/fs-adapter.js.map +1 -0
- package/dist/storage/safe-path.d.ts +13 -0
- package/dist/storage/safe-path.d.ts.map +1 -0
- package/dist/storage/safe-path.js +35 -0
- package/dist/storage/safe-path.js.map +1 -0
- package/dist/sync/activity.d.ts +53 -0
- package/dist/sync/activity.d.ts.map +1 -0
- package/dist/sync/activity.js +92 -0
- package/dist/sync/activity.js.map +1 -0
- package/dist/sync/dust-registration-estimate.d.ts +54 -0
- package/dist/sync/dust-registration-estimate.d.ts.map +1 -0
- package/dist/sync/dust-registration-estimate.js +103 -0
- package/dist/sync/dust-registration-estimate.js.map +1 -0
- package/dist/sync/node-sync-store.d.ts +10 -0
- package/dist/sync/node-sync-store.d.ts.map +1 -0
- package/dist/sync/node-sync-store.js +42 -0
- package/dist/sync/node-sync-store.js.map +1 -0
- package/dist/sync/operations.d.ts +138 -0
- package/dist/sync/operations.d.ts.map +1 -0
- package/dist/sync/operations.js +395 -0
- package/dist/sync/operations.js.map +1 -0
- package/dist/sync/preseed-parts.d.ts +18 -0
- package/dist/sync/preseed-parts.d.ts.map +1 -0
- package/dist/sync/preseed-parts.js +33 -0
- package/dist/sync/preseed-parts.js.map +1 -0
- package/dist/sync/preseed.d.ts +95 -0
- package/dist/sync/preseed.d.ts.map +1 -0
- package/dist/sync/preseed.js +365 -0
- package/dist/sync/preseed.js.map +1 -0
- package/dist/sync/progress.d.ts +45 -0
- package/dist/sync/progress.d.ts.map +1 -0
- package/dist/sync/progress.js +51 -0
- package/dist/sync/progress.js.map +1 -0
- package/dist/sync/sdk-dedup.d.ts +53 -0
- package/dist/sync/sdk-dedup.d.ts.map +1 -0
- package/dist/sync/sdk-dedup.js +134 -0
- package/dist/sync/sdk-dedup.js.map +1 -0
- package/dist/sync/sync-store.d.ts +33 -0
- package/dist/sync/sync-store.d.ts.map +1 -0
- package/dist/sync/sync-store.js +45 -0
- package/dist/sync/sync-store.js.map +1 -0
- package/dist/sync/wallet-sync.d.ts +153 -0
- package/dist/sync/wallet-sync.d.ts.map +1 -0
- package/dist/sync/wallet-sync.js +943 -0
- package/dist/sync/wallet-sync.js.map +1 -0
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +33 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/exit-codes.d.ts +8 -0
- package/dist/types/exit-codes.d.ts.map +1 -0
- package/dist/types/exit-codes.js +7 -0
- package/dist/types/exit-codes.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/network.d.ts +65 -0
- package/dist/types/network.d.ts.map +1 -0
- package/dist/types/network.js +108 -0
- package/dist/types/network.js.map +1 -0
- package/dist/types/tokens.d.ts +3 -0
- package/dist/types/tokens.d.ts.map +1 -0
- package/dist/types/tokens.js +11 -0
- package/dist/types/tokens.js.map +1 -0
- package/dist/types/transaction.d.ts +13 -0
- package/dist/types/transaction.d.ts.map +1 -0
- package/dist/types/transaction.js +2 -0
- package/dist/types/transaction.js.map +1 -0
- package/dist/types/wallet.d.ts +82 -0
- package/dist/types/wallet.d.ts.map +1 -0
- package/dist/types/wallet.js +2 -0
- package/dist/types/wallet.js.map +1 -0
- package/dist/wallet/address-validate.d.ts +7 -0
- package/dist/wallet/address-validate.d.ts.map +1 -0
- package/dist/wallet/address-validate.js +28 -0
- package/dist/wallet/address-validate.js.map +1 -0
- package/dist/wallet/address.d.ts +32 -0
- package/dist/wallet/address.d.ts.map +1 -0
- package/dist/wallet/address.js +107 -0
- package/dist/wallet/address.js.map +1 -0
- package/dist/wallet/app-secret.d.ts +15 -0
- package/dist/wallet/app-secret.d.ts.map +1 -0
- package/dist/wallet/app-secret.js +45 -0
- package/dist/wallet/app-secret.js.map +1 -0
- package/dist/wallet/balance-format.d.ts +12 -0
- package/dist/wallet/balance-format.d.ts.map +1 -0
- package/dist/wallet/balance-format.js +51 -0
- package/dist/wallet/balance-format.js.map +1 -0
- package/dist/wallet/batch-transfer.d.ts +36 -0
- package/dist/wallet/batch-transfer.d.ts.map +1 -0
- package/dist/wallet/batch-transfer.js +93 -0
- package/dist/wallet/batch-transfer.js.map +1 -0
- package/dist/wallet/keystore.d.ts +16 -0
- package/dist/wallet/keystore.d.ts.map +1 -0
- package/dist/wallet/keystore.js +76 -0
- package/dist/wallet/keystore.js.map +1 -0
- package/dist/wallet/manager.d.ts +77 -0
- package/dist/wallet/manager.d.ts.map +1 -0
- package/dist/wallet/manager.js +474 -0
- package/dist/wallet/manager.js.map +1 -0
- package/dist/wallet/mnemonic.d.ts +5 -0
- package/dist/wallet/mnemonic.d.ts.map +1 -0
- package/dist/wallet/mnemonic.js +28 -0
- package/dist/wallet/mnemonic.js.map +1 -0
- package/dist/wallet/sign-message.d.ts +20 -0
- package/dist/wallet/sign-message.d.ts.map +1 -0
- package/dist/wallet/sign-message.js +58 -0
- package/dist/wallet/sign-message.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
// Wallet sync engine using WalletFacade for real balance tracking.
|
|
2
|
+
// Caches serialized sync state via a SyncStateStore so subsequent launches
|
|
3
|
+
// are fast. This module must stay free of static node:*/ws imports and
|
|
4
|
+
// top-level side effects so it can be bundled for the browser.
|
|
5
|
+
// See NOTICE for attribution.
|
|
6
|
+
import * as Rx from 'rxjs';
|
|
7
|
+
import * as ledger from '@midnight-ntwrk/ledger-v8';
|
|
8
|
+
import { WalletFacade } from '@midnightntwrk/wallet-sdk/facade';
|
|
9
|
+
import { makeDefaultSubmissionService, } from '@midnightntwrk/wallet-sdk/capabilities/submission';
|
|
10
|
+
// CustomDustWallet/CustomShieldedWallet let v8's dedup builders wrap the sync
|
|
11
|
+
// pipeline (see sdk-dedup); the plain wallets remain for non-deduped paths.
|
|
12
|
+
import { CustomDustWallet } from '@midnightntwrk/wallet-sdk/dust';
|
|
13
|
+
import { CustomShieldedWallet } from '@midnightntwrk/wallet-sdk/shielded';
|
|
14
|
+
import { UnshieldedWallet, PublicKey, createKeystore } from '@midnightntwrk/wallet-sdk/unshielded';
|
|
15
|
+
import { InMemoryTransactionHistoryStorage } from '@midnightntwrk/wallet-sdk';
|
|
16
|
+
import { WalletEntrySchema, mergeWalletEntries } from '@midnightntwrk/wallet-sdk/facade';
|
|
17
|
+
import { setNetworkId } from '@midnight-ntwrk/midnight-js/network-id';
|
|
18
|
+
import { resolveProverConfig } from '../types/network.js';
|
|
19
|
+
import { createWalletProvingService } from '../proof/provider.js';
|
|
20
|
+
import { NIGHT_TOKEN_ID, formatNight } from '../types/tokens.js';
|
|
21
|
+
import { formatDustBalance } from '../wallet/balance-format.js';
|
|
22
|
+
import { ensureEmptyRefCache, preSeedNewWallet } from './preseed.js';
|
|
23
|
+
import { InMemorySyncStateStore, syncStateKey } from './sync-store.js';
|
|
24
|
+
import { dedupingShieldedBuilder, dedupingDustBuilder } from './sdk-dedup.js';
|
|
25
|
+
import { overallSyncProgress } from './progress.js';
|
|
26
|
+
import { partsToSeed } from './preseed-parts.js';
|
|
27
|
+
// Re-exported so existing importers (core/browser barrels, CLI/TUI) keep working;
|
|
28
|
+
// the definitions live in the WASM-free ../types/tokens module.
|
|
29
|
+
export { NIGHT_TOKEN_ID, formatNight };
|
|
30
|
+
/**
|
|
31
|
+
* A submission service that resolves as soon as the transaction is accepted
|
|
32
|
+
* into the pool ('Submitted'), rather than blocking until 'Finalized' like the
|
|
33
|
+
* SDK default.
|
|
34
|
+
*
|
|
35
|
+
* `WalletFacade.submitTransaction` always asks its submission service for
|
|
36
|
+
* 'Finalized', so a send blocks the whole round-trip on block finalization
|
|
37
|
+
* (12–60s+). In the extension that round-trip is a Chrome MV3 runtime message,
|
|
38
|
+
* whose port can't outlive that wait — it closes, the UI sees "No response",
|
|
39
|
+
* and shows a failure even though the transaction was submitted fine and lands.
|
|
40
|
+
*
|
|
41
|
+
* What matters for the send flow is that the transaction reached the pool;
|
|
42
|
+
* inclusion/finalization is reflected afterwards through normal sync and tx
|
|
43
|
+
* history. So we hand the facade a service that ignores the requested level and
|
|
44
|
+
* always resolves at 'Submitted'. The facade still records the pending
|
|
45
|
+
* transaction around this call (optimistic balances) and reverts it if
|
|
46
|
+
* submission is rejected.
|
|
47
|
+
*/
|
|
48
|
+
function makeSubmittedOnlySubmissionService(relayURL) {
|
|
49
|
+
const inner = makeDefaultSubmissionService({ relayURL });
|
|
50
|
+
return {
|
|
51
|
+
submitTransaction: ((tx) => inner.submitTransaction(tx, 'Submitted')),
|
|
52
|
+
close: () => inner.close(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* WebSocket polyfill for Node.js (wallet SDK assumes browser WebSocket).
|
|
57
|
+
* Browsers/service workers have a global WebSocket, so the `ws` package is
|
|
58
|
+
* only loaded — lazily — when the global is missing.
|
|
59
|
+
*/
|
|
60
|
+
async function ensureWebSocket() {
|
|
61
|
+
if (typeof globalThis.WebSocket !== 'undefined')
|
|
62
|
+
return;
|
|
63
|
+
// Specifier goes through a variable so bundlers can't statically follow it
|
|
64
|
+
// into browser builds (browsers always have a global WebSocket anyway).
|
|
65
|
+
const specifier = 'ws';
|
|
66
|
+
const { WebSocket } = await import(/* @vite-ignore */ specifier);
|
|
67
|
+
globalThis.WebSocket = WebSocket;
|
|
68
|
+
}
|
|
69
|
+
// Suppress noisy SDK console + stdout output.
|
|
70
|
+
// The @polkadot/util logger writes directly to process.stdout, not console.
|
|
71
|
+
// Suppress known-noisy SDK log messages. Match specific strings only —
|
|
72
|
+
// never broad patterns like '_tag' that could swallow security-relevant errors.
|
|
73
|
+
const SDK_NOISE = [
|
|
74
|
+
'RPC-CORE',
|
|
75
|
+
'API-WS',
|
|
76
|
+
'subscribeRuntimeVersion',
|
|
77
|
+
'disconnected from',
|
|
78
|
+
'Normal Closure',
|
|
79
|
+
'Abnormal Closure',
|
|
80
|
+
'Error processing tx history',
|
|
81
|
+
'Wallet.Sync',
|
|
82
|
+
'ws://localhost:9944',
|
|
83
|
+
];
|
|
84
|
+
function isSdkNoise(msg) {
|
|
85
|
+
return SDK_NOISE.some((pattern) => msg.includes(pattern));
|
|
86
|
+
}
|
|
87
|
+
let logSuppressionInstalled = false;
|
|
88
|
+
/**
|
|
89
|
+
* Install console/stdout/stderr/unhandledRejection filters for SDK noise.
|
|
90
|
+
* Deferred to first sync so importing this module has no side effects.
|
|
91
|
+
*
|
|
92
|
+
* The console half runs EVERYWHERE, including the extension's wallet worker.
|
|
93
|
+
* It was Node-only for a long time, gated behind the `process` check below, so
|
|
94
|
+
* the browser showed every `API-WS: disconnected … Abnormal Closure` the relay
|
|
95
|
+
* produced — strings already on the curated SDK_NOISE list, filtered out of the
|
|
96
|
+
* CLI and TUI but not out of the surface most users actually see.
|
|
97
|
+
*
|
|
98
|
+
* What CANNOT be silenced here: `WebSocket connection to '…' failed: <status>`.
|
|
99
|
+
* Chrome emits that from the network stack, not from JavaScript, so no console
|
|
100
|
+
* patch reaches it. That one is not benign anyway — it means the node is genuinely
|
|
101
|
+
* unreachable, and the relay banner says so in the UI.
|
|
102
|
+
*/
|
|
103
|
+
function installLogSuppression() {
|
|
104
|
+
if (logSuppressionInstalled)
|
|
105
|
+
return;
|
|
106
|
+
logSuppressionInstalled = true;
|
|
107
|
+
// Node-only streams: the @polkadot/util logger writes directly to stdout there,
|
|
108
|
+
// bypassing console entirely. No equivalent in a browser worker.
|
|
109
|
+
if (typeof process !== 'undefined' && typeof process.stdout?.write === 'function') {
|
|
110
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
111
|
+
process.stdout.write = (chunk, ...rest) => {
|
|
112
|
+
if (typeof chunk === 'string' && isSdkNoise(chunk))
|
|
113
|
+
return true;
|
|
114
|
+
return origStdoutWrite(chunk, ...rest);
|
|
115
|
+
};
|
|
116
|
+
// Also intercept stderr — SDK sync errors go through stderr as unhandled rejections
|
|
117
|
+
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
118
|
+
process.stderr.write = (chunk, ...rest) => {
|
|
119
|
+
if (typeof chunk === 'string' && isSdkNoise(chunk))
|
|
120
|
+
return true;
|
|
121
|
+
return origStderrWrite(chunk, ...rest);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const origConsoleError = console.error;
|
|
125
|
+
const origConsoleWarn = console.warn;
|
|
126
|
+
const origConsoleLog = console.log;
|
|
127
|
+
// Check all args for noise (SDK sometimes passes objects as later arguments)
|
|
128
|
+
// Known Effect _tag values from the wallet SDK that are safe to suppress.
|
|
129
|
+
const KNOWN_NOISE_TAGS = new Set(['Wallet.Sync', 'ApiDisconnected', 'WebSocketError']);
|
|
130
|
+
function argsAreNoise(args) {
|
|
131
|
+
return args.some((a) => {
|
|
132
|
+
if (typeof a === 'string')
|
|
133
|
+
return isSdkNoise(a);
|
|
134
|
+
// Effect errors: only suppress known-safe _tag values, not arbitrary ones
|
|
135
|
+
if (a && typeof a === 'object' && typeof a._tag === 'string') {
|
|
136
|
+
return KNOWN_NOISE_TAGS.has(a._tag) || isSdkNoise(a._tag);
|
|
137
|
+
}
|
|
138
|
+
// Stringified objects — check the string representation
|
|
139
|
+
try {
|
|
140
|
+
return isSdkNoise(String(a));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
console.error = (...args) => {
|
|
148
|
+
if (argsAreNoise(args))
|
|
149
|
+
return;
|
|
150
|
+
origConsoleError.apply(console, args);
|
|
151
|
+
};
|
|
152
|
+
console.warn = (...args) => {
|
|
153
|
+
if (argsAreNoise(args))
|
|
154
|
+
return;
|
|
155
|
+
origConsoleWarn.apply(console, args);
|
|
156
|
+
};
|
|
157
|
+
console.log = (...args) => {
|
|
158
|
+
if (argsAreNoise(args))
|
|
159
|
+
return;
|
|
160
|
+
origConsoleLog.apply(console, args);
|
|
161
|
+
};
|
|
162
|
+
// Suppress unhandled rejections from SDK sync retries — these are non-fatal
|
|
163
|
+
// and the SDK handles reconnection internally. Node-only: a browser worker has
|
|
164
|
+
// no process, and its equivalent (an 'unhandledrejection' event) is not
|
|
165
|
+
// intercepted here because a swallowed rejection in the worker would hide
|
|
166
|
+
// failures the extension has no other channel to report.
|
|
167
|
+
if (typeof process === 'undefined' || typeof process.on !== 'function')
|
|
168
|
+
return;
|
|
169
|
+
process.on('unhandledRejection', (reason) => {
|
|
170
|
+
const tag = reason?._tag ?? '';
|
|
171
|
+
const msg = reason?.message ?? String(reason ?? '');
|
|
172
|
+
if (tag === 'Wallet.Sync' || isSdkNoise(msg) || isSdkNoise(tag)) {
|
|
173
|
+
// Silently swallowed — SDK will retry
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Re-throw non-SDK rejections so they're visible
|
|
177
|
+
origConsoleError.call(console, 'Unhandled rejection:', reason);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const DUST_COST_PARAMETERS = {
|
|
181
|
+
additionalFeeOverhead: 300000000000000n,
|
|
182
|
+
feeBlocksMargin: 5,
|
|
183
|
+
};
|
|
184
|
+
export const EMPTY_COINS = {
|
|
185
|
+
shielded: { available: [], pending: [] },
|
|
186
|
+
unshielded: { available: [], pending: [] },
|
|
187
|
+
dust: { available: [], pending: [] },
|
|
188
|
+
};
|
|
189
|
+
export const EMPTY_SUB_PROGRESS = {
|
|
190
|
+
shielded: { applied: 0, total: 0 },
|
|
191
|
+
unshielded: { applied: 0, total: 0 },
|
|
192
|
+
dust: { applied: 0, total: 0 },
|
|
193
|
+
};
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Sync state cache — SyncStateStore-backed, per wallet + network
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
let defaultStore = null;
|
|
198
|
+
/**
|
|
199
|
+
* Resolve the sync-state store: explicit option → Node filesystem store
|
|
200
|
+
* (legacy ~/.moth layout, loaded lazily so browser bundles never pull in
|
|
201
|
+
* node:fs) → volatile in-memory store.
|
|
202
|
+
*/
|
|
203
|
+
export async function resolveSyncStore(explicit) {
|
|
204
|
+
if (explicit)
|
|
205
|
+
return explicit;
|
|
206
|
+
if (defaultStore)
|
|
207
|
+
return defaultStore;
|
|
208
|
+
let store;
|
|
209
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
210
|
+
// Specifier goes through a variable so bundlers can't statically follow
|
|
211
|
+
// the node:fs-backed store into browser builds.
|
|
212
|
+
const specifier = './node-sync-store.js';
|
|
213
|
+
const mod = await import(/* @vite-ignore */ specifier);
|
|
214
|
+
store = new mod.NodeSyncStateStore();
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
store = new InMemorySyncStateStore();
|
|
218
|
+
}
|
|
219
|
+
defaultStore = store;
|
|
220
|
+
return store;
|
|
221
|
+
}
|
|
222
|
+
function loadCachedState(store, walletName, networkId, part) {
|
|
223
|
+
return store.get(syncStateKey(networkId, walletName, part));
|
|
224
|
+
}
|
|
225
|
+
function saveCachedState(store, walletName, networkId, part, state) {
|
|
226
|
+
return store.put(syncStateKey(networkId, walletName, part), state);
|
|
227
|
+
}
|
|
228
|
+
// Store-based (async) — keeps the browser (IndexedDB) path working; the Node
|
|
229
|
+
// filesystem cache is a concrete SyncStateStore behind resolveSyncStore. v8's
|
|
230
|
+
// sync fs-based readWalletCacheState/pre-seed bridge is re-expressed against
|
|
231
|
+
// this async store in preseed.ts rather than reintroduced here.
|
|
232
|
+
async function evictCachedState(store, walletName, networkId, part) {
|
|
233
|
+
try {
|
|
234
|
+
await store.delete(syncStateKey(networkId, walletName, part));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
/* ignore */
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// URL helpers
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
function toWsUrl(url) {
|
|
244
|
+
return url.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Bring up the WalletFacade (shielded + unshielded + dust) and start syncing.
|
|
248
|
+
* Takes `walletKeys` (the typed bundle derived once at unlock — Option A, the
|
|
249
|
+
* raw seed is never threaded here). Pre-seed of brand-new wallets derives the
|
|
250
|
+
* bundle up front (see preseed.ts) and calls this directly.
|
|
251
|
+
*/
|
|
252
|
+
/** A sub-wallet's own fraction, for the progress line. `done` wins over the
|
|
253
|
+
* counters: a sub-wallet with nothing to apply is complete, not stalled. */
|
|
254
|
+
function subPct(sub, done) {
|
|
255
|
+
if (done)
|
|
256
|
+
return '100%';
|
|
257
|
+
return sub.total > 0 ? `${Math.round(Math.min(1, sub.applied / sub.total) * 100)}%` : '100%';
|
|
258
|
+
}
|
|
259
|
+
export async function startWalletSync(keys, network, onProgress, walletName, isNewWallet,
|
|
260
|
+
/** Chain tip index at wallet creation time. Skips scanning blocks before this index on first sync. */
|
|
261
|
+
birthday, options) {
|
|
262
|
+
installLogSuppression();
|
|
263
|
+
await ensureWebSocket();
|
|
264
|
+
const store = await resolveSyncStore(options?.syncStore);
|
|
265
|
+
setNetworkId(network.id);
|
|
266
|
+
onProgress?.('Deriving keys...');
|
|
267
|
+
const name = walletName ?? 'default';
|
|
268
|
+
// Option A: keys arrive pre-derived; the seed was dropped at unlock.
|
|
269
|
+
const shieldedSecretKeys = keys.shieldedSecretKeys;
|
|
270
|
+
const dustSecretKey = keys.dustSecretKey;
|
|
271
|
+
const keystore = createKeystore(keys.nightExternalKey, network.id);
|
|
272
|
+
const indexerHttpUrl = network.indexerUrl;
|
|
273
|
+
const indexerWsUrl = toWsUrl(indexerHttpUrl) + '/ws';
|
|
274
|
+
const relayURL = new URL(toWsUrl(network.nodeUrl));
|
|
275
|
+
const prover = resolveProverConfig(network);
|
|
276
|
+
// Transaction history follows the same cache lifecycle as the sub-wallet
|
|
277
|
+
// states below: restore from the store so history survives restarts, sync
|
|
278
|
+
// only fills in what happened since.
|
|
279
|
+
const txHistoryStorage = await loadHistoryStorage(store, name, network.id, onProgress);
|
|
280
|
+
// Cast confined to the SDK's config intersection: the literal can't satisfy
|
|
281
|
+
// DefaultTransactionHistoryConfiguration structurally, so it's typed as the
|
|
282
|
+
// factory's config type, matching the prior runtime behaviour.
|
|
283
|
+
const walletCfg = {
|
|
284
|
+
networkId: network.id,
|
|
285
|
+
indexerClientConnection: { indexerHttpUrl, indexerWsUrl },
|
|
286
|
+
relayURL,
|
|
287
|
+
// Shared transaction history storage for all sub-wallets (v4 SDK requirement)
|
|
288
|
+
txHistoryStorage,
|
|
289
|
+
costParameters: DUST_COST_PARAMETERS,
|
|
290
|
+
batchUpdates: {
|
|
291
|
+
size: options?.batchUpdates?.size ?? 500,
|
|
292
|
+
timeout: options?.batchUpdates?.timeout ?? 500,
|
|
293
|
+
spacing: options?.batchUpdates?.spacing ?? 50,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
// --- Pre-seed: fill in whichever sub-wallet caches are missing ---
|
|
297
|
+
//
|
|
298
|
+
// Per-part, not all-or-nothing. This used to gate on the SHIELDED cache alone,
|
|
299
|
+
// as a proxy for "this wallet has no state yet". That proxy broke exactly where
|
|
300
|
+
// it mattered most: a DUST rebuild evicts the dust cache and nothing else, so
|
|
301
|
+
// shielded was still present, the gate was false, and dust walked all 1.4M
|
|
302
|
+
// events from genesis — 78.6 min on preprod — with a perfectly good reference
|
|
303
|
+
// sitting unused. "Rebuild records" is what a user reaches for when dust looks
|
|
304
|
+
// wrong, so the narrow, careful-looking operation was the slowest to recover
|
|
305
|
+
// while a full indexer-change wipe was fast.
|
|
306
|
+
//
|
|
307
|
+
// Mixed heights are fine, and are tested rather than assumed: the sub-wallets
|
|
308
|
+
// carry independent cursors, so dust can restore at the reference's height
|
|
309
|
+
// while shielded and unshielded resume at tip and each catches up on its own
|
|
310
|
+
// stream. Measured on preview: dust rewound to the reference (64,771) alongside
|
|
311
|
+
// shielded at tip (64,982) reached fully synced in 1.0s with identical balances.
|
|
312
|
+
const cached = {
|
|
313
|
+
shielded: await loadCachedState(store, name, network.id, 'shielded'),
|
|
314
|
+
unshielded: await loadCachedState(store, name, network.id, 'unshielded'),
|
|
315
|
+
dust: await loadCachedState(store, name, network.id, 'dust'),
|
|
316
|
+
};
|
|
317
|
+
const missingParts = partsToSeed(cached);
|
|
318
|
+
if ((isNewWallet || birthday) && missingParts.length > 0) {
|
|
319
|
+
onProgress?.('Pre-seeding new wallet from reference...');
|
|
320
|
+
try {
|
|
321
|
+
const emptyRef = await ensureEmptyRefCache(network, onProgress, store);
|
|
322
|
+
// SAFETY: only seed a wallet that cannot have had activity before the
|
|
323
|
+
// reference's height. The reference holds the chain's state at that height,
|
|
324
|
+
// so seeding an older wallet would start it past its own history and lose
|
|
325
|
+
// funds from view. `birthday` is the wallet's creation height, so a wallet
|
|
326
|
+
// created after the reference was built is safe; anything else — a restore
|
|
327
|
+
// from mnemonic, a wallet whose cache was cleared or evicted, or a missing
|
|
328
|
+
// birthday — must take the slow path instead. Without this, the guard above
|
|
329
|
+
// (`isNewWallet || birthday`) admits any wallet that merely lacks a cache.
|
|
330
|
+
const seedable = emptyRef !== null && birthday !== undefined && emptyRef.height <= birthday;
|
|
331
|
+
if (emptyRef && !seedable) {
|
|
332
|
+
onProgress?.(birthday === undefined
|
|
333
|
+
? 'Pre-seed: no wallet birthday to compare — syncing from genesis'
|
|
334
|
+
: `Pre-seed: reference is newer than this wallet (height ${emptyRef.height} > birthday ${birthday}) — syncing from genesis`);
|
|
335
|
+
}
|
|
336
|
+
if (emptyRef && seedable) {
|
|
337
|
+
const preSeeded = preSeedNewWallet(keys, network.id, emptyRef);
|
|
338
|
+
if (preSeeded) {
|
|
339
|
+
// Only where absent. A part that already has a cache is at least as
|
|
340
|
+
// far along as the reference, so overwriting it would throw away
|
|
341
|
+
// progress — and after a DUST rebuild, shielded and unshielded are
|
|
342
|
+
// precisely the parts that must be left alone.
|
|
343
|
+
const seeded = [];
|
|
344
|
+
if (!cached.shielded) {
|
|
345
|
+
await saveCachedState(store, name, network.id, 'shielded', preSeeded.shielded);
|
|
346
|
+
seeded.push('shielded');
|
|
347
|
+
}
|
|
348
|
+
if (!cached.unshielded) {
|
|
349
|
+
await saveCachedState(store, name, network.id, 'unshielded', preSeeded.unshielded);
|
|
350
|
+
seeded.push('unshielded');
|
|
351
|
+
}
|
|
352
|
+
if (!cached.dust && preSeeded.dust) {
|
|
353
|
+
await saveCachedState(store, name, network.id, 'dust', preSeeded.dust);
|
|
354
|
+
seeded.push('dust');
|
|
355
|
+
}
|
|
356
|
+
onProgress?.(seeded.length > 0
|
|
357
|
+
? `Pre-seed complete — ${seeded.join(' + ')} at chain tip`
|
|
358
|
+
: 'Pre-seed: nothing to seed, every sub-wallet already cached');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
onProgress?.(`Pre-seed failed, will sync from genesis: ${err}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// --- Shielded wallet: try restore from cache ---
|
|
367
|
+
// Use CustomShieldedWallet with a deduping syncCapability so that
|
|
368
|
+
// re-sent boundary events from the indexer don't trip the WASM tree
|
|
369
|
+
// (see sync/sdk-dedup.ts for the upstream bug context).
|
|
370
|
+
onProgress?.('Starting shielded wallet...');
|
|
371
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
372
|
+
const shieldedBuilder = dedupingShieldedBuilder();
|
|
373
|
+
let shieldedWallet;
|
|
374
|
+
let restoredFromCache = false;
|
|
375
|
+
const savedShielded = await loadCachedState(store, name, network.id, 'shielded');
|
|
376
|
+
if (savedShielded) {
|
|
377
|
+
try {
|
|
378
|
+
onProgress?.('Restoring shielded state from cache...');
|
|
379
|
+
shieldedWallet = CustomShieldedWallet(walletCfg, shieldedBuilder).restore(savedShielded);
|
|
380
|
+
restoredFromCache = true;
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
onProgress?.('Shielded cache corrupted, syncing from genesis...');
|
|
384
|
+
await evictCachedState(store, name, network.id, 'shielded');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!shieldedWallet) {
|
|
388
|
+
shieldedWallet = CustomShieldedWallet(walletCfg, shieldedBuilder).startWithSecretKeys(shieldedSecretKeys);
|
|
389
|
+
}
|
|
390
|
+
// --- Unshielded wallet: try restore from cache ---
|
|
391
|
+
onProgress?.('Starting unshielded wallet...');
|
|
392
|
+
let unshieldedWallet;
|
|
393
|
+
const savedUnshielded = await loadCachedState(store, name, network.id, 'unshielded');
|
|
394
|
+
if (savedUnshielded) {
|
|
395
|
+
try {
|
|
396
|
+
onProgress?.('Restoring unshielded state from cache...');
|
|
397
|
+
unshieldedWallet = UnshieldedWallet(walletCfg).restore(savedUnshielded);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
onProgress?.('Unshielded cache corrupted, syncing from genesis...');
|
|
401
|
+
await evictCachedState(store, name, network.id, 'unshielded');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (!unshieldedWallet) {
|
|
405
|
+
unshieldedWallet = UnshieldedWallet(walletCfg).startWithPublicKey(PublicKey.fromKeyStore(keystore));
|
|
406
|
+
}
|
|
407
|
+
// --- Dust wallet: try restore from cache ---
|
|
408
|
+
// Same deduping wrapper as the shielded wallet — the dust SDK has the
|
|
409
|
+
// same boundary-event off-by-one in its applyUpdate.
|
|
410
|
+
onProgress?.('Starting dust wallet...');
|
|
411
|
+
const dustCfg = {
|
|
412
|
+
networkId: network.id,
|
|
413
|
+
costParameters: DUST_COST_PARAMETERS,
|
|
414
|
+
indexerClientConnection: { indexerHttpUrl, indexerWsUrl },
|
|
415
|
+
txHistoryStorage,
|
|
416
|
+
};
|
|
417
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
418
|
+
const dustBuilder = dedupingDustBuilder();
|
|
419
|
+
let dustWallet;
|
|
420
|
+
const savedDust = await loadCachedState(store, name, network.id, 'dust');
|
|
421
|
+
if (savedDust) {
|
|
422
|
+
try {
|
|
423
|
+
onProgress?.('Restoring dust state from cache...');
|
|
424
|
+
dustWallet = CustomDustWallet(dustCfg, dustBuilder).restore(savedDust);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
onProgress?.('Dust cache corrupted, syncing from genesis...');
|
|
428
|
+
await evictCachedState(store, name, network.id, 'dust');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!dustWallet) {
|
|
432
|
+
dustWallet = CustomDustWallet(dustCfg, dustBuilder).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);
|
|
433
|
+
}
|
|
434
|
+
if (restoredFromCache) {
|
|
435
|
+
onProgress?.('Sync state restored from cache — catching up...');
|
|
436
|
+
}
|
|
437
|
+
// --- WalletFacade ---
|
|
438
|
+
onProgress?.('Initializing wallet facade...');
|
|
439
|
+
const facade = await WalletFacade.init({
|
|
440
|
+
configuration: walletCfg,
|
|
441
|
+
// The SDK defaults to a proof server. Supply the service explicitly so
|
|
442
|
+
// WASM mode follows the documented makeWasmProvingService() path.
|
|
443
|
+
provingService: () => createWalletProvingService(prover),
|
|
444
|
+
// Resolve submissions at 'Submitted' instead of the default 'Finalized' so a
|
|
445
|
+
// send doesn't block its message round-trip on finalization — see
|
|
446
|
+
// makeSubmittedOnlySubmissionService.
|
|
447
|
+
submissionService: (cfg) => makeSubmittedOnlySubmissionService(cfg.relayURL),
|
|
448
|
+
shielded: () => shieldedWallet,
|
|
449
|
+
unshielded: () => unshieldedWallet,
|
|
450
|
+
dust: () => dustWallet,
|
|
451
|
+
});
|
|
452
|
+
await facade.start(shieldedSecretKeys, dustSecretKey);
|
|
453
|
+
onProgress?.('Syncing with network...');
|
|
454
|
+
// Subscribe to progressive state updates — don't block on full sync.
|
|
455
|
+
// Emit partial balances immediately so the UI shows shielded/unshielded
|
|
456
|
+
// balances as soon as those sub-wallets are ready, while dust keeps scanning.
|
|
457
|
+
const emptySyncProgress = {
|
|
458
|
+
percentage: 0,
|
|
459
|
+
etaSeconds: null,
|
|
460
|
+
slowest: null,
|
|
461
|
+
shieldedSynced: false,
|
|
462
|
+
unshieldedSynced: false,
|
|
463
|
+
dustSynced: false,
|
|
464
|
+
};
|
|
465
|
+
let latestBalances = {
|
|
466
|
+
shielded: {},
|
|
467
|
+
unshielded: {},
|
|
468
|
+
dust: 0n,
|
|
469
|
+
dustGeneration: null,
|
|
470
|
+
syncProgress: emptySyncProgress,
|
|
471
|
+
synced: false,
|
|
472
|
+
coins: EMPTY_COINS,
|
|
473
|
+
subProgress: EMPTY_SUB_PROGRESS,
|
|
474
|
+
};
|
|
475
|
+
let emissionCount = 0;
|
|
476
|
+
const subscribers = [];
|
|
477
|
+
const syncStartTime = Date.now();
|
|
478
|
+
let lastProgressPct = 0;
|
|
479
|
+
let hasSavedCache = false;
|
|
480
|
+
let lastCacheSaveTime = 0;
|
|
481
|
+
const subscription = facade
|
|
482
|
+
.state()
|
|
483
|
+
.pipe(Rx.auditTime(1000))
|
|
484
|
+
.subscribe({
|
|
485
|
+
next: (s) => {
|
|
486
|
+
emissionCount++;
|
|
487
|
+
const balances = extractBalancesPartial(s, syncStartTime, lastProgressPct);
|
|
488
|
+
latestBalances = balances;
|
|
489
|
+
lastProgressPct = balances.syncProgress.percentage;
|
|
490
|
+
const nightTotal = (balances.unshielded[NIGHT_TOKEN_ID] ?? 0n) + (balances.shielded[NIGHT_TOKEN_ID] ?? 0n);
|
|
491
|
+
const pct = Math.round(balances.syncProgress.percentage * 100);
|
|
492
|
+
const eta = balances.syncProgress.etaSeconds;
|
|
493
|
+
const etaStr = eta !== null ? formatEta(eta) : '';
|
|
494
|
+
const slowest = balances.syncProgress.slowest;
|
|
495
|
+
const slowestLabel = slowest ? ` (${slowest})` : '';
|
|
496
|
+
if (emissionCount % 50 === 0 ||
|
|
497
|
+
emissionCount <= 3 ||
|
|
498
|
+
balances.synced ||
|
|
499
|
+
pct !== Math.round(lastProgressPct * 100)) {
|
|
500
|
+
// DUST is denominated in SPECKS (10^15 per DUST); NIGHT in STARS
|
|
501
|
+
// (10^6). formatNight was being applied to both, so every DUST figure
|
|
502
|
+
// ever logged was wrong by a factor of 10^9 — and plausibly wrong,
|
|
503
|
+
// which is worse. formatDustBalance has existed and been exported the
|
|
504
|
+
// whole time; this line simply predates it.
|
|
505
|
+
//
|
|
506
|
+
// The progress line names the sub-wallet the percentage belongs to.
|
|
507
|
+
// Reporting the slowest sub-wallet's fraction with no attribution, and
|
|
508
|
+
// suffixing it with a NIGHT balance, made "syncing 27% — NIGHT: 8378"
|
|
509
|
+
// read as "NIGHT is 27% synced" while the panel showed shielded and
|
|
510
|
+
// unshielded at 100%. Balances belong on the synced line, once.
|
|
511
|
+
onProgress?.(balances.synced
|
|
512
|
+
? `● synced — NIGHT: ${formatNight(nightTotal)}, DUST: ${formatDustBalance(balances.dust)}`
|
|
513
|
+
: `○ syncing ${pct}%${slowestLabel} — shielded ${subPct(balances.subProgress.shielded, balances.syncProgress.shieldedSynced)}, unshielded ${subPct(balances.subProgress.unshielded, balances.syncProgress.unshieldedSynced)}, dust ${subPct(balances.subProgress.dust, balances.syncProgress.dustSynced)}${etaStr ? ` (${etaStr} remaining)` : ''}`);
|
|
514
|
+
}
|
|
515
|
+
for (const cb of subscribers) {
|
|
516
|
+
try {
|
|
517
|
+
cb(balances);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
/* subscriber error */
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Save sync cache every 60s so progress survives ctrl-C / crashes
|
|
524
|
+
const cacheNow = Date.now();
|
|
525
|
+
if (cacheNow - lastCacheSaveTime > 60_000) {
|
|
526
|
+
lastCacheSaveTime = cacheNow;
|
|
527
|
+
saveCache(store, facade, txHistoryStorage, name, network.id).catch(() => { });
|
|
528
|
+
}
|
|
529
|
+
if (balances.synced && !hasSavedCache) {
|
|
530
|
+
hasSavedCache = true;
|
|
531
|
+
saveCache(store, facade, txHistoryStorage, name, network.id).catch(() => { });
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
error: (err) => {
|
|
535
|
+
onProgress?.(`Sync error: ${err}`);
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
// Wait briefly for first emission so we have something to return
|
|
539
|
+
await new Promise((resolve) => {
|
|
540
|
+
const timeout = setTimeout(resolve, 5_000);
|
|
541
|
+
const earlyCheck = facade
|
|
542
|
+
.state()
|
|
543
|
+
.pipe(Rx.first())
|
|
544
|
+
.subscribe((s) => {
|
|
545
|
+
latestBalances = extractBalancesPartial(s);
|
|
546
|
+
clearTimeout(timeout);
|
|
547
|
+
earlyCheck.unsubscribe();
|
|
548
|
+
resolve();
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
const refresh = async () => {
|
|
552
|
+
return latestBalances;
|
|
553
|
+
};
|
|
554
|
+
const subscribe = (cb) => {
|
|
555
|
+
subscribers.push(cb);
|
|
556
|
+
// Emit current state immediately
|
|
557
|
+
cb(latestBalances);
|
|
558
|
+
return () => {
|
|
559
|
+
const idx = subscribers.indexOf(cb);
|
|
560
|
+
if (idx >= 0)
|
|
561
|
+
subscribers.splice(idx, 1);
|
|
562
|
+
};
|
|
563
|
+
};
|
|
564
|
+
const stop = async () => {
|
|
565
|
+
subscription.unsubscribe();
|
|
566
|
+
await saveCache(store, facade, txHistoryStorage, name, network.id).catch(() => { });
|
|
567
|
+
try {
|
|
568
|
+
await facade.stop();
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
/* ignore */
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
// `balances` is exposed as a getter so callers (notably the daemon's
|
|
575
|
+
// `getBalances`) always read the live snapshot. A plain property
|
|
576
|
+
// would freeze the reference at the moment startWalletSync returned —
|
|
577
|
+
// every subsequent sync update reassigns `latestBalances` to a new
|
|
578
|
+
// object, so a snapshotted property would never reflect any of them.
|
|
579
|
+
return {
|
|
580
|
+
facade,
|
|
581
|
+
get balances() {
|
|
582
|
+
return latestBalances;
|
|
583
|
+
},
|
|
584
|
+
stop,
|
|
585
|
+
refresh,
|
|
586
|
+
subscribe,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Restore the shared transaction history storage from its cached serialization,
|
|
591
|
+
* mirroring the sub-wallet restore path: use the cache when it decodes, evict
|
|
592
|
+
* and start empty when it doesn't (sync rebuilds it from the indexer).
|
|
593
|
+
*/
|
|
594
|
+
async function loadHistoryStorage(store, walletName, networkId, onProgress) {
|
|
595
|
+
const saved = await loadCachedState(store, walletName, networkId, 'history');
|
|
596
|
+
if (saved) {
|
|
597
|
+
try {
|
|
598
|
+
onProgress?.('Restoring transaction history from cache...');
|
|
599
|
+
return InMemoryTransactionHistoryStorage.restore(saved, WalletEntrySchema, mergeWalletEntries);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
onProgress?.('Transaction history cache corrupted, rebuilding from sync...');
|
|
603
|
+
await evictCachedState(store, walletName, networkId, 'history');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return new InMemoryTransactionHistoryStorage(WalletEntrySchema, mergeWalletEntries);
|
|
607
|
+
}
|
|
608
|
+
async function saveCache(store, facade, history, walletName, networkId) {
|
|
609
|
+
try {
|
|
610
|
+
const [sh, un, du, hi] = await Promise.all([
|
|
611
|
+
facade.shielded.state
|
|
612
|
+
.pipe(Rx.first())
|
|
613
|
+
.toPromise()
|
|
614
|
+
.then((s) => s?.capabilities?.serialization?.serialize?.(s.state) ?? null),
|
|
615
|
+
facade.unshielded.state
|
|
616
|
+
.pipe(Rx.first())
|
|
617
|
+
.toPromise()
|
|
618
|
+
.then((s) => s?.capabilities?.serialization?.serialize?.(s.state) ?? null),
|
|
619
|
+
facade.dust.state
|
|
620
|
+
.pipe(Rx.first())
|
|
621
|
+
.toPromise()
|
|
622
|
+
.then((s) => s?.capabilities?.serialization?.serialize?.(s.state) ?? null),
|
|
623
|
+
history.serialize().catch(() => null),
|
|
624
|
+
]).catch(() => [null, null, null, null]);
|
|
625
|
+
if (sh)
|
|
626
|
+
await saveCachedState(store, walletName, networkId, 'shielded', sh);
|
|
627
|
+
if (un)
|
|
628
|
+
await saveCachedState(store, walletName, networkId, 'unshielded', un);
|
|
629
|
+
if (du)
|
|
630
|
+
await saveCachedState(store, walletName, networkId, 'dust', du);
|
|
631
|
+
if (hi)
|
|
632
|
+
await saveCachedState(store, walletName, networkId, 'history', hi);
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
/* best effort */
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function extractBalancesPartial(state, syncStartTime = 0, prevPct = 0) {
|
|
639
|
+
let shielded = {};
|
|
640
|
+
let unshielded = {};
|
|
641
|
+
let dust = 0n;
|
|
642
|
+
let dustGeneration = null;
|
|
643
|
+
let synced = false;
|
|
644
|
+
// Per-sub-wallet sync status
|
|
645
|
+
let shieldedSynced = false;
|
|
646
|
+
let unshieldedSynced = false;
|
|
647
|
+
let dustSynced = false;
|
|
648
|
+
// Per-coin breakdown
|
|
649
|
+
const coins = {
|
|
650
|
+
shielded: { available: [], pending: [] },
|
|
651
|
+
unshielded: { available: [], pending: [] },
|
|
652
|
+
dust: { available: [], pending: [] },
|
|
653
|
+
};
|
|
654
|
+
// Per-sub-wallet raw sync progress (applied/total)
|
|
655
|
+
const subProgress = {
|
|
656
|
+
shielded: { applied: 0, total: 0 },
|
|
657
|
+
unshielded: { applied: 0, total: 0 },
|
|
658
|
+
dust: { applied: 0, total: 0 },
|
|
659
|
+
};
|
|
660
|
+
try {
|
|
661
|
+
synced = state.isSynced === true;
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
/* */
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
const sb = state.shielded?.balances;
|
|
668
|
+
if (sb && typeof sb === 'object')
|
|
669
|
+
shielded = sb;
|
|
670
|
+
// Per-coin breakdown
|
|
671
|
+
for (const c of state.shielded.availableCoins) {
|
|
672
|
+
coins.shielded.available.push({ value: c.coin?.value ?? 0n, type: c.coin?.type ?? '' });
|
|
673
|
+
}
|
|
674
|
+
for (const c of state.shielded.pendingCoins) {
|
|
675
|
+
coins.shielded.pending.push({ value: c.coin?.value ?? 0n, type: c.coin?.type ?? '' });
|
|
676
|
+
}
|
|
677
|
+
// Sub-progress — v4 SDK exposes a SyncProgress on `progress`. The abstractions package
|
|
678
|
+
// defines: appliedIndex, highestRelevantWalletIndex, highestIndex, highestRelevantIndex.
|
|
679
|
+
const sp = state.shielded?.progress;
|
|
680
|
+
const shApplied = sp?.appliedIndex ?? 0n;
|
|
681
|
+
const shTotal = sp?.highestRelevantWalletIndex ?? 0n;
|
|
682
|
+
subProgress.shielded = { applied: Number(shApplied), total: Number(shTotal) };
|
|
683
|
+
if (typeof sp?.isStrictlyComplete === 'function') {
|
|
684
|
+
shieldedSynced = sp.isStrictlyComplete();
|
|
685
|
+
}
|
|
686
|
+
else if (shTotal > 0n && shApplied >= shTotal) {
|
|
687
|
+
shieldedSynced = true;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
/* shielded not ready */
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const ub = state.unshielded?.balances;
|
|
695
|
+
// Clone: we fold booked (pending) inputs into the displayed balance below
|
|
696
|
+
// and must not mutate the SDK's own state object.
|
|
697
|
+
if (ub && typeof ub === 'object')
|
|
698
|
+
unshielded = { ...ub };
|
|
699
|
+
// Per-coin breakdown
|
|
700
|
+
for (const c of state.unshielded.availableCoins) {
|
|
701
|
+
coins.unshielded.available.push({
|
|
702
|
+
value: c.utxo?.value ?? 0n,
|
|
703
|
+
type: c.utxo?.type ?? '',
|
|
704
|
+
registeredForDustGeneration: c.meta?.registeredForDustGeneration === true,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
for (const c of state.unshielded.pendingCoins) {
|
|
708
|
+
const value = c.utxo?.value ?? 0n;
|
|
709
|
+
const type = c.utxo?.type ?? '';
|
|
710
|
+
coins.unshielded.pending.push({
|
|
711
|
+
value,
|
|
712
|
+
type,
|
|
713
|
+
registeredForDustGeneration: c.meta?.registeredForDustGeneration === true,
|
|
714
|
+
});
|
|
715
|
+
// Count booked inputs toward the displayed balance. A send or DUST
|
|
716
|
+
// registration reserves its own NIGHT UTxOs (moved available→pending)
|
|
717
|
+
// while the transaction is in flight, then they settle back to the
|
|
718
|
+
// wallet on apply — so leaving them out flashes the balance down to zero
|
|
719
|
+
// mid-registration. Unshielded pending holds ONLY these booked inputs,
|
|
720
|
+
// never incoming coins, so this can't over-count receipts.
|
|
721
|
+
unshielded[type] = (unshielded[type] ?? 0n) + value;
|
|
722
|
+
}
|
|
723
|
+
// v4 SDK unshielded SyncProgress: { appliedId, highestTransactionId, isConnected }
|
|
724
|
+
const up = state.unshielded?.progress;
|
|
725
|
+
const unApplied = up?.appliedId ?? 0n;
|
|
726
|
+
const unTotal = up?.highestTransactionId ?? 0n;
|
|
727
|
+
subProgress.unshielded = { applied: Number(unApplied), total: Number(unTotal) };
|
|
728
|
+
if (typeof up?.isStrictlyComplete === 'function') {
|
|
729
|
+
unshieldedSynced = up.isStrictlyComplete();
|
|
730
|
+
}
|
|
731
|
+
else if (unTotal > 0n && unApplied >= unTotal) {
|
|
732
|
+
unshieldedSynced = true;
|
|
733
|
+
}
|
|
734
|
+
else if (synced) {
|
|
735
|
+
unshieldedSynced = true;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
/* unshielded not ready */
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const d = state.dust?.balance?.(new Date());
|
|
743
|
+
if (typeof d === 'bigint')
|
|
744
|
+
dust = d;
|
|
745
|
+
// Dust sub-wallet synced check
|
|
746
|
+
if (synced)
|
|
747
|
+
dustSynced = true;
|
|
748
|
+
// Per-coin breakdown — dust coins carry max-cap + dtime
|
|
749
|
+
for (const c of state.dust.availableCoins) {
|
|
750
|
+
coins.dust.available.push({
|
|
751
|
+
generatedNow: c.generatedNow ?? 0n,
|
|
752
|
+
maxCap: c.maxCap ?? 0n,
|
|
753
|
+
maxCapReachedAt: c.maxCapReachedAt instanceof Date ? c.maxCapReachedAt : new Date(c.maxCapReachedAt ?? 0),
|
|
754
|
+
dtime: c.dtime ? (c.dtime instanceof Date ? c.dtime : new Date(c.dtime)) : null,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
for (const c of state.dust.pendingCoins) {
|
|
758
|
+
coins.dust.pending.push({
|
|
759
|
+
generatedNow: c.generatedNow ?? 0n,
|
|
760
|
+
maxCap: c.maxCap ?? 0n,
|
|
761
|
+
maxCapReachedAt: c.maxCapReachedAt instanceof Date ? c.maxCapReachedAt : new Date(c.maxCapReachedAt ?? 0),
|
|
762
|
+
dtime: c.dtime ? (c.dtime instanceof Date ? c.dtime : new Date(c.dtime)) : null,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
// v4 SDK dust SyncProgress shares the abstractions shape with shielded
|
|
766
|
+
const dp = state.dust?.progress;
|
|
767
|
+
const duApplied = dp?.appliedIndex ?? 0n;
|
|
768
|
+
const duTotal = dp?.highestRelevantWalletIndex ?? 0n;
|
|
769
|
+
subProgress.dust = { applied: Number(duApplied), total: Number(duTotal) };
|
|
770
|
+
if (typeof dp?.isStrictlyComplete === 'function') {
|
|
771
|
+
dustSynced = dp.isStrictlyComplete();
|
|
772
|
+
}
|
|
773
|
+
else if (duTotal > 0n && duApplied >= duTotal) {
|
|
774
|
+
dustSynced = true;
|
|
775
|
+
}
|
|
776
|
+
else if (synced) {
|
|
777
|
+
dustSynced = true;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
/* dust not ready */
|
|
782
|
+
}
|
|
783
|
+
// Overall progress: the slowest sub-wallet, clamped below 100% until actually
|
|
784
|
+
// synced. Arithmetic lives in ./progress.ts so it is unit-testable without WASM.
|
|
785
|
+
const { percentage, etaSeconds, slowest } = overallSyncProgress({
|
|
786
|
+
shielded: subProgress.shielded,
|
|
787
|
+
unshielded: subProgress.unshielded,
|
|
788
|
+
dust: subProgress.dust,
|
|
789
|
+
shieldedSynced,
|
|
790
|
+
unshieldedSynced,
|
|
791
|
+
dustSynced,
|
|
792
|
+
synced,
|
|
793
|
+
elapsedMs: syncStartTime > 0 ? Date.now() - syncStartTime : 0,
|
|
794
|
+
});
|
|
795
|
+
// percentage/etaSeconds already account for `synced` (see overallSyncProgress);
|
|
796
|
+
// this only reconciles the per-sub-wallet flags with the facade's own verdict.
|
|
797
|
+
if (synced) {
|
|
798
|
+
shieldedSynced = true;
|
|
799
|
+
unshieldedSynced = true;
|
|
800
|
+
dustSynced = true;
|
|
801
|
+
}
|
|
802
|
+
// Creation time of the newest UTXO, tolerant of absent metadata.
|
|
803
|
+
const newestCtime = (utxos) => utxos.reduce((newest, c) => {
|
|
804
|
+
const ctime = c.meta?.ctime;
|
|
805
|
+
return ctime && (!newest || ctime > newest) ? ctime : newest;
|
|
806
|
+
}, null);
|
|
807
|
+
// Extract DUST generation info from the facade's dust sub-wallet state.
|
|
808
|
+
// This matches mn-tui's extractDustGeneration pattern.
|
|
809
|
+
try {
|
|
810
|
+
const nightRatio = ledger.LedgerParameters.initialParameters().dust.nightDustRatio;
|
|
811
|
+
// v4 API: availableCoins is a property returning DustFullInfo[]
|
|
812
|
+
const coins = state.dust.availableCoins.filter((coin) => coin.maxCap > 0n);
|
|
813
|
+
if (coins.length > 0) {
|
|
814
|
+
let limitRaw = 0n;
|
|
815
|
+
let ratePerDay = 0n;
|
|
816
|
+
let fillTime = new Date(0);
|
|
817
|
+
for (const coin of coins) {
|
|
818
|
+
limitRaw += coin.maxCap;
|
|
819
|
+
ratePerDay += coin.rate * 86400n;
|
|
820
|
+
const cap = coin.maxCapReachedAt;
|
|
821
|
+
if (cap > fillTime)
|
|
822
|
+
fillTime = cap;
|
|
823
|
+
}
|
|
824
|
+
// Registered NIGHT UTXOs — their sum is the exact backing of the cap;
|
|
825
|
+
// balance beyond it is unregistered and generates nothing.
|
|
826
|
+
const registeredUtxos = state.unshielded.availableCoins.filter((c) => c.utxo?.type === NIGHT_TOKEN_ID && c.meta?.registeredForDustGeneration === true);
|
|
827
|
+
const registeredNight = registeredUtxos.reduce((sum, c) => sum + (c.utxo?.value ?? 0n), 0n);
|
|
828
|
+
dustGeneration = {
|
|
829
|
+
balance: dust,
|
|
830
|
+
designated: nightRatio > 0n ? limitRaw / nightRatio : 0n,
|
|
831
|
+
ratePerDay,
|
|
832
|
+
limit: limitRaw,
|
|
833
|
+
fillTime,
|
|
834
|
+
numUtxos: coins.length,
|
|
835
|
+
registered: registeredUtxos.length > 0 || coins.length > 0,
|
|
836
|
+
registeredNight,
|
|
837
|
+
newestRegisteredAt: newestCtime(registeredUtxos),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
// Check if any UTXOs are registered even if no dust coins yet
|
|
842
|
+
const registeredUtxos = state.unshielded.availableCoins.filter((c) => c.utxo?.type === NIGHT_TOKEN_ID && c.meta?.registeredForDustGeneration === true);
|
|
843
|
+
dustGeneration = {
|
|
844
|
+
balance: dust,
|
|
845
|
+
designated: 0n,
|
|
846
|
+
ratePerDay: 0n,
|
|
847
|
+
limit: 0n,
|
|
848
|
+
fillTime: new Date(0),
|
|
849
|
+
numUtxos: 0,
|
|
850
|
+
registered: registeredUtxos.length > 0,
|
|
851
|
+
registeredNight: registeredUtxos.reduce((sum, c) => sum + (c.utxo?.value ?? 0n), 0n),
|
|
852
|
+
newestRegisteredAt: newestCtime(registeredUtxos),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
/* dust generation info not available */
|
|
858
|
+
}
|
|
859
|
+
const syncProgress = { percentage, etaSeconds, slowest, shieldedSynced, unshieldedSynced, dustSynced };
|
|
860
|
+
return { shielded, unshielded, dust, dustGeneration, syncProgress, synced, coins, subProgress };
|
|
861
|
+
}
|
|
862
|
+
function extractBalances(state) {
|
|
863
|
+
const partial = extractBalancesPartial(state);
|
|
864
|
+
return {
|
|
865
|
+
...partial,
|
|
866
|
+
synced: true,
|
|
867
|
+
syncProgress: { percentage: 1, etaSeconds: 0, slowest: null, shieldedSynced: true, unshieldedSynced: true, dustSynced: true },
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function formatEta(seconds) {
|
|
871
|
+
if (seconds < 60)
|
|
872
|
+
return `${seconds}s`;
|
|
873
|
+
const m = Math.floor(seconds / 60);
|
|
874
|
+
const s = seconds % 60;
|
|
875
|
+
if (m < 60)
|
|
876
|
+
return `${m}m ${s}s`;
|
|
877
|
+
const h = Math.floor(m / 60);
|
|
878
|
+
return `${h}h ${m % 60}m`;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Clear cached sync state for a wallet on a specific network.
|
|
882
|
+
* Useful after a devnet genesis reset or corrupted state.
|
|
883
|
+
*/
|
|
884
|
+
export async function clearSyncCache(walletName, networkId, store) {
|
|
885
|
+
const resolved = await resolveSyncStore(store);
|
|
886
|
+
await evictCachedState(resolved, walletName, networkId, 'shielded');
|
|
887
|
+
await evictCachedState(resolved, walletName, networkId, 'unshielded');
|
|
888
|
+
await evictCachedState(resolved, walletName, networkId, 'dust');
|
|
889
|
+
await evictCachedState(resolved, walletName, networkId, 'history');
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Remove every artifact a wallet leaves behind so re-creating a wallet under
|
|
893
|
+
* the same name never silently inherits stale state from a previous seed:
|
|
894
|
+
* - the serialized sync state held in the SyncStateStore (the `.dat` entries;
|
|
895
|
+
* IndexedDB in the extension, the fs-backed store under the daemon/CLI/TUI),
|
|
896
|
+
* cleared via clearSyncCache so this works in the browser too; and
|
|
897
|
+
* - on node only, the per-wallet `~/.moth/sync/<network>/<name>/` directory
|
|
898
|
+
* and the matching daemon `.sock` file, which live outside the store.
|
|
899
|
+
*
|
|
900
|
+
* Best-effort: missing artifacts are ignored. The node-only fs work is behind a
|
|
901
|
+
* dynamic import so this module keeps its "no static node:* imports" invariant
|
|
902
|
+
* (see the file header) and stays browser-bundleable.
|
|
903
|
+
*/
|
|
904
|
+
export async function removeWalletSyncArtifacts(walletName, networkId, store) {
|
|
905
|
+
// Store-backed state — browser-safe, and on node this unlinks the .dat files.
|
|
906
|
+
await clearSyncCache(walletName, networkId, store);
|
|
907
|
+
// The daemon/CLI also leave a directory + socket on disk that the store
|
|
908
|
+
// abstraction does not own. Skip entirely outside node (e.g. the extension).
|
|
909
|
+
if (typeof process === 'undefined' || !process.versions?.node)
|
|
910
|
+
return;
|
|
911
|
+
try {
|
|
912
|
+
const { rmSync, unlinkSync } = await import(/* @vite-ignore */ 'node:fs');
|
|
913
|
+
const { join } = await import(/* @vite-ignore */ 'node:path');
|
|
914
|
+
const { homedir } = await import(/* @vite-ignore */ 'node:os');
|
|
915
|
+
const cacheBase = join(homedir(), '.moth', 'sync');
|
|
916
|
+
try {
|
|
917
|
+
rmSync(join(cacheBase, networkId, walletName), { recursive: true, force: true });
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
/* directory absent or already gone */
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
unlinkSync(join(cacheBase, networkId, `${walletName}.sock`));
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
/* socket absent or already cleaned by the daemon's shutdown */
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
/* node fs/path/os unavailable — nothing on disk to clean */
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Clear ONLY the dust sub-wallet's cached state, keeping the (much larger)
|
|
935
|
+
* shielded/unshielded caches. The next sync start rebuilds the dust view from
|
|
936
|
+
* the chain — the targeted repair for a dust view that stopped ingesting
|
|
937
|
+
* generation records for newer NIGHT UTXOs.
|
|
938
|
+
*/
|
|
939
|
+
export async function clearDustSyncCache(walletName, networkId, store) {
|
|
940
|
+
const resolved = await resolveSyncStore(store);
|
|
941
|
+
await evictCachedState(resolved, walletName, networkId, 'dust');
|
|
942
|
+
}
|
|
943
|
+
//# sourceMappingURL=wallet-sync.js.map
|