@dusk-network/app-core 0.3.0 → 0.3.2
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 +19 -0
- package/README.md +5 -4
- package/package.json +1 -1
- package/src/bridge.d.ts +46 -10
- package/src/bridge.js +708 -231
- package/src/history.d.ts +2 -0
- package/src/history.js +3 -2
package/src/bridge.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { networkFor, routeForAccount, routes } from "@dusk-network/networks";
|
|
14
14
|
import { isAccount } from "@dusk-network/networks/account";
|
|
15
|
-
import { duskEvmContracts, evmAddressOf } from "@dusk-network/networks/duskevm";
|
|
15
|
+
import { BRIDGE_FUNCTIONS, duskEvmContracts, evmAddressOf } from "@dusk-network/networks/duskevm";
|
|
16
16
|
import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "./history.js";
|
|
17
17
|
|
|
18
18
|
/** @typedef {import("./bridge.js").Arrival} Arrival */
|
|
@@ -21,6 +21,7 @@ import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "
|
|
|
21
21
|
/** @typedef {import("./bridge.js").DepositArrivalOptions} DepositArrivalOptions */
|
|
22
22
|
/** @typedef {import("./bridge.js").FetchLike} FetchLike */
|
|
23
23
|
/** @typedef {import("./bridge.js").HistoryLike} HistoryLike */
|
|
24
|
+
/** @typedef {import("./bridge.js").HistoryScan} HistoryScan */
|
|
24
25
|
/** @typedef {import("./bridge.js").Migration} Migration */
|
|
25
26
|
/** @typedef {import("./bridge.js").MigrationPayout} MigrationPayout */
|
|
26
27
|
/** @typedef {import("./bridge.js").MigrationPayoutOptions} MigrationPayoutOptions */
|
|
@@ -46,9 +47,10 @@ import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "
|
|
|
46
47
|
/**
|
|
47
48
|
* A credit on the other chain: its transaction, its block and time, its
|
|
48
49
|
* amount in wei, whether it went through, and the address it came from as
|
|
49
|
-
* 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`).
|
|
50
52
|
*
|
|
51
|
-
* @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
|
|
52
54
|
*/
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -82,6 +84,17 @@ const PACE_MARGIN = 1.2;
|
|
|
82
84
|
const START_MARGIN = 50n;
|
|
83
85
|
const START_CHECKS = 4;
|
|
84
86
|
const MIN_WINDOW = 50n;
|
|
87
|
+
// How far back from the head a search's last window reaches: those blocks
|
|
88
|
+
// are read again on each call.
|
|
89
|
+
const OVERLAP = 1_000n;
|
|
90
|
+
// How long after a search first reaches the head it reads its blocks again
|
|
91
|
+
// from its first one; each pass after waits as long again as it has been
|
|
92
|
+
// since then, so the waits about double each time.
|
|
93
|
+
const RECHECK_MS = 60_000;
|
|
94
|
+
// How many passes over the blocks before a credit, since it was found, a
|
|
95
|
+
// transfer waits for before it takes the credit where a later equal one,
|
|
96
|
+
// still without its own, could have had it instead.
|
|
97
|
+
const CONFIRM_PASSES = 8;
|
|
85
98
|
// What an RPC says when it won't search that many blocks at once.
|
|
86
99
|
const RANGE_REFUSED = /limit|range|too many|exceed/i;
|
|
87
100
|
const DUSK_HASH = /^[0-9a-f]{64}$/;
|
|
@@ -199,9 +212,42 @@ const msOf = (value) => (typeof value === "number" && Number.isFinite(value) &&
|
|
|
199
212
|
*/
|
|
200
213
|
const heightOf = (value) => (typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null);
|
|
201
214
|
|
|
215
|
+
/**
|
|
216
|
+
* How far a transfer's history is read, as a result gave it; null for
|
|
217
|
+
* anything else.
|
|
218
|
+
*
|
|
219
|
+
* @param {unknown} value
|
|
220
|
+
* @returns {HistoryScan | null}
|
|
221
|
+
*/
|
|
222
|
+
function historyScanOf(value) {
|
|
223
|
+
const read = /** @type {any} */ (value);
|
|
224
|
+
|
|
225
|
+
if (!read || typeof read !== "object" || typeof read.of !== "string" || !Array.isArray(read.found)) return null;
|
|
226
|
+
|
|
227
|
+
const lo = read.lo === null ? null : heightOf(read.lo);
|
|
228
|
+
const next = read.next === null ? null : { from: heightOf(read.next?.from), page: heightOf(read.next?.page), to: heightOf(read.next?.to) };
|
|
229
|
+
|
|
230
|
+
if ((read.lo !== null && lo === null) || (lo === null && next === null)) return null;
|
|
231
|
+
if (next && (next.from === null || next.to === null || next.page === null || next.page < 1 || next.from > next.to)) return null;
|
|
232
|
+
|
|
233
|
+
const found = read.found.flatMap((/** @type {any} */ each) => {
|
|
234
|
+
const hash = duskHashOf(each?.hash);
|
|
235
|
+
const height = heightOf(each?.height);
|
|
236
|
+
const value = each?.value;
|
|
237
|
+
|
|
238
|
+
return hash && height !== null && typeof value === "string" && /^\d{1,20}$/.test(value) ? [{ hash, height, value }] : [];
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// One left out could be equal: it's read afresh.
|
|
242
|
+
if (found.length !== read.found.length) return null;
|
|
243
|
+
|
|
244
|
+
return { found, lo, next: /** @type {HistoryScan["next"]} */ (next), of: read.of };
|
|
245
|
+
}
|
|
246
|
+
|
|
202
247
|
/**
|
|
203
248
|
* Where a search stopped, what it searched for, and the credits it found
|
|
204
|
-
* before,
|
|
249
|
+
* before, and how far the transfer's history is read, as a result gave
|
|
250
|
+
* them; null for anything else.
|
|
205
251
|
*
|
|
206
252
|
* @param {unknown} value
|
|
207
253
|
* @returns {Scan | null}
|
|
@@ -209,19 +255,34 @@ const heightOf = (value) => (typeof value === "number" && Number.isSafeInteger(v
|
|
|
209
255
|
function scanOf(value) {
|
|
210
256
|
const scan = /** @type {any} */ (value);
|
|
211
257
|
const from = quantityOf(scan?.from);
|
|
258
|
+
const history = historyScanOf(scan?.history);
|
|
259
|
+
/** @type {Scan} */
|
|
260
|
+
const out = history ? { history } : {};
|
|
261
|
+
|
|
262
|
+
if (from !== null && typeof scan?.basis === "number" && Number.isFinite(scan.basis) && typeof scan.search === "string") {
|
|
263
|
+
const seen = (Array.isArray(scan.seen) ? scan.seen : []).flatMap((/** @type {any} */ credit) => {
|
|
264
|
+
const hash = evmHashOf(credit?.hash);
|
|
265
|
+
const block = quantityOf(credit?.block);
|
|
266
|
+
const at = msOf(credit?.at);
|
|
267
|
+
const sender = credit?.sender === null ? null : addressOf(credit?.sender);
|
|
268
|
+
// The passes done before it was found: none where it isn't said.
|
|
269
|
+
const pass = Number.isSafeInteger(credit?.pass) && credit.pass >= 0 ? credit.pass : 0;
|
|
270
|
+
|
|
271
|
+
return hash && block !== null && at !== null && sender !== "" ? [{ at, block: block.toString(), hash, pass, sender }] : [];
|
|
272
|
+
});
|
|
273
|
+
// Where it began, and its passes over the blocks again: without them,
|
|
274
|
+
// as a search kept by an earlier version.
|
|
275
|
+
const start = quantityOf(scan.start);
|
|
276
|
+
const recheck = start === null ? null : recheckOf(scan.recheck);
|
|
277
|
+
|
|
278
|
+
Object.assign(out, { basis: scan.basis, from: from.toString(), search: scan.search, seen });
|
|
279
|
+
if (start !== null) out.start = start.toString();
|
|
280
|
+
if (recheck) out.recheck = recheckIn(recheck);
|
|
281
|
+
} else if (!history) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
212
284
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const seen = (Array.isArray(scan.seen) ? scan.seen : []).flatMap((/** @type {any} */ credit) => {
|
|
216
|
-
const hash = evmHashOf(credit?.hash);
|
|
217
|
-
const block = quantityOf(credit?.block);
|
|
218
|
-
const at = msOf(credit?.at);
|
|
219
|
-
const sender = credit?.sender === null ? null : addressOf(credit?.sender);
|
|
220
|
-
|
|
221
|
-
return hash && block !== null && at !== null && sender !== "" ? [{ at, block: block.toString(), hash, sender }] : [];
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
return { basis: scan.basis, from: from.toString(), search: scan.search, seen };
|
|
285
|
+
return out;
|
|
225
286
|
}
|
|
226
287
|
|
|
227
288
|
/**
|
|
@@ -409,6 +470,36 @@ function clustersOf(queue, windowMs) {
|
|
|
409
470
|
*/
|
|
410
471
|
const allOpen = (queue, windowMs, now) => clustersOf(queue, windowMs).every((cluster) => now < cluster[cluster.length - 1].at + windowMs);
|
|
411
472
|
|
|
473
|
+
/**
|
|
474
|
+
* How a group's credits are matched: see `settle`.
|
|
475
|
+
*
|
|
476
|
+
* @typedef {{ guarded: boolean, now: number, slackMs: number, used: ReadonlySet<string>, windowMs: number, rivals?: ReadonlySet<string> }} Rules
|
|
477
|
+
*/
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Whether a credit could pay a transfer, whether or not another one took it:
|
|
481
|
+
* it went through, isn't one the app gave another of its transfers when
|
|
482
|
+
* this one is the app's, comes from none of `rivals`, and is in the
|
|
483
|
+
* transfer's time and at or after the block it can't be before.
|
|
484
|
+
*
|
|
485
|
+
* @param {Queued} item
|
|
486
|
+
* @param {Credit} credit
|
|
487
|
+
* @param {Rules} rules
|
|
488
|
+
*/
|
|
489
|
+
function pays(item, credit, { guarded, rivals = new Set(), slackMs, used, windowMs }) {
|
|
490
|
+
const lo = item.at - (guarded ? windowMs : slackMs);
|
|
491
|
+
const hi = guarded ? item.at + windowMs : Infinity;
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
credit.success &&
|
|
495
|
+
(!item.given || !used.has(credit.hash)) &&
|
|
496
|
+
(credit.from === null || !rivals.has(credit.from)) &&
|
|
497
|
+
credit.at >= lo &&
|
|
498
|
+
credit.at <= hi &&
|
|
499
|
+
(item.notBefore === null || credit.block === null || credit.block >= item.notBefore)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
412
503
|
/**
|
|
413
504
|
* Each transfer of a queue of equal ones, oldest first, takes the first
|
|
414
505
|
* credit that went through after it that no earlier one took: from
|
|
@@ -429,10 +520,11 @@ const allOpen = (queue, windowMs, now) => clustersOf(queue, windowMs).every((clu
|
|
|
429
520
|
*
|
|
430
521
|
* @param {ReadonlyArray<Queued>} queue oldest first
|
|
431
522
|
* @param {ReadonlyArray<Credit>} credits
|
|
432
|
-
* @param {
|
|
523
|
+
* @param {Rules} rules
|
|
433
524
|
* @returns {{ assigned: Map<string, Credit>, verdicts: Map<string, Verdict> }}
|
|
434
525
|
*/
|
|
435
|
-
function settle(queue, credits,
|
|
526
|
+
function settle(queue, credits, rules) {
|
|
527
|
+
const { guarded, now, used, windowMs } = rules;
|
|
436
528
|
const sorted = [...credits].sort(byTime);
|
|
437
529
|
/** @type {Map<string, Credit>} */
|
|
438
530
|
const assigned = new Map();
|
|
@@ -441,18 +533,7 @@ function settle(queue, credits, { guarded, now, rivals = new Set(), slackMs, use
|
|
|
441
533
|
const verdicts = new Map();
|
|
442
534
|
|
|
443
535
|
for (const item of queue) {
|
|
444
|
-
const
|
|
445
|
-
const hi = guarded ? item.at + windowMs : Infinity;
|
|
446
|
-
const credit = sorted.find(
|
|
447
|
-
(c) =>
|
|
448
|
-
c.success &&
|
|
449
|
-
!taken.has(c.hash) &&
|
|
450
|
-
(!item.given || !used.has(c.hash)) &&
|
|
451
|
-
(c.from === null || !rivals.has(c.from)) &&
|
|
452
|
-
c.at >= lo &&
|
|
453
|
-
c.at <= hi &&
|
|
454
|
-
(item.notBefore === null || c.block === null || c.block >= item.notBefore)
|
|
455
|
-
);
|
|
536
|
+
const credit = sorted.find((c) => !taken.has(c.hash) && pays(item, c, rules));
|
|
456
537
|
|
|
457
538
|
if (credit) {
|
|
458
539
|
assigned.set(item.hash, credit);
|
|
@@ -693,32 +774,48 @@ async function startBefore(read, url, atMs, cache) {
|
|
|
693
774
|
|
|
694
775
|
/**
|
|
695
776
|
* The credits asked for from block `start`, a window of blocks at a time, at
|
|
696
|
-
* most `maxWindows`, until
|
|
777
|
+
* most `maxWindows`, until `head`, `enough`, or a window ending after
|
|
697
778
|
* `until` (`covered`): each checked against what was asked, with its block's
|
|
698
779
|
* time. A window the RPC won't search at once is asked again a quarter as
|
|
699
|
-
* wide
|
|
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`
|
|
782
|
+
* reaches back up to `overlap` blocks from it, no wider than `size`, though
|
|
783
|
+
* not under `floor`: those blocks are read again (from `low`). `asked`: the
|
|
784
|
+
* windows asked.
|
|
700
785
|
*
|
|
701
786
|
* @param {Reader} read
|
|
702
|
-
* @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
|
|
703
|
-
* @returns {Promise<{ complete: boolean, covered: boolean, credits: Credit[], next: bigint }>}
|
|
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 }>}
|
|
704
789
|
*/
|
|
705
|
-
async function searchLogs(read, { address, amountOf, blockTimes, enough, head, known, maxWindows, start, topics, until, url, wei }) {
|
|
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 }) {
|
|
706
791
|
// What an earlier search found before `start`.
|
|
707
792
|
/** @type {Credit[]} */
|
|
708
793
|
const credits = [...known];
|
|
709
794
|
let from = start;
|
|
710
|
-
let size =
|
|
795
|
+
let size = width;
|
|
711
796
|
let covered = false;
|
|
712
|
-
|
|
713
|
-
|
|
797
|
+
let asked = 0;
|
|
798
|
+
/** @type {bigint | null} */
|
|
799
|
+
let low = null;
|
|
800
|
+
/** @type {bigint | null} */
|
|
801
|
+
let refused = null;
|
|
802
|
+
/** @type {bigint | null} */
|
|
803
|
+
let took = null;
|
|
804
|
+
|
|
805
|
+
while (asked < maxWindows && from <= head) {
|
|
714
806
|
const to = from + size - 1n < head ? from + size - 1n : head;
|
|
807
|
+
const back = head + 1n - (size < overlap ? size : overlap);
|
|
808
|
+
const lo = to === head && back < from ? (back > floor ? back : floor) : from;
|
|
715
809
|
/** @type {any} */
|
|
716
810
|
let logs;
|
|
717
811
|
|
|
812
|
+
asked += 1;
|
|
813
|
+
|
|
718
814
|
try {
|
|
719
|
-
logs = await read.rpc(url, "eth_getLogs", [{ address, fromBlock: hex(
|
|
815
|
+
logs = await read.rpc(url, "eth_getLogs", [{ address, fromBlock: hex(lo), toBlock: hex(to), topics }]);
|
|
720
816
|
} catch (error) {
|
|
721
817
|
if (size > MIN_WINDOW && RANGE_REFUSED.test(messageOf(error))) {
|
|
818
|
+
if (refused === null || to - lo + 1n < refused) refused = to - lo + 1n;
|
|
722
819
|
size = size / 4n > MIN_WINDOW ? size / 4n : MIN_WINDOW;
|
|
723
820
|
continue;
|
|
724
821
|
}
|
|
@@ -726,8 +823,11 @@ async function searchLogs(read, { address, amountOf, blockTimes, enough, head, k
|
|
|
726
823
|
throw error;
|
|
727
824
|
}
|
|
728
825
|
|
|
826
|
+
if (to === head) low = lo;
|
|
827
|
+
if (took === null || to - lo + 1n > took) took = to - lo + 1n;
|
|
828
|
+
|
|
729
829
|
for (const log of Array.isArray(logs) ? logs : []) {
|
|
730
|
-
const found = creditLog(log, { address, amountOf, from, to, topics, wei });
|
|
830
|
+
const found = creditLog(log, { address, amountOf, from: lo, to, topics, wei });
|
|
731
831
|
|
|
732
832
|
if (!found || credits.some((credit) => credit.hash === found.hash)) continue;
|
|
733
833
|
if (found.at !== null) blockTimes?.set(Number(found.block), found.at);
|
|
@@ -744,7 +844,39 @@ async function searchLogs(read, { address, amountOf, blockTimes, enough, head, k
|
|
|
744
844
|
}
|
|
745
845
|
}
|
|
746
846
|
|
|
747
|
-
return { complete: from > head, covered, credits, next: from };
|
|
847
|
+
return { asked, complete: from > head, covered, credits, low, next: from, refused, size, took };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* A search's passes over its blocks again, as a result gave them: where the
|
|
852
|
+
* 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.
|
|
855
|
+
*
|
|
856
|
+
* @param {unknown} value
|
|
857
|
+
* @returns {{ at: number, from: bigint | null, passes: number, since: number, size: number | null } | null}
|
|
858
|
+
*/
|
|
859
|
+
function recheckOf(value) {
|
|
860
|
+
const kept = /** @type {any} */ (value);
|
|
861
|
+
const from = kept?.from === null ? null : quantityOf(kept?.from);
|
|
862
|
+
/** @param {unknown} ms */
|
|
863
|
+
const time = (ms) => typeof ms === "number" && Number.isFinite(ms);
|
|
864
|
+
const size = Number.isSafeInteger(kept?.size) && kept.size >= Number(MIN_WINDOW) && kept.size <= LOG_WINDOW ? kept.size : null;
|
|
865
|
+
|
|
866
|
+
if (!kept || !time(kept.at) || !time(kept.since) || !Number.isSafeInteger(kept.passes) || kept.passes < 0) return null;
|
|
867
|
+
if (kept.from !== null && from === null) return null;
|
|
868
|
+
|
|
869
|
+
return { at: kept.at, from, passes: kept.passes, since: kept.since, size };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* A search's passes over its blocks again, as its scan keeps them.
|
|
874
|
+
*
|
|
875
|
+
* @param {NonNullable<ReturnType<typeof recheckOf>>} recheck
|
|
876
|
+
* @returns {NonNullable<Scan["recheck"]>}
|
|
877
|
+
*/
|
|
878
|
+
function recheckIn({ at, from, passes, since, size }) {
|
|
879
|
+
return { at, from: from === null ? null : from.toString(), passes, since, ...(size === null ? {} : { size }) };
|
|
748
880
|
}
|
|
749
881
|
|
|
750
882
|
/**
|
|
@@ -754,6 +886,16 @@ async function searchLogs(read, { address, amountOf, blockTimes, enough, head, k
|
|
|
754
886
|
* sent at (as the app saw the chain's head then), else from about when it
|
|
755
887
|
* was sent.
|
|
756
888
|
*
|
|
889
|
+
* Each block is read more than once: on each call, those up to `OVERLAP`
|
|
890
|
+
* under the head, and once the search has reached the head with a transfer
|
|
891
|
+
* not arrived, all of them again from where the search began, a pass at a
|
|
892
|
+
* time: the first a minute later, each after as long again as it has been
|
|
893
|
+
* since then, and one after the other, as far as its credit, while an
|
|
894
|
+
* 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.
|
|
898
|
+
*
|
|
757
899
|
* @param {Reader} read
|
|
758
900
|
* @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
|
|
759
901
|
* @param {{ url: string, maxWindows: number, blockTimes: BlockTimes | undefined, now: number, slackMs: number, windowMs: number, used: Set<string> }} options
|
|
@@ -780,7 +922,12 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
780
922
|
// The logs searched for: a search for others, as for another sender or
|
|
781
923
|
// amount, isn't carried on.
|
|
782
924
|
const search = [filter.address, ...filter.topics.map((topic) => topic ?? "*"), filter.wei.toString()].join(":");
|
|
783
|
-
|
|
925
|
+
/** @type {{ basis: number, from: string, seen?: Scan["seen"], start?: string, recheck?: Scan["recheck"] }[]} */
|
|
926
|
+
const resumed = live.flatMap(({ scan }) =>
|
|
927
|
+
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 }]
|
|
929
|
+
: []
|
|
930
|
+
);
|
|
784
931
|
/** @type {Map<string, Credit>} */
|
|
785
932
|
const carried = new Map();
|
|
786
933
|
|
|
@@ -788,7 +935,7 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
788
935
|
// counts again, as the transfer it paid may be among the queue's.
|
|
789
936
|
for (const scan of resumed) {
|
|
790
937
|
for (const credit of scan.seen ?? []) {
|
|
791
|
-
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 });
|
|
938
|
+
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 });
|
|
792
939
|
}
|
|
793
940
|
}
|
|
794
941
|
|
|
@@ -796,43 +943,193 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
796
943
|
let head;
|
|
797
944
|
/** @type {bigint} */
|
|
798
945
|
let start;
|
|
946
|
+
// The block the search began at, and its passes over the blocks before.
|
|
947
|
+
/** @type {bigint} */
|
|
948
|
+
let floor;
|
|
949
|
+
/** @type {ReturnType<typeof recheckOf>} */
|
|
950
|
+
let recheck = null;
|
|
799
951
|
|
|
800
|
-
if (resumed.length) {
|
|
801
|
-
|
|
802
|
-
|
|
952
|
+
if (resumed.length && resumed.every((scan) => scan.basis === first.at)) {
|
|
953
|
+
const kept = resumed.reduce((a, b) => (BigInt(b.from) < BigInt(a.from) ? b : a));
|
|
954
|
+
|
|
955
|
+
start = BigInt(kept.from);
|
|
956
|
+
recheck = recheckOf(kept.recheck);
|
|
957
|
+
|
|
958
|
+
if (resumed.every((scan) => scan.start !== undefined)) {
|
|
959
|
+
floor = minOf(resumed.map((scan) => BigInt(/** @type {string} */ (scan.start))));
|
|
960
|
+
head = await headOf(read, url);
|
|
961
|
+
} else {
|
|
962
|
+
// Kept without where it began: from where a search from the first
|
|
963
|
+
// begins.
|
|
964
|
+
const fresh = first.notBefore !== null ? { head: await headOf(read, url), start: first.notBefore } : await startBefore(read, url, reach, blockTimes);
|
|
965
|
+
|
|
966
|
+
floor = fresh.start;
|
|
967
|
+
head = fresh.head;
|
|
968
|
+
}
|
|
969
|
+
} else if (resumed.length) {
|
|
970
|
+
// Begun from a transfer before the first now, as one that since
|
|
971
|
+
// arrived: none of the queue's credits is before where a search from
|
|
972
|
+
// the first starts, which it skips to.
|
|
973
|
+
const from = minOf(resumed.map((scan) => BigInt(scan.from)));
|
|
974
|
+
const fresh = first.notBefore !== null ? { head: await headOf(read, url), start: first.notBefore } : await startBefore(read, url, reach, blockTimes);
|
|
975
|
+
|
|
976
|
+
head = fresh.head;
|
|
977
|
+
start = from > fresh.start ? from : fresh.start;
|
|
978
|
+
floor = fresh.start;
|
|
803
979
|
} else if (first.notBefore !== null) {
|
|
804
980
|
start = first.notBefore;
|
|
981
|
+
floor = start;
|
|
805
982
|
head = await headOf(read, url);
|
|
806
983
|
} else {
|
|
807
984
|
({ head, start } = await startBefore(read, url, reach, blockTimes));
|
|
985
|
+
floor = start;
|
|
808
986
|
}
|
|
809
987
|
|
|
988
|
+
if (floor > start) floor = start;
|
|
989
|
+
|
|
990
|
+
// No later than a credit found before: the blocks are read again from it.
|
|
991
|
+
for (const credit of carried.values()) if (credit.block !== null && credit.block < floor) floor = credit.block;
|
|
992
|
+
|
|
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);
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* The given transfers' credits with `found`: those that arrived, by their
|
|
999
|
+
* credits, and those held back (`held`), with the last of their credits'
|
|
1000
|
+
* blocks (`top`). One a later transfer of the queue, still without a
|
|
1001
|
+
* credit, could have had the credit of instead is held back until the
|
|
1002
|
+
* blocks before it are read again `CONFIRM_PASSES` times since it was
|
|
1003
|
+
* found: its own may yet be found there.
|
|
1004
|
+
*
|
|
1005
|
+
* @param {Credit[]} found
|
|
1006
|
+
* @param {boolean} seen
|
|
1007
|
+
*/
|
|
1008
|
+
const settled = (found, seen) => {
|
|
1009
|
+
const { assigned, verdicts } = settle(queue, found, rules);
|
|
1010
|
+
const passes = recheck?.passes ?? 0;
|
|
1011
|
+
const mine = new Set(live.map((item) => item.hash));
|
|
1012
|
+
/** @type {Set<string>} */
|
|
1013
|
+
const arrived = new Set();
|
|
1014
|
+
/** @type {Set<string>} */
|
|
1015
|
+
const held = new Set();
|
|
1016
|
+
/** @type {bigint | null} */
|
|
1017
|
+
let top = null;
|
|
1018
|
+
|
|
1019
|
+
queue.forEach((item, i) => {
|
|
1020
|
+
const credit = assigned.get(item.hash);
|
|
1021
|
+
|
|
1022
|
+
if (!credit || !mine.has(item.hash) || verdicts.get(item.hash) !== "arrived" || !(seen || !guarded)) return;
|
|
1023
|
+
|
|
1024
|
+
// A later one without a credit could have had this one instead.
|
|
1025
|
+
const doubt = !guarded && passes - (credit.pass ?? 0) < CONFIRM_PASSES && queue.slice(i + 1).some((later) => !assigned.has(later.hash) && pays(later, credit, rules));
|
|
1026
|
+
|
|
1027
|
+
if (!doubt) {
|
|
1028
|
+
arrived.add(credit.hash);
|
|
1029
|
+
} else {
|
|
1030
|
+
held.add(item.hash);
|
|
1031
|
+
if (credit.block !== null && (top === null || credit.block > top)) top = credit.block;
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
return { arrived, assigned, held, top, verdicts };
|
|
1036
|
+
};
|
|
1037
|
+
/**
|
|
1038
|
+
* The credits found in this call, with the passes done before the next
|
|
1039
|
+
* one to begin.
|
|
1040
|
+
*
|
|
1041
|
+
* @param {Credit[]} found
|
|
1042
|
+
* @returns {Credit[]}
|
|
1043
|
+
*/
|
|
1044
|
+
const mark = (found) => {
|
|
1045
|
+
const pass = recheck === null ? 0 : recheck.passes + (recheck.from !== null ? 1 : 0);
|
|
1046
|
+
|
|
1047
|
+
return found.map((credit) => (credit.pass === undefined ? { ...credit, pass } : credit));
|
|
1048
|
+
};
|
|
810
1049
|
// Where the credits can't be told apart, only those in the transfers'
|
|
811
1050
|
// windows count: once they're all over, read no further than the last.
|
|
812
1051
|
const last = queue[queue.length - 1].at + windowMs;
|
|
813
|
-
const
|
|
1052
|
+
const until = guarded && now >= last ? last : null;
|
|
1053
|
+
const due = recheck !== null && (recheck.from !== null || now >= recheck.at);
|
|
1054
|
+
const ahead = await searchLogs(read, {
|
|
814
1055
|
...filter,
|
|
815
1056
|
blockTimes,
|
|
816
1057
|
// Once every transfer of the queue has its credit: read no further.
|
|
817
1058
|
enough: (found) => !guarded && settle(queue, found, rules).assigned.size === queue.length,
|
|
1059
|
+
floor,
|
|
818
1060
|
head,
|
|
819
1061
|
known: [...carried.values()],
|
|
820
1062
|
maxWindows,
|
|
1063
|
+
overlap: width < OVERLAP ? width : OVERLAP,
|
|
821
1064
|
start,
|
|
822
|
-
until
|
|
1065
|
+
until,
|
|
823
1066
|
url,
|
|
824
1067
|
});
|
|
1068
|
+
const { complete, covered, next } = ahead;
|
|
825
1069
|
const seen = complete || covered;
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const
|
|
1070
|
+
// Its window at the head reached back to where the search began: every
|
|
1071
|
+
// block read before this call is read again, as by a pass.
|
|
1072
|
+
const again = recheck !== null && ahead.low !== null && ahead.low <= floor && start > floor;
|
|
1073
|
+
|
|
1074
|
+
if (again && recheck !== null) recheck = { ...recheck, passes: recheck.passes + 1 };
|
|
1075
|
+
|
|
1076
|
+
let credits = mark(ahead.credits);
|
|
1077
|
+
const before = settled(credits, seen);
|
|
1078
|
+
let { refused, took } = ahead;
|
|
1079
|
+
|
|
1080
|
+
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
|
+
|
|
1082
|
+
// A transfer not arrived yet: the blocks read before this call, again, as
|
|
1083
|
+
// far as the search ahead reads; while an arrival is held back, as far as
|
|
1084
|
+
// its credit. With what the search ahead leaves of a call's windows, one
|
|
1085
|
+
// at least.
|
|
1086
|
+
if (recheck !== null && (due || before.held.size) && before.arrived.size < live.length) {
|
|
1087
|
+
const from = recheck.from !== null && recheck.from > floor ? recheck.from : floor;
|
|
1088
|
+
const below = ahead.low !== null && ahead.low < start ? ahead.low : start;
|
|
1089
|
+
const pass = await searchLogs(read, {
|
|
1090
|
+
...filter,
|
|
1091
|
+
blockTimes,
|
|
1092
|
+
// Read through: a credit further on, still before those found ahead,
|
|
1093
|
+
// could be a transfer's first.
|
|
1094
|
+
enough: () => false,
|
|
1095
|
+
floor: from,
|
|
1096
|
+
head: before.top !== null && before.top < below ? before.top : below - 1n,
|
|
1097
|
+
known: credits,
|
|
1098
|
+
maxWindows: maxWindows < 1 ? 0 : Math.max(1, maxWindows - ahead.asked),
|
|
1099
|
+
size: ahead.size < width ? ahead.size : width,
|
|
1100
|
+
start: from,
|
|
1101
|
+
until,
|
|
1102
|
+
url,
|
|
1103
|
+
});
|
|
1104
|
+
const done = pass.complete || pass.covered;
|
|
1105
|
+
// A pass that read no block is none: only one that read the blocks again
|
|
1106
|
+
// counts, and not twice in a call.
|
|
1107
|
+
const counted = done && !again && pass.next > floor;
|
|
1108
|
+
|
|
1109
|
+
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
|
+
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
|
+
}
|
|
829
1114
|
|
|
830
|
-
|
|
831
|
-
|
|
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;
|
|
832
1121
|
|
|
833
|
-
if (
|
|
1122
|
+
if (narrower < width) recheck = { ...recheck, size: Number(narrower) };
|
|
834
1123
|
}
|
|
835
1124
|
|
|
1125
|
+
const { arrived, assigned, held, verdicts } = settled(credits, seen);
|
|
1126
|
+
|
|
1127
|
+
// Passes go on one after the other while an arrival is held back; once one
|
|
1128
|
+
// arrives with others still waiting, theirs begin again a minute later,
|
|
1129
|
+
// the waits growing from there.
|
|
1130
|
+
if (recheck !== null && held.size) recheck = { ...recheck, at: Math.min(recheck.at, now) };
|
|
1131
|
+
else if (recheck !== null && arrived.size && arrived.size < live.length) recheck = { ...recheck, at: Math.min(recheck.at, now + RECHECK_MS), since: now };
|
|
1132
|
+
|
|
836
1133
|
// The next search carries on from where this one stopped, with the
|
|
837
1134
|
// credits found so far: a transfer that arrived may come back from the
|
|
838
1135
|
// chain among those before the next one, and takes its own again.
|
|
@@ -840,15 +1137,19 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
840
1137
|
const scan = {
|
|
841
1138
|
basis: first.at,
|
|
842
1139
|
from: next.toString(),
|
|
1140
|
+
...(recheck ? { recheck: recheckIn(recheck) } : {}),
|
|
843
1141
|
search,
|
|
844
|
-
seen: credits.flatMap((credit) => (credit.at >= reach && credit.block !== null ? [{ at: credit.at, block: credit.block.toString(), hash: credit.hash, sender: credit.from }] : [])),
|
|
1142
|
+
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 }] : [])),
|
|
1143
|
+
start: floor.toString(),
|
|
845
1144
|
};
|
|
846
1145
|
|
|
847
1146
|
for (const item of live) {
|
|
848
1147
|
const credit = assigned.get(item.hash);
|
|
849
1148
|
const verdict = verdicts.get(item.hash);
|
|
850
1149
|
|
|
851
|
-
if (
|
|
1150
|
+
if (held.has(item.hash)) {
|
|
1151
|
+
out.set(item.hash, { scan, state: "waiting" });
|
|
1152
|
+
} else if (credit && arrived.has(credit.hash)) {
|
|
852
1153
|
used.add(credit.hash);
|
|
853
1154
|
out.set(item.hash, {
|
|
854
1155
|
amount: credit.wei / WEI_PER_LUX,
|
|
@@ -1016,50 +1317,224 @@ async function detailsOf(archive, hashes, { args = false, network }, counter) {
|
|
|
1016
1317
|
}
|
|
1017
1318
|
|
|
1018
1319
|
/**
|
|
1019
|
-
* A
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
|
|
1320
|
+
* A transfer whose history is read: its hash, its Dusk block's time and
|
|
1321
|
+
* height, the block on the other chain its credit can't be before, and its
|
|
1322
|
+
* scan.
|
|
1323
|
+
*
|
|
1324
|
+
* @typedef {{ hash: string, at: number, height: number, notBefore: bigint | null, scan: Scan | null }} Reading
|
|
1325
|
+
*/
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* The history a group of equal transfers needs, each transfer's read on
|
|
1329
|
+
* from where its scan stopped: its account's history from `reach` blocks
|
|
1330
|
+
* under its block up to it, and `reach` further down at a time while the
|
|
1331
|
+
* run of equal transfers it is in reaches within `horizonMs` of the bottom.
|
|
1332
|
+
* The transfer itself must be listed there: until it is, the archive
|
|
1333
|
+
* hasn't read its block. Each transfer reads `pages` pages at most, so a
|
|
1334
|
+
* later one's history never keeps an earlier one waiting.
|
|
1335
|
+
*
|
|
1336
|
+
* Those whose history is all read are `covered`, and `given` as the queue
|
|
1337
|
+
* holds them, with `queue`: theirs and the equal transfers found
|
|
1338
|
+
* (`peersOf`), and `unread`: by transfer, the heights it read of those that
|
|
1339
|
+
* could be equal but can't be read. Each one's reading, done or not, is in
|
|
1340
|
+
* `states`.
|
|
1341
|
+
*
|
|
1342
|
+
* @param {Archive} archive
|
|
1343
|
+
* @param {ReadonlyArray<Reading>} items
|
|
1344
|
+
* @param {{ account: string, direction: "in" | "out", horizonMs: number, reach: number, pages: number, keep: (entry: HistoryEntry) => boolean, of: (item: Reading) => string, peersOf: (found: HistoryScan["found"]) => Promise<{ peers: Queued[], unread: ReadonlyArray<{ hash: string, height: number }> }> }} read
|
|
1345
|
+
* @param {{ requests: number }} counter
|
|
1346
|
+
*/
|
|
1347
|
+
async function readHistories(archive, items, { account, direction, horizonMs, keep, of, pages, peersOf, reach }, counter) {
|
|
1348
|
+
const walks = items.map((item) => {
|
|
1349
|
+
const kept = item.scan?.history;
|
|
1350
|
+
/** @type {HistoryScan} */
|
|
1351
|
+
const state =
|
|
1352
|
+
kept && kept.of === of(item)
|
|
1353
|
+
? structuredClone(/** @type {HistoryScan} */ (kept))
|
|
1354
|
+
: { found: [], lo: null, next: { from: Math.max(0, item.height - reach), page: 1, to: item.height }, of: of(item) };
|
|
1355
|
+
|
|
1356
|
+
return { item, left: pages, state };
|
|
1357
|
+
});
|
|
1358
|
+
/** @type {Reading[]} */
|
|
1359
|
+
let covered = [];
|
|
1360
|
+
/** @type {Queued[]} */
|
|
1361
|
+
let given = [];
|
|
1362
|
+
/** @type {Queued[]} */
|
|
1363
|
+
let queue = [];
|
|
1364
|
+
/** @type {Map<string, number[]>} */
|
|
1365
|
+
const unread = new Map();
|
|
1366
|
+
|
|
1367
|
+
for (;;) {
|
|
1368
|
+
// Each one's next range, as far as its pages go.
|
|
1369
|
+
for (const walk of walks) {
|
|
1370
|
+
const { state } = walk;
|
|
1371
|
+
|
|
1372
|
+
while (state.next && walk.left > 0) {
|
|
1373
|
+
const next = /** @type {NonNullable<HistoryScan["next"]>} */ (state.next);
|
|
1374
|
+
const range = await historyRange(archive, { account, direction, from: next.from, page: next.page, pages: walk.left, to: next.to });
|
|
1375
|
+
const found = /** @type {HistoryScan["found"][number][]} */ (state.found);
|
|
1376
|
+
|
|
1377
|
+
counter.requests += range.requests;
|
|
1378
|
+
walk.left -= range.requests;
|
|
1379
|
+
|
|
1380
|
+
for (const entry of range.entries) {
|
|
1381
|
+
if (keep(entry) && !found.some((each) => each.hash === entry.hash)) found.push({ hash: entry.hash, height: entry.blockHeight, value: String(entry.value) });
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (!range.complete) {
|
|
1385
|
+
state.next = { ...next, page: next.page + range.requests };
|
|
1386
|
+
} else if (state.lo === null && !found.some((each) => each.hash === walk.item.hash)) {
|
|
1387
|
+
// Its own block isn't read yet: its last page is read again.
|
|
1388
|
+
state.next = { ...next, page: next.page + range.requests - 1 };
|
|
1389
|
+
walk.left = 0;
|
|
1390
|
+
} else {
|
|
1391
|
+
state.lo = next.from;
|
|
1392
|
+
state.next = null;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const done = walks.filter((walk) => !walk.state.next);
|
|
1398
|
+
|
|
1399
|
+
if (!done.length) {
|
|
1400
|
+
covered = [];
|
|
1401
|
+
given = [];
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
given = done.map((walk) => queued(walk.item)).sort(byTime);
|
|
1406
|
+
|
|
1407
|
+
const mine = new Set(given.map((item) => item.hash));
|
|
1408
|
+
// The others found: those given are in the queue as they are.
|
|
1409
|
+
const { peers, unread: unknown } = await peersOf(uniqueByHash(done.flatMap((walk) => walk.state.found)).filter((entry) => !mine.has(entry.hash)));
|
|
1410
|
+
let deeper = false;
|
|
1411
|
+
|
|
1412
|
+
queue = queueOf(given, peers, horizonMs);
|
|
1413
|
+
|
|
1414
|
+
// A run reaching within a horizon of the bottom may go on under it: the
|
|
1415
|
+
// history is read further down.
|
|
1416
|
+
for (const walk of done) {
|
|
1417
|
+
const lo = /** @type {number} */ (walk.state.lo);
|
|
1418
|
+
const bottomAt = lo === 0 ? -Infinity : walk.item.at - (walk.item.height - lo) * DUSK_BLOCK_MS;
|
|
1419
|
+
const self = /** @type {Queued} */ (given.find((each) => each.hash === walk.item.hash));
|
|
1420
|
+
|
|
1421
|
+
if (runStart(queue, self, horizonMs).at - bottomAt < horizonMs) {
|
|
1422
|
+
walk.state.next = { from: Math.max(0, lo - reach), page: 1, to: lo - 1 };
|
|
1423
|
+
deeper = true;
|
|
1424
|
+
} else {
|
|
1425
|
+
unread.set(
|
|
1426
|
+
walk.item.hash,
|
|
1427
|
+
unknown.flatMap((each) => (each.height >= lo && each.height <= walk.item.height ? [each.height] : []))
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (!deeper) {
|
|
1433
|
+
covered = done.map((walk) => walk.item);
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
return { covered, given, queue, states: new Map(walks.map((walk) => [walk.item.hash, walk.state])), unread };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Of the deposits a sender's history lists, those equal to the group's
|
|
1443
|
+
* (`to`, `amount`), from their calls: each with the block the app gave it
|
|
1444
|
+
* its credit can't be before; and those whose call or time the archive
|
|
1445
|
+
* doesn't give, which could be any deposit of up to what they paid, but
|
|
1446
|
+
* for one the app gave: its own facts tell. `calls` keeps each call read.
|
|
1025
1447
|
*
|
|
1026
1448
|
* @param {Archive} archive
|
|
1027
1449
|
* @param {Network} net
|
|
1028
|
-
* @param {{
|
|
1450
|
+
* @param {ReadonlyArray<{ hash: string, height: number }>} found
|
|
1451
|
+
* @param {{ amount: bigint, to: string, bounds: ReadonlyMap<string, bigint | null>, calls: Map<string, any>, supplied: ReadonlyMap<string, DepositItem> }} group
|
|
1029
1452
|
* @param {{ requests: number }} counter
|
|
1030
1453
|
*/
|
|
1031
|
-
async function
|
|
1032
|
-
const
|
|
1033
|
-
const read = await
|
|
1454
|
+
async function depositPeers(archive, net, found, { amount, bounds, calls, supplied, to }, counter) {
|
|
1455
|
+
const missing = found.flatMap((entry) => (calls.has(entry.hash) ? [] : [entry.hash]));
|
|
1456
|
+
const read = await detailsOf(archive, missing, { args: true, network: net }, counter);
|
|
1034
1457
|
|
|
1035
|
-
|
|
1458
|
+
for (const hash of missing) calls.set(hash, read.get(hash) ?? null);
|
|
1036
1459
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1460
|
+
/** @type {Queued[]} */
|
|
1461
|
+
const peers = [];
|
|
1462
|
+
/** @type {{ hash: string, height: number }[]} */
|
|
1463
|
+
const unread = [];
|
|
1039
1464
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const deposits = [];
|
|
1044
|
-
/** @type {{ hash: string, height: number, value: bigint }[]} */
|
|
1045
|
-
const unresolved = [];
|
|
1465
|
+
for (const entry of found) {
|
|
1466
|
+
const call = calls.get(entry.hash);
|
|
1467
|
+
const bridged = call?.bridge;
|
|
1046
1468
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1469
|
+
// Failed, or a call that isn't a deposit: no DUSK left for DuskEVM. A
|
|
1470
|
+
// deposit whose arguments can't be read could be any.
|
|
1471
|
+
if (call?.success === false || (bridged && bridged.kind !== "deposit") || (bridged === null && call?.fnName !== BRIDGE_FUNCTIONS.deposit)) continue;
|
|
1050
1472
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1473
|
+
const own = supplied.get(entry.hash);
|
|
1474
|
+
let at = msOf(call?.timestamp);
|
|
1475
|
+
let paid = luxOf(bridged?.amount);
|
|
1476
|
+
let recipient = addressOf(bridged?.to);
|
|
1053
1477
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const recipient = addressOf(bridged?.to);
|
|
1478
|
+
// One the app gave whose call can't be read: its own facts.
|
|
1479
|
+
if (own && (!call || at === null || paid === null || !recipient)) ({ amount: paid, at, to: recipient } = own);
|
|
1057
1480
|
|
|
1058
|
-
if (
|
|
1059
|
-
else
|
|
1481
|
+
if (at === null || paid === null || !recipient) unread.push(entry);
|
|
1482
|
+
else if (recipient === to && paid === amount) peers.push({ at, given: false, hash: entry.hash, notBefore: bounds.get(entry.hash) ?? null, scan: null });
|
|
1060
1483
|
}
|
|
1061
1484
|
|
|
1062
|
-
return {
|
|
1485
|
+
return { peers, unread };
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Where a scan's search stopped, without its history.
|
|
1490
|
+
*
|
|
1491
|
+
* @param {Scan | null | undefined} scan
|
|
1492
|
+
* @returns {Scan}
|
|
1493
|
+
*/
|
|
1494
|
+
function searchOf(scan) {
|
|
1495
|
+
if (scan?.from === undefined) return {};
|
|
1496
|
+
|
|
1497
|
+
/** @type {Scan} */
|
|
1498
|
+
const out = { basis: scan.basis, from: scan.from, search: scan.search, seen: scan.seen };
|
|
1499
|
+
|
|
1500
|
+
if (scan.start !== undefined) out.start = scan.start;
|
|
1501
|
+
if (scan.recheck !== undefined) out.recheck = scan.recheck;
|
|
1502
|
+
|
|
1503
|
+
return out;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* The transfers whose history isn't all read yet wait, with where their
|
|
1508
|
+
* reading stopped beside where their last search did.
|
|
1509
|
+
*
|
|
1510
|
+
* @param {ReadonlyArray<{ hash: string, scan: Scan | null }>} items
|
|
1511
|
+
* @param {{ covered: ReadonlyArray<{ hash: string }>, states: ReadonlyMap<string, HistoryScan> }} history
|
|
1512
|
+
* @param {Map<string, Arrival>} results
|
|
1513
|
+
*/
|
|
1514
|
+
function waitForHistory(items, { covered, states }, results) {
|
|
1515
|
+
const read = new Set(covered.map((item) => item.hash));
|
|
1516
|
+
|
|
1517
|
+
for (const item of items) {
|
|
1518
|
+
if (!read.has(item.hash)) results.set(item.hash, { scan: { ...searchOf(item.scan), history: /** @type {HistoryScan} */ (states.get(item.hash)) }, state: "waiting" });
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* A result, with how far its transfer's history is read, which every one
|
|
1524
|
+
* but an arrival keeps in its scan for the next call.
|
|
1525
|
+
*
|
|
1526
|
+
* @param {string} hash
|
|
1527
|
+
* @param {Arrival} arrival
|
|
1528
|
+
* @param {ReadonlyArray<Queued>} given
|
|
1529
|
+
* @param {ReadonlyMap<string, HistoryScan>} states
|
|
1530
|
+
* @returns {Arrival}
|
|
1531
|
+
*/
|
|
1532
|
+
function withHistory(hash, arrival, given, states) {
|
|
1533
|
+
const history = states.get(hash);
|
|
1534
|
+
|
|
1535
|
+
if (!history || arrival.state === "arrived") return arrival;
|
|
1536
|
+
|
|
1537
|
+
return { ...arrival, scan: { ...(arrival.scan ?? searchOf(given.find((each) => each.hash === hash)?.scan)), history } };
|
|
1063
1538
|
}
|
|
1064
1539
|
|
|
1065
1540
|
// ---------------------------------------------------------------------------
|
|
@@ -1380,34 +1855,6 @@ async function onExplorer(read, live, { api, bridge, maxPages, now, past, result
|
|
|
1380
1855
|
* @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
|
|
1381
1856
|
*/
|
|
1382
1857
|
async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows, net, now, past, results, url, used, windowMs }) {
|
|
1383
|
-
/** @type {Map<string, Awaited<ReturnType<typeof senderDeposits>> & { bottom: number, top: number, topAt: number }>} */
|
|
1384
|
-
const histories = new Map();
|
|
1385
|
-
|
|
1386
|
-
// The senders' deposits shortly before theirs, one history read a sender:
|
|
1387
|
-
// the app's deposits no longer looked for among them.
|
|
1388
|
-
if (archive) {
|
|
1389
|
-
const reach = Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS);
|
|
1390
|
-
|
|
1391
|
-
for (const [sender, items] of groupBy(
|
|
1392
|
-
live.filter((item) => item.sender !== null && item.height !== null),
|
|
1393
|
-
(item) => /** @type {string} */ (item.sender)
|
|
1394
|
-
)) {
|
|
1395
|
-
const heights = items.map((item) => /** @type {number} */ (item.height));
|
|
1396
|
-
const top = Math.max(...heights);
|
|
1397
|
-
const bottom = Math.max(0, Math.min(...heights) - reach);
|
|
1398
|
-
const least = items.reduce((a, item) => (item.amount < a ? item.amount : a), MAX_U64);
|
|
1399
|
-
|
|
1400
|
-
try {
|
|
1401
|
-
const found = await senderDeposits(archive, net, { from: bottom, least, sender, skip: new Set(items.map((item) => item.hash)), to: top }, read.counter);
|
|
1402
|
-
const topAt = /** @type {number} */ (items.find((item) => item.height === top)?.at);
|
|
1403
|
-
|
|
1404
|
-
histories.set(sender, { ...found, bottom, top, topAt });
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
1858
|
// Equal deposits (the same DuskEVM address and amount) from a known sender
|
|
1412
1859
|
// first, a sender at a time, its events telling its credits apart; then
|
|
1413
1860
|
// those whose sender isn't known, which take none from the senders of the
|
|
@@ -1438,46 +1885,69 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
|
|
|
1438
1885
|
if (rest.length) unknown.push({ items: rest, rivals: aliases.get(key) ?? new Set() });
|
|
1439
1886
|
}
|
|
1440
1887
|
|
|
1441
|
-
// The block each deposit given can't be credited before, the app's own
|
|
1888
|
+
// The block each deposit given can't be credited before, the app's own,
|
|
1889
|
+
// the deposits given, and the deposits' calls, read once.
|
|
1442
1890
|
const bounds = new Map(list.map((item) => [item.hash, item.notBefore]));
|
|
1891
|
+
const supplied = new Map(list.map((item) => [item.hash, item]));
|
|
1892
|
+
/** @type {Map<string, any>} */
|
|
1893
|
+
const calls = new Map();
|
|
1894
|
+
const contract = duskEvmContracts(net)?.bridge;
|
|
1443
1895
|
|
|
1444
1896
|
for (const { items, rivals } of [...known, ...unknown]) {
|
|
1445
1897
|
const { amount, sender, to } = items[0];
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
? /** @type {NonNullable<typeof history>} */ (history).deposits
|
|
1451
|
-
.filter((deposit) => deposit.to === to && deposit.amount === amount)
|
|
1452
|
-
.map((deposit) => ({ at: deposit.at, given: false, hash: deposit.hash, notBefore: bounds.get(deposit.hash) ?? null, scan: null }))
|
|
1453
|
-
: [];
|
|
1454
|
-
const queue = vouched
|
|
1455
|
-
? queueOf(given, alike, DEPOSIT_HORIZON_MS)
|
|
1456
|
-
: withPast(
|
|
1457
|
-
given,
|
|
1458
|
-
past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
|
|
1459
|
-
windowMs
|
|
1460
|
-
);
|
|
1898
|
+
const vouched = Boolean(archive) && sender !== null && items.every((item) => item.height !== null);
|
|
1899
|
+
let given = items.map(queued).sort(byTime);
|
|
1900
|
+
/** @type {Queued[]} */
|
|
1901
|
+
let queue;
|
|
1461
1902
|
/** @type {Set<string>} */
|
|
1462
1903
|
const unsure = new Set();
|
|
1904
|
+
/** @type {Map<string, HistoryScan>} */
|
|
1905
|
+
let states = new Map();
|
|
1463
1906
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1907
|
+
if (vouched) {
|
|
1908
|
+
// Each deposit's own hours of its sender's history, with its sender's
|
|
1909
|
+
// equal deposits there, the app's no longer looked for among them.
|
|
1910
|
+
/** @type {Awaited<ReturnType<typeof readHistories>>} */
|
|
1911
|
+
let history;
|
|
1912
|
+
|
|
1913
|
+
try {
|
|
1914
|
+
history = await readHistories(
|
|
1915
|
+
/** @type {Archive} */ (archive),
|
|
1916
|
+
/** @type {Reading[]} */ ([...items].sort((a, b) => /** @type {number} */ (a.height) - /** @type {number} */ (b.height))),
|
|
1917
|
+
{
|
|
1918
|
+
account: /** @type {string} */ (sender),
|
|
1919
|
+
direction: "out",
|
|
1920
|
+
horizonMs: DEPOSIT_HORIZON_MS,
|
|
1921
|
+
keep: (entry) => entry.counterparty === contract && entry.value >= amount,
|
|
1922
|
+
of: (item) => `${sender}:${item.height}:${amount}`,
|
|
1923
|
+
pages: HISTORY_PAGES,
|
|
1924
|
+
peersOf: (found) => depositPeers(/** @type {Archive} */ (archive), net, found, { amount, bounds, calls, supplied, to }, read.counter),
|
|
1925
|
+
reach: Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS),
|
|
1926
|
+
},
|
|
1927
|
+
read.counter
|
|
1928
|
+
);
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
|
|
1931
|
+
continue;
|
|
1480
1932
|
}
|
|
1933
|
+
|
|
1934
|
+
states = history.states;
|
|
1935
|
+
waitForHistory(items, history, results);
|
|
1936
|
+
|
|
1937
|
+
if (!history.covered.length) continue;
|
|
1938
|
+
|
|
1939
|
+
given = history.given;
|
|
1940
|
+
queue = history.queue;
|
|
1941
|
+
|
|
1942
|
+
// One before it whose call can't be read could be equal: the credits'
|
|
1943
|
+
// order can't be told.
|
|
1944
|
+
for (const item of given) if (/** @type {number[]} */ (history.unread.get(item.hash)).length) unsure.add(item.hash);
|
|
1945
|
+
} else {
|
|
1946
|
+
queue = withPast(
|
|
1947
|
+
given,
|
|
1948
|
+
past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
|
|
1949
|
+
windowMs
|
|
1950
|
+
);
|
|
1481
1951
|
}
|
|
1482
1952
|
|
|
1483
1953
|
try {
|
|
@@ -1499,9 +1969,9 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
|
|
|
1499
1969
|
{ blockTimes, maxWindows, now, slackMs: CLOCK_SLACK_MS, url, used, windowMs }
|
|
1500
1970
|
);
|
|
1501
1971
|
|
|
1502
|
-
for (const [hash, arrival] of found) results.set(hash, arrival);
|
|
1972
|
+
for (const [hash, arrival] of found) results.set(hash, withHistory(hash, arrival, given, states));
|
|
1503
1973
|
} catch (error) {
|
|
1504
|
-
for (const item of
|
|
1974
|
+
for (const item of given) results.set(item.hash, withHistory(item.hash, { error: messageOf(error), reason: "error", state: "unknown" }, given, states));
|
|
1505
1975
|
}
|
|
1506
1976
|
}
|
|
1507
1977
|
}
|
|
@@ -1602,97 +2072,62 @@ export async function serviceArrivals({
|
|
|
1602
2072
|
}
|
|
1603
2073
|
|
|
1604
2074
|
const live = triage(known, results, { now, waitMs });
|
|
1605
|
-
/** @type {Map<string, { complete: boolean, entries: HistoryEntry[], bottom: number, top: number, topAt: number, horizonMs: number }>} */
|
|
1606
|
-
const histories = new Map();
|
|
1607
|
-
|
|
1608
|
-
// Each route's account's history, back from its transfers: every transfer
|
|
1609
|
-
// that named the same address, in any case, and could be paid before.
|
|
1610
|
-
for (const [id, items] of groupBy(live, (item) => item.route.id)) {
|
|
1611
|
-
const { route } = items[0];
|
|
1612
|
-
const horizonMs = horizonOf(route);
|
|
1613
|
-
const heights = items.map((item) => item.height);
|
|
1614
|
-
const top = Math.max(...heights);
|
|
1615
|
-
const bottom = Math.max(0, Math.min(...heights) - Math.ceil((3 * horizonMs) / DUSK_BLOCK_MS));
|
|
1616
|
-
|
|
1617
|
-
try {
|
|
1618
|
-
const range = await historyRange(archive, { account: route.account, direction: "in", from: bottom, pages: HISTORY_PAGES, to: top });
|
|
1619
|
-
|
|
1620
|
-
read.counter.requests += range.requests;
|
|
1621
|
-
histories.set(id, { bottom, complete: range.complete, entries: range.entries, horizonMs, top, topAt: /** @type {SentItem} */ (items.find((item) => item.height === top)).at });
|
|
1622
|
-
} catch (error) {
|
|
1623
|
-
for (const item of items) results.set(item.hash, failed(error));
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
const groups = [...groupBy(live.filter((item) => histories.has(item.route.id)), (item) => `${item.route.id}:${item.to}:${item.net}`).values()];
|
|
1628
|
-
const polled = new Set(groups.flat().map((item) => item.hash));
|
|
1629
2075
|
// The fee of each transfer the app gave, those no longer looked for too,
|
|
1630
2076
|
// and the block its payout can't be before.
|
|
1631
2077
|
const feeOf = new Map(routed.map((item) => [item.hash, item.fee ?? /** @type {Route & { kind: "service" }} */ (item.route).feeLux]));
|
|
1632
2078
|
const bounds = new Map(routed.map((item) => [item.hash, item.notBefore]));
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
const others = groups.map((items) => {
|
|
1638
|
-
const { route, to } = items[0];
|
|
1639
|
-
const least = items.reduce((a, item) => (item.net + item.fee < a ? item.net + item.fee : a), MAX_U64);
|
|
1640
|
-
|
|
1641
|
-
return (/** @type {NonNullable<ReturnType<typeof histories.get>>} */ (histories.get(route.id)).entries).filter(
|
|
1642
|
-
(entry) => !polled.has(entry.hash) && entry.kind === "transfer" && entry.direction === "in" && entry.memo.toLowerCase() === to && (entry.value >= least || feeOf.has(entry.hash))
|
|
1643
|
-
);
|
|
1644
|
-
});
|
|
1645
|
-
/** @type {Map<string, TransferFacts>} */
|
|
1646
|
-
let peers;
|
|
1647
|
-
|
|
1648
|
-
try {
|
|
1649
|
-
peers = new Map([...sent, ...(await transfersOf(archive, others.flat().flatMap((entry) => (sent.has(entry.hash) ? [] : [entry.hash])), read.counter))]);
|
|
1650
|
-
} catch (error) {
|
|
1651
|
-
for (const items of groups) for (const item of items) results.set(item.hash, failed(error));
|
|
1652
|
-
|
|
1653
|
-
return done();
|
|
1654
|
-
}
|
|
2079
|
+
// What each transfer to a route's account paid whom, its memo and its
|
|
2080
|
+
// time, from its own transaction, read once.
|
|
2081
|
+
/** @type {Map<string, TransferFacts | null>} */
|
|
2082
|
+
const facts = new Map(sent);
|
|
1655
2083
|
|
|
1656
|
-
for (const
|
|
2084
|
+
for (const items of groupBy(live, (item) => `${item.route.id}:${item.to}:${item.net}`).values()) {
|
|
1657
2085
|
const { net: payout, route, to } = items[0];
|
|
1658
|
-
const
|
|
2086
|
+
const horizonMs = horizonOf(route);
|
|
1659
2087
|
const fees = new Set(items.map((item) => item.fee));
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2088
|
+
const least = payout + [...fees].reduce((a, fee) => (fee < a ? fee : a));
|
|
2089
|
+
// Each transfer's own hours of the route's account's history: every
|
|
2090
|
+
// transfer that named the same address, in any case, and could be paid
|
|
2091
|
+
// before, the app's no longer looked for among them. Of those, the ones
|
|
2092
|
+
// that could pay out as much under the group's fees, or under their own.
|
|
2093
|
+
/** @type {Awaited<ReturnType<typeof readHistories>>} */
|
|
2094
|
+
let history;
|
|
2095
|
+
|
|
2096
|
+
try {
|
|
2097
|
+
history = await readHistories(
|
|
2098
|
+
archive,
|
|
2099
|
+
[...items].sort((a, b) => a.height - b.height),
|
|
2100
|
+
{
|
|
2101
|
+
account: route.account,
|
|
2102
|
+
direction: "in",
|
|
2103
|
+
horizonMs,
|
|
2104
|
+
keep: (entry) => entry.kind === "transfer" && entry.direction === "in" && entry.memo.toLowerCase() === to && entry.value >= payout,
|
|
2105
|
+
of: (item) => `${route.account}:${item.height}:${to}:${payout}`,
|
|
2106
|
+
pages: HISTORY_PAGES,
|
|
2107
|
+
peersOf: (found) =>
|
|
2108
|
+
servicePeers(
|
|
2109
|
+
archive,
|
|
2110
|
+
found.filter((entry) => BigInt(entry.value) >= least || feeOf.has(entry.hash)),
|
|
2111
|
+
{ bounds, facts, feeOf, fees, payout, route, to },
|
|
2112
|
+
read.counter
|
|
2113
|
+
),
|
|
2114
|
+
reach: Math.ceil((3 * horizonMs) / DUSK_BLOCK_MS),
|
|
2115
|
+
},
|
|
2116
|
+
read.counter
|
|
2117
|
+
);
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
for (const item of items) results.set(item.hash, failed(error));
|
|
2120
|
+
continue;
|
|
1681
2121
|
}
|
|
1682
2122
|
|
|
1683
|
-
|
|
1684
|
-
const queue = queueOf(given, alike, history.horizonMs);
|
|
1685
|
-
const bottomAt = history.bottom === 0 ? -Infinity : history.topAt - (history.top - history.bottom) * DUSK_BLOCK_MS;
|
|
1686
|
-
/** @type {Set<string>} */
|
|
1687
|
-
const unsure = new Set();
|
|
2123
|
+
waitForHistory(items, history, results);
|
|
1688
2124
|
|
|
1689
|
-
|
|
1690
|
-
// that can't be read: the payouts' order can't be told.
|
|
1691
|
-
for (const item of given) {
|
|
1692
|
-
const height = /** @type {SentItem} */ (items.find((each) => each.hash === item.hash)).height;
|
|
2125
|
+
if (!history.covered.length) continue;
|
|
1693
2126
|
|
|
1694
|
-
|
|
1695
|
-
|
|
2127
|
+
const { given } = history;
|
|
2128
|
+
// One before it that can't be read could be equal: the payouts' order
|
|
2129
|
+
// can't be told.
|
|
2130
|
+
const unsure = new Set(given.flatMap((item) => (/** @type {number[]} */ (history.unread.get(item.hash)).length ? [item.hash] : [])));
|
|
1696
2131
|
|
|
1697
2132
|
try {
|
|
1698
2133
|
const found = await followLogs(
|
|
@@ -1706,21 +2141,63 @@ export async function serviceArrivals({
|
|
|
1706
2141
|
},
|
|
1707
2142
|
given,
|
|
1708
2143
|
guarded: false,
|
|
1709
|
-
queue,
|
|
2144
|
+
queue: history.queue,
|
|
1710
2145
|
unsure,
|
|
1711
2146
|
},
|
|
1712
2147
|
{ blockTimes, maxWindows, now, slackMs: CLOCK_SLACK_MS, url: String(rpcUrl ?? route.chain.rpcUrl), used, windowMs: CREDIT_WINDOW_MS }
|
|
1713
2148
|
);
|
|
1714
2149
|
|
|
1715
|
-
for (const [hash, arrival] of found) results.set(hash, arrival);
|
|
2150
|
+
for (const [hash, arrival] of found) results.set(hash, withHistory(hash, arrival, given, history.states));
|
|
1716
2151
|
} catch (error) {
|
|
1717
|
-
for (const item of
|
|
2152
|
+
for (const item of given) results.set(item.hash, withHistory(item.hash, failed(error), given, history.states));
|
|
1718
2153
|
}
|
|
1719
2154
|
}
|
|
1720
2155
|
|
|
1721
2156
|
return done();
|
|
1722
2157
|
}
|
|
1723
2158
|
|
|
2159
|
+
/**
|
|
2160
|
+
* Of the transfers a route's account received naming a group's address,
|
|
2161
|
+
* those equal to the group's, from their own transactions: one the app
|
|
2162
|
+
* gave pays out what its own fee leaves, one it didn't what the group's
|
|
2163
|
+
* fees do; each with the block the app gave it its payout can't be before.
|
|
2164
|
+
* And those whose transaction can't be read. `facts` keeps each
|
|
2165
|
+
* transaction read.
|
|
2166
|
+
*
|
|
2167
|
+
* @param {Archive} archive
|
|
2168
|
+
* @param {ReadonlyArray<{ hash: string, height: number }>} found
|
|
2169
|
+
* @param {{ bounds: ReadonlyMap<string, bigint | null>, facts: Map<string, TransferFacts | null>, feeOf: ReadonlyMap<string, bigint>, fees: ReadonlySet<bigint>, payout: bigint, route: Route & { kind: "service" }, to: string }} group
|
|
2170
|
+
* @param {{ requests: number }} counter
|
|
2171
|
+
*/
|
|
2172
|
+
async function servicePeers(archive, found, { bounds, facts, feeOf, fees, payout, route, to }, counter) {
|
|
2173
|
+
const missing = found.flatMap((entry) => (facts.has(entry.hash) ? [] : [entry.hash]));
|
|
2174
|
+
const read = await transfersOf(archive, missing, counter);
|
|
2175
|
+
|
|
2176
|
+
for (const hash of missing) facts.set(hash, read.get(hash) ?? null);
|
|
2177
|
+
|
|
2178
|
+
/** @type {Queued[]} */
|
|
2179
|
+
const peers = [];
|
|
2180
|
+
/** @type {{ hash: string, height: number }[]} */
|
|
2181
|
+
const unread = [];
|
|
2182
|
+
|
|
2183
|
+
for (const entry of found) {
|
|
2184
|
+
const fact = facts.get(entry.hash);
|
|
2185
|
+
|
|
2186
|
+
if (!fact || fact.kind === null || fact.at === null || (fact.kind === "moonlight" && (fact.value === null || fact.receiver === null))) {
|
|
2187
|
+
unread.push(entry);
|
|
2188
|
+
} else if (!fact.failed && fact.kind === "moonlight" && fact.receiver === route.account && fact.memo.toLowerCase() === to) {
|
|
2189
|
+
const value = /** @type {bigint} */ (fact.value);
|
|
2190
|
+
const own = feeOf.get(entry.hash);
|
|
2191
|
+
|
|
2192
|
+
if (own === undefined ? [...fees].some((fee) => value - fee === payout) : value - own === payout) {
|
|
2193
|
+
peers.push({ at: fact.at, given: false, hash: entry.hash, notBefore: bounds.get(entry.hash) ?? null, scan: null });
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
return { peers, unread };
|
|
2199
|
+
}
|
|
2200
|
+
|
|
1724
2201
|
// ---------------------------------------------------------------------------
|
|
1725
2202
|
// Migrations into Dusk
|
|
1726
2203
|
// ---------------------------------------------------------------------------
|