@dusk-network/app-core 0.3.2 → 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/src/bridge.js CHANGED
@@ -76,14 +76,16 @@ const MAX_U64 = (1n << 64n) - 1n;
76
76
  const TIMEOUT_MS = 8_000;
77
77
  // About one Dusk block every 10 seconds: how far back a sender's history is read.
78
78
  const DUSK_BLOCK_MS = 10_000;
79
- // The blocks the chain's pace is read over, how much further back than the
80
- // pace says a search starts, and how often it steps back again from a block
81
- // that is still later than the transfer.
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.
82
83
  const PACE_SAMPLE = 100_000n;
83
- const PACE_MARGIN = 1.2;
84
- const START_MARGIN = 50n;
85
- const START_CHECKS = 4;
84
+ const PACE_STEP = 10_000n;
85
+ const START_MS = 10 * 60_000;
86
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;
87
89
  // How far back from the head a search's last window reaches: those blocks
88
90
  // are read again on each call.
89
91
  const OVERLAP = 1_000n;
@@ -245,9 +247,9 @@ function historyScanOf(value) {
245
247
  }
246
248
 
247
249
  /**
248
- * Where a search stopped, what it searched for, and the credits it found
249
- * before, and how far the transfer's history is read, as a result gave
250
- * 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.
251
253
  *
252
254
  * @param {unknown} value
253
255
  * @returns {Scan | null}
@@ -274,10 +276,16 @@ function scanOf(value) {
274
276
  // as a search kept by an earlier version.
275
277
  const start = quantityOf(scan.start);
276
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;
277
283
 
278
284
  Object.assign(out, { basis: scan.basis, from: from.toString(), search: scan.search, seen });
279
285
  if (start !== null) out.start = start.toString();
280
286
  if (recheck) out.recheck = recheckIn(recheck);
287
+ if (size !== null) out.size = size;
288
+ if (refused !== null) out.refused = refused;
281
289
  } else if (!history) {
282
290
  return null;
283
291
  }
@@ -395,6 +403,10 @@ function reader(fetchFn, timeoutMs) {
395
403
 
396
404
  return {
397
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(),
398
410
 
399
411
  /**
400
412
  * @param {string} url
@@ -693,80 +705,125 @@ async function blockTime(read, url, block, cache) {
693
705
 
694
706
  if (typeof kept === "number" && Number.isFinite(kept)) return kept;
695
707
 
696
- const seconds = quantityOf((await read.rpc(url, "eth_getBlockByNumber", [hex(block), false]))?.timestamp);
708
+ return once(read, `time ${url} ${block}`, async () => {
709
+ const seconds = quantityOf((await read.rpc(url, "eth_getBlockByNumber", [hex(block), false]))?.timestamp);
697
710
 
698
- if (seconds === null) throw new Error(`The chain gave no time for block ${block}`);
711
+ if (seconds === null) throw new Error(`The chain gave no time for block ${block}`);
699
712
 
700
- const at = Number(seconds) * 1000;
713
+ const at = Number(seconds) * 1000;
701
714
 
702
- cache?.set(Number(block), at);
715
+ cache?.set(Number(block), at);
703
716
 
704
- return at;
717
+ return at;
718
+ });
719
+ }
720
+
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;
705
742
  }
706
743
 
707
744
  /**
745
+ * The chain's head, read once a call: the head its pace was read with, where
746
+ * the call read that.
747
+ *
708
748
  * @param {Reader} read
709
749
  * @param {string} url
710
750
  * @returns {Promise<bigint>}
711
751
  */
712
752
  async function headOf(read, url) {
713
- const head = quantityOf(await read.rpc(url, "eth_blockNumber", []));
753
+ const paced = read.once.get(`pace ${url}`);
754
+
755
+ if (paced) return (await paced).head;
714
756
 
715
- if (head === null) throw new Error("The chain gave no head");
757
+ return once(read, `head ${url}`, async () => {
758
+ const head = quantityOf(await read.rpc(url, "eth_blockNumber", []));
716
759
 
717
- return head;
760
+ if (head === null) throw new Error("The chain gave no head");
761
+
762
+ return head;
763
+ });
718
764
  }
719
765
 
720
766
  /**
721
- * The chain's head, and a block from before `atMs`: estimated from the
722
- * chain's pace over its last 100,000 blocks, a fifth further back, then
723
- * stepped back while its own time is still later, twice as far each time,
724
- * 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.
725
769
  *
726
770
  * @param {Reader} read
727
771
  * @param {string} url
728
- * @param {number} atMs
729
772
  * @param {BlockTimes | undefined} cache
730
- * @returns {Promise<{ head: bigint, start: bigint }>}
773
+ * @returns {Promise<{ head: bigint, seconds: number, pace: number }>}
731
774
  */
732
- async function startBefore(read, url, atMs, cache) {
733
- const latest = await read.rpc(url, "eth_getBlockByNumber", ["latest", false]);
734
- const head = quantityOf(latest?.number);
735
- const headSeconds = quantityOf(latest?.timestamp);
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);
736
780
 
737
- if (head === null || headSeconds === null) throw new Error("The chain gave no head");
781
+ if (head === null || headSeconds === null) throw new Error("The chain gave no head");
738
782
 
739
- cache?.set(Number(head), Number(headSeconds) * 1000);
783
+ cache?.set(Number(head), Number(headSeconds) * 1000);
784
+ read.once.set(`time ${url} ${head}`, Promise.resolve(Number(headSeconds) * 1000));
740
785
 
741
- const back = head < PACE_SAMPLE ? head : PACE_SAMPLE;
742
- // Blocks a second.
743
- let pace = 1;
786
+ const back = head < PACE_SAMPLE ? 0n : ((head - PACE_SAMPLE) / PACE_STEP) * PACE_STEP;
787
+ let pace = 1;
744
788
 
745
- if (back > 0n) {
746
- const sample = await blockTime(read, url, head - back, cache);
747
- const span = Number(headSeconds) - sample / 1000;
789
+ if (back < head) {
790
+ const span = Number(headSeconds) - (await blockTime(read, url, back, cache)) / 1000;
748
791
 
749
- if (span > 0) pace = Number(back) / span;
750
- }
792
+ if (span > 0) pace = Number(head - back) / span;
793
+ }
751
794
 
752
- /**
753
- * @param {bigint} block
754
- * @param {number} seconds
755
- * @param {number} [times]
756
- */
757
- const stepBack = (block, seconds, times = 1) => {
758
- const step = BigInt(Math.ceil(Math.max(0, seconds) * pace * PACE_MARGIN * times)) + START_MARGIN;
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));
759
816
 
760
817
  return block > step ? block - step : 0n;
761
818
  };
762
- let start = stepBack(head, Number(headSeconds) - atMs / 1000);
819
+ let start = back(head, seconds * 1000 - atMs + START_MS, 1);
763
820
 
764
- for (let check = 0; check < START_CHECKS && start > 0n; check += 1) {
821
+ for (let times = 2; start > 0n; times *= 2) {
765
822
  const at = await blockTime(read, url, start, cache);
766
823
 
767
824
  if (at <= atMs) break;
768
825
 
769
- start = stepBack(start, (at - atMs) / 1000, 2 ** (check + 1));
826
+ start = back(start, at - atMs + START_MS, times);
770
827
  }
771
828
 
772
829
  return { head, start };
@@ -775,37 +832,43 @@ async function startBefore(read, url, atMs, cache) {
775
832
  /**
776
833
  * The credits asked for from block `start`, a window of blocks at a time, at
777
834
  * most `maxWindows`, until `head`, `enough`, or a window ending after
778
- * `until` (`covered`): each checked against what was asked, with its block's
779
- * time. A window the RPC won't search at once is asked again a quarter as
780
- * wide (`size`, the width it ends with; `refused`, the narrowest it
781
- * refused, and `took`, the widest it answered). The window ending at `head`
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`
782
843
  * reaches back up to `overlap` blocks from it, no wider than `size`, though
783
844
  * not under `floor`: those blocks are read again (from `low`). `asked`: the
784
845
  * windows asked.
785
846
  *
786
847
  * @param {Reader} read
787
- * @param {{ url: string, address: string, topics: ReadonlyArray<string | null>, wei: bigint, amountOf: (data: string) => bigint | null, start: bigint, floor: bigint, head: bigint, size?: bigint, overlap?: bigint, maxWindows: number, blockTimes: BlockTimes | undefined, enough: (credits: Credit[]) => boolean, until: number | null, known: Credit[] }} search
788
- * @returns {Promise<{ asked: number, complete: boolean, covered: boolean, credits: Credit[], low: bigint | null, next: bigint, refused: bigint | null, size: bigint, took: bigint | null }>}
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 }>}
789
850
  */
790
- async function searchLogs(read, { address, amountOf, blockTimes, enough, floor, head, known, maxWindows, overlap = OVERLAP, size: width = BigInt(LOG_WINDOW), 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 }) {
791
852
  // What an earlier search found before `start`.
792
853
  /** @type {Credit[]} */
793
854
  const credits = [...known];
794
855
  let from = start;
795
856
  let size = width;
857
+ let refused = kept;
796
858
  let covered = false;
797
859
  let asked = 0;
798
860
  /** @type {bigint | null} */
799
861
  let low = null;
800
862
  /** @type {bigint | null} */
801
- let refused = null;
802
- /** @type {bigint | null} */
803
863
  let took = null;
804
864
 
805
- while (asked < maxWindows && from <= head) {
865
+ if (until !== null && start > floor && start <= head) covered = (await blockTime(read, url, start - 1n, blockTimes)) > until;
866
+
867
+ while (!covered && asked < maxWindows && from <= head) {
806
868
  const to = from + size - 1n < head ? from + size - 1n : head;
807
869
  const back = head + 1n - (size < overlap ? size : overlap);
808
870
  const lo = to === head && back < from ? (back > floor ? back : floor) : from;
871
+ const blocks = to - lo + 1n;
809
872
  /** @type {any} */
810
873
  let logs;
811
874
 
@@ -814,9 +877,11 @@ async function searchLogs(read, { address, amountOf, blockTimes, enough, floor,
814
877
  try {
815
878
  logs = await read.rpc(url, "eth_getLogs", [{ address, fromBlock: hex(lo), toBlock: hex(to), topics }]);
816
879
  } catch (error) {
817
- if (size > MIN_WINDOW && RANGE_REFUSED.test(messageOf(error))) {
818
- if (refused === null || to - lo + 1n < refused) refused = to - lo + 1n;
819
- size = size / 4n > MIN_WINDOW ? size / 4n : MIN_WINDOW;
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;
820
885
  continue;
821
886
  }
822
887
 
@@ -824,7 +889,14 @@ async function searchLogs(read, { address, amountOf, blockTimes, enough, floor,
824
889
  }
825
890
 
826
891
  if (to === head) low = lo;
827
- if (took === null || to - lo + 1n > took) took = to - lo + 1n;
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
+ }
828
900
 
829
901
  for (const log of Array.isArray(logs) ? logs : []) {
830
902
  const found = creditLog(log, { address, amountOf, from: lo, to, topics, wei });
@@ -844,14 +916,15 @@ async function searchLogs(read, { address, amountOf, blockTimes, enough, floor,
844
916
  }
845
917
  }
846
918
 
847
- return { asked, complete: from > head, covered, credits, low, next: from, refused, size, took };
919
+ return { asked, complete: from > head, covered, credits, low, next: from, refused, size };
848
920
  }
849
921
 
850
922
  /**
851
923
  * A search's passes over its blocks again, as a result gave them: where the
852
924
  * one under way is (null between two), when the next begins, how many are
853
- * done, when the search first reached the head, and the widest window it
854
- * reads them in where the RPC refused a wider one; null for anything else.
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.
855
928
  *
856
929
  * @param {unknown} value
857
930
  * @returns {{ at: number, from: bigint | null, passes: number, since: number, size: number | null } | null}
@@ -875,8 +948,8 @@ function recheckOf(value) {
875
948
  * @param {NonNullable<ReturnType<typeof recheckOf>>} recheck
876
949
  * @returns {NonNullable<Scan["recheck"]>}
877
950
  */
878
- function recheckIn({ at, from, passes, since, size }) {
879
- return { at, from: from === null ? null : from.toString(), passes, since, ...(size === null ? {} : { size }) };
951
+ function recheckIn({ at, from, passes, since }) {
952
+ return { at, from: from === null ? null : from.toString(), passes, since };
880
953
  }
881
954
 
882
955
  /**
@@ -892,9 +965,10 @@ function recheckIn({ at, from, passes, since, size }) {
892
965
  * time: the first a minute later, each after as long again as it has been
893
966
  * since then, and one after the other, as far as its credit, while an
894
967
  * arrival is held back. A pass under way takes what the search ahead leaves
895
- * of a call's windows, one at least, in windows no wider than the RPC took
896
- * before. A pass counts once it read the blocks again, as does a call whose
897
- * window at the head reached back to where the search began.
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.
898
972
  *
899
973
  * @param {Reader} read
900
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
@@ -922,10 +996,10 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
922
996
  // The logs searched for: a search for others, as for another sender or
923
997
  // amount, isn't carried on.
924
998
  const search = [filter.address, ...filter.topics.map((topic) => topic ?? "*"), filter.wei.toString()].join(":");
925
- /** @type {{ basis: number, from: string, seen?: Scan["seen"], start?: string, recheck?: Scan["recheck"] }[]} */
999
+ /** @type {{ basis: number, from: string, seen?: Scan["seen"], start?: string, recheck?: Scan["recheck"], size?: number, refused?: number }[]} */
926
1000
  const resumed = live.flatMap(({ scan }) =>
927
1001
  scan?.from !== undefined && scan.search === search && scan.basis !== undefined && scan.basis <= first.at
928
- ? [{ basis: scan.basis, from: scan.from, recheck: scan.recheck, seen: scan.seen, start: scan.start }]
1002
+ ? [{ basis: scan.basis, from: scan.from, recheck: scan.recheck, refused: scan.refused, seen: scan.seen, size: scan.size, start: scan.start }]
929
1003
  : []
930
1004
  );
931
1005
  /** @type {Map<string, Credit>} */
@@ -990,9 +1064,11 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
990
1064
  // No later than a credit found before: the blocks are read again from it.
991
1065
  for (const credit of carried.values()) if (credit.block !== null && credit.block < floor) floor = credit.block;
992
1066
 
993
- // The widest window the blocks are read again in, where the RPC refused a
994
- // wider one.
995
- const width = recheck?.size ? BigInt(recheck.size) : BigInt(LOG_WINDOW);
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);
996
1072
 
997
1073
  /**
998
1074
  * The given transfers' credits with `found`: those that arrived, by their
@@ -1060,7 +1136,8 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
1060
1136
  head,
1061
1137
  known: [...carried.values()],
1062
1138
  maxWindows,
1063
- overlap: width < OVERLAP ? width : OVERLAP,
1139
+ refused: refusals.length ? minOf(refusals) : null,
1140
+ size: width,
1064
1141
  start,
1065
1142
  until,
1066
1143
  url,
@@ -1075,7 +1152,7 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
1075
1152
 
1076
1153
  let credits = mark(ahead.credits);
1077
1154
  const before = settled(credits, seen);
1078
- let { refused, took } = ahead;
1155
+ let { refused, size } = ahead;
1079
1156
 
1080
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 };
1081
1158
 
@@ -1096,7 +1173,8 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
1096
1173
  head: before.top !== null && before.top < below ? before.top : below - 1n,
1097
1174
  known: credits,
1098
1175
  maxWindows: maxWindows < 1 ? 0 : Math.max(1, maxWindows - ahead.asked),
1099
- size: ahead.size < width ? ahead.size : width,
1176
+ refused,
1177
+ size,
1100
1178
  start: from,
1101
1179
  until,
1102
1180
  url,
@@ -1108,18 +1186,7 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
1108
1186
 
1109
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 };
1110
1188
  credits = mark(pass.credits);
1111
- if (pass.refused !== null && (refused === null || pass.refused < refused)) refused = pass.refused;
1112
- if (pass.took !== null && (took === null || pass.took > took)) took = pass.took;
1113
- }
1114
-
1115
- // A window refused: from the next call on, the blocks are read again in
1116
- // ones as wide as the widest it took narrower than that, else a quarter
1117
- // as wide.
1118
- if (recheck !== null && refused !== null) {
1119
- const fits = took !== null && took < refused ? took : refused / 4n;
1120
- const narrower = fits > MIN_WINDOW ? fits : MIN_WINDOW;
1121
-
1122
- if (narrower < width) recheck = { ...recheck, size: Number(narrower) };
1189
+ ({ refused, size } = pass);
1123
1190
  }
1124
1191
 
1125
1192
  const { arrived, assigned, held, verdicts } = settled(credits, seen);
@@ -1138,8 +1205,10 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
1138
1205
  basis: first.at,
1139
1206
  from: next.toString(),
1140
1207
  ...(recheck ? { recheck: recheckIn(recheck) } : {}),
1208
+ ...(refused === null ? {} : { refused: Number(refused) }),
1141
1209
  search,
1142
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),
1143
1212
  start: floor.toString(),
1144
1213
  };
1145
1214
 
@@ -1486,7 +1555,8 @@ async function depositPeers(archive, net, found, { amount, bounds, calls, suppli
1486
1555
  }
1487
1556
 
1488
1557
  /**
1489
- * Where a scan's search stopped, without its history.
1558
+ * Where a scan's search stopped, with the width of its windows, without its
1559
+ * history.
1490
1560
  *
1491
1561
  * @param {Scan | null | undefined} scan
1492
1562
  * @returns {Scan}
@@ -1499,6 +1569,8 @@ function searchOf(scan) {
1499
1569
 
1500
1570
  if (scan.start !== undefined) out.start = scan.start;
1501
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;
1502
1574
 
1503
1575
  return out;
1504
1576
  }
@@ -2072,6 +2144,12 @@ export async function serviceArrivals({
2072
2144
  }
2073
2145
 
2074
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);
2075
2153
  // The fee of each transfer the app gave, those no longer looked for too,
2076
2154
  // and the block its payout can't be before.
2077
2155
  const feeOf = new Map(routed.map((item) => [item.hash, item.fee ?? /** @type {Route & { kind: "service" }} */ (item.route).feeLux]));
@@ -2144,7 +2222,7 @@ export async function serviceArrivals({
2144
2222
  queue: history.queue,
2145
2223
  unsure,
2146
2224
  },
2147
- { 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 }
2148
2226
  );
2149
2227
 
2150
2228
  for (const [hash, arrival] of found) results.set(hash, withHistory(hash, arrival, given, history.states));
@@ -458,6 +458,13 @@ export type WithdrawalScan = {
458
458
  target: number | null;
459
459
  /** The blocks one search asks for. */
460
460
  window: number;
461
+ /**
462
+ * Its passes over the final blocks, read again once they're all covered:
463
+ * the highest block the one under way reads next, downwards (`from`, null
464
+ * between two), when the next one begins (`at`), and when the blocks were
465
+ * first all covered (`since`), in milliseconds.
466
+ */
467
+ recheck?: { from: number | null; at: number; since: number };
461
468
  updatedAt?: number;
462
469
  };
463
470
 
@@ -465,10 +472,17 @@ export type WithdrawalScan = {
465
472
  * One run over an address's withdrawals, from the bridge's own event
466
473
  * (`ETHBridgeInitiated`, by its sender), the newest final blocks first, a
467
474
  * window at a time, narrower where the RPC asks for it, within
468
- * `maxRequests`. Only final blocks count as covered: those above can still
469
- * be reorged away. The next run carries on below and reads only what is
470
- * new above; under a final head lower than the last run's, what was
471
- * covered and found above it is dropped, and read again.
475
+ * `maxRequests`, the head's requests among them. Only final blocks count as
476
+ * covered: those above can still be reorged away. Each run reads the
477
+ * newest 50,000 final blocks, the new ones first, in as many windows as they
478
+ * take though in half its requests at most (while older blocks are still to
479
+ * be read, only once the final head has moved), and carries on below; under
480
+ * a final head lower than the last run's, what was covered and found above
481
+ * it is dropped, and read again. Once all the final blocks are
482
+ * covered, they're read again from the newest down, a pass at a time with
483
+ * the requests a run has left: the first an hour later, each after as long
484
+ * again as it has been since. A withdrawal is found once: one read again
485
+ * isn't found again.
472
486
  */
473
487
  export declare function syncWithdrawals(options: {
474
488
  /** The DuskEVM address that sent them. */