@dusk-network/app-core 0.3.0 → 0.3.1
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 +12 -0
- package/README.md +4 -3
- package/package.json +1 -1
- package/src/bridge.d.ts +35 -8
- package/src/bridge.js +407 -188
- package/src/history.d.ts +2 -0
- package/src/history.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are listed here. It follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [0.3.1]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `/history`: `historyRange` reads on from a `page`
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `/bridge`: each transfer reads only its own hours of its account's history, read on from its `scan` while a call can't read it all: it waits then, rather than staying unsure
|
|
14
|
+
- `/bridge`: a transfer the archive doesn't list in its account's history yet waits, rather than taking an earlier one's credit
|
|
15
|
+
- `/bridge`: a deposit whose call's arguments the archive doesn't give counts as one that could be equal, rather than none
|
|
16
|
+
|
|
5
17
|
## [0.3.0]
|
|
6
18
|
|
|
7
19
|
### Added
|
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ const history = createHistory({
|
|
|
98
98
|
|
|
99
99
|
### Other reads
|
|
100
100
|
|
|
101
|
-
- `historyRange(archive, { account, from, to, direction, pages })`: an account's entries between two heights, a page at a time.
|
|
101
|
+
- `historyRange(archive, { account, from, to, direction, pages, page })`: an account's entries between two heights, a page at a time, from `page` on.
|
|
102
102
|
- `fetchTxDetails(archive, hashes, options)` and `fetchTxNonce(archive, hash)`: a transaction's time, outcome, fee and call, and its nonce.
|
|
103
103
|
- `archive.query(text)`: one GraphQL request, its integers past 2^53 kept as text. A request answered 429, 502, 503 or 504 is sent again, twice at most.
|
|
104
104
|
|
|
@@ -201,9 +201,10 @@ arrivalsOf(arrivals); // Map { hash => { hash: "0x029e…", at: 1793080333000 }
|
|
|
201
201
|
- Each credit pays one transfer. Equal transfers (the same route, recipient and payout) are paid in the order they were sent: the oldest takes the first credit after it, from 2 minutes before its block, as the chains' clocks differ. The app passes the transfers it still waits for, and the credits its others arrived with (`taken`): none of those pays a waiting one, though an earlier transfer read from the chain takes its own back, as one given but not looked for does.
|
|
202
202
|
- A transfer's time is its Dusk block's, never the device's clock. One without it yet isn't looked for. One sent more than a week ago (`waitMs`) is unknown, and isn't asked about.
|
|
203
203
|
- A deposit is followed on DuskEVM's explorer (`via: "explorer"`, the default): the recipient's incoming calls, newest first, read once for all its deposits, back past the oldest, 5 pages at most. Those calls don't say who deposited, so a payment is a deposit's only when it can't be another's: within 2 minutes either side of its block, once those minutes are over, with no other payment of its amount there (a failed one counts) that isn't another given deposit's. Nothing is asked before then.
|
|
204
|
-
- On DuskEVM's RPC (`via: "rpc"`), a deposit is found by DuskEVM's bridge event from its sender's DuskEVM address. With `/history`'s archive, each sender's equal deposits in the hours before its own are read from its history and paid first; a
|
|
204
|
+
- On DuskEVM's RPC (`via: "rpc"`), a deposit is found by DuskEVM's bridge event from its sender's DuskEVM address. With `/history`'s archive, each sender's equal deposits in the hours before its own are read from its history and paid first; a deposit there whose call can't be read leaves the order unclear. Without it, the RPC's credits are taken as the explorer's are. A deposit given without its sender takes no credit from the sender of another equal one given.
|
|
205
205
|
- A migration is paid by a transfer to the account from its route's payout account, with the migration's transaction hash as the memo, of exactly its amount: from the account's entries, less any `refund` paid to the account with it, or from the archive's memo index, which gives each payout's own amount. One from that account of another amount leaves it unsure, not waiting: the value of an entry an earlier version kept holds any gas refund paid to the account. With `entries`, the payouts there of migrations the app didn't list come too (`unlisted`), each with its `payer`; where one account pays out more than one route, a payout of a migration not listed doesn't say which: its `route` is null.
|
|
206
|
-
- A transfer out is read from the chain: what the route's account received, the address its memo gives as the route's bridge reads it (0x and 40 hex digits, any case, nothing around them), and its block. It arrives with the DUSK token's `Transfer` from its route's payout address to that address, of what the account received less the fee. The account's history lists every transfer that named the same address, in any case: equal ones in the hours before (a given one's by its own fee) are paid first, and one that can't be read
|
|
206
|
+
- A transfer out is read from the chain: what the route's account received, the address its memo gives as the route's bridge reads it (0x and 40 hex digits, any case, nothing around them), and its block. It arrives with the DUSK token's `Transfer` from its route's payout address to that address, of what the account received less the fee. The account's history lists every transfer that named the same address, in any case: equal ones in the hours before (a given one's by its own fee) are paid first, and one that can't be read leaves the order unclear. One that pays nothing out is `no-payout`, with its `problem`. `serviceTransferOf` gives what left the account (`sent`), and what the account received (`amount`) only where no gas was left to refund: an entry's value holds any refund paid to another account.
|
|
207
|
+
- With the archive, each transfer reads only its own hours of its account's history (its sender's for a deposit, the route's account's for a transfer out), further back while equal ones before it run on, and only once the archive lists the transfer there. A call reads 4 pages of it (`HISTORY_PAGES`) for each: a transfer whose history is longer waits, and the next call reads on from its `scan`.
|
|
207
208
|
- A route without its payout account or address in the network's file pays out nothing that can be told apart: `no-route`.
|
|
208
209
|
- An RPC search starts at a block from before the transfer, estimated from the chain's pace and checked by its time, and reads 8 windows of 5,000 blocks at most, narrower where the RPC asks for it. A result that isn't arrived says where the next search starts, with what it searched for and the credits found so far (`scan`): the app keeps it with the arrivals it was made with, and passes both back. A search for another sender or amount starts afresh.
|
|
209
210
|
|
package/package.json
CHANGED
package/src/bridge.d.ts
CHANGED
|
@@ -85,7 +85,7 @@ export declare const DEPOSIT_HORIZON_MS: 7200000;
|
|
|
85
85
|
/** Equal transfers out sent within this of each other are paid in their order, or 4 times the route's usual time: 12 hours, in milliseconds. */
|
|
86
86
|
export declare const SERVICE_HORIZON_MS: 43200000;
|
|
87
87
|
|
|
88
|
-
/** Pages of
|
|
88
|
+
/** Pages of its account's history one call reads for a transfer's earlier equal ones; a longer history is read on in the next call, from its `scan`. */
|
|
89
89
|
export declare const HISTORY_PAGES: 4;
|
|
90
90
|
|
|
91
91
|
/** `Transfer(address,address,uint256)`: the DUSK token's transfer that pays a transfer out. */
|
|
@@ -94,15 +94,37 @@ export declare const TRANSFER_TOPIC: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f1
|
|
|
94
94
|
/** `ETHBridgeFinalized(address,address,uint256,bytes)`: DuskEVM's bridge paying a deposit, from the depositor's DuskEVM address. */
|
|
95
95
|
export declare const ETH_BRIDGE_FINALIZED_TOPIC: "0x31b2166ff604fc5672ea5df08a78081d2bc6d746cadce880747f3643d819e83d";
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* How far a transfer's own history is read: what it was read for (`of`),
|
|
99
|
+
* all of it from `lo` up to the transfer's block once `lo` is set, the
|
|
100
|
+
* range read next (`next`: its first block, its last and the page it
|
|
101
|
+
* carries on from), and the transfers found in it that could be equal to
|
|
102
|
+
* it, each with what it paid, in lux (`value`).
|
|
103
|
+
*/
|
|
104
|
+
export type HistoryScan = {
|
|
105
|
+
of: string;
|
|
106
|
+
lo: number | null;
|
|
107
|
+
next: { from: number; to: number; page: number } | null;
|
|
108
|
+
found: ReadonlyArray<{ hash: string; height: number; value: string }>;
|
|
109
|
+
};
|
|
110
|
+
|
|
97
111
|
/**
|
|
98
112
|
* Where a search stopped: the block the next one starts at, the time of the
|
|
99
113
|
* transfer it began from, the logs it searched for (`search`), and the
|
|
100
114
|
* credits it found before (`seen`), which the next one counts again. A
|
|
101
115
|
* search for other logs, as for another sender or amount, starts afresh.
|
|
102
|
-
*
|
|
103
|
-
* is
|
|
116
|
+
* With the archive, how far the transfer's own history is read
|
|
117
|
+
* (`history`): a history longer than one call reads is read on from there,
|
|
118
|
+
* before any search. Kept by the app with the credits it was made with,
|
|
119
|
+
* and passed back as it is.
|
|
104
120
|
*/
|
|
105
|
-
export type Scan = {
|
|
121
|
+
export type Scan = {
|
|
122
|
+
from?: string;
|
|
123
|
+
basis?: number;
|
|
124
|
+
search?: string;
|
|
125
|
+
seen?: ReadonlyArray<{ hash: string; block: string; at: number; sender: string | null }>;
|
|
126
|
+
history?: HistoryScan;
|
|
127
|
+
};
|
|
106
128
|
|
|
107
129
|
/**
|
|
108
130
|
* Why a transfer's credit is unknown:
|
|
@@ -190,7 +212,9 @@ export type DepositArrivalOptions = {
|
|
|
190
212
|
/**
|
|
191
213
|
* `/history`'s archive, for "rpc": each sender's equal deposits in the
|
|
192
214
|
* hours before its own are read from its history, those given but not
|
|
193
|
-
* looked for too, and paid first.
|
|
215
|
+
* looked for too, and paid first. Each deposit reads its own hours, once
|
|
216
|
+
* the archive lists it there, `HISTORY_PAGES` pages a call: a deposit
|
|
217
|
+
* whose history is read on in the next call waits, with its `scan`.
|
|
194
218
|
* Without it, or for a deposit without its sender and height, its credit
|
|
195
219
|
* is a deposit's only as where they can't be told apart.
|
|
196
220
|
*/
|
|
@@ -279,9 +303,12 @@ export type ServiceArrivalOptions = {
|
|
|
279
303
|
* route's account received in the hours before that named the same address,
|
|
280
304
|
* in any case, and pay out the same are paid first: its history lists them,
|
|
281
305
|
* and their own transactions say what each received (one given but not
|
|
282
|
-
* looked for pays out what its own fee leaves). One that can't be
|
|
283
|
-
*
|
|
284
|
-
*
|
|
306
|
+
* looked for pays out what its own fee leaves). One that can't be read
|
|
307
|
+
* leaves the order unclear: unsure. Each transfer reads its own hours of
|
|
308
|
+
* that history, once the archive lists it there, `HISTORY_PAGES` pages a
|
|
309
|
+
* call: one whose history is read on in the next call waits, with its
|
|
310
|
+
* `scan`. Throws a TypeError without an archive, and for a network there's
|
|
311
|
+
* no file for.
|
|
285
312
|
*/
|
|
286
313
|
export declare function serviceArrivals(options: ServiceArrivalOptions): Promise<{ arrivals: Map<string, Arrival>; requests: number }>;
|
|
287
314
|
|
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 */
|
|
@@ -199,9 +200,42 @@ const msOf = (value) => (typeof value === "number" && Number.isFinite(value) &&
|
|
|
199
200
|
*/
|
|
200
201
|
const heightOf = (value) => (typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null);
|
|
201
202
|
|
|
203
|
+
/**
|
|
204
|
+
* How far a transfer's history is read, as a result gave it; null for
|
|
205
|
+
* anything else.
|
|
206
|
+
*
|
|
207
|
+
* @param {unknown} value
|
|
208
|
+
* @returns {HistoryScan | null}
|
|
209
|
+
*/
|
|
210
|
+
function historyScanOf(value) {
|
|
211
|
+
const read = /** @type {any} */ (value);
|
|
212
|
+
|
|
213
|
+
if (!read || typeof read !== "object" || typeof read.of !== "string" || !Array.isArray(read.found)) return null;
|
|
214
|
+
|
|
215
|
+
const lo = read.lo === null ? null : heightOf(read.lo);
|
|
216
|
+
const next = read.next === null ? null : { from: heightOf(read.next?.from), page: heightOf(read.next?.page), to: heightOf(read.next?.to) };
|
|
217
|
+
|
|
218
|
+
if ((read.lo !== null && lo === null) || (lo === null && next === null)) return null;
|
|
219
|
+
if (next && (next.from === null || next.to === null || next.page === null || next.page < 1 || next.from > next.to)) return null;
|
|
220
|
+
|
|
221
|
+
const found = read.found.flatMap((/** @type {any} */ each) => {
|
|
222
|
+
const hash = duskHashOf(each?.hash);
|
|
223
|
+
const height = heightOf(each?.height);
|
|
224
|
+
const value = each?.value;
|
|
225
|
+
|
|
226
|
+
return hash && height !== null && typeof value === "string" && /^\d{1,20}$/.test(value) ? [{ hash, height, value }] : [];
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// One left out could be equal: it's read afresh.
|
|
230
|
+
if (found.length !== read.found.length) return null;
|
|
231
|
+
|
|
232
|
+
return { found, lo, next: /** @type {HistoryScan["next"]} */ (next), of: read.of };
|
|
233
|
+
}
|
|
234
|
+
|
|
202
235
|
/**
|
|
203
236
|
* Where a search stopped, what it searched for, and the credits it found
|
|
204
|
-
* before,
|
|
237
|
+
* before, and how far the transfer's history is read, as a result gave
|
|
238
|
+
* them; null for anything else.
|
|
205
239
|
*
|
|
206
240
|
* @param {unknown} value
|
|
207
241
|
* @returns {Scan | null}
|
|
@@ -209,19 +243,26 @@ const heightOf = (value) => (typeof value === "number" && Number.isSafeInteger(v
|
|
|
209
243
|
function scanOf(value) {
|
|
210
244
|
const scan = /** @type {any} */ (value);
|
|
211
245
|
const from = quantityOf(scan?.from);
|
|
246
|
+
const history = historyScanOf(scan?.history);
|
|
247
|
+
/** @type {Scan} */
|
|
248
|
+
const out = history ? { history } : {};
|
|
212
249
|
|
|
213
|
-
if (from
|
|
250
|
+
if (from !== null && typeof scan?.basis === "number" && Number.isFinite(scan.basis) && typeof scan.search === "string") {
|
|
251
|
+
const seen = (Array.isArray(scan.seen) ? scan.seen : []).flatMap((/** @type {any} */ credit) => {
|
|
252
|
+
const hash = evmHashOf(credit?.hash);
|
|
253
|
+
const block = quantityOf(credit?.block);
|
|
254
|
+
const at = msOf(credit?.at);
|
|
255
|
+
const sender = credit?.sender === null ? null : addressOf(credit?.sender);
|
|
214
256
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const block = quantityOf(credit?.block);
|
|
218
|
-
const at = msOf(credit?.at);
|
|
219
|
-
const sender = credit?.sender === null ? null : addressOf(credit?.sender);
|
|
257
|
+
return hash && block !== null && at !== null && sender !== "" ? [{ at, block: block.toString(), hash, sender }] : [];
|
|
258
|
+
});
|
|
220
259
|
|
|
221
|
-
|
|
222
|
-
})
|
|
260
|
+
Object.assign(out, { basis: scan.basis, from: from.toString(), search: scan.search, seen });
|
|
261
|
+
} else if (!history) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
223
264
|
|
|
224
|
-
return
|
|
265
|
+
return out;
|
|
225
266
|
}
|
|
226
267
|
|
|
227
268
|
/**
|
|
@@ -780,7 +821,10 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
780
821
|
// The logs searched for: a search for others, as for another sender or
|
|
781
822
|
// amount, isn't carried on.
|
|
782
823
|
const search = [filter.address, ...filter.topics.map((topic) => topic ?? "*"), filter.wei.toString()].join(":");
|
|
783
|
-
|
|
824
|
+
/** @type {{ basis: number, from: string, seen?: Scan["seen"] }[]} */
|
|
825
|
+
const resumed = live.flatMap(({ scan }) =>
|
|
826
|
+
scan?.from !== undefined && scan.search === search && scan.basis !== undefined && scan.basis <= first.at ? [{ basis: scan.basis, from: scan.from, seen: scan.seen }] : []
|
|
827
|
+
);
|
|
784
828
|
/** @type {Map<string, Credit>} */
|
|
785
829
|
const carried = new Map();
|
|
786
830
|
|
|
@@ -797,9 +841,18 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
797
841
|
/** @type {bigint} */
|
|
798
842
|
let start;
|
|
799
843
|
|
|
800
|
-
if (resumed.length) {
|
|
844
|
+
if (resumed.length && resumed.every((scan) => scan.basis === first.at)) {
|
|
801
845
|
start = minOf(resumed.map((scan) => BigInt(scan.from)));
|
|
802
846
|
head = await headOf(read, url);
|
|
847
|
+
} else if (resumed.length) {
|
|
848
|
+
// Begun from a transfer before the first now, as one that since
|
|
849
|
+
// arrived: none of the queue's credits is before where a search from
|
|
850
|
+
// the first starts, which it skips to.
|
|
851
|
+
const from = minOf(resumed.map((scan) => BigInt(scan.from)));
|
|
852
|
+
const fresh = first.notBefore !== null ? { head: await headOf(read, url), start: first.notBefore } : await startBefore(read, url, reach, blockTimes);
|
|
853
|
+
|
|
854
|
+
head = fresh.head;
|
|
855
|
+
start = from > fresh.start ? from : fresh.start;
|
|
803
856
|
} else if (first.notBefore !== null) {
|
|
804
857
|
start = first.notBefore;
|
|
805
858
|
head = await headOf(read, url);
|
|
@@ -1016,50 +1069,214 @@ async function detailsOf(archive, hashes, { args = false, network }, counter) {
|
|
|
1016
1069
|
}
|
|
1017
1070
|
|
|
1018
1071
|
/**
|
|
1019
|
-
* A
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
|
|
1072
|
+
* A transfer whose history is read: its hash, its Dusk block's time and
|
|
1073
|
+
* height, the block on the other chain its credit can't be before, and its
|
|
1074
|
+
* scan.
|
|
1075
|
+
*
|
|
1076
|
+
* @typedef {{ hash: string, at: number, height: number, notBefore: bigint | null, scan: Scan | null }} Reading
|
|
1077
|
+
*/
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* The history a group of equal transfers needs, each transfer's read on
|
|
1081
|
+
* from where its scan stopped: its account's history from `reach` blocks
|
|
1082
|
+
* under its block up to it, and `reach` further down at a time while the
|
|
1083
|
+
* run of equal transfers it is in reaches within `horizonMs` of the bottom.
|
|
1084
|
+
* The transfer itself must be listed there: until it is, the archive
|
|
1085
|
+
* hasn't read its block. Each transfer reads `pages` pages at most, so a
|
|
1086
|
+
* later one's history never keeps an earlier one waiting.
|
|
1087
|
+
*
|
|
1088
|
+
* Those whose history is all read are `covered`, and `given` as the queue
|
|
1089
|
+
* holds them, with `queue`: theirs and the equal transfers found
|
|
1090
|
+
* (`peersOf`), and `unread`: by transfer, the heights it read of those that
|
|
1091
|
+
* could be equal but can't be read. Each one's reading, done or not, is in
|
|
1092
|
+
* `states`.
|
|
1093
|
+
*
|
|
1094
|
+
* @param {Archive} archive
|
|
1095
|
+
* @param {ReadonlyArray<Reading>} items
|
|
1096
|
+
* @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
|
|
1097
|
+
* @param {{ requests: number }} counter
|
|
1098
|
+
*/
|
|
1099
|
+
async function readHistories(archive, items, { account, direction, horizonMs, keep, of, pages, peersOf, reach }, counter) {
|
|
1100
|
+
const walks = items.map((item) => {
|
|
1101
|
+
const kept = item.scan?.history;
|
|
1102
|
+
/** @type {HistoryScan} */
|
|
1103
|
+
const state =
|
|
1104
|
+
kept && kept.of === of(item)
|
|
1105
|
+
? structuredClone(/** @type {HistoryScan} */ (kept))
|
|
1106
|
+
: { found: [], lo: null, next: { from: Math.max(0, item.height - reach), page: 1, to: item.height }, of: of(item) };
|
|
1107
|
+
|
|
1108
|
+
return { item, left: pages, state };
|
|
1109
|
+
});
|
|
1110
|
+
/** @type {Reading[]} */
|
|
1111
|
+
let covered = [];
|
|
1112
|
+
/** @type {Queued[]} */
|
|
1113
|
+
let given = [];
|
|
1114
|
+
/** @type {Queued[]} */
|
|
1115
|
+
let queue = [];
|
|
1116
|
+
/** @type {Map<string, number[]>} */
|
|
1117
|
+
const unread = new Map();
|
|
1118
|
+
|
|
1119
|
+
for (;;) {
|
|
1120
|
+
// Each one's next range, as far as its pages go.
|
|
1121
|
+
for (const walk of walks) {
|
|
1122
|
+
const { state } = walk;
|
|
1123
|
+
|
|
1124
|
+
while (state.next && walk.left > 0) {
|
|
1125
|
+
const next = /** @type {NonNullable<HistoryScan["next"]>} */ (state.next);
|
|
1126
|
+
const range = await historyRange(archive, { account, direction, from: next.from, page: next.page, pages: walk.left, to: next.to });
|
|
1127
|
+
const found = /** @type {HistoryScan["found"][number][]} */ (state.found);
|
|
1128
|
+
|
|
1129
|
+
counter.requests += range.requests;
|
|
1130
|
+
walk.left -= range.requests;
|
|
1131
|
+
|
|
1132
|
+
for (const entry of range.entries) {
|
|
1133
|
+
if (keep(entry) && !found.some((each) => each.hash === entry.hash)) found.push({ hash: entry.hash, height: entry.blockHeight, value: String(entry.value) });
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (!range.complete) {
|
|
1137
|
+
state.next = { ...next, page: next.page + range.requests };
|
|
1138
|
+
} else if (state.lo === null && !found.some((each) => each.hash === walk.item.hash)) {
|
|
1139
|
+
// Its own block isn't read yet: its last page is read again.
|
|
1140
|
+
state.next = { ...next, page: next.page + range.requests - 1 };
|
|
1141
|
+
walk.left = 0;
|
|
1142
|
+
} else {
|
|
1143
|
+
state.lo = next.from;
|
|
1144
|
+
state.next = null;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const done = walks.filter((walk) => !walk.state.next);
|
|
1150
|
+
|
|
1151
|
+
if (!done.length) {
|
|
1152
|
+
covered = [];
|
|
1153
|
+
given = [];
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
given = done.map((walk) => queued(walk.item)).sort(byTime);
|
|
1158
|
+
|
|
1159
|
+
const mine = new Set(given.map((item) => item.hash));
|
|
1160
|
+
// The others found: those given are in the queue as they are.
|
|
1161
|
+
const { peers, unread: unknown } = await peersOf(uniqueByHash(done.flatMap((walk) => walk.state.found)).filter((entry) => !mine.has(entry.hash)));
|
|
1162
|
+
let deeper = false;
|
|
1163
|
+
|
|
1164
|
+
queue = queueOf(given, peers, horizonMs);
|
|
1165
|
+
|
|
1166
|
+
// A run reaching within a horizon of the bottom may go on under it: the
|
|
1167
|
+
// history is read further down.
|
|
1168
|
+
for (const walk of done) {
|
|
1169
|
+
const lo = /** @type {number} */ (walk.state.lo);
|
|
1170
|
+
const bottomAt = lo === 0 ? -Infinity : walk.item.at - (walk.item.height - lo) * DUSK_BLOCK_MS;
|
|
1171
|
+
const self = /** @type {Queued} */ (given.find((each) => each.hash === walk.item.hash));
|
|
1172
|
+
|
|
1173
|
+
if (runStart(queue, self, horizonMs).at - bottomAt < horizonMs) {
|
|
1174
|
+
walk.state.next = { from: Math.max(0, lo - reach), page: 1, to: lo - 1 };
|
|
1175
|
+
deeper = true;
|
|
1176
|
+
} else {
|
|
1177
|
+
unread.set(
|
|
1178
|
+
walk.item.hash,
|
|
1179
|
+
unknown.flatMap((each) => (each.height >= lo && each.height <= walk.item.height ? [each.height] : []))
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (!deeper) {
|
|
1185
|
+
covered = done.map((walk) => walk.item);
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return { covered, given, queue, states: new Map(walks.map((walk) => [walk.item.hash, walk.state])), unread };
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Of the deposits a sender's history lists, those equal to the group's
|
|
1195
|
+
* (`to`, `amount`), from their calls: each with the block the app gave it
|
|
1196
|
+
* its credit can't be before; and those whose call or time the archive
|
|
1197
|
+
* doesn't give, which could be any deposit of up to what they paid, but
|
|
1198
|
+
* for one the app gave: its own facts tell. `calls` keeps each call read.
|
|
1025
1199
|
*
|
|
1026
1200
|
* @param {Archive} archive
|
|
1027
1201
|
* @param {Network} net
|
|
1028
|
-
* @param {{
|
|
1202
|
+
* @param {ReadonlyArray<{ hash: string, height: number }>} found
|
|
1203
|
+
* @param {{ amount: bigint, to: string, bounds: ReadonlyMap<string, bigint | null>, calls: Map<string, any>, supplied: ReadonlyMap<string, DepositItem> }} group
|
|
1029
1204
|
* @param {{ requests: number }} counter
|
|
1030
1205
|
*/
|
|
1031
|
-
async function
|
|
1032
|
-
const
|
|
1033
|
-
const read = await
|
|
1206
|
+
async function depositPeers(archive, net, found, { amount, bounds, calls, supplied, to }, counter) {
|
|
1207
|
+
const missing = found.flatMap((entry) => (calls.has(entry.hash) ? [] : [entry.hash]));
|
|
1208
|
+
const read = await detailsOf(archive, missing, { args: true, network: net }, counter);
|
|
1034
1209
|
|
|
1035
|
-
|
|
1210
|
+
for (const hash of missing) calls.set(hash, read.get(hash) ?? null);
|
|
1036
1211
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1212
|
+
/** @type {Queued[]} */
|
|
1213
|
+
const peers = [];
|
|
1214
|
+
/** @type {{ hash: string, height: number }[]} */
|
|
1215
|
+
const unread = [];
|
|
1039
1216
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const deposits = [];
|
|
1044
|
-
/** @type {{ hash: string, height: number, value: bigint }[]} */
|
|
1045
|
-
const unresolved = [];
|
|
1217
|
+
for (const entry of found) {
|
|
1218
|
+
const call = calls.get(entry.hash);
|
|
1219
|
+
const bridged = call?.bridge;
|
|
1046
1220
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1221
|
+
// Failed, or a call that isn't a deposit: no DUSK left for DuskEVM. A
|
|
1222
|
+
// deposit whose arguments can't be read could be any.
|
|
1223
|
+
if (call?.success === false || (bridged && bridged.kind !== "deposit") || (bridged === null && call?.fnName !== BRIDGE_FUNCTIONS.deposit)) continue;
|
|
1050
1224
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1225
|
+
const own = supplied.get(entry.hash);
|
|
1226
|
+
let at = msOf(call?.timestamp);
|
|
1227
|
+
let paid = luxOf(bridged?.amount);
|
|
1228
|
+
let recipient = addressOf(bridged?.to);
|
|
1053
1229
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const recipient = addressOf(bridged?.to);
|
|
1230
|
+
// One the app gave whose call can't be read: its own facts.
|
|
1231
|
+
if (own && (!call || at === null || paid === null || !recipient)) ({ amount: paid, at, to: recipient } = own);
|
|
1057
1232
|
|
|
1058
|
-
if (
|
|
1059
|
-
else
|
|
1233
|
+
if (at === null || paid === null || !recipient) unread.push(entry);
|
|
1234
|
+
else if (recipient === to && paid === amount) peers.push({ at, given: false, hash: entry.hash, notBefore: bounds.get(entry.hash) ?? null, scan: null });
|
|
1060
1235
|
}
|
|
1061
1236
|
|
|
1062
|
-
return {
|
|
1237
|
+
return { peers, unread };
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Where a scan's search stopped, without its history.
|
|
1242
|
+
*
|
|
1243
|
+
* @param {Scan | null | undefined} scan
|
|
1244
|
+
* @returns {Scan}
|
|
1245
|
+
*/
|
|
1246
|
+
const searchOf = (scan) => (scan?.from !== undefined ? { basis: scan.basis, from: scan.from, search: scan.search, seen: scan.seen } : {});
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* The transfers whose history isn't all read yet wait, with where their
|
|
1250
|
+
* reading stopped beside where their last search did.
|
|
1251
|
+
*
|
|
1252
|
+
* @param {ReadonlyArray<{ hash: string, scan: Scan | null }>} items
|
|
1253
|
+
* @param {{ covered: ReadonlyArray<{ hash: string }>, states: ReadonlyMap<string, HistoryScan> }} history
|
|
1254
|
+
* @param {Map<string, Arrival>} results
|
|
1255
|
+
*/
|
|
1256
|
+
function waitForHistory(items, { covered, states }, results) {
|
|
1257
|
+
const read = new Set(covered.map((item) => item.hash));
|
|
1258
|
+
|
|
1259
|
+
for (const item of items) {
|
|
1260
|
+
if (!read.has(item.hash)) results.set(item.hash, { scan: { ...searchOf(item.scan), history: /** @type {HistoryScan} */ (states.get(item.hash)) }, state: "waiting" });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* A result, with how far its transfer's history is read, which every one
|
|
1266
|
+
* but an arrival keeps in its scan for the next call.
|
|
1267
|
+
*
|
|
1268
|
+
* @param {string} hash
|
|
1269
|
+
* @param {Arrival} arrival
|
|
1270
|
+
* @param {ReadonlyArray<Queued>} given
|
|
1271
|
+
* @param {ReadonlyMap<string, HistoryScan>} states
|
|
1272
|
+
* @returns {Arrival}
|
|
1273
|
+
*/
|
|
1274
|
+
function withHistory(hash, arrival, given, states) {
|
|
1275
|
+
const history = states.get(hash);
|
|
1276
|
+
|
|
1277
|
+
if (!history || arrival.state === "arrived") return arrival;
|
|
1278
|
+
|
|
1279
|
+
return { ...arrival, scan: { ...(arrival.scan ?? searchOf(given.find((each) => each.hash === hash)?.scan)), history } };
|
|
1063
1280
|
}
|
|
1064
1281
|
|
|
1065
1282
|
// ---------------------------------------------------------------------------
|
|
@@ -1380,34 +1597,6 @@ async function onExplorer(read, live, { api, bridge, maxPages, now, past, result
|
|
|
1380
1597
|
* @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
1598
|
*/
|
|
1382
1599
|
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
1600
|
// Equal deposits (the same DuskEVM address and amount) from a known sender
|
|
1412
1601
|
// first, a sender at a time, its events telling its credits apart; then
|
|
1413
1602
|
// those whose sender isn't known, which take none from the senders of the
|
|
@@ -1438,46 +1627,69 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
|
|
|
1438
1627
|
if (rest.length) unknown.push({ items: rest, rivals: aliases.get(key) ?? new Set() });
|
|
1439
1628
|
}
|
|
1440
1629
|
|
|
1441
|
-
// The block each deposit given can't be credited before, the app's own
|
|
1630
|
+
// The block each deposit given can't be credited before, the app's own,
|
|
1631
|
+
// the deposits given, and the deposits' calls, read once.
|
|
1442
1632
|
const bounds = new Map(list.map((item) => [item.hash, item.notBefore]));
|
|
1633
|
+
const supplied = new Map(list.map((item) => [item.hash, item]));
|
|
1634
|
+
/** @type {Map<string, any>} */
|
|
1635
|
+
const calls = new Map();
|
|
1636
|
+
const contract = duskEvmContracts(net)?.bridge;
|
|
1443
1637
|
|
|
1444
1638
|
for (const { items, rivals } of [...known, ...unknown]) {
|
|
1445
1639
|
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
|
-
);
|
|
1640
|
+
const vouched = Boolean(archive) && sender !== null && items.every((item) => item.height !== null);
|
|
1641
|
+
let given = items.map(queued).sort(byTime);
|
|
1642
|
+
/** @type {Queued[]} */
|
|
1643
|
+
let queue;
|
|
1461
1644
|
/** @type {Set<string>} */
|
|
1462
1645
|
const unsure = new Set();
|
|
1646
|
+
/** @type {Map<string, HistoryScan>} */
|
|
1647
|
+
let states = new Map();
|
|
1463
1648
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1649
|
+
if (vouched) {
|
|
1650
|
+
// Each deposit's own hours of its sender's history, with its sender's
|
|
1651
|
+
// equal deposits there, the app's no longer looked for among them.
|
|
1652
|
+
/** @type {Awaited<ReturnType<typeof readHistories>>} */
|
|
1653
|
+
let history;
|
|
1654
|
+
|
|
1655
|
+
try {
|
|
1656
|
+
history = await readHistories(
|
|
1657
|
+
/** @type {Archive} */ (archive),
|
|
1658
|
+
/** @type {Reading[]} */ ([...items].sort((a, b) => /** @type {number} */ (a.height) - /** @type {number} */ (b.height))),
|
|
1659
|
+
{
|
|
1660
|
+
account: /** @type {string} */ (sender),
|
|
1661
|
+
direction: "out",
|
|
1662
|
+
horizonMs: DEPOSIT_HORIZON_MS,
|
|
1663
|
+
keep: (entry) => entry.counterparty === contract && entry.value >= amount,
|
|
1664
|
+
of: (item) => `${sender}:${item.height}:${amount}`,
|
|
1665
|
+
pages: HISTORY_PAGES,
|
|
1666
|
+
peersOf: (found) => depositPeers(/** @type {Archive} */ (archive), net, found, { amount, bounds, calls, supplied, to }, read.counter),
|
|
1667
|
+
reach: Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS),
|
|
1668
|
+
},
|
|
1669
|
+
read.counter
|
|
1670
|
+
);
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
|
|
1673
|
+
continue;
|
|
1480
1674
|
}
|
|
1675
|
+
|
|
1676
|
+
states = history.states;
|
|
1677
|
+
waitForHistory(items, history, results);
|
|
1678
|
+
|
|
1679
|
+
if (!history.covered.length) continue;
|
|
1680
|
+
|
|
1681
|
+
given = history.given;
|
|
1682
|
+
queue = history.queue;
|
|
1683
|
+
|
|
1684
|
+
// One before it whose call can't be read could be equal: the credits'
|
|
1685
|
+
// order can't be told.
|
|
1686
|
+
for (const item of given) if (/** @type {number[]} */ (history.unread.get(item.hash)).length) unsure.add(item.hash);
|
|
1687
|
+
} else {
|
|
1688
|
+
queue = withPast(
|
|
1689
|
+
given,
|
|
1690
|
+
past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
|
|
1691
|
+
windowMs
|
|
1692
|
+
);
|
|
1481
1693
|
}
|
|
1482
1694
|
|
|
1483
1695
|
try {
|
|
@@ -1499,9 +1711,9 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
|
|
|
1499
1711
|
{ blockTimes, maxWindows, now, slackMs: CLOCK_SLACK_MS, url, used, windowMs }
|
|
1500
1712
|
);
|
|
1501
1713
|
|
|
1502
|
-
for (const [hash, arrival] of found) results.set(hash, arrival);
|
|
1714
|
+
for (const [hash, arrival] of found) results.set(hash, withHistory(hash, arrival, given, states));
|
|
1503
1715
|
} catch (error) {
|
|
1504
|
-
for (const item of
|
|
1716
|
+
for (const item of given) results.set(item.hash, withHistory(item.hash, { error: messageOf(error), reason: "error", state: "unknown" }, given, states));
|
|
1505
1717
|
}
|
|
1506
1718
|
}
|
|
1507
1719
|
}
|
|
@@ -1602,97 +1814,62 @@ export async function serviceArrivals({
|
|
|
1602
1814
|
}
|
|
1603
1815
|
|
|
1604
1816
|
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
1817
|
// The fee of each transfer the app gave, those no longer looked for too,
|
|
1630
1818
|
// and the block its payout can't be before.
|
|
1631
1819
|
const feeOf = new Map(routed.map((item) => [item.hash, item.fee ?? /** @type {Route & { kind: "service" }} */ (item.route).feeLux]));
|
|
1632
1820
|
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;
|
|
1821
|
+
// What each transfer to a route's account paid whom, its memo and its
|
|
1822
|
+
// time, from its own transaction, read once.
|
|
1823
|
+
/** @type {Map<string, TransferFacts | null>} */
|
|
1824
|
+
const facts = new Map(sent);
|
|
1647
1825
|
|
|
1648
|
-
|
|
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
|
-
}
|
|
1655
|
-
|
|
1656
|
-
for (const [i, items] of groups.entries()) {
|
|
1826
|
+
for (const items of groupBy(live, (item) => `${item.route.id}:${item.to}:${item.net}`).values()) {
|
|
1657
1827
|
const { net: payout, route, to } = items[0];
|
|
1658
|
-
const
|
|
1828
|
+
const horizonMs = horizonOf(route);
|
|
1659
1829
|
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
|
-
|
|
1830
|
+
const least = payout + [...fees].reduce((a, fee) => (fee < a ? fee : a));
|
|
1831
|
+
// Each transfer's own hours of the route's account's history: every
|
|
1832
|
+
// transfer that named the same address, in any case, and could be paid
|
|
1833
|
+
// before, the app's no longer looked for among them. Of those, the ones
|
|
1834
|
+
// that could pay out as much under the group's fees, or under their own.
|
|
1835
|
+
/** @type {Awaited<ReturnType<typeof readHistories>>} */
|
|
1836
|
+
let history;
|
|
1837
|
+
|
|
1838
|
+
try {
|
|
1839
|
+
history = await readHistories(
|
|
1840
|
+
archive,
|
|
1841
|
+
[...items].sort((a, b) => a.height - b.height),
|
|
1842
|
+
{
|
|
1843
|
+
account: route.account,
|
|
1844
|
+
direction: "in",
|
|
1845
|
+
horizonMs,
|
|
1846
|
+
keep: (entry) => entry.kind === "transfer" && entry.direction === "in" && entry.memo.toLowerCase() === to && entry.value >= payout,
|
|
1847
|
+
of: (item) => `${route.account}:${item.height}:${to}:${payout}`,
|
|
1848
|
+
pages: HISTORY_PAGES,
|
|
1849
|
+
peersOf: (found) =>
|
|
1850
|
+
servicePeers(
|
|
1851
|
+
archive,
|
|
1852
|
+
found.filter((entry) => BigInt(entry.value) >= least || feeOf.has(entry.hash)),
|
|
1853
|
+
{ bounds, facts, feeOf, fees, payout, route, to },
|
|
1854
|
+
read.counter
|
|
1855
|
+
),
|
|
1856
|
+
reach: Math.ceil((3 * horizonMs) / DUSK_BLOCK_MS),
|
|
1857
|
+
},
|
|
1858
|
+
read.counter
|
|
1859
|
+
);
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
for (const item of items) results.set(item.hash, failed(error));
|
|
1862
|
+
continue;
|
|
1681
1863
|
}
|
|
1682
1864
|
|
|
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();
|
|
1865
|
+
waitForHistory(items, history, results);
|
|
1688
1866
|
|
|
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;
|
|
1867
|
+
if (!history.covered.length) continue;
|
|
1693
1868
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1869
|
+
const { given } = history;
|
|
1870
|
+
// One before it that can't be read could be equal: the payouts' order
|
|
1871
|
+
// can't be told.
|
|
1872
|
+
const unsure = new Set(given.flatMap((item) => (/** @type {number[]} */ (history.unread.get(item.hash)).length ? [item.hash] : [])));
|
|
1696
1873
|
|
|
1697
1874
|
try {
|
|
1698
1875
|
const found = await followLogs(
|
|
@@ -1706,21 +1883,63 @@ export async function serviceArrivals({
|
|
|
1706
1883
|
},
|
|
1707
1884
|
given,
|
|
1708
1885
|
guarded: false,
|
|
1709
|
-
queue,
|
|
1886
|
+
queue: history.queue,
|
|
1710
1887
|
unsure,
|
|
1711
1888
|
},
|
|
1712
1889
|
{ blockTimes, maxWindows, now, slackMs: CLOCK_SLACK_MS, url: String(rpcUrl ?? route.chain.rpcUrl), used, windowMs: CREDIT_WINDOW_MS }
|
|
1713
1890
|
);
|
|
1714
1891
|
|
|
1715
|
-
for (const [hash, arrival] of found) results.set(hash, arrival);
|
|
1892
|
+
for (const [hash, arrival] of found) results.set(hash, withHistory(hash, arrival, given, history.states));
|
|
1716
1893
|
} catch (error) {
|
|
1717
|
-
for (const item of
|
|
1894
|
+
for (const item of given) results.set(item.hash, withHistory(item.hash, failed(error), given, history.states));
|
|
1718
1895
|
}
|
|
1719
1896
|
}
|
|
1720
1897
|
|
|
1721
1898
|
return done();
|
|
1722
1899
|
}
|
|
1723
1900
|
|
|
1901
|
+
/**
|
|
1902
|
+
* Of the transfers a route's account received naming a group's address,
|
|
1903
|
+
* those equal to the group's, from their own transactions: one the app
|
|
1904
|
+
* gave pays out what its own fee leaves, one it didn't what the group's
|
|
1905
|
+
* fees do; each with the block the app gave it its payout can't be before.
|
|
1906
|
+
* And those whose transaction can't be read. `facts` keeps each
|
|
1907
|
+
* transaction read.
|
|
1908
|
+
*
|
|
1909
|
+
* @param {Archive} archive
|
|
1910
|
+
* @param {ReadonlyArray<{ hash: string, height: number }>} found
|
|
1911
|
+
* @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
|
|
1912
|
+
* @param {{ requests: number }} counter
|
|
1913
|
+
*/
|
|
1914
|
+
async function servicePeers(archive, found, { bounds, facts, feeOf, fees, payout, route, to }, counter) {
|
|
1915
|
+
const missing = found.flatMap((entry) => (facts.has(entry.hash) ? [] : [entry.hash]));
|
|
1916
|
+
const read = await transfersOf(archive, missing, counter);
|
|
1917
|
+
|
|
1918
|
+
for (const hash of missing) facts.set(hash, read.get(hash) ?? null);
|
|
1919
|
+
|
|
1920
|
+
/** @type {Queued[]} */
|
|
1921
|
+
const peers = [];
|
|
1922
|
+
/** @type {{ hash: string, height: number }[]} */
|
|
1923
|
+
const unread = [];
|
|
1924
|
+
|
|
1925
|
+
for (const entry of found) {
|
|
1926
|
+
const fact = facts.get(entry.hash);
|
|
1927
|
+
|
|
1928
|
+
if (!fact || fact.kind === null || fact.at === null || (fact.kind === "moonlight" && (fact.value === null || fact.receiver === null))) {
|
|
1929
|
+
unread.push(entry);
|
|
1930
|
+
} else if (!fact.failed && fact.kind === "moonlight" && fact.receiver === route.account && fact.memo.toLowerCase() === to) {
|
|
1931
|
+
const value = /** @type {bigint} */ (fact.value);
|
|
1932
|
+
const own = feeOf.get(entry.hash);
|
|
1933
|
+
|
|
1934
|
+
if (own === undefined ? [...fees].some((fee) => value - fee === payout) : value - own === payout) {
|
|
1935
|
+
peers.push({ at: fact.at, given: false, hash: entry.hash, notBefore: bounds.get(entry.hash) ?? null, scan: null });
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
return { peers, unread };
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1724
1943
|
// ---------------------------------------------------------------------------
|
|
1725
1944
|
// Migrations into Dusk
|
|
1726
1945
|
// ---------------------------------------------------------------------------
|
package/src/history.d.ts
CHANGED
|
@@ -373,6 +373,8 @@ export type RangeOptions = {
|
|
|
373
373
|
direction?: "in" | "out" | "both";
|
|
374
374
|
/** The most pages to read, 1 by default. */
|
|
375
375
|
pages?: number;
|
|
376
|
+
/** The first page to read, 1 by default: a read of the same range carries on where an earlier one stopped. */
|
|
377
|
+
page?: number;
|
|
376
378
|
};
|
|
377
379
|
|
|
378
380
|
/**
|
package/src/history.js
CHANGED
|
@@ -816,7 +816,7 @@ function nextWindow(window, count) {
|
|
|
816
816
|
* @param {RangeOptions} options
|
|
817
817
|
* @returns {Promise<{ entries: HistoryEntry[], complete: boolean, requests: number }>}
|
|
818
818
|
*/
|
|
819
|
-
export async function historyRange(archive, { account, from = 0, to, direction = "both", pages = 1 }) {
|
|
819
|
+
export async function historyRange(archive, { account, from = 0, to, direction = "both", pages = 1, page: first = 1 }) {
|
|
820
820
|
/** @type {Record<string, ReadonlyArray<"in" | "out">>} */
|
|
821
821
|
const ASKED = { both: ["out", "in"], in: ["in"], out: ["out"] };
|
|
822
822
|
const aliases = Object.hasOwn(ASKED, direction) ? ASKED[direction] : null;
|
|
@@ -827,8 +827,9 @@ export async function historyRange(archive, { account, from = 0, to, direction =
|
|
|
827
827
|
const entries = [];
|
|
828
828
|
let complete = false;
|
|
829
829
|
let requests = 0;
|
|
830
|
+
const start = Math.max(1, Math.floor(Number(first)) || 1);
|
|
830
831
|
|
|
831
|
-
for (let page =
|
|
832
|
+
for (let page = start; page < start + Math.max(1, Math.floor(pages)) && !complete; page += 1) {
|
|
832
833
|
requests += 1;
|
|
833
834
|
|
|
834
835
|
const data = await archive.query(rangeQuery(account, { aliases, from, page, to }));
|