@spirobel/monero-wallet-api 0.3.16 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/api.d.ts +5 -2
  2. package/dist/api.js +5 -1
  3. package/dist/io/atomicWrite.js +2 -1
  4. package/dist/io/indexedDB.js +7 -2
  5. package/dist/keypairs-seeds/keypairs.d.ts +21 -0
  6. package/dist/keypairs-seeds/keypairs.js +21 -0
  7. package/dist/node-interaction/binaryEndpoints.d.ts +31 -2
  8. package/dist/node-interaction/binaryEndpoints.js +61 -43
  9. package/dist/node-interaction/jsonEndpoints.d.ts +257 -130
  10. package/dist/node-interaction/jsonEndpoints.js +159 -119
  11. package/dist/node-interaction/nodeUrl.d.ts +21 -6
  12. package/dist/node-interaction/nodeUrl.js +50 -8
  13. package/dist/scanning-syncing/backgroundWorker.d.ts +9 -6
  14. package/dist/scanning-syncing/backgroundWorker.js +76 -39
  15. package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.d.ts +44 -0
  16. package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +353 -0
  17. package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.d.ts +15 -0
  18. package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +27 -0
  19. package/dist/scanning-syncing/{scanresult → blocksbuffer}/getBlocksbinBuffer.d.ts +5 -5
  20. package/dist/scanning-syncing/{scanresult → blocksbuffer}/getBlocksbinBuffer.js +7 -5
  21. package/dist/scanning-syncing/connectionStatus.d.ts +33 -9
  22. package/dist/scanning-syncing/connectionStatus.js +89 -19
  23. package/dist/scanning-syncing/scanSettings.d.ts +45 -0
  24. package/dist/scanning-syncing/scanSettings.js +63 -0
  25. package/dist/scanning-syncing/scanresult/reorg.d.ts +0 -11
  26. package/dist/scanning-syncing/scanresult/reorg.js +39 -16
  27. package/dist/scanning-syncing/scanresult/scanCache.d.ts +16 -2
  28. package/dist/scanning-syncing/scanresult/scanCache.js +58 -2
  29. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +18 -0
  30. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +56 -11
  31. package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +127 -0
  32. package/dist/scanning-syncing/scanresult/scanCoordination.js +611 -0
  33. package/dist/scanning-syncing/scanresult/scanLoop.d.ts +20 -0
  34. package/dist/scanning-syncing/scanresult/scanLoop.js +116 -0
  35. package/dist/scanning-syncing/scanresult/scanResult.d.ts +24 -3
  36. package/dist/scanning-syncing/scanresult/scanResult.js +151 -98
  37. package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
  38. package/dist/scanning-syncing/worker-mains/coordinator-main.d.ts +1 -0
  39. package/dist/scanning-syncing/worker-mains/coordinator-main.js +23 -0
  40. package/dist/scanning-syncing/worker-mains/cpubound-main.d.ts +5 -0
  41. package/dist/scanning-syncing/worker-mains/cpubound-main.js +112 -0
  42. package/dist/scanning-syncing/worker-mains/worker.d.ts +1 -1
  43. package/dist/scanning-syncing/worker-mains/worker.js +58 -6
  44. package/dist/viewpair/ViewPair.d.ts +30 -7
  45. package/dist/viewpair/ViewPair.js +56 -118
  46. package/dist/wasm-processing/wasmFile.js +1 -1
  47. package/package.json +22 -24
  48. package/dist/io/extension.d.ts +0 -18
  49. package/dist/io/extension.js +0 -11
@@ -1,10 +1,72 @@
1
- import { atomicWrite } from "../api";
1
+ import { atomicWrite, } from "../api";
2
2
  import { SCAN_SETTINGS_STORE_NAME_DEFAULT } from "./scanSettings";
3
3
  export const DEFAULT_CONNECTION_STATUS_PREFIX = "ConnectionStatus-";
4
- export function connectionStatusFilePath(scan_settings_path) {
5
- if (!scan_settings_path)
6
- scan_settings_path = SCAN_SETTINGS_STORE_NAME_DEFAULT;
7
- return `${DEFAULT_CONNECTION_STATUS_PREFIX}${scan_settings_path}`;
4
+ export function msToHHMM(ms) {
5
+ const totalSeconds = Math.floor(ms / 1000);
6
+ const minutes = Math.floor(totalSeconds / 60);
7
+ const hours = Math.floor(minutes / 60);
8
+ const remainingMinutes = minutes % 60;
9
+ if (!Number.isFinite(hours) || !Number.isFinite(minutes)) {
10
+ return "00:00";
11
+ }
12
+ const paddedHours = String(hours).padStart(2, "0");
13
+ const paddedMinutes = String(remainingMinutes).padStart(2, "0");
14
+ return `${paddedHours}:${paddedMinutes}`;
15
+ }
16
+ export function emptyConnectionStatus(overrides) {
17
+ const defaultStatus = {
18
+ last_packet: {
19
+ status: "no_connection_yet",
20
+ bytes_read: 0,
21
+ node_url: "",
22
+ timestamp: new Date().toISOString(),
23
+ },
24
+ sync: {
25
+ scanned_ranges: [],
26
+ daemon_height: 0,
27
+ current_scan_height: 0,
28
+ eta: "00:00",
29
+ timestamp: new Date().toISOString(),
30
+ },
31
+ };
32
+ return overrides ? { ...defaultStatus, ...overrides } : defaultStatus;
33
+ }
34
+ export async function updateSyncETA(daemon_height, current_scan_height, last_height, last_timestamp, scan_settings_path) {
35
+ const blocks_till_tip = daemon_height - current_scan_height;
36
+ let blocks_since_last_update = null;
37
+ let duration = null;
38
+ if (typeof last_height === "number") {
39
+ blocks_since_last_update = current_scan_height - last_height;
40
+ }
41
+ if (typeof last_timestamp === "number") {
42
+ duration = Date.now() - last_timestamp;
43
+ }
44
+ let eta = "00:00";
45
+ if (blocks_since_last_update !== null &&
46
+ duration !== null &&
47
+ duration > 0 &&
48
+ blocks_till_tip > 0) {
49
+ const blocks_per_ms = blocks_since_last_update / duration;
50
+ const eta_ms = blocks_till_tip / blocks_per_ms;
51
+ eta = msToHHMM(eta_ms);
52
+ }
53
+ await readWriteConnectionStatusFile((cs) => {
54
+ cs.sync = {
55
+ ...cs.sync,
56
+ daemon_height,
57
+ current_scan_height,
58
+ eta,
59
+ timestamp: new Date().toISOString(),
60
+ };
61
+ }, scan_settings_path);
62
+ return { last_height: current_scan_height, last_timestamp: Date.now() };
63
+ }
64
+ export function connectionStatusFilePath(scan_settings_path = SCAN_SETTINGS_STORE_NAME_DEFAULT) {
65
+ const parts = scan_settings_path.split("/");
66
+ const basename = parts.pop();
67
+ const dir = parts.join("/");
68
+ const prefix = dir ? `${dir}/` : "";
69
+ return `${prefix}${DEFAULT_CONNECTION_STATUS_PREFIX}${basename}`;
8
70
  }
9
71
  export async function readConnectionStatusDefaultLocation(scan_settings_path) {
10
72
  return await readConnectionStatusFile(connectionStatusFilePath(scan_settings_path));
@@ -15,21 +77,29 @@ export async function readConnectionStatusFile(connectionStatusFilePath) {
15
77
  .catch(() => undefined);
16
78
  return jsonString ? JSON.parse(jsonString) : undefined;
17
79
  }
18
- export async function writeConnectionStatusFile(status, node_url, bytes_read = 0, scan_settings_path) {
19
- const connectionStatus = {
20
- last_packet: {
21
- status,
22
- bytes_read,
23
- node_url,
24
- timestamp: new Date().toISOString(),
25
- },
26
- };
80
+ export async function writeConnectionStatusFile(connectionStatus, scan_settings_path) {
27
81
  return await atomicWrite(connectionStatusFilePath(scan_settings_path), JSON.stringify(connectionStatus, null, 2));
28
82
  }
29
83
  export async function readWriteConnectionStatusFile(writeCB, scan_settings_path) {
30
- const connectionStatus = await readConnectionStatusDefaultLocation(scan_settings_path);
31
- const cb = await writeCB(connectionStatus);
32
- if (typeof cb === "undefined")
33
- return;
34
- return await writeConnectionStatusFile(cb.last_packet.status, cb.last_packet.node_url, cb.last_packet.bytes_read, scan_settings_path);
84
+ let connectionStatus = await readConnectionStatusDefaultLocation(scan_settings_path);
85
+ if (!connectionStatus)
86
+ connectionStatus = emptyConnectionStatus();
87
+ await writeCB(connectionStatus);
88
+ await writeConnectionStatusFile(connectionStatus, scan_settings_path);
89
+ return connectionStatus;
90
+ }
91
+ /**
92
+ * read the connection status from disk (typical /path/to/ConnectionStatus-ScanSettings.json)
93
+ * and persist empty inital status if not found
94
+ * @param scan_settings_path path to the scan settings file /path/to/ScanSettings.json
95
+ * @returns connection status or empty initalized connection status
96
+ */
97
+ export async function readOrInitConnectionStatus(scan_settings_path) {
98
+ const cs = await readConnectionStatusFile(connectionStatusFilePath(scan_settings_path));
99
+ if (typeof cs === "undefined") {
100
+ const emptyInitial = emptyConnectionStatus();
101
+ await writeConnectionStatusFile(emptyInitial, scan_settings_path);
102
+ return emptyInitial;
103
+ }
104
+ return cs;
35
105
  }
@@ -34,11 +34,15 @@ export type ScanSettings = {
34
34
  wallets: ScanSetting[];
35
35
  node_url: string;
36
36
  start_height: number | null;
37
+ merchant_confirmations?: number | null;
38
+ cpu_worker_count?: number;
37
39
  };
38
40
  export type ScanSettingsOpened = {
39
41
  wallets: (ScanSettingOpened | undefined)[];
40
42
  node_url: string;
41
43
  start_height: number | null;
44
+ merchant_confirmations?: number | null;
45
+ cpu_worker_count?: number;
42
46
  };
43
47
  /**
44
48
  * Writes scan settings to the default or specified storage file in json.
@@ -71,6 +75,38 @@ export declare function writeDaemonHeightAsStartHeightToScanSettings(node_url: s
71
75
  export declare function cullTooLargeScanHeight(node_url: string, settingsStorePath?: string): Promise<number>;
72
76
  export declare function writeNodeUrlToScanSettings(node_url: string, settingsStorePath?: string): Promise<number>;
73
77
  export declare function readNodeUrlFromScanSettings(settingsStorePath?: string): Promise<string | undefined>;
78
+ /**
79
+ * Writes the merchant confirmation threshold to scan settings.
80
+ *
81
+ * This setting only has effect when the wallet is run as part of a checkout system,
82
+ * that accepts payments.
83
+ * It determines after how many confirmations a transaction is accepted
84
+ * for a payment. The actual enforcement of this policy is left to the
85
+ * custom payment-system code that consumes this library.
86
+ *
87
+ * @param merchant_confirmations - Number of confirmations required before
88
+ * accepting a payment, or `null` to leave it unset.
89
+ * @param settingsStorePath - Optional path for the settings file.
90
+ * Defaults to `SCAN_SETTINGS_STORE_NAME_DEFAULT`.
91
+ * @returns A promise that resolves when the file is successfully written.
92
+ */
93
+ export declare function writeMerchantConfirmationsToScanSettings(merchant_confirmations: number | null, settingsStorePath?: string): Promise<number>;
94
+ /**
95
+ * Reads the merchant confirmation threshold from scan settings.
96
+ *
97
+ * This setting only has effect when the wallet is run as part of a checkout system,
98
+ * that accepts payments.
99
+ *
100
+ * It determines after how many confirmations a transaction is accepted
101
+ * for a payment. The actual enforcement of this policy is left to the
102
+ * custom payment-system code that consumes this library.
103
+ *
104
+ * @param settingsStorePath - Optional path for the settings file.
105
+ * Defaults to `SCAN_SETTINGS_STORE_NAME_DEFAULT`.
106
+ * @returns The configured number of confirmations, null, or `undefined`
107
+ * if the setting has not been persisted yet.
108
+ */
109
+ export declare function readMerchantConfirmationsFromScanSettings(settingsStorePath?: string): Promise<number | null | undefined>;
74
110
  /**
75
111
  * Reads scan settings from the default or specified storage file.
76
112
  * secret_view_key and spend_private_key are read from environment variables
@@ -106,4 +142,13 @@ export declare function walletSettingsPlusKeys(wallet_settings: ScanSettingOpene
106
142
  export declare function writeWalletToScanSettings(params: WriteScanSettingParams): Promise<number>;
107
143
  export declare function openScanSettingsFile(scan_settings_path?: string): Promise<ScanSettings | undefined>;
108
144
  export declare function openNonHaltedWallets(scan_settings_path?: string): Promise<ScanSettingOpened[]>;
145
+ export declare function getNonHaltedWallets(scan_settings?: ScanSettings): ScanSetting[];
109
146
  export declare function doesScanSettingsFileExist(scan_settings_path?: string): Promise<boolean>;
147
+ /**
148
+ * Either 1. the consumer explicitly sets a pathprefix, we extract it from the scan_settings_path
149
+ * or we pick the default which is nothing
150
+ * @param scan_settings_path optional scan settings path
151
+ * @param pathPrefix optional path prefix
152
+ * @returns definite path prefix
153
+ */
154
+ export declare function getPathPrefix(scan_settings_path?: string, pathPrefix?: string): string;
@@ -87,6 +87,49 @@ export async function readNodeUrlFromScanSettings(settingsStorePath = SCAN_SETTI
87
87
  const scanSettings = await openScanSettingsFile(settingsStorePath);
88
88
  return scanSettings?.node_url;
89
89
  }
90
+ /**
91
+ * Writes the merchant confirmation threshold to scan settings.
92
+ *
93
+ * This setting only has effect when the wallet is run as part of a checkout system,
94
+ * that accepts payments.
95
+ * It determines after how many confirmations a transaction is accepted
96
+ * for a payment. The actual enforcement of this policy is left to the
97
+ * custom payment-system code that consumes this library.
98
+ *
99
+ * @param merchant_confirmations - Number of confirmations required before
100
+ * accepting a payment, or `null` to leave it unset.
101
+ * @param settingsStorePath - Optional path for the settings file.
102
+ * Defaults to `SCAN_SETTINGS_STORE_NAME_DEFAULT`.
103
+ * @returns A promise that resolves when the file is successfully written.
104
+ */
105
+ export async function writeMerchantConfirmationsToScanSettings(merchant_confirmations, settingsStorePath = SCAN_SETTINGS_STORE_NAME_DEFAULT) {
106
+ const scanSettings = (await openScanSettingsFile(settingsStorePath)) || {
107
+ node_url: LOCAL_NODE_DEFAULT_URL,
108
+ wallets: [],
109
+ start_height: null,
110
+ };
111
+ scanSettings.merchant_confirmations = merchant_confirmations;
112
+ return await atomicWrite(settingsStorePath, JSON.stringify(scanSettings, null, 2));
113
+ }
114
+ /**
115
+ * Reads the merchant confirmation threshold from scan settings.
116
+ *
117
+ * This setting only has effect when the wallet is run as part of a checkout system,
118
+ * that accepts payments.
119
+ *
120
+ * It determines after how many confirmations a transaction is accepted
121
+ * for a payment. The actual enforcement of this policy is left to the
122
+ * custom payment-system code that consumes this library.
123
+ *
124
+ * @param settingsStorePath - Optional path for the settings file.
125
+ * Defaults to `SCAN_SETTINGS_STORE_NAME_DEFAULT`.
126
+ * @returns The configured number of confirmations, null, or `undefined`
127
+ * if the setting has not been persisted yet.
128
+ */
129
+ export async function readMerchantConfirmationsFromScanSettings(settingsStorePath = SCAN_SETTINGS_STORE_NAME_DEFAULT) {
130
+ const scanSettings = await openScanSettingsFile(settingsStorePath);
131
+ return scanSettings?.merchant_confirmations;
132
+ }
90
133
  /**
91
134
  * Reads scan settings from the default or specified storage file.
92
135
  * secret_view_key and spend_private_key are read from environment variables
@@ -247,6 +290,12 @@ export async function openNonHaltedWallets(scan_settings_path = SCAN_SETTINGS_ST
247
290
  throw new Error("no non halted wallets in scan settings");
248
291
  return nonHaltedWallets;
249
292
  }
293
+ export function getNonHaltedWallets(scan_settings) {
294
+ if (!scan_settings)
295
+ return [];
296
+ const nonHaltedWallets = scan_settings.wallets.filter((wallet) => !wallet?.halted);
297
+ return nonHaltedWallets;
298
+ }
250
299
  export async function doesScanSettingsFileExist(scan_settings_path = SCAN_SETTINGS_STORE_NAME_DEFAULT) {
251
300
  const scan_settings_file_content = await Bun.file(scan_settings_path)
252
301
  .text()
@@ -258,3 +307,17 @@ export async function doesScanSettingsFileExist(scan_settings_path = SCAN_SETTIN
258
307
  return false;
259
308
  }
260
309
  }
310
+ /**
311
+ * Either 1. the consumer explicitly sets a pathprefix, we extract it from the scan_settings_path
312
+ * or we pick the default which is nothing
313
+ * @param scan_settings_path optional scan settings path
314
+ * @param pathPrefix optional path prefix
315
+ * @returns definite path prefix
316
+ */
317
+ export function getPathPrefix(scan_settings_path = SCAN_SETTINGS_STORE_NAME_DEFAULT, pathPrefix) {
318
+ const parts = scan_settings_path.split("/");
319
+ const basename = parts.pop();
320
+ const dir = parts.join("/");
321
+ const prefix = dir ? `${dir}/` : "";
322
+ return pathPrefix ?? prefix;
323
+ }
@@ -1,14 +1,3 @@
1
- import type { BlockInfo, Output } from "../../api";
2
- import type { KeyImage } from "./computeKeyImage";
3
1
  import { type ScanResult } from "./scanResult";
4
2
  import type { CacheRange, ChangedOutput, ScanCache } from "./scanCache";
5
- export type ReorgInfo = {
6
- split_height: BlockInfo;
7
- removed_outputs: ReorgedOutput[];
8
- reverted_spends: ReorgedOutput[];
9
- };
10
- export type ReorgedOutput = {
11
- old_output_state: Output;
12
- key_image: KeyImage;
13
- };
14
3
  export declare function handleReorg(current_range: CacheRange, result: ScanResult, cache: ScanCache, oldRange: CacheRange): [CacheRange, ChangedOutput[]];
@@ -1,25 +1,48 @@
1
1
  import { makeNewRange } from "./scanResult";
2
2
  export function handleReorg(current_range, result, cache, oldRange) {
3
3
  let changed_outputs = [];
4
+ console.log("[handleReorg] oldRange hashes:", oldRange.block_hashes.map((h) => h.block_height + ":" + h.block_hash.slice(0, 8)));
5
+ console.log("[handleReorg] cache outputs:", Object.entries(cache.outputs).map(([id, o]) => "h=" + o.block_height + " spent=" + (o.spent_block_height ?? "no")));
6
+ console.log("[handleReorg] result infos:", result.block_infos.length +
7
+ " blocks, first=" +
8
+ result.block_infos[0]?.block_height, "last=" + result.block_infos.at(-1)?.block_height);
4
9
  // we need to check where anchor candidate is and if not found, try the same for anchor
5
10
  // if else throw on catastrophic reorg
6
11
  for (const block_hash of oldRange.block_hashes) {
7
12
  const split_height_index = result.block_infos.findIndex((b) => b.block_hash === block_hash.block_hash);
13
+ if (!(split_height_index > 0))
14
+ continue;
15
+ console.log("[handleReorg] trying hash", block_hash.block_height, block_hash.block_hash.slice(0, 8), "found at idx", split_height_index);
8
16
  const split_height = result.block_infos[split_height_index];
9
17
  // still a chance to find the split height, (could be candidate_anchor or anchor)
10
18
  if (!split_height)
11
19
  continue;
12
20
  // we found the split height & do the reorg
13
- const reorg_info = {
14
- split_height,
15
- removed_outputs: [],
16
- reverted_spends: [],
17
- };
21
+ console.log("[handleReorg] SPLIT FOUND at height", split_height.block_height);
22
+ if (!cache.reorg_info) {
23
+ cache.reorg_info = {
24
+ split_heights: [split_height],
25
+ removed_outputs: [],
26
+ reverted_spends: [],
27
+ };
28
+ }
29
+ else {
30
+ cache.reorg_info.split_heights.push(split_height);
31
+ }
32
+ // First, collect reverted spends from all outputs (before any removals)
33
+ // so that outputs with block_height < split_height but spent_block_height >= split_height are captured
34
+ const reverted_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.spent_block_height !== undefined &&
35
+ output.spent_block_height >= split_height.block_height);
18
36
  const removed_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.block_height >= split_height.block_height);
37
+ console.log("[handleReorg] removed_outputs count:", removed_outputs.length, "reverted count:", reverted_outputs.length);
19
38
  for (const [id, old_output_state] of removed_outputs) {
20
39
  // 1. find key_image of output to be removed (as it was reorged)
21
40
  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
22
- reorg_info.removed_outputs.push({ old_output_state, key_image });
41
+ cache.reorg_info.removed_outputs.push({
42
+ old_output_state,
43
+ key_image,
44
+ split_height,
45
+ });
23
46
  // 2. remove from outputs and own_key_images
24
47
  delete cache.outputs[id];
25
48
  delete cache.own_key_images[key_image];
@@ -28,21 +51,22 @@ export function handleReorg(current_range, result, cache, oldRange) {
28
51
  change_reason: "reorged",
29
52
  });
30
53
  }
31
- //for reverted spents, just do the same again with spent_height
32
- const reverted_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.spent_block_height !== undefined &&
33
- output.spent_block_height >= split_height.block_height);
34
54
  for (const [id, old_output_state_pointer] of reverted_outputs) {
35
55
  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
36
56
  const old_output_state = Object.assign({}, old_output_state_pointer);
37
- reorg_info.reverted_spends.push({
57
+ cache.reorg_info.reverted_spends.push({
38
58
  old_output_state,
39
59
  key_image, // in this case key_image only used here, does not get removed
60
+ split_height,
40
61
  });
41
- // remove spend info from original cache
42
- delete cache.outputs[id].spent_relative_index;
43
- delete cache.outputs[id].spent_in_tx_hash;
44
- delete cache.outputs[id].spent_block_height;
45
- delete cache.outputs[id].spent_block_timestamp;
62
+ // remove spend info from original cache (if output still exists
63
+ // it may have been deleted above in the removed_outputs loop)
64
+ if (cache.outputs[id]) {
65
+ delete cache.outputs[id].spent_relative_index;
66
+ delete cache.outputs[id].spent_in_tx_hash;
67
+ delete cache.outputs[id].spent_block_height;
68
+ delete cache.outputs[id].spent_block_timestamp;
69
+ }
46
70
  changed_outputs.push({
47
71
  output: old_output_state,
48
72
  change_reason: "reorged_spent",
@@ -51,7 +75,6 @@ export function handleReorg(current_range, result, cache, oldRange) {
51
75
  // find current range in scanned ranges and change its end value + latest_block_hash
52
76
  oldRange.end = split_height.block_height;
53
77
  oldRange.block_hashes[0] = split_height;
54
- cache.reorg_info = reorg_info;
55
78
  // fix current_range
56
79
  let anchor = result.block_infos.slice(-100)[0]; // if we got lots of new blocks
57
80
  const old_anchor = oldRange.block_hashes.at(-1);
@@ -1,8 +1,10 @@
1
1
  import { type KeyImage } from "./computeKeyImage";
2
- import type { ReorgInfo } from "./reorg";
3
- import type { BlockInfo, FeeEstimateResponse, GetBlockHeadersRange, GetBlockHeadersRangeParams, Output, SendRawTransactionResult, ViewPair } from "../../api";
2
+ import { type BlockInfo, type FeeEstimateResponse, type GetBlockHeadersRange, type GetBlockHeadersRangeParams, type Output, type SendRawTransactionResult, type ViewPair } from "../../api";
4
3
  import type { Payment } from "../../send-functionality/inputSelection";
5
4
  export declare function initScanCache(viewpair: ViewPair, start_height: number, scan_settings_path?: string, pathPrefix?: string): Promise<CacheRange>;
5
+ export declare function getBlockInfoForHeight(height: number, node_url: string): Promise<BlockInfo>;
6
+ export declare function makeCacheRangeForHeight(height: number, node_url: string): Promise<CacheRange>;
7
+ export declare function initScanCacheFile(viewpair: ViewPair, scan_settings_path?: string, pathPrefix?: string): Promise<ScanCache>;
6
8
  export declare function readCacheFile(cacheFilePath: string): Promise<ScanCache | undefined>;
7
9
  export declare function cacheFileDefaultLocation(primary_address: string, pathPrefix?: string): string;
8
10
  export declare function readCacheFileDefaultLocation(primary_address: string, pathPrefix?: string): Promise<ScanCache | undefined>;
@@ -14,8 +16,10 @@ export type WriteCacheFileParams = {
14
16
  export declare function writeCacheFileDefaultLocationThrows(params: WriteCacheFileParams): Promise<void>;
15
17
  export declare function writeCacheToFile(cache: ScanCache, pathPrefix?: string): Promise<number>;
16
18
  export declare function lastRange(ranges: CacheRange[]): CacheRange | undefined;
19
+ export declare function lastRangeThrows(ranges: CacheRange[]): CacheRange;
17
20
  export declare function mergeRanges(ranges: CacheRange[]): CacheRange[];
18
21
  export declare const findRange: (ranges: CacheRange[], value: number) => CacheRange | null;
22
+ export declare function findRangeThrows(ranges: CacheRange[], value: number): CacheRange;
19
23
  export type CacheRange = {
20
24
  start: number;
21
25
  end: number;
@@ -44,6 +48,16 @@ export type ScanCache = {
44
48
  reorg_info?: ReorgInfo;
45
49
  daemon_height: number;
46
50
  };
51
+ export type ReorgInfo = {
52
+ split_heights: BlockInfo[];
53
+ removed_outputs: ReorgedOutput[];
54
+ reverted_spends: ReorgedOutput[];
55
+ };
56
+ export type ReorgedOutput = {
57
+ old_output_state: Output;
58
+ key_image: KeyImage;
59
+ split_height: BlockInfo;
60
+ };
47
61
  export type TxLog = {
48
62
  inputs_index: string[];
49
63
  payments: Payment[];
@@ -1,4 +1,5 @@
1
1
  import {} from "./computeKeyImage";
2
+ import { get_block_headers_range, } from "../../api";
2
3
  import { atomicWrite } from "../../io/atomicWrite";
3
4
  export async function initScanCache(viewpair, start_height, scan_settings_path, pathPrefix) {
4
5
  const initialCache = await readCacheFileDefaultLocation(viewpair.primary_address, pathPrefix);
@@ -43,6 +44,44 @@ export async function initScanCache(viewpair, start_height, scan_settings_path,
43
44
  await writeCacheToFile(cache, pathPrefix);
44
45
  return current_range;
45
46
  }
47
+ export async function getBlockInfoForHeight(height, node_url) {
48
+ const blockHeaderResponse = await get_block_headers_range(node_url, {
49
+ start_height: height,
50
+ end_height: height,
51
+ });
52
+ const header = blockHeaderResponse.headers[0];
53
+ return {
54
+ block_hash: header.hash,
55
+ block_height: header.height,
56
+ block_timestamp: header.timestamp,
57
+ };
58
+ }
59
+ export async function makeCacheRangeForHeight(height, node_url) {
60
+ const hash_at_height = await getBlockInfoForHeight(height, node_url);
61
+ const newRange = {
62
+ start: height,
63
+ end: height,
64
+ block_hashes: [hash_at_height, hash_at_height, hash_at_height],
65
+ };
66
+ return newRange;
67
+ }
68
+ export async function initScanCacheFile(viewpair, scan_settings_path, pathPrefix) {
69
+ const initialCache = await readCacheFileDefaultLocation(viewpair.primary_address, pathPrefix);
70
+ let cache = {
71
+ daemon_height: 0,
72
+ outputs: {},
73
+ own_key_images: {},
74
+ scanned_ranges: [],
75
+ primary_address: viewpair.primary_address,
76
+ };
77
+ if (initialCache)
78
+ cache = initialCache;
79
+ cache.scanned_ranges = mergeRanges(cache.scanned_ranges);
80
+ await viewpair.addSubaddressesToScanCache(cache, scan_settings_path);
81
+ // write to cache
82
+ await writeCacheToFile(cache, pathPrefix);
83
+ return cache;
84
+ }
46
85
  export async function readCacheFile(cacheFilePath) {
47
86
  const jsonString = await Bun.file(cacheFilePath)
48
87
  .text()
@@ -56,6 +95,10 @@ export async function readCacheFile(cacheFilePath) {
56
95
  : undefined;
57
96
  }
58
97
  export function cacheFileDefaultLocation(primary_address, pathPrefix) {
98
+ if (pathPrefix && pathPrefix.endsWith("/"))
99
+ pathPrefix = pathPrefix.slice(0, -1);
100
+ if (pathPrefix)
101
+ pathPrefix += "/";
59
102
  return `${pathPrefix ?? ""}${primary_address}_cache.json`;
60
103
  }
61
104
  export async function readCacheFileDefaultLocation(primary_address, pathPrefix) {
@@ -78,6 +121,11 @@ export function lastRange(ranges) {
78
121
  return undefined;
79
122
  return ranges.reduce((maxRange, current) => (current.end > maxRange.end ? current : maxRange), ranges[0]);
80
123
  }
124
+ export function lastRangeThrows(ranges) {
125
+ if (!ranges.length)
126
+ throw new Error("ranges is empty");
127
+ return ranges.reduce((maxRange, current) => (current.end > maxRange.end ? current : maxRange), ranges[0]);
128
+ }
81
129
  export function mergeRanges(ranges) {
82
130
  if (ranges.length <= 1)
83
131
  return ranges.map((r) => ({ ...r }));
@@ -89,8 +137,10 @@ export function mergeRanges(ranges) {
89
137
  // If last range overlaps or touches current range
90
138
  if (curr.start <= last.end) {
91
139
  // Extend last range to cover both (take max end value)
92
- last.end = Math.max(last.end, curr.end);
93
- last.block_hashes = curr.block_hashes;
140
+ if (curr.end > last.end) {
141
+ last.end = curr.end;
142
+ last.block_hashes = curr.block_hashes;
143
+ }
94
144
  }
95
145
  else {
96
146
  // No overlap: add current range as new merged interval
@@ -101,6 +151,12 @@ export function mergeRanges(ranges) {
101
151
  }
102
152
  // find the cache range that contains the given height, if not found return null
103
153
  export const findRange = (ranges, value) => ranges.find((r) => value >= r.start && value <= r.end) ?? null;
154
+ export function findRangeThrows(ranges, value) {
155
+ const range = findRange(ranges, value);
156
+ if (!range)
157
+ throw new Error(`range not found for value: ${value}`);
158
+ return range;
159
+ }
104
160
  export function handleScanError(error) {
105
161
  // treat errno 0 code "ConnectionRefused" as non fatal outcome, and rethrow,
106
162
  // so that UI can be informed after catching it higher up
@@ -28,6 +28,22 @@ export declare class ScanCacheOpened {
28
28
  private scan_settings_path?;
29
29
  private pathPrefix?;
30
30
  private workerError?;
31
+ /** how many decoys to sample per input (default 20, ring size is 11) */
32
+ decoySampleCount: number;
33
+ /**
34
+ * when true, retry makeInput with higher sample counts on failure.
35
+ *
36
+ * PRIVACY WARNING: retrying contacts the node multiple times for the same
37
+ * input, each time with a different set of candidate indices. this lets the
38
+ * node correlate which output is the real spend across the retries.
39
+ * only ever enable this on your own local trusted node, never on a remote
40
+ * public node.
41
+ *
42
+ * defaults to false, on failure the original error propagates.
43
+ */
44
+ decoyRetry: boolean;
45
+ /** sample sizes to try when decoyRetry is enabled, in order */
46
+ readonly decoyRetrySizes: number[];
31
47
  static create(params: ScanCacheOpenedCreateParams): Promise<ScanCacheOpened>;
32
48
  get start_height(): number | null;
33
49
  get current_height(): number | null;
@@ -120,6 +136,8 @@ export declare class ScanCacheOpened {
120
136
  feed(params: CacheChangedCallbackParameters): Promise<void>;
121
137
  private _cache;
122
138
  private worker?;
139
+ private last_eta_height;
140
+ private last_eta_timestamp;
123
141
  private constructor();
124
142
  private _stats;
125
143
  private notifyListeners;