@spirobel/monero-wallet-api 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +6 -0
  2. package/dist/api.d.ts +1 -1
  3. package/dist/api.js +1 -1
  4. package/dist/io/logging.d.ts +6 -6
  5. package/dist/io/logging.js +4 -3
  6. package/dist/io/sleep.d.ts +1 -1
  7. package/dist/io/sleep.js +16 -1
  8. package/dist/multisig/escrow.js +0 -1
  9. package/dist/scanning-syncing/backgroundWorker.d.ts +2 -1
  10. package/dist/scanning-syncing/backgroundWorker.js +104 -34
  11. package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +33 -2
  12. package/dist/scanning-syncing/connectionStatus.d.ts +6 -0
  13. package/dist/scanning-syncing/connectionStatus.js +15 -0
  14. package/dist/scanning-syncing/connectionStatusOpened.d.ts +3 -1
  15. package/dist/scanning-syncing/connectionStatusOpened.js +22 -5
  16. package/dist/scanning-syncing/openWallet.d.ts +4 -1
  17. package/dist/scanning-syncing/openWallet.js +3 -0
  18. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +12 -4
  19. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +188 -107
  20. package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +6 -25
  21. package/dist/scanning-syncing/scanresult/scanCoordination.js +89 -209
  22. package/dist/scanning-syncing/scanresult/scanLoop.d.ts +1 -2
  23. package/dist/scanning-syncing/scanresult/scanLoop.js +0 -13
  24. package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
  25. package/dist/scanning-syncing/worker-mains/coordinator-main.d.ts +1 -0
  26. package/dist/scanning-syncing/worker-mains/coordinator-main.js +16 -2
  27. package/dist/scanning-syncing/worker-mains/cpubound-main.d.ts +5 -2
  28. package/dist/scanning-syncing/worker-mains/cpubound-main.js +10 -8
  29. package/dist/scanning-syncing/worker-mains/worker.js +50 -2
  30. package/dist/scansettings/scanSettingsOpened.js +3 -6
  31. package/dist/tools/monero-tools.js +1 -1
  32. package/dist/viewpair/ViewPair.js +9 -4
  33. package/dist/wasm-processing/wasmFile.js +1 -1
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @spirobel/monero-wallet-api
2
2
 
3
+ ## install
4
+
5
+ ```bash
6
+ bun add @spirobel/monero-wallet-api
7
+ ```
8
+
3
9
  ## quick start
4
10
 
5
11
  ```ts
package/dist/api.d.ts CHANGED
@@ -14,7 +14,7 @@ export * from "./multisig/multisig";
14
14
  export * from "./multisig/DistributedKeyGenerator";
15
15
  export * from "./multisig/escrow";
16
16
  export * from "./multisig/MultiSigTxSigner";
17
- export { startWebworker, createWebworker, makeWebworkerScript, } from "./scanning-syncing/backgroundWorker";
17
+ export { createWebworker, makeWebworkerScript, } from "./scanning-syncing/backgroundWorker";
18
18
  export { spendable } from "./scanning-syncing/scanresult/scanResult";
19
19
  export { openWallets, openWallet } from "./scanning-syncing/openWallet";
20
20
  export * from "./scanning-syncing/blocksbuffer/blocksBufferFetchLoop";
package/dist/api.js CHANGED
@@ -13,7 +13,7 @@ export * from "./multisig/multisig";
13
13
  export * from "./multisig/DistributedKeyGenerator";
14
14
  export * from "./multisig/escrow";
15
15
  export * from "./multisig/MultiSigTxSigner";
16
- export { startWebworker, createWebworker, makeWebworkerScript, } from "./scanning-syncing/backgroundWorker";
16
+ export { createWebworker, makeWebworkerScript, } from "./scanning-syncing/backgroundWorker";
17
17
  export { spendable } from "./scanning-syncing/scanresult/scanResult";
18
18
  export { openWallets, openWallet } from "./scanning-syncing/openWallet";
19
19
  export * from "./scanning-syncing/blocksbuffer/blocksBufferFetchLoop";
@@ -13,7 +13,7 @@
13
13
  * "logs_include": ["blocksBufferFetchLoop", "makeWorkItemsFromBlocksBuffer", "logBufStatus"]
14
14
  * -> block fetching, reconciliation, buffer state.
15
15
  *
16
- * "logs_include": ["processScanResult", "processWorkItem", "processScanResultForWorkItem"]
16
+ * "logs_include": ["processScanResult", "processWorkItem"]
17
17
  * -> result processing, cache updates, reorg detection.
18
18
  *
19
19
  * "logs_include": ["handleConnectionStatusChanges", "handleScanError"]
@@ -23,7 +23,7 @@
23
23
  * "logs": "file" writes to `<coordinator|cpubound|mainthread>-<id>-<timestamp>.log`.
24
24
  * "logs": "console-and-file" writes to file and console.
25
25
  */
26
- export declare const LOGGING_FUNCTIONS: readonly ["findWorkToBeDone", "workToBeDoneForBatch", "makeWorkItemsFromBlocksBuffer", "reconcileWorkItemDone", "processScanResultForWorkItem", "processWorkItem", "logBufStatus", "coordinatorMain", "coordinatorMainMultithreaded", "scheduleWorkOnCpuPorts", "processScanResult", "handleScanError", "createWebworker", "startWebworker", "blocksBufferFetchLoop", "reduceStartHeightToTip", "handleConnectionStatusChanges", "handleCpuboundScan", "coordinatorMainWorker", "CPU_PORT_HANDLER", "atomicWrite", "multisigMainWorkerCall"];
26
+ export declare const LOGGING_FUNCTIONS: readonly ["findWorkToBeDone", "workToBeDoneForBatch", "makeWorkItemsFromBlocksBuffer", "reconcileWorkItemDone", "processWorkItem", "logBufStatus", "coordinatorMain", "coordinatorMainMultithreaded", "scheduleWorkOnCpuPorts", "processScanResult", "handleScanError", "createWebworker", "startWebworker", "blocksBufferFetchLoop", "reduceStartHeightToTip", "handleConnectionStatusChanges", "handleCpuboundScan", "coordinatorMainWorker", "CPU_PORT_HANDLER", "atomicWrite", "multisigMainWorkerCall"];
27
27
  export type PossibleLogs = (typeof LOGGING_FUNCTIONS)[number];
28
28
  export type LogSetting = "console" | "file" | "console-and-file" | "off";
29
29
  export type FileLogMessage = {
@@ -31,10 +31,10 @@ export type FileLogMessage = {
31
31
  message: any;
32
32
  };
33
33
  export declare function setupLoggingPath(scan_settings_path: string, path_prefix: string, role: "coordinator" | "cpubound" | "mainthread" | "multisig", cpu_worker_id?: number): Promise<{
34
- logs: LogSetting | undefined;
35
- logs_include: ("findWorkToBeDone" | "workToBeDoneForBatch" | "makeWorkItemsFromBlocksBuffer" | "reconcileWorkItemDone" | "processScanResultForWorkItem" | "processWorkItem" | "logBufStatus" | "coordinatorMain" | "coordinatorMainMultithreaded" | "scheduleWorkOnCpuPorts" | "processScanResult" | "handleScanError" | "createWebworker" | "startWebworker" | "blocksBufferFetchLoop" | "reduceStartHeightToTip" | "handleConnectionStatusChanges" | "handleCpuboundScan" | "coordinatorMainWorker" | "CPU_PORT_HANDLER" | "atomicWrite" | "multisigMainWorkerCall")[] | undefined;
36
- logs_exclude: ("findWorkToBeDone" | "workToBeDoneForBatch" | "makeWorkItemsFromBlocksBuffer" | "reconcileWorkItemDone" | "processScanResultForWorkItem" | "processWorkItem" | "logBufStatus" | "coordinatorMain" | "coordinatorMainMultithreaded" | "scheduleWorkOnCpuPorts" | "processScanResult" | "handleScanError" | "createWebworker" | "startWebworker" | "blocksBufferFetchLoop" | "reduceStartHeightToTip" | "handleConnectionStatusChanges" | "handleCpuboundScan" | "coordinatorMainWorker" | "CPU_PORT_HANDLER" | "atomicWrite" | "multisigMainWorkerCall")[] | undefined;
34
+ logs: "console" | "file" | "console-and-file";
35
+ logs_include: ("findWorkToBeDone" | "workToBeDoneForBatch" | "makeWorkItemsFromBlocksBuffer" | "reconcileWorkItemDone" | "processWorkItem" | "logBufStatus" | "coordinatorMain" | "coordinatorMainMultithreaded" | "scheduleWorkOnCpuPorts" | "processScanResult" | "handleScanError" | "createWebworker" | "startWebworker" | "blocksBufferFetchLoop" | "reduceStartHeightToTip" | "handleConnectionStatusChanges" | "handleCpuboundScan" | "coordinatorMainWorker" | "CPU_PORT_HANDLER" | "atomicWrite" | "multisigMainWorkerCall")[] | undefined;
36
+ logs_exclude: ("findWorkToBeDone" | "workToBeDoneForBatch" | "makeWorkItemsFromBlocksBuffer" | "reconcileWorkItemDone" | "processWorkItem" | "logBufStatus" | "coordinatorMain" | "coordinatorMainMultithreaded" | "scheduleWorkOnCpuPorts" | "processScanResult" | "handleScanError" | "createWebworker" | "startWebworker" | "blocksBufferFetchLoop" | "reduceStartHeightToTip" | "handleConnectionStatusChanges" | "handleCpuboundScan" | "coordinatorMainWorker" | "CPU_PORT_HANDLER" | "atomicWrite" | "multisigMainWorkerCall")[] | undefined;
37
37
  file_logbuffer: FileLogMessage[];
38
38
  global_logging_path: string;
39
- }>;
39
+ } | undefined>;
40
40
  export declare function log(fnname: string, message: any): void;
@@ -15,7 +15,7 @@ import { atomicWrite } from "./atomicWrite";
15
15
  * "logs_include": ["blocksBufferFetchLoop", "makeWorkItemsFromBlocksBuffer", "logBufStatus"]
16
16
  * -> block fetching, reconciliation, buffer state.
17
17
  *
18
- * "logs_include": ["processScanResult", "processWorkItem", "processScanResultForWorkItem"]
18
+ * "logs_include": ["processScanResult", "processWorkItem"]
19
19
  * -> result processing, cache updates, reorg detection.
20
20
  *
21
21
  * "logs_include": ["handleConnectionStatusChanges", "handleScanError"]
@@ -31,7 +31,6 @@ export const LOGGING_FUNCTIONS = [
31
31
  "workToBeDoneForBatch",
32
32
  "makeWorkItemsFromBlocksBuffer",
33
33
  "reconcileWorkItemDone",
34
- "processScanResultForWorkItem",
35
34
  "processWorkItem",
36
35
  "logBufStatus",
37
36
  "coordinatorMain",
@@ -63,6 +62,8 @@ export const LOGGING_FUNCTIONS = [
63
62
  export async function setupLoggingPath(scan_settings_path, path_prefix, role, cpu_worker_id) {
64
63
  const scanSettings = await openScanSettingsFile(scan_settings_path);
65
64
  const logs = scanSettings?.logs;
65
+ if (logs === "off" || typeof logs === "undefined")
66
+ return;
66
67
  const logs_include = scanSettings?.logs_include;
67
68
  const logs_exclude = scanSettings?.logs_exclude;
68
69
  const file_logbuffer = [];
@@ -147,7 +148,7 @@ export function log(fnname, message) {
147
148
  if (!setup)
148
149
  return;
149
150
  const { logs, logs_include, logs_exclude, file_logbuffer } = setup;
150
- if (logs === "off")
151
+ if (logs === "off" || typeof logs === "undefined")
151
152
  return;
152
153
  // Apply include/exclude filters
153
154
  if (logs_include && logs_include.length > 0 && !logs_include.includes(fnname))
@@ -1 +1 @@
1
- export declare const sleep: (ms: number) => Promise<unknown>;
1
+ export declare const sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
package/dist/io/sleep.js CHANGED
@@ -1 +1,16 @@
1
- export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1
+ export const sleep = (ms, signal) => new Promise((resolve, reject) => {
2
+ if (signal?.aborted) {
3
+ reject(signal.reason ?? new DOMException("aborted", "AbortError"));
4
+ return;
5
+ }
6
+ const onAbort = () => {
7
+ clearTimeout(timer);
8
+ signal?.removeEventListener("abort", onAbort);
9
+ reject(signal?.reason ?? new DOMException("aborted", "AbortError"));
10
+ };
11
+ const timer = setTimeout(() => {
12
+ signal?.removeEventListener("abort", onAbort);
13
+ resolve();
14
+ }, ms);
15
+ signal?.addEventListener("abort", onAbort, { once: true });
16
+ });
@@ -1,5 +1,4 @@
1
1
  import { ed25519, x25519 } from "@noble/curves/ed25519.js";
2
- import { numberToBytesLE } from "@noble/curves/utils.js";
3
2
  import { blake3 } from "@noble/hashes/blake3.js";
4
3
  import { vk_from_entropy } from "../api";
5
4
  export function makeEscrowContext(context_index) {
@@ -3,9 +3,10 @@ export declare const CPU_POOL_SIZE = 4;
3
3
  export type WorkerSet = {
4
4
  fetchWorker: Worker;
5
5
  cpuWorkers: Worker[];
6
+ shutdown: (timeoutMs?: number) => Promise<void>;
6
7
  terminate: () => void;
8
+ dumpHeaps: (dir?: string) => Promise<string[]>;
7
9
  };
8
10
  export declare function createWebworker(handle_result?: (result: CacheChangedCallbackParameters) => void, scan_settings_path?: string, pathPrefix?: string, handle_error?: (error: unknown) => void): Promise<WorkerSet | undefined>;
9
11
  export declare function makeWebworkerScript(): string;
10
- export declare function startWebworker(handle_result?: (result: unknown) => void, handle_error?: (error: unknown) => void): Worker;
11
12
  export declare function startWebworkerReady(): Promise<Worker>;
@@ -1,6 +1,6 @@
1
1
  import { LOCAL_NODE_DEFAULT_URL } from "../node-interaction/nodeUrl";
2
2
  import {} from "./scanresult/scanCache";
3
- import { openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT } from "../api";
3
+ import { openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, sleep, } from "../api";
4
4
  import { log, setupLoggingPath } from "../io/logging";
5
5
  import { workerMainCode } from "./worker-entrypoints/worker";
6
6
  export const CPU_POOL_SIZE = 4;
@@ -8,10 +8,11 @@ export async function createWebworker(handle_result, scan_settings_path, pathPre
8
8
  try {
9
9
  const resolvedPath = scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT;
10
10
  const scanSettings = await openScanSettingsFile(resolvedPath);
11
- await setupLoggingPath(resolvedPath, pathPrefix ?? "", "mainthread");
11
+ if (scanSettings?.logs !== "off" && scanSettings?.logs)
12
+ await setupLoggingPath(resolvedPath, pathPrefix ?? "", "mainthread");
12
13
  const node_url = scanSettings?.node_url || LOCAL_NODE_DEFAULT_URL;
13
14
  const cpu_worker_count = typeof scanSettings?.cpu_worker_count !== "undefined"
14
- ? scanSettings?.cpu_worker_count
15
+ ? scanSettings?.cpu_worker_count || 1
15
16
  : CPU_POOL_SIZE;
16
17
  // spawn CPU workers
17
18
  const cpuWorkers = [];
@@ -32,6 +33,7 @@ export async function createWebworker(handle_result, scan_settings_path, pathPre
32
33
  };
33
34
  cpuWorkers.push(cpuWorker);
34
35
  cpuPorts.push(channel.port2);
36
+ await sleep(50);
35
37
  }
36
38
  const coordinationWorker = await startWebworkerReady();
37
39
  coordinationWorker.postMessage({
@@ -39,13 +41,41 @@ export async function createWebworker(handle_result, scan_settings_path, pathPre
39
41
  scan_settings_path: resolvedPath,
40
42
  pathPrefix,
41
43
  role: "coordinator",
42
- node_url,
43
- start_height: 0,
44
- }, cpuPorts); // transfer CPU port2s to coordination worker
44
+ }, cpuPorts); // transfer CPU ports to coordination worker
45
45
  log("createWebworker", [
46
46
  "coordinator worker started, node_url=" + node_url,
47
47
  ", cpuPorts=" + cpuPorts.length,
48
48
  ]);
49
+ let shutdownResolve;
50
+ let shuttingDown = false;
51
+ const hardTerminate = () => {
52
+ for (const p of cpuPorts) {
53
+ try {
54
+ p.close();
55
+ }
56
+ catch {
57
+ // port may already be closed
58
+ }
59
+ }
60
+ coordinationWorker.onmessage = null;
61
+ coordinationWorker.onerror = null;
62
+ for (const w of cpuWorkers) {
63
+ w.onerror = null;
64
+ w.onmessage = null;
65
+ try {
66
+ w.terminate();
67
+ }
68
+ catch {
69
+ // already dead
70
+ }
71
+ }
72
+ try {
73
+ coordinationWorker.terminate();
74
+ }
75
+ catch {
76
+ // already dead
77
+ }
78
+ };
49
79
  coordinationWorker.onmessage = (event) => {
50
80
  if (event.data.type === "ERROR") {
51
81
  if (handle_error)
@@ -55,14 +85,74 @@ export async function createWebworker(handle_result, scan_settings_path, pathPre
55
85
  if (handle_result)
56
86
  handle_result(event.data.payload);
57
87
  }
88
+ else if (event.data.type === "SHUTDOWN_DONE") {
89
+ log("createWebworker", ["SHUTDOWN_DONE received"]);
90
+ shutdownResolve?.();
91
+ shutdownResolve = undefined;
92
+ }
58
93
  };
94
+ // wait for one HEAP_SNAPSHOT_DONE / ERROR from a worker
95
+ const waitHeap = (w, path, timeoutMs = 15000) => new Promise((resolve, reject) => {
96
+ const t = setTimeout(() => {
97
+ w.removeEventListener("message", onMsg);
98
+ reject(new Error("heap snapshot timeout: " + path));
99
+ }, timeoutMs);
100
+ const onMsg = (event) => {
101
+ if (event.data?.type === "HEAP_SNAPSHOT_DONE") {
102
+ clearTimeout(t);
103
+ w.removeEventListener("message", onMsg);
104
+ resolve(event.data.path || path);
105
+ }
106
+ else if (event.data?.type === "HEAP_SNAPSHOT_ERROR") {
107
+ clearTimeout(t);
108
+ w.removeEventListener("message", onMsg);
109
+ reject(new Error(String(event.data.error || "heap snapshot failed")));
110
+ }
111
+ };
112
+ w.addEventListener("message", onMsg);
113
+ try {
114
+ w.postMessage({ type: "heap_snapshot", path });
115
+ }
116
+ catch (err) {
117
+ clearTimeout(t);
118
+ w.removeEventListener("message", onMsg);
119
+ reject(err);
120
+ }
121
+ });
59
122
  return {
60
123
  fetchWorker: coordinationWorker,
61
124
  cpuWorkers,
125
+ // graceful only: abort fetch, cancel cpus, wait. does not terminate.
126
+ shutdown: async (timeoutMs = 5000) => {
127
+ if (shuttingDown)
128
+ return;
129
+ shuttingDown = true;
130
+ log("createWebworker", ["shutdown start, timeoutMs=", timeoutMs]);
131
+ const done = new Promise((resolve) => {
132
+ shutdownResolve = resolve;
133
+ });
134
+ try {
135
+ coordinationWorker.postMessage({ type: "shutdown" });
136
+ }
137
+ catch (err) {
138
+ log("createWebworker", ["post shutdown failed", String(err)]);
139
+ return;
140
+ }
141
+ await Promise.race([done, sleep(timeoutMs)]);
142
+ },
62
143
  terminate: () => {
63
- coordinationWorker.terminate();
64
- for (const w of cpuWorkers)
65
- w.terminate();
144
+ hardTerminate();
145
+ },
146
+ dumpHeaps: async (dir = ".") => {
147
+ const stamp = Date.now();
148
+ const paths = [];
149
+ const coordPath = `${dir}/coordinator-${stamp}.heapsnapshot`;
150
+ paths.push(await waitHeap(coordinationWorker, coordPath));
151
+ for (let i = 0; i < cpuWorkers.length; i++) {
152
+ const p = `${dir}/cpubound-${i}-${stamp}.heapsnapshot`;
153
+ paths.push(await waitHeap(cpuWorkers[i], p));
154
+ }
155
+ return paths;
66
156
  },
67
157
  };
68
158
  }
@@ -73,36 +163,16 @@ export async function createWebworker(handle_result, scan_settings_path, pathPre
73
163
  export function makeWebworkerScript() {
74
164
  return workerMainCode;
75
165
  }
76
- export function startWebworker(handle_result, handle_error) {
77
- const blob = new Blob([workerMainCode], {
78
- type: "text/javascript",
79
- });
80
- const url = URL.createObjectURL(blob);
81
- const worker = new Worker(url, { type: "module" });
82
- worker.onmessage = (event) => {
83
- switch (event.data.type) {
84
- case "RESULT":
85
- if (handle_result)
86
- handle_result(event.data.payload);
87
- break;
88
- case "ERROR":
89
- case "scan_error":
90
- if (handle_error)
91
- handle_error(event.data.payload ?? event.data.error);
92
- break;
93
- case "DEBUG":
94
- log("startWebworker", ["worker debug:", event.data.payload]);
95
- break;
96
- }
97
- };
98
- return worker;
99
- }
166
+ const blob = new Blob([workerMainCode], {
167
+ type: "text/javascript",
168
+ });
169
+ const url = URL.createObjectURL(blob);
100
170
  // creates a worker via startWebworker, then waits for it to signal
101
171
  // WORKER_READY (meaning self.onmessage = handleMessage has executed).
102
172
  // after that, postMessage is safe, no race between worker startup
103
173
  // and message delivery.
104
174
  export function startWebworkerReady() {
105
- const worker = startWebworker();
175
+ const worker = new Worker(url, { type: "module" });
106
176
  return new Promise((resolve) => {
107
177
  const onReady = (e) => {
108
178
  if (e.data?.type === "WORKER_READY") {
@@ -27,6 +27,10 @@ max_blocks_buffer_size = MAX_BLOCKS_BUFFER_SIZE, anchor_range, stopSync) {
27
27
  };
28
28
  yield connection_status.last_packet;
29
29
  while (true) {
30
+ if (stopSync?.aborted) {
31
+ log("blocksBufferFetchLoop", ["aborted, exiting fetch loop"]);
32
+ return;
33
+ }
30
34
  // avoid overwriting last_packet in case of catastrophic reorg
31
35
  // this generator will block on this. coordinator should rethrow the CatastrophicReorgError
32
36
  if (connection_status.last_packet.status === "catastrophic_reorg") {
@@ -39,9 +43,17 @@ max_blocks_buffer_size = MAX_BLOCKS_BUFFER_SIZE, anchor_range, stopSync) {
39
43
  bytes_read: 0,
40
44
  node_url: node_url,
41
45
  timestamp: new Date().toISOString(),
46
+ daemon_height: connection_status.sync.daemon_height ||
47
+ connection_status.last_packet.daemon_height,
42
48
  };
43
49
  yield connection_status.last_packet;
44
- await sleep(5000);
50
+ try {
51
+ await sleep(1000, stopSync);
52
+ }
53
+ catch {
54
+ log("blocksBufferFetchLoop", ["aborted during buffer-full sleep"]);
55
+ return;
56
+ }
45
57
  continue;
46
58
  }
47
59
  log("blocksBufferFetchLoop", [
@@ -56,11 +68,18 @@ max_blocks_buffer_size = MAX_BLOCKS_BUFFER_SIZE, anchor_range, stopSync) {
56
68
  get_blocks_bin = await doRPCrequest(nodeUrl, current_range, stopSync);
57
69
  }
58
70
  catch (error) {
71
+ // abort is intentional shutdown, not a connection failure
72
+ if (stopSync?.aborted || isAbortError(error)) {
73
+ log("blocksBufferFetchLoop", ["aborted during rpc request"]);
74
+ return;
75
+ }
59
76
  connection_status.last_packet = {
60
77
  status: "connection_failed",
61
78
  bytes_read: 0,
62
79
  node_url,
63
80
  timestamp: new Date().toISOString(),
81
+ daemon_height: connection_status.sync.daemon_height ||
82
+ connection_status.last_packet.daemon_height,
64
83
  };
65
84
  yield connection_status.last_packet;
66
85
  throw error;
@@ -78,7 +97,13 @@ max_blocks_buffer_size = MAX_BLOCKS_BUFFER_SIZE, anchor_range, stopSync) {
78
97
  yield connection_status.last_packet;
79
98
  // no new blocks: at tip, sleep and retry
80
99
  if (!result_meta.block_infos.length) {
81
- await sleep(1000);
100
+ try {
101
+ await sleep(1000, stopSync);
102
+ }
103
+ catch {
104
+ log("blocksBufferFetchLoop", ["aborted during tip sleep"]);
105
+ return;
106
+ }
82
107
  continue;
83
108
  }
84
109
  const bufferItem = makeBlocksBufferItem(result_meta, get_blocks_bin);
@@ -379,3 +404,9 @@ export async function reduceStartHeightToTip(start_height, node_url) {
379
404
  export class CatastrophicReorgError extends Error {
380
405
  name = "CatastrophicReorgError";
381
406
  }
407
+ function isAbortError(error) {
408
+ if (!error || typeof error !== "object")
409
+ return false;
410
+ const name = error.name;
411
+ return name === "AbortError" || name === "DOMException";
412
+ }
@@ -19,6 +19,12 @@ export type ConnectionStatus = {
19
19
  last_packet: ConnectionSatusLastPacket;
20
20
  sync: ConnectionStatusSync;
21
21
  };
22
+ export declare function applyWalletScanProgress(cs: ConnectionStatus, progress: {
23
+ current_scan_height: number;
24
+ scanned_ranges?: CacheRange[];
25
+ daemon_height?: number;
26
+ eta?: string;
27
+ }): void;
22
28
  export declare const DEFAULT_CONNECTION_STATUS_PREFIX = "ConnectionStatus-";
23
29
  export declare function msToHHMM(ms: number): string;
24
30
  export declare function emptyConnectionStatus(overrides?: Partial<ConnectionStatus>): ConnectionStatus;
@@ -1,5 +1,20 @@
1
1
  import { atomicWrite, } from "../api";
2
2
  import { SCAN_SETTINGS_STORE_NAME_DEFAULT } from "../api";
3
+ // wallet progress write: always set scan fields; only replace eta when a new one is provided
4
+ // so a missing eta does not wipe / flicker the previous value.
5
+ export function applyWalletScanProgress(cs, progress) {
6
+ cs.sync.current_scan_height = progress.current_scan_height;
7
+ if (progress.scanned_ranges) {
8
+ cs.sync.scanned_ranges = progress.scanned_ranges;
9
+ }
10
+ if (typeof progress.daemon_height === "number") {
11
+ cs.sync.daemon_height = progress.daemon_height;
12
+ }
13
+ if (progress.eta !== undefined) {
14
+ cs.sync.eta = progress.eta;
15
+ }
16
+ cs.sync.timestamp = new Date().toISOString();
17
+ }
3
18
  export const DEFAULT_CONNECTION_STATUS_PREFIX = "ConnectionStatus-";
4
19
  export function msToHHMM(ms) {
5
20
  const totalSeconds = Math.floor(ms / 1000);
@@ -4,12 +4,14 @@ export declare class ConnectionStatusOpened {
4
4
  private _timer;
5
5
  private _cached;
6
6
  private _prevConnected;
7
+ private _prevSyncKey;
7
8
  private _path;
8
9
  constructor(scan_settings_path: string, _onChange?: ((status: ConnectionStatus | null) => void) | null | undefined);
9
- watch(intervalMs?: number): void;
10
+ watch(intervalMs?: number): Promise<void>;
10
11
  unwatch(): void;
11
12
  get connectionStatus(): ConnectionStatus | null;
12
13
  get isConnected(): boolean;
13
14
  get daemonHeight(): number | undefined;
15
+ private _syncKey;
14
16
  private _poll;
15
17
  }
@@ -4,15 +4,17 @@ export class ConnectionStatusOpened {
4
4
  _timer;
5
5
  _cached = null;
6
6
  _prevConnected = false;
7
+ // notify when sync progress fields change, not only connect flip
8
+ _prevSyncKey = "";
7
9
  _path;
8
10
  constructor(scan_settings_path, _onChange) {
9
11
  this._onChange = _onChange;
10
12
  this._path = connectionStatusFilePath(scan_settings_path);
11
13
  }
12
- watch(intervalMs = 2500) {
14
+ async watch(intervalMs = 2500) {
13
15
  if (this._timer)
14
16
  return;
15
- this._poll();
17
+ await this._poll();
16
18
  this._timer = setInterval(() => this._poll(), intervalMs);
17
19
  }
18
20
  unwatch() {
@@ -37,14 +39,29 @@ export class ConnectionStatusOpened {
37
39
  return age >= 0 && age <= 10_000;
38
40
  }
39
41
  get daemonHeight() {
40
- return this._cached?.last_packet?.daemon_height;
42
+ return this._cached?.sync?.daemon_height;
43
+ }
44
+ _syncKey(cs) {
45
+ const s = cs?.sync;
46
+ if (!s)
47
+ return "";
48
+ return [
49
+ s.daemon_height ?? "",
50
+ s.current_scan_height ?? "",
51
+ s.eta ?? "",
52
+ s.timestamp ?? "",
53
+ ].join("|");
41
54
  }
42
55
  async _poll() {
43
56
  this._cached =
44
57
  (await readConnectionStatusFile(this._path).catch(() => null)) || null;
45
58
  const now = this.isConnected;
46
- if (now !== this._prevConnected) {
47
- this._prevConnected = now;
59
+ const syncKey = this._syncKey(this._cached);
60
+ const connectedChanged = now !== this._prevConnected;
61
+ const syncChanged = syncKey !== this._prevSyncKey;
62
+ this._prevConnected = now;
63
+ this._prevSyncKey = syncKey;
64
+ if (connectedChanged || syncChanged) {
48
65
  this._onChange?.(this._cached);
49
66
  }
50
67
  }
@@ -7,9 +7,12 @@ import { ManyScanCachesOpened, ScanCacheOpened, type ManyScanCachesOpenedCreateO
7
7
  * @param options.no_worker to feed the ManyScanCachesOpened manually with .feed(params) from CacheChangedCallbackParams
8
8
  * @param options.no_stats to disable creation of stats file with aggregated information e.g. amount per wallet / subaddress
9
9
  * @param options.notifyMasterChanged pass the output of this to another (no_worker) instance to feed
10
+ * @param options.logs "console" | "file" | "console-and-file" | "off"
11
+ * @param options.logs_include function names to include (e.g. handleCpuboundScan, blocksBufferFetchLoop)
12
+ * @param options.logs_exclude function names to exclude
10
13
  * @returns Promise<ManyScanCachesOpened>
11
14
  */
12
- export declare function openWallets(options?: ManyScanCachesOpenedCreateOptions): Promise<ManyScanCachesOpened | undefined>;
15
+ export declare function openWallets(options?: ManyScanCachesOpenedCreateOptions): Promise<ManyScanCachesOpened>;
13
16
  /**
14
17
  * Opens a **single wallet** for scanning.
15
18
  * **Touches ScanSettings.json**,
@@ -8,6 +8,9 @@ import { openScanSettingsFile, writeWalletToScanSettings } from "../api";
8
8
  * @param options.no_worker to feed the ManyScanCachesOpened manually with .feed(params) from CacheChangedCallbackParams
9
9
  * @param options.no_stats to disable creation of stats file with aggregated information e.g. amount per wallet / subaddress
10
10
  * @param options.notifyMasterChanged pass the output of this to another (no_worker) instance to feed
11
+ * @param options.logs "console" | "file" | "console-and-file" | "off"
12
+ * @param options.logs_include function names to include (e.g. handleCpuboundScan, blocksBufferFetchLoop)
13
+ * @param options.logs_exclude function names to exclude
11
14
  * @returns Promise<ManyScanCachesOpened>
12
15
  */
13
16
  export async function openWallets(options) {
@@ -129,8 +129,10 @@ export declare class ScanCacheOpened {
129
129
  remove: () => null;
130
130
  };
131
131
  pause(): Promise<void>;
132
- stopWorker(): void;
132
+ dumpWorkerHeaps(dir?: string): Promise<string[]>;
133
+ stopWorker(): Promise<void>;
133
134
  unpause(): Promise<void>;
135
+ private _onWorkerError;
134
136
  /**
135
137
  * selectInputs returns array of inputs, whose sum is larger than amount
136
138
  * adds approximate fee for 10kb transaction to amount if feePerByte is supplied
@@ -177,6 +179,9 @@ export type ManyScanCachesOpenedCreateOptions = {
177
179
  retryDelayMs?: number;
178
180
  connectionStatusIntervalMs?: number;
179
181
  onConnectionStatusChange?: ((status: ConnectionStatus | null) => void) | null;
182
+ logs?: LogSetting;
183
+ logs_include?: PossibleLogs[];
184
+ logs_exclude?: PossibleLogs[];
180
185
  };
181
186
  export declare class ManyScanCachesOpened {
182
187
  readonly connectionStatusOpened: ConnectionStatusOpened;
@@ -186,8 +191,9 @@ export declare class ManyScanCachesOpened {
186
191
  get current_height(): number | null;
187
192
  get node_url(): string;
188
193
  changeNodeUrlAndStartHeight(node_url?: string, start_height?: number | null): Promise<void>;
189
- retry(): Promise<boolean>;
190
- stopWorker(): void;
194
+ retry(): Promise<boolean | undefined>;
195
+ stopWorker(): Promise<void>;
196
+ dumpWorkerHeaps(dir?: string): Promise<string[]>;
191
197
  changeNodeUrl(node_url: string): Promise<void>;
192
198
  get merchant_confirmations(): number | null | undefined;
193
199
  get cpu_worker_count(): number | undefined;
@@ -202,10 +208,12 @@ export declare class ManyScanCachesOpened {
202
208
  setCpuWorkerCount(cpu_worker_count: number | undefined): Promise<void>;
203
209
  setLogSettings(logs?: LogSetting | null, logs_include?: PossibleLogs[] | null, logs_exclude?: PossibleLogs[] | null): Promise<void>;
204
210
  setWalletName(primary_address: string, name?: string): Promise<void>;
211
+ setWalletSlot(primary_address: string, slot?: number): Promise<void>;
205
212
  changeStartHeight(start_height: number | null): Promise<void>;
206
213
  private static _buildWallets;
207
- static create(options: ManyScanCachesOpenedCreateOptions): Promise<ManyScanCachesOpened | undefined>;
214
+ static create(options: ManyScanCachesOpenedCreateOptions): Promise<ManyScanCachesOpened>;
208
215
  buildWallets(): Promise<void>;
216
+ private reloadWalletsAfterStop;
209
217
  addViewWallet(primary_address: string, view_key: string, fields?: {
210
218
  wallet_name?: string;
211
219
  wallet_slot?: number;