@dusk-network/app-core 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +8 -4
- package/package.json +5 -1
- package/src/bridge-common.js +639 -0
- package/src/bridge-explorer.d.ts +34 -0
- package/src/bridge-explorer.js +186 -0
- package/src/bridge.d.ts +69 -15
- package/src/bridge.js +259 -698
- package/src/duskevm-withdrawals.d.ts +23 -5
- package/src/duskevm-withdrawals.js +79 -11
package/src/bridge.js
CHANGED
|
@@ -10,11 +10,46 @@
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { routeForAccount, routes } from "@dusk-network/networks";
|
|
14
14
|
import { isAccount } from "@dusk-network/networks/account";
|
|
15
|
-
import { BRIDGE_FUNCTIONS, duskEvmContracts, evmAddressOf } from "@dusk-network/networks/duskevm";
|
|
15
|
+
import { BRIDGE_FUNCTIONS, decodeBridgeCall, duskEvmContracts, evmAddressOf, isBridgeContract } from "@dusk-network/networks/duskevm";
|
|
16
|
+
import {
|
|
17
|
+
CREDIT_WINDOW_MS,
|
|
18
|
+
EVM_ADDRESS,
|
|
19
|
+
EVM_HASH,
|
|
20
|
+
TIMEOUT_MS,
|
|
21
|
+
WAIT_MS,
|
|
22
|
+
WEI_PER_LUX,
|
|
23
|
+
addressOf,
|
|
24
|
+
allOpen,
|
|
25
|
+
byTime,
|
|
26
|
+
depositsIn,
|
|
27
|
+
duskHashOf,
|
|
28
|
+
evmHashOf,
|
|
29
|
+
groupBy,
|
|
30
|
+
heightOf,
|
|
31
|
+
inOrder,
|
|
32
|
+
luxOf,
|
|
33
|
+
messageOf,
|
|
34
|
+
msOf,
|
|
35
|
+
networkOf,
|
|
36
|
+
pays,
|
|
37
|
+
quantityOf,
|
|
38
|
+
queued,
|
|
39
|
+
reader,
|
|
40
|
+
settle,
|
|
41
|
+
triage,
|
|
42
|
+
uniqueByHash,
|
|
43
|
+
urlOf,
|
|
44
|
+
usedOf,
|
|
45
|
+
withPast,
|
|
46
|
+
} from "./bridge-common.js";
|
|
47
|
+
import { depositArrivalsFromExplorer } from "./bridge-explorer.js";
|
|
16
48
|
import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "./history.js";
|
|
17
49
|
|
|
50
|
+
export { CREDIT_WINDOW_MS, EXPLORER_PAGES, WAIT_MS } from "./bridge-common.js";
|
|
51
|
+
export { depositArrivalsFromExplorer } from "./bridge-explorer.js";
|
|
52
|
+
|
|
18
53
|
/** @typedef {import("./bridge.js").Arrival} Arrival */
|
|
19
54
|
/** @typedef {import("./bridge.js").BlockTimes} BlockTimes */
|
|
20
55
|
/** @typedef {import("./bridge.js").Deposit} Deposit */
|
|
@@ -35,34 +70,15 @@ import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "
|
|
|
35
70
|
/** @typedef {import("@dusk-network/networks").Network} Network */
|
|
36
71
|
/** @typedef {import("@dusk-network/networks").NetworkRef} NetworkRef */
|
|
37
72
|
/** @typedef {import("@dusk-network/networks").Route} Route */
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* A credit on the other chain: its transaction, its block and time, its
|
|
49
|
-
* amount in wei, whether it went through, and the address it came from as
|
|
50
|
-
* its log names it (lower case; null where it isn't named). Found by an RPC
|
|
51
|
-
* search, the passes over its blocks again done before it was (`pass`).
|
|
52
|
-
*
|
|
53
|
-
* @typedef {{ hash: string, block: bigint | null, at: number, wei: bigint, success: boolean, from: string | null, pass?: number }} Credit
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* What a group's matching gave each transfer.
|
|
58
|
-
*
|
|
59
|
-
* @typedef {"arrived" | "waiting" | "unsure" | "late"} Verdict
|
|
60
|
-
*/
|
|
73
|
+
/** @typedef {import("./bridge.js").BridgeNetwork} BridgeNetwork */
|
|
74
|
+
/** @typedef {import("./bridge.js").FactCache} FactCache */
|
|
75
|
+
/** @typedef {import("./bridge-common.js").Credit} Credit */
|
|
76
|
+
/** @typedef {import("./bridge-common.js").DepositItem} DepositItem */
|
|
77
|
+
/** @typedef {import("./bridge-common.js").Queued} Queued */
|
|
78
|
+
/** @typedef {import("./bridge-common.js").Reader} Reader */
|
|
79
|
+
/** @typedef {import("./bridge-common.js").Rules} Rules */
|
|
61
80
|
|
|
62
81
|
export const CLOCK_SLACK_MS = 120_000;
|
|
63
|
-
export const CREDIT_WINDOW_MS = 120_000;
|
|
64
|
-
export const WAIT_MS = 7 * 24 * 60 * 60_000;
|
|
65
|
-
export const EXPLORER_PAGES = 5;
|
|
66
82
|
export const LOG_WINDOW = 5_000;
|
|
67
83
|
export const LOG_WINDOWS = 8;
|
|
68
84
|
export const DEPOSIT_HORIZON_MS = 2 * 60 * 60_000;
|
|
@@ -71,9 +87,6 @@ export const HISTORY_PAGES = 4;
|
|
|
71
87
|
export const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
72
88
|
export const ETH_BRIDGE_FINALIZED_TOPIC = "0x31b2166ff604fc5672ea5df08a78081d2bc6d746cadce880747f3643d819e83d";
|
|
73
89
|
|
|
74
|
-
const WEI_PER_LUX = 1_000_000_000n;
|
|
75
|
-
const MAX_U64 = (1n << 64n) - 1n;
|
|
76
|
-
const TIMEOUT_MS = 8_000;
|
|
77
90
|
// About one Dusk block every 10 seconds: how far back a sender's history is read.
|
|
78
91
|
const DUSK_BLOCK_MS = 10_000;
|
|
79
92
|
// The blocks the chain's pace is read over, from a block rounded down to a
|
|
@@ -99,84 +112,12 @@ const RECHECK_MS = 60_000;
|
|
|
99
112
|
const CONFIRM_PASSES = 8;
|
|
100
113
|
// What an RPC says when it won't search that many blocks at once.
|
|
101
114
|
const RANGE_REFUSED = /limit|range|too many|exceed/i;
|
|
102
|
-
const DUSK_HASH = /^[0-9a-f]{64}$/;
|
|
103
|
-
const EVM_HASH = /^0x[0-9a-f]{64}$/;
|
|
104
|
-
const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
105
115
|
const WORDS = /^0x(?:[0-9a-fA-F]{64})+$/;
|
|
106
116
|
|
|
107
117
|
// ---------------------------------------------------------------------------
|
|
108
118
|
// Values
|
|
109
119
|
// ---------------------------------------------------------------------------
|
|
110
120
|
|
|
111
|
-
/**
|
|
112
|
-
* Lux from a bigint, a safe integer or its decimal text: null for anything
|
|
113
|
-
* else, and past a u64.
|
|
114
|
-
*
|
|
115
|
-
* @param {unknown} value
|
|
116
|
-
* @returns {bigint | null}
|
|
117
|
-
*/
|
|
118
|
-
function luxOf(value) {
|
|
119
|
-
/** @type {bigint} */
|
|
120
|
-
let n;
|
|
121
|
-
|
|
122
|
-
if (typeof value === "bigint") n = value;
|
|
123
|
-
else if (typeof value === "number" && Number.isSafeInteger(value)) n = BigInt(value);
|
|
124
|
-
else if (typeof value === "string" && /^\d+$/.test(value.trim())) n = BigInt(value.trim());
|
|
125
|
-
else return null;
|
|
126
|
-
|
|
127
|
-
return n >= 0n && n <= MAX_U64 ? n : null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* A block number or a time as JSON-RPC writes it (hex), or as decimal: null
|
|
132
|
-
* for anything else.
|
|
133
|
-
*
|
|
134
|
-
* @param {unknown} value
|
|
135
|
-
* @returns {bigint | null}
|
|
136
|
-
*/
|
|
137
|
-
function quantityOf(value) {
|
|
138
|
-
if (typeof value === "bigint") return value >= 0n ? value : null;
|
|
139
|
-
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
|
|
140
|
-
|
|
141
|
-
const text = String(value ?? "").trim();
|
|
142
|
-
|
|
143
|
-
return /^0x[0-9a-fA-F]+$/.test(text) || /^\d+$/.test(text) ? BigInt(text) : null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* A Dusk transaction's hash, lower case, without 0x; "" for anything else.
|
|
148
|
-
*
|
|
149
|
-
* @param {unknown} value
|
|
150
|
-
*/
|
|
151
|
-
const duskHashOf = (value) => {
|
|
152
|
-
const hash = String(value ?? "")
|
|
153
|
-
.trim()
|
|
154
|
-
.toLowerCase()
|
|
155
|
-
.replace(/^0x/, "");
|
|
156
|
-
|
|
157
|
-
return DUSK_HASH.test(hash) ? hash : "";
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* An EVM transaction's hash, lower case; "" for anything else.
|
|
162
|
-
*
|
|
163
|
-
* @param {unknown} value
|
|
164
|
-
*/
|
|
165
|
-
const evmHashOf = (value) => {
|
|
166
|
-
const hash = String(value ?? "")
|
|
167
|
-
.trim()
|
|
168
|
-
.toLowerCase();
|
|
169
|
-
|
|
170
|
-
return EVM_HASH.test(hash) ? hash : "";
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* An EVM address, lower case; "" for anything else.
|
|
175
|
-
*
|
|
176
|
-
* @param {unknown} value
|
|
177
|
-
*/
|
|
178
|
-
const addressOf = (value) => (typeof value === "string" && EVM_ADDRESS.test(value) ? value.toLowerCase() : "");
|
|
179
|
-
|
|
180
121
|
/**
|
|
181
122
|
* An address as an indexed log topic: 32 bytes, lower case.
|
|
182
123
|
*
|
|
@@ -187,33 +128,6 @@ const topicOf = (address) => `0x${address.slice(2).toLowerCase().padStart(64, "0
|
|
|
187
128
|
/** @param {bigint} n */
|
|
188
129
|
const hex = (n) => `0x${n.toString(16)}`;
|
|
189
130
|
|
|
190
|
-
/**
|
|
191
|
-
* @param {unknown} error
|
|
192
|
-
* @returns {string}
|
|
193
|
-
*/
|
|
194
|
-
const messageOf = (error) => String(/** @type {any} */ (error)?.message ?? error ?? "");
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* A Blockscout time ("2026-10-27T05:52:13.000000Z") in milliseconds.
|
|
198
|
-
*
|
|
199
|
-
* @param {unknown} text
|
|
200
|
-
*/
|
|
201
|
-
const timeOf = (text) => Date.parse(String(text ?? "").replace(/(\.\d{3})\d+/, "$1"));
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* A time in milliseconds, or null.
|
|
205
|
-
*
|
|
206
|
-
* @param {unknown} value
|
|
207
|
-
*/
|
|
208
|
-
const msOf = (value) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null);
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* A height, or null.
|
|
212
|
-
*
|
|
213
|
-
* @param {unknown} value
|
|
214
|
-
*/
|
|
215
|
-
const heightOf = (value) => (typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null);
|
|
216
|
-
|
|
217
131
|
/**
|
|
218
132
|
* How far a transfer's history is read, as a result gave it; null for
|
|
219
133
|
* anything else.
|
|
@@ -293,286 +207,16 @@ function scanOf(value) {
|
|
|
293
207
|
return out;
|
|
294
208
|
}
|
|
295
209
|
|
|
296
|
-
/**
|
|
297
|
-
* The network a reference names; a TypeError for none there's a file for.
|
|
298
|
-
*
|
|
299
|
-
* @param {NetworkRef} network
|
|
300
|
-
* @returns {Network}
|
|
301
|
-
*/
|
|
302
|
-
function networkOf(network) {
|
|
303
|
-
const net = network && typeof network === "object" ? network : networkFor(/** @type {number | bigint | string} */ (network));
|
|
304
|
-
|
|
305
|
-
if (!net || typeof net.chainId !== "number" || !net.routes) throw new TypeError("Not one of the networks");
|
|
306
|
-
|
|
307
|
-
return net;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* @template T
|
|
312
|
-
* @param {Iterable<T>} items
|
|
313
|
-
* @param {(item: T) => string} keyOf
|
|
314
|
-
* @returns {Map<string, T[]>}
|
|
315
|
-
*/
|
|
316
|
-
function groupBy(items, keyOf) {
|
|
317
|
-
/** @type {Map<string, T[]>} */
|
|
318
|
-
const out = new Map();
|
|
319
|
-
|
|
320
|
-
for (const item of items) {
|
|
321
|
-
const key = keyOf(item);
|
|
322
|
-
|
|
323
|
-
out.set(key, [...(out.get(key) ?? []), item]);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return out;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Oldest first, then by hash, so the order is the same everywhere.
|
|
331
|
-
*
|
|
332
|
-
* @param {{ at: number, hash: string, block?: bigint | null }} a
|
|
333
|
-
* @param {{ at: number, hash: string, block?: bigint | null }} b
|
|
334
|
-
*/
|
|
335
|
-
const byTime = (a, b) =>
|
|
336
|
-
a.at - b.at || (a.block !== undefined && b.block !== undefined && a.block !== null && b.block !== null && a.block !== b.block ? (a.block < b.block ? -1 : 1) : 0) || (a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0);
|
|
337
|
-
|
|
338
210
|
/**
|
|
339
211
|
* @param {ReadonlyArray<bigint>} list
|
|
340
212
|
* @returns {bigint}
|
|
341
213
|
*/
|
|
342
214
|
const minOf = (list) => list.reduce((a, b) => (b < a ? b : a));
|
|
343
215
|
|
|
344
|
-
// ---------------------------------------------------------------------------
|
|
345
|
-
// Requests
|
|
346
|
-
// ---------------------------------------------------------------------------
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* @param {string} url
|
|
350
|
-
* @returns {string}
|
|
351
|
-
*/
|
|
352
|
-
function hostOf(url) {
|
|
353
|
-
try {
|
|
354
|
-
return new URL(url).host;
|
|
355
|
-
} catch {
|
|
356
|
-
return url;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* The app's fetch, each request within `timeoutMs`, counted.
|
|
362
|
-
*
|
|
363
|
-
* @param {FetchLike | undefined} fetchFn
|
|
364
|
-
* @param {number} timeoutMs
|
|
365
|
-
*/
|
|
366
|
-
function reader(fetchFn, timeoutMs) {
|
|
367
|
-
const counter = { requests: 0 };
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* @param {string} url
|
|
371
|
-
* @param {{ method: "GET" | "POST", headers: Record<string, string>, body?: string }} init
|
|
372
|
-
* @returns {Promise<{ ok: boolean, status: number, body: any, host: string }>}
|
|
373
|
-
*/
|
|
374
|
-
async function exchange(url, init) {
|
|
375
|
-
const controller = new AbortController();
|
|
376
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
377
|
-
const host = hostOf(url);
|
|
378
|
-
|
|
379
|
-
counter.requests += 1;
|
|
380
|
-
|
|
381
|
-
try {
|
|
382
|
-
const send = fetchFn ?? globalThis.fetch;
|
|
383
|
-
const response = await send(url, { ...init, signal: controller.signal });
|
|
384
|
-
const text = await response.text();
|
|
385
|
-
/** @type {any} */
|
|
386
|
-
let body;
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
body = JSON.parse(text);
|
|
390
|
-
} catch {
|
|
391
|
-
throw new Error(response.ok ? `${host} didn't answer with JSON` : `${host} answers with HTTP ${response.status}`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return { body, host, ok: response.ok, status: response.status };
|
|
395
|
-
} catch (error) {
|
|
396
|
-
if (controller.signal.aborted) throw new Error(`${host} didn't answer within ${Math.round(timeoutMs / 100) / 10} s`);
|
|
397
|
-
|
|
398
|
-
throw error;
|
|
399
|
-
} finally {
|
|
400
|
-
clearTimeout(timer);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return {
|
|
405
|
-
counter,
|
|
406
|
-
// What a call reads once for all its searches, by what and where: a
|
|
407
|
-
// chain's head and pace.
|
|
408
|
-
/** @type {Map<string, Promise<any>>} */
|
|
409
|
-
once: new Map(),
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* @param {string} url
|
|
413
|
-
* @returns {Promise<any>}
|
|
414
|
-
*/
|
|
415
|
-
async getJson(url) {
|
|
416
|
-
const answer = await exchange(url, { headers: { accept: "application/json" }, method: "GET" });
|
|
417
|
-
|
|
418
|
-
if (!answer.ok) throw new Error(`${answer.host} answers with HTTP ${answer.status}`);
|
|
419
|
-
|
|
420
|
-
return answer.body;
|
|
421
|
-
},
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* One JSON-RPC call; its error's message, with its code, when it refuses.
|
|
425
|
-
*
|
|
426
|
-
* @param {string} url
|
|
427
|
-
* @param {string} method
|
|
428
|
-
* @param {unknown[]} params
|
|
429
|
-
* @returns {Promise<any>}
|
|
430
|
-
*/
|
|
431
|
-
async rpc(url, method, params) {
|
|
432
|
-
const answer = await exchange(url, {
|
|
433
|
-
body: JSON.stringify({ id: 1, jsonrpc: "2.0", method, params }),
|
|
434
|
-
headers: { "content-type": "application/json" },
|
|
435
|
-
method: "POST",
|
|
436
|
-
});
|
|
437
|
-
const error = answer.body?.error;
|
|
438
|
-
|
|
439
|
-
if (error) throw Object.assign(new Error(String(error.message ?? `${method} was refused`)), { code: error.code });
|
|
440
|
-
if (!answer.ok) throw new Error(`${answer.host} answers ${method} with HTTP ${answer.status}`);
|
|
441
|
-
|
|
442
|
-
return answer.body?.result;
|
|
443
|
-
},
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/** @typedef {ReturnType<typeof reader>} Reader */
|
|
448
|
-
|
|
449
216
|
// ---------------------------------------------------------------------------
|
|
450
217
|
// Matching: each credit pays one transfer, in the order they were sent
|
|
451
218
|
// ---------------------------------------------------------------------------
|
|
452
219
|
|
|
453
|
-
/**
|
|
454
|
-
* Transfers whose windows touch, each window from `windowMs` before its
|
|
455
|
-
* time to `windowMs` after: a credit in one could be another's.
|
|
456
|
-
*
|
|
457
|
-
* @param {ReadonlyArray<Queued>} queue oldest first
|
|
458
|
-
* @param {number} windowMs
|
|
459
|
-
* @returns {Queued[][]}
|
|
460
|
-
*/
|
|
461
|
-
function clustersOf(queue, windowMs) {
|
|
462
|
-
/** @type {Queued[][]} */
|
|
463
|
-
const out = [];
|
|
464
|
-
|
|
465
|
-
for (const item of queue) {
|
|
466
|
-
const last = out[out.length - 1];
|
|
467
|
-
|
|
468
|
-
if (last && item.at - windowMs <= last[last.length - 1].at + windowMs) last.push(item);
|
|
469
|
-
else out.push([item]);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return out;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Whether every window of the queue is still open: nothing in it can be
|
|
477
|
-
* told apart yet.
|
|
478
|
-
*
|
|
479
|
-
* @param {ReadonlyArray<Queued>} queue
|
|
480
|
-
* @param {number} windowMs
|
|
481
|
-
* @param {number} now
|
|
482
|
-
*/
|
|
483
|
-
const allOpen = (queue, windowMs, now) => clustersOf(queue, windowMs).every((cluster) => now < cluster[cluster.length - 1].at + windowMs);
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* How a group's credits are matched: see `settle`.
|
|
487
|
-
*
|
|
488
|
-
* @typedef {{ guarded: boolean, now: number, slackMs: number, used: ReadonlySet<string>, windowMs: number, rivals?: ReadonlySet<string> }} Rules
|
|
489
|
-
*/
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Whether a credit could pay a transfer, whether or not another one took it:
|
|
493
|
-
* it went through, isn't one the app gave another of its transfers when
|
|
494
|
-
* this one is the app's, comes from none of `rivals`, and is in the
|
|
495
|
-
* transfer's time and at or after the block it can't be before.
|
|
496
|
-
*
|
|
497
|
-
* @param {Queued} item
|
|
498
|
-
* @param {Credit} credit
|
|
499
|
-
* @param {Rules} rules
|
|
500
|
-
*/
|
|
501
|
-
function pays(item, credit, { guarded, rivals = new Set(), slackMs, used, windowMs }) {
|
|
502
|
-
const lo = item.at - (guarded ? windowMs : slackMs);
|
|
503
|
-
const hi = guarded ? item.at + windowMs : Infinity;
|
|
504
|
-
|
|
505
|
-
return (
|
|
506
|
-
credit.success &&
|
|
507
|
-
(!item.given || !used.has(credit.hash)) &&
|
|
508
|
-
(credit.from === null || !rivals.has(credit.from)) &&
|
|
509
|
-
credit.at >= lo &&
|
|
510
|
-
credit.at <= hi &&
|
|
511
|
-
(item.notBefore === null || credit.block === null || credit.block >= item.notBefore)
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Each transfer of a queue of equal ones, oldest first, takes the first
|
|
517
|
-
* credit that went through after it that no earlier one took: from
|
|
518
|
-
* `slackMs` before it on, and at or after the block it can't be before.
|
|
519
|
-
* None comes from an address of `rivals`: the senders of other transfers
|
|
520
|
-
* the app gave, whose credits these could be. A credit the app gave another
|
|
521
|
-
* of its transfers (`used`) is none of the app's own waiting ones; a
|
|
522
|
-
* transfer read from the chain may have it, as the one it paid comes back
|
|
523
|
-
* among them.
|
|
524
|
-
*
|
|
525
|
-
* Where the queue may not hold every transfer that could have made the
|
|
526
|
-
* credits (`guarded`), a credit is a transfer's only within `windowMs`
|
|
527
|
-
* either side of it, and once the windows that touch its own are over,
|
|
528
|
-
* only when every payment of its amount in them (those that failed too) is
|
|
529
|
-
* one of those transfers' credits, or one the app gave another. Otherwise
|
|
530
|
-
* it could be another transfer's: unsure. A transfer whose windows closed
|
|
531
|
-
* without a credit is late: one coming later can't be told apart.
|
|
532
|
-
*
|
|
533
|
-
* @param {ReadonlyArray<Queued>} queue oldest first
|
|
534
|
-
* @param {ReadonlyArray<Credit>} credits
|
|
535
|
-
* @param {Rules} rules
|
|
536
|
-
* @returns {{ assigned: Map<string, Credit>, verdicts: Map<string, Verdict> }}
|
|
537
|
-
*/
|
|
538
|
-
function settle(queue, credits, rules) {
|
|
539
|
-
const { guarded, now, used, windowMs } = rules;
|
|
540
|
-
const sorted = [...credits].sort(byTime);
|
|
541
|
-
/** @type {Map<string, Credit>} */
|
|
542
|
-
const assigned = new Map();
|
|
543
|
-
const taken = new Set();
|
|
544
|
-
/** @type {Map<string, Verdict>} */
|
|
545
|
-
const verdicts = new Map();
|
|
546
|
-
|
|
547
|
-
for (const item of queue) {
|
|
548
|
-
const credit = sorted.find((c) => !taken.has(c.hash) && pays(item, c, rules));
|
|
549
|
-
|
|
550
|
-
if (credit) {
|
|
551
|
-
assigned.set(item.hash, credit);
|
|
552
|
-
taken.add(credit.hash);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (!guarded) {
|
|
557
|
-
for (const item of queue) verdicts.set(item.hash, assigned.has(item.hash) ? "arrived" : "waiting");
|
|
558
|
-
|
|
559
|
-
return { assigned, verdicts };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
for (const cluster of clustersOf(queue, windowMs)) {
|
|
563
|
-
const lo = cluster[0].at - windowMs;
|
|
564
|
-
const hi = cluster[cluster.length - 1].at + windowMs;
|
|
565
|
-
const own = new Set(cluster.flatMap((item) => (assigned.has(item.hash) ? [/** @type {Credit} */ (assigned.get(item.hash)).hash] : [])));
|
|
566
|
-
const stray = sorted.some((c) => c.at >= lo && c.at <= hi && !used.has(c.hash) && !own.has(c.hash));
|
|
567
|
-
|
|
568
|
-
for (const item of cluster) {
|
|
569
|
-
verdicts.set(item.hash, now < hi ? "waiting" : stray ? "unsure" : assigned.has(item.hash) ? "arrived" : "late");
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
return { assigned, verdicts };
|
|
574
|
-
}
|
|
575
|
-
|
|
576
220
|
/**
|
|
577
221
|
* The queue of equal transfers: the app's, and those read from the chain
|
|
578
222
|
* that were sent shortly before one of them, each within `horizonMs` of the
|
|
@@ -595,25 +239,6 @@ function queueOf(given, read, horizonMs) {
|
|
|
595
239
|
return all.filter((_, i) => kept[i]);
|
|
596
240
|
}
|
|
597
241
|
|
|
598
|
-
/**
|
|
599
|
-
* A queue of equal transfers where the credits can't be told apart, with
|
|
600
|
-
* the ones the app gave but no longer looks for (waited for too long) whose
|
|
601
|
-
* windows touch theirs: those were sent first, and take their credits
|
|
602
|
-
* first.
|
|
603
|
-
*
|
|
604
|
-
* @param {ReadonlyArray<Queued>} given
|
|
605
|
-
* @param {ReadonlyArray<Queued>} past
|
|
606
|
-
* @param {number} windowMs
|
|
607
|
-
* @returns {Queued[]}
|
|
608
|
-
*/
|
|
609
|
-
function withPast(given, past, windowMs) {
|
|
610
|
-
const polled = new Set(given.map((item) => item.hash));
|
|
611
|
-
|
|
612
|
-
return clustersOf([...given, ...past].sort(byTime), windowMs)
|
|
613
|
-
.filter((cluster) => cluster.some((item) => polled.has(item.hash)))
|
|
614
|
-
.flat();
|
|
615
|
-
}
|
|
616
|
-
|
|
617
242
|
/**
|
|
618
243
|
* The first transfer of the run of equal ones before `item`, each within
|
|
619
244
|
* `horizonMs` of the next.
|
|
@@ -1371,10 +996,9 @@ async function transfersOf(archive, hashes, counter) {
|
|
|
1371
996
|
*
|
|
1372
997
|
* @param {Archive} archive
|
|
1373
998
|
* @param {ReadonlyArray<string>} hashes
|
|
1374
|
-
* @param {{ network?: Network, args?: boolean }} options
|
|
1375
999
|
* @param {{ requests: number }} counter
|
|
1376
1000
|
*/
|
|
1377
|
-
async function detailsOf(archive, hashes,
|
|
1001
|
+
async function detailsOf(archive, hashes, counter) {
|
|
1378
1002
|
/** @type {Map<string, any>} */
|
|
1379
1003
|
const out = new Map();
|
|
1380
1004
|
const list = [...new Set(hashes)];
|
|
@@ -1384,12 +1008,161 @@ async function detailsOf(archive, hashes, { args = false, network }, counter) {
|
|
|
1384
1008
|
|
|
1385
1009
|
counter.requests += 1;
|
|
1386
1010
|
|
|
1387
|
-
for (const [hash, found] of await fetchTxDetails(archive, batch
|
|
1011
|
+
for (const [hash, found] of await fetchTxDetails(archive, batch)) out.set(hash, found);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return out;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* A deposit's call, as `tx(hash) { blockHeight blockTimestamp err tx {
|
|
1019
|
+
* callData { contractId fnName data } } }` gives it: whether it went
|
|
1020
|
+
* through, its block's time, its function, and for a call of DuskEVM's
|
|
1021
|
+
* bridge or Portal on Dusk, what it did (`bridge`, as `/history` reads it:
|
|
1022
|
+
* null for one that isn't a deposit, a proof or a claim). Null for none.
|
|
1023
|
+
*
|
|
1024
|
+
* @param {Network} net
|
|
1025
|
+
* @param {any} raw
|
|
1026
|
+
* @returns {{ bridge?: { kind: string, amount?: string, to?: string } | null, fnName: string | undefined, success: boolean, timestamp: number | null } | null}
|
|
1027
|
+
*/
|
|
1028
|
+
function callOf(net, raw) {
|
|
1029
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1030
|
+
|
|
1031
|
+
const call = raw.tx?.callData;
|
|
1032
|
+
const contractId = call?.contractId ? String(call.contractId) : undefined;
|
|
1033
|
+
const fnName = call?.fnName ? String(call.fnName) : undefined;
|
|
1034
|
+
const seconds = Number(raw.blockTimestamp);
|
|
1035
|
+
/** @type {NonNullable<ReturnType<typeof callOf>>} */
|
|
1036
|
+
const out = { fnName, success: !failedTx(raw), timestamp: Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : null };
|
|
1037
|
+
|
|
1038
|
+
if (isBridgeContract(net, contractId)) {
|
|
1039
|
+
const done = decodeBridgeCall(net, { args: typeof call?.data === "string" ? call.data : null, contractId, fnName });
|
|
1040
|
+
|
|
1041
|
+
out.bridge = done === null ? null : done.kind === "deposit" ? { amount: String(done.amountLux), kind: "deposit", to: done.recipient } : { kind: done.kind };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return out;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Whether a transaction, as the archive gives it, failed.
|
|
1049
|
+
*
|
|
1050
|
+
* @param {any} raw
|
|
1051
|
+
*/
|
|
1052
|
+
const failedTx = (raw) => raw?.err !== null && raw?.err !== undefined && raw?.err !== "";
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* A transaction's block height, as the archive gives it; null for none.
|
|
1056
|
+
*
|
|
1057
|
+
* @param {any} raw
|
|
1058
|
+
*/
|
|
1059
|
+
const heightIn = (raw) => (/^\d+$/.test(String(raw?.blockHeight)) ? heightOf(Number(raw.blockHeight)) : null);
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Whether a deposit's call, as `callOf` reads it, says all a match reads of
|
|
1063
|
+
* it: its time, and that it failed, that it's another call of DuskEVM's
|
|
1064
|
+
* bridge or Portal than a deposit, or a deposit's amount and DuskEVM
|
|
1065
|
+
* address. One that doesn't, as one whose arguments the archive gave cut
|
|
1066
|
+
* short, is asked for again: the archive may give it whole later.
|
|
1067
|
+
*
|
|
1068
|
+
* @param {ReturnType<typeof callOf>} call
|
|
1069
|
+
*/
|
|
1070
|
+
function decided(call) {
|
|
1071
|
+
if (!call || call.timestamp === null) return false;
|
|
1072
|
+
if (!call.success) return true;
|
|
1073
|
+
// Not a call of the bridge's contracts, as the network names them.
|
|
1074
|
+
if (call.bridge === undefined) return false;
|
|
1075
|
+
if (call.bridge === null) return call.fnName !== undefined && call.fnName !== BRIDGE_FUNCTIONS.deposit;
|
|
1076
|
+
|
|
1077
|
+
return call.bridge.kind !== "deposit" || (luxOf(call.bridge.amount) !== null && addressOf(call.bridge.to) !== "");
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* The calls of deposits a sender's history lists, by hash, as `callOf` reads
|
|
1082
|
+
* them: a batch a request, with the last finalized block. One that can't
|
|
1083
|
+
* change any more, in a block at or under that one, and that says all a
|
|
1084
|
+
* match reads of it (`decided`), is kept in the app's `cache`, by the
|
|
1085
|
+
* network, its bridge and the transaction's hash, and isn't asked for
|
|
1086
|
+
* again; others are asked for each time.
|
|
1087
|
+
*
|
|
1088
|
+
* @param {Archive} archive
|
|
1089
|
+
* @param {Network} net
|
|
1090
|
+
* @param {ReadonlyArray<string>} hashes
|
|
1091
|
+
* @param {FactCache | undefined} cache
|
|
1092
|
+
* @param {{ requests: number }} counter
|
|
1093
|
+
* @returns {Promise<Map<string, NonNullable<ReturnType<typeof callOf>>>>}
|
|
1094
|
+
*/
|
|
1095
|
+
async function depositCalls(archive, net, hashes, cache, counter) {
|
|
1096
|
+
/** @type {Map<string, NonNullable<ReturnType<typeof callOf>>>} */
|
|
1097
|
+
const out = new Map();
|
|
1098
|
+
/** @param {string} hash */
|
|
1099
|
+
const keyOf = (hash) => `deposit:${net.chainId}:${duskEvmContracts(net)?.bridge ?? ""}:${hash}`;
|
|
1100
|
+
/** @type {string[]} */
|
|
1101
|
+
const missing = [];
|
|
1102
|
+
|
|
1103
|
+
for (const hash of new Set(hashes)) {
|
|
1104
|
+
const kept = cache ? keptCall(net, cache, keyOf(hash), hash) : null;
|
|
1105
|
+
|
|
1106
|
+
if (kept) out.set(hash, kept);
|
|
1107
|
+
else missing.push(hash);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
for (let i = 0; i < missing.length; i += DETAILS_BATCH) {
|
|
1111
|
+
const batch = missing.slice(i, i + DETAILS_BATCH);
|
|
1112
|
+
|
|
1113
|
+
counter.requests += 1;
|
|
1114
|
+
|
|
1115
|
+
const data = await archive.query(`query { pair: lastBlockPair { json } ${batch.map((hash, j) => `t${j}: tx(hash: "${hash}") { blockHeight blockTimestamp err tx { callData { contractId fnName data } } }`).join(" ")} }`);
|
|
1116
|
+
const finalized = Number(data?.pair?.json?.last_finalized_block?.[0]);
|
|
1117
|
+
|
|
1118
|
+
batch.forEach((hash, j) => {
|
|
1119
|
+
const raw = data?.[`t${j}`];
|
|
1120
|
+
const call = callOf(net, raw);
|
|
1121
|
+
|
|
1122
|
+
if (!call) return;
|
|
1123
|
+
|
|
1124
|
+
out.set(hash, call);
|
|
1125
|
+
|
|
1126
|
+
const height = heightIn(raw);
|
|
1127
|
+
|
|
1128
|
+
if (cache && height !== null && Number.isSafeInteger(finalized) && height <= finalized && decided(call)) {
|
|
1129
|
+
try {
|
|
1130
|
+
cache.set(keyOf(hash), JSON.stringify({ hash, tx: raw }));
|
|
1131
|
+
} catch {
|
|
1132
|
+
// Not kept: asked for again next time.
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1388
1136
|
}
|
|
1389
1137
|
|
|
1390
1138
|
return out;
|
|
1391
1139
|
}
|
|
1392
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* A deposit's call kept in the app's cache, as `callOf` reads it; null where
|
|
1143
|
+
* none is kept, or what is kept isn't this transaction's call, in a block,
|
|
1144
|
+
* saying all a match reads of it.
|
|
1145
|
+
*
|
|
1146
|
+
* @param {Network} net
|
|
1147
|
+
* @param {FactCache} cache
|
|
1148
|
+
* @param {string} key
|
|
1149
|
+
* @param {string} hash
|
|
1150
|
+
*/
|
|
1151
|
+
function keptCall(net, cache, key, hash) {
|
|
1152
|
+
try {
|
|
1153
|
+
const text = cache.get(key);
|
|
1154
|
+
|
|
1155
|
+
if (typeof text !== "string" || !text) return null;
|
|
1156
|
+
|
|
1157
|
+
const kept = JSON.parse(text);
|
|
1158
|
+
const call = kept?.hash === hash && heightIn(kept.tx) !== null ? callOf(net, kept.tx) : null;
|
|
1159
|
+
|
|
1160
|
+
return decided(call) ? call : null;
|
|
1161
|
+
} catch {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1393
1166
|
/**
|
|
1394
1167
|
* A transfer whose history is read: its hash, its Dusk block's time and
|
|
1395
1168
|
* height, the block on the other chain its credit can't be before, and its
|
|
@@ -1517,17 +1290,18 @@ async function readHistories(archive, items, { account, direction, horizonMs, ke
|
|
|
1517
1290
|
* (`to`, `amount`), from their calls: each with the block the app gave it
|
|
1518
1291
|
* its credit can't be before; and those whose call or time the archive
|
|
1519
1292
|
* doesn't give, which could be any deposit of up to what they paid, but
|
|
1520
|
-
* for one the app gave: its own facts tell. `calls` keeps each call read
|
|
1293
|
+
* for one the app gave: its own facts tell. `calls` keeps each call read,
|
|
1294
|
+
* and the app's `cache` those that can't change any more.
|
|
1521
1295
|
*
|
|
1522
1296
|
* @param {Archive} archive
|
|
1523
1297
|
* @param {Network} net
|
|
1524
1298
|
* @param {ReadonlyArray<{ hash: string, height: number }>} found
|
|
1525
|
-
* @param {{ amount: bigint, to: string, bounds: ReadonlyMap<string, bigint | null>, calls: Map<string, any>, supplied: ReadonlyMap<string, DepositItem> }} group
|
|
1299
|
+
* @param {{ amount: bigint, to: string, bounds: ReadonlyMap<string, bigint | null>, cache: FactCache | undefined, calls: Map<string, any>, supplied: ReadonlyMap<string, DepositItem> }} group
|
|
1526
1300
|
* @param {{ requests: number }} counter
|
|
1527
1301
|
*/
|
|
1528
|
-
async function depositPeers(archive, net, found, { amount, bounds, calls, supplied, to }, counter) {
|
|
1302
|
+
async function depositPeers(archive, net, found, { amount, bounds, cache, calls, supplied, to }, counter) {
|
|
1529
1303
|
const missing = found.flatMap((entry) => (calls.has(entry.hash) ? [] : [entry.hash]));
|
|
1530
|
-
const read = await
|
|
1304
|
+
const read = await depositCalls(archive, net, missing, cache, counter);
|
|
1531
1305
|
|
|
1532
1306
|
for (const hash of missing) calls.set(hash, read.get(hash) ?? null);
|
|
1533
1307
|
|
|
@@ -1618,39 +1392,6 @@ function withHistory(hash, arrival, given, states) {
|
|
|
1618
1392
|
// Inputs
|
|
1619
1393
|
// ---------------------------------------------------------------------------
|
|
1620
1394
|
|
|
1621
|
-
/**
|
|
1622
|
-
* A deposit as given, checked.
|
|
1623
|
-
*
|
|
1624
|
-
* @typedef {{ amount: bigint, at: number | null, hash: string, height: number | null, notBefore: bigint | null, scan: Scan | null, sender: string | null, to: string }} DepositItem
|
|
1625
|
-
*/
|
|
1626
|
-
|
|
1627
|
-
/**
|
|
1628
|
-
* @param {Deposit} deposit
|
|
1629
|
-
* @returns {DepositItem}
|
|
1630
|
-
*/
|
|
1631
|
-
function depositIn(deposit) {
|
|
1632
|
-
const hash = duskHashOf(deposit?.hash);
|
|
1633
|
-
const amount = luxOf(deposit?.amount);
|
|
1634
|
-
const to = addressOf(deposit?.to);
|
|
1635
|
-
const sender = deposit?.sender ?? null;
|
|
1636
|
-
|
|
1637
|
-
if (!hash) throw new TypeError("A deposit's hash is its Dusk transaction's: 64 hex digits");
|
|
1638
|
-
if (amount === null || amount === 0n) throw new TypeError("A deposit's amount is lux: more than 0, at most a u64");
|
|
1639
|
-
if (!to) throw new TypeError("A deposit pays a DuskEVM address: 0x and 40 hex digits");
|
|
1640
|
-
if (sender !== null && !isAccount(sender)) throw new TypeError("A deposit's sender is a Dusk account");
|
|
1641
|
-
|
|
1642
|
-
return {
|
|
1643
|
-
amount,
|
|
1644
|
-
at: msOf(deposit.at),
|
|
1645
|
-
hash,
|
|
1646
|
-
height: heightOf(deposit.height),
|
|
1647
|
-
notBefore: quantityOf(deposit.notBefore),
|
|
1648
|
-
scan: scanOf(deposit.scan),
|
|
1649
|
-
sender: /** @type {string | null} */ (sender),
|
|
1650
|
-
to,
|
|
1651
|
-
};
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
1395
|
/**
|
|
1655
1396
|
* A transfer out as given, checked: its route, the fee the app gives where
|
|
1656
1397
|
* it gives one, and where an earlier search stopped.
|
|
@@ -1690,63 +1431,6 @@ function transferIn(transfer, services) {
|
|
|
1690
1431
|
*/
|
|
1691
1432
|
const horizonOf = (route) => Math.max(SERVICE_HORIZON_MS, 4 * (route.typicalSeconds ?? 0) * 1000);
|
|
1692
1433
|
|
|
1693
|
-
/**
|
|
1694
|
-
* @param {{ hash: string, at: number | null, notBefore: bigint | null, scan: Scan | null }} item
|
|
1695
|
-
* @returns {Queued}
|
|
1696
|
-
*/
|
|
1697
|
-
const queued = (item) => ({ at: /** @type {number} */ (item.at), given: true, hash: item.hash, notBefore: item.notBefore, scan: item.scan });
|
|
1698
|
-
|
|
1699
|
-
/**
|
|
1700
|
-
* Sorts out the transfers that aren't looked for: without their block's
|
|
1701
|
-
* time yet, waiting; sent longer ago than they're waited for, unknown.
|
|
1702
|
-
*
|
|
1703
|
-
* @template {{ hash: string, at: number | null }} T
|
|
1704
|
-
* @param {ReadonlyArray<T>} list
|
|
1705
|
-
* @param {Map<string, Arrival>} results
|
|
1706
|
-
* @param {{ now: number, waitMs: number }} options
|
|
1707
|
-
* @returns {T[]}
|
|
1708
|
-
*/
|
|
1709
|
-
function triage(list, results, { now, waitMs }) {
|
|
1710
|
-
/** @type {T[]} */
|
|
1711
|
-
const live = [];
|
|
1712
|
-
|
|
1713
|
-
for (const item of list) {
|
|
1714
|
-
if (item.at === null) results.set(item.hash, { state: "waiting" });
|
|
1715
|
-
else if (now - item.at > waitMs) results.set(item.hash, { reason: "expired", state: "unknown" });
|
|
1716
|
-
else live.push(item);
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
return live;
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
/**
|
|
1723
|
-
* Each item once, by hash: the first.
|
|
1724
|
-
*
|
|
1725
|
-
* @template {{ hash: string }} T
|
|
1726
|
-
* @param {ReadonlyArray<T>} list
|
|
1727
|
-
* @returns {T[]}
|
|
1728
|
-
*/
|
|
1729
|
-
function uniqueByHash(list) {
|
|
1730
|
-
const seen = new Set();
|
|
1731
|
-
|
|
1732
|
-
return list.filter((item) => !seen.has(item.hash) && Boolean(seen.add(item.hash)));
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
/**
|
|
1736
|
-
* The results in the order the transfers came.
|
|
1737
|
-
*
|
|
1738
|
-
* @param {ReadonlyArray<{ hash: string }>} list
|
|
1739
|
-
* @param {Map<string, Arrival>} results
|
|
1740
|
-
* @returns {Map<string, Arrival>}
|
|
1741
|
-
*/
|
|
1742
|
-
const inOrder = (list, results) => new Map(list.map((item) => [item.hash, /** @type {Arrival} */ (results.get(item.hash))]));
|
|
1743
|
-
|
|
1744
|
-
/**
|
|
1745
|
-
* @param {Iterable<string> | undefined} taken
|
|
1746
|
-
* @param {(value: unknown) => string} hashOf
|
|
1747
|
-
*/
|
|
1748
|
-
const usedOf = (taken, hashOf) => new Set([...(taken ?? [])].map(hashOf).filter(Boolean));
|
|
1749
|
-
|
|
1750
1434
|
// ---------------------------------------------------------------------------
|
|
1751
1435
|
// Deposits to DuskEVM
|
|
1752
1436
|
// ---------------------------------------------------------------------------
|
|
@@ -1755,183 +1439,52 @@ const usedOf = (taken, hashOf) => new Set([...(taken ?? [])].map(hashOf).filter(
|
|
|
1755
1439
|
* @param {DepositArrivalOptions} options
|
|
1756
1440
|
* @returns {Promise<{ arrivals: Map<string, Arrival>, requests: number }>}
|
|
1757
1441
|
*/
|
|
1758
|
-
export async function depositArrivals({
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
}
|
|
1442
|
+
export async function depositArrivals(options) {
|
|
1443
|
+
const {
|
|
1444
|
+
network,
|
|
1445
|
+
deposits,
|
|
1446
|
+
via = "explorer",
|
|
1447
|
+
fetch: fetchFn,
|
|
1448
|
+
rpcUrl,
|
|
1449
|
+
archive,
|
|
1450
|
+
taken,
|
|
1451
|
+
blockTimes,
|
|
1452
|
+
cache,
|
|
1453
|
+
now = Date.now(),
|
|
1454
|
+
waitMs = WAIT_MS,
|
|
1455
|
+
windowMs = CREDIT_WINDOW_MS,
|
|
1456
|
+
maxWindows = LOG_WINDOWS,
|
|
1457
|
+
timeoutMs = TIMEOUT_MS,
|
|
1458
|
+
} = options;
|
|
1459
|
+
|
|
1775
1460
|
if (via !== "explorer" && via !== "rpc") throw new RangeError('A deposit is followed on the "explorer" or the "rpc"');
|
|
1461
|
+
if (via === "explorer") return depositArrivalsFromExplorer(options);
|
|
1776
1462
|
|
|
1777
|
-
const net =
|
|
1778
|
-
const list = uniqueByHash(Array.from(deposits ?? [], depositIn));
|
|
1463
|
+
const { evm, list, live, net, past, results, used } = depositsIn({ deposits, network, now, taken, waitMs }, scanOf);
|
|
1779
1464
|
const read = reader(fetchFn, timeoutMs);
|
|
1780
|
-
/** @type {Map<string, Arrival>} */
|
|
1781
|
-
const results = new Map();
|
|
1782
|
-
const evm = net.duskEvm;
|
|
1783
1465
|
|
|
1784
|
-
if (
|
|
1785
|
-
|
|
1466
|
+
if (evm) {
|
|
1467
|
+
const url = urlOf(rpcUrl, evm.rpcUrl, "DuskEVM RPC");
|
|
1786
1468
|
|
|
1787
|
-
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
const used = usedOf(taken, evmHashOf);
|
|
1791
|
-
const live = triage(list, results, { now, waitMs });
|
|
1792
|
-
// Waited for too long: not looked for, though still before the newer ones.
|
|
1793
|
-
const past = list.filter((item) => item.at !== null && now - item.at > waitMs);
|
|
1794
|
-
const bridge = evm.predeploys.l2StandardBridge.toLowerCase();
|
|
1795
|
-
const rules = { now, past, results, used, windowMs };
|
|
1796
|
-
|
|
1797
|
-
if (via === "explorer") {
|
|
1798
|
-
await onExplorer(read, live, { ...rules, api: String(explorerUrl ?? evm.blockscoutApiUrl), bridge, maxPages });
|
|
1799
|
-
} else {
|
|
1800
|
-
await onRpc(read, live, { ...rules, archive, blockTimes, bridge, list, maxWindows, net, url: String(rpcUrl ?? evm.rpcUrl) });
|
|
1469
|
+
await onRpc(read, live, { archive, blockTimes, bridge: evm.predeploys.l2StandardBridge.toLowerCase(), cache, list, maxWindows, net, now, past, results, url, used, windowMs });
|
|
1801
1470
|
}
|
|
1802
1471
|
|
|
1803
1472
|
return { arrivals: inOrder(list, results), requests: read.counter.requests };
|
|
1804
1473
|
}
|
|
1805
1474
|
|
|
1806
|
-
/**
|
|
1807
|
-
* The L2 bridge's payments to an address, as DuskEVM's explorer lists its
|
|
1808
|
-
* incoming calls: newest first, a page at a time (the explorer's
|
|
1809
|
-
* `next_page_params`), back past `since`, at most `maxPages`. `complete`
|
|
1810
|
-
* once they reach it, or there are no more.
|
|
1811
|
-
*
|
|
1812
|
-
* @param {Reader} read
|
|
1813
|
-
* @param {{ api: string, bridge: string, maxPages: number, since: number, to: string }} ask
|
|
1814
|
-
* @returns {Promise<{ complete: boolean, payments: Credit[] }>}
|
|
1815
|
-
*/
|
|
1816
|
-
async function explorerPayments(read, { api, bridge, maxPages, since, to }) {
|
|
1817
|
-
const base = `${api.replace(/\/+$/, "")}/addresses/${to}/internal-transactions`;
|
|
1818
|
-
/** @type {any[]} */
|
|
1819
|
-
const items = [];
|
|
1820
|
-
/** @type {Record<string, unknown> | null} */
|
|
1821
|
-
let next = null;
|
|
1822
|
-
let complete = false;
|
|
1823
|
-
|
|
1824
|
-
for (let page = 0; page < Math.max(1, Math.floor(maxPages)) && !complete; page += 1) {
|
|
1825
|
-
const query = new URLSearchParams({ filter: "to" });
|
|
1826
|
-
|
|
1827
|
-
for (const [key, value] of Object.entries(next ?? {})) query.set(key, String(value));
|
|
1828
|
-
|
|
1829
|
-
const body = await read.getJson(`${base}?${query}`);
|
|
1830
|
-
const calls = Array.isArray(body?.items) ? body.items : [];
|
|
1831
|
-
const oldest = Math.min(...calls.map((/** @type {any} */ call) => timeOf(call?.timestamp)).filter(Number.isFinite));
|
|
1832
|
-
|
|
1833
|
-
items.push(...calls);
|
|
1834
|
-
next = body?.next_page_params && typeof body.next_page_params === "object" ? body.next_page_params : null;
|
|
1835
|
-
complete = !next || !calls.length || oldest < since;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
/** @type {Map<string, Credit>} */
|
|
1839
|
-
const payments = new Map();
|
|
1840
|
-
|
|
1841
|
-
for (const call of items) {
|
|
1842
|
-
const hash = evmHashOf(call?.transaction_hash);
|
|
1843
|
-
const at = timeOf(call?.timestamp);
|
|
1844
|
-
const value = String(call?.value ?? "");
|
|
1845
|
-
const payee = call?.to?.hash;
|
|
1846
|
-
|
|
1847
|
-
if (!hash || payments.has(hash) || !Number.isFinite(at) || !/^\d+$/.test(value)) continue;
|
|
1848
|
-
if (String(call?.from?.hash ?? "").toLowerCase() !== bridge) continue;
|
|
1849
|
-
if (payee !== undefined && payee !== null && String(payee).toLowerCase() !== to) continue;
|
|
1850
|
-
|
|
1851
|
-
payments.set(hash, { at, block: quantityOf(call?.block_number), from: null, hash, success: call?.success !== false, wei: BigInt(value) });
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
return { complete, payments: [...payments.values()] };
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
/**
|
|
1858
|
-
* Deposits followed on DuskEVM's explorer: each recipient's incoming calls
|
|
1859
|
-
* read once for all its deposits, and matched as where the credits can't be
|
|
1860
|
-
* told apart, as they don't say who deposited. Equal deposits past their
|
|
1861
|
-
* wait just before them take their payments first, their results kept.
|
|
1862
|
-
*
|
|
1863
|
-
* @param {Reader} read
|
|
1864
|
-
* @param {DepositItem[]} live
|
|
1865
|
-
* @param {{ api: string, bridge: string, maxPages: number, now: number, past: DepositItem[], results: Map<string, Arrival>, used: Set<string>, windowMs: number }} options
|
|
1866
|
-
*/
|
|
1867
|
-
async function onExplorer(read, live, { api, bridge, maxPages, now, past, results, used, windowMs }) {
|
|
1868
|
-
for (const [to, items] of groupBy(live, (item) => item.to)) {
|
|
1869
|
-
const polled = new Set(items.map((item) => item.hash));
|
|
1870
|
-
const groups = [...groupBy(items, (item) => item.amount.toString()).values()].map((group) => ({
|
|
1871
|
-
queue: withPast(
|
|
1872
|
-
group.map(queued),
|
|
1873
|
-
past.filter((item) => item.to === to && item.amount === group[0].amount).map(queued),
|
|
1874
|
-
windowMs
|
|
1875
|
-
),
|
|
1876
|
-
wei: group[0].amount * WEI_PER_LUX,
|
|
1877
|
-
}));
|
|
1878
|
-
|
|
1879
|
-
// Nothing can be told apart before a window is over: nothing is asked.
|
|
1880
|
-
if (groups.every(({ queue }) => allOpen(queue, windowMs, now))) {
|
|
1881
|
-
for (const item of items) results.set(item.hash, { state: "waiting" });
|
|
1882
|
-
continue;
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
/** @type {{ complete: boolean, payments: Credit[] }} */
|
|
1886
|
-
let page;
|
|
1887
|
-
|
|
1888
|
-
try {
|
|
1889
|
-
page = await explorerPayments(read, { api, bridge, maxPages, since: Math.min(...groups.flatMap(({ queue }) => queue.map((item) => item.at))) - windowMs, to });
|
|
1890
|
-
} catch (error) {
|
|
1891
|
-
for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
|
|
1892
|
-
continue;
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
// Not back to the oldest: a payment of an older one may be a newer one's.
|
|
1896
|
-
if (!page.complete) {
|
|
1897
|
-
for (const item of items) results.set(item.hash, { reason: "beyond", state: "unknown" });
|
|
1898
|
-
continue;
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
for (const { queue, wei } of groups) {
|
|
1902
|
-
const { assigned, verdicts } = settle(
|
|
1903
|
-
queue,
|
|
1904
|
-
page.payments.filter((payment) => payment.wei === wei),
|
|
1905
|
-
{ guarded: true, now, slackMs: windowMs, used, windowMs }
|
|
1906
|
-
);
|
|
1907
|
-
|
|
1908
|
-
for (const item of queue.filter((each) => polled.has(each.hash))) {
|
|
1909
|
-
const credit = assigned.get(item.hash);
|
|
1910
|
-
const verdict = verdicts.get(item.hash);
|
|
1911
|
-
|
|
1912
|
-
if (credit && verdict === "arrived") {
|
|
1913
|
-
used.add(credit.hash);
|
|
1914
|
-
results.set(item.hash, { amount: credit.wei / WEI_PER_LUX, at: credit.at, block: credit.block === null ? null : Number(credit.block), hash: credit.hash, state: "arrived" });
|
|
1915
|
-
} else {
|
|
1916
|
-
results.set(item.hash, verdict === "waiting" ? { state: "waiting" } : { reason: verdict === "unsure" ? "unsure" : "late", state: "unknown" });
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
1475
|
/**
|
|
1924
1476
|
* Deposits followed on DuskEVM's RPC, by the bridge's events from each
|
|
1925
1477
|
* sender's DuskEVM address: a group of equal deposits from one sender at a
|
|
1926
1478
|
* time, with the sender's equal deposits shortly before, from its history,
|
|
1927
|
-
* when there's an archive
|
|
1928
|
-
*
|
|
1479
|
+
* when there's an archive and the network names DuskEVM's bridge on Dusk;
|
|
1480
|
+
* without them, or while the archive can't be read, with the equal ones
|
|
1481
|
+
* given that are past their wait just before them.
|
|
1929
1482
|
*
|
|
1930
1483
|
* @param {Reader} read
|
|
1931
1484
|
* @param {DepositItem[]} live
|
|
1932
|
-
* @param {{ archive: Archive | undefined, blockTimes: BlockTimes | undefined, bridge: string, list: DepositItem[], maxWindows: number, net: Network, now: number, past: DepositItem[], results: Map<string, Arrival>, url: string, used: Set<string>, windowMs: number }} options
|
|
1485
|
+
* @param {{ archive: Archive | undefined, blockTimes: BlockTimes | undefined, bridge: string, cache: FactCache | undefined, list: DepositItem[], maxWindows: number, net: Network, now: number, past: DepositItem[], results: Map<string, Arrival>, url: string, used: Set<string>, windowMs: number }} options
|
|
1933
1486
|
*/
|
|
1934
|
-
async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows, net, now, past, results, url, used, windowMs }) {
|
|
1487
|
+
async function onRpc(read, live, { archive, blockTimes, bridge, cache, list, maxWindows, net, now, past, results, url, used, windowMs }) {
|
|
1935
1488
|
// Equal deposits (the same DuskEVM address and amount) from a known sender
|
|
1936
1489
|
// first, a sender at a time, its events telling its credits apart; then
|
|
1937
1490
|
// those whose sender isn't known, which take none from the senders of the
|
|
@@ -1968,58 +1521,66 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
|
|
|
1968
1521
|
const supplied = new Map(list.map((item) => [item.hash, item]));
|
|
1969
1522
|
/** @type {Map<string, any>} */
|
|
1970
1523
|
const calls = new Map();
|
|
1971
|
-
|
|
1524
|
+
// DuskEVM's bridge on Dusk, which a sender's history lists its deposits to.
|
|
1525
|
+
const contract = duskEvmContracts(net)?.bridge ?? null;
|
|
1972
1526
|
|
|
1973
1527
|
for (const { items, rivals } of [...known, ...unknown]) {
|
|
1974
1528
|
const { amount, sender, to } = items[0];
|
|
1975
|
-
const vouched = Boolean(archive) && sender !== null && items.every((item) => item.height !== null);
|
|
1976
1529
|
let given = items.map(queued).sort(byTime);
|
|
1977
1530
|
/** @type {Queued[]} */
|
|
1978
|
-
let queue;
|
|
1531
|
+
let queue = [];
|
|
1979
1532
|
/** @type {Set<string>} */
|
|
1980
1533
|
const unsure = new Set();
|
|
1981
1534
|
/** @type {Map<string, HistoryScan>} */
|
|
1982
1535
|
let states = new Map();
|
|
1536
|
+
// Whether the sender's history tells its equal deposits' order.
|
|
1537
|
+
let vouched = false;
|
|
1983
1538
|
|
|
1984
|
-
if (
|
|
1539
|
+
if (archive && contract && sender !== null && items.every((item) => item.height !== null)) {
|
|
1985
1540
|
// Each deposit's own hours of its sender's history, with its sender's
|
|
1986
1541
|
// equal deposits there, the app's no longer looked for among them.
|
|
1987
|
-
/** @type {Awaited<ReturnType<typeof readHistories>>} */
|
|
1988
|
-
let history;
|
|
1542
|
+
/** @type {Awaited<ReturnType<typeof readHistories>> | null} */
|
|
1543
|
+
let history = null;
|
|
1989
1544
|
|
|
1990
1545
|
try {
|
|
1991
1546
|
history = await readHistories(
|
|
1992
|
-
|
|
1547
|
+
archive,
|
|
1993
1548
|
/** @type {Reading[]} */ ([...items].sort((a, b) => /** @type {number} */ (a.height) - /** @type {number} */ (b.height))),
|
|
1994
1549
|
{
|
|
1995
|
-
account:
|
|
1550
|
+
account: sender,
|
|
1996
1551
|
direction: "out",
|
|
1997
1552
|
horizonMs: DEPOSIT_HORIZON_MS,
|
|
1998
1553
|
keep: (entry) => entry.counterparty === contract && entry.value >= amount,
|
|
1999
1554
|
of: (item) => `${sender}:${item.height}:${amount}`,
|
|
2000
1555
|
pages: HISTORY_PAGES,
|
|
2001
|
-
peersOf: (found) => depositPeers(
|
|
1556
|
+
peersOf: (found) => depositPeers(archive, net, found, { amount, bounds, cache, calls, supplied, to }, read.counter),
|
|
2002
1557
|
reach: Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS),
|
|
2003
1558
|
},
|
|
2004
1559
|
read.counter
|
|
2005
1560
|
);
|
|
2006
|
-
} catch
|
|
2007
|
-
|
|
2008
|
-
|
|
1561
|
+
} catch {
|
|
1562
|
+
// The archive can't be read now: followed as without it, its history
|
|
1563
|
+
// read on by a later call from where it stopped.
|
|
1564
|
+
states = new Map(items.flatMap((item) => (item.scan?.history ? [[item.hash, item.scan.history]] : [])));
|
|
2009
1565
|
}
|
|
2010
1566
|
|
|
2011
|
-
|
|
2012
|
-
|
|
1567
|
+
if (history) {
|
|
1568
|
+
vouched = true;
|
|
1569
|
+
states = history.states;
|
|
1570
|
+
waitForHistory(items, history, results);
|
|
2013
1571
|
|
|
2014
|
-
|
|
1572
|
+
if (!history.covered.length) continue;
|
|
2015
1573
|
|
|
2016
|
-
|
|
2017
|
-
|
|
1574
|
+
given = history.given;
|
|
1575
|
+
queue = history.queue;
|
|
2018
1576
|
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
1577
|
+
// One before it whose call can't be read could be equal: the credits'
|
|
1578
|
+
// order can't be told.
|
|
1579
|
+
for (const item of given) if (/** @type {number[]} */ (history.unread.get(item.hash)).length) unsure.add(item.hash);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
if (!vouched) {
|
|
2023
1584
|
queue = withPast(
|
|
2024
1585
|
given,
|
|
2025
1586
|
past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
|
|
@@ -2368,7 +1929,7 @@ export async function migrationPayouts({ network, migrations, entries, archive,
|
|
|
2368
1929
|
const listed = await byMemo(archive, asked.map((item) => item.hash), counter);
|
|
2369
1930
|
const found = [...listed.values()].flat().flatMap((raw) => payoutsIn(raw, /** @type {string} */ (account), net));
|
|
2370
1931
|
// Their times, and whether they went through.
|
|
2371
|
-
const details = await detailsOf(archive, found.map((payout) => payout.payout),
|
|
1932
|
+
const details = await detailsOf(archive, found.map((payout) => payout.payout), counter);
|
|
2372
1933
|
|
|
2373
1934
|
candidates = found.flatMap((payout) => {
|
|
2374
1935
|
const known = details.get(payout.payout);
|
|
@@ -2505,4 +2066,4 @@ export function arrivalsOf(arrivals) {
|
|
|
2505
2066
|
for (const [hash, arrival] of arrivals) if (arrival.state === "arrived") out.set(hash, { at: arrival.at, hash: arrival.hash });
|
|
2506
2067
|
|
|
2507
2068
|
return out;
|
|
2508
|
-
}
|
|
2069
|
+
}
|