@spirobel/monero-wallet-api 0.3.18 → 0.4.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.
- package/dist/api.d.ts +5 -2
- package/dist/api.js +5 -1
- package/dist/io/atomicWrite.js +3 -1
- package/dist/io/indexedDB.js +7 -2
- package/dist/io/logging.d.ts +40 -0
- package/dist/io/logging.js +172 -0
- package/dist/keypairs-seeds/keypairs.d.ts +21 -0
- package/dist/keypairs-seeds/keypairs.js +21 -0
- package/dist/node-interaction/binaryEndpoints.d.ts +31 -2
- package/dist/node-interaction/binaryEndpoints.js +61 -43
- package/dist/node-interaction/jsonEndpoints.d.ts +257 -130
- package/dist/node-interaction/jsonEndpoints.js +159 -119
- package/dist/node-interaction/nodeUrl.d.ts +21 -6
- package/dist/node-interaction/nodeUrl.js +50 -8
- package/dist/scanning-syncing/backgroundWorker.d.ts +10 -6
- package/dist/scanning-syncing/backgroundWorker.js +88 -39
- package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.d.ts +44 -0
- package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +355 -0
- package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.d.ts +15 -0
- package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +28 -0
- package/dist/scanning-syncing/{scanresult → blocksbuffer}/getBlocksbinBuffer.d.ts +5 -5
- package/dist/scanning-syncing/{scanresult → blocksbuffer}/getBlocksbinBuffer.js +7 -5
- package/dist/scanning-syncing/connectionStatus.d.ts +33 -9
- package/dist/scanning-syncing/connectionStatus.js +89 -19
- package/dist/scanning-syncing/scanSettings.d.ts +21 -0
- package/dist/scanning-syncing/scanSettings.js +29 -0
- package/dist/scanning-syncing/scanresult/reorg.d.ts +0 -11
- package/dist/scanning-syncing/scanresult/reorg.js +39 -16
- package/dist/scanning-syncing/scanresult/scanCache.d.ts +16 -2
- package/dist/scanning-syncing/scanresult/scanCache.js +65 -5
- package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +18 -0
- package/dist/scanning-syncing/scanresult/scanCacheOpened.js +56 -11
- package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +127 -0
- package/dist/scanning-syncing/scanresult/scanCoordination.js +602 -0
- package/dist/scanning-syncing/scanresult/scanLoop.d.ts +20 -0
- package/dist/scanning-syncing/scanresult/scanLoop.js +116 -0
- package/dist/scanning-syncing/scanresult/scanResult.d.ts +24 -3
- package/dist/scanning-syncing/scanresult/scanResult.js +159 -97
- package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
- package/dist/scanning-syncing/worker-mains/coordinator-main.d.ts +1 -0
- package/dist/scanning-syncing/worker-mains/coordinator-main.js +24 -0
- package/dist/scanning-syncing/worker-mains/cpubound-main.d.ts +5 -0
- package/dist/scanning-syncing/worker-mains/cpubound-main.js +122 -0
- package/dist/scanning-syncing/worker-mains/worker.d.ts +1 -1
- package/dist/scanning-syncing/worker-mains/worker.js +62 -6
- package/dist/viewpair/ViewPair.d.ts +30 -7
- package/dist/viewpair/ViewPair.js +56 -118
- package/dist/wasm-processing/wasmFile.js +1 -1
- package/package.json +22 -24
- package/dist/io/extension.d.ts +0 -18
- package/dist/io/extension.js +0 -11
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, } from "../../api";
|
|
2
|
+
import { markWorkItemAsDone, } from "./scanLoop";
|
|
3
|
+
import { makeWorkItem, scanLoop } from "./scanLoop";
|
|
4
|
+
import { cullTooLargeScanHeight, getNonHaltedWallets, getPathPrefix, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, } from "../scanSettings";
|
|
5
|
+
import { findRange, initScanCacheFile, lastRange, makeCacheRangeForHeight, mergeRanges, } from "./scanCache";
|
|
6
|
+
import { sendToCpuWorker } from "../worker-mains/cpubound-main";
|
|
7
|
+
import { log } from "../../io/logging";
|
|
8
|
+
/**
|
|
9
|
+
* this depends only on ScanSettings.json start_height and wallet caches scanned_ranges
|
|
10
|
+
* side effect: will init wallet cache file if it does not exist
|
|
11
|
+
* side effect: will merge scan ranges + add subaddreses to existing cache files
|
|
12
|
+
* @param scan_settings_path
|
|
13
|
+
*/
|
|
14
|
+
export async function findWorkToBeDone(scan_settings_path = SCAN_SETTINGS_STORE_NAME_DEFAULT, pathPrefix) {
|
|
15
|
+
const parts = scan_settings_path.split("/");
|
|
16
|
+
const basename = parts.pop();
|
|
17
|
+
const dir = parts.join("/");
|
|
18
|
+
const prefix = dir ? `${dir}/` : "";
|
|
19
|
+
const scan_settings = await openScanSettingsFile(scan_settings_path);
|
|
20
|
+
if (!scan_settings)
|
|
21
|
+
return false;
|
|
22
|
+
const total_start_height = await cullTooLargeScanHeight(scan_settings.node_url, scan_settings_path);
|
|
23
|
+
const wallets = getNonHaltedWallets(scan_settings);
|
|
24
|
+
if (!wallets.length)
|
|
25
|
+
return false;
|
|
26
|
+
const potential_anchor_ranges = [];
|
|
27
|
+
const wallet_caches = [];
|
|
28
|
+
const wallet_configs = [];
|
|
29
|
+
let wallet_without_anchor_at_start_height = false;
|
|
30
|
+
for (const wallet of wallets) {
|
|
31
|
+
const walletSettingsWithKeys = await walletSettingsPlusKeys({
|
|
32
|
+
...wallet,
|
|
33
|
+
node_url: scan_settings.node_url,
|
|
34
|
+
start_height: total_start_height,
|
|
35
|
+
});
|
|
36
|
+
const newWalletViewPair = await ViewPair.create(wallet.primary_address, walletSettingsWithKeys.secret_view_key, wallet.subaddress_index, walletSettingsWithKeys.node_url);
|
|
37
|
+
const walletCache = await initScanCacheFile(newWalletViewPair, scan_settings_path, pathPrefix ?? prefix);
|
|
38
|
+
if (!walletCache)
|
|
39
|
+
throw new Error("wallet cache not found and new one could not be created for " +
|
|
40
|
+
wallet.primary_address);
|
|
41
|
+
wallet_caches.push(walletCache);
|
|
42
|
+
wallet_configs.push({
|
|
43
|
+
primary_address: wallet.primary_address,
|
|
44
|
+
secret_view_key: walletSettingsWithKeys.secret_view_key,
|
|
45
|
+
secret_spend_key: walletSettingsWithKeys.secret_spend_key,
|
|
46
|
+
subaddress_index: wallet.subaddress_index || 0,
|
|
47
|
+
cache: walletCache,
|
|
48
|
+
});
|
|
49
|
+
const range = findRange(walletCache.scanned_ranges, total_start_height);
|
|
50
|
+
if (!range) {
|
|
51
|
+
wallet_without_anchor_at_start_height = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
potential_anchor_ranges.push(range);
|
|
55
|
+
}
|
|
56
|
+
//go over all wallets and make sure they have an anchor range at start_height
|
|
57
|
+
if (wallet_without_anchor_at_start_height) {
|
|
58
|
+
const range_at_start = await makeCacheRangeForHeight(total_start_height, scan_settings.node_url);
|
|
59
|
+
potential_anchor_ranges.push(range_at_start);
|
|
60
|
+
for (const wallet_cache of wallet_caches) {
|
|
61
|
+
// only add the range to wallets that don't already have one
|
|
62
|
+
if (!findRange(wallet_cache.scanned_ranges, total_start_height)) {
|
|
63
|
+
log("findWorkToBeDone", "inserting range_at_start");
|
|
64
|
+
wallet_cache.scanned_ranges.push(range_at_start);
|
|
65
|
+
wallet_cache.scanned_ranges = mergeRanges(wallet_cache.scanned_ranges);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const anchor_range = potential_anchor_ranges.reduce((a, b) => a.end < b.end ? a : b);
|
|
70
|
+
const start_height = anchor_range.end;
|
|
71
|
+
// connection settings scanned_ranges is reset on every scan
|
|
72
|
+
// (done in setupBlocksBufferGenerator init)
|
|
73
|
+
// ( they cant contain newer ranges then resulting start height after
|
|
74
|
+
// lowest fast forward start height on all wallets )
|
|
75
|
+
return {
|
|
76
|
+
wallet_configs,
|
|
77
|
+
start_height,
|
|
78
|
+
anchor_range,
|
|
79
|
+
scan_settings,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function workToBeDoneForBatch(cache, batch_meta_infos) {
|
|
83
|
+
const begin_height = batch_meta_infos[0].block_height;
|
|
84
|
+
const end_height = batch_meta_infos[batch_meta_infos.length - 1].block_height;
|
|
85
|
+
log("workToBeDoneForBatch", [begin_height, end_height]);
|
|
86
|
+
const foundRange = findRange(cache.scanned_ranges, begin_height);
|
|
87
|
+
log("workToBeDoneForBatch", ["foundRange", foundRange]);
|
|
88
|
+
if (foundRange) {
|
|
89
|
+
const fullycovered = cache.scanned_ranges.find((r) => r.start <= begin_height && r.end > end_height);
|
|
90
|
+
if (fullycovered) {
|
|
91
|
+
return "skip";
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// NORMAL CASE directly in front of tip
|
|
95
|
+
const tip = foundRange.block_hashes.at(0);
|
|
96
|
+
if (!tip)
|
|
97
|
+
throw new Error("[workToBeDoneForBatch] tip not found, malformed range that covers the work to be done for this batch");
|
|
98
|
+
const tipindex = findTipIndex(batch_meta_infos, tip);
|
|
99
|
+
if (tipindex === "reorg_found") {
|
|
100
|
+
log("workToBeDoneForBatch", "reorg found");
|
|
101
|
+
return { from: 0 };
|
|
102
|
+
}
|
|
103
|
+
else if (tipindex === "empty_blocks_array") {
|
|
104
|
+
return "skip";
|
|
105
|
+
}
|
|
106
|
+
log("workToBeDoneForBatch", ["tipindex", tipindex]);
|
|
107
|
+
return { from: tipindex }; // NORMAL case of scheduling directly in front of the tip
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// NORMAL case of scheduling work ahead of the already processed ranges, with gap so we can do CPU work in parallel
|
|
112
|
+
// didnt see this range might be ahead and will be processed in order after the prior batches are processed
|
|
113
|
+
return { from: 0 };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* called when the blocks buffer generator yields "blocks_buffer_changed".
|
|
118
|
+
* adds new work items for blocks buffer items not yet referenced.
|
|
119
|
+
* per wallet
|
|
120
|
+
*/
|
|
121
|
+
export function makeWorkItemsFromBlocksBuffer(blocksBuffer, workItemBuffer, walletConfig, from, to) {
|
|
122
|
+
// add work items for blocks buffer items not yet referenced
|
|
123
|
+
for (const batch of blocksBuffer) {
|
|
124
|
+
const alreadyReferenced = workItemBuffer.some((w) => w.batch.local_uuid === batch.local_uuid &&
|
|
125
|
+
w.walletConfig.primary_address === walletConfig.primary_address);
|
|
126
|
+
if (!alreadyReferenced) {
|
|
127
|
+
const workToBeDone = workToBeDoneForBatch(walletConfig.cache, batch.get_blocks_result_meta.block_infos);
|
|
128
|
+
if (workToBeDone === "skip") {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const workItem = makeWorkItem(walletConfig, batch, from, to);
|
|
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
|
+
]);
|
|
135
|
+
workItemBuffer.push(workItem);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function makeWorkItemsForAllWallets(wallet_configs, blocksBuffer, workBuffer) {
|
|
140
|
+
for (const wc of wallet_configs) {
|
|
141
|
+
makeWorkItemsFromBlocksBuffer(blocksBuffer, workBuffer, wc);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* called when a work item at the left end of the work buffer is done.
|
|
146
|
+
* shifts done items off the left, and removes their batch from the
|
|
147
|
+
* blocks buffer if no remaining work items reference it.
|
|
148
|
+
*/
|
|
149
|
+
export function reconcileWorkItemDone(blocksBuffer, workItemBuffer) {
|
|
150
|
+
log("reconcileWorkItemDone", [
|
|
151
|
+
`workItemBuffer.length=${workItemBuffer.length} , blocksBuffer.length=${blocksBuffer.length}`,
|
|
152
|
+
]);
|
|
153
|
+
while (workItemBuffer.length > 0 &&
|
|
154
|
+
workItemBuffer[0].status === "process_result_done") {
|
|
155
|
+
const removed = workItemBuffer.shift();
|
|
156
|
+
const stillReferenced = workItemBuffer.some((w) => w.batch.local_uuid === removed.batch.local_uuid);
|
|
157
|
+
log("reconcileWorkItemDone", [
|
|
158
|
+
`workItem: ${removed.work_uuid.slice()} removed. stillReferenced=${stillReferenced}`,
|
|
159
|
+
]);
|
|
160
|
+
if (!stillReferenced) {
|
|
161
|
+
const idx = blocksBuffer.findIndex((b) => b.local_uuid === removed.batch.local_uuid);
|
|
162
|
+
// this really means we have to save work items scanCache to file
|
|
163
|
+
// before setting done = true
|
|
164
|
+
if (idx !== -1)
|
|
165
|
+
blocksBuffer.splice(idx, 1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* one round of the select race. takes two primed promises (blocks, scan),
|
|
171
|
+
* races them, returns winner info. the loser stays in flight.
|
|
172
|
+
*/
|
|
173
|
+
export async function raceStep(blocksPromise, scanPromise) {
|
|
174
|
+
const winner = await Promise.race([
|
|
175
|
+
blocksPromise.then((v) => ({ src: "blocks", value: v })),
|
|
176
|
+
scanPromise.then((v) => ({ src: "scan", value: v })),
|
|
177
|
+
]);
|
|
178
|
+
const value = winner.value;
|
|
179
|
+
const done = !!winner.value.done;
|
|
180
|
+
return { src: winner.src, value, done };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* handle a yield from the blocks buffer fetch loop.
|
|
184
|
+
*/
|
|
185
|
+
export async function handleBlocksYield(value, scanSettingsPath) {
|
|
186
|
+
if ("local_uuid" in value && typeof value.local_uuid === "string") {
|
|
187
|
+
return { isBlocksBufferChanged: true };
|
|
188
|
+
}
|
|
189
|
+
await handleConnectionStatusChanges(value, scanSettingsPath);
|
|
190
|
+
return { isBlocksBufferChanged: false };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* process a scan result for a completed work item.
|
|
194
|
+
* updates the cache, writes to disk, marks work item done, reconciles.
|
|
195
|
+
*/
|
|
196
|
+
export async function processScanResultForWorkItem(value, workBuffer, blocksBuffer, pathPrefix, secret_spend_key) {
|
|
197
|
+
if (value.type !== "Ready" || !value.result || !value.work_uuid)
|
|
198
|
+
return;
|
|
199
|
+
const item = await markWorkItemAsDone(value, workBuffer);
|
|
200
|
+
log("processScanResultForWorkItem", `item_work_uuid=${item?.work_uuid}`);
|
|
201
|
+
if (!item || item.status !== "scanwork_done")
|
|
202
|
+
throw new Error("[processScanResultForWorkItem] item not found or not scanwork not done. item_status=" +
|
|
203
|
+
item?.status);
|
|
204
|
+
const firstBlock = item.batch.get_blocks_result_meta.block_infos[item.from];
|
|
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
|
+
]);
|
|
211
|
+
const lastBlock = item.batch.get_blocks_result_meta.block_infos[item.to];
|
|
212
|
+
const cache = item.walletConfig.cache;
|
|
213
|
+
await processScanResult({
|
|
214
|
+
from_height: firstBlock.block_height,
|
|
215
|
+
to_height: lastBlock.block_height,
|
|
216
|
+
result: value.result,
|
|
217
|
+
scanCache: cache,
|
|
218
|
+
secret_spend_key,
|
|
219
|
+
});
|
|
220
|
+
await writeCacheToFile(cache, pathPrefix);
|
|
221
|
+
item.status = "process_result_done";
|
|
222
|
+
reconcileWorkItemDone(blocksBuffer, workBuffer);
|
|
223
|
+
//because cache is tied to the workitems by reference,
|
|
224
|
+
// the following workitems will have the most recent cache
|
|
225
|
+
//TODO: if we do cpu workers we need to be sure to wait with the processing at the top of this functon
|
|
226
|
+
// until all workitems for this wallet before it (to the left of it) are done
|
|
227
|
+
// the cpu worker loop generator will have to ensure the order of this
|
|
228
|
+
// currently as a sideeffect of the work scheduling function on fetch result aka blocks buffer changed,
|
|
229
|
+
// this is already implicitly handled
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* process a scan result for a completed work item.
|
|
233
|
+
* updates the cache, writes to disk, marks work item done, reconciles blockbuffer with this.
|
|
234
|
+
*/
|
|
235
|
+
export async function processWorkItem(item, workBuffer, blocksBuffer, pathPrefix, secret_spend_key) {
|
|
236
|
+
if (item.status !== "scanwork_done")
|
|
237
|
+
throw new Error("[processWorkItem] item not found or not scanwork not done. item_status=" +
|
|
238
|
+
item?.status);
|
|
239
|
+
const cache = item.walletConfig.cache;
|
|
240
|
+
const last_in_cache = lastRange(cache.scanned_ranges)?.block_hashes.at(0);
|
|
241
|
+
if (typeof last_in_cache !== "undefined") {
|
|
242
|
+
const actual_tip_index = findTipIndex(item.batch.get_blocks_result_meta.block_infos, last_in_cache);
|
|
243
|
+
if (typeof actual_tip_index === "number") {
|
|
244
|
+
item.from = actual_tip_index;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const firstBlock = item.batch.get_blocks_result_meta.block_infos[item.from];
|
|
248
|
+
log("processWorkItem", [
|
|
249
|
+
`block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} from=${item.from} to=${item.to}`,
|
|
250
|
+
]);
|
|
251
|
+
const lastBlock = item.batch.get_blocks_result_meta.block_infos[item.to];
|
|
252
|
+
log("processWorkItem", [
|
|
253
|
+
item.walletConfig.primary_address.slice(0, 6),
|
|
254
|
+
"@",
|
|
255
|
+
firstBlock.block_height,
|
|
256
|
+
"-",
|
|
257
|
+
lastBlock.block_height,
|
|
258
|
+
]);
|
|
259
|
+
let res;
|
|
260
|
+
try {
|
|
261
|
+
res = await processScanResult({
|
|
262
|
+
from_height: firstBlock.block_height,
|
|
263
|
+
to_height: lastBlock.block_height,
|
|
264
|
+
result: item.result,
|
|
265
|
+
scanCache: cache,
|
|
266
|
+
secret_spend_key,
|
|
267
|
+
});
|
|
268
|
+
log("processWorkItem", ["res=", res]);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
console.error("[processWorkItem] error=", error);
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
await writeCacheToFile(cache, pathPrefix);
|
|
275
|
+
item.status = "process_result_done";
|
|
276
|
+
log("processWorkItem", `process_result_done work_uuid=${item?.work_uuid}`);
|
|
277
|
+
// remove from blocksbuffer if no more work items reference it
|
|
278
|
+
reconcileWorkItemDone(blocksBuffer, workBuffer);
|
|
279
|
+
//because cache is tied to the workitems by reference,
|
|
280
|
+
// the following workitems will have the most recent cache
|
|
281
|
+
// if we do cpu workers we need to be sure to wait with the processing at the top of this functon
|
|
282
|
+
// until all workitems for this wallet before it (to the left of it) are done
|
|
283
|
+
// the cpu worker loop generator will have to ensure the order of this
|
|
284
|
+
// currently as a sideeffect of the work scheduling function on fetch result aka blocks buffer changed,
|
|
285
|
+
// this is already implicitly handled
|
|
286
|
+
return res;
|
|
287
|
+
}
|
|
288
|
+
function logBufStatus(blocksBuffer, workBuffer, ports, label) {
|
|
289
|
+
const bb = blocksBuffer.map((b) => b.get_blocks_result_meta.block_infos[0]?.block_height +
|
|
290
|
+
"-" +
|
|
291
|
+
b.get_blocks_result_meta.block_infos.at(-1)?.block_height);
|
|
292
|
+
const wb = workBuffer.map((w) => w.status +
|
|
293
|
+
w.walletConfig.primary_address.slice(0, 6) +
|
|
294
|
+
"@" +
|
|
295
|
+
w.batch.get_blocks_result_meta.block_infos[w.from]?.block_height +
|
|
296
|
+
"-" +
|
|
297
|
+
w.batch.get_blocks_result_meta.block_infos[w.to]?.block_height);
|
|
298
|
+
log("logBufStatus", [
|
|
299
|
+
`[buf] ${label} blocks=[${bb.join(",")}] work=[${wb.join(",")}]`,
|
|
300
|
+
]);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* coordinator main: async generator that drives the full scan cycle.
|
|
304
|
+
* takes only scanSettingsPath, derives everything else internally.
|
|
305
|
+
* yields events for some of the underlying generator events,
|
|
306
|
+
* look at CoordinatorEvent, processing not decoupled from cpu bound scan work,
|
|
307
|
+
* as this single threaded, there is no point
|
|
308
|
+
*/
|
|
309
|
+
export async function* coordinatorMain(scanSettingsPath, pathPrefix) {
|
|
310
|
+
const ctx = await setupCoordinator(scanSettingsPath, pathPrefix);
|
|
311
|
+
if (!ctx)
|
|
312
|
+
throw new Error("[coordinatorMain] findWorkToBeDone returned false");
|
|
313
|
+
const work_to_be_done = ctx.work_to_be_done;
|
|
314
|
+
const blocksBuffer = ctx.blocksBuffer;
|
|
315
|
+
const workBuffer = ctx.workBuffer;
|
|
316
|
+
const blocksGenerator = ctx.blocksGenerator;
|
|
317
|
+
let blocksPromise = blocksGenerator.next();
|
|
318
|
+
const scanGens = new Map();
|
|
319
|
+
const scanPromises = new Map();
|
|
320
|
+
for (const wc of work_to_be_done.wallet_configs) {
|
|
321
|
+
scanGens.set(wc.primary_address, scanLoop({
|
|
322
|
+
primary_address: wc.primary_address,
|
|
323
|
+
secret_view_key: wc.secret_view_key,
|
|
324
|
+
secret_spend_key: wc.secret_spend_key,
|
|
325
|
+
subaddress_index: wc.subaddress_index,
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
// prime all scan generators
|
|
329
|
+
// ( technically no need to do this as loop + scan generator + workbuffer structure mean it will happen automatically)
|
|
330
|
+
for (const [, gen] of scanGens) {
|
|
331
|
+
await gen.next();
|
|
332
|
+
}
|
|
333
|
+
while (true) {
|
|
334
|
+
const races = [blocksPromise.then((v) => ({ src: "blocks", value: v }))];
|
|
335
|
+
for (const [addr, p] of scanPromises) {
|
|
336
|
+
races.push(p.then((v) => ({ src: "scan", addr, value: v })));
|
|
337
|
+
}
|
|
338
|
+
const winner = await Promise.race(races);
|
|
339
|
+
log("coordinatorMain", [
|
|
340
|
+
"winner=" + winner.src + " addr=" + String(winner.addr ?? "").slice(0, 8),
|
|
341
|
+
]);
|
|
342
|
+
if (winner.src === "blocks") {
|
|
343
|
+
const result = winner.value.value;
|
|
344
|
+
const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
|
|
345
|
+
if (isBlocksBufferChanged) {
|
|
346
|
+
makeWorkItemsForAllWallets(work_to_be_done.wallet_configs, blocksBuffer, workBuffer);
|
|
347
|
+
// start scan promises for wallets that now have work
|
|
348
|
+
for (const wc of work_to_be_done.wallet_configs) {
|
|
349
|
+
const addr = wc.primary_address;
|
|
350
|
+
if (scanPromises.has(addr))
|
|
351
|
+
continue;
|
|
352
|
+
const gen = scanGens.get(addr);
|
|
353
|
+
log("coordinatorMain", ["scanGens.get(addr)", gen]);
|
|
354
|
+
if (!gen)
|
|
355
|
+
continue;
|
|
356
|
+
const item = workBuffer.find((x) => x.status === "fresh" && x.walletConfig.primary_address === addr);
|
|
357
|
+
if (item) {
|
|
358
|
+
item.status = "scanwork_in_progress";
|
|
359
|
+
scanPromises.set(addr, gen.next(item));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
logBufStatus(blocksBuffer, workBuffer, scanPromises, "after_blocks");
|
|
363
|
+
yield { type: "blocks_buffer_changed" };
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
logBufStatus(blocksBuffer, workBuffer, scanPromises, "fetch_conn");
|
|
367
|
+
yield { type: "connection_status", status: result };
|
|
368
|
+
}
|
|
369
|
+
blocksPromise = blocksGenerator.next();
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const addr = winner.addr;
|
|
373
|
+
const gen = scanGens.get(addr);
|
|
374
|
+
const value = winner.value.value;
|
|
375
|
+
if (value.type === "InProgress") {
|
|
376
|
+
scanPromises.set(addr, gen.next());
|
|
377
|
+
}
|
|
378
|
+
else if (value.type === "Ready") {
|
|
379
|
+
const wc = work_to_be_done.wallet_configs.find((x) => x.primary_address === addr);
|
|
380
|
+
if (!wc)
|
|
381
|
+
throw new Error("[coordinatorMain] wallet config not found on process result");
|
|
382
|
+
await processScanResultForWorkItem(value, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wc?.secret_spend_key);
|
|
383
|
+
const nextItem = workBuffer.find((x) => x.status !== "process_result_done" &&
|
|
384
|
+
x.walletConfig.primary_address === addr);
|
|
385
|
+
if (nextItem) {
|
|
386
|
+
scanPromises.set(addr, gen.next(nextItem));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
scanPromises.delete(addr);
|
|
390
|
+
}
|
|
391
|
+
logBufStatus(blocksBuffer, workBuffer, scanPromises, "after_scan");
|
|
392
|
+
yield {
|
|
393
|
+
type: "scan_ready",
|
|
394
|
+
address: addr,
|
|
395
|
+
result: value,
|
|
396
|
+
newCache: wc.cache,
|
|
397
|
+
changed_outputs: [],
|
|
398
|
+
};
|
|
399
|
+
// signal idle when no scans active and nothing buffered
|
|
400
|
+
if (scanPromises.size === 0 && blocksBuffer.length === 0) {
|
|
401
|
+
yield { type: "all_idle" };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
export async function setupCoordinator(scanSettingsPath, pathPrefix) {
|
|
408
|
+
const work_to_be_done = await findWorkToBeDone(scanSettingsPath, pathPrefix);
|
|
409
|
+
if (!work_to_be_done)
|
|
410
|
+
return false;
|
|
411
|
+
const { generator: blocksGenerator, blocksBuffer } = await setupBlocksBufferGenerator({
|
|
412
|
+
nodeUrl: work_to_be_done.scan_settings.node_url,
|
|
413
|
+
startHeight: work_to_be_done.start_height,
|
|
414
|
+
anchor_range: work_to_be_done.anchor_range,
|
|
415
|
+
scanSettingsPath,
|
|
416
|
+
});
|
|
417
|
+
const workBuffer = [];
|
|
418
|
+
//TODO: refactor to per wallet buffer
|
|
419
|
+
// const walletSyncInfos: WalletSyncInfo[] = [];
|
|
420
|
+
// for (const wallet of work_to_be_done.wallet_configs) {
|
|
421
|
+
// walletSyncInfos.push({
|
|
422
|
+
// walletConfig: wallet,
|
|
423
|
+
// work_buffer: [],
|
|
424
|
+
// });
|
|
425
|
+
// }
|
|
426
|
+
return {
|
|
427
|
+
// walletSyncInfos,
|
|
428
|
+
blocksGenerator,
|
|
429
|
+
workBuffer,
|
|
430
|
+
blocksBuffer,
|
|
431
|
+
work_to_be_done,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* coordinator main (multithreaded): dispatches scan work to CPU workers
|
|
436
|
+
* via MessagePorts, processes results in order per wallet.
|
|
437
|
+
* falls back to single-threaded coordinator if no cpuPorts provided.
|
|
438
|
+
*/
|
|
439
|
+
export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix, cpuPorts) {
|
|
440
|
+
if (!cpuPorts || cpuPorts.length === 0) {
|
|
441
|
+
// throw new Error(
|
|
442
|
+
// "[coordinatorMain Multithreaded] cpuPorts empty, there must be at least cpu worker",
|
|
443
|
+
// );
|
|
444
|
+
log("coordinatorMainMultithreaded", "fallback to single-threaded");
|
|
445
|
+
yield* coordinatorMain(scanSettingsPath, pathPrefix);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const ctx = await setupCoordinator(scanSettingsPath, pathPrefix);
|
|
449
|
+
if (!ctx)
|
|
450
|
+
throw new Error("[coordinatorMain Multithreaded] findWorkToBeDone returned false");
|
|
451
|
+
const work_to_be_done = ctx.work_to_be_done;
|
|
452
|
+
const blocksBuffer = ctx.blocksBuffer;
|
|
453
|
+
const workBuffer = ctx.workBuffer;
|
|
454
|
+
const blocksGenerator = ctx.blocksGenerator;
|
|
455
|
+
let blocksPromise = blocksGenerator.next();
|
|
456
|
+
const freePorts = [];
|
|
457
|
+
for (const port of cpuPorts) {
|
|
458
|
+
const ps = {
|
|
459
|
+
port,
|
|
460
|
+
promise: null,
|
|
461
|
+
};
|
|
462
|
+
freePorts.push(ps);
|
|
463
|
+
}
|
|
464
|
+
let race_count = 0;
|
|
465
|
+
while (true) {
|
|
466
|
+
race_count++;
|
|
467
|
+
log("coordinatorMainMultithreaded", ["race_count", race_count]);
|
|
468
|
+
await scheduleWorkOnCpuPorts(freePorts, workBuffer);
|
|
469
|
+
const scan_promises = freePorts
|
|
470
|
+
.filter((ps) => ps.promise)
|
|
471
|
+
.map((ps) => ps.promise);
|
|
472
|
+
const races = [
|
|
473
|
+
...scan_promises,
|
|
474
|
+
blocksPromise.then((v) => ({
|
|
475
|
+
src: "blocks",
|
|
476
|
+
value: v,
|
|
477
|
+
})),
|
|
478
|
+
];
|
|
479
|
+
log("coordinatorMainMultithreaded", ["races", races]);
|
|
480
|
+
const winner = await Promise.race(races);
|
|
481
|
+
log("coordinatorMainMultithreaded", "cpu ports status");
|
|
482
|
+
if ("src" in winner && winner.src === "blocks") {
|
|
483
|
+
const result = winner.value.value;
|
|
484
|
+
const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
|
|
485
|
+
if (isBlocksBufferChanged) {
|
|
486
|
+
//todo pass new blocksbuffer items after simplified blocksbuffer fetch loop
|
|
487
|
+
makeWorkItemsForAllWallets(work_to_be_done.wallet_configs, [result], workBuffer);
|
|
488
|
+
yield { type: "blocks_buffer_changed" };
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
yield { type: "connection_status", status: result };
|
|
492
|
+
}
|
|
493
|
+
blocksPromise = blocksGenerator.next();
|
|
494
|
+
}
|
|
495
|
+
logBufStatus(blocksBuffer, workBuffer, freePorts, "after_winner");
|
|
496
|
+
for (const wallet of work_to_be_done.wallet_configs) {
|
|
497
|
+
const workitems_for_wallet = workBuffer.filter((x) => x.walletConfig.primary_address === wallet.primary_address);
|
|
498
|
+
const processable = [];
|
|
499
|
+
for (const w of workitems_for_wallet) {
|
|
500
|
+
if (w.status === "scanwork_done") {
|
|
501
|
+
processable.push(w);
|
|
502
|
+
}
|
|
503
|
+
else if (w.status === "process_result_done") {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const to_be_processed = processable[0];
|
|
511
|
+
if (!to_be_processed)
|
|
512
|
+
continue;
|
|
513
|
+
const res = await processWorkItem(to_be_processed, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wallet.secret_spend_key);
|
|
514
|
+
// log("coordinatorMainMultithreaded", "processWorkItem result");
|
|
515
|
+
yield {
|
|
516
|
+
type: "scan_ready",
|
|
517
|
+
address: wallet.primary_address,
|
|
518
|
+
result: {
|
|
519
|
+
type: "Ready",
|
|
520
|
+
work_uuid: to_be_processed.work_uuid,
|
|
521
|
+
result: to_be_processed.result,
|
|
522
|
+
},
|
|
523
|
+
newCache: wallet.cache,
|
|
524
|
+
changed_outputs: res.changed_outputs,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
530
|
+
const fresh_work = work_buffer.filter((x) => x.status === "fresh");
|
|
531
|
+
const empty_ports = ports.filter((x) => x.promise === null);
|
|
532
|
+
let fresh_work_index = 0;
|
|
533
|
+
for (const port_status of empty_ports) {
|
|
534
|
+
const item = fresh_work[fresh_work_index];
|
|
535
|
+
fresh_work_index++;
|
|
536
|
+
if (!item)
|
|
537
|
+
return;
|
|
538
|
+
let resolve_port;
|
|
539
|
+
let resolve_workstart;
|
|
540
|
+
const workstart_promise = new Promise((resolve) => {
|
|
541
|
+
resolve_workstart = resolve;
|
|
542
|
+
});
|
|
543
|
+
const onmessage = (event) => {
|
|
544
|
+
const msg = event.data;
|
|
545
|
+
// handle the result here:
|
|
546
|
+
log("scheduleWorkOnCpuPorts", ["onmessage result", msg]);
|
|
547
|
+
if (msg.work_uuid !== item.work_uuid) {
|
|
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
|
+
]);
|
|
554
|
+
throw new Error("[scheduleWorkOnCpuPorts] wrong work_uuid in msg");
|
|
555
|
+
}
|
|
556
|
+
if (msg.type === "WORKSTART") {
|
|
557
|
+
resolve_workstart();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (item.status !== "scanwork_in_progress") {
|
|
561
|
+
log("scheduleWorkOnCpuPorts", "wrong status in msg");
|
|
562
|
+
throw new Error("[scheduleWorkOnCpuPorts] wrong status in msg");
|
|
563
|
+
}
|
|
564
|
+
item.result = msg.result;
|
|
565
|
+
item.status = "scanwork_done";
|
|
566
|
+
port_status.promise = null;
|
|
567
|
+
resolve_port(msg);
|
|
568
|
+
};
|
|
569
|
+
port_status.port.onmessage = onmessage;
|
|
570
|
+
item.status = "scanwork_in_progress";
|
|
571
|
+
log("scheduleWorkOnCpuPorts", ["scheduling work item", item.work_uuid]);
|
|
572
|
+
port_status.promise = new Promise((resolve) => {
|
|
573
|
+
resolve_port = resolve;
|
|
574
|
+
});
|
|
575
|
+
const strippedItem = {
|
|
576
|
+
...item,
|
|
577
|
+
walletConfig: {
|
|
578
|
+
primary_address: item.walletConfig.primary_address,
|
|
579
|
+
secret_view_key: item.walletConfig.secret_view_key,
|
|
580
|
+
//secret_spend_key: item.walletConfig.secret_spend_key, only needed for processResult to make ownkeyimages
|
|
581
|
+
subaddress_index: item.walletConfig.subaddress_index,
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
for (let attempt = 1;; attempt++) {
|
|
585
|
+
sendToCpuWorker(port_status.port, strippedItem);
|
|
586
|
+
try {
|
|
587
|
+
await Promise.race([
|
|
588
|
+
workstart_promise,
|
|
589
|
+
sleep(1000).then(() => Promise.reject(new Error("timeout"))),
|
|
590
|
+
]);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
log("scheduleWorkOnCpuPorts", [
|
|
595
|
+
"resend attempt",
|
|
596
|
+
attempt,
|
|
597
|
+
item.work_uuid,
|
|
598
|
+
]);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type GetBlocksBinBufferItem, type ScanResult, type WalletConfig, type WalletConfigPlusCache } from "../../api";
|
|
2
|
+
export type WorkItem = {
|
|
3
|
+
walletConfig: WalletConfigPlusCache;
|
|
4
|
+
batch: GetBlocksBinBufferItem;
|
|
5
|
+
work_uuid: string;
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
status: "fresh" | "scanwork_in_progress" | "scanwork_done" | "process_result_done";
|
|
9
|
+
result?: ScanResult;
|
|
10
|
+
};
|
|
11
|
+
export declare function makeWorkItem(walletConfig: WalletConfigPlusCache, batch: GetBlocksBinBufferItem, from?: number, to?: number): WorkItem;
|
|
12
|
+
export type ScanLoopIteratorResult = IteratorYieldResult<ScanLoopYield>;
|
|
13
|
+
export type ScanLoopInput = WorkItem | "cancel" | undefined;
|
|
14
|
+
export type ScanLoopYield = {
|
|
15
|
+
type: "Ready" | "InProgress";
|
|
16
|
+
work_uuid?: string;
|
|
17
|
+
result?: ScanResult;
|
|
18
|
+
};
|
|
19
|
+
export declare function scanLoop(wallet: WalletConfig): AsyncGenerator<ScanLoopYield, void, ScanLoopInput>;
|
|
20
|
+
export declare function markWorkItemAsDone(loop_event: ScanLoopYield, work_buffer: WorkItem[]): Promise<WorkItem | undefined>;
|