@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,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 { BlockInfo, Output } from "../../api";
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 declare function processScanResult(params: ProcessScanResultParams): Promise<CacheRange>;
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 { mergeRanges, findRange, readCacheFileDefaultLocation, lastRange, writeCacheToFile, } from "./scanCache";
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
- let current_range = params.current_range;
7
- const { result, cacheChanged, secret_spend_key, pathPrefix, use_master_current_range, } = params;
7
+ const { result, scanCache: cache, secret_spend_key } = params;
8
+ let changed_outputs = [];
8
9
  if (!(result && "primary_address" in result))
9
- return current_range;
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 (use_master_current_range) {
16
- const last = lastRange(cache.scanned_ranges);
17
- if (last) {
18
- last.block_hashes = current_range.block_hashes;
19
- last.end = current_range.end;
20
- }
21
- else {
22
- cache.scanned_ranges.push(current_range);
23
- }
24
- }
25
- try {
26
- if (result && "new_height" in result) {
27
- const [new_range, changed_outputs] = updateScanHeight(current_range, result, cache);
28
- current_range = new_range;
29
- changed_outputs.push(...(await detectOutputs(result, cache, secret_spend_key)));
30
- if (secret_spend_key)
31
- changed_outputs.push(...detectOwnspends(result, cache));
32
- // write to cache
33
- await writeCacheToFile(cache, params.pathPrefix);
34
- await cacheChanged({
35
- newCache: cache,
36
- changed_outputs,
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
- return current_range;
40
- }
41
- catch (e) {
42
- params.catastrophic_reorg_cb();
43
- throw e;
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 updateScanHeight(current_range, result, cache) {
47
- let last_block_hash_of_result = result.block_infos.at(-1);
48
- let current_blockhash = current_range?.block_hashes.at(0);
49
- if (!current_blockhash)
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
+ }