@spirobel/monero-wallet-api 0.4.0 → 0.5.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.
- package/README.md +80 -8
- package/dist/api.d.ts +4 -0
- package/dist/api.js +4 -0
- package/dist/io/atomicWrite.js +2 -1
- package/dist/io/logging.d.ts +40 -0
- package/dist/io/logging.js +174 -0
- package/dist/keypairs-seeds/keypairs.d.ts +8 -0
- package/dist/keypairs-seeds/keypairs.js +31 -3
- package/dist/multisig/DistributedKeyGenerator.d.ts +125 -0
- package/dist/multisig/DistributedKeyGenerator.js +197 -0
- package/dist/multisig/MultiSigTxSigner.d.ts +68 -0
- package/dist/multisig/MultiSigTxSigner.js +123 -0
- package/dist/multisig/escrow.d.ts +58 -0
- package/dist/multisig/escrow.js +104 -0
- package/dist/multisig/multisig.d.ts +36 -0
- package/dist/multisig/multisig.js +140 -0
- package/dist/node-interaction/nodeUrl.js +2 -1
- package/dist/scanning-syncing/backgroundWorker.d.ts +2 -0
- package/dist/scanning-syncing/backgroundWorker.js +20 -8
- package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +9 -7
- package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +2 -1
- package/dist/scanning-syncing/scanSettings.d.ts +11 -0
- package/dist/scanning-syncing/scanSettings.js +18 -3
- package/dist/scanning-syncing/scanresult/computeKeyImage.d.ts +2 -1
- package/dist/scanning-syncing/scanresult/computeKeyImage.js +4 -2
- package/dist/scanning-syncing/scanresult/scanCache.js +7 -3
- package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +11 -0
- package/dist/scanning-syncing/scanresult/scanCacheOpened.js +104 -11
- package/dist/scanning-syncing/scanresult/scanCoordination.js +63 -72
- package/dist/scanning-syncing/scanresult/scanResult.d.ts +1 -1
- package/dist/scanning-syncing/scanresult/scanResult.js +19 -6
- package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
- package/dist/scanning-syncing/worker-mains/coordinator-main.js +3 -2
- package/dist/scanning-syncing/worker-mains/cpubound-main.js +24 -14
- package/dist/scanning-syncing/worker-mains/multisig-main.d.ts +13 -0
- package/dist/scanning-syncing/worker-mains/multisig-main.js +31 -0
- package/dist/scanning-syncing/worker-mains/worker.js +23 -7
- package/dist/send-functionality/multisig.d.ts +72 -0
- package/dist/send-functionality/multisig.js +97 -0
- package/dist/send-functionality/transactionBuilding.d.ts +1 -0
- package/dist/send-functionality/transactionBuilding.js +23 -2
- package/dist/viewpair/ViewPair.d.ts +8 -0
- package/dist/viewpair/ViewPair.js +16 -8
- package/dist/wasm-processing/wasmFile.d.ts +1 -0
- package/dist/wasm-processing/wasmFile.js +8 -3
- package/dist/wasm-processing/wasmProcessor.d.ts +2 -2
- package/dist/wasm-processing/wasmProcessor.js +4 -5
- package/package.json +6 -2
|
@@ -2,7 +2,7 @@ import { atomicWrite, 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 { openScanSettingsFile, readPrivateSpendKeyFromEnv, readWalletFromScanSettings, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, writeNodeUrlToScanSettings, writeStartHeightToScanSettings, writeWalletToScanSettings, } from "../scanSettings";
|
|
5
|
+
import { getPathPrefix, openScanSettingsFile, readPrivateSpendKeyFromEnv, readWalletFromScanSettings, SCAN_SETTINGS_STORE_NAME_DEFAULT, SUB_ADDRESS_INDEX_DEFAULT_VALUE, walletSettingsPlusKeys, writeNodeUrlToScanSettings, writeStartHeightToScanSettings, writeWalletToScanSettings, } from "../scanSettings";
|
|
6
6
|
import { findRange, lastRange, readCacheFileDefaultLocation, writeCacheFileDefaultLocationThrows, } from "./scanCache";
|
|
7
7
|
import { alignScanStatsWithCache, isSelfSpent, processTxlogInputs, processTxlogPayments, writeStatsFileDefaultLocation, } from "./scanStats";
|
|
8
8
|
import { updateSyncETA, readWriteConnectionStatusFile, } from "../connectionStatus";
|
|
@@ -53,19 +53,32 @@ export class ScanCacheOpened {
|
|
|
53
53
|
// unpause will start scanning from this.wallet_scan_settings.start_height
|
|
54
54
|
await scanCacheOpen.unpause();
|
|
55
55
|
}
|
|
56
|
-
scanCacheOpen.
|
|
56
|
+
scanCacheOpen._highest_subaddress_index =
|
|
57
|
+
walletSettings.subaddress_index || SUB_ADDRESS_INDEX_DEFAULT_VALUE;
|
|
58
|
+
if (!params.no_stats) {
|
|
59
|
+
scanCacheOpen._stats = await alignScanStatsWithCache(scanCacheOpen._cache, scanCacheOpen.view_pair, params.primary_address, getPathPrefix(params.scan_settings_path, params.pathPrefix), walletSettings.subaddress_index, lastRange(scanCacheOpen._cache.scanned_ranges)?.end);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
scanCacheOpen._no_stats = params.no_stats; // true
|
|
63
|
+
}
|
|
57
64
|
return scanCacheOpen;
|
|
58
65
|
}
|
|
59
66
|
get start_height() {
|
|
60
67
|
return this._start_height;
|
|
61
68
|
}
|
|
69
|
+
get subaddress_index() {
|
|
70
|
+
if (typeof this._highest_subaddress_index === "undefined" ||
|
|
71
|
+
this._highest_subaddress_index === null)
|
|
72
|
+
return SUB_ADDRESS_INDEX_DEFAULT_VALUE;
|
|
73
|
+
return this._highest_subaddress_index;
|
|
74
|
+
}
|
|
62
75
|
get current_height() {
|
|
63
76
|
let current_range = findRange(this._cache.scanned_ranges, this._start_height || 0);
|
|
64
77
|
return current_range?.end || null;
|
|
65
78
|
}
|
|
66
79
|
get current_top_range_height() {
|
|
67
80
|
if (typeof this._stats === "undefined" || this._stats === null)
|
|
68
|
-
return
|
|
81
|
+
return this.current_height;
|
|
69
82
|
return this._stats.height;
|
|
70
83
|
}
|
|
71
84
|
async changeStartHeight(start_height) {
|
|
@@ -181,6 +194,21 @@ export class ScanCacheOpened {
|
|
|
181
194
|
throw new Error("privateSpendKey not found in env");
|
|
182
195
|
return await signTransaction(unsignedTx, privateSpendKey);
|
|
183
196
|
}
|
|
197
|
+
async getFeeEstimate() {
|
|
198
|
+
const node = await NodeUrl.create(this.node_url);
|
|
199
|
+
const feeEstimate = await node.getFeeEstimate();
|
|
200
|
+
const feePerByte = BigInt(feeEstimate.fees[0]);
|
|
201
|
+
const max_plausible_fee = 20000000000n; // 0.02 XMR
|
|
202
|
+
const feeFor10kb = feePerByte * 10000n;
|
|
203
|
+
//2. check if fee is too high
|
|
204
|
+
if (feeFor10kb > max_plausible_fee) {
|
|
205
|
+
throw new Error(`fee too high:
|
|
206
|
+
${feeFor10kb} (fee for 10kb tx size) > ${max_plausible_fee} (0.001 XMR)
|
|
207
|
+
most likely your node is faulty. connect to another node.
|
|
208
|
+
preferably run one yourself locally.`);
|
|
209
|
+
}
|
|
210
|
+
return feeEstimate;
|
|
211
|
+
}
|
|
184
212
|
async calculateFeeAndSelectInputs(params) {
|
|
185
213
|
const sum = sumPayments(params.payments);
|
|
186
214
|
const node = await NodeUrl.create(this.node_url);
|
|
@@ -243,6 +271,49 @@ export class ScanCacheOpened {
|
|
|
243
271
|
});
|
|
244
272
|
return unsignedTx;
|
|
245
273
|
}
|
|
274
|
+
async makeSweepTransactionFromSelectedInputs(destination_address, selectedInputs, feeEstimate) {
|
|
275
|
+
// 4. get output distribution
|
|
276
|
+
const node = await NodeUrl.create(this.node_url);
|
|
277
|
+
const distibution = await node.getOutputDistribution();
|
|
278
|
+
const inputs = [];
|
|
279
|
+
for (const input of selectedInputs) {
|
|
280
|
+
// 5. sample decoys & get outs from node: here is where a privacy compromising event could happen
|
|
281
|
+
const sizesToTry = this.decoyRetry
|
|
282
|
+
? this.decoyRetrySizes
|
|
283
|
+
: [this.decoySampleCount];
|
|
284
|
+
let madeInput;
|
|
285
|
+
for (const size of sizesToTry) {
|
|
286
|
+
if (madeInput)
|
|
287
|
+
break;
|
|
288
|
+
try {
|
|
289
|
+
const prepared = prepareInput(node, distibution, input, size);
|
|
290
|
+
const wasmInput = node.makeInput(prepared.input, prepared.sample.candidates, await prepared.outsResponse);
|
|
291
|
+
madeInput = wasmInput;
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
if (size === sizesToTry[sizesToTry.length - 1])
|
|
295
|
+
throw e;
|
|
296
|
+
// fall through to next size
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!madeInput)
|
|
300
|
+
throw new Error("failed to make input");
|
|
301
|
+
inputs.push(madeInput);
|
|
302
|
+
}
|
|
303
|
+
// 7. make transaction: combine inputs, payments + fee info
|
|
304
|
+
const unsignedTx = this.view_pair.makeSweepTransaction({
|
|
305
|
+
inputs,
|
|
306
|
+
payments: [
|
|
307
|
+
{
|
|
308
|
+
address: destination_address,
|
|
309
|
+
amount: "0",
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
fee_response: feeEstimate,
|
|
313
|
+
fee_priority: "unimportant",
|
|
314
|
+
});
|
|
315
|
+
return unsignedTx;
|
|
316
|
+
}
|
|
246
317
|
/**
|
|
247
318
|
* this function returns the unsigned transaction, throws {@link SendError}
|
|
248
319
|
*/
|
|
@@ -250,16 +321,30 @@ export class ScanCacheOpened {
|
|
|
250
321
|
const { selectedInputs, feeEstimate } = await this.calculateFeeAndSelectInputs(params);
|
|
251
322
|
return await this.makeTransactionFromSelectedInputs(params.payments, selectedInputs, feeEstimate);
|
|
252
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* sweep inputs to external wallet address (the wallet will receive input amount - fee)
|
|
326
|
+
* @param inputs use spendableInputs() to find inputs to put in
|
|
327
|
+
*/
|
|
328
|
+
async sweepToExternalWallet(destination_address, inputs) {
|
|
329
|
+
const feeEstimate = await this.getFeeEstimate();
|
|
330
|
+
return await this.makeSweepTransactionFromSelectedInputs(destination_address, inputs, feeEstimate);
|
|
331
|
+
}
|
|
253
332
|
get daemon_height() {
|
|
254
333
|
return this._cache.daemon_height;
|
|
255
334
|
}
|
|
256
335
|
get amount() {
|
|
336
|
+
if (this.no_stats)
|
|
337
|
+
throw new Error("instance has no_stats option active");
|
|
257
338
|
return this._stats?.total_spendable_amount || 0n;
|
|
258
339
|
}
|
|
259
340
|
get pending_amount() {
|
|
341
|
+
if (this.no_stats)
|
|
342
|
+
throw new Error("instance has no_stats option active");
|
|
260
343
|
return this._stats?.total_pending_amount || 0n;
|
|
261
344
|
}
|
|
262
345
|
get subaddresses() {
|
|
346
|
+
if (this.no_stats)
|
|
347
|
+
throw new Error("instance has no_stats option active");
|
|
263
348
|
return Object.values(this._stats?.subaddresses || {});
|
|
264
349
|
}
|
|
265
350
|
get tx_logs() {
|
|
@@ -403,6 +488,7 @@ export class ScanCacheOpened {
|
|
|
403
488
|
const last_subaddress_index = walletSettings.subaddress_index || 0;
|
|
404
489
|
const minor = last_subaddress_index + 1;
|
|
405
490
|
const subaddress = this.view_pair.makeSubaddress(minor);
|
|
491
|
+
this._highest_subaddress_index = minor;
|
|
406
492
|
await writeWalletToScanSettings({
|
|
407
493
|
primary_address: this.view_pair.primary_address,
|
|
408
494
|
subaddress_index: minor,
|
|
@@ -417,13 +503,14 @@ export class ScanCacheOpened {
|
|
|
417
503
|
created_at_timestamp,
|
|
418
504
|
not_yet_included: true,
|
|
419
505
|
};
|
|
420
|
-
this.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
stats
|
|
425
|
-
|
|
426
|
-
|
|
506
|
+
if (!this._no_stats)
|
|
507
|
+
this._stats = await writeStatsFileDefaultLocation({
|
|
508
|
+
primary_address: this.primary_address,
|
|
509
|
+
pathPrefix: getPathPrefix(this.scan_settings_path, this.pathPrefix),
|
|
510
|
+
writeCallback: async (stats) => {
|
|
511
|
+
stats.subaddresses[minor.toString()] = new_subaddress;
|
|
512
|
+
},
|
|
513
|
+
});
|
|
427
514
|
return new_subaddress;
|
|
428
515
|
}
|
|
429
516
|
/**
|
|
@@ -546,7 +633,8 @@ export class ScanCacheOpened {
|
|
|
546
633
|
if (this.view_pair.primary_address !== params.newCache.primary_address)
|
|
547
634
|
return;
|
|
548
635
|
this._cache = params.newCache;
|
|
549
|
-
|
|
636
|
+
if (!this._no_stats)
|
|
637
|
+
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);
|
|
550
638
|
if (!this.no_worker) {
|
|
551
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);
|
|
552
640
|
this.last_eta_height = etaResult.last_height;
|
|
@@ -557,6 +645,11 @@ export class ScanCacheOpened {
|
|
|
557
645
|
listener(params);
|
|
558
646
|
}
|
|
559
647
|
}
|
|
648
|
+
_highest_subaddress_index = null;
|
|
649
|
+
_no_stats = false;
|
|
650
|
+
get no_stats() {
|
|
651
|
+
return this._no_stats;
|
|
652
|
+
}
|
|
560
653
|
_cache = {
|
|
561
654
|
daemon_height: 0,
|
|
562
655
|
outputs: {},
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges,
|
|
1
|
+
import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, } from "../../api";
|
|
2
2
|
import { markWorkItemAsDone, } from "./scanLoop";
|
|
3
3
|
import { makeWorkItem, scanLoop } from "./scanLoop";
|
|
4
4
|
import { cullTooLargeScanHeight, getNonHaltedWallets, getPathPrefix, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, } from "../scanSettings";
|
|
5
5
|
import { findRange, initScanCacheFile, lastRange, makeCacheRangeForHeight, mergeRanges, } from "./scanCache";
|
|
6
6
|
import { sendToCpuWorker } from "../worker-mains/cpubound-main";
|
|
7
|
+
import { log } from "../../io/logging";
|
|
7
8
|
/**
|
|
8
9
|
* this depends only on ScanSettings.json start_height and wallet caches scanned_ranges
|
|
9
10
|
* side effect: will init wallet cache file if it does not exist
|
|
@@ -59,7 +60,7 @@ export async function findWorkToBeDone(scan_settings_path = SCAN_SETTINGS_STORE_
|
|
|
59
60
|
for (const wallet_cache of wallet_caches) {
|
|
60
61
|
// only add the range to wallets that don't already have one
|
|
61
62
|
if (!findRange(wallet_cache.scanned_ranges, total_start_height)) {
|
|
62
|
-
|
|
63
|
+
log("findWorkToBeDone", "inserting range_at_start");
|
|
63
64
|
wallet_cache.scanned_ranges.push(range_at_start);
|
|
64
65
|
wallet_cache.scanned_ranges = mergeRanges(wallet_cache.scanned_ranges);
|
|
65
66
|
}
|
|
@@ -81,9 +82,9 @@ export async function findWorkToBeDone(scan_settings_path = SCAN_SETTINGS_STORE_
|
|
|
81
82
|
export function workToBeDoneForBatch(cache, batch_meta_infos) {
|
|
82
83
|
const begin_height = batch_meta_infos[0].block_height;
|
|
83
84
|
const end_height = batch_meta_infos[batch_meta_infos.length - 1].block_height;
|
|
84
|
-
|
|
85
|
+
log("workToBeDoneForBatch", [begin_height, end_height]);
|
|
85
86
|
const foundRange = findRange(cache.scanned_ranges, begin_height);
|
|
86
|
-
|
|
87
|
+
log("workToBeDoneForBatch", ["foundRange", foundRange]);
|
|
87
88
|
if (foundRange) {
|
|
88
89
|
const fullycovered = cache.scanned_ranges.find((r) => r.start <= begin_height && r.end > end_height);
|
|
89
90
|
if (fullycovered) {
|
|
@@ -96,13 +97,13 @@ export function workToBeDoneForBatch(cache, batch_meta_infos) {
|
|
|
96
97
|
throw new Error("[workToBeDoneForBatch] tip not found, malformed range that covers the work to be done for this batch");
|
|
97
98
|
const tipindex = findTipIndex(batch_meta_infos, tip);
|
|
98
99
|
if (tipindex === "reorg_found") {
|
|
99
|
-
|
|
100
|
+
log("workToBeDoneForBatch", "reorg found");
|
|
100
101
|
return { from: 0 };
|
|
101
102
|
}
|
|
102
103
|
else if (tipindex === "empty_blocks_array") {
|
|
103
104
|
return "skip";
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
log("workToBeDoneForBatch", ["tipindex", tipindex]);
|
|
106
107
|
return { from: tipindex }; // NORMAL case of scheduling directly in front of the tip
|
|
107
108
|
}
|
|
108
109
|
}
|
|
@@ -123,36 +124,14 @@ export function makeWorkItemsFromBlocksBuffer(blocksBuffer, workItemBuffer, wall
|
|
|
123
124
|
const alreadyReferenced = workItemBuffer.some((w) => w.batch.local_uuid === batch.local_uuid &&
|
|
124
125
|
w.walletConfig.primary_address === walletConfig.primary_address);
|
|
125
126
|
if (!alreadyReferenced) {
|
|
126
|
-
// const begin_height =
|
|
127
|
-
// batch.get_blocks_result_meta.block_infos[0].block_height;
|
|
128
|
-
// const end_height =
|
|
129
|
-
// batch.get_blocks_result_meta.block_infos[
|
|
130
|
-
// batch.get_blocks_result_meta.block_infos.length - 1
|
|
131
|
-
// ].block_height;
|
|
132
|
-
// console.log(walletConfig.cache.scanned_ranges);
|
|
133
|
-
// console.log(begin_height);
|
|
134
|
-
// // skip batches fully covered by an existing scanned range
|
|
135
|
-
// if (
|
|
136
|
-
// walletConfig.cache.scanned_ranges.some(
|
|
137
|
-
// (r) => r.start <= begin_height && r.end >= end_height,
|
|
138
|
-
// )
|
|
139
|
-
// ) {
|
|
140
|
-
// throw new Error(
|
|
141
|
-
// "skip" +
|
|
142
|
-
// begin_height +
|
|
143
|
-
// " end_height" +
|
|
144
|
-
// end_height +
|
|
145
|
-
// " " +
|
|
146
|
-
// walletConfig.primary_address.slice(0, 6),
|
|
147
|
-
// );
|
|
148
|
-
// continue;
|
|
149
|
-
// }
|
|
150
127
|
const workToBeDone = workToBeDoneForBatch(walletConfig.cache, batch.get_blocks_result_meta.block_infos);
|
|
151
128
|
if (workToBeDone === "skip") {
|
|
152
129
|
continue;
|
|
153
130
|
}
|
|
154
131
|
const workItem = makeWorkItem(walletConfig, batch, from, to);
|
|
155
|
-
|
|
132
|
+
log("makeWorkItemsFromBlocksBuffer", [
|
|
133
|
+
`uuid=${workItem.work_uuid.slice()} to=${workItem.to} from=${workItem.from} batchbegin_height=${batch.get_blocks_result_meta.block_infos[0].block_height} batchend_height=${batch.get_blocks_result_meta.block_infos[batch.get_blocks_result_meta.block_infos.length - 1].block_height}`,
|
|
134
|
+
]);
|
|
156
135
|
workItemBuffer.push(workItem);
|
|
157
136
|
}
|
|
158
137
|
}
|
|
@@ -168,16 +147,16 @@ export function makeWorkItemsForAllWallets(wallet_configs, blocksBuffer, workBuf
|
|
|
168
147
|
* blocks buffer if no remaining work items reference it.
|
|
169
148
|
*/
|
|
170
149
|
export function reconcileWorkItemDone(blocksBuffer, workItemBuffer) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
150
|
+
log("reconcileWorkItemDone", [
|
|
151
|
+
`workItemBuffer.length=${workItemBuffer.length} , blocksBuffer.length=${blocksBuffer.length}`,
|
|
152
|
+
]);
|
|
174
153
|
while (workItemBuffer.length > 0 &&
|
|
175
154
|
workItemBuffer[0].status === "process_result_done") {
|
|
176
155
|
const removed = workItemBuffer.shift();
|
|
177
156
|
const stillReferenced = workItemBuffer.some((w) => w.batch.local_uuid === removed.batch.local_uuid);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
157
|
+
log("reconcileWorkItemDone", [
|
|
158
|
+
`workItem: ${removed.work_uuid.slice()} removed. stillReferenced=${stillReferenced}`,
|
|
159
|
+
]);
|
|
181
160
|
if (!stillReferenced) {
|
|
182
161
|
const idx = blocksBuffer.findIndex((b) => b.local_uuid === removed.batch.local_uuid);
|
|
183
162
|
// this really means we have to save work items scanCache to file
|
|
@@ -218,17 +197,20 @@ export async function processScanResultForWorkItem(value, workBuffer, blocksBuff
|
|
|
218
197
|
if (value.type !== "Ready" || !value.result || !value.work_uuid)
|
|
219
198
|
return;
|
|
220
199
|
const item = await markWorkItemAsDone(value, workBuffer);
|
|
221
|
-
|
|
200
|
+
log("processScanResultForWorkItem", `item_work_uuid=${item?.work_uuid}`);
|
|
222
201
|
if (!item || item.status !== "scanwork_done")
|
|
223
202
|
throw new Error("[processScanResultForWorkItem] item not found or not scanwork not done. item_status=" +
|
|
224
203
|
item?.status);
|
|
225
204
|
const firstBlock = item.batch.get_blocks_result_meta.block_infos[item.from];
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
205
|
+
log("processScanResultForWorkItem", [
|
|
206
|
+
`block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} firstBlock=${firstBlock?.block_height} firstBlockHash=${firstBlock?.block_hash?.slice(0, 8)}`,
|
|
207
|
+
]);
|
|
208
|
+
log("processScanResultForWorkItem", [
|
|
209
|
+
`block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} from=${item.from} to=${item.to}`,
|
|
210
|
+
]);
|
|
229
211
|
const lastBlock = item.batch.get_blocks_result_meta.block_infos[item.to];
|
|
230
212
|
const cache = item.walletConfig.cache;
|
|
231
|
-
await
|
|
213
|
+
await processScanResult({
|
|
232
214
|
from_height: firstBlock.block_height,
|
|
233
215
|
to_height: lastBlock.block_height,
|
|
234
216
|
result: value.result,
|
|
@@ -263,28 +245,27 @@ export async function processWorkItem(item, workBuffer, blocksBuffer, pathPrefix
|
|
|
263
245
|
}
|
|
264
246
|
}
|
|
265
247
|
const firstBlock = item.batch.get_blocks_result_meta.block_infos[item.from];
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
248
|
+
log("processWorkItem", [
|
|
249
|
+
`block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} from=${item.from} to=${item.to}`,
|
|
250
|
+
]);
|
|
269
251
|
const lastBlock = item.batch.get_blocks_result_meta.block_infos[item.to];
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// );
|
|
252
|
+
log("processWorkItem", [
|
|
253
|
+
item.walletConfig.primary_address.slice(0, 6),
|
|
254
|
+
"@",
|
|
255
|
+
firstBlock.block_height,
|
|
256
|
+
"-",
|
|
257
|
+
lastBlock.block_height,
|
|
258
|
+
]);
|
|
278
259
|
let res;
|
|
279
260
|
try {
|
|
280
|
-
res = await
|
|
261
|
+
res = await processScanResult({
|
|
281
262
|
from_height: firstBlock.block_height,
|
|
282
263
|
to_height: lastBlock.block_height,
|
|
283
264
|
result: item.result,
|
|
284
265
|
scanCache: cache,
|
|
285
266
|
secret_spend_key,
|
|
286
267
|
});
|
|
287
|
-
|
|
268
|
+
log("processWorkItem", ["res=", res]);
|
|
288
269
|
}
|
|
289
270
|
catch (error) {
|
|
290
271
|
console.error("[processWorkItem] error=", error);
|
|
@@ -292,7 +273,7 @@ export async function processWorkItem(item, workBuffer, blocksBuffer, pathPrefix
|
|
|
292
273
|
}
|
|
293
274
|
await writeCacheToFile(cache, pathPrefix);
|
|
294
275
|
item.status = "process_result_done";
|
|
295
|
-
|
|
276
|
+
log("processWorkItem", `process_result_done work_uuid=${item?.work_uuid}`);
|
|
296
277
|
// remove from blocksbuffer if no more work items reference it
|
|
297
278
|
reconcileWorkItemDone(blocksBuffer, workBuffer);
|
|
298
279
|
//because cache is tied to the workitems by reference,
|
|
@@ -314,7 +295,9 @@ function logBufStatus(blocksBuffer, workBuffer, ports, label) {
|
|
|
314
295
|
w.batch.get_blocks_result_meta.block_infos[w.from]?.block_height +
|
|
315
296
|
"-" +
|
|
316
297
|
w.batch.get_blocks_result_meta.block_infos[w.to]?.block_height);
|
|
317
|
-
|
|
298
|
+
log("logBufStatus", [
|
|
299
|
+
`[buf] ${label} blocks=[${bb.join(",")}] work=[${wb.join(",")}]`,
|
|
300
|
+
]);
|
|
318
301
|
}
|
|
319
302
|
/**
|
|
320
303
|
* coordinator main: async generator that drives the full scan cycle.
|
|
@@ -353,10 +336,9 @@ export async function* coordinatorMain(scanSettingsPath, pathPrefix) {
|
|
|
353
336
|
races.push(p.then((v) => ({ src: "scan", addr, value: v })));
|
|
354
337
|
}
|
|
355
338
|
const winner = await Promise.race(races);
|
|
356
|
-
|
|
357
|
-
winner.src +
|
|
358
|
-
|
|
359
|
-
String(winner.addr ?? "").slice(0, 8));
|
|
339
|
+
log("coordinatorMain", [
|
|
340
|
+
"winner=" + winner.src + " addr=" + String(winner.addr ?? "").slice(0, 8),
|
|
341
|
+
]);
|
|
360
342
|
if (winner.src === "blocks") {
|
|
361
343
|
const result = winner.value.value;
|
|
362
344
|
const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
|
|
@@ -368,7 +350,7 @@ export async function* coordinatorMain(scanSettingsPath, pathPrefix) {
|
|
|
368
350
|
if (scanPromises.has(addr))
|
|
369
351
|
continue;
|
|
370
352
|
const gen = scanGens.get(addr);
|
|
371
|
-
|
|
353
|
+
log("coordinatorMain", ["scanGens.get(addr)", gen]);
|
|
372
354
|
if (!gen)
|
|
373
355
|
continue;
|
|
374
356
|
const item = workBuffer.find((x) => x.status === "fresh" && x.walletConfig.primary_address === addr);
|
|
@@ -459,7 +441,7 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
459
441
|
// throw new Error(
|
|
460
442
|
// "[coordinatorMain Multithreaded] cpuPorts empty, there must be at least cpu worker",
|
|
461
443
|
// );
|
|
462
|
-
|
|
444
|
+
log("coordinatorMainMultithreaded", "fallback to single-threaded");
|
|
463
445
|
yield* coordinatorMain(scanSettingsPath, pathPrefix);
|
|
464
446
|
return;
|
|
465
447
|
}
|
|
@@ -482,7 +464,7 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
482
464
|
let race_count = 0;
|
|
483
465
|
while (true) {
|
|
484
466
|
race_count++;
|
|
485
|
-
|
|
467
|
+
log("coordinatorMainMultithreaded", ["race_count", race_count]);
|
|
486
468
|
await scheduleWorkOnCpuPorts(freePorts, workBuffer);
|
|
487
469
|
const scan_promises = freePorts
|
|
488
470
|
.filter((ps) => ps.promise)
|
|
@@ -494,9 +476,9 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
494
476
|
value: v,
|
|
495
477
|
})),
|
|
496
478
|
];
|
|
497
|
-
|
|
479
|
+
log("coordinatorMainMultithreaded", ["races", races]);
|
|
498
480
|
const winner = await Promise.race(races);
|
|
499
|
-
|
|
481
|
+
log("coordinatorMainMultithreaded", "cpu ports status");
|
|
500
482
|
if ("src" in winner && winner.src === "blocks") {
|
|
501
483
|
const result = winner.value.value;
|
|
502
484
|
const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
|
|
@@ -529,7 +511,7 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
529
511
|
if (!to_be_processed)
|
|
530
512
|
continue;
|
|
531
513
|
const res = await processWorkItem(to_be_processed, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wallet.secret_spend_key);
|
|
532
|
-
//
|
|
514
|
+
// log("coordinatorMainMultithreaded", "processWorkItem result");
|
|
533
515
|
yield {
|
|
534
516
|
type: "scan_ready",
|
|
535
517
|
address: wallet.primary_address,
|
|
@@ -561,9 +543,14 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
561
543
|
const onmessage = (event) => {
|
|
562
544
|
const msg = event.data;
|
|
563
545
|
// handle the result here:
|
|
564
|
-
|
|
546
|
+
log("scheduleWorkOnCpuPorts", ["onmessage result", msg]);
|
|
565
547
|
if (msg.work_uuid !== item.work_uuid) {
|
|
566
|
-
|
|
548
|
+
log("scheduleWorkOnCpuPorts", [
|
|
549
|
+
"wrong work_uuid in msg msg.work_uuid=",
|
|
550
|
+
msg.work_uuid,
|
|
551
|
+
"item.work_uuid=",
|
|
552
|
+
item.work_uuid,
|
|
553
|
+
]);
|
|
567
554
|
throw new Error("[scheduleWorkOnCpuPorts] wrong work_uuid in msg");
|
|
568
555
|
}
|
|
569
556
|
if (msg.type === "WORKSTART") {
|
|
@@ -571,7 +558,7 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
571
558
|
return;
|
|
572
559
|
}
|
|
573
560
|
if (item.status !== "scanwork_in_progress") {
|
|
574
|
-
|
|
561
|
+
log("scheduleWorkOnCpuPorts", "wrong status in msg");
|
|
575
562
|
throw new Error("[scheduleWorkOnCpuPorts] wrong status in msg");
|
|
576
563
|
}
|
|
577
564
|
item.result = msg.result;
|
|
@@ -581,7 +568,7 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
581
568
|
};
|
|
582
569
|
port_status.port.onmessage = onmessage;
|
|
583
570
|
item.status = "scanwork_in_progress";
|
|
584
|
-
|
|
571
|
+
log("scheduleWorkOnCpuPorts", ["scheduling work item", item.work_uuid]);
|
|
585
572
|
port_status.promise = new Promise((resolve) => {
|
|
586
573
|
resolve_port = resolve;
|
|
587
574
|
});
|
|
@@ -604,7 +591,11 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
604
591
|
break;
|
|
605
592
|
}
|
|
606
593
|
catch {
|
|
607
|
-
|
|
594
|
+
log("scheduleWorkOnCpuPorts", [
|
|
595
|
+
"resend attempt",
|
|
596
|
+
attempt,
|
|
597
|
+
item.work_uuid,
|
|
598
|
+
]);
|
|
608
599
|
}
|
|
609
600
|
}
|
|
610
601
|
}
|
|
@@ -22,7 +22,7 @@ export type ProcessScanResult = {
|
|
|
22
22
|
current_range: CacheRange;
|
|
23
23
|
changed_outputs: ChangedOutput[];
|
|
24
24
|
};
|
|
25
|
-
export declare function
|
|
25
|
+
export declare function processScanResult(params: ProcessScanResultParamsWithoutSideEffects): Promise<ProcessScanResult>;
|
|
26
26
|
export declare function processResultReturnValue(cache: ScanCache, params: ProcessScanResultParamsWithoutSideEffects, changed_outputs: ChangedOutput[]): {
|
|
27
27
|
current_range: CacheRange;
|
|
28
28
|
changed_outputs: ChangedOutput[];
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { CatastrophicReorgError
|
|
1
|
+
import { CatastrophicReorgError } from "../../api";
|
|
2
2
|
import { computeKeyImage } from "./computeKeyImage";
|
|
3
|
-
import {
|
|
3
|
+
import { log } from "../../io/logging";
|
|
4
|
+
import { mergeRanges, findRange, findRangeThrows } from "./scanCache";
|
|
4
5
|
import {} from "../../node-interaction/binaryEndpoints";
|
|
5
|
-
|
|
6
|
+
import { WasmProcessor } from "../../wasm-processing/wasmProcessor";
|
|
7
|
+
import { monero_wallet_api_wasm } from "../../wasm-processing/wasmFile";
|
|
8
|
+
export async function processScanResult(params) {
|
|
6
9
|
const { result, scanCache: cache, secret_spend_key } = params;
|
|
7
10
|
let changed_outputs = [];
|
|
8
11
|
if (!(result && "primary_address" in result))
|
|
@@ -25,7 +28,10 @@ export async function processScanResultWITHOUT_SIDE_EFFECTS(params) {
|
|
|
25
28
|
throw new CatastrophicReorgError("could not locate anchors, should not try to processResult on stale workItems");
|
|
26
29
|
const split_height = result.block_infos[split_height_index];
|
|
27
30
|
// we found the split height & do the reorg
|
|
28
|
-
|
|
31
|
+
log("processScanResult", [
|
|
32
|
+
"SPLIT FOUND at height",
|
|
33
|
+
split_height.block_height,
|
|
34
|
+
]);
|
|
29
35
|
if (!cache.reorg_info) {
|
|
30
36
|
cache.reorg_info = {
|
|
31
37
|
split_heights: [split_height],
|
|
@@ -41,7 +47,12 @@ export async function processScanResultWITHOUT_SIDE_EFFECTS(params) {
|
|
|
41
47
|
const reverted_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.spent_block_height !== undefined &&
|
|
42
48
|
output.spent_block_height >= split_height.block_height);
|
|
43
49
|
const removed_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.block_height >= split_height.block_height);
|
|
44
|
-
|
|
50
|
+
log("processScanResult", [
|
|
51
|
+
"removed_outputs count:",
|
|
52
|
+
removed_outputs.length,
|
|
53
|
+
"reverted count:",
|
|
54
|
+
reverted_outputs.length,
|
|
55
|
+
]);
|
|
45
56
|
for (const [id, old_output_state] of removed_outputs) {
|
|
46
57
|
// 1. find key_image of output to be removed (as it was reorged)
|
|
47
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
|
|
@@ -128,6 +139,7 @@ export function makeNewRange(newRange, cache) {
|
|
|
128
139
|
// Assumption: result is new, cache is still old. (this + detectOwnspends() turns the catch new, based on the scan result)
|
|
129
140
|
export async function detectOutputs(result, cache, spend_private_key) {
|
|
130
141
|
let changed_outputs = [];
|
|
142
|
+
const wasmProcessor = await WasmProcessor.init(monero_wallet_api_wasm);
|
|
131
143
|
for (const output of result.outputs) {
|
|
132
144
|
// 0. prevent burning bug and avoid overwriting earlier found outputs
|
|
133
145
|
const duplicate = Object.values(cache.outputs).find((ex) => ex.stealth_address === output.stealth_address && !ex.burned);
|
|
@@ -158,7 +170,7 @@ export async function detectOutputs(result, cache, spend_private_key) {
|
|
|
158
170
|
changed_outputs.push({ output, change_reason: "added" });
|
|
159
171
|
// 3. if this is not view only, add the key image to the cache, to find transactions spent by this wallet
|
|
160
172
|
if (spend_private_key) {
|
|
161
|
-
let keyImage = await computeKeyImage(output, spend_private_key);
|
|
173
|
+
let keyImage = await computeKeyImage(output, spend_private_key, wasmProcessor);
|
|
162
174
|
if (keyImage) {
|
|
163
175
|
cache.own_key_images[keyImage] = globalId;
|
|
164
176
|
}
|
|
@@ -214,6 +226,7 @@ export function outputStatus(output, cache, current_height) {
|
|
|
214
226
|
if (unlock_height > current_height) {
|
|
215
227
|
return { status: "pending", unlock_height };
|
|
216
228
|
}
|
|
229
|
+
// TODO: check that that output is not txlog input indeces
|
|
217
230
|
return { status: "spendable" };
|
|
218
231
|
}
|
|
219
232
|
export function spendable(output, cache, current_height) {
|