@spirobel/monero-wallet-api 0.5.1 → 0.5.3

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 (28) hide show
  1. package/dist/actionlog/actionLog.d.ts +4 -0
  2. package/dist/actionlog/actionLog.js +9 -0
  3. package/dist/actionlog/actionLogOpened.d.ts +1 -0
  4. package/dist/actionlog/actionLogOpened.js +1 -0
  5. package/dist/api.d.ts +3 -2
  6. package/dist/api.js +3 -2
  7. package/dist/scanning-syncing/backgroundWorker.js +1 -1
  8. package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +29 -3
  9. package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +6 -3
  10. package/dist/scanning-syncing/connectionStatus.d.ts +1 -4
  11. package/dist/scanning-syncing/connectionStatus.js +1 -31
  12. package/dist/scanning-syncing/connectionStatusOpened.d.ts +15 -0
  13. package/dist/scanning-syncing/connectionStatusOpened.js +51 -0
  14. package/dist/scanning-syncing/openWallet.js +1 -1
  15. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +55 -7
  16. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +191 -50
  17. package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +1 -1
  18. package/dist/scanning-syncing/scanresult/scanCoordination.js +64 -2
  19. package/dist/scanning-syncing/scansettings/scanSettings.d.ts +165 -0
  20. package/dist/scanning-syncing/scansettings/scanSettings.js +338 -0
  21. package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
  22. package/dist/scanning-syncing/worker-mains/worker.js +1 -2
  23. package/dist/scansettings/scanSettings.d.ts +165 -0
  24. package/dist/scansettings/scanSettings.js +338 -0
  25. package/dist/scansettings/scanSettingsOpened.d.ts +127 -0
  26. package/dist/scansettings/scanSettingsOpened.js +475 -0
  27. package/dist/viewpair/ViewPair.js +1 -1
  28. package/package.json +1 -1
@@ -1,12 +1,15 @@
1
- import { atomicWrite, NodeUrl, signTransaction, ViewPair, } from "../../api";
1
+ import { NodeUrl, signTransaction, ViewPair, } from "../../api";
2
2
  import { prepareInput, sumPayments, } from "../../send-functionality/inputSelection";
3
3
  import { createWebworker } from "../backgroundWorker";
4
4
  import { spendable } from "./scanResult";
5
- import { getPathPrefix, openScanSettingsFile, readPrivateSpendKeyFromEnv, readWalletFromScanSettings, SCAN_SETTINGS_STORE_NAME_DEFAULT, SUB_ADDRESS_INDEX_DEFAULT_VALUE, walletSettingsPlusKeys, writeNodeUrlToScanSettings, writeStartHeightToScanSettings, writeWalletToScanSettings, } from "../scanSettings";
5
+ import { getPathPrefix, readPrivateSpendKeyFromEnv, SCAN_SETTINGS_STORE_NAME_DEFAULT, SUB_ADDRESS_INDEX_DEFAULT_VALUE, walletSettingsPlusKeys, } from "../../api";
6
+ import { ScanSettingsOpened } from "../../scansettings/scanSettingsOpened";
7
+ import { ConnectionStatusOpened } from "../connectionStatusOpened";
6
8
  import { findRange, lastRange, readCacheFileDefaultLocation, writeCacheFileDefaultLocationThrows, } from "./scanCache";
7
9
  import { alignScanStatsWithCache, isSelfSpent, processTxlogInputs, processTxlogPayments, writeStatsFileDefaultLocation, } from "./scanStats";
8
- import { updateSyncETA, readWriteConnectionStatusFile, } from "../connectionStatus";
10
+ import { readWriteConnectionStatusFile, } from "../connectionStatus";
9
11
  export class ScanCacheOpened {
12
+ _scanSettings;
10
13
  view_pair;
11
14
  wallet_route;
12
15
  wallet_name;
@@ -35,17 +38,24 @@ export class ScanCacheOpened {
35
38
  decoyRetrySizes = [20, 50, 100, 200, 500];
36
39
  static async create(params) {
37
40
  const theCatchToBeOpened = await readCacheFileDefaultLocation(params.primary_address, params.pathPrefix);
38
- const walletSettings = await readWalletFromScanSettings(params.primary_address, params.scan_settings_path);
41
+ if (!params.primary_address)
42
+ throw new Error(`primary_address is required, potentially half filled out wallet setting in: ${params.scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT}`);
43
+ // use ScanSettingsOpened instead of direct scanSettings calls
44
+ const scanSettings = await ScanSettingsOpened.create(params.scan_settings_path, params.pathPrefix);
45
+ const walletSettings = scanSettings.getWallet(params.primary_address);
39
46
  if (!walletSettings)
40
47
  throw new Error(`wallet not found in settings. did you call openwallet with the right params?
41
48
  Either wrong file name supplied to params.scan_settings_path: ${params.scan_settings_path}
42
49
  Or wrong primary_address supplied params.primary_address: ${params.primary_address}`);
43
- if (!params.primary_address)
44
- throw new Error(`primary_address is required, potentially half filled out wallet setting in: ${params.scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT}`);
50
+ const walletWithSettings = {
51
+ ...walletSettings,
52
+ node_url: scanSettings.node_url,
53
+ start_height: scanSettings.start_height,
54
+ };
45
55
  // read secret_view_key and secret_spend_key from env
46
- const walletSettingsWithKeys = await walletSettingsPlusKeys(walletSettings);
56
+ const walletSettingsWithKeys = await walletSettingsPlusKeys(walletWithSettings);
47
57
  // create viewpair + ScanCacheOpened instance
48
- const scanCacheOpen = new ScanCacheOpened(await ViewPair.create(params.primary_address, walletSettingsWithKeys.secret_view_key, walletSettings.subaddress_index, walletSettings.node_url), walletSettings.wallet_route, walletSettings.wallet_name, walletSettings.wallet_slot, params.no_worker || false, params.masterCacheChanged || null, walletSettings.start_height, params.scan_settings_path, params.pathPrefix, params.workerError);
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);
49
59
  if (theCatchToBeOpened)
50
60
  scanCacheOpen._cache = theCatchToBeOpened;
51
61
  if (!walletSettings.halted) {
@@ -86,7 +96,7 @@ export class ScanCacheOpened {
86
96
  this.worker.terminate();
87
97
  delete this.worker;
88
98
  }
89
- await writeStartHeightToScanSettings(start_height, this.scan_settings_path);
99
+ await this._scanSettings.setStartHeight(start_height);
90
100
  this._start_height = start_height;
91
101
  await this.unpause();
92
102
  }
@@ -144,17 +154,32 @@ export class ScanCacheOpened {
144
154
  set node_url(nu) {
145
155
  this.view_pair.node_url = nu;
146
156
  }
157
+ get merchant_confirmations() {
158
+ return this._scanSettings.merchant_confirmations;
159
+ }
160
+ get cpu_worker_count() {
161
+ return this._scanSettings.cpu_worker_count;
162
+ }
163
+ get logs() {
164
+ return this._scanSettings.logs;
165
+ }
166
+ get logs_include() {
167
+ return this._scanSettings.logs_include;
168
+ }
169
+ get logs_exclude() {
170
+ return this._scanSettings.logs_exclude;
171
+ }
147
172
  async changeNodeUrlAndStartHeight(node_url, start_height) {
148
173
  if (this.worker) {
149
174
  this.worker.terminate();
150
175
  delete this.worker;
151
176
  }
152
177
  if (node_url !== undefined) {
153
- await writeNodeUrlToScanSettings(node_url, this.scan_settings_path);
178
+ await this._scanSettings.setNodeUrl(node_url);
154
179
  this.node_url = node_url;
155
180
  }
156
181
  if (start_height !== undefined) {
157
- await writeStartHeightToScanSettings(start_height, this.scan_settings_path);
182
+ await this._scanSettings.setStartHeight(start_height);
158
183
  this._start_height = start_height;
159
184
  }
160
185
  await this.unpause();
@@ -164,17 +189,37 @@ export class ScanCacheOpened {
164
189
  this.worker.terminate();
165
190
  delete this.worker;
166
191
  }
167
- await writeNodeUrlToScanSettings(node_url, this.scan_settings_path);
192
+ await this._scanSettings.setNodeUrl(node_url);
168
193
  this.node_url = node_url;
169
194
  await this.unpause();
170
195
  }
196
+ async setMerchantConfirmations(merchant_confirmations) {
197
+ this.stopWorker();
198
+ await this._scanSettings.setMerchantConfirmations(merchant_confirmations);
199
+ await this.unpause();
200
+ }
201
+ async setCpuWorkerCount(cpu_worker_count) {
202
+ this.stopWorker();
203
+ await this._scanSettings.setCpuWorkerCount(cpu_worker_count);
204
+ await this.unpause();
205
+ }
206
+ async setLogSettings(logs, logs_include, logs_exclude) {
207
+ this.stopWorker();
208
+ await this._scanSettings.setLogSettings(logs, logs_include, logs_exclude);
209
+ await this.unpause();
210
+ }
211
+ async setWalletName(name) {
212
+ await this._scanSettings.setWalletName(this.view_pair.primary_address, name);
213
+ }
171
214
  async retry() {
172
215
  if (this.worker) {
173
216
  this.worker.terminate();
174
217
  delete this.worker;
175
218
  }
176
- const scan_settings = await readWalletFromScanSettings(this.primary_address, this.scan_settings_path).catch(() => false);
177
- if (scan_settings) {
219
+ // scansettings so external changes (e.g. from a sidebar frontend instance) are picked up
220
+ await this._scanSettings.reload();
221
+ const walletStillExists = this._scanSettings.walletExists(this.primary_address);
222
+ if (walletStillExists) {
178
223
  // if there is no scan settings file,
179
224
  // the retry loop is stopped.
180
225
  // the wallet reset happens through deleting all the scan setting + cache files
@@ -182,7 +227,7 @@ export class ScanCacheOpened {
182
227
  //TODO ? write connection status retry
183
228
  await this.unpause();
184
229
  }
185
- return scan_settings ? true : false;
230
+ return walletStillExists;
186
231
  }
187
232
  async sendTransaction(signedTx) {
188
233
  const node = await NodeUrl.create(this.node_url);
@@ -480,7 +525,9 @@ export class ScanCacheOpened {
480
525
  * @returns Adressstring
481
526
  */
482
527
  async makeSubaddress() {
483
- const walletSettings = await readWalletFromScanSettings(this.view_pair.primary_address, this.scan_settings_path);
528
+ // reload scansettings so external changes (e.g. from a sidebar frontend instance) are picked up
529
+ await this._scanSettings.reload();
530
+ const walletSettings = this._scanSettings.getWallet(this.view_pair.primary_address);
484
531
  if (!walletSettings)
485
532
  throw new Error(`wallet not found in settings. did you call openwallet with the right params?
486
533
  Either wrong file name supplied to params.scan_settings_path: ${this.scan_settings_path}
@@ -489,11 +536,7 @@ export class ScanCacheOpened {
489
536
  const minor = last_subaddress_index + 1;
490
537
  const subaddress = this.view_pair.makeSubaddress(minor);
491
538
  this._highest_subaddress_index = minor;
492
- await writeWalletToScanSettings({
493
- primary_address: this.view_pair.primary_address,
494
- subaddress_index: minor,
495
- scan_settings_path: this.scan_settings_path,
496
- });
539
+ await this._scanSettings.setSubaddressIndex(this.view_pair.primary_address, minor);
497
540
  const created_at_height = lastRange(this._cache.scanned_ranges)?.end || 0;
498
541
  const created_at_timestamp = new Date().getTime();
499
542
  const new_subaddress = {
@@ -529,11 +572,7 @@ export class ScanCacheOpened {
529
572
  async pause() {
530
573
  if (this.worker)
531
574
  this.worker.terminate();
532
- return await writeWalletToScanSettings({
533
- primary_address: this.view_pair.primary_address,
534
- halted: true,
535
- scan_settings_path: this.scan_settings_path,
536
- });
575
+ return await this._scanSettings.haltWallet(this.view_pair.primary_address);
537
576
  }
538
577
  stopWorker() {
539
578
  if (this.worker) {
@@ -566,11 +605,7 @@ export class ScanCacheOpened {
566
605
  });
567
606
  });
568
607
  }
569
- return await writeWalletToScanSettings({
570
- primary_address: this.view_pair.primary_address,
571
- halted: false,
572
- scan_settings_path: this.scan_settings_path,
573
- });
608
+ return await this._scanSettings.unhaltWallet(this.view_pair.primary_address);
574
609
  }
575
610
  /**
576
611
  * selectInputs returns array of inputs, whose sum is larger than amount
@@ -635,11 +670,6 @@ export class ScanCacheOpened {
635
670
  this._cache = params.newCache;
636
671
  if (!this._no_stats)
637
672
  this._stats = await alignScanStatsWithCache(this._cache, this.view_pair, this.primary_address, getPathPrefix(this.scan_settings_path, this.pathPrefix), this.subaddress_index, lastRange(this._cache.scanned_ranges)?.end);
638
- if (!this.no_worker) {
639
- const etaResult = await updateSyncETA(this._cache.daemon_height, this.current_height || 0, this.last_eta_height, this.last_eta_timestamp, this.scan_settings_path);
640
- this.last_eta_height = etaResult.last_height;
641
- this.last_eta_timestamp = etaResult.last_timestamp;
642
- }
643
673
  for (const listener of this.notifyListeners) {
644
674
  if (listener)
645
675
  listener(params);
@@ -660,7 +690,8 @@ export class ScanCacheOpened {
660
690
  worker = undefined;
661
691
  last_eta_height = null;
662
692
  last_eta_timestamp = null;
663
- constructor(view_pair, wallet_route, wallet_name, wallet_slot, no_worker, masterCacheChanged, _start_height, scan_settings_path, pathPrefix, workerError) {
693
+ constructor(_scanSettings, view_pair, wallet_route, wallet_name, wallet_slot, no_worker, masterCacheChanged, _start_height, scan_settings_path, pathPrefix, workerError) {
694
+ this._scanSettings = _scanSettings;
664
695
  this.view_pair = view_pair;
665
696
  this.wallet_route = wallet_route;
666
697
  this.wallet_name = wallet_name;
@@ -676,7 +707,9 @@ export class ScanCacheOpened {
676
707
  notifyListeners = [];
677
708
  }
678
709
  export class ManyScanCachesOpened {
679
- wallets;
710
+ connectionStatusOpened;
711
+ _scanSettings;
712
+ _options;
680
713
  get start_height() {
681
714
  if (this.wallets.length === 0)
682
715
  return null;
@@ -716,18 +749,70 @@ export class ManyScanCachesOpened {
716
749
  const masterWallet = this.wallets[0];
717
750
  return await masterWallet.changeNodeUrl(node_url);
718
751
  }
752
+ get merchant_confirmations() {
753
+ if (this.wallets.length === 0)
754
+ return undefined;
755
+ return this.wallets[0]?.merchant_confirmations;
756
+ }
757
+ get cpu_worker_count() {
758
+ if (this.wallets.length === 0)
759
+ return undefined;
760
+ return this.wallets[0]?.cpu_worker_count;
761
+ }
762
+ get logs() {
763
+ if (this.wallets.length === 0)
764
+ return undefined;
765
+ return this.wallets[0]?.logs;
766
+ }
767
+ get logs_include() {
768
+ if (this.wallets.length === 0)
769
+ return undefined;
770
+ return this.wallets[0]?.logs_include;
771
+ }
772
+ get logs_exclude() {
773
+ if (this.wallets.length === 0)
774
+ return undefined;
775
+ return this.wallets[0]?.logs_exclude;
776
+ }
777
+ get connectionStatus() {
778
+ return this.connectionStatusOpened.connectionStatus;
779
+ }
780
+ get daemonHeight() {
781
+ return this.connectionStatusOpened.daemonHeight;
782
+ }
783
+ watchConnectionStatus(intervalMs) {
784
+ this.connectionStatusOpened.watch(intervalMs);
785
+ }
786
+ unwatchConnectionStatus() {
787
+ this.connectionStatusOpened.unwatch();
788
+ }
789
+ async setMerchantConfirmations(merchant_confirmations) {
790
+ if (this.wallets.length === 0)
791
+ throw new Error("no wallets");
792
+ return await this.wallets[0].setMerchantConfirmations(merchant_confirmations);
793
+ }
794
+ async setCpuWorkerCount(cpu_worker_count) {
795
+ if (this.wallets.length === 0)
796
+ throw new Error("no wallets");
797
+ return await this.wallets[0].setCpuWorkerCount(cpu_worker_count);
798
+ }
799
+ async setLogSettings(logs, logs_include, logs_exclude) {
800
+ if (this.wallets.length === 0)
801
+ throw new Error("no wallets");
802
+ return await this.wallets[0].setLogSettings(logs, logs_include, logs_exclude);
803
+ }
804
+ async setWalletName(primary_address, name) {
805
+ await this._scanSettings.setWalletName(primary_address, name);
806
+ }
719
807
  async changeStartHeight(start_height) {
720
808
  if (this.wallets.length === 0)
721
809
  throw new Error("no wallets");
722
810
  const masterWallet = this.wallets[0];
723
811
  return await masterWallet.changeStartHeight(start_height);
724
812
  }
725
- static async create({ scan_settings_path, pathPrefix, no_worker, notifyMasterChanged, no_stats, workerError, }) {
726
- const scan_settings = await openScanSettingsFile(scan_settings_path);
727
- if (!scan_settings?.wallets)
728
- throw new Error(`no wallets in settings file. Did you supply the right path?
729
- are there wallets in the default '${SCAN_SETTINGS_STORE_NAME_DEFAULT}' file?`);
730
- const nonHaltedWallets = scan_settings.wallets.filter((wallet) => !wallet?.halted);
813
+ static async _buildWallets(scanSettingsOpened, options) {
814
+ const { scan_settings_path, pathPrefix, no_worker, no_stats, workerError, notifyMasterChanged, } = options;
815
+ const nonHaltedWallets = scanSettingsOpened.wallets.filter((wallet) => !wallet?.halted);
731
816
  if (!nonHaltedWallets.length)
732
817
  return undefined;
733
818
  const openedWallets = [];
@@ -776,15 +861,71 @@ export class ManyScanCachesOpened {
776
861
  });
777
862
  openedWallets.push(onlyWallet);
778
863
  }
779
- return new ManyScanCachesOpened(openedWallets);
864
+ return openedWallets;
865
+ }
866
+ static async create(options) {
867
+ const { scan_settings_path, pathPrefix, onConnectionStatusChange, connectionStatusIntervalMs, autoRetry, retryDelayMs, } = options;
868
+ const scanSettingsOpened = await ScanSettingsOpened.create(scan_settings_path, pathPrefix);
869
+ if (!scanSettingsOpened.wallets || scanSettingsOpened.wallets.length === 0)
870
+ throw new Error(`no wallets in settings file. Did you supply the right path?
871
+ are there wallets in the default '${SCAN_SETTINGS_STORE_NAME_DEFAULT}' file?`);
872
+ // wrap workerError with auto-retry
873
+ let retryTimer;
874
+ let instance = null;
875
+ const newOptions = { ...options };
876
+ if (autoRetry) {
877
+ const originalError = options.workerError;
878
+ newOptions.workerError = (err) => {
879
+ originalError?.(err);
880
+ if (!retryTimer) {
881
+ retryTimer = setTimeout(() => {
882
+ retryTimer = undefined;
883
+ instance?.retry();
884
+ }, retryDelayMs ?? 1000);
885
+ }
886
+ };
887
+ }
888
+ const wallets = await this._buildWallets(scanSettingsOpened, newOptions);
889
+ if (!wallets)
890
+ return undefined;
891
+ const csOpened = new ConnectionStatusOpened(scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT, onConnectionStatusChange ?? null);
892
+ csOpened.watch(connectionStatusIntervalMs);
893
+ instance = new ManyScanCachesOpened(wallets, csOpened, scanSettingsOpened, newOptions);
894
+ return instance;
895
+ }
896
+ async buildWallets() {
897
+ for (const w of this._wallets) {
898
+ w.stopWorker();
899
+ }
900
+ await this._scanSettings.reload();
901
+ const newWallets = await ManyScanCachesOpened._buildWallets(this._scanSettings, this._options);
902
+ if (!newWallets)
903
+ throw new Error("no non-halted wallets left after rebuild, use addViewWallet or addSpendWallet first");
904
+ this._wallets = newWallets;
905
+ }
906
+ async addViewWallet(primary_address, view_key, fields) {
907
+ await this._scanSettings.addViewWallet(primary_address, view_key, fields);
908
+ await this.buildWallets();
909
+ }
910
+ async addSpendWallet(wallet_secret, fields) {
911
+ await this._scanSettings.addSpendWallet(wallet_secret, fields);
912
+ await this.buildWallets();
913
+ }
914
+ async removeWallet(primary_address) {
915
+ await this._scanSettings.removeWallet(primary_address);
916
+ await this.buildWallets();
780
917
  }
781
- /**
782
- * feed the master wallet and therefore all wallets
783
- */
784
918
  async feed(params) {
785
919
  await this.wallets[0].feed(params);
786
920
  }
787
- constructor(wallets) {
788
- this.wallets = wallets;
921
+ _wallets = [];
922
+ get wallets() {
923
+ return this._wallets;
924
+ }
925
+ constructor(wallets, connectionStatusOpened, _scanSettings, _options) {
926
+ this.connectionStatusOpened = connectionStatusOpened;
927
+ this._scanSettings = _scanSettings;
928
+ this._options = _options;
929
+ this._wallets = wallets;
789
930
  }
790
931
  }
@@ -1,7 +1,7 @@
1
1
  import { type GetBlocksBinBufferItem, type BlocksBufferLoopResult, type BlocksBufferIteratorResult, type ProcessScanResult, type BlockInfo } from "../../api";
2
2
  import { type ScanLoopYield } from "./scanLoop";
3
3
  import { type WorkItem } from "./scanLoop";
4
- import { type ScanSettings } from "../scanSettings";
4
+ import { type ScanSettings } from "../../api";
5
5
  import { type CacheRange, type ScanCache } from "./scanCache";
6
6
  export type WalletConfig = {
7
7
  primary_address: string;
@@ -1,7 +1,7 @@
1
- import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, } from "../../api";
1
+ import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, readWriteConnectionStatusFile, connectionStatusFilePath, } from "../../api";
2
2
  import { markWorkItemAsDone, } from "./scanLoop";
3
3
  import { makeWorkItem, scanLoop } from "./scanLoop";
4
- import { cullTooLargeScanHeight, getNonHaltedWallets, getPathPrefix, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, } from "../scanSettings";
4
+ import { cullTooLargeScanHeight, getNonHaltedWallets, getPathPrefix, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, } from "../../api";
5
5
  import { findRange, initScanCacheFile, lastRange, makeCacheRangeForHeight, mergeRanges, } from "./scanCache";
6
6
  import { sendToCpuWorker } from "../worker-mains/cpubound-main";
7
7
  import { log } from "../../io/logging";
@@ -299,6 +299,30 @@ function logBufStatus(blocksBuffer, workBuffer, ports, label) {
299
299
  `[buf] ${label} blocks=[${bb.join(",")}] work=[${wb.join(",")}]`,
300
300
  ]);
301
301
  }
302
+ function msToHHMM(ms) {
303
+ if (!Number.isFinite(ms) || ms < 0)
304
+ return "00:00";
305
+ const totalSeconds = Math.floor(ms / 1000);
306
+ const minutes = Math.floor(totalSeconds / 60);
307
+ const hours = Math.floor(minutes / 60);
308
+ const remainingMinutes = minutes % 60;
309
+ return `${String(hours).padStart(2, "0")}:${String(remainingMinutes).padStart(2, "0")}`;
310
+ }
311
+ function computeETA(cache, totalBlocksScanned, scanStartTime) {
312
+ const scanProgress = lastRange(cache.scanned_ranges)?.end || 0;
313
+ const elapsed = Date.now() - scanStartTime;
314
+ const daemonHeight = cache.daemon_height;
315
+ if (totalBlocksScanned <= 0 || elapsed <= 0 || daemonHeight <= 0)
316
+ return undefined;
317
+ const remaining = daemonHeight - scanProgress;
318
+ if (remaining <= 0)
319
+ return "00:00";
320
+ const blocksPerMs = totalBlocksScanned / elapsed;
321
+ if (blocksPerMs <= 0)
322
+ return undefined;
323
+ const etaMs = remaining / blocksPerMs;
324
+ return msToHHMM(etaMs);
325
+ }
302
326
  /**
303
327
  * coordinator main: async generator that drives the full scan cycle.
304
328
  * takes only scanSettingsPath, derives everything else internally.
@@ -314,6 +338,8 @@ export async function* coordinatorMain(scanSettingsPath, pathPrefix) {
314
338
  const blocksBuffer = ctx.blocksBuffer;
315
339
  const workBuffer = ctx.workBuffer;
316
340
  const blocksGenerator = ctx.blocksGenerator;
341
+ let totalBlocksScanned = 0;
342
+ let scanStartTime = Date.now();
317
343
  let blocksPromise = blocksGenerator.next();
318
344
  const scanGens = new Map();
319
345
  const scanPromises = new Map();
@@ -379,7 +405,25 @@ export async function* coordinatorMain(scanSettingsPath, pathPrefix) {
379
405
  const wc = work_to_be_done.wallet_configs.find((x) => x.primary_address === addr);
380
406
  if (!wc)
381
407
  throw new Error("[coordinatorMain] wallet config not found on process result");
408
+ // count blocks scanned in this work item for ETA
409
+ const doneItem = workBuffer.find((x) => x.work_uuid === value.work_uuid);
410
+ const blockCount = doneItem ? doneItem.to - doneItem.from + 1 : 0;
382
411
  await processScanResultForWorkItem(value, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wc?.secret_spend_key);
412
+ totalBlocksScanned += blockCount;
413
+ const eta = computeETA(wc.cache, totalBlocksScanned, scanStartTime);
414
+ // write connection status sync (includes eta)
415
+ if (eta) {
416
+ await readWriteConnectionStatusFile((cs) => {
417
+ cs.sync = {
418
+ ...cs.sync,
419
+ scanned_ranges: wc.cache.scanned_ranges,
420
+ daemon_height: wc.cache.daemon_height,
421
+ current_scan_height: lastRange(wc.cache.scanned_ranges)?.end || 0,
422
+ eta,
423
+ timestamp: new Date().toISOString(),
424
+ };
425
+ }, scanSettingsPath);
426
+ }
383
427
  const nextItem = workBuffer.find((x) => x.status !== "process_result_done" &&
384
428
  x.walletConfig.primary_address === addr);
385
429
  if (nextItem) {
@@ -452,6 +496,8 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
452
496
  const blocksBuffer = ctx.blocksBuffer;
453
497
  const workBuffer = ctx.workBuffer;
454
498
  const blocksGenerator = ctx.blocksGenerator;
499
+ let totalBlocksScanned = 0;
500
+ let scanStartTime = Date.now();
455
501
  let blocksPromise = blocksGenerator.next();
456
502
  const freePorts = [];
457
503
  for (const port of cpuPorts) {
@@ -510,6 +556,22 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
510
556
  const to_be_processed = processable[0];
511
557
  if (!to_be_processed)
512
558
  continue;
559
+ const blockCount = to_be_processed.to - to_be_processed.from + 1;
560
+ totalBlocksScanned += blockCount;
561
+ const eta = computeETA(wallet.cache, totalBlocksScanned, scanStartTime);
562
+ // write connection status sync (includes eta)
563
+ if (eta) {
564
+ await readWriteConnectionStatusFile((cs) => {
565
+ cs.sync = {
566
+ ...cs.sync,
567
+ scanned_ranges: wallet.cache.scanned_ranges,
568
+ daemon_height: wallet.cache.daemon_height,
569
+ current_scan_height: lastRange(wallet.cache.scanned_ranges)?.end || 0,
570
+ eta,
571
+ timestamp: new Date().toISOString(),
572
+ };
573
+ }, scanSettingsPath);
574
+ }
513
575
  const res = await processWorkItem(to_be_processed, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wallet.secret_spend_key);
514
576
  // log("coordinatorMainMultithreaded", "processWorkItem result");
515
577
  yield {
@@ -0,0 +1,165 @@
1
+ import type { LogSetting, PossibleLogs } from "../../io/logging";
2
+ export declare const SCAN_SETTINGS_STORE_NAME_DEFAULT = "ScanSettings.json";
3
+ export declare const SUB_ADDRESS_INDEX_DEFAULT_VALUE = 1;
4
+ export type ScanSetting = {
5
+ primary_address: string;
6
+ subaddress_index?: number;
7
+ halted?: boolean;
8
+ wallet_route?: string;
9
+ wallet_name?: string;
10
+ wallet_slot?: number;
11
+ };
12
+ export type WriteScanSettingParams = {
13
+ primary_address: string;
14
+ start_height?: number | null;
15
+ subaddress_index?: number;
16
+ halted?: boolean;
17
+ scan_settings_path?: string;
18
+ node_url?: string;
19
+ wallet_route?: string;
20
+ wallet_name?: string;
21
+ wallet_slot?: number;
22
+ logs?: LogSetting;
23
+ logs_include?: PossibleLogs[];
24
+ logs_exclude?: PossibleLogs[];
25
+ };
26
+ export type ScanSettingOpened = {
27
+ primary_address: string;
28
+ start_height: number | null;
29
+ node_url: string;
30
+ subaddress_index?: number;
31
+ secret_view_key?: string;
32
+ halted?: boolean;
33
+ secret_spend_key?: string;
34
+ wallet_route?: string;
35
+ wallet_name?: string;
36
+ wallet_slot?: number;
37
+ };
38
+ export type ScanSettings = {
39
+ wallets: ScanSetting[];
40
+ node_url: string;
41
+ start_height: number | null;
42
+ merchant_confirmations?: number | null;
43
+ cpu_worker_count?: number;
44
+ logs?: LogSetting;
45
+ logs_include?: PossibleLogs[];
46
+ logs_exclude?: PossibleLogs[];
47
+ };
48
+ export type ScanSettingsOpened = {
49
+ wallets: (ScanSettingOpened | undefined)[];
50
+ node_url: string;
51
+ start_height: number | null;
52
+ merchant_confirmations?: number | null;
53
+ cpu_worker_count?: number;
54
+ logs?: LogSetting;
55
+ logs_include?: PossibleLogs[];
56
+ logs_exclude?: PossibleLogs[];
57
+ };
58
+ /**
59
+ * Writes scan settings to the default or specified storage file in json.
60
+ *
61
+ * @example
62
+ * ```
63
+ * const settings: ScanSettings = {
64
+ * wallets: [{
65
+ * primary_address: "5dsf...",
66
+ * start_height: 1741707,
67
+ * }],
68
+ * node_urls: ["https://monerooo.roooo"]
69
+ * };
70
+ * await writeScanSettings(settings);
71
+ * ```
72
+ *
73
+ * @param scan_settings - The complete {@link ScanSettings} configuration to persist.
74
+ * @param scan_settings_path - Optional path for the settings file. Defaults to `SCAN_SETTINGS_STORE_NAME_DEFAULT`.
75
+ * @returns A promise that resolves when the file is successfully written.
76
+ * @throws Will throw if file writing fails (e.g., permissions, disk space).
77
+ */
78
+ export declare function writeScanSettings(scan_settings: ScanSettings, scan_settings_path?: string): Promise<number>;
79
+ export type WriteScanSettingsFileParams = {
80
+ scan_settings_path?: string;
81
+ writeCallback: (settings: ScanSettings) => void | Promise<void>;
82
+ };
83
+ export declare function writeScanSettingsFileDefaultLocation(params: WriteScanSettingsFileParams): Promise<void>;
84
+ export declare function writeStartHeightToScanSettings(start_height: number | null, scan_settings_path?: string): Promise<number>;
85
+ export declare function writeDaemonHeightAsStartHeightToScanSettings(node_url: string, scan_settings_path?: string): Promise<number>;
86
+ export declare function cullTooLargeScanHeight(node_url: string, scan_settings_path?: string): Promise<number>;
87
+ export declare function writeNodeUrlToScanSettings(node_url: string, scan_settings_path?: string): Promise<number>;
88
+ export declare function readNodeUrlFromScanSettings(scan_settings_path?: string): Promise<string | undefined>;
89
+ /**
90
+ * Writes the merchant confirmation threshold to scan settings.
91
+ *
92
+ * This setting only has effect when the wallet is run as part of a checkout system,
93
+ * that accepts payments.
94
+ * It determines after how many confirmations a transaction is accepted
95
+ * for a payment. The actual enforcement of this policy is left to the
96
+ * custom payment-system code that consumes this library.
97
+ *
98
+ * @param merchant_confirmations - Number of confirmations required before
99
+ * accepting a payment, or `null` to leave it unset.
100
+ * @param scan_settings_path - Optional path for the settings file.
101
+ * Defaults to `SCAN_SETTINGS_STORE_NAME_DEFAULT`.
102
+ * @returns A promise that resolves when the file is successfully written.
103
+ */
104
+ export declare function writeMerchantConfirmationsToScanSettings(merchant_confirmations: number | null, scan_settings_path?: string): Promise<number>;
105
+ /**
106
+ * Reads the merchant confirmation threshold from scan settings.
107
+ *
108
+ * This setting only has effect when the wallet is run as part of a checkout system,
109
+ * that accepts payments.
110
+ *
111
+ * It determines after how many confirmations a transaction is accepted
112
+ * for a payment. The actual enforcement of this policy is left to the
113
+ * custom payment-system code that consumes this library.
114
+ *
115
+ * @param scan_settings_path - Optional path for the settings file.
116
+ * Defaults to `SCAN_SETTINGS_STORE_NAME_DEFAULT`.
117
+ * @returns The configured number of confirmations, null, or `undefined`
118
+ * if the setting has not been persisted yet.
119
+ */
120
+ export declare function readMerchantConfirmationsFromScanSettings(scan_settings_path?: string): Promise<number | null | undefined>;
121
+ /**
122
+ * Reads scan settings from the default or specified storage file.
123
+ * secret_view_key and spend_private_key are read from environment variables
124
+ *
125
+ * @example
126
+ * ```
127
+ * const settings = await readScanSettings();
128
+ * if (settings) {
129
+ * console.log(settings.wallets?.primary_address);
130
+ * }
131
+ * ```
132
+ *
133
+ * @param scan_settings_path - Path to the settings file. Defaults to `SCAN_SETTINGS_STORE_NAME_DEFAULT`.
134
+ * @returns The parsed {@link ScanSettings} object if file exists and is valid JSON, otherwise `undefined`.
135
+ */
136
+ export declare function readScanSettings(scan_settings_path?: string): Promise<ScanSettingsOpened | undefined>;
137
+ export declare function readPrivateSpendKeyFromEnv(primary_address: string): string | undefined;
138
+ export declare function readPrivateViewKeyFromEnv(primary_address: string): string | undefined;
139
+ export declare function readWalletFromScanSettings(primary_address: string, scan_settings_path?: string): Promise<ScanSettingOpened | undefined>;
140
+ export declare function readWalletsFromScanSettings(scan_settings_path?: string): Promise<ScanSettingOpened[]>;
141
+ export declare function walletSettingsPlusKeys(wallet_settings: ScanSettingOpened, secret_view_key?: string, secret_spend_key?: string): Promise<{
142
+ secret_view_key: string;
143
+ secret_spend_key: string | undefined;
144
+ primary_address: string;
145
+ start_height: number | null;
146
+ node_url: string;
147
+ subaddress_index?: number;
148
+ halted?: boolean;
149
+ wallet_route?: string;
150
+ wallet_name?: string;
151
+ wallet_slot?: number;
152
+ }>;
153
+ export declare function writeWalletToScanSettings(params: WriteScanSettingParams): Promise<number>;
154
+ export declare function openScanSettingsFile(scan_settings_path?: string): Promise<ScanSettings | undefined>;
155
+ export declare function openNonHaltedWallets(scan_settings_path?: string): Promise<ScanSettingOpened[]>;
156
+ export declare function getNonHaltedWallets(scan_settings?: ScanSettings): ScanSetting[];
157
+ export declare function doesScanSettingsFileExist(scan_settings_path?: string): Promise<boolean>;
158
+ /**
159
+ * Either 1. the consumer explicitly sets a pathprefix, we extract it from the scan_settings_path
160
+ * or we pick the default which is nothing
161
+ * @param scan_settings_path optional scan settings path
162
+ * @param pathPrefix optional path prefix
163
+ * @returns definite path prefix
164
+ */
165
+ export declare function getPathPrefix(scan_settings_path?: string, pathPrefix?: string): string;