@spirobel/monero-wallet-api 0.4.0 → 0.4.1

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.
@@ -1,5 +1,6 @@
1
+ import { log } from "./logging";
1
2
  export async function atomicWrite(targetPath, data) {
2
- //console.log("[atomicWrite] writing to", targetPath);
3
+ log("atomicWrite", ["writing to", targetPath]);
3
4
  // in the browser we don't have rename + indexedDB writes are atomic in any case
4
5
  if (areWeInTheBrowser)
5
6
  return await Bun.write(targetPath, data);
@@ -0,0 +1,40 @@
1
+ /**
2
+ * logging function names registry.
3
+ *
4
+ * useful filter combinations for `scanSettings.json`:
5
+ *
6
+ * "logs": "console",
7
+ * "logs_include": ["handleCpuboundScan", "logBufStatus"]
8
+ * -> cpu worker progress + buffer/work status.
9
+ *
10
+ * "logs_include": ["coordinatorMainMultithreaded", "scheduleWorkOnCpuPorts", "workToBeDoneForBatch"]
11
+ * -> coordinator events: race count, work dispatch, batch decisions.
12
+ *
13
+ * "logs_include": ["blocksBufferFetchLoop", "makeWorkItemsFromBlocksBuffer", "logBufStatus"]
14
+ * -> block fetching, reconciliation, buffer state.
15
+ *
16
+ * "logs_include": ["processScanResult", "processWorkItem", "processScanResultForWorkItem"]
17
+ * -> result processing, cache updates, reorg detection.
18
+ *
19
+ * "logs_include": ["handleConnectionStatusChanges", "handleScanError"]
20
+ * -> connection events and scan errors.
21
+ *
22
+ * "logs": "off" disables all logging.
23
+ * "logs": "file" writes to `<coordinator|cpubound|mainthread>-<id>-<timestamp>.log`.
24
+ * "logs": "console-and-file" writes to file and console.
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"];
27
+ export type PossibleLogs = (typeof LOGGING_FUNCTIONS)[number];
28
+ export type LogSetting = "console" | "file" | "console-and-file" | "off";
29
+ export type FileLogMessage = {
30
+ timestamp: string;
31
+ message: any;
32
+ };
33
+ export declare function setupLoggingPath(scan_settings_path: string, path_prefix: string, role: "coordinator" | "cpubound" | "mainthread", 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")[] | 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")[] | undefined;
37
+ file_logbuffer: FileLogMessage[];
38
+ global_logging_path: string;
39
+ }>;
40
+ export declare function log(fnname: string, message: any): void;
@@ -0,0 +1,172 @@
1
+ import { getPathPrefix, openScanSettingsFile } from "../api";
2
+ import { atomicWrite } from "./atomicWrite";
3
+ /**
4
+ * logging function names registry.
5
+ *
6
+ * useful filter combinations for `scanSettings.json`:
7
+ *
8
+ * "logs": "console",
9
+ * "logs_include": ["handleCpuboundScan", "logBufStatus"]
10
+ * -> cpu worker progress + buffer/work status.
11
+ *
12
+ * "logs_include": ["coordinatorMainMultithreaded", "scheduleWorkOnCpuPorts", "workToBeDoneForBatch"]
13
+ * -> coordinator events: race count, work dispatch, batch decisions.
14
+ *
15
+ * "logs_include": ["blocksBufferFetchLoop", "makeWorkItemsFromBlocksBuffer", "logBufStatus"]
16
+ * -> block fetching, reconciliation, buffer state.
17
+ *
18
+ * "logs_include": ["processScanResult", "processWorkItem", "processScanResultForWorkItem"]
19
+ * -> result processing, cache updates, reorg detection.
20
+ *
21
+ * "logs_include": ["handleConnectionStatusChanges", "handleScanError"]
22
+ * -> connection events and scan errors.
23
+ *
24
+ * "logs": "off" disables all logging.
25
+ * "logs": "file" writes to `<coordinator|cpubound|mainthread>-<id>-<timestamp>.log`.
26
+ * "logs": "console-and-file" writes to file and console.
27
+ */
28
+ export const LOGGING_FUNCTIONS = [
29
+ // scanCoordination.ts
30
+ "findWorkToBeDone",
31
+ "workToBeDoneForBatch",
32
+ "makeWorkItemsFromBlocksBuffer",
33
+ "reconcileWorkItemDone",
34
+ "processScanResultForWorkItem",
35
+ "processWorkItem",
36
+ "logBufStatus",
37
+ "coordinatorMain",
38
+ "coordinatorMainMultithreaded",
39
+ "scheduleWorkOnCpuPorts",
40
+ // scanResult.ts
41
+ "processScanResult",
42
+ // scanCache.ts
43
+ "handleScanError",
44
+ // backgroundWorker.ts
45
+ "createWebworker",
46
+ "startWebworker",
47
+ // blocksBufferFetchLoop.ts
48
+ "blocksBufferFetchLoop",
49
+ "reduceStartHeightToTip",
50
+ // blocksbufferCoordination.ts
51
+ "handleConnectionStatusChanges",
52
+ // cpubound-main.ts
53
+ "handleCpuboundScan",
54
+ // coordinator-main.ts
55
+ "coordinatorMainWorker",
56
+ // worker.ts
57
+ "CPU_PORT_HANDLER",
58
+ // io/atomicWrite.ts
59
+ "atomicWrite",
60
+ ];
61
+ export async function setupLoggingPath(scan_settings_path, path_prefix, role, cpu_worker_id) {
62
+ const scanSettings = await openScanSettingsFile(scan_settings_path);
63
+ const logs = scanSettings?.logs;
64
+ const logs_include = scanSettings?.logs_include;
65
+ const logs_exclude = scanSettings?.logs_exclude;
66
+ const file_logbuffer = [];
67
+ const logging_path_prefix = getPathPrefix(scan_settings_path, path_prefix);
68
+ const timestamp = new Date().toISOString().slice(0, 19).replace("T", " ");
69
+ const global_logging_path = `${logging_path_prefix}${role}-${cpu_worker_id ?? ""}-${timestamp}.log`;
70
+ const global_log_setup = {
71
+ logs,
72
+ logs_include,
73
+ logs_exclude,
74
+ file_logbuffer,
75
+ global_logging_path,
76
+ };
77
+ globalThis.__global_log_setup = global_log_setup;
78
+ startLogFlusher(global_log_setup);
79
+ return global_log_setup;
80
+ }
81
+ function safeStringify(value) {
82
+ try {
83
+ return JSON.stringify(value, (_key, v) => {
84
+ if (typeof v === "bigint")
85
+ return v.toString();
86
+ if (typeof v === "function")
87
+ return `fn ${v.name || "anon"}`;
88
+ if (v !== null && typeof v === "object") {
89
+ // promise or thenable
90
+ if (typeof v.then === "function") {
91
+ try {
92
+ return typeof Bun !== "undefined" && Bun.inspect
93
+ ? Bun.inspect(v)
94
+ : "Promise";
95
+ }
96
+ catch {
97
+ return "Promise";
98
+ }
99
+ }
100
+ // message port
101
+ const ctor = v.constructor;
102
+ if (ctor?.name === "MessagePort")
103
+ return "MessagePort";
104
+ }
105
+ return v;
106
+ });
107
+ }
108
+ catch {
109
+ return String(value);
110
+ }
111
+ }
112
+ const flusherRef = new WeakMap();
113
+ function startLogFlusher(setup) {
114
+ if (setup.logs !== "file" && setup.logs !== "console-and-file")
115
+ return;
116
+ if (flusherRef.has(setup))
117
+ return; // already running
118
+ let writtenIndex = 0;
119
+ const id = setInterval(async () => {
120
+ const buffer = setup.file_logbuffer;
121
+ if (buffer.length <= writtenIndex)
122
+ return; // nothing new
123
+ const newEntries = buffer.slice(writtenIndex);
124
+ writtenIndex = buffer.length;
125
+ if (newEntries.length === 0)
126
+ return;
127
+ const lines = newEntries
128
+ .map((e) => `[${e.timestamp}] ${safeStringify(e.message)}`)
129
+ .join("\n");
130
+ try {
131
+ const existing = await Bun.file(setup.global_logging_path)
132
+ .text()
133
+ .catch(() => "");
134
+ const newContent = existing ? existing + "\n" + lines : lines;
135
+ await atomicWrite(setup.global_logging_path, newContent);
136
+ }
137
+ catch (err) {
138
+ console.error("[logFlusher] write failed:", err);
139
+ }
140
+ }, 2000);
141
+ flusherRef.set(setup, id);
142
+ }
143
+ export function log(fnname, message) {
144
+ const setup = globalThis.__global_log_setup;
145
+ if (!setup)
146
+ return;
147
+ const { logs, logs_include, logs_exclude, file_logbuffer } = setup;
148
+ if (logs === "off")
149
+ return;
150
+ // Apply include/exclude filters
151
+ if (logs_include && logs_include.length > 0 && !logs_include.includes(fnname))
152
+ return;
153
+ if (logs_exclude && logs_exclude.includes(fnname))
154
+ return;
155
+ // skip messages that are about writing to the log file itself
156
+ // (prevents atomicWrite calls inside the flusher from feeding back)
157
+ const logPath = setup.global_logging_path;
158
+ if (logPath) {
159
+ const msg = message;
160
+ if ((typeof msg === "string" && msg.includes(logPath)) ||
161
+ (Array.isArray(msg) &&
162
+ msg.some((m) => typeof m === "string" && m.includes(logPath))))
163
+ return;
164
+ }
165
+ const timestamp = new Date().toISOString().slice(0, 19).replace("T", " ");
166
+ if (logs === "console" || logs === "console-and-file") {
167
+ console.log(`[${fnname}]`, ...(Array.isArray(message) ? message : [message]));
168
+ }
169
+ if (logs === "file" || logs === "console-and-file") {
170
+ file_logbuffer.push({ timestamp, message });
171
+ }
172
+ }
@@ -1,4 +1,5 @@
1
1
  import { type CacheChangedCallbackParameters } from "./scanresult/scanCache";
2
+ export declare const CPU_POOL_SIZE = 4;
2
3
  export type WorkerSet = {
3
4
  fetchWorker: Worker;
4
5
  cpuWorkers: Worker[];
@@ -1,11 +1,14 @@
1
1
  import { LOCAL_NODE_DEFAULT_URL } from "../node-interaction/nodeUrl";
2
2
  import {} from "./scanresult/scanCache";
3
- import { openScanSettingsFile } from "./scanSettings";
3
+ import { openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, } from "./scanSettings";
4
+ import { log, setupLoggingPath } from "../io/logging";
4
5
  import { workerMainCode } from "./worker-entrypoints/worker";
5
- const CPU_POOL_SIZE = 4;
6
+ export const CPU_POOL_SIZE = 4;
6
7
  export async function createWebworker(handle_result, scan_settings_path, pathPrefix, handle_error) {
7
8
  try {
8
- const scanSettings = await openScanSettingsFile(scan_settings_path);
9
+ const resolvedPath = scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT;
10
+ const scanSettings = await openScanSettingsFile(resolvedPath);
11
+ await setupLoggingPath(resolvedPath, pathPrefix ?? "", "mainthread");
9
12
  const node_url = scanSettings?.node_url || LOCAL_NODE_DEFAULT_URL;
10
13
  const cpu_worker_count = typeof scanSettings?.cpu_worker_count !== "undefined"
11
14
  ? scanSettings?.cpu_worker_count
@@ -16,7 +19,13 @@ export async function createWebworker(handle_result, scan_settings_path, pathPre
16
19
  for (let i = 0; i < cpu_worker_count; i++) {
17
20
  const channel = new MessageChannel();
18
21
  const cpuWorker = await startWebworkerReady();
19
- cpuWorker.postMessage({ type: "setup", role: "cpubound", cpu_worker_id: i }, [channel.port1]);
22
+ cpuWorker.postMessage({
23
+ type: "setup",
24
+ role: "cpubound",
25
+ cpu_worker_id: i,
26
+ scan_settings_path: resolvedPath,
27
+ pathPrefix,
28
+ }, [channel.port1]);
20
29
  cpuWorker.onerror = (e) => {
21
30
  if (handle_error)
22
31
  handle_error("cpu worker " + i + " error: " + (e.message ?? String(e)));
@@ -27,13 +36,16 @@ export async function createWebworker(handle_result, scan_settings_path, pathPre
27
36
  const coordinationWorker = await startWebworkerReady();
28
37
  coordinationWorker.postMessage({
29
38
  type: "setup",
30
- scan_settings_path,
39
+ scan_settings_path: resolvedPath,
31
40
  pathPrefix,
32
41
  role: "coordinator",
33
42
  node_url,
34
43
  start_height: 0,
35
44
  }, cpuPorts); // transfer CPU port2s to coordination worker
36
- console.log("[createWebworker] coordinator worker started, node_url=" + node_url, ", cpuPorts=" + cpuPorts.length);
45
+ log("createWebworker", [
46
+ "coordinator worker started, node_url=" + node_url,
47
+ ", cpuPorts=" + cpuPorts.length,
48
+ ]);
37
49
  coordinationWorker.onmessage = (event) => {
38
50
  if (event.data.type === "ERROR") {
39
51
  if (handle_error)
@@ -79,7 +91,7 @@ export function startWebworker(handle_result, handle_error) {
79
91
  handle_error(event.data.payload ?? event.data.error);
80
92
  break;
81
93
  case "DEBUG":
82
- console.log("worker debug:", event.data.payload);
94
+ log("startWebworker", ["worker debug:", event.data.payload]);
83
95
  break;
84
96
  }
85
97
  };
@@ -1,5 +1,6 @@
1
1
  import { findRange, get_block_headers_range, get_info, mergeRanges, NodeUrl, sleep, } from "../../api";
2
2
  import {} from "../connectionStatus";
3
+ import { log } from "../../io/logging";
3
4
  export const MAX_BLOCKS_BUFFER_SIZE = 10;
4
5
  // runs forever, fetching blocks from the node.
5
6
  // handles scan level reorg detection and catastrophic reorg
@@ -8,7 +9,7 @@ connection_status, // we make a local copy of this and pass last_packet and sync
8
9
  max_blocks_buffer_size = MAX_BLOCKS_BUFFER_SIZE, anchor_range, stopSync) {
9
10
  connection_status = structuredClone(connection_status);
10
11
  const nodeUrl = await NodeUrl.create(node_url);
11
- console.log("[blocksBufferFetchLoop] NodeUrl created, fetching info...");
12
+ log("blocksBufferFetchLoop", ["NodeUrl:", nodeUrl.node_url, "start:", start_height]);
12
13
  start_height = await reduceStartHeightToTip(start_height, nodeUrl.node_url);
13
14
  // initialise ranges on first call
14
15
  let { current_range, scanned_ranges } = await initScannedRanges(nodeUrl.node_url, start_height, anchor_range ? [anchor_range] : []);
@@ -38,12 +39,10 @@ max_blocks_buffer_size = MAX_BLOCKS_BUFFER_SIZE, anchor_range, stopSync) {
38
39
  await sleep(5000);
39
40
  continue;
40
41
  }
41
- console.log("[blocksBufferFetchLoop] fetching from current_range...");
42
+ log("blocksBufferFetchLoop", ["fetching", nodeUrl.node_url, current_range.start, "->", current_range.end]);
42
43
  const get_blocks_bin = await doRPCrequest(nodeUrl, current_range, stopSync);
43
44
  const result_meta = await nodeUrl.loadGetBlocksBinResponse(get_blocks_bin);
44
- console.log("[blocksBufferFetchLoop] response: " +
45
- result_meta.block_infos.length +
46
- " blocks");
45
+ log("blocksBufferFetchLoop", "response: " + result_meta.block_infos.length + " blocks");
47
46
  connection_status.last_packet = {
48
47
  status: "OK",
49
48
  bytes_read: get_blocks_bin.length,
@@ -343,8 +342,11 @@ export async function reduceStartHeightToTip(start_height, node_url) {
343
342
  if (start_height > getInfo.height - 1) {
344
343
  const oldStartHeight = start_height;
345
344
  start_height = getInfo.height - 1;
346
- console.log("[reduceStartHeightToTip] start height was larger than daemon height, setting start_height=" +
347
- start_height, " oldStartHeight=" + oldStartHeight);
345
+ log("reduceStartHeightToTip", [
346
+ "start height was larger than daemon height, setting start_height=" +
347
+ start_height,
348
+ " oldStartHeight=" + oldStartHeight,
349
+ ]);
348
350
  }
349
351
  return start_height;
350
352
  }
@@ -1,4 +1,5 @@
1
1
  import { blocksBufferFetchLoop, readWriteConnectionStatusFile, CatastrophicReorgError, readOrInitConnectionStatus, } from "../../api";
2
+ import { log } from "../../io/logging";
2
3
  export async function setupBlocksBufferGenerator(params) {
3
4
  const blocksBuffer = [];
4
5
  const connection_status = await readOrInitConnectionStatus(params.scanSettingsPath);
@@ -13,7 +14,7 @@ export async function handleConnectionStatusChanges(event, scanSettingsPath) {
13
14
  cs2.last_packet = event;
14
15
  }, scanSettingsPath);
15
16
  if (event.status === "catastrophic_reorg") {
16
- console.log("[blocksbufferCoordinator] catastrophic reorg, stopping");
17
+ log("handleConnectionStatusChanges", "catastrophic reorg, stopping");
17
18
  throw new CatastrophicReorgError("[blocksbufferCoordinator] catastrophic reorg, stopping");
18
19
  }
19
20
  }
@@ -1,3 +1,4 @@
1
+ import type { LogSetting, PossibleLogs } from "../io/logging";
1
2
  export declare const SCAN_SETTINGS_STORE_NAME_DEFAULT = "ScanSettings.json";
2
3
  export type ScanSetting = {
3
4
  primary_address: string;
@@ -17,6 +18,9 @@ export type WriteScanSettingParams = {
17
18
  wallet_route?: string;
18
19
  wallet_name?: string;
19
20
  wallet_slot?: number;
21
+ logs?: LogSetting;
22
+ logs_include?: PossibleLogs[];
23
+ logs_exclude?: PossibleLogs[];
20
24
  };
21
25
  export type ScanSettingOpened = {
22
26
  primary_address: string;
@@ -36,6 +40,9 @@ export type ScanSettings = {
36
40
  start_height: number | null;
37
41
  merchant_confirmations?: number | null;
38
42
  cpu_worker_count?: number;
43
+ logs?: LogSetting;
44
+ logs_include?: PossibleLogs[];
45
+ logs_exclude?: PossibleLogs[];
39
46
  };
40
47
  export type ScanSettingsOpened = {
41
48
  wallets: (ScanSettingOpened | undefined)[];
@@ -43,6 +50,9 @@ export type ScanSettingsOpened = {
43
50
  start_height: number | null;
44
51
  merchant_confirmations?: number | null;
45
52
  cpu_worker_count?: number;
53
+ logs?: LogSetting;
54
+ logs_include?: PossibleLogs[];
55
+ logs_exclude?: PossibleLogs[];
46
56
  };
47
57
  /**
48
58
  * Writes scan settings to the default or specified storage file in json.
@@ -239,10 +239,19 @@ export async function writeWalletToScanSettings(params) {
239
239
  },
240
240
  ],
241
241
  node_url: params.node_url,
242
+ logs: params.logs,
243
+ logs_include: params.logs_include,
244
+ logs_exclude: params.logs_exclude,
242
245
  start_height: params.start_height || null,
243
246
  }, params.scan_settings_path);
244
247
  }
245
248
  scanSettings.node_url = params.node_url || scanSettings.node_url;
249
+ if (params.logs)
250
+ scanSettings.logs = params.logs;
251
+ if (params.logs_include)
252
+ scanSettings.logs_include = params.logs_include;
253
+ if (params.logs_exclude)
254
+ scanSettings.logs_exclude = params.logs_exclude;
246
255
  scanSettings.start_height =
247
256
  params.start_height === undefined
248
257
  ? scanSettings.start_height
@@ -1,6 +1,7 @@
1
1
  import {} from "./computeKeyImage";
2
2
  import { get_block_headers_range, } from "../../api";
3
3
  import { atomicWrite } from "../../io/atomicWrite";
4
+ import { log } from "../../io/logging";
4
5
  export async function initScanCache(viewpair, start_height, scan_settings_path, pathPrefix) {
5
6
  const initialCache = await readCacheFileDefaultLocation(viewpair.primary_address, pathPrefix);
6
7
  let cache = {
@@ -161,7 +162,7 @@ export function handleScanError(error) {
161
162
  // treat errno 0 code "ConnectionRefused" as non fatal outcome, and rethrow,
162
163
  // so that UI can be informed after catching it higher up
163
164
  if (isConnectionError(error)) {
164
- console.log("Scan stopped. node might be offline. Connection Refused");
165
+ log("handleScanError", "Scan stopped. node might be offline. Connection Refused");
165
166
  throw error;
166
167
  }
167
168
  // Treat AbortError as a normal, non-fatal outcome
@@ -169,11 +170,14 @@ export function handleScanError(error) {
169
170
  typeof error === "object" &&
170
171
  (("name" in error && error.name === "AbortError") ||
171
172
  ("code" in error && error.code === 20))) {
172
- console.log("Scan was aborted.");
173
+ log("handleScanError", "Scan was aborted.");
173
174
  return;
174
175
  }
175
176
  else {
176
- console.log(error, "\n, scanWithCache in scanning-syncing/scanWithCache.ts`");
177
+ log("handleScanError", [
178
+ error,
179
+ "\n, scanWithCache in scanning-syncing/scanWithCache.ts`",
180
+ ]);
177
181
  throw error;
178
182
  }
179
183
  }