@spirobel/monero-wallet-api 0.5.4 → 0.6.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.
Files changed (91) hide show
  1. package/dist/actionlog/actionLogOpened.d.ts +96 -1
  2. package/dist/actionlog/actionLogOpened.js +386 -1
  3. package/dist/actionlog/actionLogTypes.d.ts +114 -0
  4. package/dist/actionlog/actionLogTypes.js +198 -0
  5. package/dist/actionlog/backend/idbBackend.d.ts +13 -0
  6. package/dist/actionlog/backend/idbBackend.js +109 -0
  7. package/dist/actionlog/backend/jsonlBackend.d.ts +19 -0
  8. package/dist/actionlog/backend/jsonlBackend.js +110 -0
  9. package/dist/actionlog/backend/memoryBackend.d.ts +11 -0
  10. package/dist/actionlog/backend/memoryBackend.js +31 -0
  11. package/dist/actionlog/backend/sqliteBackend.d.ts +14 -0
  12. package/dist/actionlog/backend/sqliteBackend.js +96 -0
  13. package/dist/actionlog/backend.d.ts +12 -0
  14. package/dist/actionlog/backend.js +1 -0
  15. package/dist/actionlog/idbBackend.d.ts +19 -0
  16. package/dist/actionlog/idbBackend.js +98 -0
  17. package/dist/actionlog/jsonlBackend.d.ts +19 -0
  18. package/dist/actionlog/jsonlBackend.js +75 -0
  19. package/dist/actionlog/memoryBackend.d.ts +14 -0
  20. package/dist/actionlog/memoryBackend.js +38 -0
  21. package/dist/actionlog/openMaps.d.ts +11 -0
  22. package/dist/actionlog/openMaps.js +32 -0
  23. package/dist/actionlog/sqliteBackend.d.ts +18 -0
  24. package/dist/actionlog/sqliteBackend.js +105 -0
  25. package/dist/actionlog/toolContext.d.ts +36 -0
  26. package/dist/actionlog/toolContext.js +1 -0
  27. package/dist/actionlog/types.d.ts +62 -0
  28. package/dist/actionlog/types.js +62 -0
  29. package/dist/api.d.ts +4 -0
  30. package/dist/api.js +3 -0
  31. package/dist/io/sleep.d.ts +1 -1
  32. package/dist/io/sleep.js +16 -1
  33. package/dist/scanning-syncing/backgroundWorker.d.ts +2 -0
  34. package/dist/scanning-syncing/backgroundWorker.js +92 -10
  35. package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +33 -2
  36. package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +2 -2
  37. package/dist/scanning-syncing/connectionStatus.d.ts +8 -0
  38. package/dist/scanning-syncing/connectionStatus.js +50 -0
  39. package/dist/scanning-syncing/connectionStatusOpened.d.ts +3 -1
  40. package/dist/scanning-syncing/connectionStatusOpened.js +22 -14
  41. package/dist/scanning-syncing/openWallet.d.ts +4 -1
  42. package/dist/scanning-syncing/openWallet.js +3 -0
  43. package/dist/scanning-syncing/scanresult/scanCache.d.ts +5 -1
  44. package/dist/scanning-syncing/scanresult/scanCache.js +11 -2
  45. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +56 -5
  46. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +422 -83
  47. package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +7 -3
  48. package/dist/scanning-syncing/scanresult/scanCoordination.js +126 -28
  49. package/dist/scanning-syncing/scanresult/scanLoop.d.ts +1 -2
  50. package/dist/scanning-syncing/scanresult/scanLoop.js +0 -13
  51. package/dist/scanning-syncing/scanresult/scanResult.js +17 -12
  52. package/dist/scanning-syncing/scanresult/scanStats.js +8 -5
  53. package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
  54. package/dist/scanning-syncing/worker-mains/coordinator-main.d.ts +1 -0
  55. package/dist/scanning-syncing/worker-mains/coordinator-main.js +16 -1
  56. package/dist/scanning-syncing/worker-mains/cpubound-main.js +3 -2
  57. package/dist/scanning-syncing/worker-mains/worker.js +45 -2
  58. package/dist/tools/calls/001.d.ts +20 -0
  59. package/dist/tools/calls/001.js +236 -0
  60. package/dist/tools/calls/002-content.d.ts +46 -0
  61. package/dist/tools/calls/002-content.js +135 -0
  62. package/dist/tools/calls/002-fail-cleanup.d.ts +27 -0
  63. package/dist/tools/calls/002-fail-cleanup.js +60 -0
  64. package/dist/tools/calls/002.d.ts +64 -0
  65. package/dist/tools/calls/002.js +353 -0
  66. package/dist/tools/content.d.ts +22 -0
  67. package/dist/tools/content.js +41 -0
  68. package/dist/tools/counterparty.d.ts +3 -0
  69. package/dist/tools/counterparty.js +1 -0
  70. package/dist/tools/download-public-suffix.d.ts +1 -0
  71. package/dist/tools/download-public-suffix.js +20 -0
  72. package/dist/tools/globals.d.ts +196 -0
  73. package/dist/tools/globals.js +14 -0
  74. package/dist/tools/monero-tools-content.d.ts +29 -0
  75. package/dist/tools/monero-tools-content.js +80 -0
  76. package/dist/tools/monero-tools.d.ts +27 -52
  77. package/dist/tools/monero-tools.js +127 -222
  78. package/dist/tools/public-suffix-list.d.ts +1 -0
  79. package/dist/tools/public-suffix-list.js +10336 -0
  80. package/dist/tools/runtimeMessages.d.ts +4 -0
  81. package/dist/tools/runtimeMessages.js +18 -0
  82. package/dist/tools/toolActionLogContext.d.ts +33 -0
  83. package/dist/tools/toolActionLogContext.js +1 -0
  84. package/dist/tools/toolArm.d.ts +47 -0
  85. package/dist/tools/toolArm.js +1 -0
  86. package/dist/tools/ui.d.ts +2 -0
  87. package/dist/tools/ui.js +1 -0
  88. package/dist/tools/worker.d.ts +1 -0
  89. package/dist/tools/worker.js +1 -0
  90. package/dist/wasm-processing/wasmFile.js +2 -2
  91. package/package.json +8 -1
@@ -29,6 +29,7 @@ export type WorkToBeDone = {
29
29
  * @param scan_settings_path
30
30
  */
31
31
  export declare function findWorkToBeDone(scan_settings_path?: string, pathPrefix?: string): Promise<WorkToBeDone | false>;
32
+ export declare function fastForwardFetch(wallet_configs: WalletConfigPlusCache[], start_height: number, batchEnd: number): CacheRange | null;
32
33
  export declare function workToBeDoneForBatch(cache: ScanCache, batch_meta_infos: BlockInfo[]): "skip" | {
33
34
  from: number;
34
35
  };
@@ -64,7 +65,6 @@ export type CoordinatorEvent = {
64
65
  } | {
65
66
  type: "scan_ready";
66
67
  address: string;
67
- result: ScanLoopYield;
68
68
  newCache: ScanCache;
69
69
  changed_outputs: {
70
70
  output: any;
@@ -72,11 +72,13 @@ export type CoordinatorEvent = {
72
72
  }[];
73
73
  } | {
74
74
  type: "all_idle";
75
+ } | {
76
+ type: "shutdown_done";
75
77
  } | {
76
78
  type: "error";
77
79
  error: Error;
78
80
  };
79
- export declare function setupCoordinator(scanSettingsPath?: string, pathPrefix?: string): Promise<false | {
81
+ export declare function setupCoordinator(scanSettingsPath?: string, pathPrefix?: string, stopSync?: AbortSignal): Promise<false | {
80
82
  blocksGenerator: AsyncGenerator<BlocksBufferLoopResult, any, any>;
81
83
  workBuffer: WorkItem[];
82
84
  blocksBuffer: GetBlocksBinBufferItem[];
@@ -96,10 +98,12 @@ export type BlocksBufferRacer = {
96
98
  };
97
99
  export type ScanLoopRacer = ScanLoopYield;
98
100
  export type Racers = BlocksBufferRacer | ScanLoopRacer;
101
+ export declare function resetInProgressWorkItems(workBuffer: WorkItem[]): void;
102
+ export declare function cancelBusyCpuPorts(ports: PortStatus[], timeoutMs?: number): Promise<void>;
99
103
  /**
100
104
  * coordinator main (multithreaded): dispatches scan work to CPU workers
101
105
  * via MessagePorts, processes results in order per wallet.
102
106
  * falls back to single-threaded coordinator if no cpuPorts provided.
103
107
  */
104
- export declare function coordinatorMainMultithreaded(scanSettingsPath?: string, pathPrefix?: string, cpuPorts?: MessagePort[]): AsyncGenerator<CoordinatorEvent>;
108
+ export declare function coordinatorMainMultithreaded(scanSettingsPath?: string, pathPrefix?: string, cpuPorts?: MessagePort[], stopSync?: AbortSignal): AsyncGenerator<CoordinatorEvent>;
105
109
  export declare function scheduleWorkOnCpuPorts(ports: PortStatus[], work_buffer: WorkItem[]): Promise<void>;
@@ -1,8 +1,8 @@
1
- import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, readWriteConnectionStatusFile, } from "../../api";
2
- import { markWorkItemAsDone, } from "./scanLoop";
1
+ import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, readWriteConnectionStatusFile, applyWalletScanProgress, } from "../../api";
2
+ import {} from "./scanLoop";
3
3
  import { makeWorkItem } from "./scanLoop";
4
4
  import { cullTooLargeScanHeight, getNonHaltedWallets, getPathPrefix, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, } from "../../api";
5
- import { findRange, initScanCacheFile, lastRange, makeCacheRangeForHeight, mergeRanges, } from "./scanCache";
5
+ import { currentScanHeightFromRanges, findRange, initScanCacheFile, lastRange, makeCacheRangeForHeight, mergeRanges, } from "./scanCache";
6
6
  import { sendToCpuWorker } from "../worker-mains/cpubound-main";
7
7
  import { log } from "../../io/logging";
8
8
  /**
@@ -79,6 +79,19 @@ export async function findWorkToBeDone(scan_settings_path = SCAN_SETTINGS_STORE_
79
79
  scan_settings,
80
80
  };
81
81
  }
82
+ // slowest wallet past this batch: return its range so fetch can restart at that tip.
83
+ export function fastForwardFetch(wallet_configs, start_height, batchEnd) {
84
+ if (!wallet_configs.length)
85
+ return null;
86
+ const slowest = Math.min(...wallet_configs.map((w) => currentScanHeightFromRanges(w.cache.scanned_ranges, start_height)));
87
+ if (slowest <= batchEnd)
88
+ return null;
89
+ const owner = wallet_configs.find((w) => currentScanHeightFromRanges(w.cache.scanned_ranges, start_height) ===
90
+ slowest);
91
+ return owner
92
+ ? findRange(owner.cache.scanned_ranges, start_height)
93
+ : null;
94
+ }
82
95
  export function workToBeDoneForBatch(cache, batch_meta_infos) {
83
96
  const begin_height = batch_meta_infos[0].block_height;
84
97
  const end_height = batch_meta_infos[batch_meta_infos.length - 1].block_height;
@@ -220,6 +233,9 @@ export async function processWorkItem(item, workBuffer, blocksBuffer, pathPrefix
220
233
  throw error;
221
234
  }
222
235
  await writeCacheToFile(cache, pathPrefix);
236
+ // raw cpu ScanResult is fully merged into cache now. drop it so workBuffer
237
+ // does not pin all_key_images / hash strings until left-edge free.
238
+ item.result = undefined;
223
239
  item.status = "process_result_done";
224
240
  log("processWorkItem", `process_result_done work_uuid=${item?.work_uuid}`);
225
241
  // remove from blocksbuffer if no more work items reference it
@@ -271,15 +287,22 @@ function computeETA(cache, totalBlocksScanned, scanStartTime) {
271
287
  const etaMs = remaining / blocksPerMs;
272
288
  return msToHHMM(etaMs);
273
289
  }
274
- export async function setupCoordinator(scanSettingsPath, pathPrefix) {
290
+ export async function setupCoordinator(scanSettingsPath, pathPrefix, stopSync) {
275
291
  const work_to_be_done = await findWorkToBeDone(scanSettingsPath, pathPrefix);
276
292
  if (!work_to_be_done)
277
293
  return false;
294
+ // bring connection status file scan height in line with / cache file / in memory state
295
+ await readWriteConnectionStatusFile((cs) => {
296
+ applyWalletScanProgress(cs, {
297
+ current_scan_height: currentScanHeightFromRanges(work_to_be_done.wallet_configs[0]?.cache.scanned_ranges ?? [], work_to_be_done.scan_settings.start_height || 0),
298
+ });
299
+ }, scanSettingsPath);
278
300
  const { generator: blocksGenerator, blocksBuffer } = await setupBlocksBufferGenerator({
279
301
  nodeUrl: work_to_be_done.scan_settings.node_url,
280
302
  startHeight: work_to_be_done.start_height,
281
303
  anchor_range: work_to_be_done.anchor_range,
282
304
  scanSettingsPath,
305
+ stopSync,
283
306
  });
284
307
  const workBuffer = [];
285
308
  //TODO: refactor to per wallet buffer
@@ -298,22 +321,53 @@ export async function setupCoordinator(scanSettingsPath, pathPrefix) {
298
321
  work_to_be_done,
299
322
  };
300
323
  }
324
+ // flip inflight work back to fresh so a restart can reschedule it
325
+ export function resetInProgressWorkItems(workBuffer) {
326
+ for (const item of workBuffer) {
327
+ if (item.status === "scanwork_in_progress") {
328
+ item.status = "fresh";
329
+ item.result = undefined;
330
+ }
331
+ }
332
+ }
333
+ // tell busy cpu ports to cancel and wait for their promises (with timeout)
334
+ export async function cancelBusyCpuPorts(ports, timeoutMs = 3000) {
335
+ const busy = ports.filter((ps) => ps.promise);
336
+ for (const ps of busy) {
337
+ try {
338
+ sendToCpuWorker(ps.port, "cancel");
339
+ }
340
+ catch (err) {
341
+ log("cancelBusyCpuPorts", ["send cancel failed", String(err)]);
342
+ }
343
+ }
344
+ if (busy.length === 0)
345
+ return;
346
+ await Promise.race([
347
+ Promise.allSettled(busy.map((ps) => ps.promise)),
348
+ sleep(timeoutMs),
349
+ ]);
350
+ for (const ps of ports) {
351
+ ps.promise = null;
352
+ }
353
+ }
301
354
  /**
302
355
  * coordinator main (multithreaded): dispatches scan work to CPU workers
303
356
  * via MessagePorts, processes results in order per wallet.
304
357
  * falls back to single-threaded coordinator if no cpuPorts provided.
305
358
  */
306
- export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix, cpuPorts) {
359
+ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix, cpuPorts, stopSync) {
307
360
  if (!cpuPorts || cpuPorts.length === 0) {
308
361
  throw new Error("[coordinatorMain Multithreaded] cpuPorts empty, there must be at least cpu worker");
309
362
  }
310
- const ctx = await setupCoordinator(scanSettingsPath, pathPrefix);
363
+ const ctx = await setupCoordinator(scanSettingsPath, pathPrefix, stopSync);
311
364
  if (!ctx)
312
365
  throw new Error("[coordinatorMain Multithreaded] findWorkToBeDone returned false");
313
366
  const work_to_be_done = ctx.work_to_be_done;
314
- const blocksBuffer = ctx.blocksBuffer;
367
+ let blocksBuffer = ctx.blocksBuffer;
315
368
  const workBuffer = ctx.workBuffer;
316
- const blocksGenerator = ctx.blocksGenerator;
369
+ let blocksGenerator = ctx.blocksGenerator;
370
+ let fetchAt = work_to_be_done.start_height;
317
371
  let totalBlocksScanned = 0;
318
372
  let scanStartTime = Date.now();
319
373
  let blocksPromise = blocksGenerator.next();
@@ -327,6 +381,13 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
327
381
  }
328
382
  let race_count = 0;
329
383
  while (true) {
384
+ if (stopSync?.aborted) {
385
+ log("coordinatorMainMultithreaded", ["shutdown signal, draining"]);
386
+ await cancelBusyCpuPorts(freePorts);
387
+ resetInProgressWorkItems(workBuffer);
388
+ yield { type: "shutdown_done" };
389
+ return;
390
+ }
330
391
  race_count++;
331
392
  log("coordinatorMainMultithreaded", ["race_count", race_count]);
332
393
  await scheduleWorkOnCpuPorts(freePorts, workBuffer);
@@ -344,6 +405,16 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
344
405
  const winner = await Promise.race(races);
345
406
  log("coordinatorMainMultithreaded", "cpu ports status");
346
407
  if ("src" in winner && winner.src === "blocks") {
408
+ // fetch loop returned (done) on abort, treat as shutdown
409
+ if (winner.value.done) {
410
+ log("coordinatorMainMultithreaded", [
411
+ "blocks generator done, draining",
412
+ ]);
413
+ await cancelBusyCpuPorts(freePorts);
414
+ resetInProgressWorkItems(workBuffer);
415
+ yield { type: "shutdown_done" };
416
+ return;
417
+ }
347
418
  const result = winner.value.value;
348
419
  const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
349
420
  if (isBlocksBufferChanged) {
@@ -376,30 +447,38 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
376
447
  continue;
377
448
  const blockCount = to_be_processed.to - to_be_processed.from + 1;
378
449
  totalBlocksScanned += blockCount;
379
- const eta = computeETA(wallet.cache, totalBlocksScanned, scanStartTime);
380
- // write connection status sync (includes eta)
381
- if (eta) {
382
- await readWriteConnectionStatusFile((cs) => {
383
- cs.sync = {
384
- ...cs.sync,
385
- scanned_ranges: wallet.cache.scanned_ranges,
386
- daemon_height: wallet.cache.daemon_height,
387
- current_scan_height: lastRange(wallet.cache.scanned_ranges)?.end || 0,
388
- eta,
389
- timestamp: new Date().toISOString(),
390
- };
391
- }, scanSettingsPath);
392
- }
393
450
  const res = await processWorkItem(to_be_processed, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wallet.secret_spend_key);
451
+ const jumped = fastForwardFetch(work_to_be_done.wallet_configs, work_to_be_done.scan_settings.start_height || 0, to_be_processed.batch.get_blocks_result_meta.block_infos[to_be_processed.to].block_height);
452
+ if (jumped && jumped.end > fetchAt) {
453
+ log("coordinatorMainMultithreaded", ["jump", fetchAt, "->", jumped.end]);
454
+ await blocksGenerator.return(undefined);
455
+ const next = await setupBlocksBufferGenerator({
456
+ nodeUrl: work_to_be_done.scan_settings.node_url,
457
+ startHeight: jumped.end,
458
+ anchor_range: jumped,
459
+ scanSettingsPath,
460
+ stopSync,
461
+ });
462
+ blocksBuffer = next.blocksBuffer;
463
+ blocksGenerator = next.generator;
464
+ blocksPromise = blocksGenerator.next();
465
+ fetchAt = jumped.end;
466
+ }
467
+ // always persist wallet progress after process; eta only if we have a new one
468
+ // so missing eta does not wipe the previous value (no flicker)
469
+ const eta = computeETA(wallet.cache, totalBlocksScanned, scanStartTime);
470
+ await readWriteConnectionStatusFile((cs) => {
471
+ applyWalletScanProgress(cs, {
472
+ current_scan_height: currentScanHeightFromRanges(wallet.cache.scanned_ranges, work_to_be_done.scan_settings.start_height || 0),
473
+ scanned_ranges: wallet.cache.scanned_ranges,
474
+ daemon_height: wallet.cache.daemon_height,
475
+ eta,
476
+ });
477
+ }, scanSettingsPath);
394
478
  // log("coordinatorMainMultithreaded", "processWorkItem result");
395
479
  yield {
396
480
  type: "scan_ready",
397
481
  address: wallet.primary_address,
398
- result: {
399
- type: "Ready",
400
- work_uuid: to_be_processed.work_uuid,
401
- result: to_be_processed.result,
402
- },
403
482
  newCache: wallet.cache,
404
483
  changed_outputs: res.changed_outputs,
405
484
  };
@@ -424,6 +503,17 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
424
503
  const msg = event.data;
425
504
  // handle the result here:
426
505
  log("scheduleWorkOnCpuPorts", ["onmessage result", msg]);
506
+ // cancel ack may omit work_uuid when idle, but inflight cancel carries it
507
+ if (msg.type === "Canceled") {
508
+ if (item.status === "scanwork_in_progress") {
509
+ item.status = "fresh";
510
+ item.result = undefined;
511
+ }
512
+ port_status.promise = null;
513
+ // thin resolve: dont put full msg/result on the port promise on cancel
514
+ resolve_port({ type: "Canceled", work_uuid: item.work_uuid });
515
+ return;
516
+ }
427
517
  if (msg.work_uuid !== item.work_uuid) {
428
518
  log("scheduleWorkOnCpuPorts", [
429
519
  "wrong work_uuid in msg msg.work_uuid=",
@@ -441,10 +531,18 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
441
531
  log("scheduleWorkOnCpuPorts", "wrong status in msg");
442
532
  throw new Error("[scheduleWorkOnCpuPorts] wrong status in msg");
443
533
  }
534
+ // fat ScanResult lives only on the work item until process clears it
444
535
  item.result = msg.result;
445
536
  item.status = "scanwork_done";
537
+ // cpu already has its own buffer copy; process only needs meta.
538
+ // drop work-item bytes now; blocksBuffer still owns the batch until left-edge free.
539
+ item.batch = {
540
+ ...item.batch,
541
+ data: new Uint8Array(0),
542
+ };
446
543
  port_status.promise = null;
447
- resolve_port(msg);
544
+ // thin resolve: race/await must not retain all_key_images via promise value
545
+ resolve_port({ type: msg.type, work_uuid: msg.work_uuid });
448
546
  };
449
547
  port_status.port.onmessage = onmessage;
450
548
  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
- }
@@ -39,7 +39,7 @@ export async function processScanResult(params) {
39
39
  reverted_spends: [],
40
40
  };
41
41
  }
42
- else {
42
+ else if (!cache.reorg_info.split_heights.some((s) => s.block_hash === split_height.block_hash)) {
43
43
  cache.reorg_info.split_heights.push(split_height);
44
44
  }
45
45
  // First, collect reverted spends from all outputs (before any removals)
@@ -56,11 +56,14 @@ export async function processScanResult(params) {
56
56
  for (const [id, old_output_state] of removed_outputs) {
57
57
  // 1. find key_image of output to be removed (as it was reorged)
58
58
  const [key_image] = Object.entries(cache.own_key_images).find(([own_key_image, globalid]) => globalid === id) || [""]; // if this is viewonly the key_image will be empty
59
- cache.reorg_info.removed_outputs.push({
60
- old_output_state,
61
- key_image,
62
- split_height,
63
- });
59
+ if (!cache.reorg_info.removed_outputs.some((r) => r.old_output_state.index_on_blockchain ===
60
+ old_output_state.index_on_blockchain)) {
61
+ cache.reorg_info.removed_outputs.push({
62
+ old_output_state,
63
+ key_image,
64
+ split_height,
65
+ });
66
+ }
64
67
  // 2. remove from outputs and own_key_images
65
68
  delete cache.outputs[id];
66
69
  delete cache.own_key_images[key_image];
@@ -72,11 +75,14 @@ export async function processScanResult(params) {
72
75
  for (const [id, old_output_state_pointer] of reverted_outputs) {
73
76
  const [key_image] = Object.entries(cache.own_key_images).find(([own_key_image, globalid]) => globalid === id) || [""]; // if this is viewonly the key_image will be empty
74
77
  const old_output_state = Object.assign({}, old_output_state_pointer);
75
- cache.reorg_info.reverted_spends.push({
76
- old_output_state,
77
- key_image, // in this case key_image only used here, does not get removed
78
- split_height,
79
- });
78
+ if (!cache.reorg_info.reverted_spends.some((r) => r.old_output_state.index_on_blockchain ===
79
+ old_output_state.index_on_blockchain)) {
80
+ cache.reorg_info.reverted_spends.push({
81
+ old_output_state,
82
+ key_image, // in this case key_image only used here, does not get removed
83
+ split_height,
84
+ });
85
+ }
80
86
  // remove spend info from original cache (if output still exists
81
87
  // it may have been deleted above in the removed_outputs loop)
82
88
  if (cache.outputs[id]) {
@@ -226,7 +232,6 @@ export function outputStatus(output, cache, current_height) {
226
232
  if (unlock_height > current_height) {
227
233
  return { status: "pending", unlock_height };
228
234
  }
229
- // TODO: check that that output is not txlog input indeces
230
235
  return { status: "spendable" };
231
236
  }
232
237
  export function spendable(output, cache, current_height) {
@@ -120,16 +120,19 @@ export function processFoundTransactions(cache, stats, current_height) {
120
120
  }
121
121
  // handle spent case
122
122
  if (output.spent_in_tx_hash) {
123
- const spent_utxo_value = cache.pending_spent_utxos
124
- ? cache.pending_spent_utxos[output.index_on_blockchain]
125
- : null;
126
- const txlog = cache.tx_logs && spent_utxo_value
127
- ? cache.tx_logs[spent_utxo_value]
123
+ const spentIndex = cache.pending_spent_utxos
124
+ ? cache.pending_spent_utxos[String(output.index_on_blockchain)]
125
+ : undefined;
126
+ // index 0 is a real log. do not treat it as missing.
127
+ const txlog = cache.tx_logs && spentIndex !== undefined
128
+ ? cache.tx_logs[spentIndex]
128
129
  : undefined;
129
130
  const spentTx = stats.found_transactions[output.spent_in_tx_hash];
130
131
  if (spentTx) {
131
132
  spentTx.amount -= output.amount;
132
133
  spentTx.inputs.push(output);
134
+ if (!spentTx.txlog && txlog)
135
+ spentTx.txlog = txlog;
133
136
  }
134
137
  else {
135
138
  stats.found_transactions[output.spent_in_tx_hash] = {