@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.
Files changed (51) hide show
  1. package/dist/api.d.ts +5 -2
  2. package/dist/api.js +5 -1
  3. package/dist/io/atomicWrite.js +3 -1
  4. package/dist/io/indexedDB.js +7 -2
  5. package/dist/io/logging.d.ts +40 -0
  6. package/dist/io/logging.js +172 -0
  7. package/dist/keypairs-seeds/keypairs.d.ts +21 -0
  8. package/dist/keypairs-seeds/keypairs.js +21 -0
  9. package/dist/node-interaction/binaryEndpoints.d.ts +31 -2
  10. package/dist/node-interaction/binaryEndpoints.js +61 -43
  11. package/dist/node-interaction/jsonEndpoints.d.ts +257 -130
  12. package/dist/node-interaction/jsonEndpoints.js +159 -119
  13. package/dist/node-interaction/nodeUrl.d.ts +21 -6
  14. package/dist/node-interaction/nodeUrl.js +50 -8
  15. package/dist/scanning-syncing/backgroundWorker.d.ts +10 -6
  16. package/dist/scanning-syncing/backgroundWorker.js +88 -39
  17. package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.d.ts +44 -0
  18. package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +355 -0
  19. package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.d.ts +15 -0
  20. package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +28 -0
  21. package/dist/scanning-syncing/{scanresult → blocksbuffer}/getBlocksbinBuffer.d.ts +5 -5
  22. package/dist/scanning-syncing/{scanresult → blocksbuffer}/getBlocksbinBuffer.js +7 -5
  23. package/dist/scanning-syncing/connectionStatus.d.ts +33 -9
  24. package/dist/scanning-syncing/connectionStatus.js +89 -19
  25. package/dist/scanning-syncing/scanSettings.d.ts +21 -0
  26. package/dist/scanning-syncing/scanSettings.js +29 -0
  27. package/dist/scanning-syncing/scanresult/reorg.d.ts +0 -11
  28. package/dist/scanning-syncing/scanresult/reorg.js +39 -16
  29. package/dist/scanning-syncing/scanresult/scanCache.d.ts +16 -2
  30. package/dist/scanning-syncing/scanresult/scanCache.js +65 -5
  31. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +18 -0
  32. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +56 -11
  33. package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +127 -0
  34. package/dist/scanning-syncing/scanresult/scanCoordination.js +602 -0
  35. package/dist/scanning-syncing/scanresult/scanLoop.d.ts +20 -0
  36. package/dist/scanning-syncing/scanresult/scanLoop.js +116 -0
  37. package/dist/scanning-syncing/scanresult/scanResult.d.ts +24 -3
  38. package/dist/scanning-syncing/scanresult/scanResult.js +159 -97
  39. package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
  40. package/dist/scanning-syncing/worker-mains/coordinator-main.d.ts +1 -0
  41. package/dist/scanning-syncing/worker-mains/coordinator-main.js +24 -0
  42. package/dist/scanning-syncing/worker-mains/cpubound-main.d.ts +5 -0
  43. package/dist/scanning-syncing/worker-mains/cpubound-main.js +122 -0
  44. package/dist/scanning-syncing/worker-mains/worker.d.ts +1 -1
  45. package/dist/scanning-syncing/worker-mains/worker.js +62 -6
  46. package/dist/viewpair/ViewPair.d.ts +30 -7
  47. package/dist/viewpair/ViewPair.js +56 -118
  48. package/dist/wasm-processing/wasmFile.js +1 -1
  49. package/package.json +22 -24
  50. package/dist/io/extension.d.ts +0 -18
  51. package/dist/io/extension.js +0 -11
@@ -0,0 +1,44 @@
1
+ import { NodeUrl, type BlockInfo, type CacheRange, type GetBlocksResultMeta } from "../../api";
2
+ import { type ConnectionSatusLastPacket, type ConnectionStatus, type ConnectionStatusSync } from "../connectionStatus";
3
+ export type GetBlocksBinBufferItem = {
4
+ local_uuid: string;
5
+ get_blocks_result_meta: GetBlocksResultMeta;
6
+ data: Uint8Array;
7
+ };
8
+ export declare const MAX_BLOCKS_BUFFER_SIZE = 10;
9
+ export type BlocksBufferIteratorResult = IteratorYieldResult<BlocksBufferLoopResult>;
10
+ export type BlocksBufferLoopResult = ConnectionSatusLastPacket | ConnectionStatusSync | GetBlocksBinBufferItem;
11
+ export declare function blocksBufferFetchLoop(node_url: string, start_height: number, blocks_buffer: GetBlocksBinBufferItem[], // pass by reference
12
+ connection_status: ConnectionStatus, // we make a local copy of this and pass last_packet and sync updates seperately
13
+ max_blocks_buffer_size?: number, anchor_range?: CacheRange, stopSync?: AbortSignal): AsyncGenerator<BlocksBufferLoopResult>;
14
+ export declare function makeBlocksBufferItem(result_meta: GetBlocksResultMeta, get_blocks_bin: Uint8Array): GetBlocksBinBufferItem;
15
+ export declare function popBlocksBufferItemFromIndex(blocks_buffer: GetBlocksBinBufferItem[], index: number): GetBlocksBinBufferItem | undefined;
16
+ export declare function findBufferitemBySplitHeight(blocks_buffer: GetBlocksBinBufferItem[], split_height: BlockInfo): GetBlocksBinBufferItem | undefined;
17
+ export declare function findBufferItemIndexByLocalId(blocks_buffer: GetBlocksBinBufferItem[], local_uuid: string): number;
18
+ export declare function popBlocksBufferItemsFromSplitHeight(blocks_buffer: GetBlocksBinBufferItem[], split_height: BlockInfo): GetBlocksBinBufferItem[];
19
+ export declare function doRPCrequest(nodeUrl: NodeUrl, current_range: CacheRange, stopSync?: AbortSignal): Promise<Uint8Array<ArrayBufferLike>>;
20
+ export type BlocksBufferScanStatus = {
21
+ current_range: CacheRange;
22
+ scanned_ranges: CacheRange[];
23
+ };
24
+ export type BlocksBufferReorgResult = BlocksBufferScanStatus & {
25
+ split_height?: BlockInfo;
26
+ };
27
+ export declare function initScannedRanges(node_url: string, start_height: number, scanned_ranges?: CacheRange[]): Promise<BlocksBufferScanStatus>;
28
+ export declare function updateBlocksBufferScanHeight(current_range: CacheRange, result_meta: GetBlocksResultMeta, scanned_ranges: CacheRange[]): Promise<BlocksBufferReorgResult>;
29
+ export declare function makeNewBlocksBufferScanRange(newRange: CacheRange, scanned_ranges: CacheRange[]): CacheRange;
30
+ export declare function handleBlocksBufferReorg(current_range: CacheRange, result_meta: GetBlocksResultMeta, scanned_ranges: CacheRange[], oldRange: CacheRange): Promise<BlocksBufferReorgResult>;
31
+ /**
32
+ * getBlocks.bin monero RPC call will block clients as peers,
33
+ * after 3 attempts of fetching a height higher than tip,
34
+ * this is ugly but it is what it is, so we need to do a get_info
35
+ * rpc call to get the tip height and reduce the start_height to the tip height,
36
+ * if it is larger than the tip height
37
+ * @param start_height
38
+ * @param node_url
39
+ * @returns Promise<number> a promise with the new potentially reduced start_height
40
+ */
41
+ export declare function reduceStartHeightToTip(start_height: number, node_url: string): Promise<number>;
42
+ export declare class CatastrophicReorgError extends Error {
43
+ name: string;
44
+ }
@@ -0,0 +1,355 @@
1
+ import { findRange, get_block_headers_range, get_info, mergeRanges, NodeUrl, sleep, } from "../../api";
2
+ import {} from "../connectionStatus";
3
+ import { log } from "../../io/logging";
4
+ export const MAX_BLOCKS_BUFFER_SIZE = 10;
5
+ // runs forever, fetching blocks from the node.
6
+ // handles scan level reorg detection and catastrophic reorg
7
+ export async function* blocksBufferFetchLoop(node_url, start_height, blocks_buffer, // pass by reference
8
+ connection_status, // we make a local copy of this and pass last_packet and sync updates seperately
9
+ max_blocks_buffer_size = MAX_BLOCKS_BUFFER_SIZE, anchor_range, stopSync) {
10
+ connection_status = structuredClone(connection_status);
11
+ const nodeUrl = await NodeUrl.create(node_url);
12
+ log("blocksBufferFetchLoop", ["NodeUrl:", nodeUrl.node_url, "start:", start_height]);
13
+ start_height = await reduceStartHeightToTip(start_height, nodeUrl.node_url);
14
+ // initialise ranges on first call
15
+ let { current_range, scanned_ranges } = await initScannedRanges(nodeUrl.node_url, start_height, anchor_range ? [anchor_range] : []);
16
+ connection_status.sync.scanned_ranges = scanned_ranges;
17
+ connection_status.last_packet = {
18
+ status: "no_connection_yet",
19
+ bytes_read: 0,
20
+ node_url: nodeUrl.node_url,
21
+ timestamp: new Date().toISOString(),
22
+ };
23
+ yield connection_status.last_packet;
24
+ while (true) {
25
+ // avoid overwriting last_packet in case of catastrophic reorg
26
+ // this generator will block on this. coordinator should rethrow the CatastrophicReorgError
27
+ if (connection_status.last_packet.status === "catastrophic_reorg") {
28
+ yield connection_status.last_packet;
29
+ continue;
30
+ }
31
+ if (blocks_buffer.length >= max_blocks_buffer_size) {
32
+ connection_status.last_packet = {
33
+ status: "blocks_buffer_full",
34
+ bytes_read: 0,
35
+ node_url: node_url,
36
+ timestamp: new Date().toISOString(),
37
+ };
38
+ yield connection_status.last_packet;
39
+ await sleep(5000);
40
+ continue;
41
+ }
42
+ log("blocksBufferFetchLoop", ["fetching", nodeUrl.node_url, current_range.start, "->", current_range.end]);
43
+ const get_blocks_bin = await doRPCrequest(nodeUrl, current_range, stopSync);
44
+ const result_meta = await nodeUrl.loadGetBlocksBinResponse(get_blocks_bin);
45
+ log("blocksBufferFetchLoop", "response: " + result_meta.block_infos.length + " blocks");
46
+ connection_status.last_packet = {
47
+ status: "OK",
48
+ bytes_read: get_blocks_bin.length,
49
+ node_url: node_url,
50
+ timestamp: new Date().toISOString(),
51
+ };
52
+ yield connection_status.last_packet;
53
+ // no new blocks: at tip, sleep and retry
54
+ if (!result_meta.block_infos.length) {
55
+ await sleep(1000);
56
+ continue;
57
+ }
58
+ const bufferItem = makeBlocksBufferItem(result_meta, get_blocks_bin);
59
+ try {
60
+ const parse_result = await updateBlocksBufferScanHeight(current_range, result_meta, connection_status.sync.scanned_ranges);
61
+ connection_status.sync.scanned_ranges = parse_result.scanned_ranges;
62
+ current_range = parse_result.current_range;
63
+ if (parse_result.split_height) {
64
+ // push a new ReorgInfo entry for this reorg
65
+ if (!connection_status.sync.reorg_info) {
66
+ connection_status.sync.reorg_info = {
67
+ split_heights: [parse_result.split_height],
68
+ removed_outputs: [],
69
+ reverted_spends: [],
70
+ };
71
+ }
72
+ else {
73
+ connection_status.sync.reorg_info.split_heights.push(parse_result.split_height);
74
+ }
75
+ //WE DONT DO THIS. its an elegant little thing that causes trouble
76
+ // as a result we would have to track for each wallet what workitems
77
+ // have been processed already
78
+ // the logic for what has been done becomes much more complex
79
+ // when we dont know what exactly was added
80
+ // NVM got it to work with workToBeDoneForBatch to work without passing explicit blocks
81
+ // pop blocks at/above the split height if there are any
82
+ // (buffer may be empty on a fresh generator start)
83
+ // if (blocks_buffer.length > 0) {
84
+ // popBlocksBufferItemsFromSplitHeight(
85
+ // blocks_buffer,
86
+ // parse_result.split_height,
87
+ // );
88
+ // }
89
+ blocks_buffer.push(bufferItem);
90
+ yield bufferItem;
91
+ }
92
+ else {
93
+ blocks_buffer.push(bufferItem);
94
+ yield bufferItem;
95
+ }
96
+ //TODO: only write this to disk after blocks_buffer_changed has been handled by wallets.
97
+ //look for same comment in blocksBufferCoordination.ts
98
+ yield connection_status.sync;
99
+ }
100
+ catch (error) {
101
+ //cat reorg
102
+ if (error instanceof CatastrophicReorgError) {
103
+ connection_status.last_packet = {
104
+ status: "catastrophic_reorg",
105
+ bytes_read: 0,
106
+ node_url,
107
+ timestamp: new Date().toISOString(),
108
+ };
109
+ yield connection_status.last_packet;
110
+ }
111
+ else {
112
+ throw error;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ export function makeBlocksBufferItem(result_meta, get_blocks_bin) {
118
+ const bufferItem = {
119
+ local_uuid: crypto.randomUUID(),
120
+ data: get_blocks_bin,
121
+ get_blocks_result_meta: result_meta,
122
+ };
123
+ return bufferItem;
124
+ }
125
+ export function popBlocksBufferItemFromIndex(blocks_buffer, index) {
126
+ if (index < 0 || index >= blocks_buffer.length)
127
+ return;
128
+ return blocks_buffer.splice(index, 1)[0];
129
+ }
130
+ export function findBufferitemBySplitHeight(blocks_buffer, split_height) {
131
+ for (const item of blocks_buffer) {
132
+ const infos = item.get_blocks_result_meta.block_infos;
133
+ if (!infos?.length)
134
+ continue;
135
+ const start = infos[0].block_height;
136
+ const end = infos.at(-1).block_height;
137
+ if (start <= split_height.block_height &&
138
+ split_height.block_height <= end) {
139
+ return item;
140
+ }
141
+ }
142
+ }
143
+ export function findBufferItemIndexByLocalId(blocks_buffer, local_uuid) {
144
+ return blocks_buffer.findIndex((item) => item.local_uuid === local_uuid);
145
+ }
146
+ export function popBlocksBufferItemsFromSplitHeight(blocks_buffer, split_height) {
147
+ const found = findBufferitemBySplitHeight(blocks_buffer, split_height);
148
+ if (!found) {
149
+ // buffer is empty or doesn't contain the split height.
150
+ // this can happen on a fresh generator start, nothing to pop.
151
+ return [];
152
+ }
153
+ const foundIndex = findBufferItemIndexByLocalId(blocks_buffer, found.local_uuid);
154
+ if (foundIndex < 0) {
155
+ // item was removed between finding it and getting its index
156
+ return [];
157
+ }
158
+ // pop from this index onward, everything at or above the split
159
+ const removed = [];
160
+ for (let i = blocks_buffer.length - 1; i >= foundIndex; i--) {
161
+ removed.unshift(popBlocksBufferItemFromIndex(blocks_buffer, i));
162
+ }
163
+ return removed;
164
+ }
165
+ export async function doRPCrequest(nodeUrl, current_range, stopSync) {
166
+ const get_blocks_bin = await nodeUrl.getBlocksBinExecuteRequest({
167
+ block_ids: current_range.block_hashes.map((b) => b.block_hash),
168
+ }, stopSync);
169
+ return get_blocks_bin;
170
+ }
171
+ export async function initScannedRanges(node_url, start_height, scanned_ranges = []) {
172
+ {
173
+ let current_height = start_height;
174
+ // merge existing ranges & find end of current range
175
+ scanned_ranges = mergeRanges(scanned_ranges);
176
+ let current_range = findRange(scanned_ranges, current_height);
177
+ let start_block_hash = current_range?.block_hashes[0];
178
+ if (!start_block_hash) {
179
+ const blockHeaderResponse = (await get_block_headers_range(node_url, {
180
+ start_height,
181
+ end_height: start_height,
182
+ })).headers[0];
183
+ start_block_hash = {
184
+ block_hash: blockHeaderResponse.hash,
185
+ block_height: blockHeaderResponse.height,
186
+ block_timestamp: blockHeaderResponse.timestamp,
187
+ };
188
+ const newRange = {
189
+ start: start_block_hash.block_height,
190
+ end: start_block_hash.block_height,
191
+ block_hashes: [start_block_hash, start_block_hash, start_block_hash],
192
+ };
193
+ current_range = newRange;
194
+ scanned_ranges.push(newRange);
195
+ }
196
+ if (!start_block_hash)
197
+ throw new Error("could not find start block hash");
198
+ if (current_range == null || !current_range?.block_hashes.length)
199
+ throw new Error("current_range was malformed. block_hashes is empty");
200
+ return { current_range, scanned_ranges };
201
+ }
202
+ }
203
+ export async function updateBlocksBufferScanHeight(current_range, result_meta, scanned_ranges) {
204
+ let last_block_hash_of_result = result_meta.block_infos.at(-1);
205
+ let current_blockhash = current_range?.block_hashes.at(0);
206
+ if (!current_blockhash)
207
+ throw new Error("current_range passed to updateScanHeight was malformed. block_hashes is empty");
208
+ if (!last_block_hash_of_result)
209
+ return { current_range, scanned_ranges }; // block_infos empty, no change (we are at tip and there was no new block)
210
+ // if last blockhash is undefined it means there was not reorg, we are at tip, block_infos is empty ( no new blocks )
211
+ const oldRange = findRange(scanned_ranges, current_blockhash.block_height);
212
+ if (!oldRange)
213
+ throw new Error(`could not find scan range for height ${current_blockhash.block_height},\n that means the blocks in the response from getBlocks.bin do not overlap\n with the scanned ranges in the cache. This should not happen, as even if \n we are starting from a new start_height that has been supplied to scanWithCache,\n it has been found as an existing range in the cache, or it has been\n added as a new range before we started scannning.`);
214
+ // now we need to find the block_infos of old range in the new geblocksbin response result block_infos
215
+ // if we cant find the new range, there was a reorg and we need to clean all outputs after that and log what happened
216
+ let first_block_hash = result_meta.block_infos.at(0);
217
+ if (!first_block_hash)
218
+ return { current_range, scanned_ranges }; // block_infos empty, no change (we are at tip and there was no new block) current_range; // should never happen, if there is last_block_hash there should be first_block_hash
219
+ // if the first block hash in the response is not the same as the last block hash in the old range, there was a reorg
220
+ if (!(first_block_hash.block_hash === current_blockhash.block_hash)) {
221
+ return await handleBlocksBufferReorg(current_range, result_meta, scanned_ranges, oldRange);
222
+ }
223
+ // scan only happens in one direction,
224
+ // to scan earlier ranges: abort and recall with smaller start_height
225
+ // getblocksbin will return up to 1000 blocks at once
226
+ // so this should never happen, except if we just popped a block (but that case is handled above in the reorg case)
227
+ if (current_blockhash.block_height > last_block_hash_of_result.block_height)
228
+ throw new Error(`current scan height was larger than block height of last block from latest scan result. \n Most likely connected to faulty node / catastrophic reorg.\n current height: ${current_blockhash.block_height}, new height: ${last_block_hash_of_result.block_height}`);
229
+ // guard against degenerate single-block ranges
230
+ if (current_blockhash.block_height === last_block_hash_of_result.block_height) {
231
+ oldRange.end = last_block_hash_of_result.block_height;
232
+ oldRange.block_hashes[0] = last_block_hash_of_result;
233
+ return { current_range: oldRange, scanned_ranges };
234
+ }
235
+ // 1. add new scanned range
236
+ let anchor = undefined;
237
+ let anchor_candidate = undefined;
238
+ if (oldRange.block_hashes.length >= 3) {
239
+ const old_anchor = oldRange.block_hashes.at(-1);
240
+ const old_anchor_candidate = oldRange.block_hashes.at(-2);
241
+ anchor = old_anchor;
242
+ anchor_candidate = old_anchor_candidate;
243
+ if (
244
+ // if the old range has an anchor, and the anchor is more than 200 blocks old
245
+ typeof old_anchor?.block_height === "number" &&
246
+ current_blockhash.block_height - old_anchor.block_height > 200) {
247
+ anchor = old_anchor_candidate; // use the anchor_candidate as anchor
248
+ // new anchor_candidate: is the one 100 blocks in, or the old scan tip
249
+ anchor_candidate =
250
+ result_meta.block_infos.slice(-100)[0] || oldRange?.block_hashes.at(0); // use use the old scan tip as anchor candidate
251
+ }
252
+ }
253
+ // if there is no old anchor, use the one 100 blocks in, or the last block hash
254
+ anchor =
255
+ anchor ||
256
+ result_meta.block_infos.slice(-100)[0] ||
257
+ last_block_hash_of_result;
258
+ // carry over the old anchor candidate or use the last block
259
+ anchor_candidate = anchor_candidate || last_block_hash_of_result;
260
+ const newRange = {
261
+ start: current_blockhash.block_height,
262
+ end: last_block_hash_of_result.block_height,
263
+ block_hashes: [last_block_hash_of_result, anchor_candidate, anchor],
264
+ };
265
+ return {
266
+ current_range: makeNewBlocksBufferScanRange(newRange, scanned_ranges),
267
+ scanned_ranges,
268
+ };
269
+ }
270
+ export function makeNewBlocksBufferScanRange(newRange, scanned_ranges) {
271
+ scanned_ranges.push(newRange);
272
+ // merge existing ranges & find end of current range
273
+ const merged = mergeRanges(scanned_ranges);
274
+ scanned_ranges.length = 0;
275
+ scanned_ranges.push(...merged);
276
+ // if we hit the end of a range we already scanned, move scan tip to the end
277
+ const fastForward = findRange(scanned_ranges, newRange.end);
278
+ if (fastForward)
279
+ return fastForward;
280
+ return newRange;
281
+ }
282
+ export async function handleBlocksBufferReorg(current_range, result_meta, scanned_ranges, oldRange) {
283
+ // we need to check where anchor candidate is and if not found, try the same for anchor
284
+ // if else throw on catastrophic reorg
285
+ for (const block_hash of oldRange.block_hashes) {
286
+ const split_height_index = result_meta.block_infos.findIndex((b) => b.block_hash === block_hash.block_hash);
287
+ const split_height = result_meta.block_infos[split_height_index];
288
+ // still a chance to find the split height, (could be candidate_anchor or anchor)
289
+ if (!split_height)
290
+ continue;
291
+ // we found the split height
292
+ // find current range in scanned ranges and change its end value + latest_block_hash
293
+ oldRange.end = split_height.block_height;
294
+ oldRange.block_hashes[0] = split_height;
295
+ // fix current_range
296
+ let anchor = result_meta.block_infos.slice(-100)[0]; // if we got lots of new blocks
297
+ const old_anchor = oldRange.block_hashes.at(-1);
298
+ if (!anchor &&
299
+ old_anchor &&
300
+ split_height.block_height > old_anchor.block_height //if we did not get many new blocks + split height was candidate anchor
301
+ ) {
302
+ anchor = old_anchor; // we keep the anchor the same
303
+ }
304
+ if (!anchor)
305
+ anchor = split_height;
306
+ let last_block_hash_of_result = result_meta.block_infos.at(-1);
307
+ const end = last_block_hash_of_result.block_height;
308
+ const start = current_range.start > end ? end : current_range.start;
309
+ // preserve the old deep anchor (3rd element) so subsequent reorgs
310
+ // have a fallback beyond the split height. without this, both anchors
311
+ // point to the same height and the next pop past it is catastrophic.
312
+ const oldDeepAnchor = oldRange.block_hashes.at(-1);
313
+ const newAnchor = oldDeepAnchor && oldDeepAnchor.block_height < anchor.block_height
314
+ ? oldDeepAnchor
315
+ : anchor;
316
+ const newRange = {
317
+ start,
318
+ end,
319
+ block_hashes: [last_block_hash_of_result, anchor, newAnchor],
320
+ };
321
+ return {
322
+ current_range: makeNewBlocksBufferScanRange(newRange, scanned_ranges),
323
+ scanned_ranges,
324
+ split_height,
325
+ };
326
+ }
327
+ // we tried all the block hashes and could not find the split height
328
+ throw new CatastrophicReorgError("Could not find reorg split height. Most likely connected to faulty node / catastrophic reorg.");
329
+ }
330
+ /**
331
+ * getBlocks.bin monero RPC call will block clients as peers,
332
+ * after 3 attempts of fetching a height higher than tip,
333
+ * this is ugly but it is what it is, so we need to do a get_info
334
+ * rpc call to get the tip height and reduce the start_height to the tip height,
335
+ * if it is larger than the tip height
336
+ * @param start_height
337
+ * @param node_url
338
+ * @returns Promise<number> a promise with the new potentially reduced start_height
339
+ */
340
+ export async function reduceStartHeightToTip(start_height, node_url) {
341
+ const getInfo = await get_info(node_url);
342
+ if (start_height > getInfo.height - 1) {
343
+ const oldStartHeight = start_height;
344
+ start_height = getInfo.height - 1;
345
+ log("reduceStartHeightToTip", [
346
+ "start height was larger than daemon height, setting start_height=" +
347
+ start_height,
348
+ " oldStartHeight=" + oldStartHeight,
349
+ ]);
350
+ }
351
+ return start_height;
352
+ }
353
+ export class CatastrophicReorgError extends Error {
354
+ name = "CatastrophicReorgError";
355
+ }
@@ -0,0 +1,15 @@
1
+ import { type GetBlocksBinBufferItem, type ConnectionStatus, type BlocksBufferLoopResult, type CacheRange } from "../../api";
2
+ export type SetupBlocksBufferGeneratorParams = {
3
+ nodeUrl: string;
4
+ startHeight: number;
5
+ anchor_range?: CacheRange;
6
+ stopSync?: AbortSignal;
7
+ maxBufferItems?: number;
8
+ scanSettingsPath?: string;
9
+ };
10
+ export declare function setupBlocksBufferGenerator(params: SetupBlocksBufferGeneratorParams): Promise<{
11
+ generator: AsyncGenerator<BlocksBufferLoopResult, any, any>;
12
+ blocksBuffer: GetBlocksBinBufferItem[];
13
+ connection_status: ConnectionStatus;
14
+ }>;
15
+ export declare function handleConnectionStatusChanges(event: BlocksBufferLoopResult, scanSettingsPath?: string): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { blocksBufferFetchLoop, readWriteConnectionStatusFile, CatastrophicReorgError, readOrInitConnectionStatus, } from "../../api";
2
+ import { log } from "../../io/logging";
3
+ export async function setupBlocksBufferGenerator(params) {
4
+ const blocksBuffer = [];
5
+ const connection_status = await readOrInitConnectionStatus(params.scanSettingsPath);
6
+ const generator = blocksBufferFetchLoop(params.nodeUrl, params.startHeight, blocksBuffer, connection_status, params.maxBufferItems, params.anchor_range, params.stopSync);
7
+ return { generator, blocksBuffer, connection_status };
8
+ }
9
+ export async function handleConnectionStatusChanges(event, scanSettingsPath) {
10
+ if ("local_uuid" in event && typeof event.local_uuid === "string")
11
+ return;
12
+ if ("status" in event) {
13
+ await readWriteConnectionStatusFile((cs2) => {
14
+ cs2.last_packet = event;
15
+ }, scanSettingsPath);
16
+ if (event.status === "catastrophic_reorg") {
17
+ log("handleConnectionStatusChanges", "catastrophic reorg, stopping");
18
+ throw new CatastrophicReorgError("[blocksbufferCoordinator] catastrophic reorg, stopping");
19
+ }
20
+ }
21
+ if ("scanned_ranges" in event) {
22
+ //TODO: only write this to disk after blocks_buffer_changed has been handled by wallets.
23
+ //look for same comment in blocksBufferFetchLoop.ts
24
+ await readWriteConnectionStatusFile((cs2) => {
25
+ cs2.sync = event;
26
+ }, scanSettingsPath);
27
+ }
28
+ }
@@ -1,15 +1,15 @@
1
1
  import type { BlockInfo, GetBlocksBinRequest, ViewPair } from "../../api";
2
2
  import type { ScanSetting } from "../scanSettings";
3
- import { type CacheRange } from "./scanCache";
4
- export declare function writeGetblocksBinBuffer(getBlocksBinResponseContent: Uint8Array, block_infos: BlockInfo[], pathPrefix?: string): Promise<number | undefined>;
5
- export type GetBlocksBinBufferItem = {
3
+ import { type CacheRange } from "../scanresult/scanCache";
4
+ export declare function writeGetblocksBinBuffer(getBlocksBinResponseContent: Uint8Array, block_infos: BlockInfo[], pathPrefix?: string): Promise<GetBlocksBinBufferItemFilename | undefined>;
5
+ export type GetBlocksBinBufferItemFilename = {
6
6
  start: number;
7
7
  end: number;
8
8
  filename: string;
9
9
  date: string;
10
10
  last_block_hash: string;
11
11
  };
12
- export declare function readGetblocksBinBuffer(current_height: number, pathPrefix?: string): Promise<GetBlocksBinBufferItem[]>;
12
+ export declare function readGetblocksBinBuffer(current_height: number, pathPrefix?: string): Promise<GetBlocksBinBufferItemFilename[]>;
13
13
  export type BlocksGenerator = AsyncGenerator<Uint8Array>;
14
14
  export type SlaveViewPair = {
15
15
  viewpair: ViewPair;
@@ -22,7 +22,7 @@ declare module "bun" {
22
22
  }
23
23
  }
24
24
  export declare function trimGetBlocksBinBuffer(nonHaltedWallets: ScanSetting[], pathPrefix?: string): Promise<void>;
25
- export declare function readGetblocksBinBufferItems(pathPrefix?: string): Promise<GetBlocksBinBufferItem[]>;
25
+ export declare function readGetblocksBinBufferItems(pathPrefix?: string): Promise<GetBlocksBinBufferItemFilename[]>;
26
26
  export interface HasGetBlocksBinExecuteRequestMethod {
27
27
  getBlocksBinExecuteRequest: (params: GetBlocksBinRequest, stopSync?: AbortSignal) => Promise<Uint8Array>;
28
28
  }
@@ -1,17 +1,19 @@
1
1
  import { atomicWrite } from "../../io/atomicWrite";
2
2
  import { readDir } from "../../io/readDir";
3
- import { readCacheFileDefaultLocation } from "./scanCache";
3
+ import { readCacheFileDefaultLocation, } from "../scanresult/scanCache";
4
4
  export async function writeGetblocksBinBuffer(getBlocksBinResponseContent, block_infos, pathPrefix) {
5
5
  if (!block_infos.length)
6
6
  return;
7
7
  const start = block_infos[0].block_height;
8
- const end = block_infos.at(-1)?.block_height;
9
- const block_hash = block_infos.at(-1)?.block_hash;
8
+ const end = block_infos.at(-1).block_height;
9
+ const block_hash = block_infos.at(-1).block_hash;
10
10
  const bufferItems = await readGetblocksBinBufferItems(pathPrefix);
11
11
  if (bufferItems.find((bi) => bi.last_block_hash === block_hash))
12
12
  return;
13
- // if we have the same end blockhash already, dont write
14
- return await atomicWrite(`${pathPrefix ?? ""}getblocksbinbuffer/${Date.now()}-${start}-${end}-${block_hash}-getblocks.bin`, getBlocksBinResponseContent);
13
+ const date = Date.now().toString();
14
+ const filename = `${date}-${start}-${end}-${block_hash}-getblocks.bin`;
15
+ await atomicWrite(`${pathPrefix ?? ""}getblocksbinbuffer/${filename}`, getBlocksBinResponseContent);
16
+ return { start, end, filename, date, last_block_hash: block_hash };
15
17
  }
16
18
  export async function readGetblocksBinBuffer(current_height, pathPrefix) {
17
19
  const bufferItems = await readGetblocksBinBufferItems(pathPrefix);
@@ -1,15 +1,39 @@
1
- export type ConnectionStatusOptions = "OK" | "partial_read" | "connection_failed" | "no_connection_yet" | "catastrophic_reorg";
1
+ import { type CacheRange, type ReorgInfo } from "../api";
2
+ export type ConnectionStatusOptions = "OK" | "partial_read" | "connection_failed" | "blocks_buffer_full" | "no_connection_yet" | "catastrophic_reorg";
3
+ export type ConnectionSatusLastPacket = {
4
+ status: ConnectionStatusOptions;
5
+ bytes_read: number;
6
+ node_url: string;
7
+ timestamp: string;
8
+ };
9
+ export type ConnectionStatusSync = {
10
+ reorg_info?: ReorgInfo;
11
+ scanned_ranges: CacheRange[];
12
+ daemon_height: number;
13
+ current_scan_height: number;
14
+ eta: string;
15
+ timestamp: string;
16
+ };
2
17
  export type ConnectionStatus = {
3
- last_packet: {
4
- status: ConnectionStatusOptions;
5
- bytes_read: number;
6
- node_url: string;
7
- timestamp: string;
8
- };
18
+ last_packet: ConnectionSatusLastPacket;
19
+ sync: ConnectionStatusSync;
9
20
  };
10
21
  export declare const DEFAULT_CONNECTION_STATUS_PREFIX = "ConnectionStatus-";
22
+ export declare function msToHHMM(ms: number): string;
23
+ export declare function emptyConnectionStatus(overrides?: Partial<ConnectionStatus>): ConnectionStatus;
24
+ export declare function updateSyncETA(daemon_height: number, current_scan_height: number, last_height: number | null, last_timestamp: number | null, scan_settings_path?: string): Promise<{
25
+ last_height: number;
26
+ last_timestamp: number;
27
+ }>;
11
28
  export declare function connectionStatusFilePath(scan_settings_path?: string): string;
12
29
  export declare function readConnectionStatusDefaultLocation(scan_settings_path?: string): Promise<ConnectionStatus | undefined>;
13
30
  export declare function readConnectionStatusFile(connectionStatusFilePath: string): Promise<ConnectionStatus | undefined>;
14
- export declare function writeConnectionStatusFile(status: ConnectionStatusOptions, node_url: string, bytes_read?: number, scan_settings_path?: string): Promise<number>;
15
- export declare function readWriteConnectionStatusFile(writeCB: (cs: ConnectionStatus | undefined) => ConnectionStatus | undefined, scan_settings_path?: string): Promise<number | undefined>;
31
+ export declare function writeConnectionStatusFile(connectionStatus: ConnectionStatus, scan_settings_path?: string): Promise<number>;
32
+ export declare function readWriteConnectionStatusFile(writeCB: (cs: ConnectionStatus) => void, scan_settings_path?: string): Promise<ConnectionStatus>;
33
+ /**
34
+ * read the connection status from disk (typical /path/to/ConnectionStatus-ScanSettings.json)
35
+ * and persist empty inital status if not found
36
+ * @param scan_settings_path path to the scan settings file /path/to/ScanSettings.json
37
+ * @returns connection status or empty initalized connection status
38
+ */
39
+ export declare function readOrInitConnectionStatus(scan_settings_path?: string): Promise<ConnectionStatus>;