@dusk-network/app-core 0.4.0 → 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.
@@ -1278,19 +903,24 @@ function exactJson(text) {
1278
903
  return JSON.parse(text.replace(TOKEN, (match, string) => string ?? (/^-?\d+$/.test(match) && !Number.isSafeInteger(Number(match)) ? `"${match}"` : match)));
1279
904
  }
1280
905
 
1281
- const utf8 = new TextDecoder();
906
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
1282
907
 
1283
908
  /**
1284
- * A memo's bytes, given as hex, as text, read as the route's bridge reads
1285
- * them: as UTF-8, bytes that aren't UTF-8 becoming the replacement
1286
- * character; "" for none.
909
+ * A memo's bytes, given as hex, as UTF-8 text; "" for none, and for bytes
910
+ * that aren't UTF-8: an address or a hash is read from text only.
1287
911
  *
1288
912
  * @param {unknown} hex
1289
913
  */
1290
914
  function memoOf(hex) {
1291
915
  const text = String(hex ?? "").replace(/^0x/i, "");
1292
916
 
1293
- return /^(?:[0-9a-fA-F]{2})+$/.test(text) ? utf8.decode(Uint8Array.from(text.match(/../g) ?? [], (byte) => parseInt(byte, 16))) : "";
917
+ if (!/^(?:[0-9a-fA-F]{2})+$/.test(text)) return "";
918
+
919
+ try {
920
+ return utf8.decode(Uint8Array.from(text.match(/../g) ?? [], (byte) => parseInt(byte, 16)));
921
+ } catch {
922
+ return "";
923
+ }
1294
924
  }
1295
925
 
1296
926
  /**
@@ -1366,10 +996,9 @@ async function transfersOf(archive, hashes, counter) {
1366
996
  *
1367
997
  * @param {Archive} archive
1368
998
  * @param {ReadonlyArray<string>} hashes
1369
- * @param {{ network?: Network, args?: boolean }} options
1370
999
  * @param {{ requests: number }} counter
1371
1000
  */
1372
- async function detailsOf(archive, hashes, { args = false, network }, counter) {
1001
+ async function detailsOf(archive, hashes, counter) {
1373
1002
  /** @type {Map<string, any>} */
1374
1003
  const out = new Map();
1375
1004
  const list = [...new Set(hashes)];
@@ -1379,12 +1008,161 @@ async function detailsOf(archive, hashes, { args = false, network }, counter) {
1379
1008
 
1380
1009
  counter.requests += 1;
1381
1010
 
1382
- 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 };
1383
1042
  }
1384
1043
 
1385
1044
  return out;
1386
1045
  }
1387
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
+ });
1136
+ }
1137
+
1138
+ return out;
1139
+ }
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
+
1388
1166
  /**
1389
1167
  * A transfer whose history is read: its hash, its Dusk block's time and
1390
1168
  * height, the block on the other chain its credit can't be before, and its
@@ -1512,17 +1290,18 @@ async function readHistories(archive, items, { account, direction, horizonMs, ke
1512
1290
  * (`to`, `amount`), from their calls: each with the block the app gave it
1513
1291
  * its credit can't be before; and those whose call or time the archive
1514
1292
  * doesn't give, which could be any deposit of up to what they paid, but
1515
- * 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.
1516
1295
  *
1517
1296
  * @param {Archive} archive
1518
1297
  * @param {Network} net
1519
1298
  * @param {ReadonlyArray<{ hash: string, height: number }>} found
1520
- * @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
1521
1300
  * @param {{ requests: number }} counter
1522
1301
  */
1523
- 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) {
1524
1303
  const missing = found.flatMap((entry) => (calls.has(entry.hash) ? [] : [entry.hash]));
1525
- const read = await detailsOf(archive, missing, { args: true, network: net }, counter);
1304
+ const read = await depositCalls(archive, net, missing, cache, counter);
1526
1305
 
1527
1306
  for (const hash of missing) calls.set(hash, read.get(hash) ?? null);
1528
1307
 
@@ -1613,39 +1392,6 @@ function withHistory(hash, arrival, given, states) {
1613
1392
  // Inputs
1614
1393
  // ---------------------------------------------------------------------------
1615
1394
 
1616
- /**
1617
- * A deposit as given, checked.
1618
- *
1619
- * @typedef {{ amount: bigint, at: number | null, hash: string, height: number | null, notBefore: bigint | null, scan: Scan | null, sender: string | null, to: string }} DepositItem
1620
- */
1621
-
1622
- /**
1623
- * @param {Deposit} deposit
1624
- * @returns {DepositItem}
1625
- */
1626
- function depositIn(deposit) {
1627
- const hash = duskHashOf(deposit?.hash);
1628
- const amount = luxOf(deposit?.amount);
1629
- const to = addressOf(deposit?.to);
1630
- const sender = deposit?.sender ?? null;
1631
-
1632
- if (!hash) throw new TypeError("A deposit's hash is its Dusk transaction's: 64 hex digits");
1633
- if (amount === null || amount === 0n) throw new TypeError("A deposit's amount is lux: more than 0, at most a u64");
1634
- if (!to) throw new TypeError("A deposit pays a DuskEVM address: 0x and 40 hex digits");
1635
- if (sender !== null && !isAccount(sender)) throw new TypeError("A deposit's sender is a Dusk account");
1636
-
1637
- return {
1638
- amount,
1639
- at: msOf(deposit.at),
1640
- hash,
1641
- height: heightOf(deposit.height),
1642
- notBefore: quantityOf(deposit.notBefore),
1643
- scan: scanOf(deposit.scan),
1644
- sender: /** @type {string | null} */ (sender),
1645
- to,
1646
- };
1647
- }
1648
-
1649
1395
  /**
1650
1396
  * A transfer out as given, checked: its route, the fee the app gives where
1651
1397
  * it gives one, and where an earlier search stopped.
@@ -1685,63 +1431,6 @@ function transferIn(transfer, services) {
1685
1431
  */
1686
1432
  const horizonOf = (route) => Math.max(SERVICE_HORIZON_MS, 4 * (route.typicalSeconds ?? 0) * 1000);
1687
1433
 
1688
- /**
1689
- * @param {{ hash: string, at: number | null, notBefore: bigint | null, scan: Scan | null }} item
1690
- * @returns {Queued}
1691
- */
1692
- const queued = (item) => ({ at: /** @type {number} */ (item.at), given: true, hash: item.hash, notBefore: item.notBefore, scan: item.scan });
1693
-
1694
- /**
1695
- * Sorts out the transfers that aren't looked for: without their block's
1696
- * time yet, waiting; sent longer ago than they're waited for, unknown.
1697
- *
1698
- * @template {{ hash: string, at: number | null }} T
1699
- * @param {ReadonlyArray<T>} list
1700
- * @param {Map<string, Arrival>} results
1701
- * @param {{ now: number, waitMs: number }} options
1702
- * @returns {T[]}
1703
- */
1704
- function triage(list, results, { now, waitMs }) {
1705
- /** @type {T[]} */
1706
- const live = [];
1707
-
1708
- for (const item of list) {
1709
- if (item.at === null) results.set(item.hash, { state: "waiting" });
1710
- else if (now - item.at > waitMs) results.set(item.hash, { reason: "expired", state: "unknown" });
1711
- else live.push(item);
1712
- }
1713
-
1714
- return live;
1715
- }
1716
-
1717
- /**
1718
- * Each item once, by hash: the first.
1719
- *
1720
- * @template {{ hash: string }} T
1721
- * @param {ReadonlyArray<T>} list
1722
- * @returns {T[]}
1723
- */
1724
- function uniqueByHash(list) {
1725
- const seen = new Set();
1726
-
1727
- return list.filter((item) => !seen.has(item.hash) && Boolean(seen.add(item.hash)));
1728
- }
1729
-
1730
- /**
1731
- * The results in the order the transfers came.
1732
- *
1733
- * @param {ReadonlyArray<{ hash: string }>} list
1734
- * @param {Map<string, Arrival>} results
1735
- * @returns {Map<string, Arrival>}
1736
- */
1737
- const inOrder = (list, results) => new Map(list.map((item) => [item.hash, /** @type {Arrival} */ (results.get(item.hash))]));
1738
-
1739
- /**
1740
- * @param {Iterable<string> | undefined} taken
1741
- * @param {(value: unknown) => string} hashOf
1742
- */
1743
- const usedOf = (taken, hashOf) => new Set([...(taken ?? [])].map(hashOf).filter(Boolean));
1744
-
1745
1434
  // ---------------------------------------------------------------------------
1746
1435
  // Deposits to DuskEVM
1747
1436
  // ---------------------------------------------------------------------------
@@ -1750,183 +1439,52 @@ const usedOf = (taken, hashOf) => new Set([...(taken ?? [])].map(hashOf).filter(
1750
1439
  * @param {DepositArrivalOptions} options
1751
1440
  * @returns {Promise<{ arrivals: Map<string, Arrival>, requests: number }>}
1752
1441
  */
1753
- export async function depositArrivals({
1754
- network,
1755
- deposits,
1756
- via = "explorer",
1757
- fetch: fetchFn,
1758
- explorerUrl,
1759
- rpcUrl,
1760
- archive,
1761
- taken,
1762
- blockTimes,
1763
- now = Date.now(),
1764
- waitMs = WAIT_MS,
1765
- windowMs = CREDIT_WINDOW_MS,
1766
- maxPages = EXPLORER_PAGES,
1767
- maxWindows = LOG_WINDOWS,
1768
- timeoutMs = TIMEOUT_MS,
1769
- }) {
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
+
1770
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);
1771
1462
 
1772
- const net = networkOf(network);
1773
- const list = uniqueByHash(Array.from(deposits ?? [], depositIn));
1463
+ const { evm, list, live, net, past, results, used } = depositsIn({ deposits, network, now, taken, waitMs }, scanOf);
1774
1464
  const read = reader(fetchFn, timeoutMs);
1775
- /** @type {Map<string, Arrival>} */
1776
- const results = new Map();
1777
- const evm = net.duskEvm;
1778
1465
 
1779
- if (!evm) {
1780
- 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");
1781
1468
 
1782
- return { arrivals: inOrder(list, results), requests: 0 };
1783
- }
1784
-
1785
- const used = usedOf(taken, evmHashOf);
1786
- const live = triage(list, results, { now, waitMs });
1787
- // Waited for too long: not looked for, though still before the newer ones.
1788
- const past = list.filter((item) => item.at !== null && now - item.at > waitMs);
1789
- const bridge = evm.predeploys.l2StandardBridge.toLowerCase();
1790
- const rules = { now, past, results, used, windowMs };
1791
-
1792
- if (via === "explorer") {
1793
- await onExplorer(read, live, { ...rules, api: String(explorerUrl ?? evm.blockscoutApiUrl), bridge, maxPages });
1794
- } else {
1795
- 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 });
1796
1470
  }
1797
1471
 
1798
1472
  return { arrivals: inOrder(list, results), requests: read.counter.requests };
1799
1473
  }
1800
1474
 
1801
- /**
1802
- * The L2 bridge's payments to an address, as DuskEVM's explorer lists its
1803
- * incoming calls: newest first, a page at a time (the explorer's
1804
- * `next_page_params`), back past `since`, at most `maxPages`. `complete`
1805
- * once they reach it, or there are no more.
1806
- *
1807
- * @param {Reader} read
1808
- * @param {{ api: string, bridge: string, maxPages: number, since: number, to: string }} ask
1809
- * @returns {Promise<{ complete: boolean, payments: Credit[] }>}
1810
- */
1811
- async function explorerPayments(read, { api, bridge, maxPages, since, to }) {
1812
- const base = `${api.replace(/\/+$/, "")}/addresses/${to}/internal-transactions`;
1813
- /** @type {any[]} */
1814
- const items = [];
1815
- /** @type {Record<string, unknown> | null} */
1816
- let next = null;
1817
- let complete = false;
1818
-
1819
- for (let page = 0; page < Math.max(1, Math.floor(maxPages)) && !complete; page += 1) {
1820
- const query = new URLSearchParams({ filter: "to" });
1821
-
1822
- for (const [key, value] of Object.entries(next ?? {})) query.set(key, String(value));
1823
-
1824
- const body = await read.getJson(`${base}?${query}`);
1825
- const calls = Array.isArray(body?.items) ? body.items : [];
1826
- const oldest = Math.min(...calls.map((/** @type {any} */ call) => timeOf(call?.timestamp)).filter(Number.isFinite));
1827
-
1828
- items.push(...calls);
1829
- next = body?.next_page_params && typeof body.next_page_params === "object" ? body.next_page_params : null;
1830
- complete = !next || !calls.length || oldest < since;
1831
- }
1832
-
1833
- /** @type {Map<string, Credit>} */
1834
- const payments = new Map();
1835
-
1836
- for (const call of items) {
1837
- const hash = evmHashOf(call?.transaction_hash);
1838
- const at = timeOf(call?.timestamp);
1839
- const value = String(call?.value ?? "");
1840
- const payee = call?.to?.hash;
1841
-
1842
- if (!hash || payments.has(hash) || !Number.isFinite(at) || !/^\d+$/.test(value)) continue;
1843
- if (String(call?.from?.hash ?? "").toLowerCase() !== bridge) continue;
1844
- if (payee !== undefined && payee !== null && String(payee).toLowerCase() !== to) continue;
1845
-
1846
- payments.set(hash, { at, block: quantityOf(call?.block_number), from: null, hash, success: call?.success !== false, wei: BigInt(value) });
1847
- }
1848
-
1849
- return { complete, payments: [...payments.values()] };
1850
- }
1851
-
1852
- /**
1853
- * Deposits followed on DuskEVM's explorer: each recipient's incoming calls
1854
- * read once for all its deposits, and matched as where the credits can't be
1855
- * told apart, as they don't say who deposited. Equal deposits past their
1856
- * wait just before them take their payments first, their results kept.
1857
- *
1858
- * @param {Reader} read
1859
- * @param {DepositItem[]} live
1860
- * @param {{ api: string, bridge: string, maxPages: number, now: number, past: DepositItem[], results: Map<string, Arrival>, used: Set<string>, windowMs: number }} options
1861
- */
1862
- async function onExplorer(read, live, { api, bridge, maxPages, now, past, results, used, windowMs }) {
1863
- for (const [to, items] of groupBy(live, (item) => item.to)) {
1864
- const polled = new Set(items.map((item) => item.hash));
1865
- const groups = [...groupBy(items, (item) => item.amount.toString()).values()].map((group) => ({
1866
- queue: withPast(
1867
- group.map(queued),
1868
- past.filter((item) => item.to === to && item.amount === group[0].amount).map(queued),
1869
- windowMs
1870
- ),
1871
- wei: group[0].amount * WEI_PER_LUX,
1872
- }));
1873
-
1874
- // Nothing can be told apart before a window is over: nothing is asked.
1875
- if (groups.every(({ queue }) => allOpen(queue, windowMs, now))) {
1876
- for (const item of items) results.set(item.hash, { state: "waiting" });
1877
- continue;
1878
- }
1879
-
1880
- /** @type {{ complete: boolean, payments: Credit[] }} */
1881
- let page;
1882
-
1883
- try {
1884
- page = await explorerPayments(read, { api, bridge, maxPages, since: Math.min(...groups.flatMap(({ queue }) => queue.map((item) => item.at))) - windowMs, to });
1885
- } catch (error) {
1886
- for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
1887
- continue;
1888
- }
1889
-
1890
- // Not back to the oldest: a payment of an older one may be a newer one's.
1891
- if (!page.complete) {
1892
- for (const item of items) results.set(item.hash, { reason: "beyond", state: "unknown" });
1893
- continue;
1894
- }
1895
-
1896
- for (const { queue, wei } of groups) {
1897
- const { assigned, verdicts } = settle(
1898
- queue,
1899
- page.payments.filter((payment) => payment.wei === wei),
1900
- { guarded: true, now, slackMs: windowMs, used, windowMs }
1901
- );
1902
-
1903
- for (const item of queue.filter((each) => polled.has(each.hash))) {
1904
- const credit = assigned.get(item.hash);
1905
- const verdict = verdicts.get(item.hash);
1906
-
1907
- if (credit && verdict === "arrived") {
1908
- used.add(credit.hash);
1909
- 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" });
1910
- } else {
1911
- results.set(item.hash, verdict === "waiting" ? { state: "waiting" } : { reason: verdict === "unsure" ? "unsure" : "late", state: "unknown" });
1912
- }
1913
- }
1914
- }
1915
- }
1916
- }
1917
-
1918
1475
  /**
1919
1476
  * Deposits followed on DuskEVM's RPC, by the bridge's events from each
1920
1477
  * sender's DuskEVM address: a group of equal deposits from one sender at a
1921
1478
  * time, with the sender's equal deposits shortly before, from its history,
1922
- * when there's an archive; without it, with the equal ones given that are
1923
- * 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.
1924
1482
  *
1925
1483
  * @param {Reader} read
1926
1484
  * @param {DepositItem[]} live
1927
- * @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
1928
1486
  */
1929
- 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 }) {
1930
1488
  // Equal deposits (the same DuskEVM address and amount) from a known sender
1931
1489
  // first, a sender at a time, its events telling its credits apart; then
1932
1490
  // those whose sender isn't known, which take none from the senders of the
@@ -1963,58 +1521,66 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
1963
1521
  const supplied = new Map(list.map((item) => [item.hash, item]));
1964
1522
  /** @type {Map<string, any>} */
1965
1523
  const calls = new Map();
1966
- 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;
1967
1526
 
1968
1527
  for (const { items, rivals } of [...known, ...unknown]) {
1969
1528
  const { amount, sender, to } = items[0];
1970
- const vouched = Boolean(archive) && sender !== null && items.every((item) => item.height !== null);
1971
1529
  let given = items.map(queued).sort(byTime);
1972
1530
  /** @type {Queued[]} */
1973
- let queue;
1531
+ let queue = [];
1974
1532
  /** @type {Set<string>} */
1975
1533
  const unsure = new Set();
1976
1534
  /** @type {Map<string, HistoryScan>} */
1977
1535
  let states = new Map();
1536
+ // Whether the sender's history tells its equal deposits' order.
1537
+ let vouched = false;
1978
1538
 
1979
- if (vouched) {
1539
+ if (archive && contract && sender !== null && items.every((item) => item.height !== null)) {
1980
1540
  // Each deposit's own hours of its sender's history, with its sender's
1981
1541
  // equal deposits there, the app's no longer looked for among them.
1982
- /** @type {Awaited<ReturnType<typeof readHistories>>} */
1983
- let history;
1542
+ /** @type {Awaited<ReturnType<typeof readHistories>> | null} */
1543
+ let history = null;
1984
1544
 
1985
1545
  try {
1986
1546
  history = await readHistories(
1987
- /** @type {Archive} */ (archive),
1547
+ archive,
1988
1548
  /** @type {Reading[]} */ ([...items].sort((a, b) => /** @type {number} */ (a.height) - /** @type {number} */ (b.height))),
1989
1549
  {
1990
- account: /** @type {string} */ (sender),
1550
+ account: sender,
1991
1551
  direction: "out",
1992
1552
  horizonMs: DEPOSIT_HORIZON_MS,
1993
1553
  keep: (entry) => entry.counterparty === contract && entry.value >= amount,
1994
1554
  of: (item) => `${sender}:${item.height}:${amount}`,
1995
1555
  pages: HISTORY_PAGES,
1996
- 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),
1997
1557
  reach: Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS),
1998
1558
  },
1999
1559
  read.counter
2000
1560
  );
2001
- } catch (error) {
2002
- for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
2003
- 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]] : [])));
2004
1565
  }
2005
1566
 
2006
- states = history.states;
2007
- waitForHistory(items, history, results);
1567
+ if (history) {
1568
+ vouched = true;
1569
+ states = history.states;
1570
+ waitForHistory(items, history, results);
2008
1571
 
2009
- if (!history.covered.length) continue;
1572
+ if (!history.covered.length) continue;
2010
1573
 
2011
- given = history.given;
2012
- queue = history.queue;
1574
+ given = history.given;
1575
+ queue = history.queue;
2013
1576
 
2014
- // One before it whose call can't be read could be equal: the credits'
2015
- // order can't be told.
2016
- for (const item of given) if (/** @type {number[]} */ (history.unread.get(item.hash)).length) unsure.add(item.hash);
2017
- } 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) {
2018
1584
  queue = withPast(
2019
1585
  given,
2020
1586
  past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
@@ -2363,7 +1929,7 @@ export async function migrationPayouts({ network, migrations, entries, archive,
2363
1929
  const listed = await byMemo(archive, asked.map((item) => item.hash), counter);
2364
1930
  const found = [...listed.values()].flat().flatMap((raw) => payoutsIn(raw, /** @type {string} */ (account), net));
2365
1931
  // Their times, and whether they went through.
2366
- const details = await detailsOf(archive, found.map((payout) => payout.payout), {}, counter);
1932
+ const details = await detailsOf(archive, found.map((payout) => payout.payout), counter);
2367
1933
 
2368
1934
  candidates = found.flatMap((payout) => {
2369
1935
  const known = details.get(payout.payout);
@@ -2451,8 +2017,9 @@ export function serviceTransferOf(entry, network) {
2451
2017
  const spent = luxOf(entry.gasSpent);
2452
2018
  const price = luxOf(entry.gasPrice);
2453
2019
  const amount = limit !== null && spent !== null && price !== null && (limit - spent) * price <= 0n ? value : null;
2454
- // The address the memo gives, exactly as the bridge reads it.
2455
- const memo = typeof entry.memo === "string" ? entry.memo : "";
2020
+ // The address the memo gives, exactly as the bridge reads it: from text.
2021
+ // A memo of bytes that aren't text, which /history gives as 0x hex, gives none.
2022
+ const memo = typeof entry.memo === "string" && entry.memoBinary !== true ? entry.memo : "";
2456
2023
  const to = EVM_ADDRESS.test(memo) ? memo : null;
2457
2024
  const problem = entry.success === false ? "failed" : value <= route.feeLux ? "under-fee" : to === null ? "no-address" : null;
2458
2025
 
@@ -2470,9 +2037,8 @@ export function migrationPayoutOf(entry, network) {
2470
2037
  const { at, hash, height, value } = factsOf(entry);
2471
2038
  // What the transfer paid: a gas refund paid to the account with it aside.
2472
2039
  const refund = entry?.refund === undefined || entry.refund === null ? 0n : luxOf(entry.refund);
2473
- const memo = String(entry?.memo ?? "")
2474
- .trim()
2475
- .toLowerCase();
2040
+ // The migration's hash, as text: a memo of bytes that aren't text names none.
2041
+ const memo = entry?.memoBinary === true ? "" : String(entry?.memo ?? "").trim().toLowerCase();
2476
2042
 
2477
2043
  if (!paying.length || entry.success === false || !hash || height === null || value === null || refund === null || refund > value || !EVM_HASH.test(memo)) {
2478
2044
  return null;
@@ -2500,4 +2066,4 @@ export function arrivalsOf(arrivals) {
2500
2066
  for (const [hash, arrival] of arrivals) if (arrival.state === "arrived") out.set(hash, { at: arrival.at, hash: arrival.hash });
2501
2067
 
2502
2068
  return out;
2503
- }
2069
+ }