@spirobel/monero-wallet-api 0.2.0 → 0.3.1

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 (68) hide show
  1. package/README.md +3 -3
  2. package/dist/api.d.ts +25 -96
  3. package/dist/api.js +23 -175
  4. package/dist/io/BunFileInterface.d.ts +32 -0
  5. package/dist/io/atomicWrite.d.ts +2 -0
  6. package/dist/io/atomicWrite.js +10 -0
  7. package/dist/io/extension.d.ts +18 -0
  8. package/dist/io/extension.js +11 -0
  9. package/dist/io/indexedDB.d.ts +45 -0
  10. package/dist/io/indexedDB.js +221 -0
  11. package/dist/io/readDir.d.ts +1 -0
  12. package/dist/io/readDir.js +7 -0
  13. package/dist/io/sleep.d.ts +1 -0
  14. package/dist/io/sleep.js +1 -0
  15. package/dist/keypairs-seeds/keypairs.d.ts +29 -0
  16. package/dist/keypairs-seeds/keypairs.js +207 -0
  17. package/dist/keypairs-seeds/writeKeypairs.d.ts +12 -0
  18. package/dist/keypairs-seeds/writeKeypairs.js +78 -0
  19. package/dist/node-interaction/binaryEndpoints.d.ts +59 -14
  20. package/dist/node-interaction/binaryEndpoints.js +110 -54
  21. package/dist/node-interaction/jsonEndpoints.d.ts +249 -187
  22. package/dist/node-interaction/jsonEndpoints.js +287 -0
  23. package/dist/node-interaction/nodeUrl.d.ts +129 -0
  24. package/dist/node-interaction/nodeUrl.js +113 -0
  25. package/dist/scanning-syncing/backgroundWorker.d.ts +6 -0
  26. package/dist/scanning-syncing/backgroundWorker.js +56 -0
  27. package/dist/scanning-syncing/connectionStatus.d.ts +15 -0
  28. package/dist/scanning-syncing/connectionStatus.js +35 -0
  29. package/dist/scanning-syncing/openWallet.d.ts +28 -0
  30. package/dist/scanning-syncing/openWallet.js +57 -0
  31. package/dist/scanning-syncing/scanSettings.d.ts +96 -0
  32. package/dist/scanning-syncing/scanSettings.js +240 -0
  33. package/dist/scanning-syncing/scanresult/computeKeyImage.d.ts +3 -0
  34. package/dist/scanning-syncing/scanresult/computeKeyImage.js +21 -0
  35. package/dist/scanning-syncing/scanresult/getBlocksbinBuffer.d.ts +28 -0
  36. package/dist/scanning-syncing/scanresult/getBlocksbinBuffer.js +52 -0
  37. package/dist/scanning-syncing/scanresult/reorg.d.ts +14 -0
  38. package/dist/scanning-syncing/scanresult/reorg.js +78 -0
  39. package/dist/scanning-syncing/scanresult/scanCache.d.ts +84 -0
  40. package/dist/scanning-syncing/scanresult/scanCache.js +134 -0
  41. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +149 -0
  42. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +648 -0
  43. package/dist/scanning-syncing/scanresult/scanResult.d.ts +64 -0
  44. package/dist/scanning-syncing/scanresult/scanResult.js +213 -0
  45. package/dist/scanning-syncing/scanresult/scanStats.d.ts +60 -0
  46. package/dist/scanning-syncing/scanresult/scanStats.js +273 -0
  47. package/dist/scanning-syncing/worker-entrypoints/worker.d.ts +1 -0
  48. package/dist/scanning-syncing/worker-entrypoints/worker.js +8 -0
  49. package/dist/scanning-syncing/worker-mains/worker.d.ts +1 -0
  50. package/dist/scanning-syncing/worker-mains/worker.js +7 -0
  51. package/dist/send-functionality/conversion.d.ts +4 -0
  52. package/dist/send-functionality/conversion.js +75 -0
  53. package/dist/send-functionality/inputSelection.d.ts +13 -0
  54. package/dist/send-functionality/inputSelection.js +8 -0
  55. package/dist/send-functionality/transactionBuilding.d.ts +51 -0
  56. package/dist/send-functionality/transactionBuilding.js +111 -0
  57. package/dist/tools/monero-tools.d.ts +46 -0
  58. package/dist/tools/monero-tools.js +165 -0
  59. package/dist/viewpair/ViewPair.d.ts +157 -0
  60. package/dist/viewpair/ViewPair.js +346 -0
  61. package/dist/wasm-processing/wasi.js +1 -2
  62. package/dist/wasm-processing/wasmFile.d.ts +1 -1
  63. package/dist/wasm-processing/wasmFile.js +2 -2
  64. package/dist/wasm-processing/wasmProcessor.d.ts +16 -4
  65. package/dist/wasm-processing/wasmProcessor.js +23 -7
  66. package/package.json +29 -6
  67. package/dist/testscrap.js +0 -36
  68. /package/dist/{testscrap.d.ts → io/BunFileInterface.js} +0 -0
@@ -0,0 +1,648 @@
1
+ import { atomicWrite, NodeUrl, signTransaction, ViewPair, } from "../../api";
2
+ import { prepareInput, sumPayments, } from "../../send-functionality/inputSelection";
3
+ import { createWebworker } from "../backgroundWorker";
4
+ import { spendable } from "./scanResult";
5
+ import { openScanSettingsFile, readPrivateSpendKeyFromEnv, readWalletFromScanSettings, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, writeNodeUrlToScanSettings, writeStartHeightToScanSettings, writeWalletToScanSettings, } from "../scanSettings";
6
+ import { findRange, lastRange, readCacheFileDefaultLocation, writeCacheFileDefaultLocationThrows, } from "./scanCache";
7
+ import { alignScanStatsWithCache, isSelfSpent, processTxlogInputs, processTxlogPayments, writeStatsFileDefaultLocation, } from "./scanStats";
8
+ import { readWriteConnectionStatusFile, } from "../connectionStatus";
9
+ export class ScanCacheOpened {
10
+ view_pair;
11
+ wallet_route;
12
+ no_worker;
13
+ masterCacheChanged;
14
+ _start_height;
15
+ scan_settings_path;
16
+ pathPrefix;
17
+ workerError;
18
+ static async create(params) {
19
+ const theCatchToBeOpened = await readCacheFileDefaultLocation(params.primary_address, params.pathPrefix);
20
+ const walletSettings = await readWalletFromScanSettings(params.primary_address, params.scan_settings_path);
21
+ if (!walletSettings)
22
+ throw new Error(`wallet not found in settings. did you call openwallet with the right params?
23
+ Either wrong file name supplied to params.scan_settings_path: ${params.scan_settings_path}
24
+ Or wrong primary_address supplied params.primary_address: ${params.primary_address}`);
25
+ if (!params.primary_address)
26
+ throw new Error(`primary_address is required, potentially half filled out wallet setting in: ${params.scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT}`);
27
+ // read secret_view_key and secret_spend_key from env
28
+ const walletSettingsWithKeys = await walletSettingsPlusKeys(walletSettings);
29
+ // create viewpair + ScanCacheOpened instance
30
+ const scanCacheOpen = new ScanCacheOpened(await ViewPair.create(params.primary_address, walletSettingsWithKeys.secret_view_key, walletSettings.subaddress_index, walletSettings.node_url), walletSettings.wallet_route, params.no_worker || false, params.masterCacheChanged || null, walletSettings.start_height, params.scan_settings_path, params.pathPrefix, params.workerError);
31
+ if (theCatchToBeOpened)
32
+ scanCacheOpen._cache = theCatchToBeOpened;
33
+ if (!walletSettings.halted) {
34
+ // run webworker (respecting halted param + setting)
35
+ // unpause will start scanning from this.wallet_scan_settings.start_height
36
+ await scanCacheOpen.unpause();
37
+ }
38
+ scanCacheOpen._stats = await alignScanStatsWithCache(scanCacheOpen._cache, scanCacheOpen.view_pair, params.primary_address, params.pathPrefix, walletSettings.subaddress_index, lastRange(scanCacheOpen._cache.scanned_ranges)?.end);
39
+ return scanCacheOpen;
40
+ }
41
+ get start_height() {
42
+ return this._start_height;
43
+ }
44
+ get current_height() {
45
+ let current_range = findRange(this._cache.scanned_ranges, this._start_height || 0);
46
+ return current_range?.end || null;
47
+ }
48
+ get current_top_range_height() {
49
+ if (typeof this._stats === "undefined" || this._stats === null)
50
+ return null;
51
+ return this._stats.height;
52
+ }
53
+ async changeStartHeight(start_height) {
54
+ if (this.worker) {
55
+ this.worker.terminate();
56
+ delete this.worker;
57
+ }
58
+ await writeStartHeightToScanSettings(start_height, this.scan_settings_path);
59
+ this._start_height = start_height;
60
+ await this.unpause();
61
+ }
62
+ get cache() {
63
+ return this._cache;
64
+ }
65
+ get prepending_txs() {
66
+ const txs = [];
67
+ for (const txlog of this._cache.tx_logs || []) {
68
+ if (!txlog ||
69
+ !txlog.sendResult ||
70
+ (txlog.sendResult && txlog.sendResult.status !== "OK"))
71
+ continue;
72
+ const { inputSum, alreadyRecognizedAsSpend } = processTxlogInputs(txlog, this._cache);
73
+ if (alreadyRecognizedAsSpend)
74
+ continue;
75
+ const outWardPaymentSum = processTxlogPayments(txlog, this._cache);
76
+ const self_spent = isSelfSpent(txlog.payments[0].address, this._cache);
77
+ const destination_address = txlog.payments[0].address;
78
+ const inputs = [];
79
+ for (const inputId of txlog.inputs_index) {
80
+ const input = this._cache.outputs[inputId];
81
+ inputs.push(input);
82
+ }
83
+ const typical_fee = 1000000000n; // 0.001 XMR
84
+ const amount = -outWardPaymentSum - typical_fee;
85
+ const prepending_tx = {
86
+ amount,
87
+ txlog,
88
+ inputSum,
89
+ outWardPaymentSum,
90
+ self_spent,
91
+ destination_address,
92
+ inputs,
93
+ };
94
+ txs.push(prepending_tx);
95
+ }
96
+ return txs;
97
+ }
98
+ get transactions() {
99
+ if (typeof this._stats === "undefined" || this._stats === null)
100
+ return [];
101
+ const transactions = [];
102
+ for (const tx of this._stats?.ordered_transactions) {
103
+ transactions.push(this._stats.found_transactions[tx]);
104
+ }
105
+ return transactions;
106
+ }
107
+ get primary_address() {
108
+ return this.view_pair.primary_address;
109
+ }
110
+ get node_url() {
111
+ return this.view_pair.node_url;
112
+ }
113
+ set node_url(nu) {
114
+ this.view_pair.node_url = nu;
115
+ }
116
+ async changeNodeUrlAndStartHeight(node_url, start_height) {
117
+ if (this.worker) {
118
+ this.worker.terminate();
119
+ delete this.worker;
120
+ }
121
+ if (node_url !== undefined) {
122
+ await writeNodeUrlToScanSettings(node_url, this.scan_settings_path);
123
+ this.node_url = node_url;
124
+ }
125
+ if (start_height !== undefined) {
126
+ await writeStartHeightToScanSettings(start_height, this.scan_settings_path);
127
+ this._start_height = start_height;
128
+ }
129
+ await this.unpause();
130
+ }
131
+ async changeNodeUrl(node_url) {
132
+ if (this.worker) {
133
+ this.worker.terminate();
134
+ delete this.worker;
135
+ }
136
+ await writeNodeUrlToScanSettings(node_url, this.scan_settings_path);
137
+ this.node_url = node_url;
138
+ await this.unpause();
139
+ }
140
+ async retry() {
141
+ if (this.worker) {
142
+ this.worker.terminate();
143
+ delete this.worker;
144
+ }
145
+ const scan_settings = await readWalletFromScanSettings(this.primary_address, this.scan_settings_path).catch(() => false);
146
+ if (scan_settings) {
147
+ // if there is no scan settings file,
148
+ // the retry loop is stopped.
149
+ // the wallet reset happens through deleting all the scan setting + cache files
150
+ // we want any background retry loops to stop in this case
151
+ //TODO ? write connection status retry
152
+ await this.unpause();
153
+ }
154
+ return scan_settings ? true : false;
155
+ }
156
+ async sendTransaction(signedTx) {
157
+ const node = await NodeUrl.create(this.node_url);
158
+ return await node.sendRawTransaction(signedTx);
159
+ }
160
+ async signTransaction(unsignedTx) {
161
+ const privateSpendKey = readPrivateSpendKeyFromEnv(this._cache.primary_address);
162
+ if (!privateSpendKey)
163
+ throw new Error("privateSpendKey not found in env");
164
+ return await signTransaction(unsignedTx, privateSpendKey);
165
+ }
166
+ async calculateFeeAndSelectInputs(params) {
167
+ const sum = sumPayments(params.payments);
168
+ const node = await NodeUrl.create(this.node_url);
169
+ // 1. get fee estimate
170
+ const feeEstimate = await node.getFeeEstimate();
171
+ const feePerByte = BigInt(feeEstimate.fees[0]);
172
+ if (!params.no_fee_circuit_breaker) {
173
+ // default is false / undefined -> use fee circuit breaker
174
+ const max_plausible_fee = 20000000000n; // 0.02 XMR
175
+ const feeFor10kb = feePerByte * 10000n;
176
+ //2. check if fee is too high
177
+ if (feeFor10kb > max_plausible_fee) {
178
+ throw new Error(`fee too high:
179
+ ${feeFor10kb} (fee for 10kb tx size) > ${max_plausible_fee} (0.001 XMR)
180
+ most likely your node is faulty. connect to another node.
181
+ preferably run one yourself locally.`);
182
+ }
183
+ }
184
+ // 3. select inputs TODO: log inputs indices
185
+ const selectedInputs = params.inputs || this.selectInputs(sum, feePerByte);
186
+ if (!selectedInputs.length)
187
+ throw new Error("not enough funds");
188
+ return { selectedInputs, feeEstimate };
189
+ }
190
+ async makeTransactionFromSelectedInputs(payments, selectedInputs, feeEstimate) {
191
+ // 4. get output distribution
192
+ const node = await NodeUrl.create(this.node_url);
193
+ const distibution = await node.getOutputDistribution();
194
+ const preparedInputs = [];
195
+ for (const input of selectedInputs) {
196
+ // 5. sample decoys & get outs from node: here is where a privacy compromising event could happen
197
+ preparedInputs.push(prepareInput(node, distibution, input));
198
+ }
199
+ const inputs = [];
200
+ for (const preparedInput of preparedInputs) {
201
+ // 6. make input: combine output with sampled and verified unlocked decoys
202
+ const input = node.makeInput(preparedInput.input, preparedInput.sample.candidates, await preparedInput.outsResponse);
203
+ inputs.push(input);
204
+ }
205
+ // 7. make transaction: combine inputs, payments + fee info
206
+ const unsignedTx = this.view_pair.makeTransaction({
207
+ inputs,
208
+ payments,
209
+ fee_response: feeEstimate,
210
+ fee_priority: "unimportant",
211
+ });
212
+ return unsignedTx;
213
+ }
214
+ /**
215
+ * this function returns the unsigned transaction, throws {@link SendError}
216
+ */
217
+ async makeTransaction(params) {
218
+ const { selectedInputs, feeEstimate } = await this.calculateFeeAndSelectInputs(params);
219
+ return await this.makeTransactionFromSelectedInputs(params.payments, selectedInputs, feeEstimate);
220
+ }
221
+ get daemon_height() {
222
+ return this._cache.daemon_height;
223
+ }
224
+ get amount() {
225
+ return this._stats?.total_spendable_amount || 0n;
226
+ }
227
+ get pending_amount() {
228
+ return this._stats?.total_pending_amount || 0n;
229
+ }
230
+ get subaddresses() {
231
+ return Object.values(this._stats?.subaddresses || {});
232
+ }
233
+ get tx_logs() {
234
+ return this._cache.tx_logs || [];
235
+ }
236
+ async makeSignSendTransaction(params) {
237
+ let maybeInputs = [];
238
+ let maybeFeeEstimate;
239
+ let maybeSendResult;
240
+ try {
241
+ const { selectedInputs, feeEstimate } = await this.calculateFeeAndSelectInputs(params);
242
+ maybeInputs = selectedInputs;
243
+ maybeFeeEstimate = feeEstimate;
244
+ const unsignedTx = await this.makeTransactionFromSelectedInputs(params.payments, selectedInputs, feeEstimate);
245
+ const signedTx = await this.signTransaction(unsignedTx);
246
+ const sendResult = await this.sendTransaction(signedTx);
247
+ maybeSendResult = sendResult;
248
+ if (sendResult.status !== "OK")
249
+ throw new Error("send raw transaction rpc returned error");
250
+ // before writing the scan cache, we stop the worker to avoid a race
251
+ if (this.worker) {
252
+ this.worker.terminate();
253
+ delete this.worker;
254
+ }
255
+ // write txlog to cache + update pending_spent_utxos (affects stats + spendability)
256
+ await writeCacheFileDefaultLocationThrows({
257
+ primary_address: this.primary_address,
258
+ pathPrefix: this.pathPrefix,
259
+ writeCallback: async (cache) => {
260
+ if (!cache.tx_logs)
261
+ cache.tx_logs = [];
262
+ if (!cache.pending_spent_utxos)
263
+ cache.pending_spent_utxos = {};
264
+ const inputs_index = selectedInputs.map((input) => String(input.index_on_blockchain));
265
+ const txLog = {
266
+ sendResult,
267
+ feeEstimate,
268
+ payments: params.payments,
269
+ node_url: this.node_url,
270
+ inputs_index,
271
+ height: this.current_height,
272
+ timestamp: Date.now(),
273
+ };
274
+ const newLen = cache.tx_logs.push(txLog);
275
+ const txLogIndex = newLen - 1;
276
+ for (const inputId of inputs_index) {
277
+ cache.pending_spent_utxos[inputId] = txLogIndex;
278
+ }
279
+ },
280
+ });
281
+ const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
282
+ if (!newCache)
283
+ throw new Error(`cache not found for primary address: ${this.primary_address}, and path prefix: ${this.pathPrefix}`);
284
+ const changed_outputs = selectedInputs.map((input) => ({
285
+ change_reason: "spent",
286
+ output: input,
287
+ }));
288
+ await this.feed({
289
+ newCache,
290
+ changed_outputs,
291
+ });
292
+ // restart the worker
293
+ await this.unpause();
294
+ return sendResult;
295
+ }
296
+ catch (e) {
297
+ // before writing the scan cache, we stop the worker to avoid a race
298
+ if (this.worker) {
299
+ this.worker.terminate();
300
+ delete this.worker;
301
+ }
302
+ // write txlog error to cache
303
+ await writeCacheFileDefaultLocationThrows({
304
+ primary_address: this.primary_address,
305
+ pathPrefix: this.pathPrefix,
306
+ writeCallback: async (cache) => {
307
+ if (!cache.tx_logs)
308
+ cache.tx_logs = [];
309
+ if (!cache.pending_spent_utxos)
310
+ cache.pending_spent_utxos = {};
311
+ const inputs_index = maybeInputs.map((input) => String(input.index_on_blockchain));
312
+ const txLog = {
313
+ sendResult: maybeSendResult,
314
+ error: String(e || "unknown error"),
315
+ feeEstimate: maybeFeeEstimate,
316
+ payments: params.payments,
317
+ node_url: this.node_url,
318
+ inputs_index,
319
+ height: this.current_height,
320
+ timestamp: Date.now(),
321
+ };
322
+ const newLen = cache.tx_logs.push(txLog);
323
+ },
324
+ });
325
+ const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
326
+ if (!newCache)
327
+ throw new Error(`cache not found for primary address: ${this.primary_address}, and path prefix: ${this.pathPrefix}`);
328
+ const changed_outputs = maybeInputs.map((input) => ({
329
+ change_reason: "spent",
330
+ output: input,
331
+ }));
332
+ await this.feed({
333
+ newCache,
334
+ changed_outputs,
335
+ });
336
+ // restart the worker
337
+ await this.unpause();
338
+ throw e;
339
+ }
340
+ }
341
+ /**
342
+ * makeStandardTransaction
343
+ */
344
+ makeStandardTransaction(destination_address, amount) {
345
+ return this.makeTransaction({
346
+ payments: [{ address: destination_address, amount }],
347
+ });
348
+ }
349
+ /**
350
+ * makeIntegratedAddress
351
+ */
352
+ makeIntegratedAddress(paymentId) {
353
+ return this.view_pair.makeIntegratedAddress(paymentId);
354
+ }
355
+ /**
356
+ * This method makes a Subaddress for the Address of the Viewpair it was opened with.
357
+ * The network (mainnet, stagenet, testnet) is the same as the one of the Viewpairaddress.
358
+ * will increment minor by 1 on major 0 in "ScanSettings.json" subaddresses definition
359
+ *
360
+ * if there is an active scan going on, call this here on ScanCacheOpened, so the new subaddress will be scanned
361
+ * (and not on a viewpair / scancacheopened instance that is not conducting the scan, aka where no_worker is true)
362
+ *
363
+ * @returns Adressstring
364
+ */
365
+ async makeSubaddress() {
366
+ const walletSettings = await readWalletFromScanSettings(this.view_pair.primary_address, this.scan_settings_path);
367
+ if (!walletSettings)
368
+ throw new Error(`wallet not found in settings. did you call openwallet with the right params?
369
+ Either wrong file name supplied to params.scan_settings_path: ${this.scan_settings_path}
370
+ Or wrong primary_address supplied params.primary_address: ${this.view_pair.primary_address}`);
371
+ const last_subaddress_index = walletSettings.subaddress_index || 0;
372
+ const minor = last_subaddress_index + 1;
373
+ const subaddress = this.view_pair.makeSubaddress(minor);
374
+ await writeWalletToScanSettings({
375
+ primary_address: this.view_pair.primary_address,
376
+ subaddress_index: minor,
377
+ scan_settings_path: this.scan_settings_path,
378
+ });
379
+ const created_at_height = lastRange(this._cache.scanned_ranges)?.end || 0;
380
+ const created_at_timestamp = new Date().getTime();
381
+ const new_subaddress = {
382
+ minor,
383
+ address: subaddress,
384
+ created_at_height,
385
+ created_at_timestamp,
386
+ not_yet_included: true,
387
+ };
388
+ this._stats = await writeStatsFileDefaultLocation({
389
+ primary_address: this.primary_address,
390
+ pathPrefix: this.pathPrefix,
391
+ writeCallback: async (stats) => {
392
+ stats.subaddresses[minor.toString()] = new_subaddress;
393
+ },
394
+ });
395
+ return new_subaddress;
396
+ }
397
+ /**
398
+ * notify
399
+ *
400
+ * ChangeReason = "added" | "ownspend" | "reorged" | "burned";
401
+ */
402
+ //TODO PAUSE NOTIFY listner and node status / connection error
403
+ notify(callback) {
404
+ this.notifyListeners.push(callback);
405
+ const id = this.notifyListeners.length - 1;
406
+ return {
407
+ remove: () => (this.notifyListeners[id] = null),
408
+ };
409
+ }
410
+ async pause() {
411
+ if (this.worker)
412
+ this.worker.terminate();
413
+ return await writeWalletToScanSettings({
414
+ primary_address: this.view_pair.primary_address,
415
+ halted: true,
416
+ scan_settings_path: this.scan_settings_path,
417
+ });
418
+ }
419
+ stopWorker() {
420
+ if (this.worker) {
421
+ this.worker.terminate();
422
+ delete this.worker;
423
+ }
424
+ }
425
+ async unpause() {
426
+ // if worker does not exist yet, start it (if we are not slave / no_worker)
427
+ if (!this.worker && !this.no_worker) {
428
+ this.worker = createWebworker(async (params) => await this.feed(params), this.scan_settings_path, this.pathPrefix, (error) => {
429
+ const workerErrCB = this.workerError;
430
+ readWriteConnectionStatusFile((cs) => {
431
+ if (cs?.last_packet.status === "catastrophic_reorg")
432
+ return;
433
+ const connectionStatus = {
434
+ last_packet: {
435
+ status: "connection_failed",
436
+ bytes_read: 0,
437
+ node_url: this.node_url,
438
+ timestamp: new Date().toISOString(),
439
+ },
440
+ };
441
+ return connectionStatus;
442
+ }).then(() => {
443
+ if (workerErrCB)
444
+ workerErrCB(error);
445
+ });
446
+ });
447
+ }
448
+ return await writeWalletToScanSettings({
449
+ primary_address: this.view_pair.primary_address,
450
+ halted: false,
451
+ scan_settings_path: this.scan_settings_path,
452
+ });
453
+ }
454
+ /**
455
+ * selectInputs returns array of inputs, whose sum is larger than amount
456
+ * adds approximate fee for 10kb transaction to amount if feePerByte is supplied
457
+ */
458
+ selectInputs(amount, feePerByte) {
459
+ if (feePerByte)
460
+ amount += feePerByte * 10000n; // 10kb * feePerByte; for sweeping low amounts inputs[] supplied directly
461
+ const oneInputIsEnough = this.selectOneInput(amount);
462
+ if (oneInputIsEnough)
463
+ return [oneInputIsEnough];
464
+ return this.selectMultipleInputs(amount);
465
+ }
466
+ /**
467
+ * selectOneInput larger than amount, (smallest one matching this amount)
468
+ */
469
+ selectOneInput(amount) {
470
+ return this.spendableInputs()
471
+ .filter((output) => output.amount >= amount)
472
+ .sort((a, b) => b.amount > a.amount ? -1 : b.amount < a.amount ? 1 : 0)[0];
473
+ }
474
+ /**
475
+ * selectMultipleInputs larger than amount, sorted from largest to smallest until total reaches amount
476
+ */
477
+ selectMultipleInputs(amount) {
478
+ const selected = [];
479
+ let total = 0n;
480
+ for (const output of this.spendableInputs()) {
481
+ selected.push(output);
482
+ total += output.amount;
483
+ if (total >= amount)
484
+ return selected;
485
+ }
486
+ return [];
487
+ }
488
+ /**
489
+ * get spendableInputs
490
+ */
491
+ spendableInputs() {
492
+ return Object.values(this._cache.outputs)
493
+ .filter((output) => spendable(output, this._cache, this.current_height || 0))
494
+ .sort((a, b) => (a.amount > b.amount ? -1 : a.amount < b.amount ? 1 : 0));
495
+ }
496
+ /**
497
+ * feed the ScanCacheOpened with new ScanCache as syncing happens
498
+ * if primary_address does not match, do not feed
499
+ * if masterCacheChanged is set, it will be called here
500
+ * for all primary addresses
501
+ */
502
+ async feed(params) {
503
+ //TODO update aggregated amount stats + height
504
+ if (this.masterCacheChanged)
505
+ this.masterCacheChanged(params);
506
+ if (this.view_pair.primary_address !== params.newCache.primary_address)
507
+ return;
508
+ this._cache = params.newCache;
509
+ this._stats = await alignScanStatsWithCache(this._cache, this.view_pair, this.primary_address, this.pathPrefix, undefined, lastRange(this._cache.scanned_ranges)?.end);
510
+ for (const listener of this.notifyListeners) {
511
+ if (listener)
512
+ listener(params);
513
+ }
514
+ }
515
+ _cache = {
516
+ daemon_height: 0,
517
+ outputs: {},
518
+ own_key_images: {},
519
+ scanned_ranges: [],
520
+ primary_address: "",
521
+ };
522
+ worker = undefined;
523
+ constructor(view_pair, wallet_route, no_worker, masterCacheChanged, _start_height, scan_settings_path, pathPrefix, workerError) {
524
+ this.view_pair = view_pair;
525
+ this.wallet_route = wallet_route;
526
+ this.no_worker = no_worker;
527
+ this.masterCacheChanged = masterCacheChanged;
528
+ this._start_height = _start_height;
529
+ this.scan_settings_path = scan_settings_path;
530
+ this.pathPrefix = pathPrefix;
531
+ this.workerError = workerError;
532
+ }
533
+ _stats = null;
534
+ notifyListeners = [];
535
+ }
536
+ export class ManyScanCachesOpened {
537
+ wallets;
538
+ get start_height() {
539
+ if (this.wallets.length === 0)
540
+ return null;
541
+ return this.wallets[0]?.start_height;
542
+ }
543
+ get current_height() {
544
+ if (this.wallets.length === 0)
545
+ return null;
546
+ return this.wallets[0]?.current_height;
547
+ }
548
+ get node_url() {
549
+ if (this.wallets.length === 0)
550
+ return "";
551
+ return this.wallets[0]?.node_url;
552
+ }
553
+ async changeNodeUrlAndStartHeight(node_url, start_height) {
554
+ if (this.wallets.length === 0)
555
+ throw new Error("no wallets");
556
+ const masterWallet = this.wallets[0];
557
+ return await masterWallet.changeNodeUrlAndStartHeight(node_url, start_height);
558
+ }
559
+ async retry() {
560
+ if (this.wallets.length === 0)
561
+ throw new Error("no wallets");
562
+ const masterWallet = this.wallets[0];
563
+ return await masterWallet.retry();
564
+ }
565
+ stopWorker() {
566
+ if (this.wallets.length === 0)
567
+ throw new Error("no wallets");
568
+ const masterWallet = this.wallets[0];
569
+ return masterWallet.stopWorker();
570
+ }
571
+ async changeNodeUrl(node_url) {
572
+ if (this.wallets.length === 0)
573
+ throw new Error("no wallets");
574
+ const masterWallet = this.wallets[0];
575
+ return await masterWallet.changeNodeUrl(node_url);
576
+ }
577
+ async changeStartHeight(start_height) {
578
+ if (this.wallets.length === 0)
579
+ throw new Error("no wallets");
580
+ const masterWallet = this.wallets[0];
581
+ return await masterWallet.changeStartHeight(start_height);
582
+ }
583
+ static async create({ scan_settings_path, pathPrefix, no_worker, notifyMasterChanged, no_stats, workerError, }) {
584
+ const scan_settings = await openScanSettingsFile(scan_settings_path);
585
+ if (!scan_settings?.wallets)
586
+ throw new Error(`no wallets in settings file. Did you supply the right path?
587
+ are there wallets in the default '${SCAN_SETTINGS_STORE_NAME_DEFAULT}' file?`);
588
+ const nonHaltedWallets = scan_settings.wallets.filter((wallet) => !wallet?.halted);
589
+ if (!nonHaltedWallets.length)
590
+ return undefined;
591
+ const openedWallets = [];
592
+ const firstNonHaltedWallet = nonHaltedWallets[0];
593
+ if (nonHaltedWallets.length > 1) {
594
+ const slaveWallets = [];
595
+ for (const wallet of nonHaltedWallets.slice(1)) {
596
+ if (!wallet || wallet.halted)
597
+ continue;
598
+ const slaveWallet = await ScanCacheOpened.create({
599
+ ...wallet,
600
+ no_worker: true, // slaves depend on master worker
601
+ scan_settings_path,
602
+ pathPrefix,
603
+ no_stats,
604
+ });
605
+ slaveWallets.push(slaveWallet);
606
+ }
607
+ const masterWallet = await ScanCacheOpened.create({
608
+ ...firstNonHaltedWallet,
609
+ masterCacheChanged: async (params) => {
610
+ notifyMasterChanged?.(params);
611
+ for (const slave of slaveWallets) {
612
+ await slave.feed(params);
613
+ }
614
+ },
615
+ scan_settings_path,
616
+ pathPrefix,
617
+ no_stats,
618
+ no_worker, // pass no_worker, if you want to manually feed()
619
+ workerError,
620
+ });
621
+ openedWallets.push(masterWallet, ...slaveWallets);
622
+ }
623
+ else {
624
+ const onlyWallet = await ScanCacheOpened.create({
625
+ masterCacheChanged: async (params) => {
626
+ notifyMasterChanged?.(params);
627
+ },
628
+ ...firstNonHaltedWallet,
629
+ scan_settings_path,
630
+ pathPrefix,
631
+ no_stats,
632
+ no_worker, // pass no_worker, if you want to manually feed()
633
+ workerError,
634
+ });
635
+ openedWallets.push(onlyWallet);
636
+ }
637
+ return new ManyScanCachesOpened(openedWallets);
638
+ }
639
+ /**
640
+ * feed the master wallet and therefore all wallets
641
+ */
642
+ async feed(params) {
643
+ await this.wallets[0].feed(params);
644
+ }
645
+ constructor(wallets) {
646
+ this.wallets = wallets;
647
+ }
648
+ }