@dusk-network/app-core 0.4.0 → 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 +24 -0
- package/README.md +10 -6
- 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 +78 -20
- package/src/bridge.js +274 -708
- package/src/duskevm-withdrawals.d.ts +33 -4
- package/src/duskevm-withdrawals.js +218 -12
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.
|
|
@@ -1278,19 +903,24 @@ function exactJson(text) {
|
|
|
1278
903
|
return JSON.parse(text.replace(TOKEN, (match, string) => string ?? (/^-?\d+$/.test(match) && !Number.isSafeInteger(Number(match)) ? `"${match}"` : match)));
|
|
1279
904
|
}
|
|
1280
905
|
|
|
1281
|
-
const utf8 = new TextDecoder();
|
|
906
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
1282
907
|
|
|
1283
908
|
/**
|
|
1284
|
-
* A memo's bytes, given as hex, as text
|
|
1285
|
-
*
|
|
1286
|
-
* character; "" for none.
|
|
909
|
+
* A memo's bytes, given as hex, as UTF-8 text; "" for none, and for bytes
|
|
910
|
+
* that aren't UTF-8: an address or a hash is read from text only.
|
|
1287
911
|
*
|
|
1288
912
|
* @param {unknown} hex
|
|
1289
913
|
*/
|
|
1290
914
|
function memoOf(hex) {
|
|
1291
915
|
const text = String(hex ?? "").replace(/^0x/i, "");
|
|
1292
916
|
|
|
1293
|
-
|
|
917
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(text)) return "";
|
|
918
|
+
|
|
919
|
+
try {
|
|
920
|
+
return utf8.decode(Uint8Array.from(text.match(/../g) ?? [], (byte) => parseInt(byte, 16)));
|
|
921
|
+
} catch {
|
|
922
|
+
return "";
|
|
923
|
+
}
|
|
1294
924
|
}
|
|
1295
925
|
|
|
1296
926
|
/**
|
|
@@ -1366,10 +996,9 @@ async function transfersOf(archive, hashes, counter) {
|
|
|
1366
996
|
*
|
|
1367
997
|
* @param {Archive} archive
|
|
1368
998
|
* @param {ReadonlyArray<string>} hashes
|
|
1369
|
-
* @param {{ network?: Network, args?: boolean }} options
|
|
1370
999
|
* @param {{ requests: number }} counter
|
|
1371
1000
|
*/
|
|
1372
|
-
async function detailsOf(archive, hashes,
|
|
1001
|
+
async function detailsOf(archive, hashes, counter) {
|
|
1373
1002
|
/** @type {Map<string, any>} */
|
|
1374
1003
|
const out = new Map();
|
|
1375
1004
|
const list = [...new Set(hashes)];
|
|
@@ -1379,12 +1008,161 @@ async function detailsOf(archive, hashes, { args = false, network }, counter) {
|
|
|
1379
1008
|
|
|
1380
1009
|
counter.requests += 1;
|
|
1381
1010
|
|
|
1382
|
-
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 };
|
|
1383
1042
|
}
|
|
1384
1043
|
|
|
1385
1044
|
return out;
|
|
1386
1045
|
}
|
|
1387
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
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return out;
|
|
1139
|
+
}
|
|
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
|
+
|
|
1388
1166
|
/**
|
|
1389
1167
|
* A transfer whose history is read: its hash, its Dusk block's time and
|
|
1390
1168
|
* height, the block on the other chain its credit can't be before, and its
|
|
@@ -1512,17 +1290,18 @@ async function readHistories(archive, items, { account, direction, horizonMs, ke
|
|
|
1512
1290
|
* (`to`, `amount`), from their calls: each with the block the app gave it
|
|
1513
1291
|
* its credit can't be before; and those whose call or time the archive
|
|
1514
1292
|
* doesn't give, which could be any deposit of up to what they paid, but
|
|
1515
|
-
* 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.
|
|
1516
1295
|
*
|
|
1517
1296
|
* @param {Archive} archive
|
|
1518
1297
|
* @param {Network} net
|
|
1519
1298
|
* @param {ReadonlyArray<{ hash: string, height: number }>} found
|
|
1520
|
-
* @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
|
|
1521
1300
|
* @param {{ requests: number }} counter
|
|
1522
1301
|
*/
|
|
1523
|
-
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) {
|
|
1524
1303
|
const missing = found.flatMap((entry) => (calls.has(entry.hash) ? [] : [entry.hash]));
|
|
1525
|
-
const read = await
|
|
1304
|
+
const read = await depositCalls(archive, net, missing, cache, counter);
|
|
1526
1305
|
|
|
1527
1306
|
for (const hash of missing) calls.set(hash, read.get(hash) ?? null);
|
|
1528
1307
|
|
|
@@ -1613,39 +1392,6 @@ function withHistory(hash, arrival, given, states) {
|
|
|
1613
1392
|
// Inputs
|
|
1614
1393
|
// ---------------------------------------------------------------------------
|
|
1615
1394
|
|
|
1616
|
-
/**
|
|
1617
|
-
* A deposit as given, checked.
|
|
1618
|
-
*
|
|
1619
|
-
* @typedef {{ amount: bigint, at: number | null, hash: string, height: number | null, notBefore: bigint | null, scan: Scan | null, sender: string | null, to: string }} DepositItem
|
|
1620
|
-
*/
|
|
1621
|
-
|
|
1622
|
-
/**
|
|
1623
|
-
* @param {Deposit} deposit
|
|
1624
|
-
* @returns {DepositItem}
|
|
1625
|
-
*/
|
|
1626
|
-
function depositIn(deposit) {
|
|
1627
|
-
const hash = duskHashOf(deposit?.hash);
|
|
1628
|
-
const amount = luxOf(deposit?.amount);
|
|
1629
|
-
const to = addressOf(deposit?.to);
|
|
1630
|
-
const sender = deposit?.sender ?? null;
|
|
1631
|
-
|
|
1632
|
-
if (!hash) throw new TypeError("A deposit's hash is its Dusk transaction's: 64 hex digits");
|
|
1633
|
-
if (amount === null || amount === 0n) throw new TypeError("A deposit's amount is lux: more than 0, at most a u64");
|
|
1634
|
-
if (!to) throw new TypeError("A deposit pays a DuskEVM address: 0x and 40 hex digits");
|
|
1635
|
-
if (sender !== null && !isAccount(sender)) throw new TypeError("A deposit's sender is a Dusk account");
|
|
1636
|
-
|
|
1637
|
-
return {
|
|
1638
|
-
amount,
|
|
1639
|
-
at: msOf(deposit.at),
|
|
1640
|
-
hash,
|
|
1641
|
-
height: heightOf(deposit.height),
|
|
1642
|
-
notBefore: quantityOf(deposit.notBefore),
|
|
1643
|
-
scan: scanOf(deposit.scan),
|
|
1644
|
-
sender: /** @type {string | null} */ (sender),
|
|
1645
|
-
to,
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
1395
|
/**
|
|
1650
1396
|
* A transfer out as given, checked: its route, the fee the app gives where
|
|
1651
1397
|
* it gives one, and where an earlier search stopped.
|
|
@@ -1685,63 +1431,6 @@ function transferIn(transfer, services) {
|
|
|
1685
1431
|
*/
|
|
1686
1432
|
const horizonOf = (route) => Math.max(SERVICE_HORIZON_MS, 4 * (route.typicalSeconds ?? 0) * 1000);
|
|
1687
1433
|
|
|
1688
|
-
/**
|
|
1689
|
-
* @param {{ hash: string, at: number | null, notBefore: bigint | null, scan: Scan | null }} item
|
|
1690
|
-
* @returns {Queued}
|
|
1691
|
-
*/
|
|
1692
|
-
const queued = (item) => ({ at: /** @type {number} */ (item.at), given: true, hash: item.hash, notBefore: item.notBefore, scan: item.scan });
|
|
1693
|
-
|
|
1694
|
-
/**
|
|
1695
|
-
* Sorts out the transfers that aren't looked for: without their block's
|
|
1696
|
-
* time yet, waiting; sent longer ago than they're waited for, unknown.
|
|
1697
|
-
*
|
|
1698
|
-
* @template {{ hash: string, at: number | null }} T
|
|
1699
|
-
* @param {ReadonlyArray<T>} list
|
|
1700
|
-
* @param {Map<string, Arrival>} results
|
|
1701
|
-
* @param {{ now: number, waitMs: number }} options
|
|
1702
|
-
* @returns {T[]}
|
|
1703
|
-
*/
|
|
1704
|
-
function triage(list, results, { now, waitMs }) {
|
|
1705
|
-
/** @type {T[]} */
|
|
1706
|
-
const live = [];
|
|
1707
|
-
|
|
1708
|
-
for (const item of list) {
|
|
1709
|
-
if (item.at === null) results.set(item.hash, { state: "waiting" });
|
|
1710
|
-
else if (now - item.at > waitMs) results.set(item.hash, { reason: "expired", state: "unknown" });
|
|
1711
|
-
else live.push(item);
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
return live;
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
/**
|
|
1718
|
-
* Each item once, by hash: the first.
|
|
1719
|
-
*
|
|
1720
|
-
* @template {{ hash: string }} T
|
|
1721
|
-
* @param {ReadonlyArray<T>} list
|
|
1722
|
-
* @returns {T[]}
|
|
1723
|
-
*/
|
|
1724
|
-
function uniqueByHash(list) {
|
|
1725
|
-
const seen = new Set();
|
|
1726
|
-
|
|
1727
|
-
return list.filter((item) => !seen.has(item.hash) && Boolean(seen.add(item.hash)));
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
/**
|
|
1731
|
-
* The results in the order the transfers came.
|
|
1732
|
-
*
|
|
1733
|
-
* @param {ReadonlyArray<{ hash: string }>} list
|
|
1734
|
-
* @param {Map<string, Arrival>} results
|
|
1735
|
-
* @returns {Map<string, Arrival>}
|
|
1736
|
-
*/
|
|
1737
|
-
const inOrder = (list, results) => new Map(list.map((item) => [item.hash, /** @type {Arrival} */ (results.get(item.hash))]));
|
|
1738
|
-
|
|
1739
|
-
/**
|
|
1740
|
-
* @param {Iterable<string> | undefined} taken
|
|
1741
|
-
* @param {(value: unknown) => string} hashOf
|
|
1742
|
-
*/
|
|
1743
|
-
const usedOf = (taken, hashOf) => new Set([...(taken ?? [])].map(hashOf).filter(Boolean));
|
|
1744
|
-
|
|
1745
1434
|
// ---------------------------------------------------------------------------
|
|
1746
1435
|
// Deposits to DuskEVM
|
|
1747
1436
|
// ---------------------------------------------------------------------------
|
|
@@ -1750,183 +1439,52 @@ const usedOf = (taken, hashOf) => new Set([...(taken ?? [])].map(hashOf).filter(
|
|
|
1750
1439
|
* @param {DepositArrivalOptions} options
|
|
1751
1440
|
* @returns {Promise<{ arrivals: Map<string, Arrival>, requests: number }>}
|
|
1752
1441
|
*/
|
|
1753
|
-
export async function depositArrivals({
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
}
|
|
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
|
+
|
|
1770
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);
|
|
1771
1462
|
|
|
1772
|
-
const net =
|
|
1773
|
-
const list = uniqueByHash(Array.from(deposits ?? [], depositIn));
|
|
1463
|
+
const { evm, list, live, net, past, results, used } = depositsIn({ deposits, network, now, taken, waitMs }, scanOf);
|
|
1774
1464
|
const read = reader(fetchFn, timeoutMs);
|
|
1775
|
-
/** @type {Map<string, Arrival>} */
|
|
1776
|
-
const results = new Map();
|
|
1777
|
-
const evm = net.duskEvm;
|
|
1778
1465
|
|
|
1779
|
-
if (
|
|
1780
|
-
|
|
1466
|
+
if (evm) {
|
|
1467
|
+
const url = urlOf(rpcUrl, evm.rpcUrl, "DuskEVM RPC");
|
|
1781
1468
|
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
const used = usedOf(taken, evmHashOf);
|
|
1786
|
-
const live = triage(list, results, { now, waitMs });
|
|
1787
|
-
// Waited for too long: not looked for, though still before the newer ones.
|
|
1788
|
-
const past = list.filter((item) => item.at !== null && now - item.at > waitMs);
|
|
1789
|
-
const bridge = evm.predeploys.l2StandardBridge.toLowerCase();
|
|
1790
|
-
const rules = { now, past, results, used, windowMs };
|
|
1791
|
-
|
|
1792
|
-
if (via === "explorer") {
|
|
1793
|
-
await onExplorer(read, live, { ...rules, api: String(explorerUrl ?? evm.blockscoutApiUrl), bridge, maxPages });
|
|
1794
|
-
} else {
|
|
1795
|
-
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 });
|
|
1796
1470
|
}
|
|
1797
1471
|
|
|
1798
1472
|
return { arrivals: inOrder(list, results), requests: read.counter.requests };
|
|
1799
1473
|
}
|
|
1800
1474
|
|
|
1801
|
-
/**
|
|
1802
|
-
* The L2 bridge's payments to an address, as DuskEVM's explorer lists its
|
|
1803
|
-
* incoming calls: newest first, a page at a time (the explorer's
|
|
1804
|
-
* `next_page_params`), back past `since`, at most `maxPages`. `complete`
|
|
1805
|
-
* once they reach it, or there are no more.
|
|
1806
|
-
*
|
|
1807
|
-
* @param {Reader} read
|
|
1808
|
-
* @param {{ api: string, bridge: string, maxPages: number, since: number, to: string }} ask
|
|
1809
|
-
* @returns {Promise<{ complete: boolean, payments: Credit[] }>}
|
|
1810
|
-
*/
|
|
1811
|
-
async function explorerPayments(read, { api, bridge, maxPages, since, to }) {
|
|
1812
|
-
const base = `${api.replace(/\/+$/, "")}/addresses/${to}/internal-transactions`;
|
|
1813
|
-
/** @type {any[]} */
|
|
1814
|
-
const items = [];
|
|
1815
|
-
/** @type {Record<string, unknown> | null} */
|
|
1816
|
-
let next = null;
|
|
1817
|
-
let complete = false;
|
|
1818
|
-
|
|
1819
|
-
for (let page = 0; page < Math.max(1, Math.floor(maxPages)) && !complete; page += 1) {
|
|
1820
|
-
const query = new URLSearchParams({ filter: "to" });
|
|
1821
|
-
|
|
1822
|
-
for (const [key, value] of Object.entries(next ?? {})) query.set(key, String(value));
|
|
1823
|
-
|
|
1824
|
-
const body = await read.getJson(`${base}?${query}`);
|
|
1825
|
-
const calls = Array.isArray(body?.items) ? body.items : [];
|
|
1826
|
-
const oldest = Math.min(...calls.map((/** @type {any} */ call) => timeOf(call?.timestamp)).filter(Number.isFinite));
|
|
1827
|
-
|
|
1828
|
-
items.push(...calls);
|
|
1829
|
-
next = body?.next_page_params && typeof body.next_page_params === "object" ? body.next_page_params : null;
|
|
1830
|
-
complete = !next || !calls.length || oldest < since;
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
/** @type {Map<string, Credit>} */
|
|
1834
|
-
const payments = new Map();
|
|
1835
|
-
|
|
1836
|
-
for (const call of items) {
|
|
1837
|
-
const hash = evmHashOf(call?.transaction_hash);
|
|
1838
|
-
const at = timeOf(call?.timestamp);
|
|
1839
|
-
const value = String(call?.value ?? "");
|
|
1840
|
-
const payee = call?.to?.hash;
|
|
1841
|
-
|
|
1842
|
-
if (!hash || payments.has(hash) || !Number.isFinite(at) || !/^\d+$/.test(value)) continue;
|
|
1843
|
-
if (String(call?.from?.hash ?? "").toLowerCase() !== bridge) continue;
|
|
1844
|
-
if (payee !== undefined && payee !== null && String(payee).toLowerCase() !== to) continue;
|
|
1845
|
-
|
|
1846
|
-
payments.set(hash, { at, block: quantityOf(call?.block_number), from: null, hash, success: call?.success !== false, wei: BigInt(value) });
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
return { complete, payments: [...payments.values()] };
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
/**
|
|
1853
|
-
* Deposits followed on DuskEVM's explorer: each recipient's incoming calls
|
|
1854
|
-
* read once for all its deposits, and matched as where the credits can't be
|
|
1855
|
-
* told apart, as they don't say who deposited. Equal deposits past their
|
|
1856
|
-
* wait just before them take their payments first, their results kept.
|
|
1857
|
-
*
|
|
1858
|
-
* @param {Reader} read
|
|
1859
|
-
* @param {DepositItem[]} live
|
|
1860
|
-
* @param {{ api: string, bridge: string, maxPages: number, now: number, past: DepositItem[], results: Map<string, Arrival>, used: Set<string>, windowMs: number }} options
|
|
1861
|
-
*/
|
|
1862
|
-
async function onExplorer(read, live, { api, bridge, maxPages, now, past, results, used, windowMs }) {
|
|
1863
|
-
for (const [to, items] of groupBy(live, (item) => item.to)) {
|
|
1864
|
-
const polled = new Set(items.map((item) => item.hash));
|
|
1865
|
-
const groups = [...groupBy(items, (item) => item.amount.toString()).values()].map((group) => ({
|
|
1866
|
-
queue: withPast(
|
|
1867
|
-
group.map(queued),
|
|
1868
|
-
past.filter((item) => item.to === to && item.amount === group[0].amount).map(queued),
|
|
1869
|
-
windowMs
|
|
1870
|
-
),
|
|
1871
|
-
wei: group[0].amount * WEI_PER_LUX,
|
|
1872
|
-
}));
|
|
1873
|
-
|
|
1874
|
-
// Nothing can be told apart before a window is over: nothing is asked.
|
|
1875
|
-
if (groups.every(({ queue }) => allOpen(queue, windowMs, now))) {
|
|
1876
|
-
for (const item of items) results.set(item.hash, { state: "waiting" });
|
|
1877
|
-
continue;
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
/** @type {{ complete: boolean, payments: Credit[] }} */
|
|
1881
|
-
let page;
|
|
1882
|
-
|
|
1883
|
-
try {
|
|
1884
|
-
page = await explorerPayments(read, { api, bridge, maxPages, since: Math.min(...groups.flatMap(({ queue }) => queue.map((item) => item.at))) - windowMs, to });
|
|
1885
|
-
} catch (error) {
|
|
1886
|
-
for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
|
|
1887
|
-
continue;
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
// Not back to the oldest: a payment of an older one may be a newer one's.
|
|
1891
|
-
if (!page.complete) {
|
|
1892
|
-
for (const item of items) results.set(item.hash, { reason: "beyond", state: "unknown" });
|
|
1893
|
-
continue;
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
for (const { queue, wei } of groups) {
|
|
1897
|
-
const { assigned, verdicts } = settle(
|
|
1898
|
-
queue,
|
|
1899
|
-
page.payments.filter((payment) => payment.wei === wei),
|
|
1900
|
-
{ guarded: true, now, slackMs: windowMs, used, windowMs }
|
|
1901
|
-
);
|
|
1902
|
-
|
|
1903
|
-
for (const item of queue.filter((each) => polled.has(each.hash))) {
|
|
1904
|
-
const credit = assigned.get(item.hash);
|
|
1905
|
-
const verdict = verdicts.get(item.hash);
|
|
1906
|
-
|
|
1907
|
-
if (credit && verdict === "arrived") {
|
|
1908
|
-
used.add(credit.hash);
|
|
1909
|
-
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" });
|
|
1910
|
-
} else {
|
|
1911
|
-
results.set(item.hash, verdict === "waiting" ? { state: "waiting" } : { reason: verdict === "unsure" ? "unsure" : "late", state: "unknown" });
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
1475
|
/**
|
|
1919
1476
|
* Deposits followed on DuskEVM's RPC, by the bridge's events from each
|
|
1920
1477
|
* sender's DuskEVM address: a group of equal deposits from one sender at a
|
|
1921
1478
|
* time, with the sender's equal deposits shortly before, from its history,
|
|
1922
|
-
* when there's an archive
|
|
1923
|
-
*
|
|
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.
|
|
1924
1482
|
*
|
|
1925
1483
|
* @param {Reader} read
|
|
1926
1484
|
* @param {DepositItem[]} live
|
|
1927
|
-
* @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
|
|
1928
1486
|
*/
|
|
1929
|
-
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 }) {
|
|
1930
1488
|
// Equal deposits (the same DuskEVM address and amount) from a known sender
|
|
1931
1489
|
// first, a sender at a time, its events telling its credits apart; then
|
|
1932
1490
|
// those whose sender isn't known, which take none from the senders of the
|
|
@@ -1963,58 +1521,66 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
|
|
|
1963
1521
|
const supplied = new Map(list.map((item) => [item.hash, item]));
|
|
1964
1522
|
/** @type {Map<string, any>} */
|
|
1965
1523
|
const calls = new Map();
|
|
1966
|
-
|
|
1524
|
+
// DuskEVM's bridge on Dusk, which a sender's history lists its deposits to.
|
|
1525
|
+
const contract = duskEvmContracts(net)?.bridge ?? null;
|
|
1967
1526
|
|
|
1968
1527
|
for (const { items, rivals } of [...known, ...unknown]) {
|
|
1969
1528
|
const { amount, sender, to } = items[0];
|
|
1970
|
-
const vouched = Boolean(archive) && sender !== null && items.every((item) => item.height !== null);
|
|
1971
1529
|
let given = items.map(queued).sort(byTime);
|
|
1972
1530
|
/** @type {Queued[]} */
|
|
1973
|
-
let queue;
|
|
1531
|
+
let queue = [];
|
|
1974
1532
|
/** @type {Set<string>} */
|
|
1975
1533
|
const unsure = new Set();
|
|
1976
1534
|
/** @type {Map<string, HistoryScan>} */
|
|
1977
1535
|
let states = new Map();
|
|
1536
|
+
// Whether the sender's history tells its equal deposits' order.
|
|
1537
|
+
let vouched = false;
|
|
1978
1538
|
|
|
1979
|
-
if (
|
|
1539
|
+
if (archive && contract && sender !== null && items.every((item) => item.height !== null)) {
|
|
1980
1540
|
// Each deposit's own hours of its sender's history, with its sender's
|
|
1981
1541
|
// equal deposits there, the app's no longer looked for among them.
|
|
1982
|
-
/** @type {Awaited<ReturnType<typeof readHistories>>} */
|
|
1983
|
-
let history;
|
|
1542
|
+
/** @type {Awaited<ReturnType<typeof readHistories>> | null} */
|
|
1543
|
+
let history = null;
|
|
1984
1544
|
|
|
1985
1545
|
try {
|
|
1986
1546
|
history = await readHistories(
|
|
1987
|
-
|
|
1547
|
+
archive,
|
|
1988
1548
|
/** @type {Reading[]} */ ([...items].sort((a, b) => /** @type {number} */ (a.height) - /** @type {number} */ (b.height))),
|
|
1989
1549
|
{
|
|
1990
|
-
account:
|
|
1550
|
+
account: sender,
|
|
1991
1551
|
direction: "out",
|
|
1992
1552
|
horizonMs: DEPOSIT_HORIZON_MS,
|
|
1993
1553
|
keep: (entry) => entry.counterparty === contract && entry.value >= amount,
|
|
1994
1554
|
of: (item) => `${sender}:${item.height}:${amount}`,
|
|
1995
1555
|
pages: HISTORY_PAGES,
|
|
1996
|
-
peersOf: (found) => depositPeers(
|
|
1556
|
+
peersOf: (found) => depositPeers(archive, net, found, { amount, bounds, cache, calls, supplied, to }, read.counter),
|
|
1997
1557
|
reach: Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS),
|
|
1998
1558
|
},
|
|
1999
1559
|
read.counter
|
|
2000
1560
|
);
|
|
2001
|
-
} catch
|
|
2002
|
-
|
|
2003
|
-
|
|
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]] : [])));
|
|
2004
1565
|
}
|
|
2005
1566
|
|
|
2006
|
-
|
|
2007
|
-
|
|
1567
|
+
if (history) {
|
|
1568
|
+
vouched = true;
|
|
1569
|
+
states = history.states;
|
|
1570
|
+
waitForHistory(items, history, results);
|
|
2008
1571
|
|
|
2009
|
-
|
|
1572
|
+
if (!history.covered.length) continue;
|
|
2010
1573
|
|
|
2011
|
-
|
|
2012
|
-
|
|
1574
|
+
given = history.given;
|
|
1575
|
+
queue = history.queue;
|
|
2013
1576
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
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) {
|
|
2018
1584
|
queue = withPast(
|
|
2019
1585
|
given,
|
|
2020
1586
|
past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
|
|
@@ -2363,7 +1929,7 @@ export async function migrationPayouts({ network, migrations, entries, archive,
|
|
|
2363
1929
|
const listed = await byMemo(archive, asked.map((item) => item.hash), counter);
|
|
2364
1930
|
const found = [...listed.values()].flat().flatMap((raw) => payoutsIn(raw, /** @type {string} */ (account), net));
|
|
2365
1931
|
// Their times, and whether they went through.
|
|
2366
|
-
const details = await detailsOf(archive, found.map((payout) => payout.payout),
|
|
1932
|
+
const details = await detailsOf(archive, found.map((payout) => payout.payout), counter);
|
|
2367
1933
|
|
|
2368
1934
|
candidates = found.flatMap((payout) => {
|
|
2369
1935
|
const known = details.get(payout.payout);
|
|
@@ -2451,8 +2017,9 @@ export function serviceTransferOf(entry, network) {
|
|
|
2451
2017
|
const spent = luxOf(entry.gasSpent);
|
|
2452
2018
|
const price = luxOf(entry.gasPrice);
|
|
2453
2019
|
const amount = limit !== null && spent !== null && price !== null && (limit - spent) * price <= 0n ? value : null;
|
|
2454
|
-
// The address the memo gives, exactly as the bridge reads it.
|
|
2455
|
-
|
|
2020
|
+
// The address the memo gives, exactly as the bridge reads it: from text.
|
|
2021
|
+
// A memo of bytes that aren't text, which /history gives as 0x hex, gives none.
|
|
2022
|
+
const memo = typeof entry.memo === "string" && entry.memoBinary !== true ? entry.memo : "";
|
|
2456
2023
|
const to = EVM_ADDRESS.test(memo) ? memo : null;
|
|
2457
2024
|
const problem = entry.success === false ? "failed" : value <= route.feeLux ? "under-fee" : to === null ? "no-address" : null;
|
|
2458
2025
|
|
|
@@ -2470,9 +2037,8 @@ export function migrationPayoutOf(entry, network) {
|
|
|
2470
2037
|
const { at, hash, height, value } = factsOf(entry);
|
|
2471
2038
|
// What the transfer paid: a gas refund paid to the account with it aside.
|
|
2472
2039
|
const refund = entry?.refund === undefined || entry.refund === null ? 0n : luxOf(entry.refund);
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
.toLowerCase();
|
|
2040
|
+
// The migration's hash, as text: a memo of bytes that aren't text names none.
|
|
2041
|
+
const memo = entry?.memoBinary === true ? "" : String(entry?.memo ?? "").trim().toLowerCase();
|
|
2476
2042
|
|
|
2477
2043
|
if (!paying.length || entry.success === false || !hash || height === null || value === null || refund === null || refund > value || !EVM_HASH.test(memo)) {
|
|
2478
2044
|
return null;
|
|
@@ -2500,4 +2066,4 @@ export function arrivalsOf(arrivals) {
|
|
|
2500
2066
|
for (const [hash, arrival] of arrivals) if (arrival.state === "arrived") out.set(hash, { at: arrival.at, hash: arrival.hash });
|
|
2501
2067
|
|
|
2502
2068
|
return out;
|
|
2503
|
-
}
|
|
2069
|
+
}
|