@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
@@ -5,6 +5,8 @@ import { spendable } from "./scanResult";
5
5
  import { getPathPrefix, readPrivateSpendKeyFromEnv, SCAN_SETTINGS_STORE_NAME_DEFAULT, SUB_ADDRESS_INDEX_DEFAULT_VALUE, walletSettingsPlusKeys, } from "../../api";
6
6
  import { ScanSettingsOpened } from "../../scansettings/scanSettingsOpened";
7
7
  import { ConnectionStatusOpened } from "../connectionStatusOpened";
8
+ import { ActionLogOpened, } from "../../actionlog/actionLogOpened";
9
+ import { sendToBackground } from "../../tools/globals";
8
10
  import { findRange, lastRange, readCacheFileDefaultLocation, writeCacheFileDefaultLocationThrows, } from "./scanCache";
9
11
  import { alignScanStatsWithCache, isSelfSpent, processTxlogInputs, processTxlogPayments, writeStatsFileDefaultLocation, } from "./scanStats";
10
12
  import { readWriteConnectionStatusFile, } from "../connectionStatus";
@@ -37,11 +39,13 @@ export class ScanCacheOpened {
37
39
  /** sample sizes to try when decoyRetry is enabled, in order */
38
40
  decoyRetrySizes = [20, 50, 100, 200, 500];
39
41
  static async create(params) {
40
- const theCatchToBeOpened = await readCacheFileDefaultLocation(params.primary_address, params.pathPrefix);
42
+ // same dir as workers / connection status: next to ScanSettings.json
43
+ const pathPrefix = getPathPrefix(params.scan_settings_path, params.pathPrefix);
44
+ const theCatchToBeOpened = await readCacheFileDefaultLocation(params.primary_address, pathPrefix);
41
45
  if (!params.primary_address)
42
46
  throw new Error(`primary_address is required, potentially half filled out wallet setting in: ${params.scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT}`);
43
47
  // use ScanSettingsOpened instead of direct scanSettings calls
44
- const scanSettings = await ScanSettingsOpened.create(params.scan_settings_path, params.pathPrefix);
48
+ const scanSettings = await ScanSettingsOpened.create(params.scan_settings_path, pathPrefix);
45
49
  const walletSettings = scanSettings.getWallet(params.primary_address);
46
50
  if (!walletSettings)
47
51
  throw new Error(`wallet not found in settings. did you call openwallet with the right params?
@@ -55,7 +59,7 @@ export class ScanCacheOpened {
55
59
  // read secret_view_key and secret_spend_key from env
56
60
  const walletSettingsWithKeys = await walletSettingsPlusKeys(walletWithSettings);
57
61
  // create viewpair + ScanCacheOpened instance
58
- const scanCacheOpen = new ScanCacheOpened(scanSettings, await ViewPair.create(params.primary_address, walletSettingsWithKeys.secret_view_key, walletSettings.subaddress_index, scanSettings.node_url), walletSettings.wallet_route, walletSettings.wallet_name, walletSettings.wallet_slot, params.no_worker || false, params.masterCacheChanged || null, scanSettings.start_height, params.scan_settings_path, params.pathPrefix, params.workerError);
62
+ const scanCacheOpen = new ScanCacheOpened(scanSettings, await ViewPair.create(params.primary_address, walletSettingsWithKeys.secret_view_key, walletSettings.subaddress_index, scanSettings.node_url), walletSettings.wallet_route, walletSettings.wallet_name, walletSettings.wallet_slot, params.no_worker || false, params.masterCacheChanged || null, scanSettings.start_height, params.scan_settings_path, pathPrefix, params.workerError);
59
63
  if (theCatchToBeOpened)
60
64
  scanCacheOpen._cache = theCatchToBeOpened;
61
65
  if (!walletSettings.halted) {
@@ -66,7 +70,7 @@ export class ScanCacheOpened {
66
70
  scanCacheOpen._highest_subaddress_index =
67
71
  walletSettings.subaddress_index || SUB_ADDRESS_INDEX_DEFAULT_VALUE;
68
72
  if (!params.no_stats) {
69
- scanCacheOpen._stats = await alignScanStatsWithCache(scanCacheOpen._cache, scanCacheOpen.view_pair, params.primary_address, getPathPrefix(params.scan_settings_path, params.pathPrefix), walletSettings.subaddress_index, lastRange(scanCacheOpen._cache.scanned_ranges)?.end);
73
+ scanCacheOpen._stats = await alignScanStatsWithCache(scanCacheOpen._cache, scanCacheOpen.view_pair, params.primary_address, pathPrefix, walletSettings.subaddress_index, lastRange(scanCacheOpen._cache.scanned_ranges)?.end);
70
74
  }
71
75
  else {
72
76
  scanCacheOpen._no_stats = params.no_stats; // true
@@ -83,8 +87,10 @@ export class ScanCacheOpened {
83
87
  return this._highest_subaddress_index;
84
88
  }
85
89
  get current_height() {
86
- let current_range = findRange(this._cache.scanned_ranges, this._start_height || 0);
87
- return current_range?.end || null;
90
+ // progress on the range that covers start_height (start can jump;
91
+ // lastRange would pick a different segment and be wrong)
92
+ const current_range = findRange(this._cache.scanned_ranges, this._start_height || 0);
93
+ return current_range == null ? null : current_range.end;
88
94
  }
89
95
  get current_top_range_height() {
90
96
  if (typeof this._stats === "undefined" || this._stats === null)
@@ -92,7 +98,7 @@ export class ScanCacheOpened {
92
98
  return this._stats.height;
93
99
  }
94
100
  async changeStartHeight(start_height) {
95
- this.stopWorker();
101
+ await this.stopWorker();
96
102
  await this._scanSettings.setStartHeight(start_height);
97
103
  this._start_height = start_height;
98
104
  await this.unpause();
@@ -167,7 +173,7 @@ export class ScanCacheOpened {
167
173
  return this._scanSettings.logs_exclude;
168
174
  }
169
175
  async changeNodeUrlAndStartHeight(node_url, start_height) {
170
- this.stopWorker();
176
+ await this.stopWorker();
171
177
  if (node_url !== undefined) {
172
178
  await this._scanSettings.setNodeUrl(node_url);
173
179
  this.node_url = node_url;
@@ -179,7 +185,7 @@ export class ScanCacheOpened {
179
185
  await this.unpause();
180
186
  }
181
187
  async changeNodeUrl(node_url) {
182
- this.stopWorker();
188
+ await this.stopWorker();
183
189
  await this._scanSettings.setNodeUrl(node_url);
184
190
  this.node_url = node_url;
185
191
  await this.unpause();
@@ -188,7 +194,7 @@ export class ScanCacheOpened {
188
194
  await this._scanSettings.setMerchantConfirmations(merchant_confirmations);
189
195
  }
190
196
  async setCpuWorkerCount(cpu_worker_count) {
191
- this.stopWorker();
197
+ await this.stopWorker();
192
198
  await this._scanSettings.setCpuWorkerCount(cpu_worker_count);
193
199
  await this.unpause();
194
200
  }
@@ -201,7 +207,7 @@ export class ScanCacheOpened {
201
207
  await this._scanSettings.setWalletName(this.view_pair.primary_address, name);
202
208
  }
203
209
  async retry() {
204
- this.stopWorker();
210
+ await this.stopWorker();
205
211
  // scansettings so external changes (e.g. from a sidebar frontend instance) are picked up
206
212
  await this._scanSettings.reload();
207
213
  const walletStillExists = this._scanSettings.walletExists(this.primary_address);
@@ -385,18 +391,20 @@ export class ScanCacheOpened {
385
391
  let maybeInputs = [];
386
392
  let maybeFeeEstimate;
387
393
  let maybeSendResult;
394
+ let maybeSignedTx;
388
395
  try {
389
396
  const { selectedInputs, feeEstimate } = await this.calculateFeeAndSelectInputs(params);
390
397
  maybeInputs = selectedInputs;
391
398
  maybeFeeEstimate = feeEstimate;
392
399
  const unsignedTx = await this.makeTransactionFromSelectedInputs(params.payments, selectedInputs, feeEstimate);
393
400
  const signedTx = await this.signTransaction(unsignedTx);
401
+ maybeSignedTx = signedTx;
394
402
  const sendResult = await this.sendTransaction(signedTx);
395
403
  maybeSendResult = sendResult;
396
404
  if (sendResult.status !== "OK")
397
405
  throw new Error("send raw transaction rpc returned error");
398
406
  // before writing the scan cache, we stop the worker to avoid a race
399
- this.stopWorker();
407
+ await this.stopWorker();
400
408
  // write txlog to cache + update pending_spent_utxos (affects stats + spendability)
401
409
  await writeCacheFileDefaultLocationThrows({
402
410
  primary_address: this.primary_address,
@@ -415,6 +423,8 @@ export class ScanCacheOpened {
415
423
  inputs_index,
416
424
  height: this.current_height,
417
425
  timestamp: Date.now(),
426
+ invocationId: params.invocationId,
427
+ signed_tx: signedTx,
418
428
  };
419
429
  const newLen = cache.tx_logs.push(txLog);
420
430
  const txLogIndex = newLen - 1;
@@ -440,7 +450,7 @@ export class ScanCacheOpened {
440
450
  }
441
451
  catch (e) {
442
452
  // before writing the scan cache, we stop the worker to avoid a race
443
- this.stopWorker();
453
+ await this.stopWorker();
444
454
  // write txlog error to cache
445
455
  await writeCacheFileDefaultLocationThrows({
446
456
  primary_address: this.primary_address,
@@ -460,17 +470,16 @@ export class ScanCacheOpened {
460
470
  inputs_index,
461
471
  height: this.current_height,
462
472
  timestamp: Date.now(),
473
+ invocationId: params.invocationId,
474
+ signed_tx: maybeSignedTx,
463
475
  };
464
- const newLen = cache.tx_logs.push(txLog);
476
+ cache.tx_logs.push(txLog);
465
477
  },
466
478
  });
467
479
  const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
468
480
  if (!newCache)
469
481
  throw new Error(`cache not found for primary address: ${this.primary_address}, and path prefix: ${this.pathPrefix}`);
470
- const changed_outputs = maybeInputs.map((input) => ({
471
- change_reason: "spent",
472
- output: input,
473
- }));
482
+ const changed_outputs = [];
474
483
  await this.feed({
475
484
  newCache,
476
485
  changed_outputs,
@@ -480,6 +489,179 @@ export class ScanCacheOpened {
480
489
  throw e;
481
490
  }
482
491
  }
492
+ outputsForIndexes(indexes) {
493
+ const byId = new Map(this.spendableInputs().map((output) => [
494
+ String(output.index_on_blockchain),
495
+ output,
496
+ ]));
497
+ const selected = [];
498
+ for (const id of indexes) {
499
+ const output = byId.get(id);
500
+ if (!output)
501
+ throw new Error(`input not spendable: ${id}`);
502
+ selected.push(output);
503
+ }
504
+ if (!selected.length)
505
+ throw new Error("no inputs selected");
506
+ return selected;
507
+ }
508
+ async makeSignSend(params) {
509
+ // an empty list means the wallet selects the inputs
510
+ if (!params.input_indexes.length) {
511
+ return this.makeSignSendTransaction({
512
+ payments: params.payments,
513
+ });
514
+ }
515
+ return this.makeSignSendTransaction({
516
+ payments: params.payments,
517
+ inputs: this.outputsForIndexes(params.input_indexes),
518
+ });
519
+ }
520
+ async sweepSignSend(params) {
521
+ const inputs = params.input_indexes.length
522
+ ? this.outputsForIndexes(params.input_indexes)
523
+ : this.spendableInputs();
524
+ if (!inputs.length)
525
+ throw new Error("no inputs selected");
526
+ const feeEstimate = await this.getFeeEstimate();
527
+ const unsignedTx = await this.makeSweepTransactionFromSelectedInputs(params.address, inputs, feeEstimate);
528
+ const signedTx = await this.signTransaction(unsignedTx);
529
+ const payments = [{ address: params.address, amount: "0" }];
530
+ const sendResult = await this.sendTransaction(signedTx);
531
+ if (sendResult.status !== "OK") {
532
+ await this.recordSend({
533
+ payments,
534
+ inputs,
535
+ signedTx,
536
+ feeEstimate,
537
+ sendResult,
538
+ error: "send raw transaction rpc returned error",
539
+ lockInputs: false,
540
+ });
541
+ throw new Error("send raw transaction rpc returned error");
542
+ }
543
+ await this.recordSend({
544
+ payments,
545
+ inputs,
546
+ signedTx,
547
+ feeEstimate,
548
+ sendResult,
549
+ lockInputs: true,
550
+ });
551
+ return sendResult;
552
+ }
553
+ async rebroadcastTx(txLogIndex) {
554
+ const txlog = this._cache.tx_logs?.[txLogIndex];
555
+ if (!txlog?.signed_tx)
556
+ throw new Error("no signed tx to rebroadcast");
557
+ const inputs = txlog.inputs_index
558
+ .map((id) => this._cache.outputs[id])
559
+ .filter((output) => output);
560
+ if (inputs.length &&
561
+ inputs.every((output) => typeof output.spent_in_tx_hash === "string"))
562
+ throw new Error("inputs already spent on chain");
563
+ const sendResult = await this.sendTransaction(txlog.signed_tx);
564
+ await this.stopWorker();
565
+ await writeCacheFileDefaultLocationThrows({
566
+ primary_address: this.primary_address,
567
+ pathPrefix: this.pathPrefix,
568
+ writeCallback: async (cache) => {
569
+ const row = cache.tx_logs?.[txLogIndex];
570
+ if (!row)
571
+ throw new Error("tx log missing");
572
+ row.sendResult = sendResult;
573
+ if (sendResult.status === "OK") {
574
+ row.error = undefined;
575
+ if (!cache.pending_spent_utxos)
576
+ cache.pending_spent_utxos = {};
577
+ for (const inputId of row.inputs_index)
578
+ cache.pending_spent_utxos[inputId] = txLogIndex;
579
+ }
580
+ else {
581
+ row.error = sendResult.reason || "rebroadcast failed";
582
+ }
583
+ },
584
+ });
585
+ const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
586
+ if (!newCache)
587
+ throw new Error("cache not found after rebroadcast");
588
+ await this.feed({ newCache, changed_outputs: [] });
589
+ await this.unpause();
590
+ return sendResult;
591
+ }
592
+ get failed_txs() {
593
+ const logs = this._cache.tx_logs || [];
594
+ const rows = [];
595
+ for (let index = 0; index < logs.length; index++) {
596
+ const txlog = logs[index];
597
+ if (!txlog || txlog.hidden_on_send_plate)
598
+ continue;
599
+ if (txlog.sendResult?.status === "OK")
600
+ continue;
601
+ const inputs = txlog.inputs_index
602
+ .map((id) => this._cache.outputs[id])
603
+ .filter((output) => output);
604
+ if (inputs.length &&
605
+ inputs.every((output) => typeof output.spent_in_tx_hash === "string"))
606
+ continue;
607
+ rows.push({ index, txlog, inputs });
608
+ }
609
+ return rows;
610
+ }
611
+ async recordSend(args) {
612
+ await this.stopWorker();
613
+ await writeCacheFileDefaultLocationThrows({
614
+ primary_address: this.primary_address,
615
+ pathPrefix: this.pathPrefix,
616
+ writeCallback: async (cache) => {
617
+ if (!cache.tx_logs)
618
+ cache.tx_logs = [];
619
+ if (!cache.pending_spent_utxos)
620
+ cache.pending_spent_utxos = {};
621
+ const inputs_index = args.inputs.map((input) => String(input.index_on_blockchain));
622
+ const txLog = {
623
+ sendResult: args.sendResult,
624
+ error: args.error,
625
+ feeEstimate: args.feeEstimate,
626
+ payments: args.payments,
627
+ node_url: this.node_url,
628
+ inputs_index,
629
+ height: this.current_height,
630
+ timestamp: Date.now(),
631
+ signed_tx: args.signedTx,
632
+ };
633
+ const txLogIndex = cache.tx_logs.push(txLog) - 1;
634
+ if (!args.lockInputs)
635
+ return;
636
+ for (const inputId of inputs_index)
637
+ cache.pending_spent_utxos[inputId] = txLogIndex;
638
+ },
639
+ });
640
+ const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
641
+ if (!newCache)
642
+ throw new Error("cache not found after send");
643
+ const changed_outputs = args.lockInputs
644
+ ? args.inputs.map((output) => ({ change_reason: "spent", output }))
645
+ : [];
646
+ await this.feed({ newCache, changed_outputs });
647
+ await this.unpause();
648
+ }
649
+ async dismissSendPlate() {
650
+ await this.stopWorker();
651
+ await writeCacheFileDefaultLocationThrows({
652
+ primary_address: this.primary_address,
653
+ pathPrefix: this.pathPrefix,
654
+ writeCallback: async (cache) => {
655
+ for (const row of cache.tx_logs || [])
656
+ row.hidden_on_send_plate = true;
657
+ },
658
+ });
659
+ const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
660
+ if (!newCache)
661
+ throw new Error("cache not found after dismiss");
662
+ await this.feed({ newCache, changed_outputs: [] });
663
+ await this.unpause();
664
+ }
483
665
  /**
484
666
  * makeStandardTransaction
485
667
  */
@@ -550,15 +732,35 @@ export class ScanCacheOpened {
550
732
  };
551
733
  }
552
734
  async pause() {
553
- this.stopWorker();
735
+ await this.stopWorker();
554
736
  return await this._scanSettings.haltWallet(this.view_pair.primary_address);
555
737
  }
556
- stopWorker() {
738
+ // debug: dump coordinator + cpu worker heaps while they are live
739
+ // in browser: use inspector directly, this is only useful for bun
740
+ async dumpWorkerHeaps(dir) {
741
+ if (!this.worker?.dumpHeaps)
742
+ return [];
743
+ return await this.worker.dumpHeaps(dir);
744
+ }
745
+ async stopWorker() {
557
746
  if (this.worker) {
558
- this.worker.terminate();
747
+ const worker = this.worker;
748
+ // clear ref first so concurrent stop/retry cannot double shutdown
559
749
  this.worker.cpuWorkers = [];
560
750
  this.worker.fetchWorker = undefined;
561
751
  delete this.worker;
752
+ // drain coordinator first, then hard kill workers
753
+ try {
754
+ await worker.shutdown();
755
+ }
756
+ catch {
757
+ // still terminate below
758
+ }
759
+ try {
760
+ worker.terminate();
761
+ }
762
+ catch {
763
+ }
562
764
  }
563
765
  }
564
766
  async unpause() {
@@ -688,48 +890,120 @@ export class ScanCacheOpened {
688
890
  _stats = null;
689
891
  notifyListeners = [];
690
892
  }
893
+ function defaultNotifyMasterChanged(params) {
894
+ void sendToBackground("walletCacheChanged", JSON.stringify(params, (_k, v) => typeof v === "bigint" ? v.toString() : v)).catch(() => { });
895
+ }
691
896
  export class ManyScanCachesOpened {
692
897
  connectionStatusOpened;
898
+ actionLogOpened;
693
899
  _scanSettings;
694
900
  _options;
695
901
  get start_height() {
696
902
  if (this.wallets.length === 0)
697
- return null;
903
+ return this._scanSettings.start_height;
698
904
  return this.wallets[0]?.start_height;
699
905
  }
906
+ // overall scan tip = lagging non-halted wallet (this.wallets is already non-halted)
700
907
  get current_height() {
701
- if (this.wallets.length === 0)
702
- return null;
703
- return this.wallets[0]?.current_height;
908
+ let min = null;
909
+ for (const w of this.wallets) {
910
+ const h = w.current_height;
911
+ if (h == null)
912
+ continue;
913
+ if (min == null || h < min)
914
+ min = h;
915
+ }
916
+ return min;
704
917
  }
705
918
  get node_url() {
706
919
  if (this.wallets.length === 0)
707
- return "";
920
+ return this._scanSettings.node_url ?? "";
708
921
  return this.wallets[0]?.node_url;
709
922
  }
710
923
  async changeNodeUrlAndStartHeight(node_url, start_height) {
711
- if (this.wallets.length === 0)
712
- throw new Error("no wallets");
713
- const masterWallet = this.wallets[0];
714
- return await masterWallet.changeNodeUrlAndStartHeight(node_url, start_height);
924
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
925
+ await sendToBackground("worker.changeNodeUrlAndStartHeight", {
926
+ node_url,
927
+ start_height,
928
+ }).catch(() => { });
929
+ return;
930
+ }
931
+ if (this.wallets.length === 0) {
932
+ if (node_url !== undefined)
933
+ await this._scanSettings.setNodeUrl(node_url);
934
+ if (start_height !== undefined)
935
+ await this._scanSettings.setStartHeight(start_height);
936
+ return;
937
+ }
938
+ await this.wallets[0].changeNodeUrlAndStartHeight(node_url, start_height);
715
939
  }
716
940
  async retry() {
717
941
  if (this.wallets.length === 0)
718
- throw new Error("no wallets");
942
+ return;
719
943
  const masterWallet = this.wallets[0];
720
944
  return await masterWallet.retry();
721
945
  }
722
- stopWorker() {
946
+ async stopWorker() {
947
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
948
+ await sendToBackground("worker.stopWorker", null).catch(() => { });
949
+ return;
950
+ }
723
951
  if (this.wallets.length === 0)
724
- throw new Error("no wallets");
952
+ return;
725
953
  const masterWallet = this.wallets[0];
726
- return masterWallet.stopWorker();
954
+ return await masterWallet.stopWorker();
727
955
  }
728
- async changeNodeUrl(node_url) {
956
+ // debug: dump live coordinator + cpu worker heaps via master wallet workers
957
+ // in browser: use inspector directly, this is only useful for bun
958
+ async dumpWorkerHeaps(dir) {
729
959
  if (this.wallets.length === 0)
730
- throw new Error("no wallets");
731
- const masterWallet = this.wallets[0];
732
- return await masterWallet.changeNodeUrl(node_url);
960
+ return [];
961
+ return await this.wallets[0].dumpWorkerHeaps(dir);
962
+ }
963
+ async changeNodeUrl(node_url) {
964
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
965
+ await sendToBackground("worker.setNodeUrl", { node_url }).catch(() => { });
966
+ return;
967
+ }
968
+ if (this.wallets.length === 0) {
969
+ await this._scanSettings.setNodeUrl(node_url);
970
+ return;
971
+ }
972
+ await this.wallets[0].changeNodeUrl(node_url);
973
+ }
974
+ walletByAddress(primary_address) {
975
+ const wallet = this.wallets.find((row) => row.primary_address === primary_address);
976
+ if (!wallet)
977
+ throw new Error("wallet not found");
978
+ return wallet;
979
+ }
980
+ async makeSignSend(params) {
981
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
982
+ await sendToBackground("worker.makeSignSend", params).catch(() => { });
983
+ return;
984
+ }
985
+ return this.walletByAddress(params.primary_address).makeSignSend(params);
986
+ }
987
+ async sweepSignSend(params) {
988
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
989
+ await sendToBackground("worker.sweepSignSend", params).catch(() => { });
990
+ return;
991
+ }
992
+ return this.walletByAddress(params.primary_address).sweepSignSend(params);
993
+ }
994
+ async rebroadcastTx(params) {
995
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
996
+ await sendToBackground("worker.rebroadcastTx", params).catch(() => { });
997
+ return;
998
+ }
999
+ return this.walletByAddress(params.primary_address).rebroadcastTx(params.tx_log_index);
1000
+ }
1001
+ async dismissSendPlate(params) {
1002
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
1003
+ await sendToBackground("worker.dismissSendPlate", params).catch(() => { });
1004
+ return;
1005
+ }
1006
+ return this.walletByAddress(params.primary_address).dismissSendPlate();
733
1007
  }
734
1008
  get merchant_confirmations() {
735
1009
  if (this.wallets.length === 0)
@@ -742,19 +1016,13 @@ export class ManyScanCachesOpened {
742
1016
  return this.wallets[0]?.cpu_worker_count;
743
1017
  }
744
1018
  get logs() {
745
- if (this.wallets.length === 0)
746
- return undefined;
747
- return this.wallets[0]?.logs;
1019
+ return this._scanSettings.logs;
748
1020
  }
749
1021
  get logs_include() {
750
- if (this.wallets.length === 0)
751
- return undefined;
752
- return this.wallets[0]?.logs_include;
1022
+ return this._scanSettings.logs_include;
753
1023
  }
754
1024
  get logs_exclude() {
755
- if (this.wallets.length === 0)
756
- return undefined;
757
- return this.wallets[0]?.logs_exclude;
1025
+ return this._scanSettings.logs_exclude;
758
1026
  }
759
1027
  get connectionStatus() {
760
1028
  return this.connectionStatusOpened.connectionStatus;
@@ -770,18 +1038,29 @@ export class ManyScanCachesOpened {
770
1038
  }
771
1039
  async setMerchantConfirmations(merchant_confirmations) {
772
1040
  if (this.wallets.length === 0)
773
- throw new Error("no wallets");
1041
+ return;
774
1042
  return await this.wallets[0].setMerchantConfirmations(merchant_confirmations);
775
1043
  }
776
1044
  async setCpuWorkerCount(cpu_worker_count) {
777
1045
  if (this.wallets.length === 0)
778
- throw new Error("no wallets");
1046
+ return;
779
1047
  return await this.wallets[0].setCpuWorkerCount(cpu_worker_count);
780
1048
  }
781
1049
  async setLogSettings(logs, logs_include, logs_exclude) {
782
- if (this.wallets.length === 0)
783
- throw new Error("no wallets");
784
- return await this.wallets[0].setLogSettings(logs, logs_include, logs_exclude);
1050
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
1051
+ await sendToBackground("worker.setLogSettings", {
1052
+ logs,
1053
+ logs_include,
1054
+ logs_exclude,
1055
+ }).catch(() => { });
1056
+ }
1057
+ await this._scanSettings.setLogSettings(logs, logs_include, logs_exclude);
1058
+ if (this.actionLogOpened.extensionMessageBus === "ui")
1059
+ return;
1060
+ for (const wallet of this.wallets)
1061
+ await wallet.stopWorker();
1062
+ for (const wallet of this.wallets)
1063
+ await wallet.unpause();
785
1064
  }
786
1065
  async setWalletName(primary_address, name) {
787
1066
  await this._scanSettings.setWalletName(primary_address, name);
@@ -790,10 +1069,17 @@ export class ManyScanCachesOpened {
790
1069
  await this._scanSettings.setWalletSlot(primary_address, slot);
791
1070
  }
792
1071
  async changeStartHeight(start_height) {
793
- if (this.wallets.length === 0)
794
- throw new Error("no wallets");
795
- const masterWallet = this.wallets[0];
796
- return await masterWallet.changeStartHeight(start_height);
1072
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
1073
+ await sendToBackground("worker.setStartHeight", {
1074
+ start_height,
1075
+ }).catch(() => { });
1076
+ return;
1077
+ }
1078
+ if (this.wallets.length === 0) {
1079
+ await this._scanSettings.setStartHeight(start_height);
1080
+ return;
1081
+ }
1082
+ await this.wallets[0].changeStartHeight(start_height);
797
1083
  }
798
1084
  static async _buildWallets(scanSettingsOpened, options) {
799
1085
  const { scan_settings_path, pathPrefix, no_worker, no_stats, workerError, notifyMasterChanged, } = options;
@@ -851,28 +1137,49 @@ export class ManyScanCachesOpened {
851
1137
  static async create(options) {
852
1138
  const { scan_settings_path, pathPrefix, onConnectionStatusChange, connectionStatusIntervalMs, autoRetry, retryDelayMs, } = options;
853
1139
  const scanSettingsOpened = await ScanSettingsOpened.create(scan_settings_path, pathPrefix);
854
- if (!scanSettingsOpened.wallets || scanSettingsOpened.wallets.length === 0)
855
- throw new Error(`no wallets in settings file. Did you supply the right path?
856
- are there wallets in the default '${SCAN_SETTINGS_STORE_NAME_DEFAULT}' file?`);
1140
+ // persist log options into ScanSettings.json before workers start
1141
+ if (options.logs !== undefined ||
1142
+ options.logs_include !== undefined ||
1143
+ options.logs_exclude !== undefined) {
1144
+ await scanSettingsOpened.setLogSettings(options.logs !== undefined ? options.logs : scanSettingsOpened.logs, options.logs_include !== undefined
1145
+ ? options.logs_include
1146
+ : scanSettingsOpened.logs_include, options.logs_exclude !== undefined
1147
+ ? options.logs_exclude
1148
+ : scanSettingsOpened.logs_exclude);
1149
+ }
857
1150
  // wrap workerError with auto-retry
858
1151
  let retryTimer;
859
1152
  let instance = null;
1153
+ const clearRetryTimer = () => {
1154
+ clearTimeout(retryTimer);
1155
+ retryTimer = undefined;
1156
+ };
860
1157
  const retryFn = async () => {
861
- await instance?.buildWallets();
862
- const node_url = instance?.node_url;
1158
+ clearRetryTimer();
1159
+ // no wallets means idle, do not rebuild or restart workers
1160
+ if (!instance || instance.wallets.length === 0)
1161
+ return;
1162
+ const node_url = instance.node_url;
863
1163
  if (!node_url)
864
- throw new Error("No nodeurl set, can't retry connection");
1164
+ return;
1165
+ // probe first; only restart workers when node is reachable
865
1166
  const getinfo_result = await get_info(node_url)
866
- .then((r) => {
867
- r?.status === "OK";
868
- })
1167
+ .then((r) => r?.status === "OK")
869
1168
  .catch(() => false);
870
- if (getinfo_result)
871
- await instance?.retry();
872
- clearTimeout(retryTimer);
873
- retryTimer = undefined;
1169
+ if (!getinfo_result) {
1170
+ // still down: try again later without buildWallets thrash
1171
+ if (!retryTimer && instance.wallets.length > 0) {
1172
+ retryTimer = setTimeout(retryFn, retryDelayMs ?? 5000);
1173
+ }
1174
+ return;
1175
+ }
1176
+ await instance.buildWallets();
874
1177
  };
875
1178
  const newOptions = { ...options };
1179
+ // browser worker (background.ts): broadcast cache to sidebar
1180
+ if (!newOptions.notifyMasterChanged && !newOptions.no_worker) {
1181
+ newOptions.notifyMasterChanged = defaultNotifyMasterChanged;
1182
+ }
876
1183
  let connectionFailedShown = false;
877
1184
  if (autoRetry) {
878
1185
  const originalError = options.workerError;
@@ -881,7 +1188,7 @@ export class ManyScanCachesOpened {
881
1188
  const msg = err instanceof Error ? err.message : String(err);
882
1189
  if (csOpened.connectionStatus?.last_packet?.status ===
883
1190
  "catastrophic_reorg") {
884
- clearTimeout(retryTimer);
1191
+ clearRetryTimer();
885
1192
  instance?.stopWorker();
886
1193
  throw new Error("catastrophic reorg, aborting ...");
887
1194
  }
@@ -893,14 +1200,13 @@ export class ManyScanCachesOpened {
893
1200
  console.error("unable to connect to node ... retrying ...");
894
1201
  }
895
1202
  }
896
- if (!retryTimer) {
1203
+ // only schedule retry when we still have wallets to scan
1204
+ if (!retryTimer && instance && instance.wallets.length > 0) {
897
1205
  retryTimer = setTimeout(retryFn, retryDelayMs ?? 5000);
898
1206
  }
899
1207
  };
900
1208
  }
901
- const wallets = await this._buildWallets(scanSettingsOpened, newOptions);
902
- if (!wallets)
903
- return undefined;
1209
+ const wallets = (await this._buildWallets(scanSettingsOpened, newOptions)) ?? [];
904
1210
  const csOpened = new ConnectionStatusOpened(scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT, autoRetry
905
1211
  ? (status) => {
906
1212
  if (status?.last_packet?.status === "OK" && connectionFailedShown) {
@@ -912,40 +1218,73 @@ export class ManyScanCachesOpened {
912
1218
  }
913
1219
  : (onConnectionStatusChange ?? null));
914
1220
  csOpened.watch(connectionStatusIntervalMs);
915
- instance = new ManyScanCachesOpened(wallets, csOpened, scanSettingsOpened, newOptions);
1221
+ const actionLogPath = newOptions.actionLogPath ??
1222
+ actionLogPathFromScanSettings(scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT);
1223
+ const actionLogOpened = await ActionLogOpened.create({
1224
+ path: actionLogPath,
1225
+ backend: newOptions.actionLogBackend,
1226
+ onChange: newOptions.onActionLogChange ?? null,
1227
+ extensionMessageBus: newOptions.extensionMessageBus,
1228
+ });
1229
+ instance = new ManyScanCachesOpened(wallets, csOpened, actionLogOpened, scanSettingsOpened, newOptions);
916
1230
  return instance;
917
1231
  }
918
1232
  async buildWallets() {
919
- this.stopWorker();
1233
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
1234
+ await sendToBackground("worker.buildWallets", null).catch(() => { });
1235
+ return;
1236
+ }
1237
+ await this.stopWorker();
1238
+ await this.reloadWalletsAfterStop();
1239
+ }
1240
+ // assumes stopWorker already ran (graceful shutdown + terminate)
1241
+ async reloadWalletsAfterStop() {
920
1242
  await this._scanSettings.reload();
1243
+ // empty after reload: stay idle, do not open ScanCacheOpened or start workers
1244
+ const remaining = this._scanSettings.wallets.filter((w) => !w?.halted);
1245
+ if (remaining.length === 0) {
1246
+ this._wallets = [];
1247
+ return;
1248
+ }
921
1249
  const newWallets = await ManyScanCachesOpened._buildWallets(this._scanSettings, this._options);
922
- if (!newWallets)
923
- throw new Error("no non-halted wallets left after rebuild, use addViewWallet or addSpendWallet first");
924
- this._wallets = newWallets;
1250
+ this._wallets = newWallets ?? [];
925
1251
  }
926
1252
  async addViewWallet(primary_address, view_key, fields) {
1253
+ await this.stopWorker();
927
1254
  await this._scanSettings.addViewWallet(primary_address, view_key, fields);
928
- await this.buildWallets();
1255
+ await this.reloadWalletsAfterStop();
929
1256
  }
930
1257
  async addSpendWallet(wallet_secret, fields) {
1258
+ await this.stopWorker();
931
1259
  await this._scanSettings.addSpendWallet(wallet_secret, fields);
932
- await this.buildWallets();
1260
+ await this.reloadWalletsAfterStop();
933
1261
  }
934
1262
  async removeWallet(primary_address) {
1263
+ await this.stopWorker();
935
1264
  await this._scanSettings.removeWallet(primary_address);
936
- await this.buildWallets();
1265
+ await this.reloadWalletsAfterStop();
937
1266
  }
938
1267
  async feed(params) {
1268
+ if (this.wallets.length === 0)
1269
+ return;
939
1270
  await this.wallets[0].feed(params);
940
1271
  }
941
1272
  _wallets = [];
942
1273
  get wallets() {
943
1274
  return this._wallets;
944
1275
  }
945
- constructor(wallets, connectionStatusOpened, _scanSettings, _options) {
1276
+ constructor(wallets, connectionStatusOpened, actionLogOpened, _scanSettings, _options) {
946
1277
  this.connectionStatusOpened = connectionStatusOpened;
1278
+ this.actionLogOpened = actionLogOpened;
947
1279
  this._scanSettings = _scanSettings;
948
1280
  this._options = _options;
949
1281
  this._wallets = wallets;
1282
+ this.actionLogOpened.bindMco(this);
950
1283
  }
951
1284
  }
1285
+ function actionLogPathFromScanSettings(scan_settings_path) {
1286
+ const base = scan_settings_path.replace(/\.json$/i, "");
1287
+ if (globalThis.areWeInTheBrowser === true)
1288
+ return "actionlog";
1289
+ return `${base}-actionlog.sqlite`;
1290
+ }