@spirobel/monero-wallet-api 0.5.5 → 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 (78) 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/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +2 -2
  32. package/dist/scanning-syncing/connectionStatus.d.ts +2 -0
  33. package/dist/scanning-syncing/connectionStatus.js +36 -1
  34. package/dist/scanning-syncing/connectionStatusOpened.d.ts +1 -1
  35. package/dist/scanning-syncing/connectionStatusOpened.js +2 -11
  36. package/dist/scanning-syncing/scanresult/scanCache.d.ts +5 -1
  37. package/dist/scanning-syncing/scanresult/scanCache.js +11 -2
  38. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +46 -1
  39. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +317 -39
  40. package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +1 -0
  41. package/dist/scanning-syncing/scanresult/scanCoordination.js +40 -4
  42. package/dist/scanning-syncing/scanresult/scanResult.js +17 -12
  43. package/dist/scanning-syncing/scanresult/scanStats.js +8 -5
  44. package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
  45. package/dist/tools/calls/001.d.ts +20 -0
  46. package/dist/tools/calls/001.js +236 -0
  47. package/dist/tools/calls/002-content.d.ts +46 -0
  48. package/dist/tools/calls/002-content.js +135 -0
  49. package/dist/tools/calls/002-fail-cleanup.d.ts +27 -0
  50. package/dist/tools/calls/002-fail-cleanup.js +60 -0
  51. package/dist/tools/calls/002.d.ts +64 -0
  52. package/dist/tools/calls/002.js +353 -0
  53. package/dist/tools/content.d.ts +22 -0
  54. package/dist/tools/content.js +41 -0
  55. package/dist/tools/counterparty.d.ts +3 -0
  56. package/dist/tools/counterparty.js +1 -0
  57. package/dist/tools/download-public-suffix.d.ts +1 -0
  58. package/dist/tools/download-public-suffix.js +20 -0
  59. package/dist/tools/globals.d.ts +196 -0
  60. package/dist/tools/globals.js +14 -0
  61. package/dist/tools/monero-tools-content.d.ts +29 -0
  62. package/dist/tools/monero-tools-content.js +80 -0
  63. package/dist/tools/monero-tools.d.ts +27 -52
  64. package/dist/tools/monero-tools.js +127 -222
  65. package/dist/tools/public-suffix-list.d.ts +1 -0
  66. package/dist/tools/public-suffix-list.js +10336 -0
  67. package/dist/tools/runtimeMessages.d.ts +4 -0
  68. package/dist/tools/runtimeMessages.js +18 -0
  69. package/dist/tools/toolActionLogContext.d.ts +33 -0
  70. package/dist/tools/toolActionLogContext.js +1 -0
  71. package/dist/tools/toolArm.d.ts +47 -0
  72. package/dist/tools/toolArm.js +1 -0
  73. package/dist/tools/ui.d.ts +2 -0
  74. package/dist/tools/ui.js +1 -0
  75. package/dist/tools/worker.d.ts +1 -0
  76. package/dist/tools/worker.js +1 -0
  77. package/dist/wasm-processing/wasmFile.js +2 -2
  78. 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";
@@ -389,12 +391,14 @@ export class ScanCacheOpened {
389
391
  let maybeInputs = [];
390
392
  let maybeFeeEstimate;
391
393
  let maybeSendResult;
394
+ let maybeSignedTx;
392
395
  try {
393
396
  const { selectedInputs, feeEstimate } = await this.calculateFeeAndSelectInputs(params);
394
397
  maybeInputs = selectedInputs;
395
398
  maybeFeeEstimate = feeEstimate;
396
399
  const unsignedTx = await this.makeTransactionFromSelectedInputs(params.payments, selectedInputs, feeEstimate);
397
400
  const signedTx = await this.signTransaction(unsignedTx);
401
+ maybeSignedTx = signedTx;
398
402
  const sendResult = await this.sendTransaction(signedTx);
399
403
  maybeSendResult = sendResult;
400
404
  if (sendResult.status !== "OK")
@@ -419,6 +423,8 @@ export class ScanCacheOpened {
419
423
  inputs_index,
420
424
  height: this.current_height,
421
425
  timestamp: Date.now(),
426
+ invocationId: params.invocationId,
427
+ signed_tx: signedTx,
422
428
  };
423
429
  const newLen = cache.tx_logs.push(txLog);
424
430
  const txLogIndex = newLen - 1;
@@ -464,17 +470,16 @@ export class ScanCacheOpened {
464
470
  inputs_index,
465
471
  height: this.current_height,
466
472
  timestamp: Date.now(),
473
+ invocationId: params.invocationId,
474
+ signed_tx: maybeSignedTx,
467
475
  };
468
- const newLen = cache.tx_logs.push(txLog);
476
+ cache.tx_logs.push(txLog);
469
477
  },
470
478
  });
471
479
  const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
472
480
  if (!newCache)
473
481
  throw new Error(`cache not found for primary address: ${this.primary_address}, and path prefix: ${this.pathPrefix}`);
474
- const changed_outputs = maybeInputs.map((input) => ({
475
- change_reason: "spent",
476
- output: input,
477
- }));
482
+ const changed_outputs = [];
478
483
  await this.feed({
479
484
  newCache,
480
485
  changed_outputs,
@@ -484,6 +489,179 @@ export class ScanCacheOpened {
484
489
  throw e;
485
490
  }
486
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
+ }
487
665
  /**
488
666
  * makeStandardTransaction
489
667
  */
@@ -712,13 +890,17 @@ export class ScanCacheOpened {
712
890
  _stats = null;
713
891
  notifyListeners = [];
714
892
  }
893
+ function defaultNotifyMasterChanged(params) {
894
+ void sendToBackground("walletCacheChanged", JSON.stringify(params, (_k, v) => typeof v === "bigint" ? v.toString() : v)).catch(() => { });
895
+ }
715
896
  export class ManyScanCachesOpened {
716
897
  connectionStatusOpened;
898
+ actionLogOpened;
717
899
  _scanSettings;
718
900
  _options;
719
901
  get start_height() {
720
902
  if (this.wallets.length === 0)
721
- return null;
903
+ return this._scanSettings.start_height;
722
904
  return this.wallets[0]?.start_height;
723
905
  }
724
906
  // overall scan tip = lagging non-halted wallet (this.wallets is already non-halted)
@@ -735,14 +917,25 @@ export class ManyScanCachesOpened {
735
917
  }
736
918
  get node_url() {
737
919
  if (this.wallets.length === 0)
738
- return "";
920
+ return this._scanSettings.node_url ?? "";
739
921
  return this.wallets[0]?.node_url;
740
922
  }
741
923
  async changeNodeUrlAndStartHeight(node_url, start_height) {
742
- if (this.wallets.length === 0)
924
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
925
+ await sendToBackground("worker.changeNodeUrlAndStartHeight", {
926
+ node_url,
927
+ start_height,
928
+ }).catch(() => { });
743
929
  return;
744
- const masterWallet = this.wallets[0];
745
- return await masterWallet.changeNodeUrlAndStartHeight(node_url, start_height);
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);
746
939
  }
747
940
  async retry() {
748
941
  if (this.wallets.length === 0)
@@ -751,6 +944,10 @@ export class ManyScanCachesOpened {
751
944
  return await masterWallet.retry();
752
945
  }
753
946
  async stopWorker() {
947
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
948
+ await sendToBackground("worker.stopWorker", null).catch(() => { });
949
+ return;
950
+ }
754
951
  if (this.wallets.length === 0)
755
952
  return;
756
953
  const masterWallet = this.wallets[0];
@@ -764,10 +961,49 @@ export class ManyScanCachesOpened {
764
961
  return await this.wallets[0].dumpWorkerHeaps(dir);
765
962
  }
766
963
  async changeNodeUrl(node_url) {
767
- if (this.wallets.length === 0)
964
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
965
+ await sendToBackground("worker.setNodeUrl", { node_url }).catch(() => { });
768
966
  return;
769
- const masterWallet = this.wallets[0];
770
- return await masterWallet.changeNodeUrl(node_url);
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();
771
1007
  }
772
1008
  get merchant_confirmations() {
773
1009
  if (this.wallets.length === 0)
@@ -811,7 +1047,20 @@ export class ManyScanCachesOpened {
811
1047
  return await this.wallets[0].setCpuWorkerCount(cpu_worker_count);
812
1048
  }
813
1049
  async setLogSettings(logs, logs_include, logs_exclude) {
814
- return await this._scanSettings.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();
815
1064
  }
816
1065
  async setWalletName(primary_address, name) {
817
1066
  await this._scanSettings.setWalletName(primary_address, name);
@@ -820,10 +1069,17 @@ export class ManyScanCachesOpened {
820
1069
  await this._scanSettings.setWalletSlot(primary_address, slot);
821
1070
  }
822
1071
  async changeStartHeight(start_height) {
823
- if (this.wallets.length === 0)
1072
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
1073
+ await sendToBackground("worker.setStartHeight", {
1074
+ start_height,
1075
+ }).catch(() => { });
824
1076
  return;
825
- const masterWallet = this.wallets[0];
826
- return await masterWallet.changeStartHeight(start_height);
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);
827
1083
  }
828
1084
  static async _buildWallets(scanSettingsOpened, options) {
829
1085
  const { scan_settings_path, pathPrefix, no_worker, no_stats, workerError, notifyMasterChanged, } = options;
@@ -894,34 +1150,36 @@ export class ManyScanCachesOpened {
894
1150
  // wrap workerError with auto-retry
895
1151
  let retryTimer;
896
1152
  let instance = null;
1153
+ const clearRetryTimer = () => {
1154
+ clearTimeout(retryTimer);
1155
+ retryTimer = undefined;
1156
+ };
897
1157
  const retryFn = async () => {
1158
+ clearRetryTimer();
898
1159
  // no wallets means idle, do not rebuild or restart workers
899
- if (!instance || instance.wallets.length === 0) {
900
- clearTimeout(retryTimer);
901
- retryTimer = undefined;
902
- return;
903
- }
904
- await instance.buildWallets();
905
- // still empty after rebuild, stay idle
906
- if (instance.wallets.length === 0) {
907
- clearTimeout(retryTimer);
908
- retryTimer = undefined;
1160
+ if (!instance || instance.wallets.length === 0)
909
1161
  return;
910
- }
911
1162
  const node_url = instance.node_url;
912
1163
  if (!node_url)
913
- throw new Error("No nodeurl set, can't retry connection");
1164
+ return;
1165
+ // probe first; only restart workers when node is reachable
914
1166
  const getinfo_result = await get_info(node_url)
915
- .then((r) => {
916
- r?.status === "OK";
917
- })
1167
+ .then((r) => r?.status === "OK")
918
1168
  .catch(() => false);
919
- if (getinfo_result)
920
- await instance?.retry();
921
- clearTimeout(retryTimer);
922
- 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();
923
1177
  };
924
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
+ }
925
1183
  let connectionFailedShown = false;
926
1184
  if (autoRetry) {
927
1185
  const originalError = options.workerError;
@@ -930,7 +1188,7 @@ export class ManyScanCachesOpened {
930
1188
  const msg = err instanceof Error ? err.message : String(err);
931
1189
  if (csOpened.connectionStatus?.last_packet?.status ===
932
1190
  "catastrophic_reorg") {
933
- clearTimeout(retryTimer);
1191
+ clearRetryTimer();
934
1192
  instance?.stopWorker();
935
1193
  throw new Error("catastrophic reorg, aborting ...");
936
1194
  }
@@ -960,10 +1218,22 @@ export class ManyScanCachesOpened {
960
1218
  }
961
1219
  : (onConnectionStatusChange ?? null));
962
1220
  csOpened.watch(connectionStatusIntervalMs);
963
- 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);
964
1230
  return instance;
965
1231
  }
966
1232
  async buildWallets() {
1233
+ if (this.actionLogOpened.extensionMessageBus === "ui") {
1234
+ await sendToBackground("worker.buildWallets", null).catch(() => { });
1235
+ return;
1236
+ }
967
1237
  await this.stopWorker();
968
1238
  await this.reloadWalletsAfterStop();
969
1239
  }
@@ -1003,10 +1273,18 @@ export class ManyScanCachesOpened {
1003
1273
  get wallets() {
1004
1274
  return this._wallets;
1005
1275
  }
1006
- constructor(wallets, connectionStatusOpened, _scanSettings, _options) {
1276
+ constructor(wallets, connectionStatusOpened, actionLogOpened, _scanSettings, _options) {
1007
1277
  this.connectionStatusOpened = connectionStatusOpened;
1278
+ this.actionLogOpened = actionLogOpened;
1008
1279
  this._scanSettings = _scanSettings;
1009
1280
  this._options = _options;
1010
1281
  this._wallets = wallets;
1282
+ this.actionLogOpened.bindMco(this);
1011
1283
  }
1012
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
+ }
@@ -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
  };
@@ -2,7 +2,7 @@ import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, pr
2
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;
@@ -278,6 +291,12 @@ export async function setupCoordinator(scanSettingsPath, pathPrefix, stopSync) {
278
291
  const work_to_be_done = await findWorkToBeDone(scanSettingsPath, pathPrefix);
279
292
  if (!work_to_be_done)
280
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);
281
300
  const { generator: blocksGenerator, blocksBuffer } = await setupBlocksBufferGenerator({
282
301
  nodeUrl: work_to_be_done.scan_settings.node_url,
283
302
  startHeight: work_to_be_done.start_height,
@@ -345,9 +364,10 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
345
364
  if (!ctx)
346
365
  throw new Error("[coordinatorMain Multithreaded] findWorkToBeDone returned false");
347
366
  const work_to_be_done = ctx.work_to_be_done;
348
- const blocksBuffer = ctx.blocksBuffer;
367
+ let blocksBuffer = ctx.blocksBuffer;
349
368
  const workBuffer = ctx.workBuffer;
350
- const blocksGenerator = ctx.blocksGenerator;
369
+ let blocksGenerator = ctx.blocksGenerator;
370
+ let fetchAt = work_to_be_done.start_height;
351
371
  let totalBlocksScanned = 0;
352
372
  let scanStartTime = Date.now();
353
373
  let blocksPromise = blocksGenerator.next();
@@ -428,12 +448,28 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
428
448
  const blockCount = to_be_processed.to - to_be_processed.from + 1;
429
449
  totalBlocksScanned += blockCount;
430
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
+ }
431
467
  // always persist wallet progress after process; eta only if we have a new one
432
468
  // so missing eta does not wipe the previous value (no flicker)
433
469
  const eta = computeETA(wallet.cache, totalBlocksScanned, scanStartTime);
434
470
  await readWriteConnectionStatusFile((cs) => {
435
471
  applyWalletScanProgress(cs, {
436
- current_scan_height: lastRange(wallet.cache.scanned_ranges)?.end || 0,
472
+ current_scan_height: currentScanHeightFromRanges(wallet.cache.scanned_ranges, work_to_be_done.scan_settings.start_height || 0),
437
473
  scanned_ranges: wallet.cache.scanned_ranges,
438
474
  daemon_height: wallet.cache.daemon_height,
439
475
  eta,
@@ -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] = {