@dusk-network/app-core 0.4.1 → 0.5.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.
package/src/bridge.js CHANGED
@@ -10,11 +10,46 @@
10
10
  * @module
11
11
  */
12
12
 
13
- import { networkFor, routeForAccount, routes } from "@dusk-network/networks";
13
+ import { routeForAccount, routes } from "@dusk-network/networks";
14
14
  import { isAccount } from "@dusk-network/networks/account";
15
- import { BRIDGE_FUNCTIONS, duskEvmContracts, evmAddressOf } from "@dusk-network/networks/duskevm";
15
+ import { BRIDGE_FUNCTIONS, decodeBridgeCall, duskEvmContracts, evmAddressOf, isBridgeContract } from "@dusk-network/networks/duskevm";
16
+ import {
17
+ CREDIT_WINDOW_MS,
18
+ EVM_ADDRESS,
19
+ EVM_HASH,
20
+ TIMEOUT_MS,
21
+ WAIT_MS,
22
+ WEI_PER_LUX,
23
+ addressOf,
24
+ allOpen,
25
+ byTime,
26
+ depositsIn,
27
+ duskHashOf,
28
+ evmHashOf,
29
+ groupBy,
30
+ heightOf,
31
+ inOrder,
32
+ luxOf,
33
+ messageOf,
34
+ msOf,
35
+ networkOf,
36
+ pays,
37
+ quantityOf,
38
+ queued,
39
+ reader,
40
+ settle,
41
+ triage,
42
+ uniqueByHash,
43
+ urlOf,
44
+ usedOf,
45
+ withPast,
46
+ } from "./bridge-common.js";
47
+ import { depositArrivalsFromExplorer } from "./bridge-explorer.js";
16
48
  import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "./history.js";
17
49
 
50
+ export { CREDIT_WINDOW_MS, EXPLORER_PAGES, WAIT_MS } from "./bridge-common.js";
51
+ export { depositArrivalsFromExplorer } from "./bridge-explorer.js";
52
+
18
53
  /** @typedef {import("./bridge.js").Arrival} Arrival */
19
54
  /** @typedef {import("./bridge.js").BlockTimes} BlockTimes */
20
55
  /** @typedef {import("./bridge.js").Deposit} Deposit */
@@ -35,34 +70,15 @@ import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "
35
70
  /** @typedef {import("@dusk-network/networks").Network} Network */
36
71
  /** @typedef {import("@dusk-network/networks").NetworkRef} NetworkRef */
37
72
  /** @typedef {import("@dusk-network/networks").Route} Route */
38
-
39
- /**
40
- * A transfer as the matching sees it: when it was sent, the block on the
41
- * other chain its credit can't be before, and whether it is one of the
42
- * app's own (`given`) or one read from the chain.
43
- *
44
- * @typedef {{ hash: string, at: number, notBefore: bigint | null, given: boolean, scan: Scan | null }} Queued
45
- */
46
-
47
- /**
48
- * A credit on the other chain: its transaction, its block and time, its
49
- * amount in wei, whether it went through, and the address it came from as
50
- * its log names it (lower case; null where it isn't named). Found by an RPC
51
- * search, the passes over its blocks again done before it was (`pass`).
52
- *
53
- * @typedef {{ hash: string, block: bigint | null, at: number, wei: bigint, success: boolean, from: string | null, pass?: number }} Credit
54
- */
55
-
56
- /**
57
- * What a group's matching gave each transfer.
58
- *
59
- * @typedef {"arrived" | "waiting" | "unsure" | "late"} Verdict
60
- */
73
+ /** @typedef {import("./bridge.js").BridgeNetwork} BridgeNetwork */
74
+ /** @typedef {import("./bridge.js").FactCache} FactCache */
75
+ /** @typedef {import("./bridge-common.js").Credit} Credit */
76
+ /** @typedef {import("./bridge-common.js").DepositItem} DepositItem */
77
+ /** @typedef {import("./bridge-common.js").Queued} Queued */
78
+ /** @typedef {import("./bridge-common.js").Reader} Reader */
79
+ /** @typedef {import("./bridge-common.js").Rules} Rules */
61
80
 
62
81
  export const CLOCK_SLACK_MS = 120_000;
63
- export const CREDIT_WINDOW_MS = 120_000;
64
- export const WAIT_MS = 7 * 24 * 60 * 60_000;
65
- export const EXPLORER_PAGES = 5;
66
82
  export const LOG_WINDOW = 5_000;
67
83
  export const LOG_WINDOWS = 8;
68
84
  export const DEPOSIT_HORIZON_MS = 2 * 60 * 60_000;
@@ -71,9 +87,6 @@ export const HISTORY_PAGES = 4;
71
87
  export const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
72
88
  export const ETH_BRIDGE_FINALIZED_TOPIC = "0x31b2166ff604fc5672ea5df08a78081d2bc6d746cadce880747f3643d819e83d";
73
89
 
74
- const WEI_PER_LUX = 1_000_000_000n;
75
- const MAX_U64 = (1n << 64n) - 1n;
76
- const TIMEOUT_MS = 8_000;
77
90
  // About one Dusk block every 10 seconds: how far back a sender's history is read.
78
91
  const DUSK_BLOCK_MS = 10_000;
79
92
  // The blocks the chain's pace is read over, from a block rounded down to a
@@ -99,84 +112,12 @@ const RECHECK_MS = 60_000;
99
112
  const CONFIRM_PASSES = 8;
100
113
  // What an RPC says when it won't search that many blocks at once.
101
114
  const RANGE_REFUSED = /limit|range|too many|exceed/i;
102
- const DUSK_HASH = /^[0-9a-f]{64}$/;
103
- const EVM_HASH = /^0x[0-9a-f]{64}$/;
104
- const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
105
115
  const WORDS = /^0x(?:[0-9a-fA-F]{64})+$/;
106
116
 
107
117
  // ---------------------------------------------------------------------------
108
118
  // Values
109
119
  // ---------------------------------------------------------------------------
110
120
 
111
- /**
112
- * Lux from a bigint, a safe integer or its decimal text: null for anything
113
- * else, and past a u64.
114
- *
115
- * @param {unknown} value
116
- * @returns {bigint | null}
117
- */
118
- function luxOf(value) {
119
- /** @type {bigint} */
120
- let n;
121
-
122
- if (typeof value === "bigint") n = value;
123
- else if (typeof value === "number" && Number.isSafeInteger(value)) n = BigInt(value);
124
- else if (typeof value === "string" && /^\d+$/.test(value.trim())) n = BigInt(value.trim());
125
- else return null;
126
-
127
- return n >= 0n && n <= MAX_U64 ? n : null;
128
- }
129
-
130
- /**
131
- * A block number or a time as JSON-RPC writes it (hex), or as decimal: null
132
- * for anything else.
133
- *
134
- * @param {unknown} value
135
- * @returns {bigint | null}
136
- */
137
- function quantityOf(value) {
138
- if (typeof value === "bigint") return value >= 0n ? value : null;
139
- if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
140
-
141
- const text = String(value ?? "").trim();
142
-
143
- return /^0x[0-9a-fA-F]+$/.test(text) || /^\d+$/.test(text) ? BigInt(text) : null;
144
- }
145
-
146
- /**
147
- * A Dusk transaction's hash, lower case, without 0x; "" for anything else.
148
- *
149
- * @param {unknown} value
150
- */
151
- const duskHashOf = (value) => {
152
- const hash = String(value ?? "")
153
- .trim()
154
- .toLowerCase()
155
- .replace(/^0x/, "");
156
-
157
- return DUSK_HASH.test(hash) ? hash : "";
158
- };
159
-
160
- /**
161
- * An EVM transaction's hash, lower case; "" for anything else.
162
- *
163
- * @param {unknown} value
164
- */
165
- const evmHashOf = (value) => {
166
- const hash = String(value ?? "")
167
- .trim()
168
- .toLowerCase();
169
-
170
- return EVM_HASH.test(hash) ? hash : "";
171
- };
172
-
173
- /**
174
- * An EVM address, lower case; "" for anything else.
175
- *
176
- * @param {unknown} value
177
- */
178
- const addressOf = (value) => (typeof value === "string" && EVM_ADDRESS.test(value) ? value.toLowerCase() : "");
179
-
180
121
  /**
181
122
  * An address as an indexed log topic: 32 bytes, lower case.
182
123
  *
@@ -187,33 +128,6 @@ const topicOf = (address) => `0x${address.slice(2).toLowerCase().padStart(64, "0
187
128
  /** @param {bigint} n */
188
129
  const hex = (n) => `0x${n.toString(16)}`;
189
130
 
190
- /**
191
- * @param {unknown} error
192
- * @returns {string}
193
- */
194
- const messageOf = (error) => String(/** @type {any} */ (error)?.message ?? error ?? "");
195
-
196
- /**
197
- * A Blockscout time ("2026-10-27T05:52:13.000000Z") in milliseconds.
198
- *
199
- * @param {unknown} text
200
- */
201
- const timeOf = (text) => Date.parse(String(text ?? "").replace(/(\.\d{3})\d+/, "$1"));
202
-
203
- /**
204
- * A time in milliseconds, or null.
205
- *
206
- * @param {unknown} value
207
- */
208
- const msOf = (value) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null);
209
-
210
- /**
211
- * A height, or null.
212
- *
213
- * @param {unknown} value
214
- */
215
- const heightOf = (value) => (typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null);
216
-
217
131
  /**
218
132
  * How far a transfer's history is read, as a result gave it; null for
219
133
  * anything else.
@@ -293,286 +207,16 @@ function scanOf(value) {
293
207
  return out;
294
208
  }
295
209
 
296
- /**
297
- * The network a reference names; a TypeError for none there's a file for.
298
- *
299
- * @param {NetworkRef} network
300
- * @returns {Network}
301
- */
302
- function networkOf(network) {
303
- const net = network && typeof network === "object" ? network : networkFor(/** @type {number | bigint | string} */ (network));
304
-
305
- if (!net || typeof net.chainId !== "number" || !net.routes) throw new TypeError("Not one of the networks");
306
-
307
- return net;
308
- }
309
-
310
- /**
311
- * @template T
312
- * @param {Iterable<T>} items
313
- * @param {(item: T) => string} keyOf
314
- * @returns {Map<string, T[]>}
315
- */
316
- function groupBy(items, keyOf) {
317
- /** @type {Map<string, T[]>} */
318
- const out = new Map();
319
-
320
- for (const item of items) {
321
- const key = keyOf(item);
322
-
323
- out.set(key, [...(out.get(key) ?? []), item]);
324
- }
325
-
326
- return out;
327
- }
328
-
329
- /**
330
- * Oldest first, then by hash, so the order is the same everywhere.
331
- *
332
- * @param {{ at: number, hash: string, block?: bigint | null }} a
333
- * @param {{ at: number, hash: string, block?: bigint | null }} b
334
- */
335
- const byTime = (a, b) =>
336
- a.at - b.at || (a.block !== undefined && b.block !== undefined && a.block !== null && b.block !== null && a.block !== b.block ? (a.block < b.block ? -1 : 1) : 0) || (a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0);
337
-
338
210
  /**
339
211
  * @param {ReadonlyArray<bigint>} list
340
212
  * @returns {bigint}
341
213
  */
342
214
  const minOf = (list) => list.reduce((a, b) => (b < a ? b : a));
343
215
 
344
- // ---------------------------------------------------------------------------
345
- // Requests
346
- // ---------------------------------------------------------------------------
347
-
348
- /**
349
- * @param {string} url
350
- * @returns {string}
351
- */
352
- function hostOf(url) {
353
- try {
354
- return new URL(url).host;
355
- } catch {
356
- return url;
357
- }
358
- }
359
-
360
- /**
361
- * The app's fetch, each request within `timeoutMs`, counted.
362
- *
363
- * @param {FetchLike | undefined} fetchFn
364
- * @param {number} timeoutMs
365
- */
366
- function reader(fetchFn, timeoutMs) {
367
- const counter = { requests: 0 };
368
-
369
- /**
370
- * @param {string} url
371
- * @param {{ method: "GET" | "POST", headers: Record<string, string>, body?: string }} init
372
- * @returns {Promise<{ ok: boolean, status: number, body: any, host: string }>}
373
- */
374
- async function exchange(url, init) {
375
- const controller = new AbortController();
376
- const timer = setTimeout(() => controller.abort(), timeoutMs);
377
- const host = hostOf(url);
378
-
379
- counter.requests += 1;
380
-
381
- try {
382
- const send = fetchFn ?? globalThis.fetch;
383
- const response = await send(url, { ...init, signal: controller.signal });
384
- const text = await response.text();
385
- /** @type {any} */
386
- let body;
387
-
388
- try {
389
- body = JSON.parse(text);
390
- } catch {
391
- throw new Error(response.ok ? `${host} didn't answer with JSON` : `${host} answers with HTTP ${response.status}`);
392
- }
393
-
394
- return { body, host, ok: response.ok, status: response.status };
395
- } catch (error) {
396
- if (controller.signal.aborted) throw new Error(`${host} didn't answer within ${Math.round(timeoutMs / 100) / 10} s`);
397
-
398
- throw error;
399
- } finally {
400
- clearTimeout(timer);
401
- }
402
- }
403
-
404
- return {
405
- counter,
406
- // What a call reads once for all its searches, by what and where: a
407
- // chain's head and pace.
408
- /** @type {Map<string, Promise<any>>} */
409
- once: new Map(),
410
-
411
- /**
412
- * @param {string} url
413
- * @returns {Promise<any>}
414
- */
415
- async getJson(url) {
416
- const answer = await exchange(url, { headers: { accept: "application/json" }, method: "GET" });
417
-
418
- if (!answer.ok) throw new Error(`${answer.host} answers with HTTP ${answer.status}`);
419
-
420
- return answer.body;
421
- },
422
-
423
- /**
424
- * One JSON-RPC call; its error's message, with its code, when it refuses.
425
- *
426
- * @param {string} url
427
- * @param {string} method
428
- * @param {unknown[]} params
429
- * @returns {Promise<any>}
430
- */
431
- async rpc(url, method, params) {
432
- const answer = await exchange(url, {
433
- body: JSON.stringify({ id: 1, jsonrpc: "2.0", method, params }),
434
- headers: { "content-type": "application/json" },
435
- method: "POST",
436
- });
437
- const error = answer.body?.error;
438
-
439
- if (error) throw Object.assign(new Error(String(error.message ?? `${method} was refused`)), { code: error.code });
440
- if (!answer.ok) throw new Error(`${answer.host} answers ${method} with HTTP ${answer.status}`);
441
-
442
- return answer.body?.result;
443
- },
444
- };
445
- }
446
-
447
- /** @typedef {ReturnType<typeof reader>} Reader */
448
-
449
216
  // ---------------------------------------------------------------------------
450
217
  // Matching: each credit pays one transfer, in the order they were sent
451
218
  // ---------------------------------------------------------------------------
452
219
 
453
- /**
454
- * Transfers whose windows touch, each window from `windowMs` before its
455
- * time to `windowMs` after: a credit in one could be another's.
456
- *
457
- * @param {ReadonlyArray<Queued>} queue oldest first
458
- * @param {number} windowMs
459
- * @returns {Queued[][]}
460
- */
461
- function clustersOf(queue, windowMs) {
462
- /** @type {Queued[][]} */
463
- const out = [];
464
-
465
- for (const item of queue) {
466
- const last = out[out.length - 1];
467
-
468
- if (last && item.at - windowMs <= last[last.length - 1].at + windowMs) last.push(item);
469
- else out.push([item]);
470
- }
471
-
472
- return out;
473
- }
474
-
475
- /**
476
- * Whether every window of the queue is still open: nothing in it can be
477
- * told apart yet.
478
- *
479
- * @param {ReadonlyArray<Queued>} queue
480
- * @param {number} windowMs
481
- * @param {number} now
482
- */
483
- const allOpen = (queue, windowMs, now) => clustersOf(queue, windowMs).every((cluster) => now < cluster[cluster.length - 1].at + windowMs);
484
-
485
- /**
486
- * How a group's credits are matched: see `settle`.
487
- *
488
- * @typedef {{ guarded: boolean, now: number, slackMs: number, used: ReadonlySet<string>, windowMs: number, rivals?: ReadonlySet<string> }} Rules
489
- */
490
-
491
- /**
492
- * Whether a credit could pay a transfer, whether or not another one took it:
493
- * it went through, isn't one the app gave another of its transfers when
494
- * this one is the app's, comes from none of `rivals`, and is in the
495
- * transfer's time and at or after the block it can't be before.
496
- *
497
- * @param {Queued} item
498
- * @param {Credit} credit
499
- * @param {Rules} rules
500
- */
501
- function pays(item, credit, { guarded, rivals = new Set(), slackMs, used, windowMs }) {
502
- const lo = item.at - (guarded ? windowMs : slackMs);
503
- const hi = guarded ? item.at + windowMs : Infinity;
504
-
505
- return (
506
- credit.success &&
507
- (!item.given || !used.has(credit.hash)) &&
508
- (credit.from === null || !rivals.has(credit.from)) &&
509
- credit.at >= lo &&
510
- credit.at <= hi &&
511
- (item.notBefore === null || credit.block === null || credit.block >= item.notBefore)
512
- );
513
- }
514
-
515
- /**
516
- * Each transfer of a queue of equal ones, oldest first, takes the first
517
- * credit that went through after it that no earlier one took: from
518
- * `slackMs` before it on, and at or after the block it can't be before.
519
- * None comes from an address of `rivals`: the senders of other transfers
520
- * the app gave, whose credits these could be. A credit the app gave another
521
- * of its transfers (`used`) is none of the app's own waiting ones; a
522
- * transfer read from the chain may have it, as the one it paid comes back
523
- * among them.
524
- *
525
- * Where the queue may not hold every transfer that could have made the
526
- * credits (`guarded`), a credit is a transfer's only within `windowMs`
527
- * either side of it, and once the windows that touch its own are over,
528
- * only when every payment of its amount in them (those that failed too) is
529
- * one of those transfers' credits, or one the app gave another. Otherwise
530
- * it could be another transfer's: unsure. A transfer whose windows closed
531
- * without a credit is late: one coming later can't be told apart.
532
- *
533
- * @param {ReadonlyArray<Queued>} queue oldest first
534
- * @param {ReadonlyArray<Credit>} credits
535
- * @param {Rules} rules
536
- * @returns {{ assigned: Map<string, Credit>, verdicts: Map<string, Verdict> }}
537
- */
538
- function settle(queue, credits, rules) {
539
- const { guarded, now, used, windowMs } = rules;
540
- const sorted = [...credits].sort(byTime);
541
- /** @type {Map<string, Credit>} */
542
- const assigned = new Map();
543
- const taken = new Set();
544
- /** @type {Map<string, Verdict>} */
545
- const verdicts = new Map();
546
-
547
- for (const item of queue) {
548
- const credit = sorted.find((c) => !taken.has(c.hash) && pays(item, c, rules));
549
-
550
- if (credit) {
551
- assigned.set(item.hash, credit);
552
- taken.add(credit.hash);
553
- }
554
- }
555
-
556
- if (!guarded) {
557
- for (const item of queue) verdicts.set(item.hash, assigned.has(item.hash) ? "arrived" : "waiting");
558
-
559
- return { assigned, verdicts };
560
- }
561
-
562
- for (const cluster of clustersOf(queue, windowMs)) {
563
- const lo = cluster[0].at - windowMs;
564
- const hi = cluster[cluster.length - 1].at + windowMs;
565
- const own = new Set(cluster.flatMap((item) => (assigned.has(item.hash) ? [/** @type {Credit} */ (assigned.get(item.hash)).hash] : [])));
566
- const stray = sorted.some((c) => c.at >= lo && c.at <= hi && !used.has(c.hash) && !own.has(c.hash));
567
-
568
- for (const item of cluster) {
569
- verdicts.set(item.hash, now < hi ? "waiting" : stray ? "unsure" : assigned.has(item.hash) ? "arrived" : "late");
570
- }
571
- }
572
-
573
- return { assigned, verdicts };
574
- }
575
-
576
220
  /**
577
221
  * The queue of equal transfers: the app's, and those read from the chain
578
222
  * that were sent shortly before one of them, each within `horizonMs` of the
@@ -595,25 +239,6 @@ function queueOf(given, read, horizonMs) {
595
239
  return all.filter((_, i) => kept[i]);
596
240
  }
597
241
 
598
- /**
599
- * A queue of equal transfers where the credits can't be told apart, with
600
- * the ones the app gave but no longer looks for (waited for too long) whose
601
- * windows touch theirs: those were sent first, and take their credits
602
- * first.
603
- *
604
- * @param {ReadonlyArray<Queued>} given
605
- * @param {ReadonlyArray<Queued>} past
606
- * @param {number} windowMs
607
- * @returns {Queued[]}
608
- */
609
- function withPast(given, past, windowMs) {
610
- const polled = new Set(given.map((item) => item.hash));
611
-
612
- return clustersOf([...given, ...past].sort(byTime), windowMs)
613
- .filter((cluster) => cluster.some((item) => polled.has(item.hash)))
614
- .flat();
615
- }
616
-
617
242
  /**
618
243
  * The first transfer of the run of equal ones before `item`, each within
619
244
  * `horizonMs` of the next.
@@ -1371,10 +996,9 @@ async function transfersOf(archive, hashes, counter) {
1371
996
  *
1372
997
  * @param {Archive} archive
1373
998
  * @param {ReadonlyArray<string>} hashes
1374
- * @param {{ network?: Network, args?: boolean }} options
1375
999
  * @param {{ requests: number }} counter
1376
1000
  */
1377
- async function detailsOf(archive, hashes, { args = false, network }, counter) {
1001
+ async function detailsOf(archive, hashes, counter) {
1378
1002
  /** @type {Map<string, any>} */
1379
1003
  const out = new Map();
1380
1004
  const list = [...new Set(hashes)];
@@ -1384,12 +1008,161 @@ async function detailsOf(archive, hashes, { args = false, network }, counter) {
1384
1008
 
1385
1009
  counter.requests += 1;
1386
1010
 
1387
- for (const [hash, found] of await fetchTxDetails(archive, batch, { args: args ? new Set(batch) : undefined, network })) out.set(hash, found);
1011
+ for (const [hash, found] of await fetchTxDetails(archive, batch)) out.set(hash, found);
1012
+ }
1013
+
1014
+ return out;
1015
+ }
1016
+
1017
+ /**
1018
+ * A deposit's call, as `tx(hash) { blockHeight blockTimestamp err tx {
1019
+ * callData { contractId fnName data } } }` gives it: whether it went
1020
+ * through, its block's time, its function, and for a call of DuskEVM's
1021
+ * bridge or Portal on Dusk, what it did (`bridge`, as `/history` reads it:
1022
+ * null for one that isn't a deposit, a proof or a claim). Null for none.
1023
+ *
1024
+ * @param {Network} net
1025
+ * @param {any} raw
1026
+ * @returns {{ bridge?: { kind: string, amount?: string, to?: string } | null, fnName: string | undefined, success: boolean, timestamp: number | null } | null}
1027
+ */
1028
+ function callOf(net, raw) {
1029
+ if (!raw || typeof raw !== "object") return null;
1030
+
1031
+ const call = raw.tx?.callData;
1032
+ const contractId = call?.contractId ? String(call.contractId) : undefined;
1033
+ const fnName = call?.fnName ? String(call.fnName) : undefined;
1034
+ const seconds = Number(raw.blockTimestamp);
1035
+ /** @type {NonNullable<ReturnType<typeof callOf>>} */
1036
+ const out = { fnName, success: !failedTx(raw), timestamp: Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : null };
1037
+
1038
+ if (isBridgeContract(net, contractId)) {
1039
+ const done = decodeBridgeCall(net, { args: typeof call?.data === "string" ? call.data : null, contractId, fnName });
1040
+
1041
+ out.bridge = done === null ? null : done.kind === "deposit" ? { amount: String(done.amountLux), kind: "deposit", to: done.recipient } : { kind: done.kind };
1042
+ }
1043
+
1044
+ return out;
1045
+ }
1046
+
1047
+ /**
1048
+ * Whether a transaction, as the archive gives it, failed.
1049
+ *
1050
+ * @param {any} raw
1051
+ */
1052
+ const failedTx = (raw) => raw?.err !== null && raw?.err !== undefined && raw?.err !== "";
1053
+
1054
+ /**
1055
+ * A transaction's block height, as the archive gives it; null for none.
1056
+ *
1057
+ * @param {any} raw
1058
+ */
1059
+ const heightIn = (raw) => (/^\d+$/.test(String(raw?.blockHeight)) ? heightOf(Number(raw.blockHeight)) : null);
1060
+
1061
+ /**
1062
+ * Whether a deposit's call, as `callOf` reads it, says all a match reads of
1063
+ * it: its time, and that it failed, that it's another call of DuskEVM's
1064
+ * bridge or Portal than a deposit, or a deposit's amount and DuskEVM
1065
+ * address. One that doesn't, as one whose arguments the archive gave cut
1066
+ * short, is asked for again: the archive may give it whole later.
1067
+ *
1068
+ * @param {ReturnType<typeof callOf>} call
1069
+ */
1070
+ function decided(call) {
1071
+ if (!call || call.timestamp === null) return false;
1072
+ if (!call.success) return true;
1073
+ // Not a call of the bridge's contracts, as the network names them.
1074
+ if (call.bridge === undefined) return false;
1075
+ if (call.bridge === null) return call.fnName !== undefined && call.fnName !== BRIDGE_FUNCTIONS.deposit;
1076
+
1077
+ return call.bridge.kind !== "deposit" || (luxOf(call.bridge.amount) !== null && addressOf(call.bridge.to) !== "");
1078
+ }
1079
+
1080
+ /**
1081
+ * The calls of deposits a sender's history lists, by hash, as `callOf` reads
1082
+ * them: a batch a request, with the last finalized block. One that can't
1083
+ * change any more, in a block at or under that one, and that says all a
1084
+ * match reads of it (`decided`), is kept in the app's `cache`, by the
1085
+ * network, its bridge and the transaction's hash, and isn't asked for
1086
+ * again; others are asked for each time.
1087
+ *
1088
+ * @param {Archive} archive
1089
+ * @param {Network} net
1090
+ * @param {ReadonlyArray<string>} hashes
1091
+ * @param {FactCache | undefined} cache
1092
+ * @param {{ requests: number }} counter
1093
+ * @returns {Promise<Map<string, NonNullable<ReturnType<typeof callOf>>>>}
1094
+ */
1095
+ async function depositCalls(archive, net, hashes, cache, counter) {
1096
+ /** @type {Map<string, NonNullable<ReturnType<typeof callOf>>>} */
1097
+ const out = new Map();
1098
+ /** @param {string} hash */
1099
+ const keyOf = (hash) => `deposit:${net.chainId}:${duskEvmContracts(net)?.bridge ?? ""}:${hash}`;
1100
+ /** @type {string[]} */
1101
+ const missing = [];
1102
+
1103
+ for (const hash of new Set(hashes)) {
1104
+ const kept = cache ? keptCall(net, cache, keyOf(hash), hash) : null;
1105
+
1106
+ if (kept) out.set(hash, kept);
1107
+ else missing.push(hash);
1108
+ }
1109
+
1110
+ for (let i = 0; i < missing.length; i += DETAILS_BATCH) {
1111
+ const batch = missing.slice(i, i + DETAILS_BATCH);
1112
+
1113
+ counter.requests += 1;
1114
+
1115
+ const data = await archive.query(`query { pair: lastBlockPair { json } ${batch.map((hash, j) => `t${j}: tx(hash: "${hash}") { blockHeight blockTimestamp err tx { callData { contractId fnName data } } }`).join(" ")} }`);
1116
+ const finalized = Number(data?.pair?.json?.last_finalized_block?.[0]);
1117
+
1118
+ batch.forEach((hash, j) => {
1119
+ const raw = data?.[`t${j}`];
1120
+ const call = callOf(net, raw);
1121
+
1122
+ if (!call) return;
1123
+
1124
+ out.set(hash, call);
1125
+
1126
+ const height = heightIn(raw);
1127
+
1128
+ if (cache && height !== null && Number.isSafeInteger(finalized) && height <= finalized && decided(call)) {
1129
+ try {
1130
+ cache.set(keyOf(hash), JSON.stringify({ hash, tx: raw }));
1131
+ } catch {
1132
+ // Not kept: asked for again next time.
1133
+ }
1134
+ }
1135
+ });
1388
1136
  }
1389
1137
 
1390
1138
  return out;
1391
1139
  }
1392
1140
 
1141
+ /**
1142
+ * A deposit's call kept in the app's cache, as `callOf` reads it; null where
1143
+ * none is kept, or what is kept isn't this transaction's call, in a block,
1144
+ * saying all a match reads of it.
1145
+ *
1146
+ * @param {Network} net
1147
+ * @param {FactCache} cache
1148
+ * @param {string} key
1149
+ * @param {string} hash
1150
+ */
1151
+ function keptCall(net, cache, key, hash) {
1152
+ try {
1153
+ const text = cache.get(key);
1154
+
1155
+ if (typeof text !== "string" || !text) return null;
1156
+
1157
+ const kept = JSON.parse(text);
1158
+ const call = kept?.hash === hash && heightIn(kept.tx) !== null ? callOf(net, kept.tx) : null;
1159
+
1160
+ return decided(call) ? call : null;
1161
+ } catch {
1162
+ return null;
1163
+ }
1164
+ }
1165
+
1393
1166
  /**
1394
1167
  * A transfer whose history is read: its hash, its Dusk block's time and
1395
1168
  * height, the block on the other chain its credit can't be before, and its
@@ -1517,17 +1290,18 @@ async function readHistories(archive, items, { account, direction, horizonMs, ke
1517
1290
  * (`to`, `amount`), from their calls: each with the block the app gave it
1518
1291
  * its credit can't be before; and those whose call or time the archive
1519
1292
  * doesn't give, which could be any deposit of up to what they paid, but
1520
- * for one the app gave: its own facts tell. `calls` keeps each call read.
1293
+ * for one the app gave: its own facts tell. `calls` keeps each call read,
1294
+ * and the app's `cache` those that can't change any more.
1521
1295
  *
1522
1296
  * @param {Archive} archive
1523
1297
  * @param {Network} net
1524
1298
  * @param {ReadonlyArray<{ hash: string, height: number }>} found
1525
- * @param {{ amount: bigint, to: string, bounds: ReadonlyMap<string, bigint | null>, calls: Map<string, any>, supplied: ReadonlyMap<string, DepositItem> }} group
1299
+ * @param {{ amount: bigint, to: string, bounds: ReadonlyMap<string, bigint | null>, cache: FactCache | undefined, calls: Map<string, any>, supplied: ReadonlyMap<string, DepositItem> }} group
1526
1300
  * @param {{ requests: number }} counter
1527
1301
  */
1528
- async function depositPeers(archive, net, found, { amount, bounds, calls, supplied, to }, counter) {
1302
+ async function depositPeers(archive, net, found, { amount, bounds, cache, calls, supplied, to }, counter) {
1529
1303
  const missing = found.flatMap((entry) => (calls.has(entry.hash) ? [] : [entry.hash]));
1530
- const read = await detailsOf(archive, missing, { args: true, network: net }, counter);
1304
+ const read = await depositCalls(archive, net, missing, cache, counter);
1531
1305
 
1532
1306
  for (const hash of missing) calls.set(hash, read.get(hash) ?? null);
1533
1307
 
@@ -1618,39 +1392,6 @@ function withHistory(hash, arrival, given, states) {
1618
1392
  // Inputs
1619
1393
  // ---------------------------------------------------------------------------
1620
1394
 
1621
- /**
1622
- * A deposit as given, checked.
1623
- *
1624
- * @typedef {{ amount: bigint, at: number | null, hash: string, height: number | null, notBefore: bigint | null, scan: Scan | null, sender: string | null, to: string }} DepositItem
1625
- */
1626
-
1627
- /**
1628
- * @param {Deposit} deposit
1629
- * @returns {DepositItem}
1630
- */
1631
- function depositIn(deposit) {
1632
- const hash = duskHashOf(deposit?.hash);
1633
- const amount = luxOf(deposit?.amount);
1634
- const to = addressOf(deposit?.to);
1635
- const sender = deposit?.sender ?? null;
1636
-
1637
- if (!hash) throw new TypeError("A deposit's hash is its Dusk transaction's: 64 hex digits");
1638
- if (amount === null || amount === 0n) throw new TypeError("A deposit's amount is lux: more than 0, at most a u64");
1639
- if (!to) throw new TypeError("A deposit pays a DuskEVM address: 0x and 40 hex digits");
1640
- if (sender !== null && !isAccount(sender)) throw new TypeError("A deposit's sender is a Dusk account");
1641
-
1642
- return {
1643
- amount,
1644
- at: msOf(deposit.at),
1645
- hash,
1646
- height: heightOf(deposit.height),
1647
- notBefore: quantityOf(deposit.notBefore),
1648
- scan: scanOf(deposit.scan),
1649
- sender: /** @type {string | null} */ (sender),
1650
- to,
1651
- };
1652
- }
1653
-
1654
1395
  /**
1655
1396
  * A transfer out as given, checked: its route, the fee the app gives where
1656
1397
  * it gives one, and where an earlier search stopped.
@@ -1690,63 +1431,6 @@ function transferIn(transfer, services) {
1690
1431
  */
1691
1432
  const horizonOf = (route) => Math.max(SERVICE_HORIZON_MS, 4 * (route.typicalSeconds ?? 0) * 1000);
1692
1433
 
1693
- /**
1694
- * @param {{ hash: string, at: number | null, notBefore: bigint | null, scan: Scan | null }} item
1695
- * @returns {Queued}
1696
- */
1697
- const queued = (item) => ({ at: /** @type {number} */ (item.at), given: true, hash: item.hash, notBefore: item.notBefore, scan: item.scan });
1698
-
1699
- /**
1700
- * Sorts out the transfers that aren't looked for: without their block's
1701
- * time yet, waiting; sent longer ago than they're waited for, unknown.
1702
- *
1703
- * @template {{ hash: string, at: number | null }} T
1704
- * @param {ReadonlyArray<T>} list
1705
- * @param {Map<string, Arrival>} results
1706
- * @param {{ now: number, waitMs: number }} options
1707
- * @returns {T[]}
1708
- */
1709
- function triage(list, results, { now, waitMs }) {
1710
- /** @type {T[]} */
1711
- const live = [];
1712
-
1713
- for (const item of list) {
1714
- if (item.at === null) results.set(item.hash, { state: "waiting" });
1715
- else if (now - item.at > waitMs) results.set(item.hash, { reason: "expired", state: "unknown" });
1716
- else live.push(item);
1717
- }
1718
-
1719
- return live;
1720
- }
1721
-
1722
- /**
1723
- * Each item once, by hash: the first.
1724
- *
1725
- * @template {{ hash: string }} T
1726
- * @param {ReadonlyArray<T>} list
1727
- * @returns {T[]}
1728
- */
1729
- function uniqueByHash(list) {
1730
- const seen = new Set();
1731
-
1732
- return list.filter((item) => !seen.has(item.hash) && Boolean(seen.add(item.hash)));
1733
- }
1734
-
1735
- /**
1736
- * The results in the order the transfers came.
1737
- *
1738
- * @param {ReadonlyArray<{ hash: string }>} list
1739
- * @param {Map<string, Arrival>} results
1740
- * @returns {Map<string, Arrival>}
1741
- */
1742
- const inOrder = (list, results) => new Map(list.map((item) => [item.hash, /** @type {Arrival} */ (results.get(item.hash))]));
1743
-
1744
- /**
1745
- * @param {Iterable<string> | undefined} taken
1746
- * @param {(value: unknown) => string} hashOf
1747
- */
1748
- const usedOf = (taken, hashOf) => new Set([...(taken ?? [])].map(hashOf).filter(Boolean));
1749
-
1750
1434
  // ---------------------------------------------------------------------------
1751
1435
  // Deposits to DuskEVM
1752
1436
  // ---------------------------------------------------------------------------
@@ -1755,183 +1439,52 @@ const usedOf = (taken, hashOf) => new Set([...(taken ?? [])].map(hashOf).filter(
1755
1439
  * @param {DepositArrivalOptions} options
1756
1440
  * @returns {Promise<{ arrivals: Map<string, Arrival>, requests: number }>}
1757
1441
  */
1758
- export async function depositArrivals({
1759
- network,
1760
- deposits,
1761
- via = "explorer",
1762
- fetch: fetchFn,
1763
- explorerUrl,
1764
- rpcUrl,
1765
- archive,
1766
- taken,
1767
- blockTimes,
1768
- now = Date.now(),
1769
- waitMs = WAIT_MS,
1770
- windowMs = CREDIT_WINDOW_MS,
1771
- maxPages = EXPLORER_PAGES,
1772
- maxWindows = LOG_WINDOWS,
1773
- timeoutMs = TIMEOUT_MS,
1774
- }) {
1442
+ export async function depositArrivals(options) {
1443
+ const {
1444
+ network,
1445
+ deposits,
1446
+ via = "explorer",
1447
+ fetch: fetchFn,
1448
+ rpcUrl,
1449
+ archive,
1450
+ taken,
1451
+ blockTimes,
1452
+ cache,
1453
+ now = Date.now(),
1454
+ waitMs = WAIT_MS,
1455
+ windowMs = CREDIT_WINDOW_MS,
1456
+ maxWindows = LOG_WINDOWS,
1457
+ timeoutMs = TIMEOUT_MS,
1458
+ } = options;
1459
+
1775
1460
  if (via !== "explorer" && via !== "rpc") throw new RangeError('A deposit is followed on the "explorer" or the "rpc"');
1461
+ if (via === "explorer") return depositArrivalsFromExplorer(options);
1776
1462
 
1777
- const net = networkOf(network);
1778
- const list = uniqueByHash(Array.from(deposits ?? [], depositIn));
1463
+ const { evm, list, live, net, past, results, used } = depositsIn({ deposits, network, now, taken, waitMs }, scanOf);
1779
1464
  const read = reader(fetchFn, timeoutMs);
1780
- /** @type {Map<string, Arrival>} */
1781
- const results = new Map();
1782
- const evm = net.duskEvm;
1783
1465
 
1784
- if (!evm) {
1785
- for (const item of list) results.set(item.hash, { reason: "no-route", state: "unknown" });
1466
+ if (evm) {
1467
+ const url = urlOf(rpcUrl, evm.rpcUrl, "DuskEVM RPC");
1786
1468
 
1787
- return { arrivals: inOrder(list, results), requests: 0 };
1788
- }
1789
-
1790
- const used = usedOf(taken, evmHashOf);
1791
- const live = triage(list, results, { now, waitMs });
1792
- // Waited for too long: not looked for, though still before the newer ones.
1793
- const past = list.filter((item) => item.at !== null && now - item.at > waitMs);
1794
- const bridge = evm.predeploys.l2StandardBridge.toLowerCase();
1795
- const rules = { now, past, results, used, windowMs };
1796
-
1797
- if (via === "explorer") {
1798
- await onExplorer(read, live, { ...rules, api: String(explorerUrl ?? evm.blockscoutApiUrl), bridge, maxPages });
1799
- } else {
1800
- await onRpc(read, live, { ...rules, archive, blockTimes, bridge, list, maxWindows, net, url: String(rpcUrl ?? evm.rpcUrl) });
1469
+ await onRpc(read, live, { archive, blockTimes, bridge: evm.predeploys.l2StandardBridge.toLowerCase(), cache, list, maxWindows, net, now, past, results, url, used, windowMs });
1801
1470
  }
1802
1471
 
1803
1472
  return { arrivals: inOrder(list, results), requests: read.counter.requests };
1804
1473
  }
1805
1474
 
1806
- /**
1807
- * The L2 bridge's payments to an address, as DuskEVM's explorer lists its
1808
- * incoming calls: newest first, a page at a time (the explorer's
1809
- * `next_page_params`), back past `since`, at most `maxPages`. `complete`
1810
- * once they reach it, or there are no more.
1811
- *
1812
- * @param {Reader} read
1813
- * @param {{ api: string, bridge: string, maxPages: number, since: number, to: string }} ask
1814
- * @returns {Promise<{ complete: boolean, payments: Credit[] }>}
1815
- */
1816
- async function explorerPayments(read, { api, bridge, maxPages, since, to }) {
1817
- const base = `${api.replace(/\/+$/, "")}/addresses/${to}/internal-transactions`;
1818
- /** @type {any[]} */
1819
- const items = [];
1820
- /** @type {Record<string, unknown> | null} */
1821
- let next = null;
1822
- let complete = false;
1823
-
1824
- for (let page = 0; page < Math.max(1, Math.floor(maxPages)) && !complete; page += 1) {
1825
- const query = new URLSearchParams({ filter: "to" });
1826
-
1827
- for (const [key, value] of Object.entries(next ?? {})) query.set(key, String(value));
1828
-
1829
- const body = await read.getJson(`${base}?${query}`);
1830
- const calls = Array.isArray(body?.items) ? body.items : [];
1831
- const oldest = Math.min(...calls.map((/** @type {any} */ call) => timeOf(call?.timestamp)).filter(Number.isFinite));
1832
-
1833
- items.push(...calls);
1834
- next = body?.next_page_params && typeof body.next_page_params === "object" ? body.next_page_params : null;
1835
- complete = !next || !calls.length || oldest < since;
1836
- }
1837
-
1838
- /** @type {Map<string, Credit>} */
1839
- const payments = new Map();
1840
-
1841
- for (const call of items) {
1842
- const hash = evmHashOf(call?.transaction_hash);
1843
- const at = timeOf(call?.timestamp);
1844
- const value = String(call?.value ?? "");
1845
- const payee = call?.to?.hash;
1846
-
1847
- if (!hash || payments.has(hash) || !Number.isFinite(at) || !/^\d+$/.test(value)) continue;
1848
- if (String(call?.from?.hash ?? "").toLowerCase() !== bridge) continue;
1849
- if (payee !== undefined && payee !== null && String(payee).toLowerCase() !== to) continue;
1850
-
1851
- payments.set(hash, { at, block: quantityOf(call?.block_number), from: null, hash, success: call?.success !== false, wei: BigInt(value) });
1852
- }
1853
-
1854
- return { complete, payments: [...payments.values()] };
1855
- }
1856
-
1857
- /**
1858
- * Deposits followed on DuskEVM's explorer: each recipient's incoming calls
1859
- * read once for all its deposits, and matched as where the credits can't be
1860
- * told apart, as they don't say who deposited. Equal deposits past their
1861
- * wait just before them take their payments first, their results kept.
1862
- *
1863
- * @param {Reader} read
1864
- * @param {DepositItem[]} live
1865
- * @param {{ api: string, bridge: string, maxPages: number, now: number, past: DepositItem[], results: Map<string, Arrival>, used: Set<string>, windowMs: number }} options
1866
- */
1867
- async function onExplorer(read, live, { api, bridge, maxPages, now, past, results, used, windowMs }) {
1868
- for (const [to, items] of groupBy(live, (item) => item.to)) {
1869
- const polled = new Set(items.map((item) => item.hash));
1870
- const groups = [...groupBy(items, (item) => item.amount.toString()).values()].map((group) => ({
1871
- queue: withPast(
1872
- group.map(queued),
1873
- past.filter((item) => item.to === to && item.amount === group[0].amount).map(queued),
1874
- windowMs
1875
- ),
1876
- wei: group[0].amount * WEI_PER_LUX,
1877
- }));
1878
-
1879
- // Nothing can be told apart before a window is over: nothing is asked.
1880
- if (groups.every(({ queue }) => allOpen(queue, windowMs, now))) {
1881
- for (const item of items) results.set(item.hash, { state: "waiting" });
1882
- continue;
1883
- }
1884
-
1885
- /** @type {{ complete: boolean, payments: Credit[] }} */
1886
- let page;
1887
-
1888
- try {
1889
- page = await explorerPayments(read, { api, bridge, maxPages, since: Math.min(...groups.flatMap(({ queue }) => queue.map((item) => item.at))) - windowMs, to });
1890
- } catch (error) {
1891
- for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
1892
- continue;
1893
- }
1894
-
1895
- // Not back to the oldest: a payment of an older one may be a newer one's.
1896
- if (!page.complete) {
1897
- for (const item of items) results.set(item.hash, { reason: "beyond", state: "unknown" });
1898
- continue;
1899
- }
1900
-
1901
- for (const { queue, wei } of groups) {
1902
- const { assigned, verdicts } = settle(
1903
- queue,
1904
- page.payments.filter((payment) => payment.wei === wei),
1905
- { guarded: true, now, slackMs: windowMs, used, windowMs }
1906
- );
1907
-
1908
- for (const item of queue.filter((each) => polled.has(each.hash))) {
1909
- const credit = assigned.get(item.hash);
1910
- const verdict = verdicts.get(item.hash);
1911
-
1912
- if (credit && verdict === "arrived") {
1913
- used.add(credit.hash);
1914
- results.set(item.hash, { amount: credit.wei / WEI_PER_LUX, at: credit.at, block: credit.block === null ? null : Number(credit.block), hash: credit.hash, state: "arrived" });
1915
- } else {
1916
- results.set(item.hash, verdict === "waiting" ? { state: "waiting" } : { reason: verdict === "unsure" ? "unsure" : "late", state: "unknown" });
1917
- }
1918
- }
1919
- }
1920
- }
1921
- }
1922
-
1923
1475
  /**
1924
1476
  * Deposits followed on DuskEVM's RPC, by the bridge's events from each
1925
1477
  * sender's DuskEVM address: a group of equal deposits from one sender at a
1926
1478
  * time, with the sender's equal deposits shortly before, from its history,
1927
- * when there's an archive; without it, with the equal ones given that are
1928
- * past their wait just before them.
1479
+ * when there's an archive and the network names DuskEVM's bridge on Dusk;
1480
+ * without them, or while the archive can't be read, with the equal ones
1481
+ * given that are past their wait just before them.
1929
1482
  *
1930
1483
  * @param {Reader} read
1931
1484
  * @param {DepositItem[]} live
1932
- * @param {{ archive: Archive | undefined, blockTimes: BlockTimes | undefined, bridge: string, list: DepositItem[], maxWindows: number, net: Network, now: number, past: DepositItem[], results: Map<string, Arrival>, url: string, used: Set<string>, windowMs: number }} options
1485
+ * @param {{ archive: Archive | undefined, blockTimes: BlockTimes | undefined, bridge: string, cache: FactCache | undefined, list: DepositItem[], maxWindows: number, net: Network, now: number, past: DepositItem[], results: Map<string, Arrival>, url: string, used: Set<string>, windowMs: number }} options
1933
1486
  */
1934
- async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows, net, now, past, results, url, used, windowMs }) {
1487
+ async function onRpc(read, live, { archive, blockTimes, bridge, cache, list, maxWindows, net, now, past, results, url, used, windowMs }) {
1935
1488
  // Equal deposits (the same DuskEVM address and amount) from a known sender
1936
1489
  // first, a sender at a time, its events telling its credits apart; then
1937
1490
  // those whose sender isn't known, which take none from the senders of the
@@ -1968,58 +1521,66 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
1968
1521
  const supplied = new Map(list.map((item) => [item.hash, item]));
1969
1522
  /** @type {Map<string, any>} */
1970
1523
  const calls = new Map();
1971
- const contract = duskEvmContracts(net)?.bridge;
1524
+ // DuskEVM's bridge on Dusk, which a sender's history lists its deposits to.
1525
+ const contract = duskEvmContracts(net)?.bridge ?? null;
1972
1526
 
1973
1527
  for (const { items, rivals } of [...known, ...unknown]) {
1974
1528
  const { amount, sender, to } = items[0];
1975
- const vouched = Boolean(archive) && sender !== null && items.every((item) => item.height !== null);
1976
1529
  let given = items.map(queued).sort(byTime);
1977
1530
  /** @type {Queued[]} */
1978
- let queue;
1531
+ let queue = [];
1979
1532
  /** @type {Set<string>} */
1980
1533
  const unsure = new Set();
1981
1534
  /** @type {Map<string, HistoryScan>} */
1982
1535
  let states = new Map();
1536
+ // Whether the sender's history tells its equal deposits' order.
1537
+ let vouched = false;
1983
1538
 
1984
- if (vouched) {
1539
+ if (archive && contract && sender !== null && items.every((item) => item.height !== null)) {
1985
1540
  // Each deposit's own hours of its sender's history, with its sender's
1986
1541
  // equal deposits there, the app's no longer looked for among them.
1987
- /** @type {Awaited<ReturnType<typeof readHistories>>} */
1988
- let history;
1542
+ /** @type {Awaited<ReturnType<typeof readHistories>> | null} */
1543
+ let history = null;
1989
1544
 
1990
1545
  try {
1991
1546
  history = await readHistories(
1992
- /** @type {Archive} */ (archive),
1547
+ archive,
1993
1548
  /** @type {Reading[]} */ ([...items].sort((a, b) => /** @type {number} */ (a.height) - /** @type {number} */ (b.height))),
1994
1549
  {
1995
- account: /** @type {string} */ (sender),
1550
+ account: sender,
1996
1551
  direction: "out",
1997
1552
  horizonMs: DEPOSIT_HORIZON_MS,
1998
1553
  keep: (entry) => entry.counterparty === contract && entry.value >= amount,
1999
1554
  of: (item) => `${sender}:${item.height}:${amount}`,
2000
1555
  pages: HISTORY_PAGES,
2001
- peersOf: (found) => depositPeers(/** @type {Archive} */ (archive), net, found, { amount, bounds, calls, supplied, to }, read.counter),
1556
+ peersOf: (found) => depositPeers(archive, net, found, { amount, bounds, cache, calls, supplied, to }, read.counter),
2002
1557
  reach: Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS),
2003
1558
  },
2004
1559
  read.counter
2005
1560
  );
2006
- } catch (error) {
2007
- for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
2008
- continue;
1561
+ } catch {
1562
+ // The archive can't be read now: followed as without it, its history
1563
+ // read on by a later call from where it stopped.
1564
+ states = new Map(items.flatMap((item) => (item.scan?.history ? [[item.hash, item.scan.history]] : [])));
2009
1565
  }
2010
1566
 
2011
- states = history.states;
2012
- waitForHistory(items, history, results);
1567
+ if (history) {
1568
+ vouched = true;
1569
+ states = history.states;
1570
+ waitForHistory(items, history, results);
2013
1571
 
2014
- if (!history.covered.length) continue;
1572
+ if (!history.covered.length) continue;
2015
1573
 
2016
- given = history.given;
2017
- queue = history.queue;
1574
+ given = history.given;
1575
+ queue = history.queue;
2018
1576
 
2019
- // One before it whose call can't be read could be equal: the credits'
2020
- // order can't be told.
2021
- for (const item of given) if (/** @type {number[]} */ (history.unread.get(item.hash)).length) unsure.add(item.hash);
2022
- } else {
1577
+ // One before it whose call can't be read could be equal: the credits'
1578
+ // order can't be told.
1579
+ for (const item of given) if (/** @type {number[]} */ (history.unread.get(item.hash)).length) unsure.add(item.hash);
1580
+ }
1581
+ }
1582
+
1583
+ if (!vouched) {
2023
1584
  queue = withPast(
2024
1585
  given,
2025
1586
  past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
@@ -2368,7 +1929,7 @@ export async function migrationPayouts({ network, migrations, entries, archive,
2368
1929
  const listed = await byMemo(archive, asked.map((item) => item.hash), counter);
2369
1930
  const found = [...listed.values()].flat().flatMap((raw) => payoutsIn(raw, /** @type {string} */ (account), net));
2370
1931
  // Their times, and whether they went through.
2371
- const details = await detailsOf(archive, found.map((payout) => payout.payout), {}, counter);
1932
+ const details = await detailsOf(archive, found.map((payout) => payout.payout), counter);
2372
1933
 
2373
1934
  candidates = found.flatMap((payout) => {
2374
1935
  const known = details.get(payout.payout);
@@ -2505,4 +2066,4 @@ export function arrivalsOf(arrivals) {
2505
2066
  for (const [hash, arrival] of arrivals) if (arrival.state === "arrived") out.set(hash, { at: arrival.at, hash: arrival.hash });
2506
2067
 
2507
2068
  return out;
2508
- }
2069
+ }