@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,116 @@
|
|
|
1
|
+
import { sleep, ViewPair, } from "../../api";
|
|
2
|
+
export function makeWorkItem(walletConfig, batch, from, to) {
|
|
3
|
+
if (to && to > batch.get_blocks_result_meta.block_infos.length - 1) {
|
|
4
|
+
// we could throw here but we dont. Off by one errors shouldnt break the application
|
|
5
|
+
// and we put so much work into making processResult idempotent
|
|
6
|
+
// so a caller bug scheduling too much work just leads to slightly worse performance
|
|
7
|
+
//throw new Error("to is out of bounds");
|
|
8
|
+
to = batch.get_blocks_result_meta.block_infos.length - 1;
|
|
9
|
+
}
|
|
10
|
+
if (typeof from === "number" && from < 0) {
|
|
11
|
+
//throw new Error("from is out of bounds");
|
|
12
|
+
from = 0;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
walletConfig,
|
|
16
|
+
batch: {
|
|
17
|
+
...batch,
|
|
18
|
+
data: batch.data.slice(),
|
|
19
|
+
},
|
|
20
|
+
work_uuid: crypto.randomUUID(),
|
|
21
|
+
from: from ?? 0, // default is to start from the batch beginning
|
|
22
|
+
to: to ?? batch.get_blocks_result_meta.block_infos.length - 1, // default is to go to the batch end
|
|
23
|
+
status: "fresh",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function* scanLoop(wallet) {
|
|
27
|
+
let scanResult;
|
|
28
|
+
let work_uuid;
|
|
29
|
+
let workitem_batch_uuid;
|
|
30
|
+
let loaded_batch_uuid;
|
|
31
|
+
while (true) {
|
|
32
|
+
const item = yield {
|
|
33
|
+
type: "Ready",
|
|
34
|
+
work_uuid,
|
|
35
|
+
result: scanResult,
|
|
36
|
+
};
|
|
37
|
+
if (item === "cancel" || item === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
work_uuid = String(item.work_uuid);
|
|
40
|
+
// 1.call loadGetBlocksBinResponse
|
|
41
|
+
workitem_batch_uuid = String(item.batch.local_uuid);
|
|
42
|
+
const viewpair = await ViewPair.create(item.walletConfig.primary_address, item.walletConfig.secret_view_key, item.walletConfig.subaddress_index);
|
|
43
|
+
await viewpair.loadGetBlocksBinResponse(item.batch.data);
|
|
44
|
+
loaded_batch_uuid = workitem_batch_uuid;
|
|
45
|
+
const first_block_meta = item.batch.get_blocks_result_meta.block_infos[0];
|
|
46
|
+
if (!first_block_meta)
|
|
47
|
+
throw new Error("no first block meta");
|
|
48
|
+
scanResult = undefined;
|
|
49
|
+
scanResult = {
|
|
50
|
+
outputs: [],
|
|
51
|
+
all_key_images: [],
|
|
52
|
+
new_height: first_block_meta.block_height + item.from,
|
|
53
|
+
primary_address: wallet.primary_address,
|
|
54
|
+
block_infos: item.batch.get_blocks_result_meta.block_infos,
|
|
55
|
+
daemon_height: item.batch.get_blocks_result_meta.daemon_height,
|
|
56
|
+
};
|
|
57
|
+
if (!(item.to >= item.from))
|
|
58
|
+
throw new Error("to must be >= from");
|
|
59
|
+
for (let i = item.from; i <= item.to; i++) {
|
|
60
|
+
// 2.call getBlocksBinScanOneBlock
|
|
61
|
+
const blockResult = await viewpair.getBlocksBinScanOneBlock(i);
|
|
62
|
+
if ("error" in blockResult)
|
|
63
|
+
throw new Error(blockResult.error);
|
|
64
|
+
//3. accumulate the scanresult
|
|
65
|
+
scanResult.outputs.push(...blockResult.outputs);
|
|
66
|
+
scanResult.all_key_images.push(...blockResult.all_key_images);
|
|
67
|
+
scanResult.new_height = first_block_meta.block_height + i;
|
|
68
|
+
// // update item.scanCache in memory will be done by the consumer
|
|
69
|
+
// it is saved on the workitem, but the workitemBuffer is managed by the consumer
|
|
70
|
+
// the consumer will reconcile the workitemBuffer on blocksbuffer changed yields from the fetchloop
|
|
71
|
+
// it will also reconcile the blocksbuffer with the workitembuffer when work was done,
|
|
72
|
+
// processScanResult will be called by the consumer and the scanresult will
|
|
73
|
+
// be persisted to disk by the consumer.
|
|
74
|
+
// if the workitem is at the left end of the workitemBuffer it will be shifted (popped from the left)
|
|
75
|
+
// if it is removed like this from the workItemBuffer and eventually no items in the workItemBuffer
|
|
76
|
+
// refercence the getBlocksBinBufferItem in the blocksbuffer, the blocksbufferitem is removed from the blocksbuffer
|
|
77
|
+
// this is how the reconiliaton workitembuffer -> blocksbuffer happens.
|
|
78
|
+
//
|
|
79
|
+
// the reconciliation in blocksbuffer -> workitembuffer starts in the fetchloop before it sends a yield blocksbuffer changed
|
|
80
|
+
// it is finished by the consumer of this generator after the yield blocksbuffer changed
|
|
81
|
+
// this happens through checking all workitemBuffer items,
|
|
82
|
+
// if their GetBlocksBinBufferItem is in the blocksbuffer (via the local_uuid)
|
|
83
|
+
// if they are not in the blocksbuffer they are removed from the workitemBuffer
|
|
84
|
+
// then for all the blocksbuffer items that are not yet referenced by workbuffer items
|
|
85
|
+
// they need to be silced into workitems and added to the workitembuffer
|
|
86
|
+
//the reconciliation blocksbuffer -> workitembuffer is done on every blocks buffer changed event from the fetchloop
|
|
87
|
+
// the reconciliation workitembuffer -> blocksbuffer is done on every workitembuffer marked done at the end of the workbuffer (left end)
|
|
88
|
+
const input = yield {
|
|
89
|
+
type: "InProgress",
|
|
90
|
+
work_uuid,
|
|
91
|
+
result: scanResult,
|
|
92
|
+
};
|
|
93
|
+
if (input === "cancel")
|
|
94
|
+
break;
|
|
95
|
+
if (typeof input === "object")
|
|
96
|
+
throw new Error("cancel oldworkitem, before sending new workitem, while work in progress");
|
|
97
|
+
await sleep(10); // make sure the loop is not tight
|
|
98
|
+
}
|
|
99
|
+
// TODO: move scanCache forward with scanResult
|
|
100
|
+
// this can not be done here, has to be done by the consumer of the generator
|
|
101
|
+
// will be done by processScanResult in the consumer
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//theoretically markWorkItemAsDone could be done in the generator as WorkItem is passed in by reference
|
|
105
|
+
// but: eventually this has to be done accross CPU worker boundaries
|
|
106
|
+
// so we act on the yielded scan result
|
|
107
|
+
export async function markWorkItemAsDone(loop_event, work_buffer) {
|
|
108
|
+
if (loop_event.type === "Ready" && loop_event.work_uuid) {
|
|
109
|
+
const work_item = work_buffer.find((w) => w.work_uuid === loop_event.work_uuid);
|
|
110
|
+
if (work_item) {
|
|
111
|
+
work_item.result = loop_event.result;
|
|
112
|
+
work_item.status = "scanwork_done";
|
|
113
|
+
return work_item;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BlockInfo, type Output } from "../../api";
|
|
2
2
|
import { type KeyImage } from "./computeKeyImage";
|
|
3
3
|
import { type ErrorResponse } from "../../node-interaction/binaryEndpoints";
|
|
4
4
|
import type { CacheChangedCallback, CacheRange, ChangedOutput, ScanCache } from "./scanCache";
|
|
@@ -11,7 +11,22 @@ export type ProcessScanResultParams = {
|
|
|
11
11
|
pathPrefix?: string;
|
|
12
12
|
use_master_current_range?: boolean;
|
|
13
13
|
};
|
|
14
|
-
export
|
|
14
|
+
export type ProcessScanResultParamsWithoutSideEffects = {
|
|
15
|
+
from_height: number;
|
|
16
|
+
secret_spend_key?: string;
|
|
17
|
+
result: ScanResult | ErrorResponse | undefined;
|
|
18
|
+
scanCache: ScanCache;
|
|
19
|
+
to_height?: number;
|
|
20
|
+
};
|
|
21
|
+
export type ProcessScanResult = {
|
|
22
|
+
current_range: CacheRange;
|
|
23
|
+
changed_outputs: ChangedOutput[];
|
|
24
|
+
};
|
|
25
|
+
export declare function processScanResult(params: ProcessScanResultParamsWithoutSideEffects): Promise<ProcessScanResult>;
|
|
26
|
+
export declare function processResultReturnValue(cache: ScanCache, params: ProcessScanResultParamsWithoutSideEffects, changed_outputs: ChangedOutput[]): {
|
|
27
|
+
current_range: CacheRange;
|
|
28
|
+
changed_outputs: ChangedOutput[];
|
|
29
|
+
};
|
|
15
30
|
export type OnchainKeyImage = {
|
|
16
31
|
key_image_hex: KeyImage;
|
|
17
32
|
relative_index: number;
|
|
@@ -35,7 +50,6 @@ export type FastForward = number;
|
|
|
35
50
|
* to look out for our own spends before proceeding the scan. This happens in the processScanResult function.
|
|
36
51
|
*/
|
|
37
52
|
export type ScanResultCallback = ((result: ScanResult | ErrorResponse | EmptyScanResult) => FastForward | void) | ((result: ScanResult | ErrorResponse | EmptyScanResult) => Promise<FastForward | void>);
|
|
38
|
-
export declare function updateScanHeight(current_range: CacheRange, result: ScanResult, cache: ScanCache): [CacheRange, ChangedOutput[]];
|
|
39
53
|
export declare function makeNewRange(newRange: CacheRange, cache: ScanCache): CacheRange;
|
|
40
54
|
export declare function detectOutputs(result: ScanResult, cache: ScanCache, spend_private_key?: string): Promise<ChangedOutput[]>;
|
|
41
55
|
export declare function detectOwnspends(result: ScanResult, cache: ScanCache): ChangedOutput[];
|
|
@@ -62,3 +76,10 @@ export type Spendable = {
|
|
|
62
76
|
export type OutputStatus = PrePending | Pending | Spent | Burnt | Reorged | Spendable;
|
|
63
77
|
export declare function outputStatus(output: Output, cache: ScanCache, current_height: number): OutputStatus;
|
|
64
78
|
export declare function spendable(output: Output, cache: ScanCache, current_height: number): boolean;
|
|
79
|
+
export declare function findTipIndex(block_infos: BlockInfo[], oldTip: BlockInfo): number | "reorg_found" | "empty_blocks_array";
|
|
80
|
+
export declare function selectAnchors(block_infos: BlockInfo[], tipIndex: number, oldRange: CacheRange, endIndex?: number): CacheRange;
|
|
81
|
+
export declare function shouldReplaceAnchor(oldRange: CacheRange, last: BlockInfo): true | undefined;
|
|
82
|
+
export declare function newAnchorCandidates(oldRange: CacheRange, block_infos: BlockInfo[], tipIndex: number, endIndex: number, last: BlockInfo): {
|
|
83
|
+
newCandidateAnchor: BlockInfo;
|
|
84
|
+
newAnchor: BlockInfo;
|
|
85
|
+
};
|
|
@@ -1,110 +1,128 @@
|
|
|
1
|
+
import { CatastrophicReorgError, } from "../../api";
|
|
1
2
|
import { computeKeyImage } from "./computeKeyImage";
|
|
2
|
-
import {
|
|
3
|
+
import { log } from "../../io/logging";
|
|
4
|
+
import { mergeRanges, findRange, readCacheFileDefaultLocation, lastRange, writeCacheToFile, findRangeThrows, } from "./scanCache";
|
|
3
5
|
import {} from "../../node-interaction/binaryEndpoints";
|
|
4
|
-
import { handleReorg } from "./reorg";
|
|
5
6
|
export async function processScanResult(params) {
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const { result, scanCache: cache, secret_spend_key } = params;
|
|
8
|
+
let changed_outputs = [];
|
|
8
9
|
if (!(result && "primary_address" in result))
|
|
9
|
-
return
|
|
10
|
-
const cache = await readCacheFileDefaultLocation(result.primary_address, pathPrefix);
|
|
11
|
-
if (!cache)
|
|
12
|
-
throw new Error(`cache not found for primary address: ${result.primary_address} and path prefix: ${pathPrefix}`);
|
|
10
|
+
return processResultReturnValue(cache, params, changed_outputs);
|
|
13
11
|
if (result && "daemon_height" in result)
|
|
14
12
|
cache.daemon_height = result.daemon_height;
|
|
15
|
-
if (
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
13
|
+
if (result && "new_height" in result) {
|
|
14
|
+
const oldRange = findRangeThrows(cache.scanned_ranges, params.from_height);
|
|
15
|
+
let tipIndex = findTipIndex(result.block_infos, oldRange.block_hashes[0]);
|
|
16
|
+
if (tipIndex === "empty_blocks_array")
|
|
17
|
+
return processResultReturnValue(cache, params, changed_outputs);
|
|
18
|
+
if (tipIndex === "reorg_found") {
|
|
19
|
+
let split_height_index = null;
|
|
20
|
+
for (const block_hash of oldRange.block_hashes) {
|
|
21
|
+
split_height_index = findTipIndex(result.block_infos, block_hash);
|
|
22
|
+
if (typeof split_height_index === "number")
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
if (typeof split_height_index !== "number")
|
|
26
|
+
throw new CatastrophicReorgError("could not locate anchors, should not try to processResult on stale workItems");
|
|
27
|
+
const split_height = result.block_infos[split_height_index];
|
|
28
|
+
// we found the split height & do the reorg
|
|
29
|
+
log("processScanResult", [
|
|
30
|
+
"SPLIT FOUND at height",
|
|
31
|
+
split_height.block_height,
|
|
32
|
+
]);
|
|
33
|
+
if (!cache.reorg_info) {
|
|
34
|
+
cache.reorg_info = {
|
|
35
|
+
split_heights: [split_height],
|
|
36
|
+
removed_outputs: [],
|
|
37
|
+
reverted_spends: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
cache.reorg_info.split_heights.push(split_height);
|
|
42
|
+
}
|
|
43
|
+
// First, collect reverted spends from all outputs (before any removals)
|
|
44
|
+
// so that outputs with block_height < split_height but spent_block_height >= split_height are captured
|
|
45
|
+
const reverted_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.spent_block_height !== undefined &&
|
|
46
|
+
output.spent_block_height >= split_height.block_height);
|
|
47
|
+
const removed_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.block_height >= split_height.block_height);
|
|
48
|
+
log("processScanResult", [
|
|
49
|
+
"removed_outputs count:",
|
|
50
|
+
removed_outputs.length,
|
|
51
|
+
"reverted count:",
|
|
52
|
+
reverted_outputs.length,
|
|
53
|
+
]);
|
|
54
|
+
for (const [id, old_output_state] of removed_outputs) {
|
|
55
|
+
// 1. find key_image of output to be removed (as it was reorged)
|
|
56
|
+
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
|
|
57
|
+
cache.reorg_info.removed_outputs.push({
|
|
58
|
+
old_output_state,
|
|
59
|
+
key_image,
|
|
60
|
+
split_height,
|
|
61
|
+
});
|
|
62
|
+
// 2. remove from outputs and own_key_images
|
|
63
|
+
delete cache.outputs[id];
|
|
64
|
+
delete cache.own_key_images[key_image];
|
|
65
|
+
changed_outputs.push({
|
|
66
|
+
output: old_output_state,
|
|
67
|
+
change_reason: "reorged",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
for (const [id, old_output_state_pointer] of reverted_outputs) {
|
|
71
|
+
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
|
|
72
|
+
const old_output_state = Object.assign({}, old_output_state_pointer);
|
|
73
|
+
cache.reorg_info.reverted_spends.push({
|
|
74
|
+
old_output_state,
|
|
75
|
+
key_image, // in this case key_image only used here, does not get removed
|
|
76
|
+
split_height,
|
|
77
|
+
});
|
|
78
|
+
// remove spend info from original cache (if output still exists
|
|
79
|
+
// it may have been deleted above in the removed_outputs loop)
|
|
80
|
+
if (cache.outputs[id]) {
|
|
81
|
+
delete cache.outputs[id].spent_relative_index;
|
|
82
|
+
delete cache.outputs[id].spent_in_tx_hash;
|
|
83
|
+
delete cache.outputs[id].spent_block_height;
|
|
84
|
+
delete cache.outputs[id].spent_block_timestamp;
|
|
85
|
+
}
|
|
86
|
+
changed_outputs.push({
|
|
87
|
+
output: old_output_state,
|
|
88
|
+
change_reason: "reorged_spent",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
tipIndex = split_height_index;
|
|
38
92
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
93
|
+
const to_height_index = result.block_infos.findIndex((b) => b.block_height === params.to_height);
|
|
94
|
+
const new_range = selectAnchors(result.block_infos, tipIndex, oldRange, to_height_index);
|
|
95
|
+
const last_height = typeof params.to_height === "number"
|
|
96
|
+
? params.to_height
|
|
97
|
+
: result.block_infos[result.block_infos.length - 1].block_height;
|
|
98
|
+
//first check that no reorg occured by 1. finding oldrange, making sure oldrange end is in the meta blockinfos
|
|
99
|
+
// if that is not met call reorg handling code
|
|
100
|
+
//2. make sure reorg handling code works indepented of where the oldrange end is in the meta blockinfos
|
|
101
|
+
//3. make sure the anchor selection logic works independent of where the oldrange end is in the meta blockinfos
|
|
102
|
+
//const [new_range, changed] = updateScanHeight(current_range, result, cache);
|
|
103
|
+
makeNewRange(new_range, cache);
|
|
104
|
+
const tipHeight = result.block_infos[tipIndex].block_height;
|
|
105
|
+
const filteredOutputs = result.outputs.filter((o) => o.block_height >= tipHeight && o.block_height <= last_height);
|
|
106
|
+
const filteredKeyImages = result.all_key_images.filter((ki) => ki.block_height >= tipHeight && ki.block_height <= last_height);
|
|
107
|
+
changed_outputs.push(...(await detectOutputs({
|
|
108
|
+
...result,
|
|
109
|
+
outputs: filteredOutputs,
|
|
110
|
+
all_key_images: filteredKeyImages,
|
|
111
|
+
}, cache, secret_spend_key)));
|
|
112
|
+
if (secret_spend_key)
|
|
113
|
+
changed_outputs.push(...detectOwnspends({
|
|
114
|
+
...result,
|
|
115
|
+
outputs: filteredOutputs,
|
|
116
|
+
all_key_images: filteredKeyImages,
|
|
117
|
+
}, cache));
|
|
44
118
|
}
|
|
119
|
+
return processResultReturnValue(cache, params, changed_outputs);
|
|
45
120
|
}
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
throw new Error("current_range passed to updateScanHeight was malformed. block_hashes is empty");
|
|
51
|
-
if (!last_block_hash_of_result)
|
|
52
|
-
return [current_range, []]; // block_infos empty, no change (we are at tip and there was no new block)
|
|
53
|
-
// if last blockhash is undefined it means there was not reorg, we are at tip, block_infos is empty ( no new blocks )
|
|
54
|
-
const oldRange = findRange(cache.scanned_ranges, current_blockhash.block_height);
|
|
55
|
-
if (!oldRange)
|
|
56
|
-
throw new Error(`could not find scan range for height ${current_blockhash.block_height},
|
|
57
|
-
that means the blocks in the response from getBlocks.bin do not overlap
|
|
58
|
-
with the scanned ranges in the cache. This should not happen, as even if
|
|
59
|
-
we are starting from a new start_height that has been supplied to scanWithCache,
|
|
60
|
-
it has been found as an existing range in the cache, or it has been
|
|
61
|
-
added as a new range before we started scannning.`);
|
|
62
|
-
// now we need to find the block_infos of old range in the new geblocksbin response result block_infos
|
|
63
|
-
// if we cant find the new range, there was a reorg and we need to clean all outputs after that and log what happened
|
|
64
|
-
let first_block_hash = result.block_infos.at(0);
|
|
65
|
-
if (!first_block_hash)
|
|
66
|
-
return [current_range, []]; // should never happen, if there is last_block_hash there should be first_block_hash
|
|
67
|
-
// 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
|
|
68
|
-
if (!(first_block_hash.block_hash === current_blockhash.block_hash)) {
|
|
69
|
-
return handleReorg(current_range, result, cache, oldRange);
|
|
70
|
-
}
|
|
71
|
-
// scan only happens in one direction,
|
|
72
|
-
// to scan earlier ranges: abort and recall with smaller start_height
|
|
73
|
-
// getblocksbin will return up to 1000 blocks at once
|
|
74
|
-
// so this should never happen, except if we just popped a block (but that case is handled above in the reorg case)
|
|
75
|
-
if (current_blockhash.block_height > last_block_hash_of_result.block_height)
|
|
76
|
-
throw new Error(`current scan height was larger than block height of last block from latest scan result.
|
|
77
|
-
Most likely connected to faulty node / catastrophic reorg.
|
|
78
|
-
current height: ${current_blockhash.block_height}, new height: ${last_block_hash_of_result.block_height}`);
|
|
79
|
-
// 1. add new scanned range
|
|
80
|
-
let anchor = undefined;
|
|
81
|
-
let anchor_candidate = undefined;
|
|
82
|
-
if (oldRange.block_hashes.length >= 3) {
|
|
83
|
-
const old_anchor = oldRange.block_hashes.at(-1);
|
|
84
|
-
const old_anchor_candidate = oldRange.block_hashes.at(-2);
|
|
85
|
-
anchor = old_anchor;
|
|
86
|
-
anchor_candidate = old_anchor_candidate;
|
|
87
|
-
if (
|
|
88
|
-
// if the old range has an anchor, and the anchor is more than 200 blocks old
|
|
89
|
-
typeof old_anchor?.block_height === "number" &&
|
|
90
|
-
current_blockhash.block_height - old_anchor.block_height > 200) {
|
|
91
|
-
anchor = old_anchor_candidate; // use the anchor_candidate as anchor
|
|
92
|
-
// new anchor_candidate: is the one 100 blocks in, or the old scan tip
|
|
93
|
-
anchor_candidate =
|
|
94
|
-
result.block_infos.slice(-100)[0] || oldRange?.block_hashes.at(0); // use use the old scan tip as anchor candidate
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// if there is no old anchor, use the one 100 blocks in, or the last block hash
|
|
98
|
-
anchor =
|
|
99
|
-
anchor || result.block_infos.slice(-100)[0] || last_block_hash_of_result;
|
|
100
|
-
// carry over the old anchor candidate or use the last block
|
|
101
|
-
anchor_candidate = anchor_candidate || last_block_hash_of_result;
|
|
102
|
-
const newRange = {
|
|
103
|
-
start: current_blockhash.block_height,
|
|
104
|
-
end: last_block_hash_of_result.block_height,
|
|
105
|
-
block_hashes: [last_block_hash_of_result, anchor_candidate, anchor],
|
|
121
|
+
export function processResultReturnValue(cache, params, changed_outputs) {
|
|
122
|
+
return {
|
|
123
|
+
current_range: findRangeThrows(cache.scanned_ranges, params.from_height),
|
|
124
|
+
changed_outputs,
|
|
106
125
|
};
|
|
107
|
-
return [makeNewRange(newRange, cache), []];
|
|
108
126
|
}
|
|
109
127
|
export function makeNewRange(newRange, cache) {
|
|
110
128
|
cache.scanned_ranges.push(newRange);
|
|
@@ -211,3 +229,47 @@ export function spendable(output, cache, current_height) {
|
|
|
211
229
|
const status = outputStatus(output, cache, current_height);
|
|
212
230
|
return status.status === "spendable";
|
|
213
231
|
}
|
|
232
|
+
export function findTipIndex(block_infos, oldTip) {
|
|
233
|
+
if (block_infos.length === 0)
|
|
234
|
+
return "empty_blocks_array";
|
|
235
|
+
const idx = block_infos.findIndex((b) => b.block_hash === oldTip.block_hash);
|
|
236
|
+
if (idx === -1)
|
|
237
|
+
return "reorg_found";
|
|
238
|
+
return idx;
|
|
239
|
+
}
|
|
240
|
+
export function selectAnchors(block_infos, tipIndex, oldRange, endIndex = block_infos.length - 1) {
|
|
241
|
+
const tip = block_infos[tipIndex];
|
|
242
|
+
const last = block_infos[endIndex];
|
|
243
|
+
const { newCandidateAnchor, newAnchor } = newAnchorCandidates(oldRange, block_infos, tipIndex, endIndex, last);
|
|
244
|
+
return {
|
|
245
|
+
start: tip.block_height,
|
|
246
|
+
end: last.block_height,
|
|
247
|
+
block_hashes: [last, newCandidateAnchor, newAnchor],
|
|
248
|
+
// newest, middle, oldest
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
export function shouldReplaceAnchor(oldRange, last) {
|
|
252
|
+
const oldAnchor = oldRange.block_hashes.at(2);
|
|
253
|
+
if (!oldAnchor)
|
|
254
|
+
throw new Error("old Range malformed");
|
|
255
|
+
if (
|
|
256
|
+
//if the old anchor is more than 200 blocks old
|
|
257
|
+
last.block_height - oldAnchor.block_height >
|
|
258
|
+
200) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function newAnchorCandidates(oldRange, block_infos, tipIndex, endIndex, last) {
|
|
263
|
+
const oldCandidateAnchor = oldRange.block_hashes.at(1);
|
|
264
|
+
const oldAnchor = oldRange.block_hashes.at(2);
|
|
265
|
+
if (!oldAnchor || !oldCandidateAnchor || !last)
|
|
266
|
+
throw new Error("old Range malformed");
|
|
267
|
+
if (shouldReplaceAnchor(oldRange, last)) {
|
|
268
|
+
const newCandidateAnchorIndex = Math.max(tipIndex, endIndex - 100);
|
|
269
|
+
const newCandidateAnchor = block_infos[newCandidateAnchorIndex];
|
|
270
|
+
if (!newCandidateAnchor)
|
|
271
|
+
throw new Error("tipIndex clamped candidate anchor index out of range");
|
|
272
|
+
return { newCandidateAnchor, newAnchor: oldCandidateAnchor };
|
|
273
|
+
}
|
|
274
|
+
return { newCandidateAnchor: oldCandidateAnchor, newAnchor: oldAnchor };
|
|
275
|
+
}
|