@dusk-network/app-core 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +53 -6
- package/package.json +19 -2
- package/src/bridge.d.ts +55 -14
- package/src/bridge.js +439 -199
- package/src/duskevm-withdrawals.d.ts +535 -0
- package/src/duskevm-withdrawals.js +2429 -0
- package/src/history.d.ts +11 -2
- package/src/history.js +14 -6
package/src/bridge.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { networkFor, routeForAccount, routes } from "@dusk-network/networks";
|
|
14
14
|
import { isAccount } from "@dusk-network/networks/account";
|
|
15
|
-
import { duskEvmContracts, evmAddressOf } from "@dusk-network/networks/duskevm";
|
|
15
|
+
import { BRIDGE_FUNCTIONS, duskEvmContracts, evmAddressOf } from "@dusk-network/networks/duskevm";
|
|
16
16
|
import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "./history.js";
|
|
17
17
|
|
|
18
18
|
/** @typedef {import("./bridge.js").Arrival} Arrival */
|
|
@@ -21,6 +21,7 @@ import { DETAILS_BATCH, TRANSFER_CONTRACT, fetchTxDetails, historyRange } from "
|
|
|
21
21
|
/** @typedef {import("./bridge.js").DepositArrivalOptions} DepositArrivalOptions */
|
|
22
22
|
/** @typedef {import("./bridge.js").FetchLike} FetchLike */
|
|
23
23
|
/** @typedef {import("./bridge.js").HistoryLike} HistoryLike */
|
|
24
|
+
/** @typedef {import("./bridge.js").HistoryScan} HistoryScan */
|
|
24
25
|
/** @typedef {import("./bridge.js").Migration} Migration */
|
|
25
26
|
/** @typedef {import("./bridge.js").MigrationPayout} MigrationPayout */
|
|
26
27
|
/** @typedef {import("./bridge.js").MigrationPayoutOptions} MigrationPayoutOptions */
|
|
@@ -199,9 +200,42 @@ const msOf = (value) => (typeof value === "number" && Number.isFinite(value) &&
|
|
|
199
200
|
*/
|
|
200
201
|
const heightOf = (value) => (typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null);
|
|
201
202
|
|
|
203
|
+
/**
|
|
204
|
+
* How far a transfer's history is read, as a result gave it; null for
|
|
205
|
+
* anything else.
|
|
206
|
+
*
|
|
207
|
+
* @param {unknown} value
|
|
208
|
+
* @returns {HistoryScan | null}
|
|
209
|
+
*/
|
|
210
|
+
function historyScanOf(value) {
|
|
211
|
+
const read = /** @type {any} */ (value);
|
|
212
|
+
|
|
213
|
+
if (!read || typeof read !== "object" || typeof read.of !== "string" || !Array.isArray(read.found)) return null;
|
|
214
|
+
|
|
215
|
+
const lo = read.lo === null ? null : heightOf(read.lo);
|
|
216
|
+
const next = read.next === null ? null : { from: heightOf(read.next?.from), page: heightOf(read.next?.page), to: heightOf(read.next?.to) };
|
|
217
|
+
|
|
218
|
+
if ((read.lo !== null && lo === null) || (lo === null && next === null)) return null;
|
|
219
|
+
if (next && (next.from === null || next.to === null || next.page === null || next.page < 1 || next.from > next.to)) return null;
|
|
220
|
+
|
|
221
|
+
const found = read.found.flatMap((/** @type {any} */ each) => {
|
|
222
|
+
const hash = duskHashOf(each?.hash);
|
|
223
|
+
const height = heightOf(each?.height);
|
|
224
|
+
const value = each?.value;
|
|
225
|
+
|
|
226
|
+
return hash && height !== null && typeof value === "string" && /^\d{1,20}$/.test(value) ? [{ hash, height, value }] : [];
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// One left out could be equal: it's read afresh.
|
|
230
|
+
if (found.length !== read.found.length) return null;
|
|
231
|
+
|
|
232
|
+
return { found, lo, next: /** @type {HistoryScan["next"]} */ (next), of: read.of };
|
|
233
|
+
}
|
|
234
|
+
|
|
202
235
|
/**
|
|
203
236
|
* Where a search stopped, what it searched for, and the credits it found
|
|
204
|
-
* before,
|
|
237
|
+
* before, and how far the transfer's history is read, as a result gave
|
|
238
|
+
* them; null for anything else.
|
|
205
239
|
*
|
|
206
240
|
* @param {unknown} value
|
|
207
241
|
* @returns {Scan | null}
|
|
@@ -209,19 +243,26 @@ const heightOf = (value) => (typeof value === "number" && Number.isSafeInteger(v
|
|
|
209
243
|
function scanOf(value) {
|
|
210
244
|
const scan = /** @type {any} */ (value);
|
|
211
245
|
const from = quantityOf(scan?.from);
|
|
246
|
+
const history = historyScanOf(scan?.history);
|
|
247
|
+
/** @type {Scan} */
|
|
248
|
+
const out = history ? { history } : {};
|
|
212
249
|
|
|
213
|
-
if (from
|
|
250
|
+
if (from !== null && typeof scan?.basis === "number" && Number.isFinite(scan.basis) && typeof scan.search === "string") {
|
|
251
|
+
const seen = (Array.isArray(scan.seen) ? scan.seen : []).flatMap((/** @type {any} */ credit) => {
|
|
252
|
+
const hash = evmHashOf(credit?.hash);
|
|
253
|
+
const block = quantityOf(credit?.block);
|
|
254
|
+
const at = msOf(credit?.at);
|
|
255
|
+
const sender = credit?.sender === null ? null : addressOf(credit?.sender);
|
|
214
256
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const block = quantityOf(credit?.block);
|
|
218
|
-
const at = msOf(credit?.at);
|
|
219
|
-
const sender = credit?.sender === null ? null : addressOf(credit?.sender);
|
|
257
|
+
return hash && block !== null && at !== null && sender !== "" ? [{ at, block: block.toString(), hash, sender }] : [];
|
|
258
|
+
});
|
|
220
259
|
|
|
221
|
-
|
|
222
|
-
})
|
|
260
|
+
Object.assign(out, { basis: scan.basis, from: from.toString(), search: scan.search, seen });
|
|
261
|
+
} else if (!history) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
223
264
|
|
|
224
|
-
return
|
|
265
|
+
return out;
|
|
225
266
|
}
|
|
226
267
|
|
|
227
268
|
/**
|
|
@@ -780,7 +821,10 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
780
821
|
// The logs searched for: a search for others, as for another sender or
|
|
781
822
|
// amount, isn't carried on.
|
|
782
823
|
const search = [filter.address, ...filter.topics.map((topic) => topic ?? "*"), filter.wei.toString()].join(":");
|
|
783
|
-
|
|
824
|
+
/** @type {{ basis: number, from: string, seen?: Scan["seen"] }[]} */
|
|
825
|
+
const resumed = live.flatMap(({ scan }) =>
|
|
826
|
+
scan?.from !== undefined && scan.search === search && scan.basis !== undefined && scan.basis <= first.at ? [{ basis: scan.basis, from: scan.from, seen: scan.seen }] : []
|
|
827
|
+
);
|
|
784
828
|
/** @type {Map<string, Credit>} */
|
|
785
829
|
const carried = new Map();
|
|
786
830
|
|
|
@@ -797,9 +841,18 @@ async function followLogs(read, { filter, given, guarded, queue, rivals, unsure
|
|
|
797
841
|
/** @type {bigint} */
|
|
798
842
|
let start;
|
|
799
843
|
|
|
800
|
-
if (resumed.length) {
|
|
844
|
+
if (resumed.length && resumed.every((scan) => scan.basis === first.at)) {
|
|
801
845
|
start = minOf(resumed.map((scan) => BigInt(scan.from)));
|
|
802
846
|
head = await headOf(read, url);
|
|
847
|
+
} else if (resumed.length) {
|
|
848
|
+
// Begun from a transfer before the first now, as one that since
|
|
849
|
+
// arrived: none of the queue's credits is before where a search from
|
|
850
|
+
// the first starts, which it skips to.
|
|
851
|
+
const from = minOf(resumed.map((scan) => BigInt(scan.from)));
|
|
852
|
+
const fresh = first.notBefore !== null ? { head: await headOf(read, url), start: first.notBefore } : await startBefore(read, url, reach, blockTimes);
|
|
853
|
+
|
|
854
|
+
head = fresh.head;
|
|
855
|
+
start = from > fresh.start ? from : fresh.start;
|
|
803
856
|
} else if (first.notBefore !== null) {
|
|
804
857
|
start = first.notBefore;
|
|
805
858
|
head = await headOf(read, url);
|
|
@@ -1016,50 +1069,214 @@ async function detailsOf(archive, hashes, { args = false, network }, counter) {
|
|
|
1016
1069
|
}
|
|
1017
1070
|
|
|
1018
1071
|
/**
|
|
1019
|
-
* A
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
|
|
1072
|
+
* A transfer whose history is read: its hash, its Dusk block's time and
|
|
1073
|
+
* height, the block on the other chain its credit can't be before, and its
|
|
1074
|
+
* scan.
|
|
1075
|
+
*
|
|
1076
|
+
* @typedef {{ hash: string, at: number, height: number, notBefore: bigint | null, scan: Scan | null }} Reading
|
|
1077
|
+
*/
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* The history a group of equal transfers needs, each transfer's read on
|
|
1081
|
+
* from where its scan stopped: its account's history from `reach` blocks
|
|
1082
|
+
* under its block up to it, and `reach` further down at a time while the
|
|
1083
|
+
* run of equal transfers it is in reaches within `horizonMs` of the bottom.
|
|
1084
|
+
* The transfer itself must be listed there: until it is, the archive
|
|
1085
|
+
* hasn't read its block. Each transfer reads `pages` pages at most, so a
|
|
1086
|
+
* later one's history never keeps an earlier one waiting.
|
|
1087
|
+
*
|
|
1088
|
+
* Those whose history is all read are `covered`, and `given` as the queue
|
|
1089
|
+
* holds them, with `queue`: theirs and the equal transfers found
|
|
1090
|
+
* (`peersOf`), and `unread`: by transfer, the heights it read of those that
|
|
1091
|
+
* could be equal but can't be read. Each one's reading, done or not, is in
|
|
1092
|
+
* `states`.
|
|
1093
|
+
*
|
|
1094
|
+
* @param {Archive} archive
|
|
1095
|
+
* @param {ReadonlyArray<Reading>} items
|
|
1096
|
+
* @param {{ account: string, direction: "in" | "out", horizonMs: number, reach: number, pages: number, keep: (entry: HistoryEntry) => boolean, of: (item: Reading) => string, peersOf: (found: HistoryScan["found"]) => Promise<{ peers: Queued[], unread: ReadonlyArray<{ hash: string, height: number }> }> }} read
|
|
1097
|
+
* @param {{ requests: number }} counter
|
|
1098
|
+
*/
|
|
1099
|
+
async function readHistories(archive, items, { account, direction, horizonMs, keep, of, pages, peersOf, reach }, counter) {
|
|
1100
|
+
const walks = items.map((item) => {
|
|
1101
|
+
const kept = item.scan?.history;
|
|
1102
|
+
/** @type {HistoryScan} */
|
|
1103
|
+
const state =
|
|
1104
|
+
kept && kept.of === of(item)
|
|
1105
|
+
? structuredClone(/** @type {HistoryScan} */ (kept))
|
|
1106
|
+
: { found: [], lo: null, next: { from: Math.max(0, item.height - reach), page: 1, to: item.height }, of: of(item) };
|
|
1107
|
+
|
|
1108
|
+
return { item, left: pages, state };
|
|
1109
|
+
});
|
|
1110
|
+
/** @type {Reading[]} */
|
|
1111
|
+
let covered = [];
|
|
1112
|
+
/** @type {Queued[]} */
|
|
1113
|
+
let given = [];
|
|
1114
|
+
/** @type {Queued[]} */
|
|
1115
|
+
let queue = [];
|
|
1116
|
+
/** @type {Map<string, number[]>} */
|
|
1117
|
+
const unread = new Map();
|
|
1118
|
+
|
|
1119
|
+
for (;;) {
|
|
1120
|
+
// Each one's next range, as far as its pages go.
|
|
1121
|
+
for (const walk of walks) {
|
|
1122
|
+
const { state } = walk;
|
|
1123
|
+
|
|
1124
|
+
while (state.next && walk.left > 0) {
|
|
1125
|
+
const next = /** @type {NonNullable<HistoryScan["next"]>} */ (state.next);
|
|
1126
|
+
const range = await historyRange(archive, { account, direction, from: next.from, page: next.page, pages: walk.left, to: next.to });
|
|
1127
|
+
const found = /** @type {HistoryScan["found"][number][]} */ (state.found);
|
|
1128
|
+
|
|
1129
|
+
counter.requests += range.requests;
|
|
1130
|
+
walk.left -= range.requests;
|
|
1131
|
+
|
|
1132
|
+
for (const entry of range.entries) {
|
|
1133
|
+
if (keep(entry) && !found.some((each) => each.hash === entry.hash)) found.push({ hash: entry.hash, height: entry.blockHeight, value: String(entry.value) });
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (!range.complete) {
|
|
1137
|
+
state.next = { ...next, page: next.page + range.requests };
|
|
1138
|
+
} else if (state.lo === null && !found.some((each) => each.hash === walk.item.hash)) {
|
|
1139
|
+
// Its own block isn't read yet: its last page is read again.
|
|
1140
|
+
state.next = { ...next, page: next.page + range.requests - 1 };
|
|
1141
|
+
walk.left = 0;
|
|
1142
|
+
} else {
|
|
1143
|
+
state.lo = next.from;
|
|
1144
|
+
state.next = null;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const done = walks.filter((walk) => !walk.state.next);
|
|
1150
|
+
|
|
1151
|
+
if (!done.length) {
|
|
1152
|
+
covered = [];
|
|
1153
|
+
given = [];
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
given = done.map((walk) => queued(walk.item)).sort(byTime);
|
|
1158
|
+
|
|
1159
|
+
const mine = new Set(given.map((item) => item.hash));
|
|
1160
|
+
// The others found: those given are in the queue as they are.
|
|
1161
|
+
const { peers, unread: unknown } = await peersOf(uniqueByHash(done.flatMap((walk) => walk.state.found)).filter((entry) => !mine.has(entry.hash)));
|
|
1162
|
+
let deeper = false;
|
|
1163
|
+
|
|
1164
|
+
queue = queueOf(given, peers, horizonMs);
|
|
1165
|
+
|
|
1166
|
+
// A run reaching within a horizon of the bottom may go on under it: the
|
|
1167
|
+
// history is read further down.
|
|
1168
|
+
for (const walk of done) {
|
|
1169
|
+
const lo = /** @type {number} */ (walk.state.lo);
|
|
1170
|
+
const bottomAt = lo === 0 ? -Infinity : walk.item.at - (walk.item.height - lo) * DUSK_BLOCK_MS;
|
|
1171
|
+
const self = /** @type {Queued} */ (given.find((each) => each.hash === walk.item.hash));
|
|
1172
|
+
|
|
1173
|
+
if (runStart(queue, self, horizonMs).at - bottomAt < horizonMs) {
|
|
1174
|
+
walk.state.next = { from: Math.max(0, lo - reach), page: 1, to: lo - 1 };
|
|
1175
|
+
deeper = true;
|
|
1176
|
+
} else {
|
|
1177
|
+
unread.set(
|
|
1178
|
+
walk.item.hash,
|
|
1179
|
+
unknown.flatMap((each) => (each.height >= lo && each.height <= walk.item.height ? [each.height] : []))
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (!deeper) {
|
|
1185
|
+
covered = done.map((walk) => walk.item);
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return { covered, given, queue, states: new Map(walks.map((walk) => [walk.item.hash, walk.state])), unread };
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Of the deposits a sender's history lists, those equal to the group's
|
|
1195
|
+
* (`to`, `amount`), from their calls: each with the block the app gave it
|
|
1196
|
+
* its credit can't be before; and those whose call or time the archive
|
|
1197
|
+
* doesn't give, which could be any deposit of up to what they paid, but
|
|
1198
|
+
* for one the app gave: its own facts tell. `calls` keeps each call read.
|
|
1025
1199
|
*
|
|
1026
1200
|
* @param {Archive} archive
|
|
1027
1201
|
* @param {Network} net
|
|
1028
|
-
* @param {{
|
|
1202
|
+
* @param {ReadonlyArray<{ hash: string, height: number }>} found
|
|
1203
|
+
* @param {{ amount: bigint, to: string, bounds: ReadonlyMap<string, bigint | null>, calls: Map<string, any>, supplied: ReadonlyMap<string, DepositItem> }} group
|
|
1029
1204
|
* @param {{ requests: number }} counter
|
|
1030
1205
|
*/
|
|
1031
|
-
async function
|
|
1032
|
-
const
|
|
1033
|
-
const read = await
|
|
1206
|
+
async function depositPeers(archive, net, found, { amount, bounds, calls, supplied, to }, counter) {
|
|
1207
|
+
const missing = found.flatMap((entry) => (calls.has(entry.hash) ? [] : [entry.hash]));
|
|
1208
|
+
const read = await detailsOf(archive, missing, { args: true, network: net }, counter);
|
|
1034
1209
|
|
|
1035
|
-
|
|
1210
|
+
for (const hash of missing) calls.set(hash, read.get(hash) ?? null);
|
|
1036
1211
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1212
|
+
/** @type {Queued[]} */
|
|
1213
|
+
const peers = [];
|
|
1214
|
+
/** @type {{ hash: string, height: number }[]} */
|
|
1215
|
+
const unread = [];
|
|
1039
1216
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const deposits = [];
|
|
1044
|
-
/** @type {{ hash: string, height: number, value: bigint }[]} */
|
|
1045
|
-
const unresolved = [];
|
|
1217
|
+
for (const entry of found) {
|
|
1218
|
+
const call = calls.get(entry.hash);
|
|
1219
|
+
const bridged = call?.bridge;
|
|
1046
1220
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1221
|
+
// Failed, or a call that isn't a deposit: no DUSK left for DuskEVM. A
|
|
1222
|
+
// deposit whose arguments can't be read could be any.
|
|
1223
|
+
if (call?.success === false || (bridged && bridged.kind !== "deposit") || (bridged === null && call?.fnName !== BRIDGE_FUNCTIONS.deposit)) continue;
|
|
1050
1224
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1225
|
+
const own = supplied.get(entry.hash);
|
|
1226
|
+
let at = msOf(call?.timestamp);
|
|
1227
|
+
let paid = luxOf(bridged?.amount);
|
|
1228
|
+
let recipient = addressOf(bridged?.to);
|
|
1053
1229
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const recipient = addressOf(bridged?.to);
|
|
1230
|
+
// One the app gave whose call can't be read: its own facts.
|
|
1231
|
+
if (own && (!call || at === null || paid === null || !recipient)) ({ amount: paid, at, to: recipient } = own);
|
|
1057
1232
|
|
|
1058
|
-
if (
|
|
1059
|
-
else
|
|
1233
|
+
if (at === null || paid === null || !recipient) unread.push(entry);
|
|
1234
|
+
else if (recipient === to && paid === amount) peers.push({ at, given: false, hash: entry.hash, notBefore: bounds.get(entry.hash) ?? null, scan: null });
|
|
1060
1235
|
}
|
|
1061
1236
|
|
|
1062
|
-
return {
|
|
1237
|
+
return { peers, unread };
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Where a scan's search stopped, without its history.
|
|
1242
|
+
*
|
|
1243
|
+
* @param {Scan | null | undefined} scan
|
|
1244
|
+
* @returns {Scan}
|
|
1245
|
+
*/
|
|
1246
|
+
const searchOf = (scan) => (scan?.from !== undefined ? { basis: scan.basis, from: scan.from, search: scan.search, seen: scan.seen } : {});
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* The transfers whose history isn't all read yet wait, with where their
|
|
1250
|
+
* reading stopped beside where their last search did.
|
|
1251
|
+
*
|
|
1252
|
+
* @param {ReadonlyArray<{ hash: string, scan: Scan | null }>} items
|
|
1253
|
+
* @param {{ covered: ReadonlyArray<{ hash: string }>, states: ReadonlyMap<string, HistoryScan> }} history
|
|
1254
|
+
* @param {Map<string, Arrival>} results
|
|
1255
|
+
*/
|
|
1256
|
+
function waitForHistory(items, { covered, states }, results) {
|
|
1257
|
+
const read = new Set(covered.map((item) => item.hash));
|
|
1258
|
+
|
|
1259
|
+
for (const item of items) {
|
|
1260
|
+
if (!read.has(item.hash)) results.set(item.hash, { scan: { ...searchOf(item.scan), history: /** @type {HistoryScan} */ (states.get(item.hash)) }, state: "waiting" });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* A result, with how far its transfer's history is read, which every one
|
|
1266
|
+
* but an arrival keeps in its scan for the next call.
|
|
1267
|
+
*
|
|
1268
|
+
* @param {string} hash
|
|
1269
|
+
* @param {Arrival} arrival
|
|
1270
|
+
* @param {ReadonlyArray<Queued>} given
|
|
1271
|
+
* @param {ReadonlyMap<string, HistoryScan>} states
|
|
1272
|
+
* @returns {Arrival}
|
|
1273
|
+
*/
|
|
1274
|
+
function withHistory(hash, arrival, given, states) {
|
|
1275
|
+
const history = states.get(hash);
|
|
1276
|
+
|
|
1277
|
+
if (!history || arrival.state === "arrived") return arrival;
|
|
1278
|
+
|
|
1279
|
+
return { ...arrival, scan: { ...(arrival.scan ?? searchOf(given.find((each) => each.hash === hash)?.scan)), history } };
|
|
1063
1280
|
}
|
|
1064
1281
|
|
|
1065
1282
|
// ---------------------------------------------------------------------------
|
|
@@ -1380,34 +1597,6 @@ async function onExplorer(read, live, { api, bridge, maxPages, now, past, result
|
|
|
1380
1597
|
* @param {{ archive: Archive | undefined, blockTimes: BlockTimes | undefined, bridge: string, list: DepositItem[], maxWindows: number, net: Network, now: number, past: DepositItem[], results: Map<string, Arrival>, url: string, used: Set<string>, windowMs: number }} options
|
|
1381
1598
|
*/
|
|
1382
1599
|
async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows, net, now, past, results, url, used, windowMs }) {
|
|
1383
|
-
/** @type {Map<string, Awaited<ReturnType<typeof senderDeposits>> & { bottom: number, top: number, topAt: number }>} */
|
|
1384
|
-
const histories = new Map();
|
|
1385
|
-
|
|
1386
|
-
// The senders' deposits shortly before theirs, one history read a sender:
|
|
1387
|
-
// the app's deposits no longer looked for among them.
|
|
1388
|
-
if (archive) {
|
|
1389
|
-
const reach = Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS);
|
|
1390
|
-
|
|
1391
|
-
for (const [sender, items] of groupBy(
|
|
1392
|
-
live.filter((item) => item.sender !== null && item.height !== null),
|
|
1393
|
-
(item) => /** @type {string} */ (item.sender)
|
|
1394
|
-
)) {
|
|
1395
|
-
const heights = items.map((item) => /** @type {number} */ (item.height));
|
|
1396
|
-
const top = Math.max(...heights);
|
|
1397
|
-
const bottom = Math.max(0, Math.min(...heights) - reach);
|
|
1398
|
-
const least = items.reduce((a, item) => (item.amount < a ? item.amount : a), MAX_U64);
|
|
1399
|
-
|
|
1400
|
-
try {
|
|
1401
|
-
const found = await senderDeposits(archive, net, { from: bottom, least, sender, skip: new Set(items.map((item) => item.hash)), to: top }, read.counter);
|
|
1402
|
-
const topAt = /** @type {number} */ (items.find((item) => item.height === top)?.at);
|
|
1403
|
-
|
|
1404
|
-
histories.set(sender, { ...found, bottom, top, topAt });
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
1600
|
// Equal deposits (the same DuskEVM address and amount) from a known sender
|
|
1412
1601
|
// first, a sender at a time, its events telling its credits apart; then
|
|
1413
1602
|
// those whose sender isn't known, which take none from the senders of the
|
|
@@ -1438,46 +1627,69 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
|
|
|
1438
1627
|
if (rest.length) unknown.push({ items: rest, rivals: aliases.get(key) ?? new Set() });
|
|
1439
1628
|
}
|
|
1440
1629
|
|
|
1441
|
-
// The block each deposit given can't be credited before, the app's own
|
|
1630
|
+
// The block each deposit given can't be credited before, the app's own,
|
|
1631
|
+
// the deposits given, and the deposits' calls, read once.
|
|
1442
1632
|
const bounds = new Map(list.map((item) => [item.hash, item.notBefore]));
|
|
1633
|
+
const supplied = new Map(list.map((item) => [item.hash, item]));
|
|
1634
|
+
/** @type {Map<string, any>} */
|
|
1635
|
+
const calls = new Map();
|
|
1636
|
+
const contract = duskEvmContracts(net)?.bridge;
|
|
1443
1637
|
|
|
1444
1638
|
for (const { items, rivals } of [...known, ...unknown]) {
|
|
1445
1639
|
const { amount, sender, to } = items[0];
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
? /** @type {NonNullable<typeof history>} */ (history).deposits
|
|
1451
|
-
.filter((deposit) => deposit.to === to && deposit.amount === amount)
|
|
1452
|
-
.map((deposit) => ({ at: deposit.at, given: false, hash: deposit.hash, notBefore: bounds.get(deposit.hash) ?? null, scan: null }))
|
|
1453
|
-
: [];
|
|
1454
|
-
const queue = vouched
|
|
1455
|
-
? queueOf(given, alike, DEPOSIT_HORIZON_MS)
|
|
1456
|
-
: withPast(
|
|
1457
|
-
given,
|
|
1458
|
-
past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
|
|
1459
|
-
windowMs
|
|
1460
|
-
);
|
|
1640
|
+
const vouched = Boolean(archive) && sender !== null && items.every((item) => item.height !== null);
|
|
1641
|
+
let given = items.map(queued).sort(byTime);
|
|
1642
|
+
/** @type {Queued[]} */
|
|
1643
|
+
let queue;
|
|
1461
1644
|
/** @type {Set<string>} */
|
|
1462
1645
|
const unsure = new Set();
|
|
1646
|
+
/** @type {Map<string, HistoryScan>} */
|
|
1647
|
+
let states = new Map();
|
|
1463
1648
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1649
|
+
if (vouched) {
|
|
1650
|
+
// Each deposit's own hours of its sender's history, with its sender's
|
|
1651
|
+
// equal deposits there, the app's no longer looked for among them.
|
|
1652
|
+
/** @type {Awaited<ReturnType<typeof readHistories>>} */
|
|
1653
|
+
let history;
|
|
1654
|
+
|
|
1655
|
+
try {
|
|
1656
|
+
history = await readHistories(
|
|
1657
|
+
/** @type {Archive} */ (archive),
|
|
1658
|
+
/** @type {Reading[]} */ ([...items].sort((a, b) => /** @type {number} */ (a.height) - /** @type {number} */ (b.height))),
|
|
1659
|
+
{
|
|
1660
|
+
account: /** @type {string} */ (sender),
|
|
1661
|
+
direction: "out",
|
|
1662
|
+
horizonMs: DEPOSIT_HORIZON_MS,
|
|
1663
|
+
keep: (entry) => entry.counterparty === contract && entry.value >= amount,
|
|
1664
|
+
of: (item) => `${sender}:${item.height}:${amount}`,
|
|
1665
|
+
pages: HISTORY_PAGES,
|
|
1666
|
+
peersOf: (found) => depositPeers(/** @type {Archive} */ (archive), net, found, { amount, bounds, calls, supplied, to }, read.counter),
|
|
1667
|
+
reach: Math.ceil((3 * DEPOSIT_HORIZON_MS) / DUSK_BLOCK_MS),
|
|
1668
|
+
},
|
|
1669
|
+
read.counter
|
|
1670
|
+
);
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
for (const item of items) results.set(item.hash, { error: messageOf(error), reason: "error", state: "unknown" });
|
|
1673
|
+
continue;
|
|
1480
1674
|
}
|
|
1675
|
+
|
|
1676
|
+
states = history.states;
|
|
1677
|
+
waitForHistory(items, history, results);
|
|
1678
|
+
|
|
1679
|
+
if (!history.covered.length) continue;
|
|
1680
|
+
|
|
1681
|
+
given = history.given;
|
|
1682
|
+
queue = history.queue;
|
|
1683
|
+
|
|
1684
|
+
// One before it whose call can't be read could be equal: the credits'
|
|
1685
|
+
// order can't be told.
|
|
1686
|
+
for (const item of given) if (/** @type {number[]} */ (history.unread.get(item.hash)).length) unsure.add(item.hash);
|
|
1687
|
+
} else {
|
|
1688
|
+
queue = withPast(
|
|
1689
|
+
given,
|
|
1690
|
+
past.filter((item) => item.to === to && item.amount === amount && item.sender === sender).map(queued),
|
|
1691
|
+
windowMs
|
|
1692
|
+
);
|
|
1481
1693
|
}
|
|
1482
1694
|
|
|
1483
1695
|
try {
|
|
@@ -1499,9 +1711,9 @@ async function onRpc(read, live, { archive, blockTimes, bridge, list, maxWindows
|
|
|
1499
1711
|
{ blockTimes, maxWindows, now, slackMs: CLOCK_SLACK_MS, url, used, windowMs }
|
|
1500
1712
|
);
|
|
1501
1713
|
|
|
1502
|
-
for (const [hash, arrival] of found) results.set(hash, arrival);
|
|
1714
|
+
for (const [hash, arrival] of found) results.set(hash, withHistory(hash, arrival, given, states));
|
|
1503
1715
|
} catch (error) {
|
|
1504
|
-
for (const item of
|
|
1716
|
+
for (const item of given) results.set(item.hash, withHistory(item.hash, { error: messageOf(error), reason: "error", state: "unknown" }, given, states));
|
|
1505
1717
|
}
|
|
1506
1718
|
}
|
|
1507
1719
|
}
|
|
@@ -1602,97 +1814,62 @@ export async function serviceArrivals({
|
|
|
1602
1814
|
}
|
|
1603
1815
|
|
|
1604
1816
|
const live = triage(known, results, { now, waitMs });
|
|
1605
|
-
/** @type {Map<string, { complete: boolean, entries: HistoryEntry[], bottom: number, top: number, topAt: number, horizonMs: number }>} */
|
|
1606
|
-
const histories = new Map();
|
|
1607
|
-
|
|
1608
|
-
// Each route's account's history, back from its transfers: every transfer
|
|
1609
|
-
// that named the same address, in any case, and could be paid before.
|
|
1610
|
-
for (const [id, items] of groupBy(live, (item) => item.route.id)) {
|
|
1611
|
-
const { route } = items[0];
|
|
1612
|
-
const horizonMs = horizonOf(route);
|
|
1613
|
-
const heights = items.map((item) => item.height);
|
|
1614
|
-
const top = Math.max(...heights);
|
|
1615
|
-
const bottom = Math.max(0, Math.min(...heights) - Math.ceil((3 * horizonMs) / DUSK_BLOCK_MS));
|
|
1616
|
-
|
|
1617
|
-
try {
|
|
1618
|
-
const range = await historyRange(archive, { account: route.account, direction: "in", from: bottom, pages: HISTORY_PAGES, to: top });
|
|
1619
|
-
|
|
1620
|
-
read.counter.requests += range.requests;
|
|
1621
|
-
histories.set(id, { bottom, complete: range.complete, entries: range.entries, horizonMs, top, topAt: /** @type {SentItem} */ (items.find((item) => item.height === top)).at });
|
|
1622
|
-
} catch (error) {
|
|
1623
|
-
for (const item of items) results.set(item.hash, failed(error));
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
const groups = [...groupBy(live.filter((item) => histories.has(item.route.id)), (item) => `${item.route.id}:${item.to}:${item.net}`).values()];
|
|
1628
|
-
const polled = new Set(groups.flat().map((item) => item.hash));
|
|
1629
1817
|
// The fee of each transfer the app gave, those no longer looked for too,
|
|
1630
1818
|
// and the block its payout can't be before.
|
|
1631
1819
|
const feeOf = new Map(routed.map((item) => [item.hash, item.fee ?? /** @type {Route & { kind: "service" }} */ (item.route).feeLux]));
|
|
1632
1820
|
const bounds = new Map(routed.map((item) => [item.hash, item.notBefore]));
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
const others = groups.map((items) => {
|
|
1638
|
-
const { route, to } = items[0];
|
|
1639
|
-
const least = items.reduce((a, item) => (item.net + item.fee < a ? item.net + item.fee : a), MAX_U64);
|
|
1640
|
-
|
|
1641
|
-
return (/** @type {NonNullable<ReturnType<typeof histories.get>>} */ (histories.get(route.id)).entries).filter(
|
|
1642
|
-
(entry) => !polled.has(entry.hash) && entry.kind === "transfer" && entry.direction === "in" && entry.memo.toLowerCase() === to && (entry.value >= least || feeOf.has(entry.hash))
|
|
1643
|
-
);
|
|
1644
|
-
});
|
|
1645
|
-
/** @type {Map<string, TransferFacts>} */
|
|
1646
|
-
let peers;
|
|
1647
|
-
|
|
1648
|
-
try {
|
|
1649
|
-
peers = new Map([...sent, ...(await transfersOf(archive, others.flat().flatMap((entry) => (sent.has(entry.hash) ? [] : [entry.hash])), read.counter))]);
|
|
1650
|
-
} catch (error) {
|
|
1651
|
-
for (const items of groups) for (const item of items) results.set(item.hash, failed(error));
|
|
1652
|
-
|
|
1653
|
-
return done();
|
|
1654
|
-
}
|
|
1821
|
+
// What each transfer to a route's account paid whom, its memo and its
|
|
1822
|
+
// time, from its own transaction, read once.
|
|
1823
|
+
/** @type {Map<string, TransferFacts | null>} */
|
|
1824
|
+
const facts = new Map(sent);
|
|
1655
1825
|
|
|
1656
|
-
for (const
|
|
1826
|
+
for (const items of groupBy(live, (item) => `${item.route.id}:${item.to}:${item.net}`).values()) {
|
|
1657
1827
|
const { net: payout, route, to } = items[0];
|
|
1658
|
-
const
|
|
1828
|
+
const horizonMs = horizonOf(route);
|
|
1659
1829
|
const fees = new Set(items.map((item) => item.fee));
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1830
|
+
const least = payout + [...fees].reduce((a, fee) => (fee < a ? fee : a));
|
|
1831
|
+
// Each transfer's own hours of the route's account's history: every
|
|
1832
|
+
// transfer that named the same address, in any case, and could be paid
|
|
1833
|
+
// before, the app's no longer looked for among them. Of those, the ones
|
|
1834
|
+
// that could pay out as much under the group's fees, or under their own.
|
|
1835
|
+
/** @type {Awaited<ReturnType<typeof readHistories>>} */
|
|
1836
|
+
let history;
|
|
1837
|
+
|
|
1838
|
+
try {
|
|
1839
|
+
history = await readHistories(
|
|
1840
|
+
archive,
|
|
1841
|
+
[...items].sort((a, b) => a.height - b.height),
|
|
1842
|
+
{
|
|
1843
|
+
account: route.account,
|
|
1844
|
+
direction: "in",
|
|
1845
|
+
horizonMs,
|
|
1846
|
+
keep: (entry) => entry.kind === "transfer" && entry.direction === "in" && entry.memo.toLowerCase() === to && entry.value >= payout,
|
|
1847
|
+
of: (item) => `${route.account}:${item.height}:${to}:${payout}`,
|
|
1848
|
+
pages: HISTORY_PAGES,
|
|
1849
|
+
peersOf: (found) =>
|
|
1850
|
+
servicePeers(
|
|
1851
|
+
archive,
|
|
1852
|
+
found.filter((entry) => BigInt(entry.value) >= least || feeOf.has(entry.hash)),
|
|
1853
|
+
{ bounds, facts, feeOf, fees, payout, route, to },
|
|
1854
|
+
read.counter
|
|
1855
|
+
),
|
|
1856
|
+
reach: Math.ceil((3 * horizonMs) / DUSK_BLOCK_MS),
|
|
1857
|
+
},
|
|
1858
|
+
read.counter
|
|
1859
|
+
);
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
for (const item of items) results.set(item.hash, failed(error));
|
|
1862
|
+
continue;
|
|
1681
1863
|
}
|
|
1682
1864
|
|
|
1683
|
-
|
|
1684
|
-
const queue = queueOf(given, alike, history.horizonMs);
|
|
1685
|
-
const bottomAt = history.bottom === 0 ? -Infinity : history.topAt - (history.top - history.bottom) * DUSK_BLOCK_MS;
|
|
1686
|
-
/** @type {Set<string>} */
|
|
1687
|
-
const unsure = new Set();
|
|
1865
|
+
waitForHistory(items, history, results);
|
|
1688
1866
|
|
|
1689
|
-
|
|
1690
|
-
// that can't be read: the payouts' order can't be told.
|
|
1691
|
-
for (const item of given) {
|
|
1692
|
-
const height = /** @type {SentItem} */ (items.find((each) => each.hash === item.hash)).height;
|
|
1867
|
+
if (!history.covered.length) continue;
|
|
1693
1868
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1869
|
+
const { given } = history;
|
|
1870
|
+
// One before it that can't be read could be equal: the payouts' order
|
|
1871
|
+
// can't be told.
|
|
1872
|
+
const unsure = new Set(given.flatMap((item) => (/** @type {number[]} */ (history.unread.get(item.hash)).length ? [item.hash] : [])));
|
|
1696
1873
|
|
|
1697
1874
|
try {
|
|
1698
1875
|
const found = await followLogs(
|
|
@@ -1706,25 +1883,78 @@ export async function serviceArrivals({
|
|
|
1706
1883
|
},
|
|
1707
1884
|
given,
|
|
1708
1885
|
guarded: false,
|
|
1709
|
-
queue,
|
|
1886
|
+
queue: history.queue,
|
|
1710
1887
|
unsure,
|
|
1711
1888
|
},
|
|
1712
1889
|
{ blockTimes, maxWindows, now, slackMs: CLOCK_SLACK_MS, url: String(rpcUrl ?? route.chain.rpcUrl), used, windowMs: CREDIT_WINDOW_MS }
|
|
1713
1890
|
);
|
|
1714
1891
|
|
|
1715
|
-
for (const [hash, arrival] of found) results.set(hash, arrival);
|
|
1892
|
+
for (const [hash, arrival] of found) results.set(hash, withHistory(hash, arrival, given, history.states));
|
|
1716
1893
|
} catch (error) {
|
|
1717
|
-
for (const item of
|
|
1894
|
+
for (const item of given) results.set(item.hash, withHistory(item.hash, failed(error), given, history.states));
|
|
1718
1895
|
}
|
|
1719
1896
|
}
|
|
1720
1897
|
|
|
1721
1898
|
return done();
|
|
1722
1899
|
}
|
|
1723
1900
|
|
|
1901
|
+
/**
|
|
1902
|
+
* Of the transfers a route's account received naming a group's address,
|
|
1903
|
+
* those equal to the group's, from their own transactions: one the app
|
|
1904
|
+
* gave pays out what its own fee leaves, one it didn't what the group's
|
|
1905
|
+
* fees do; each with the block the app gave it its payout can't be before.
|
|
1906
|
+
* And those whose transaction can't be read. `facts` keeps each
|
|
1907
|
+
* transaction read.
|
|
1908
|
+
*
|
|
1909
|
+
* @param {Archive} archive
|
|
1910
|
+
* @param {ReadonlyArray<{ hash: string, height: number }>} found
|
|
1911
|
+
* @param {{ bounds: ReadonlyMap<string, bigint | null>, facts: Map<string, TransferFacts | null>, feeOf: ReadonlyMap<string, bigint>, fees: ReadonlySet<bigint>, payout: bigint, route: Route & { kind: "service" }, to: string }} group
|
|
1912
|
+
* @param {{ requests: number }} counter
|
|
1913
|
+
*/
|
|
1914
|
+
async function servicePeers(archive, found, { bounds, facts, feeOf, fees, payout, route, to }, counter) {
|
|
1915
|
+
const missing = found.flatMap((entry) => (facts.has(entry.hash) ? [] : [entry.hash]));
|
|
1916
|
+
const read = await transfersOf(archive, missing, counter);
|
|
1917
|
+
|
|
1918
|
+
for (const hash of missing) facts.set(hash, read.get(hash) ?? null);
|
|
1919
|
+
|
|
1920
|
+
/** @type {Queued[]} */
|
|
1921
|
+
const peers = [];
|
|
1922
|
+
/** @type {{ hash: string, height: number }[]} */
|
|
1923
|
+
const unread = [];
|
|
1924
|
+
|
|
1925
|
+
for (const entry of found) {
|
|
1926
|
+
const fact = facts.get(entry.hash);
|
|
1927
|
+
|
|
1928
|
+
if (!fact || fact.kind === null || fact.at === null || (fact.kind === "moonlight" && (fact.value === null || fact.receiver === null))) {
|
|
1929
|
+
unread.push(entry);
|
|
1930
|
+
} else if (!fact.failed && fact.kind === "moonlight" && fact.receiver === route.account && fact.memo.toLowerCase() === to) {
|
|
1931
|
+
const value = /** @type {bigint} */ (fact.value);
|
|
1932
|
+
const own = feeOf.get(entry.hash);
|
|
1933
|
+
|
|
1934
|
+
if (own === undefined ? [...fees].some((fee) => value - fee === payout) : value - own === payout) {
|
|
1935
|
+
peers.push({ at: fact.at, given: false, hash: entry.hash, notBefore: bounds.get(entry.hash) ?? null, scan: null });
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
return { peers, unread };
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1724
1943
|
// ---------------------------------------------------------------------------
|
|
1725
1944
|
// Migrations into Dusk
|
|
1726
1945
|
// ---------------------------------------------------------------------------
|
|
1727
1946
|
|
|
1947
|
+
/**
|
|
1948
|
+
* The migration routes an account pays out. More than one where a
|
|
1949
|
+
* deployment has one account pay out both chains' migrations: then a
|
|
1950
|
+
* payout of it doesn't say which route it is on.
|
|
1951
|
+
*
|
|
1952
|
+
* @param {Network} net
|
|
1953
|
+
* @param {string} account
|
|
1954
|
+
* @returns {Route[]}
|
|
1955
|
+
*/
|
|
1956
|
+
const paidOutBy = (net, account) => routes(net).filter((route) => route.kind === "migration" && Boolean(route.payoutAccount) && route.payoutAccount === account.trim());
|
|
1957
|
+
|
|
1728
1958
|
/**
|
|
1729
1959
|
* The payouts of migrations a transfer the archive lists holds for
|
|
1730
1960
|
* `account`, read from its own events: from a migration route's payout
|
|
@@ -1744,12 +1974,12 @@ function payoutsIn(raw, account, net) {
|
|
|
1744
1974
|
|
|
1745
1975
|
return (Array.isArray(raw?.events) ? raw.events : []).flatMap((/** @type {any} */ event) => {
|
|
1746
1976
|
const data = event?.data;
|
|
1747
|
-
const
|
|
1977
|
+
const paying = data && event.reverted !== true && event.topic === "moonlight" && String(event.target ?? "").toLowerCase() === TRANSFER_CONTRACT && data.receiver === account && typeof data.sender === "string" ? paidOutBy(net, data.sender) : [];
|
|
1748
1978
|
const memo = memoOf(data?.memo).trim().toLowerCase();
|
|
1749
1979
|
const amount = luxOf(data?.value);
|
|
1750
1980
|
|
|
1751
|
-
return
|
|
1752
|
-
? [{ amount, at: null, block, hash: /** @type {`0x${string}`} */ (memo), payout, route:
|
|
1981
|
+
return paying.length && EVM_HASH.test(memo) && amount !== null
|
|
1982
|
+
? [{ amount, at: null, block, hash: /** @type {`0x${string}`} */ (memo), payer: data.sender.trim(), payout, route: paying.length === 1 ? paying[0].id : null }]
|
|
1753
1983
|
: [];
|
|
1754
1984
|
});
|
|
1755
1985
|
}
|
|
@@ -1811,22 +2041,22 @@ export async function migrationPayouts({ network, migrations, entries, archive,
|
|
|
1811
2041
|
}
|
|
1812
2042
|
|
|
1813
2043
|
// Each payout pays one migration: the migration its memo names, of the
|
|
1814
|
-
// amount it paid,
|
|
2044
|
+
// amount it paid, from its route's payout account.
|
|
1815
2045
|
const byBlock = [...candidates].sort((a, b) => a.block - b.block || (a.payout < b.payout ? -1 : 1));
|
|
1816
2046
|
|
|
1817
2047
|
for (const item of [...payable].sort((a, b) => (a.at ?? Infinity) - (b.at ?? Infinity))) {
|
|
1818
2048
|
if (results.has(item.hash)) continue;
|
|
1819
2049
|
|
|
1820
|
-
const named = byBlock.filter((each) => !used.has(each.payout) && each.hash === item.hash && each.
|
|
2050
|
+
const named = byBlock.filter((each) => !used.has(each.payout) && each.hash === item.hash && each.payer === item.route?.payoutAccount);
|
|
1821
2051
|
const payout = named.find((each) => each.amount === item.amount);
|
|
1822
2052
|
|
|
1823
2053
|
if (payout) {
|
|
1824
2054
|
used.add(payout.payout);
|
|
1825
2055
|
results.set(item.hash, { amount: payout.amount, at: payout.at, block: payout.block, hash: payout.payout, state: "arrived" });
|
|
1826
2056
|
} else if (named.length) {
|
|
1827
|
-
// Its payout, of another amount:
|
|
1828
|
-
//
|
|
1829
|
-
//
|
|
2057
|
+
// Its payout, of another amount: another amount was paid, or the value
|
|
2058
|
+
// of an entry an earlier version kept holds a gas refund paid to the
|
|
2059
|
+
// account. Not waiting: unsure.
|
|
1830
2060
|
results.set(item.hash, { reason: "unsure", state: "unknown" });
|
|
1831
2061
|
} else {
|
|
1832
2062
|
results.set(item.hash, item.at !== null && now - item.at > waitMs ? { reason: "expired", state: "unknown" } : { state: "waiting" });
|
|
@@ -1900,17 +2130,27 @@ export function serviceTransferOf(entry, network) {
|
|
|
1900
2130
|
*/
|
|
1901
2131
|
export function migrationPayoutOf(entry, network) {
|
|
1902
2132
|
const net = networkOf(network);
|
|
1903
|
-
const
|
|
2133
|
+
const paying = entry?.kind === "transfer" && entry.direction === "in" && typeof entry.counterparty === "string" ? paidOutBy(net, entry.counterparty) : [];
|
|
1904
2134
|
const { at, hash, height, value } = factsOf(entry);
|
|
2135
|
+
// What the transfer paid: a gas refund paid to the account with it aside.
|
|
2136
|
+
const refund = entry?.refund === undefined || entry.refund === null ? 0n : luxOf(entry.refund);
|
|
1905
2137
|
const memo = String(entry?.memo ?? "")
|
|
1906
2138
|
.trim()
|
|
1907
2139
|
.toLowerCase();
|
|
1908
2140
|
|
|
1909
|
-
if (!
|
|
2141
|
+
if (!paying.length || entry.success === false || !hash || height === null || value === null || refund === null || refund > value || !EVM_HASH.test(memo)) {
|
|
1910
2142
|
return null;
|
|
1911
2143
|
}
|
|
1912
2144
|
|
|
1913
|
-
return {
|
|
2145
|
+
return {
|
|
2146
|
+
amount: value - refund,
|
|
2147
|
+
at,
|
|
2148
|
+
block: height,
|
|
2149
|
+
hash: /** @type {`0x${string}`} */ (memo),
|
|
2150
|
+
payer: /** @type {string} */ (entry.counterparty).trim(),
|
|
2151
|
+
payout: hash,
|
|
2152
|
+
route: paying.length === 1 ? paying[0].id : null,
|
|
2153
|
+
};
|
|
1914
2154
|
}
|
|
1915
2155
|
|
|
1916
2156
|
/**
|