@dusk-network/app-core 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +12 -8
- package/package.json +1 -1
- package/src/activity.d.ts +5 -1
- package/src/activity.js +7 -1
- package/src/bridge.d.ts +27 -6
- package/src/bridge.js +433 -97
- package/src/duskevm-withdrawals.d.ts +18 -4
- package/src/duskevm-withdrawals.js +95 -17
- package/src/history.d.ts +34 -10
- package/src/history.js +80 -18
- package/src/shielded.d.ts +21 -6
- package/src/shielded.js +155 -42
package/src/bridge.js
CHANGED
|
@@ -47,9 +47,10 @@ import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "
|
|
|
47
47
|
/**
|
|
48
48
|
* A credit on the other chain: its transaction, its block and time, its
|
|
49
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).
|
|
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`).
|
|
51
52
|
*
|
|
52
|
-
* @typedef {{ hash: string, block: bigint | null, at: number, wei: bigint, success: boolean, from: string | null }} Credit
|
|
53
|
+
* @typedef {{ hash: string, block: bigint | null, at: number, wei: bigint, success: boolean, from: string | null, pass?: number }} Credit
|
|
53
54
|
*/
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -75,14 +76,27 @@ const MAX_U64 = (1n << 64n) - 1n;
|
|
|
75
76
|
const TIMEOUT_MS = 8_000;
|
|
76
77
|
// About one Dusk block every 10 seconds: how far back a sender's history is read.
|
|
77
78
|
const DUSK_BLOCK_MS = 10_000;
|
|
78
|
-
// The blocks the chain's pace is read over,
|
|
79
|
-
//
|
|
80
|
-
//
|
|
79
|
+
// The blocks the chain's pace is read over, from a block rounded down to a
|
|
80
|
+
// multiple of `PACE_STEP` so that the app's cache keeps its time, and how
|
|
81
|
+
// long before the transfer a search starts as the pace says: the block is
|
|
82
|
+
// then checked by its time, and stepped back while it's still later.
|
|
81
83
|
const PACE_SAMPLE = 100_000n;
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const START_CHECKS = 4;
|
|
84
|
+
const PACE_STEP = 10_000n;
|
|
85
|
+
const START_MS = 10 * 60_000;
|
|
85
86
|
const MIN_WINDOW = 50n;
|
|
87
|
+
// The widest a search's windows grow, while the RPC answers them whole.
|
|
88
|
+
const MAX_WINDOW = 100_000n;
|
|
89
|
+
// How far back from the head a search's last window reaches: those blocks
|
|
90
|
+
// are read again on each call.
|
|
91
|
+
const OVERLAP = 1_000n;
|
|
92
|
+
// How long after a search first reaches the head it reads its blocks again
|
|
93
|
+
// from its first one; each pass after waits as long again as it has been
|
|
94
|
+
// since then, so the waits about double each time.
|
|
95
|
+
const RECHECK_MS = 60_000;
|
|
96
|
+
// How many passes over the blocks before a credit, since it was found, a
|
|
97
|
+
// transfer waits for before it takes the credit where a later equal one,
|
|
98
|
+
// still without its own, could have had it instead.
|
|
99
|
+
const CONFIRM_PASSES = 8;
|
|
86
100
|
// What an RPC says when it won't search that many blocks at once.
|
|
87
101
|
const RANGE_REFUSED = /limit|range|too many|exceed/i;
|
|
88
102
|
const DUSK_HASH = /^[0-9a-f]{64}$/;
|
|
@@ -233,9 +247,9 @@ function historyScanOf(value) {
|
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
/**
|
|
236
|
-
* Where a search stopped, what it searched for,
|
|
237
|
-
* before, and how far the transfer's history is
|
|
238
|
-
* them; null for anything else.
|
|
250
|
+
* Where a search stopped, what it searched for, the credits it found
|
|
251
|
+
* before, the width of its windows, and how far the transfer's history is
|
|
252
|
+
* read, as a result gave them; null for anything else.
|
|
239
253
|
*
|
|
240
254
|
* @param {unknown} value
|
|
241
255
|
* @returns {Scan | null}
|
|
@@ -253,11 +267,25 @@ function scanOf(value) {
|
|
|
253
267
|
const block = quantityOf(credit?.block);
|
|
254
268
|
const at = msOf(credit?.at);
|
|
255
269
|
const sender = credit?.sender === null ? null : addressOf(credit?.sender);
|
|
270
|
+
// The passes done before it was found: none where it isn't said.
|
|
271
|
+
const pass = Number.isSafeInteger(credit?.pass) && credit.pass >= 0 ? credit.pass : 0;
|
|
256
272
|
|
|
257
|
-
return hash && block !== null && at !== null && sender !== "" ? [{ at, block: block.toString(), hash, sender }] : [];
|
|
273
|
+
return hash && block !== null && at !== null && sender !== "" ? [{ at, block: block.toString(), hash, pass, sender }] : [];
|
|
258
274
|
});
|
|
275
|
+
// Where it began, and its passes over the blocks again: without them,
|
|
276
|
+
// as a search kept by an earlier version.
|
|
277
|
+
const start = quantityOf(scan.start);
|
|
278
|
+
const recheck = start === null ? null : recheckOf(scan.recheck);
|
|
279
|
+
// The width of its windows, as an earlier version kept it for its passes
|
|
280
|
+
// where it isn't said; and the narrowest the RPC refused.
|
|
281
|
+
const size = Number.isSafeInteger(scan.size) && scan.size >= Number(MIN_WINDOW) && scan.size <= Number(MAX_WINDOW) ? scan.size : (recheck?.size ?? null);
|
|
282
|
+
const refused = Number.isSafeInteger(scan.refused) && scan.refused > 0 ? scan.refused : null;
|
|
259
283
|
|
|
260
284
|
Object.assign(out, { basis: scan.basis, from: from.toString(), search: scan.search, seen });
|
|
285
|
+
if (start !== null) out.start = start.toString();
|
|
286
|
+
if (recheck) out.recheck = recheckIn(recheck);
|
|
287
|
+
if (size !== null) out.size = size;
|
|
288
|
+
if (refused !== null) out.refused = refused;
|
|
261
289
|
} else if (!history) {
|
|
262
290
|
return null;
|
|
263
291
|
}
|
|
@@ -375,6 +403,10 @@ function reader(fetchFn, timeoutMs) {
|
|
|
375
403
|
|
|
376
404
|
return {
|
|
377
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(),
|
|
378
410
|
|
|
379
411
|
/**
|
|
380
412
|
* @param {string} url
|
|
@@ -450,6 +482,36 @@ function clustersOf(queue, windowMs) {
|
|
|
450
482
|
*/
|
|
451
483
|
const allOpen = (queue, windowMs, now) => clustersOf(queue, windowMs).every((cluster) => now < cluster[cluster.length - 1].at + windowMs);
|
|
452
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
|
+
|
|
453
515
|
/**
|
|
454
516
|
* Each transfer of a queue of equal ones, oldest first, takes the first
|
|
455
517
|
* credit that went through after it that no earlier one took: from
|
|
@@ -470,10 +532,11 @@ const allOpen = (queue, windowMs, now) => clustersOf(queue, windowMs).every((clu
|
|
|
470
532
|
*
|
|
471
533
|
* @param {ReadonlyArray<Queued>} queue oldest first
|
|
472
534
|
* @param {ReadonlyArray<Credit>} credits
|
|
473
|
-
* @param {
|
|
535
|
+
* @param {Rules} rules
|
|
474
536
|
* @returns {{ assigned: Map<string, Credit>, verdicts: Map<string, Verdict> }}
|
|
475
537
|
*/
|
|
476
|
-
function settle(queue, credits,
|
|
538
|
+
function settle(queue, credits, rules) {
|
|
539
|
+
const { guarded, now, used, windowMs } = rules;
|
|
477
540
|
const sorted = [...credits].sort(byTime);
|
|
478
541
|
/** @type {Map<string, Credit>} */
|
|
479
542
|
const assigned = new Map();
|
|
@@ -482,18 +545,7 @@ function settle(queue, credits, { guarded, now, rivals = new Set(), slackMs, use
|
|
|
482
545
|
const verdicts = new Map();
|
|
483
546
|
|
|
484
547
|
for (const item of queue) {
|
|
485
|
-
const
|
|
486
|
-
const hi = guarded ? item.at + windowMs : Infinity;
|
|
487
|
-
const credit = sorted.find(
|
|
488
|
-
(c) =>
|
|
489
|
-
c.success &&
|
|
490
|
-
!taken.has(c.hash) &&
|
|
491
|
-
(!item.given || !used.has(c.hash)) &&
|
|
492
|
-
(c.from === null || !rivals.has(c.from)) &&
|
|
493
|
-
c.at >= lo &&
|
|
494
|
-
c.at <= hi &&
|
|
495
|
-
(item.notBefore === null || c.block === null || c.block >= item.notBefore)
|
|
496
|
-
);
|
|
548
|
+
const credit = sorted.find((c) => !taken.has(c.hash) && pays(item, c, rules));
|
|
497
549
|
|
|
498
550
|
if (credit) {
|
|
499
551
|
assigned.set(item.hash, credit);
|
|
@@ -653,80 +705,125 @@ async function blockTime(read, url, block, cache) {
|
|
|
653
705
|
|
|
654
706
|
if (typeof kept === "number" && Number.isFinite(kept)) return kept;
|
|
655
707
|
|
|
656
|
-
|
|
708
|
+
return once(read, `time ${url} ${block}`, async () => {
|
|
709
|
+
const seconds = quantityOf((await read.rpc(url, "eth_getBlockByNumber", [hex(block), false]))?.timestamp);
|
|
657
710
|
|
|
658
|
-
|
|
711
|
+
if (seconds === null) throw new Error(`The chain gave no time for block ${block}`);
|
|
659
712
|
|
|
660
|
-
|
|
713
|
+
const at = Number(seconds) * 1000;
|
|
661
714
|
|
|
662
|
-
|
|
715
|
+
cache?.set(Number(block), at);
|
|
663
716
|
|
|
664
|
-
|
|
717
|
+
return at;
|
|
718
|
+
});
|
|
665
719
|
}
|
|
666
720
|
|
|
667
721
|
/**
|
|
722
|
+
* What a call reads once for all its searches on a chain.
|
|
723
|
+
*
|
|
724
|
+
* @template T
|
|
725
|
+
* @param {Reader} read
|
|
726
|
+
* @param {string} key
|
|
727
|
+
* @param {() => Promise<T>} get
|
|
728
|
+
* @returns {Promise<T>}
|
|
729
|
+
*/
|
|
730
|
+
function once(read, key, get) {
|
|
731
|
+
const kept = read.once.get(key);
|
|
732
|
+
|
|
733
|
+
if (kept) return kept;
|
|
734
|
+
|
|
735
|
+
const got = get();
|
|
736
|
+
|
|
737
|
+
read.once.set(key, got);
|
|
738
|
+
// One that failed is read again by the next search.
|
|
739
|
+
got.catch(() => read.once.delete(key));
|
|
740
|
+
|
|
741
|
+
return got;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* The chain's head, read once a call: the head its pace was read with, where
|
|
746
|
+
* the call read that.
|
|
747
|
+
*
|
|
668
748
|
* @param {Reader} read
|
|
669
749
|
* @param {string} url
|
|
670
750
|
* @returns {Promise<bigint>}
|
|
671
751
|
*/
|
|
672
752
|
async function headOf(read, url) {
|
|
673
|
-
const
|
|
753
|
+
const paced = read.once.get(`pace ${url}`);
|
|
754
|
+
|
|
755
|
+
if (paced) return (await paced).head;
|
|
674
756
|
|
|
675
|
-
|
|
757
|
+
return once(read, `head ${url}`, async () => {
|
|
758
|
+
const head = quantityOf(await read.rpc(url, "eth_blockNumber", []));
|
|
676
759
|
|
|
677
|
-
|
|
760
|
+
if (head === null) throw new Error("The chain gave no head");
|
|
761
|
+
|
|
762
|
+
return head;
|
|
763
|
+
});
|
|
678
764
|
}
|
|
679
765
|
|
|
680
766
|
/**
|
|
681
|
-
* The chain's head, and
|
|
682
|
-
*
|
|
683
|
-
* stepped back while its own time is still later, twice as far each time,
|
|
684
|
-
* as the chain's pace may have changed.
|
|
767
|
+
* The chain's head, its time, and its pace (blocks a second) over its last
|
|
768
|
+
* 100,000 blocks or so, read once a call.
|
|
685
769
|
*
|
|
686
770
|
* @param {Reader} read
|
|
687
771
|
* @param {string} url
|
|
688
|
-
* @param {number} atMs
|
|
689
772
|
* @param {BlockTimes | undefined} cache
|
|
690
|
-
* @returns {Promise<{ head: bigint,
|
|
773
|
+
* @returns {Promise<{ head: bigint, seconds: number, pace: number }>}
|
|
691
774
|
*/
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
775
|
+
function paceOf(read, url, cache) {
|
|
776
|
+
return once(read, `pace ${url}`, async () => {
|
|
777
|
+
const latest = await read.rpc(url, "eth_getBlockByNumber", ["latest", false]);
|
|
778
|
+
const head = quantityOf(latest?.number);
|
|
779
|
+
const headSeconds = quantityOf(latest?.timestamp);
|
|
696
780
|
|
|
697
|
-
|
|
781
|
+
if (head === null || headSeconds === null) throw new Error("The chain gave no head");
|
|
698
782
|
|
|
699
|
-
|
|
783
|
+
cache?.set(Number(head), Number(headSeconds) * 1000);
|
|
784
|
+
read.once.set(`time ${url} ${head}`, Promise.resolve(Number(headSeconds) * 1000));
|
|
700
785
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
let pace = 1;
|
|
786
|
+
const back = head < PACE_SAMPLE ? 0n : ((head - PACE_SAMPLE) / PACE_STEP) * PACE_STEP;
|
|
787
|
+
let pace = 1;
|
|
704
788
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const span = Number(headSeconds) - sample / 1000;
|
|
789
|
+
if (back < head) {
|
|
790
|
+
const span = Number(headSeconds) - (await blockTime(read, url, back, cache)) / 1000;
|
|
708
791
|
|
|
709
|
-
|
|
710
|
-
|
|
792
|
+
if (span > 0) pace = Number(head - back) / span;
|
|
793
|
+
}
|
|
711
794
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
795
|
+
return { head, pace, seconds: Number(headSeconds) };
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* The chain's head, and a block from before `atMs`: the block 10 minutes
|
|
801
|
+
* before it, as the chain's pace says, checked by its time, and stepped back
|
|
802
|
+
* while that's still later, twice as far each time, as the chain's pace may
|
|
803
|
+
* have changed.
|
|
804
|
+
*
|
|
805
|
+
* @param {Reader} read
|
|
806
|
+
* @param {string} url
|
|
807
|
+
* @param {number} atMs
|
|
808
|
+
* @param {BlockTimes | undefined} cache
|
|
809
|
+
* @returns {Promise<{ head: bigint, start: bigint }>}
|
|
810
|
+
*/
|
|
811
|
+
async function startBefore(read, url, atMs, cache) {
|
|
812
|
+
const { head, pace, seconds } = await paceOf(read, url, cache);
|
|
813
|
+
/** @param {bigint} block @param {number} ms @param {number} times */
|
|
814
|
+
const back = (block, ms, times) => {
|
|
815
|
+
const step = BigInt(Math.ceil((Math.max(0, ms) / 1000) * pace * times));
|
|
719
816
|
|
|
720
817
|
return block > step ? block - step : 0n;
|
|
721
818
|
};
|
|
722
|
-
let start =
|
|
819
|
+
let start = back(head, seconds * 1000 - atMs + START_MS, 1);
|
|
723
820
|
|
|
724
|
-
for (let
|
|
821
|
+
for (let times = 2; start > 0n; times *= 2) {
|
|
725
822
|
const at = await blockTime(read, url, start, cache);
|
|
726
823
|
|
|
727
824
|
if (at <= atMs) break;
|
|
728
825
|
|
|
729
|
-
start =
|
|
826
|
+
start = back(start, at - atMs + START_MS, times);
|
|
730
827
|
}
|
|
731
828
|
|
|
732
829
|
return { head, start };
|
|
@@ -734,41 +831,75 @@ async function startBefore(read, url, atMs, cache) {
|
|
|
734
831
|
|
|
735
832
|
/**
|
|
736
833
|
* The credits asked for from block `start`, a window of blocks at a time, at
|
|
737
|
-
* most `maxWindows`, until
|
|
738
|
-
* `until` (`covered
|
|
739
|
-
*
|
|
740
|
-
* wide
|
|
834
|
+
* most `maxWindows`, until `head`, `enough`, or a window ending after
|
|
835
|
+
* `until` (`covered`, as the block before `start` may already be, where a
|
|
836
|
+
* search before stopped there): each checked against what was asked, with
|
|
837
|
+
* its block's time. Each window is `size` blocks wide, twice as wide after
|
|
838
|
+
* one the RPC answered whole, up to `MAX_WINDOW`, and narrower than any it
|
|
839
|
+
* refused (`refused`, the narrowest). One it won't search at once, or one
|
|
840
|
+
* wider than `LOG_WINDOW` that isn't answered, is asked again a quarter as
|
|
841
|
+
* wide, or as wide as the widest it answered where that's wider: `size` and
|
|
842
|
+
* `refused` are as the search leaves them. The window ending at `head`
|
|
843
|
+
* reaches back up to `overlap` blocks from it, no wider than `size`, though
|
|
844
|
+
* not under `floor`: those blocks are read again (from `low`). `asked`: the
|
|
845
|
+
* windows asked.
|
|
741
846
|
*
|
|
742
847
|
* @param {Reader} read
|
|
743
|
-
* @param {{ url: string, address: string, topics: ReadonlyArray<string | null>, wei: bigint, amountOf: (data: string) => bigint | null, start: bigint, head: bigint, maxWindows: number, blockTimes: BlockTimes | undefined, enough: (credits: Credit[]) => boolean, until: number | null, known: Credit[] }} search
|
|
744
|
-
* @returns {Promise<{ complete: boolean, covered: boolean, credits: Credit[], next: bigint }>}
|
|
848
|
+
* @param {{ url: string, address: string, topics: ReadonlyArray<string | null>, wei: bigint, amountOf: (data: string) => bigint | null, start: bigint, floor: bigint, head: bigint, size: bigint, refused: bigint | null, overlap?: bigint, maxWindows: number, blockTimes: BlockTimes | undefined, enough: (credits: Credit[]) => boolean, until: number | null, known: Credit[] }} search
|
|
849
|
+
* @returns {Promise<{ asked: number, complete: boolean, covered: boolean, credits: Credit[], low: bigint | null, next: bigint, refused: bigint | null, size: bigint }>}
|
|
745
850
|
*/
|
|
746
|
-
async function searchLogs(read, { address, amountOf, blockTimes, enough, head, known, maxWindows, start, topics, until, url, wei }) {
|
|
851
|
+
async function searchLogs(read, { address, amountOf, blockTimes, enough, floor, head, known, maxWindows, overlap = OVERLAP, refused: kept, size: width, start, topics, until, url, wei }) {
|
|
747
852
|
// What an earlier search found before `start`.
|
|
748
853
|
/** @type {Credit[]} */
|
|
749
854
|
const credits = [...known];
|
|
750
855
|
let from = start;
|
|
751
|
-
let size =
|
|
856
|
+
let size = width;
|
|
857
|
+
let refused = kept;
|
|
752
858
|
let covered = false;
|
|
859
|
+
let asked = 0;
|
|
860
|
+
/** @type {bigint | null} */
|
|
861
|
+
let low = null;
|
|
862
|
+
/** @type {bigint | null} */
|
|
863
|
+
let took = null;
|
|
864
|
+
|
|
865
|
+
if (until !== null && start > floor && start <= head) covered = (await blockTime(read, url, start - 1n, blockTimes)) > until;
|
|
753
866
|
|
|
754
|
-
|
|
867
|
+
while (!covered && asked < maxWindows && from <= head) {
|
|
755
868
|
const to = from + size - 1n < head ? from + size - 1n : head;
|
|
869
|
+
const back = head + 1n - (size < overlap ? size : overlap);
|
|
870
|
+
const lo = to === head && back < from ? (back > floor ? back : floor) : from;
|
|
871
|
+
const blocks = to - lo + 1n;
|
|
756
872
|
/** @type {any} */
|
|
757
873
|
let logs;
|
|
758
874
|
|
|
875
|
+
asked += 1;
|
|
876
|
+
|
|
759
877
|
try {
|
|
760
|
-
logs = await read.rpc(url, "eth_getLogs", [{ address, fromBlock: hex(
|
|
878
|
+
logs = await read.rpc(url, "eth_getLogs", [{ address, fromBlock: hex(lo), toBlock: hex(to), topics }]);
|
|
761
879
|
} catch (error) {
|
|
762
|
-
if (
|
|
763
|
-
|
|
880
|
+
if (blocks > MIN_WINDOW && (blocks > BigInt(LOG_WINDOW) || RANGE_REFUSED.test(messageOf(error)))) {
|
|
881
|
+
const fits = took !== null && took < blocks && took > blocks / 4n ? took : blocks / 4n;
|
|
882
|
+
|
|
883
|
+
if (refused === null || blocks < refused) refused = blocks;
|
|
884
|
+
size = fits > MIN_WINDOW ? fits : MIN_WINDOW;
|
|
764
885
|
continue;
|
|
765
886
|
}
|
|
766
887
|
|
|
767
888
|
throw error;
|
|
768
889
|
}
|
|
769
890
|
|
|
891
|
+
if (to === head) low = lo;
|
|
892
|
+
if (took === null || blocks > took) took = blocks;
|
|
893
|
+
|
|
894
|
+
// Answered whole: the next one twice as wide, while narrower than any refused.
|
|
895
|
+
if (blocks === size) {
|
|
896
|
+
const wider = size * 2n < MAX_WINDOW ? size * 2n : MAX_WINDOW;
|
|
897
|
+
|
|
898
|
+
if (refused === null || wider < refused) size = wider;
|
|
899
|
+
}
|
|
900
|
+
|
|
770
901
|
for (const log of Array.isArray(logs) ? logs : []) {
|
|
771
|
-
const found = creditLog(log, { address, amountOf, from, to, topics, wei });
|
|
902
|
+
const found = creditLog(log, { address, amountOf, from: lo, to, topics, wei });
|
|
772
903
|
|
|
773
904
|
if (!found || credits.some((credit) => credit.hash === found.hash)) continue;
|
|
774
905
|
if (found.at !== null) blockTimes?.set(Number(found.block), found.at);
|
|
@@ -785,7 +916,40 @@ async function searchLogs(read, { address, amountOf, blockTimes, enough, head, k
|
|
|
785
916
|
}
|
|
786
917
|
}
|
|
787
918
|
|
|
788
|
-
return { complete: from > head, covered, credits, next: from };
|
|
919
|
+
return { asked, complete: from > head, covered, credits, low, next: from, refused, size };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* A search's passes over its blocks again, as a result gave them: where the
|
|
924
|
+
* one under way is (null between two), when the next begins, how many are
|
|
925
|
+
* done, when the search first reached the head, and the width of the
|
|
926
|
+
* windows it read them in, as an earlier version kept it; null for anything
|
|
927
|
+
* else.
|
|
928
|
+
*
|
|
929
|
+
* @param {unknown} value
|
|
930
|
+
* @returns {{ at: number, from: bigint | null, passes: number, since: number, size: number | null } | null}
|
|
931
|
+
*/
|
|
932
|
+
function recheckOf(value) {
|
|
933
|
+
const kept = /** @type {any} */ (value);
|
|
934
|
+
const from = kept?.from === null ? null : quantityOf(kept?.from);
|
|
935
|
+
/** @param {unknown} ms */
|
|
936
|
+
const time = (ms) => typeof ms === "number" && Number.isFinite(ms);
|
|
937
|
+
const size = Number.isSafeInteger(kept?.size) && kept.size >= Number(MIN_WINDOW) && kept.size <= LOG_WINDOW ? kept.size : null;
|
|
938
|
+
|
|
939
|
+
if (!kept || !time(kept.at) || !time(kept.since) || !Number.isSafeInteger(kept.passes) || kept.passes < 0) return null;
|
|
940
|
+
if (kept.from !== null && from === null) return null;
|
|
941
|
+
|
|
942
|
+
return { at: kept.at, from, passes: kept.passes, since: kept.since, size };
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* A search's passes over its blocks again, as its scan keeps them.
|
|
947
|
+
*
|
|
948
|
+
* @param {NonNullable<ReturnType<typeof recheckOf>>} recheck
|
|
949
|
+
* @returns {NonNullable<Scan["recheck"]>}
|
|
950
|
+
*/
|
|
951
|
+
function recheckIn({ at, from, passes, since }) {
|
|
952
|
+
return { at, from: from === null ? null : from.toString(), passes, since };
|
|
789
953
|
}
|
|
790
954
|
|
|
791
955
|
/**
|
|
@@ -795,6 +959,17 @@ async function searchLogs(read, { address, amountOf, blockTimes, enough, head, k
|
|
|
795
959
|
* sent at (as the app saw the chain's head then), else from about when it
|
|
796
960
|
* was sent.
|
|
797
961
|
*
|
|
962
|
+
* Each block is read more than once: on each call, those up to `OVERLAP`
|
|
963
|
+
* under the head, and once the search has reached the head with a transfer
|
|
964
|
+
* not arrived, all of them again from where the search began, a pass at a
|
|
965
|
+
* time: the first a minute later, each after as long again as it has been
|
|
966
|
+
* since then, and one after the other, as far as its credit, while an
|
|
967
|
+
* arrival is held back. A pass under way takes what the search ahead leaves
|
|
968
|
+
* of a call's windows, one at least. A pass counts once it read the blocks
|
|
969
|
+
* again, as does a call whose window at the head reached back to where the
|
|
970
|
+
* search began. Both go on in windows as wide as the searches before left
|
|
971
|
+
* them, narrower than the narrowest the RPC refused.
|
|
972
|
+
*
|
|
798
973
|
* @param {Reader} read
|
|
799
974
|
* @param {{ given: Queued[], queue: Queued[], guarded: boolean, unsure: ReadonlySet<string>, rivals?: ReadonlySet<string>, filter: { address: string, topics: ReadonlyArray<string | null>, wei: bigint, amountOf: (data: string) => bigint | null } }} group
|
|
800
975
|
* @param {{ url: string, maxWindows: number, blockTimes: BlockTimes | undefined, now: number, slackMs: number, windowMs: number, used: Set<string> }} options
|
|
@@ -821,9 +996,11 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
821
996
|
// The logs searched for: a search for others, as for another sender or
|
|
822
997
|
// amount, isn't carried on.
|
|
823
998
|
const search = [filter.address, ...filter.topics.map((topic) => topic ?? "*"), filter.wei.toString()].join(":");
|
|
824
|
-
/** @type {{ basis: number, from: string, seen?: Scan["seen"] }[]} */
|
|
999
|
+
/** @type {{ basis: number, from: string, seen?: Scan["seen"], start?: string, recheck?: Scan["recheck"], size?: number, refused?: number }[]} */
|
|
825
1000
|
const resumed = live.flatMap(({ scan }) =>
|
|
826
|
-
scan?.from !== undefined && scan.search === search && scan.basis !== undefined && scan.basis <= first.at
|
|
1001
|
+
scan?.from !== undefined && scan.search === search && scan.basis !== undefined && scan.basis <= first.at
|
|
1002
|
+
? [{ basis: scan.basis, from: scan.from, recheck: scan.recheck, refused: scan.refused, seen: scan.seen, size: scan.size, start: scan.start }]
|
|
1003
|
+
: []
|
|
827
1004
|
);
|
|
828
1005
|
/** @type {Map<string, Credit>} */
|
|
829
1006
|
const carried = new Map();
|
|
@@ -832,7 +1009,7 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
832
1009
|
// counts again, as the transfer it paid may be among the queue's.
|
|
833
1010
|
for (const scan of resumed) {
|
|
834
1011
|
for (const credit of scan.seen ?? []) {
|
|
835
|
-
if (credit.at >= reach) carried.set(credit.hash, { at: credit.at, block: BigInt(credit.block), from: credit.sender, hash: credit.hash, success: true, wei: filter.wei });
|
|
1012
|
+
if (credit.at >= reach) carried.set(credit.hash, { at: credit.at, block: BigInt(credit.block), from: credit.sender, hash: credit.hash, pass: credit.pass ?? 0, success: true, wei: filter.wei });
|
|
836
1013
|
}
|
|
837
1014
|
}
|
|
838
1015
|
|
|
@@ -840,10 +1017,29 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
840
1017
|
let head;
|
|
841
1018
|
/** @type {bigint} */
|
|
842
1019
|
let start;
|
|
1020
|
+
// The block the search began at, and its passes over the blocks before.
|
|
1021
|
+
/** @type {bigint} */
|
|
1022
|
+
let floor;
|
|
1023
|
+
/** @type {ReturnType<typeof recheckOf>} */
|
|
1024
|
+
let recheck = null;
|
|
843
1025
|
|
|
844
1026
|
if (resumed.length && resumed.every((scan) => scan.basis === first.at)) {
|
|
845
|
-
|
|
846
|
-
|
|
1027
|
+
const kept = resumed.reduce((a, b) => (BigInt(b.from) < BigInt(a.from) ? b : a));
|
|
1028
|
+
|
|
1029
|
+
start = BigInt(kept.from);
|
|
1030
|
+
recheck = recheckOf(kept.recheck);
|
|
1031
|
+
|
|
1032
|
+
if (resumed.every((scan) => scan.start !== undefined)) {
|
|
1033
|
+
floor = minOf(resumed.map((scan) => BigInt(/** @type {string} */ (scan.start))));
|
|
1034
|
+
head = await headOf(read, url);
|
|
1035
|
+
} else {
|
|
1036
|
+
// Kept without where it began: from where a search from the first
|
|
1037
|
+
// begins.
|
|
1038
|
+
const fresh = first.notBefore !== null ? { head: await headOf(read, url), start: first.notBefore } : await startBefore(read, url, reach, blockTimes);
|
|
1039
|
+
|
|
1040
|
+
floor = fresh.start;
|
|
1041
|
+
head = fresh.head;
|
|
1042
|
+
}
|
|
847
1043
|
} else if (resumed.length) {
|
|
848
1044
|
// Begun from a transfer before the first now, as one that since
|
|
849
1045
|
// arrived: none of the queue's credits is before where a search from
|
|
@@ -853,38 +1049,153 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
853
1049
|
|
|
854
1050
|
head = fresh.head;
|
|
855
1051
|
start = from > fresh.start ? from : fresh.start;
|
|
1052
|
+
floor = fresh.start;
|
|
856
1053
|
} else if (first.notBefore !== null) {
|
|
857
1054
|
start = first.notBefore;
|
|
1055
|
+
floor = start;
|
|
858
1056
|
head = await headOf(read, url);
|
|
859
1057
|
} else {
|
|
860
1058
|
({ head, start } = await startBefore(read, url, reach, blockTimes));
|
|
1059
|
+
floor = start;
|
|
861
1060
|
}
|
|
862
1061
|
|
|
1062
|
+
if (floor > start) floor = start;
|
|
1063
|
+
|
|
1064
|
+
// No later than a credit found before: the blocks are read again from it.
|
|
1065
|
+
for (const credit of carried.values()) if (credit.block !== null && credit.block < floor) floor = credit.block;
|
|
1066
|
+
|
|
1067
|
+
// The width of the windows, and the narrowest the RPC refused, as the
|
|
1068
|
+
// searches before left them: the narrowest of each.
|
|
1069
|
+
const sizes = resumed.flatMap((scan) => (scan.size === undefined ? [] : [BigInt(scan.size)]));
|
|
1070
|
+
const refusals = resumed.flatMap((scan) => (scan.refused === undefined ? [] : [BigInt(scan.refused)]));
|
|
1071
|
+
const width = sizes.length ? minOf(sizes) : BigInt(LOG_WINDOW);
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* The given transfers' credits with `found`: those that arrived, by their
|
|
1075
|
+
* credits, and those held back (`held`), with the last of their credits'
|
|
1076
|
+
* blocks (`top`). One a later transfer of the queue, still without a
|
|
1077
|
+
* credit, could have had the credit of instead is held back until the
|
|
1078
|
+
* blocks before it are read again `CONFIRM_PASSES` times since it was
|
|
1079
|
+
* found: its own may yet be found there.
|
|
1080
|
+
*
|
|
1081
|
+
* @param {Credit[]} found
|
|
1082
|
+
* @param {boolean} seen
|
|
1083
|
+
*/
|
|
1084
|
+
const settled = (found, seen) => {
|
|
1085
|
+
const { assigned, verdicts } = settle(queue, found, rules);
|
|
1086
|
+
const passes = recheck?.passes ?? 0;
|
|
1087
|
+
const mine = new Set(live.map((item) => item.hash));
|
|
1088
|
+
/** @type {Set<string>} */
|
|
1089
|
+
const arrived = new Set();
|
|
1090
|
+
/** @type {Set<string>} */
|
|
1091
|
+
const held = new Set();
|
|
1092
|
+
/** @type {bigint | null} */
|
|
1093
|
+
let top = null;
|
|
1094
|
+
|
|
1095
|
+
queue.forEach((item, i) => {
|
|
1096
|
+
const credit = assigned.get(item.hash);
|
|
1097
|
+
|
|
1098
|
+
if (!credit || !mine.has(item.hash) || verdicts.get(item.hash) !== "arrived" || !(seen || !guarded)) return;
|
|
1099
|
+
|
|
1100
|
+
// A later one without a credit could have had this one instead.
|
|
1101
|
+
const doubt = !guarded && passes - (credit.pass ?? 0) < CONFIRM_PASSES && queue.slice(i + 1).some((later) => !assigned.has(later.hash) && pays(later, credit, rules));
|
|
1102
|
+
|
|
1103
|
+
if (!doubt) {
|
|
1104
|
+
arrived.add(credit.hash);
|
|
1105
|
+
} else {
|
|
1106
|
+
held.add(item.hash);
|
|
1107
|
+
if (credit.block !== null && (top === null || credit.block > top)) top = credit.block;
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
return { arrived, assigned, held, top, verdicts };
|
|
1112
|
+
};
|
|
1113
|
+
/**
|
|
1114
|
+
* The credits found in this call, with the passes done before the next
|
|
1115
|
+
* one to begin.
|
|
1116
|
+
*
|
|
1117
|
+
* @param {Credit[]} found
|
|
1118
|
+
* @returns {Credit[]}
|
|
1119
|
+
*/
|
|
1120
|
+
const mark = (found) => {
|
|
1121
|
+
const pass = recheck === null ? 0 : recheck.passes + (recheck.from !== null ? 1 : 0);
|
|
1122
|
+
|
|
1123
|
+
return found.map((credit) => (credit.pass === undefined ? { ...credit, pass } : credit));
|
|
1124
|
+
};
|
|
863
1125
|
// Where the credits can't be told apart, only those in the transfers'
|
|
864
1126
|
// windows count: once they're all over, read no further than the last.
|
|
865
1127
|
const last = queue[queue.length - 1].at + windowMs;
|
|
866
|
-
const
|
|
1128
|
+
const until = guarded && now >= last ? last : null;
|
|
1129
|
+
const due = recheck !== null && (recheck.from !== null || now >= recheck.at);
|
|
1130
|
+
const ahead = await searchLogs(read, {
|
|
867
1131
|
...filter,
|
|
868
1132
|
blockTimes,
|
|
869
1133
|
// Once every transfer of the queue has its credit: read no further.
|
|
870
1134
|
enough: (found) => !guarded && settle(queue, found, rules).assigned.size === queue.length,
|
|
1135
|
+
floor,
|
|
871
1136
|
head,
|
|
872
1137
|
known: [...carried.values()],
|
|
873
1138
|
maxWindows,
|
|
1139
|
+
refused: refusals.length ? minOf(refusals) : null,
|
|
1140
|
+
size: width,
|
|
874
1141
|
start,
|
|
875
|
-
until
|
|
1142
|
+
until,
|
|
876
1143
|
url,
|
|
877
1144
|
});
|
|
1145
|
+
const { complete, covered, next } = ahead;
|
|
878
1146
|
const seen = complete || covered;
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const
|
|
1147
|
+
// Its window at the head reached back to where the search began: every
|
|
1148
|
+
// block read before this call is read again, as by a pass.
|
|
1149
|
+
const again = recheck !== null && ahead.low !== null && ahead.low <= floor && start > floor;
|
|
1150
|
+
|
|
1151
|
+
if (again && recheck !== null) recheck = { ...recheck, passes: recheck.passes + 1 };
|
|
1152
|
+
|
|
1153
|
+
let credits = mark(ahead.credits);
|
|
1154
|
+
const before = settled(credits, seen);
|
|
1155
|
+
let { refused, size } = ahead;
|
|
1156
|
+
|
|
1157
|
+
if ((seen || before.held.size) && recheck === null) recheck = { at: before.held.size ? now : now + RECHECK_MS, from: null, passes: 0, since: now, size: null };
|
|
1158
|
+
|
|
1159
|
+
// A transfer not arrived yet: the blocks read before this call, again, as
|
|
1160
|
+
// far as the search ahead reads; while an arrival is held back, as far as
|
|
1161
|
+
// its credit. With what the search ahead leaves of a call's windows, one
|
|
1162
|
+
// at least.
|
|
1163
|
+
if (recheck !== null && (due || before.held.size) && before.arrived.size < live.length) {
|
|
1164
|
+
const from = recheck.from !== null && recheck.from > floor ? recheck.from : floor;
|
|
1165
|
+
const below = ahead.low !== null && ahead.low < start ? ahead.low : start;
|
|
1166
|
+
const pass = await searchLogs(read, {
|
|
1167
|
+
...filter,
|
|
1168
|
+
blockTimes,
|
|
1169
|
+
// Read through: a credit further on, still before those found ahead,
|
|
1170
|
+
// could be a transfer's first.
|
|
1171
|
+
enough: () => false,
|
|
1172
|
+
floor: from,
|
|
1173
|
+
head: before.top !== null && before.top < below ? before.top : below - 1n,
|
|
1174
|
+
known: credits,
|
|
1175
|
+
maxWindows: maxWindows < 1 ? 0 : Math.max(1, maxWindows - ahead.asked),
|
|
1176
|
+
refused,
|
|
1177
|
+
size,
|
|
1178
|
+
start: from,
|
|
1179
|
+
until,
|
|
1180
|
+
url,
|
|
1181
|
+
});
|
|
1182
|
+
const done = pass.complete || pass.covered;
|
|
1183
|
+
// A pass that read no block is none: only one that read the blocks again
|
|
1184
|
+
// counts, and not twice in a call.
|
|
1185
|
+
const counted = done && !again && pass.next > floor;
|
|
1186
|
+
|
|
1187
|
+
recheck = done ? { ...recheck, at: now + Math.max(RECHECK_MS, now - recheck.since), from: null, passes: recheck.passes + (counted ? 1 : 0) } : { ...recheck, from: pass.next };
|
|
1188
|
+
credits = mark(pass.credits);
|
|
1189
|
+
({ refused, size } = pass);
|
|
1190
|
+
}
|
|
882
1191
|
|
|
883
|
-
|
|
884
|
-
const credit = assigned.get(item.hash);
|
|
1192
|
+
const { arrived, assigned, held, verdicts } = settled(credits, seen);
|
|
885
1193
|
|
|
886
|
-
|
|
887
|
-
|
|
1194
|
+
// Passes go on one after the other while an arrival is held back; once one
|
|
1195
|
+
// arrives with others still waiting, theirs begin again a minute later,
|
|
1196
|
+
// the waits growing from there.
|
|
1197
|
+
if (recheck !== null && held.size) recheck = { ...recheck, at: Math.min(recheck.at, now) };
|
|
1198
|
+
else if (recheck !== null && arrived.size && arrived.size < live.length) recheck = { ...recheck, at: Math.min(recheck.at, now + RECHECK_MS), since: now };
|
|
888
1199
|
|
|
889
1200
|
// The next search carries on from where this one stopped, with the
|
|
890
1201
|
// credits found so far: a transfer that arrived may come back from the
|
|
@@ -893,15 +1204,21 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
893
1204
|
const scan = {
|
|
894
1205
|
basis: first.at,
|
|
895
1206
|
from: next.toString(),
|
|
1207
|
+
...(recheck ? { recheck: recheckIn(recheck) } : {}),
|
|
1208
|
+
...(refused === null ? {} : { refused: Number(refused) }),
|
|
896
1209
|
search,
|
|
897
|
-
seen: credits.flatMap((credit) => (credit.at >= reach && credit.block !== null ? [{ at: credit.at, block: credit.block.toString(), hash: credit.hash, sender: credit.from }] : [])),
|
|
1210
|
+
seen: credits.flatMap((credit) => (credit.at >= reach && credit.block !== null ? [{ at: credit.at, block: credit.block.toString(), hash: credit.hash, pass: credit.pass ?? 0, sender: credit.from }] : [])),
|
|
1211
|
+
size: Number(size),
|
|
1212
|
+
start: floor.toString(),
|
|
898
1213
|
};
|
|
899
1214
|
|
|
900
1215
|
for (const item of live) {
|
|
901
1216
|
const credit = assigned.get(item.hash);
|
|
902
1217
|
const verdict = verdicts.get(item.hash);
|
|
903
1218
|
|
|
904
|
-
if (
|
|
1219
|
+
if (held.has(item.hash)) {
|
|
1220
|
+
out.set(item.hash, { scan, state: "waiting" });
|
|
1221
|
+
} else if (credit && arrived.has(credit.hash)) {
|
|
905
1222
|
used.add(credit.hash);
|
|
906
1223
|
out.set(item.hash, {
|
|
907
1224
|
amount: credit.wei / WEI_PER_LUX,
|
|
@@ -1238,12 +1555,25 @@ async function depositPeers(archive, net, found, { amount, bounds, calls, suppli
|
|
|
1238
1555
|
}
|
|
1239
1556
|
|
|
1240
1557
|
/**
|
|
1241
|
-
* Where a scan's search stopped, without its
|
|
1558
|
+
* Where a scan's search stopped, with the width of its windows, without its
|
|
1559
|
+
* history.
|
|
1242
1560
|
*
|
|
1243
1561
|
* @param {Scan | null | undefined} scan
|
|
1244
1562
|
* @returns {Scan}
|
|
1245
1563
|
*/
|
|
1246
|
-
|
|
1564
|
+
function searchOf(scan) {
|
|
1565
|
+
if (scan?.from === undefined) return {};
|
|
1566
|
+
|
|
1567
|
+
/** @type {Scan} */
|
|
1568
|
+
const out = { basis: scan.basis, from: scan.from, search: scan.search, seen: scan.seen };
|
|
1569
|
+
|
|
1570
|
+
if (scan.start !== undefined) out.start = scan.start;
|
|
1571
|
+
if (scan.recheck !== undefined) out.recheck = scan.recheck;
|
|
1572
|
+
if (scan.size !== undefined) out.size = scan.size;
|
|
1573
|
+
if (scan.refused !== undefined) out.refused = scan.refused;
|
|
1574
|
+
|
|
1575
|
+
return out;
|
|
1576
|
+
}
|
|
1247
1577
|
|
|
1248
1578
|
/**
|
|
1249
1579
|
* The transfers whose history isn't all read yet wait, with where their
|
|
@@ -1814,6 +2144,12 @@ export async function serviceArrivals({
|
|
|
1814
2144
|
}
|
|
1815
2145
|
|
|
1816
2146
|
const live = triage(known, results, { now, waitMs });
|
|
2147
|
+
// The block times the app keeps for a route's chain: by its chain ID, or
|
|
2148
|
+
// the one cache given, where the network's service routes are all on one
|
|
2149
|
+
// chain.
|
|
2150
|
+
const oneChain = new Set(services.map((route) => route.chain?.chainId)).size === 1;
|
|
2151
|
+
/** @param {Route} route @returns {BlockTimes | undefined} */
|
|
2152
|
+
const timesOf = (route) => (typeof blockTimes === "function" ? (blockTimes(route.chain.chainId) ?? undefined) : oneChain ? blockTimes : undefined);
|
|
1817
2153
|
// The fee of each transfer the app gave, those no longer looked for too,
|
|
1818
2154
|
// and the block its payout can't be before.
|
|
1819
2155
|
const feeOf = new Map(routed.map((item) => [item.hash, item.fee ?? /** @type {Route & { kind: "service" }} */ (item.route).feeLux]));
|
|
@@ -1886,7 +2222,7 @@ export async function serviceArrivals({
|
|
|
1886
2222
|
queue: history.queue,
|
|
1887
2223
|
unsure,
|
|
1888
2224
|
},
|
|
1889
|
-
{ blockTimes, maxWindows, now, slackMs: CLOCK_SLACK_MS, url: String(rpcUrl ?? route.chain.rpcUrl), used, windowMs: CREDIT_WINDOW_MS }
|
|
2225
|
+
{ blockTimes: timesOf(route), maxWindows, now, slackMs: CLOCK_SLACK_MS, url: String(rpcUrl ?? route.chain.rpcUrl), used, windowMs: CREDIT_WINDOW_MS }
|
|
1890
2226
|
);
|
|
1891
2227
|
|
|
1892
2228
|
for (const [hash, arrival] of found) results.set(hash, withHistory(hash, arrival, given, history.states));
|