@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
@@ -1,6 +1,6 @@
1
- import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, readWriteConnectionStatusFile, connectionStatusFilePath, } from "../../api";
2
- import { markWorkItemAsDone, } from "./scanLoop";
3
- import { makeWorkItem, scanLoop } from "./scanLoop";
1
+ import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, readWriteConnectionStatusFile, applyWalletScanProgress, } from "../../api";
2
+ import {} from "./scanLoop";
3
+ import { makeWorkItem } from "./scanLoop";
4
4
  import { cullTooLargeScanHeight, getNonHaltedWallets, getPathPrefix, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, } from "../../api";
5
5
  import { findRange, initScanCacheFile, lastRange, makeCacheRangeForHeight, mergeRanges, } from "./scanCache";
6
6
  import { sendToCpuWorker } from "../worker-mains/cpubound-main";
@@ -166,19 +166,6 @@ export function reconcileWorkItemDone(blocksBuffer, workItemBuffer) {
166
166
  }
167
167
  }
168
168
  }
169
- /**
170
- * one round of the select race. takes two primed promises (blocks, scan),
171
- * races them, returns winner info. the loser stays in flight.
172
- */
173
- export async function raceStep(blocksPromise, scanPromise) {
174
- const winner = await Promise.race([
175
- blocksPromise.then((v) => ({ src: "blocks", value: v })),
176
- scanPromise.then((v) => ({ src: "scan", value: v })),
177
- ]);
178
- const value = winner.value;
179
- const done = !!winner.value.done;
180
- return { src: winner.src, value, done };
181
- }
182
169
  /**
183
170
  * handle a yield from the blocks buffer fetch loop.
184
171
  */
@@ -189,45 +176,6 @@ export async function handleBlocksYield(value, scanSettingsPath) {
189
176
  await handleConnectionStatusChanges(value, scanSettingsPath);
190
177
  return { isBlocksBufferChanged: false };
191
178
  }
192
- /**
193
- * process a scan result for a completed work item.
194
- * updates the cache, writes to disk, marks work item done, reconciles.
195
- */
196
- export async function processScanResultForWorkItem(value, workBuffer, blocksBuffer, pathPrefix, secret_spend_key) {
197
- if (value.type !== "Ready" || !value.result || !value.work_uuid)
198
- return;
199
- const item = await markWorkItemAsDone(value, workBuffer);
200
- log("processScanResultForWorkItem", `item_work_uuid=${item?.work_uuid}`);
201
- if (!item || item.status !== "scanwork_done")
202
- throw new Error("[processScanResultForWorkItem] item not found or not scanwork not done. item_status=" +
203
- item?.status);
204
- const firstBlock = item.batch.get_blocks_result_meta.block_infos[item.from];
205
- log("processScanResultForWorkItem", [
206
- `block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} firstBlock=${firstBlock?.block_height} firstBlockHash=${firstBlock?.block_hash?.slice(0, 8)}`,
207
- ]);
208
- log("processScanResultForWorkItem", [
209
- `block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} from=${item.from} to=${item.to}`,
210
- ]);
211
- const lastBlock = item.batch.get_blocks_result_meta.block_infos[item.to];
212
- const cache = item.walletConfig.cache;
213
- await processScanResult({
214
- from_height: firstBlock.block_height,
215
- to_height: lastBlock.block_height,
216
- result: value.result,
217
- scanCache: cache,
218
- secret_spend_key,
219
- });
220
- await writeCacheToFile(cache, pathPrefix);
221
- item.status = "process_result_done";
222
- reconcileWorkItemDone(blocksBuffer, workBuffer);
223
- //because cache is tied to the workitems by reference,
224
- // the following workitems will have the most recent cache
225
- //TODO: if we do cpu workers we need to be sure to wait with the processing at the top of this functon
226
- // until all workitems for this wallet before it (to the left of it) are done
227
- // the cpu worker loop generator will have to ensure the order of this
228
- // currently as a sideeffect of the work scheduling function on fetch result aka blocks buffer changed,
229
- // this is already implicitly handled
230
- }
231
179
  /**
232
180
  * process a scan result for a completed work item.
233
181
  * updates the cache, writes to disk, marks work item done, reconciles blockbuffer with this.
@@ -272,6 +220,9 @@ export async function processWorkItem(item, workBuffer, blocksBuffer, pathPrefix
272
220
  throw error;
273
221
  }
274
222
  await writeCacheToFile(cache, pathPrefix);
223
+ // raw cpu ScanResult is fully merged into cache now. drop it so workBuffer
224
+ // does not pin all_key_images / hash strings until left-edge free.
225
+ item.result = undefined;
275
226
  item.status = "process_result_done";
276
227
  log("processWorkItem", `process_result_done work_uuid=${item?.work_uuid}`);
277
228
  // remove from blocksbuffer if no more work items reference it
@@ -323,132 +274,7 @@ function computeETA(cache, totalBlocksScanned, scanStartTime) {
323
274
  const etaMs = remaining / blocksPerMs;
324
275
  return msToHHMM(etaMs);
325
276
  }
326
- /**
327
- * coordinator main: async generator that drives the full scan cycle.
328
- * takes only scanSettingsPath, derives everything else internally.
329
- * yields events for some of the underlying generator events,
330
- * look at CoordinatorEvent, processing not decoupled from cpu bound scan work,
331
- * as this single threaded, there is no point
332
- */
333
- export async function* coordinatorMain(scanSettingsPath, pathPrefix) {
334
- const ctx = await setupCoordinator(scanSettingsPath, pathPrefix);
335
- if (!ctx)
336
- throw new Error("[coordinatorMain] findWorkToBeDone returned false");
337
- const work_to_be_done = ctx.work_to_be_done;
338
- const blocksBuffer = ctx.blocksBuffer;
339
- const workBuffer = ctx.workBuffer;
340
- const blocksGenerator = ctx.blocksGenerator;
341
- let totalBlocksScanned = 0;
342
- let scanStartTime = Date.now();
343
- let blocksPromise = blocksGenerator.next();
344
- const scanGens = new Map();
345
- const scanPromises = new Map();
346
- for (const wc of work_to_be_done.wallet_configs) {
347
- scanGens.set(wc.primary_address, scanLoop({
348
- primary_address: wc.primary_address,
349
- secret_view_key: wc.secret_view_key,
350
- secret_spend_key: wc.secret_spend_key,
351
- subaddress_index: wc.subaddress_index,
352
- }));
353
- }
354
- // prime all scan generators
355
- // ( technically no need to do this as loop + scan generator + workbuffer structure mean it will happen automatically)
356
- for (const [, gen] of scanGens) {
357
- await gen.next();
358
- }
359
- while (true) {
360
- const races = [blocksPromise.then((v) => ({ src: "blocks", value: v }))];
361
- for (const [addr, p] of scanPromises) {
362
- races.push(p.then((v) => ({ src: "scan", addr, value: v })));
363
- }
364
- const winner = await Promise.race(races);
365
- log("coordinatorMain", [
366
- "winner=" + winner.src + " addr=" + String(winner.addr ?? "").slice(0, 8),
367
- ]);
368
- if (winner.src === "blocks") {
369
- const result = winner.value.value;
370
- const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
371
- if (isBlocksBufferChanged) {
372
- makeWorkItemsForAllWallets(work_to_be_done.wallet_configs, blocksBuffer, workBuffer);
373
- // start scan promises for wallets that now have work
374
- for (const wc of work_to_be_done.wallet_configs) {
375
- const addr = wc.primary_address;
376
- if (scanPromises.has(addr))
377
- continue;
378
- const gen = scanGens.get(addr);
379
- log("coordinatorMain", ["scanGens.get(addr)", gen]);
380
- if (!gen)
381
- continue;
382
- const item = workBuffer.find((x) => x.status === "fresh" && x.walletConfig.primary_address === addr);
383
- if (item) {
384
- item.status = "scanwork_in_progress";
385
- scanPromises.set(addr, gen.next(item));
386
- }
387
- }
388
- logBufStatus(blocksBuffer, workBuffer, scanPromises, "after_blocks");
389
- yield { type: "blocks_buffer_changed" };
390
- }
391
- else {
392
- logBufStatus(blocksBuffer, workBuffer, scanPromises, "fetch_conn");
393
- yield { type: "connection_status", status: result };
394
- }
395
- blocksPromise = blocksGenerator.next();
396
- }
397
- else {
398
- const addr = winner.addr;
399
- const gen = scanGens.get(addr);
400
- const value = winner.value.value;
401
- if (value.type === "InProgress") {
402
- scanPromises.set(addr, gen.next());
403
- }
404
- else if (value.type === "Ready") {
405
- const wc = work_to_be_done.wallet_configs.find((x) => x.primary_address === addr);
406
- if (!wc)
407
- throw new Error("[coordinatorMain] wallet config not found on process result");
408
- // count blocks scanned in this work item for ETA
409
- const doneItem = workBuffer.find((x) => x.work_uuid === value.work_uuid);
410
- const blockCount = doneItem ? doneItem.to - doneItem.from + 1 : 0;
411
- await processScanResultForWorkItem(value, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wc?.secret_spend_key);
412
- totalBlocksScanned += blockCount;
413
- const eta = computeETA(wc.cache, totalBlocksScanned, scanStartTime);
414
- // write connection status sync (includes eta)
415
- if (eta) {
416
- await readWriteConnectionStatusFile((cs) => {
417
- cs.sync = {
418
- ...cs.sync,
419
- scanned_ranges: wc.cache.scanned_ranges,
420
- daemon_height: wc.cache.daemon_height,
421
- current_scan_height: lastRange(wc.cache.scanned_ranges)?.end || 0,
422
- eta,
423
- timestamp: new Date().toISOString(),
424
- };
425
- }, scanSettingsPath);
426
- }
427
- const nextItem = workBuffer.find((x) => x.status !== "process_result_done" &&
428
- x.walletConfig.primary_address === addr);
429
- if (nextItem) {
430
- scanPromises.set(addr, gen.next(nextItem));
431
- }
432
- else {
433
- scanPromises.delete(addr);
434
- }
435
- logBufStatus(blocksBuffer, workBuffer, scanPromises, "after_scan");
436
- yield {
437
- type: "scan_ready",
438
- address: addr,
439
- result: value,
440
- newCache: wc.cache,
441
- changed_outputs: [],
442
- };
443
- // signal idle when no scans active and nothing buffered
444
- if (scanPromises.size === 0 && blocksBuffer.length === 0) {
445
- yield { type: "all_idle" };
446
- }
447
- }
448
- }
449
- }
450
- }
451
- export async function setupCoordinator(scanSettingsPath, pathPrefix) {
277
+ export async function setupCoordinator(scanSettingsPath, pathPrefix, stopSync) {
452
278
  const work_to_be_done = await findWorkToBeDone(scanSettingsPath, pathPrefix);
453
279
  if (!work_to_be_done)
454
280
  return false;
@@ -457,6 +283,7 @@ export async function setupCoordinator(scanSettingsPath, pathPrefix) {
457
283
  startHeight: work_to_be_done.start_height,
458
284
  anchor_range: work_to_be_done.anchor_range,
459
285
  scanSettingsPath,
286
+ stopSync,
460
287
  });
461
288
  const workBuffer = [];
462
289
  //TODO: refactor to per wallet buffer
@@ -475,21 +302,46 @@ export async function setupCoordinator(scanSettingsPath, pathPrefix) {
475
302
  work_to_be_done,
476
303
  };
477
304
  }
305
+ // flip inflight work back to fresh so a restart can reschedule it
306
+ export function resetInProgressWorkItems(workBuffer) {
307
+ for (const item of workBuffer) {
308
+ if (item.status === "scanwork_in_progress") {
309
+ item.status = "fresh";
310
+ item.result = undefined;
311
+ }
312
+ }
313
+ }
314
+ // tell busy cpu ports to cancel and wait for their promises (with timeout)
315
+ export async function cancelBusyCpuPorts(ports, timeoutMs = 3000) {
316
+ const busy = ports.filter((ps) => ps.promise);
317
+ for (const ps of busy) {
318
+ try {
319
+ sendToCpuWorker(ps.port, "cancel");
320
+ }
321
+ catch (err) {
322
+ log("cancelBusyCpuPorts", ["send cancel failed", String(err)]);
323
+ }
324
+ }
325
+ if (busy.length === 0)
326
+ return;
327
+ await Promise.race([
328
+ Promise.allSettled(busy.map((ps) => ps.promise)),
329
+ sleep(timeoutMs),
330
+ ]);
331
+ for (const ps of ports) {
332
+ ps.promise = null;
333
+ }
334
+ }
478
335
  /**
479
336
  * coordinator main (multithreaded): dispatches scan work to CPU workers
480
337
  * via MessagePorts, processes results in order per wallet.
481
338
  * falls back to single-threaded coordinator if no cpuPorts provided.
482
339
  */
483
- export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix, cpuPorts) {
340
+ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix, cpuPorts, stopSync) {
484
341
  if (!cpuPorts || cpuPorts.length === 0) {
485
- // throw new Error(
486
- // "[coordinatorMain Multithreaded] cpuPorts empty, there must be at least cpu worker",
487
- // );
488
- log("coordinatorMainMultithreaded", "fallback to single-threaded");
489
- yield* coordinatorMain(scanSettingsPath, pathPrefix);
490
- return;
342
+ throw new Error("[coordinatorMain Multithreaded] cpuPorts empty, there must be at least cpu worker");
491
343
  }
492
- const ctx = await setupCoordinator(scanSettingsPath, pathPrefix);
344
+ const ctx = await setupCoordinator(scanSettingsPath, pathPrefix, stopSync);
493
345
  if (!ctx)
494
346
  throw new Error("[coordinatorMain Multithreaded] findWorkToBeDone returned false");
495
347
  const work_to_be_done = ctx.work_to_be_done;
@@ -509,6 +361,13 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
509
361
  }
510
362
  let race_count = 0;
511
363
  while (true) {
364
+ if (stopSync?.aborted) {
365
+ log("coordinatorMainMultithreaded", ["shutdown signal, draining"]);
366
+ await cancelBusyCpuPorts(freePorts);
367
+ resetInProgressWorkItems(workBuffer);
368
+ yield { type: "shutdown_done" };
369
+ return;
370
+ }
512
371
  race_count++;
513
372
  log("coordinatorMainMultithreaded", ["race_count", race_count]);
514
373
  await scheduleWorkOnCpuPorts(freePorts, workBuffer);
@@ -526,6 +385,16 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
526
385
  const winner = await Promise.race(races);
527
386
  log("coordinatorMainMultithreaded", "cpu ports status");
528
387
  if ("src" in winner && winner.src === "blocks") {
388
+ // fetch loop returned (done) on abort, treat as shutdown
389
+ if (winner.value.done) {
390
+ log("coordinatorMainMultithreaded", [
391
+ "blocks generator done, draining",
392
+ ]);
393
+ await cancelBusyCpuPorts(freePorts);
394
+ resetInProgressWorkItems(workBuffer);
395
+ yield { type: "shutdown_done" };
396
+ return;
397
+ }
529
398
  const result = winner.value.value;
530
399
  const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
531
400
  if (isBlocksBufferChanged) {
@@ -558,30 +427,22 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
558
427
  continue;
559
428
  const blockCount = to_be_processed.to - to_be_processed.from + 1;
560
429
  totalBlocksScanned += blockCount;
561
- const eta = computeETA(wallet.cache, totalBlocksScanned, scanStartTime);
562
- // write connection status sync (includes eta)
563
- if (eta) {
564
- await readWriteConnectionStatusFile((cs) => {
565
- cs.sync = {
566
- ...cs.sync,
567
- scanned_ranges: wallet.cache.scanned_ranges,
568
- daemon_height: wallet.cache.daemon_height,
569
- current_scan_height: lastRange(wallet.cache.scanned_ranges)?.end || 0,
570
- eta,
571
- timestamp: new Date().toISOString(),
572
- };
573
- }, scanSettingsPath);
574
- }
575
430
  const res = await processWorkItem(to_be_processed, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wallet.secret_spend_key);
431
+ // always persist wallet progress after process; eta only if we have a new one
432
+ // so missing eta does not wipe the previous value (no flicker)
433
+ const eta = computeETA(wallet.cache, totalBlocksScanned, scanStartTime);
434
+ await readWriteConnectionStatusFile((cs) => {
435
+ applyWalletScanProgress(cs, {
436
+ current_scan_height: lastRange(wallet.cache.scanned_ranges)?.end || 0,
437
+ scanned_ranges: wallet.cache.scanned_ranges,
438
+ daemon_height: wallet.cache.daemon_height,
439
+ eta,
440
+ });
441
+ }, scanSettingsPath);
576
442
  // log("coordinatorMainMultithreaded", "processWorkItem result");
577
443
  yield {
578
444
  type: "scan_ready",
579
445
  address: wallet.primary_address,
580
- result: {
581
- type: "Ready",
582
- work_uuid: to_be_processed.work_uuid,
583
- result: to_be_processed.result,
584
- },
585
446
  newCache: wallet.cache,
586
447
  changed_outputs: res.changed_outputs,
587
448
  };
@@ -606,6 +467,17 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
606
467
  const msg = event.data;
607
468
  // handle the result here:
608
469
  log("scheduleWorkOnCpuPorts", ["onmessage result", msg]);
470
+ // cancel ack may omit work_uuid when idle, but inflight cancel carries it
471
+ if (msg.type === "Canceled") {
472
+ if (item.status === "scanwork_in_progress") {
473
+ item.status = "fresh";
474
+ item.result = undefined;
475
+ }
476
+ port_status.promise = null;
477
+ // thin resolve: dont put full msg/result on the port promise on cancel
478
+ resolve_port({ type: "Canceled", work_uuid: item.work_uuid });
479
+ return;
480
+ }
609
481
  if (msg.work_uuid !== item.work_uuid) {
610
482
  log("scheduleWorkOnCpuPorts", [
611
483
  "wrong work_uuid in msg msg.work_uuid=",
@@ -623,10 +495,18 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
623
495
  log("scheduleWorkOnCpuPorts", "wrong status in msg");
624
496
  throw new Error("[scheduleWorkOnCpuPorts] wrong status in msg");
625
497
  }
498
+ // fat ScanResult lives only on the work item until process clears it
626
499
  item.result = msg.result;
627
500
  item.status = "scanwork_done";
501
+ // cpu already has its own buffer copy; process only needs meta.
502
+ // drop work-item bytes now; blocksBuffer still owns the batch until left-edge free.
503
+ item.batch = {
504
+ ...item.batch,
505
+ data: new Uint8Array(0),
506
+ };
628
507
  port_status.promise = null;
629
- resolve_port(msg);
508
+ // thin resolve: race/await must not retain all_key_images via promise value
509
+ resolve_port({ type: msg.type, work_uuid: msg.work_uuid });
630
510
  };
631
511
  port_status.port.onmessage = onmessage;
632
512
  item.status = "scanwork_in_progress";
@@ -12,9 +12,8 @@ export declare function makeWorkItem(walletConfig: WalletConfigPlusCache, batch:
12
12
  export type ScanLoopIteratorResult = IteratorYieldResult<ScanLoopYield>;
13
13
  export type ScanLoopInput = WorkItem | "cancel" | undefined;
14
14
  export type ScanLoopYield = {
15
- type: "Ready" | "InProgress";
15
+ type: "Ready" | "InProgress" | "Canceled";
16
16
  work_uuid?: string;
17
17
  result?: ScanResult;
18
18
  };
19
19
  export declare function scanLoop(wallet: WalletConfig): AsyncGenerator<ScanLoopYield, void, ScanLoopInput>;
20
- export declare function markWorkItemAsDone(loop_event: ScanLoopYield, work_buffer: WorkItem[]): Promise<WorkItem | undefined>;
@@ -101,16 +101,3 @@ export async function* scanLoop(wallet) {
101
101
  // will be done by processScanResult in the consumer
102
102
  }
103
103
  }
104
- //theoretically markWorkItemAsDone could be done in the generator as WorkItem is passed in by reference
105
- // but: eventually this has to be done accross CPU worker boundaries
106
- // so we act on the yielded scan result
107
- export async function markWorkItemAsDone(loop_event, work_buffer) {
108
- if (loop_event.type === "Ready" && loop_event.work_uuid) {
109
- const work_item = work_buffer.find((w) => w.work_uuid === loop_event.work_uuid);
110
- if (work_item) {
111
- work_item.result = loop_event.result;
112
- work_item.status = "scanwork_done";
113
- return work_item;
114
- }
115
- }
116
- }