@dusk-network/app-core 0.3.2 → 0.4.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 +26 -0
- package/README.md +12 -8
- package/package.json +1 -1
- package/src/activity.d.ts +5 -1
- package/src/activity.js +7 -1
- package/src/bridge.d.ts +17 -5
- package/src/bridge.js +169 -91
- package/src/duskevm-withdrawals.d.ts +18 -4
- package/src/duskevm-withdrawals.js +95 -17
- package/src/history.d.ts +34 -10
- package/src/history.js +80 -18
- package/src/shielded.d.ts +21 -6
- package/src/shielded.js +155 -42
|
@@ -127,6 +127,12 @@ const U32_MAX = (1n << 32n) - 1n;
|
|
|
127
127
|
/** The low 240 bits of a withdrawal's nonce: its count, under its version. */
|
|
128
128
|
const NONCE_COUNT = (1n << 240n) - 1n;
|
|
129
129
|
|
|
130
|
+
// The newest final blocks a sync reads again on each run, the new ones
|
|
131
|
+
// among them, and how long after it first covered all the final blocks it
|
|
132
|
+
// reads them all again: each pass after waits as long again as it has been
|
|
133
|
+
// since.
|
|
134
|
+
const L2_OVERLAP = 50_000;
|
|
135
|
+
const L2_RECHECK_MS = 60 * 60_000;
|
|
130
136
|
// Answers a read is sent again for, a quarter second, then half a second later.
|
|
131
137
|
const RETRY_STATUSES = new Set([429, 502, 503, 504]);
|
|
132
138
|
const ATTEMPTS = 3;
|
|
@@ -2086,20 +2092,41 @@ function withdrawalFromLog(log) {
|
|
|
2086
2092
|
const tooWide = (error) => /range|limit|too many|exceed/i.test(asError(error).message);
|
|
2087
2093
|
|
|
2088
2094
|
/**
|
|
2089
|
-
* The highest DuskEVM block that can't be reorged away
|
|
2095
|
+
* The highest DuskEVM block that can't be reorged away, and the requests it
|
|
2096
|
+
* took.
|
|
2090
2097
|
*
|
|
2091
2098
|
* @param {(method: string, params?: unknown[]) => Promise<any>} rpc
|
|
2099
|
+
* @returns {Promise<{ head: number, requests: number }>}
|
|
2092
2100
|
*/
|
|
2093
2101
|
async function finalHead(rpc) {
|
|
2094
2102
|
try {
|
|
2095
2103
|
const block = await rpc("eth_getBlockByNumber", ["finalized", false]);
|
|
2096
2104
|
|
|
2097
|
-
if (block?.number) return Number(BigInt(block.number));
|
|
2105
|
+
if (block?.number) return { head: Number(BigInt(block.number)), requests: 1 };
|
|
2098
2106
|
} catch {
|
|
2099
2107
|
// An RPC without "finalized": the latest block, less a margin.
|
|
2100
2108
|
}
|
|
2101
2109
|
|
|
2102
|
-
return Math.max(0, Number(BigInt(await rpc("eth_blockNumber", []))) - UNFINALIZED_MARGIN);
|
|
2110
|
+
return { head: Math.max(0, Number(BigInt(await rpc("eth_blockNumber", []))) - UNFINALIZED_MARGIN), requests: 2 };
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* A sync's passes over the blocks it covered, as a state gave them, the one
|
|
2115
|
+
* under way no higher than `target`; null for anything else.
|
|
2116
|
+
*
|
|
2117
|
+
* @param {unknown} value
|
|
2118
|
+
* @param {number} target
|
|
2119
|
+
* @returns {NonNullable<WithdrawalScan["recheck"]> | null}
|
|
2120
|
+
*/
|
|
2121
|
+
function recheckOf(value, target) {
|
|
2122
|
+
const kept = /** @type {any} */ (value);
|
|
2123
|
+
/** @param {unknown} ms */
|
|
2124
|
+
const time = (ms) => typeof ms === "number" && Number.isFinite(ms);
|
|
2125
|
+
|
|
2126
|
+
if (!kept || !time(kept.at) || !time(kept.since)) return null;
|
|
2127
|
+
if (kept.from !== null && !(Number.isSafeInteger(kept.from) && kept.from >= 0)) return null;
|
|
2128
|
+
|
|
2129
|
+
return { at: kept.at, from: kept.from === null ? null : Math.min(kept.from, target), since: kept.since };
|
|
2103
2130
|
}
|
|
2104
2131
|
|
|
2105
2132
|
/**
|
|
@@ -2114,7 +2141,8 @@ export async function syncWithdrawals({ address, bridge, duskEvm, maxRequests =
|
|
|
2114
2141
|
if (!contract) throw new TypeError("No DuskEVM bridge to read");
|
|
2115
2142
|
|
|
2116
2143
|
const rpc = evmRpc(duskEvm, net);
|
|
2117
|
-
const target = await finalHead(rpc);
|
|
2144
|
+
const { head: target, requests: asked } = await finalHead(rpc);
|
|
2145
|
+
const time = now();
|
|
2118
2146
|
// Coverage kept above the final head, as an older run read the latest, is
|
|
2119
2147
|
// read again, and what was found there is dropped with it: it may have
|
|
2120
2148
|
// been reorged away, or into another block.
|
|
@@ -2124,45 +2152,95 @@ export async function syncWithdrawals({ address, bridge, duskEvm, maxRequests =
|
|
|
2124
2152
|
const entries = Object.fromEntries(Object.entries(state?.entries ?? {}).filter(([, entry]) => Number(entry?.blockNumber) <= target));
|
|
2125
2153
|
/** @type {FoundWithdrawal[]} */
|
|
2126
2154
|
const found = [];
|
|
2127
|
-
let requests =
|
|
2155
|
+
let requests = asked;
|
|
2156
|
+
let recheck = recheckOf(state?.recheck, target);
|
|
2128
2157
|
const topics = [ETH_BRIDGE_INITIATED_TOPIC, pad(sender)];
|
|
2129
2158
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2159
|
+
/**
|
|
2160
|
+
* The address's withdrawals in blocks `lo` to `hi`, those blocks covered;
|
|
2161
|
+
* false where the RPC asks for a narrower window, which it then is.
|
|
2162
|
+
*
|
|
2163
|
+
* @param {number} lo
|
|
2164
|
+
* @param {number} hi
|
|
2165
|
+
*/
|
|
2166
|
+
const read = async (lo, hi) => {
|
|
2137
2167
|
/** @type {any[]} */
|
|
2138
2168
|
let logs;
|
|
2139
2169
|
|
|
2140
2170
|
requests += 1;
|
|
2141
2171
|
|
|
2142
2172
|
try {
|
|
2143
|
-
logs = await rpc("eth_getLogs", [{ address: contract, fromBlock: toHex(lo), toBlock: toHex(
|
|
2173
|
+
logs = await rpc("eth_getLogs", [{ address: contract, fromBlock: toHex(lo), toBlock: toHex(hi), topics }]);
|
|
2144
2174
|
} catch (error) {
|
|
2145
2175
|
if (window > 1_000 && tooWide(error)) {
|
|
2146
2176
|
window = Math.floor(window / 4);
|
|
2147
|
-
|
|
2177
|
+
return false;
|
|
2148
2178
|
}
|
|
2149
2179
|
|
|
2150
2180
|
throw error;
|
|
2151
2181
|
}
|
|
2152
2182
|
|
|
2153
2183
|
for (const log of logs ?? []) {
|
|
2184
|
+
// One found before, read again, is kept as it was.
|
|
2185
|
+
if (entries[String(log?.transactionHash).toLowerCase()]) continue;
|
|
2186
|
+
|
|
2154
2187
|
const entry = withdrawalFromLog(log);
|
|
2155
2188
|
|
|
2156
|
-
if (!entry
|
|
2189
|
+
if (!entry) continue;
|
|
2157
2190
|
|
|
2158
2191
|
entries[entry.hash] = entry;
|
|
2159
2192
|
found.push(entry);
|
|
2160
2193
|
}
|
|
2161
2194
|
|
|
2162
|
-
covered = addCoverage(covered, lo,
|
|
2195
|
+
covered = addCoverage(covered, lo, hi);
|
|
2196
|
+
|
|
2197
|
+
return true;
|
|
2198
|
+
};
|
|
2199
|
+
|
|
2200
|
+
// The newest final blocks, read again on each run, from the newest down,
|
|
2201
|
+
// so the new ones first: in as many windows as they take, though no more
|
|
2202
|
+
// than half the requests the run has left (one at least, where nothing
|
|
2203
|
+
// else is to be read). While older blocks are still to be read, a run
|
|
2204
|
+
// under the same final head as the last leaves them all to those.
|
|
2205
|
+
const lowest = Math.max(0, target - L2_OVERLAP + 1);
|
|
2206
|
+
const due = recheck !== null && (recheck.from !== null || time >= recheck.at);
|
|
2207
|
+
const older = topGap(addCoverage(covered, lowest, target), target) !== null;
|
|
2208
|
+
const half = Math.floor((maxRequests - requests) / 2);
|
|
2209
|
+
const share = older && state?.target === target ? 0 : older || due ? half : Math.max(1, half);
|
|
2210
|
+
let top = target;
|
|
2211
|
+
|
|
2212
|
+
for (let windows = 0; windows < share && top >= lowest && requests < maxRequests; windows += 1) {
|
|
2213
|
+
const lo = Math.max(0, top - window + 1);
|
|
2214
|
+
|
|
2215
|
+
if (await read(lo, top)) top = lo - 1;
|
|
2163
2216
|
}
|
|
2164
2217
|
|
|
2165
|
-
|
|
2218
|
+
while (requests < maxRequests) {
|
|
2219
|
+
const gap = topGap(covered, target);
|
|
2220
|
+
|
|
2221
|
+
if (gap) {
|
|
2222
|
+
const [a, b] = gap;
|
|
2223
|
+
|
|
2224
|
+
await read(Math.max(a, b - window + 1), b);
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// All the final blocks covered: they're read again, a pass at a time,
|
|
2229
|
+
// from the newest down, with what a run has left.
|
|
2230
|
+
if (recheck === null) recheck = { at: time + L2_RECHECK_MS, from: null, since: time };
|
|
2231
|
+
if (recheck.from === null && time < recheck.at) break;
|
|
2232
|
+
|
|
2233
|
+
const from = recheck.from ?? target;
|
|
2234
|
+
const lo = Math.max(0, from - window + 1);
|
|
2235
|
+
|
|
2236
|
+
if (!(await read(lo, from))) continue;
|
|
2237
|
+
|
|
2238
|
+
recheck = lo > 0 ? { ...recheck, from: lo - 1 } : { at: time + Math.max(L2_RECHECK_MS, time - recheck.since), from: null, since: recheck.since };
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
if (recheck === null && !topGap(covered, target)) recheck = { at: time + L2_RECHECK_MS, from: null, since: time };
|
|
2242
|
+
|
|
2243
|
+
return { found, requests, state: { covered, entries, ...(recheck ? { recheck } : {}), target, updatedAt: now(), window } };
|
|
2166
2244
|
}
|
|
2167
2245
|
|
|
2168
2246
|
/**
|
package/src/history.d.ts
CHANGED
|
@@ -119,6 +119,12 @@ export type HistoryEntry = {
|
|
|
119
119
|
refund?: bigint;
|
|
120
120
|
/** The memo as text, or as 0x hex when it isn't text; "" for none. */
|
|
121
121
|
memo: string;
|
|
122
|
+
/**
|
|
123
|
+
* True when `memo` is bytes that aren't text, given as 0x hex, so a memo
|
|
124
|
+
* typed as 0x text isn't taken for them; left out otherwise. An entry an
|
|
125
|
+
* earlier version kept has none.
|
|
126
|
+
*/
|
|
127
|
+
memoBinary?: boolean;
|
|
122
128
|
/** Whether the account paid the fee. */
|
|
123
129
|
payer: boolean;
|
|
124
130
|
};
|
|
@@ -212,6 +218,13 @@ export type HistoryState = {
|
|
|
212
218
|
[field: string]: unknown;
|
|
213
219
|
};
|
|
214
220
|
|
|
221
|
+
/**
|
|
222
|
+
* A store's check before a write, called with the state kept now, in the
|
|
223
|
+
* write's own transaction: when it throws, the store writes nothing, and
|
|
224
|
+
* may reject.
|
|
225
|
+
*/
|
|
226
|
+
export type StoreCheck = (kept: HistoryState | null) => void;
|
|
227
|
+
|
|
215
228
|
/**
|
|
216
229
|
* Where one account's history is kept on one network: the app's own
|
|
217
230
|
* storage, which the app scopes and keys. Each method may return a value or
|
|
@@ -234,12 +247,21 @@ export type HistoryStore = {
|
|
|
234
247
|
entries: KeptEntry[],
|
|
235
248
|
update: (kept: HistoryState | null) => HistoryState
|
|
236
249
|
): Promise<HistoryState | void> | HistoryState | void;
|
|
237
|
-
/**
|
|
238
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Add details to kept entries, by hash; entries that aren't kept are left
|
|
252
|
+
* alone. A store that calls `check` first, in the same transaction, keeps
|
|
253
|
+
* no details on a history another run started over since.
|
|
254
|
+
*/
|
|
255
|
+
patch(details: Map<string, EntryDetails & Record<string, unknown>>, check?: StoreCheck): Promise<void> | void;
|
|
239
256
|
/** Kept entries, newest first: at most `limit`, below the height `before` when it's given. */
|
|
240
257
|
list(options: { limit: number; before?: number }): Promise<KeptEntry[]> | KeptEntry[];
|
|
241
|
-
/**
|
|
242
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Forget what is kept of the old chain: the node is on another one now. A
|
|
260
|
+
* store that calls `check` first, in the same transaction, forgets
|
|
261
|
+
* nothing once another run started the history over on the new chain,
|
|
262
|
+
* and keeps what that run read.
|
|
263
|
+
*/
|
|
264
|
+
clear(check?: StoreCheck): Promise<void> | void;
|
|
243
265
|
};
|
|
244
266
|
|
|
245
267
|
/** When nodes last answered without the archive: the app's storage, by URL. */
|
|
@@ -472,13 +494,15 @@ export type History = {
|
|
|
472
494
|
* One sync run: the newest heights first, then older ones, within its
|
|
473
495
|
* budgets. Each step is kept as it lands, so a run that stops anywhere is
|
|
474
496
|
* carried on by the next. A run whose head names another chain than the
|
|
475
|
-
* history kept starts it over, on what is left of its budget.
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
497
|
+
* history kept starts it over, on what is left of its budget. A details
|
|
498
|
+
* request that fails ends the run's details, not the run: it resolves
|
|
499
|
+
* with `detailsError`, and the next run asks for them again. Rejects as
|
|
500
|
+
* the store does, as after a wallet reset; as a history request does;
|
|
501
|
+
* with a NoArchiveError for a node without the archive; with a
|
|
502
|
+
* StaleHistoryError once another run overtook it; and with a RangeError,
|
|
503
|
+
* before any request, for fewer than 2 history requests.
|
|
480
504
|
*/
|
|
481
|
-
sync(options?: SyncOptions): Promise<{ state: HistoryState; requests: number; added: number }>;
|
|
505
|
+
sync(options?: SyncOptions): Promise<{ state: HistoryState; requests: number; added: number; detailsError?: Error }>;
|
|
482
506
|
/** Details for kept entries that need them, as those shown before a sync gave them any: one request per 50. */
|
|
483
507
|
fillDetails(entries: ReadonlyArray<KeptEntry | ReadEntry>, options?: { onStep?: () => void }): Promise<void>;
|
|
484
508
|
/**
|
package/src/history.js
CHANGED
|
@@ -395,14 +395,16 @@ function readHead(data) {
|
|
|
395
395
|
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
396
396
|
|
|
397
397
|
/**
|
|
398
|
-
* A memo as text when it is UTF-8 without control characters, else as 0x hex
|
|
398
|
+
* A memo as text when it is UTF-8 without control characters, else as 0x hex
|
|
399
|
+
* (`binary`).
|
|
399
400
|
*
|
|
400
401
|
* @param {unknown} hex
|
|
402
|
+
* @returns {{ memo: string, binary: boolean }}
|
|
401
403
|
*/
|
|
402
|
-
function
|
|
404
|
+
function memoOf(hex) {
|
|
403
405
|
const h = String(hex ?? "");
|
|
404
406
|
|
|
405
|
-
if (!h || h.length % 2 || !/^[0-9a-f]+$/i.test(h)) return "";
|
|
407
|
+
if (!h || h.length % 2 || !/^[0-9a-f]+$/i.test(h)) return { binary: false, memo: "" };
|
|
406
408
|
|
|
407
409
|
const bytes = Uint8Array.from(h.match(/../g) ?? [], (b) => parseInt(b, 16));
|
|
408
410
|
|
|
@@ -410,12 +412,12 @@ function memoText(hex) {
|
|
|
410
412
|
const text = utf8.decode(bytes);
|
|
411
413
|
|
|
412
414
|
// eslint-disable-next-line no-control-regex
|
|
413
|
-
if (!/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(text)) return text;
|
|
415
|
+
if (!/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(text)) return { binary: false, memo: text };
|
|
414
416
|
} catch {
|
|
415
417
|
// not text
|
|
416
418
|
}
|
|
417
419
|
|
|
418
|
-
return `0x${h.toLowerCase()}
|
|
420
|
+
return { binary: true, memo: `0x${h.toLowerCase()}` };
|
|
419
421
|
}
|
|
420
422
|
|
|
421
423
|
/** @param {any} receiver */
|
|
@@ -443,6 +445,7 @@ export function parseHistoryEntry(account, raw) {
|
|
|
443
445
|
let selfValue = null;
|
|
444
446
|
let payer = false;
|
|
445
447
|
let memo = "";
|
|
448
|
+
let memoBinary = false;
|
|
446
449
|
/** @type {EntryKind | null} */
|
|
447
450
|
let kind = null;
|
|
448
451
|
/** @type {string | null} */
|
|
@@ -476,7 +479,7 @@ export function parseHistoryEntry(account, raw) {
|
|
|
476
479
|
const ours = d.sender === account || receiver === account;
|
|
477
480
|
|
|
478
481
|
if (d.sender === account) payer = true;
|
|
479
|
-
if (ours) memo =
|
|
482
|
+
if (ours) ({ binary: memoBinary, memo } = memoOf(d.memo));
|
|
480
483
|
|
|
481
484
|
if (d.sender === account && receiver === account) {
|
|
482
485
|
selfValue = (selfValue ?? 0n) + value;
|
|
@@ -574,7 +577,18 @@ export function parseHistoryEntry(account, raw) {
|
|
|
574
577
|
else counterparty = direction === "out" ? sentTo : receivedFrom;
|
|
575
578
|
}
|
|
576
579
|
|
|
577
|
-
return {
|
|
580
|
+
return {
|
|
581
|
+
blockHeight,
|
|
582
|
+
counterparty: counterparty || null,
|
|
583
|
+
direction,
|
|
584
|
+
hash,
|
|
585
|
+
kind,
|
|
586
|
+
memo,
|
|
587
|
+
...(memoBinary ? { memoBinary: true } : {}),
|
|
588
|
+
payer,
|
|
589
|
+
value,
|
|
590
|
+
...(refunded ? { refund: refunded } : {}),
|
|
591
|
+
};
|
|
578
592
|
}
|
|
579
593
|
|
|
580
594
|
/**
|
|
@@ -1071,14 +1085,16 @@ export function createHistory({ archive, account, store, network = null, callFac
|
|
|
1071
1085
|
/**
|
|
1072
1086
|
* Details for the newest kept entries that need them, or for `entries`,
|
|
1073
1087
|
* a batch per request, each kept only while the history is still the one
|
|
1074
|
-
* `chain` reads.
|
|
1088
|
+
* `chain` reads. A request that fails rejects, or with `lenient`, as in a
|
|
1089
|
+
* sync run, ends the details there and gives its error.
|
|
1075
1090
|
*
|
|
1076
|
-
* @param {{ chain: { genesis: string, kept: boolean }, entries?: ReadonlyArray<KeptEntry | ReadEntry>, maxRequests: number, onStep?: () => void }} options
|
|
1091
|
+
* @param {{ chain: { genesis: string, kept: boolean }, entries?: ReadonlyArray<KeptEntry | ReadEntry>, maxRequests: number, onStep?: () => void, lenient?: boolean }} options
|
|
1092
|
+
* @returns {Promise<Error | null>}
|
|
1077
1093
|
*/
|
|
1078
|
-
async function fillDetails({ chain, entries, maxRequests, onStep }) {
|
|
1094
|
+
async function fillDetails({ chain, entries, maxRequests, onStep, lenient = false }) {
|
|
1079
1095
|
let requests = Math.max(0, Math.floor(maxRequests));
|
|
1080
1096
|
|
|
1081
|
-
if (!requests) return;
|
|
1097
|
+
if (!requests) return null;
|
|
1082
1098
|
|
|
1083
1099
|
const due = entries ? entries.filter(needsDetails) : await pendingRows(DETAILS_BATCH * requests);
|
|
1084
1100
|
// Arguments are asked only of the calls they're read for.
|
|
@@ -1087,7 +1103,16 @@ export function createHistory({ archive, account, store, network = null, callFac
|
|
|
1087
1103
|
|
|
1088
1104
|
for (let i = 0; i < due.length && requests > 0; i += DETAILS_BATCH, requests -= 1) {
|
|
1089
1105
|
const batch = due.slice(i, i + DETAILS_BATCH).map((e) => e.hash);
|
|
1090
|
-
|
|
1106
|
+
/** @type {Map<string, EntryDetails & Record<string, unknown>>} */
|
|
1107
|
+
let details;
|
|
1108
|
+
|
|
1109
|
+
try {
|
|
1110
|
+
details = await fetchTxDetails(archive, batch, { account, args, callFacts, network: net, now });
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
if (!lenient) throw error;
|
|
1113
|
+
|
|
1114
|
+
return error instanceof Error ? error : new Error(messageOf(error));
|
|
1115
|
+
}
|
|
1091
1116
|
|
|
1092
1117
|
// Asked for the bridge, and its call isn't the bridge's: read, and kept so.
|
|
1093
1118
|
for (const [hash, found] of details) if (forBridge.has(hash) && found.bridge === undefined) found.bridge = null;
|
|
@@ -1096,11 +1121,25 @@ export function createHistory({ archive, account, store, network = null, callFac
|
|
|
1096
1121
|
if (details.size) {
|
|
1097
1122
|
if (staleFor(await store.getState(), chain)) throw new StaleHistoryError();
|
|
1098
1123
|
|
|
1099
|
-
|
|
1124
|
+
/** @type {StaleHistoryError | null} */
|
|
1125
|
+
let refused = null;
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
// Checked again in the write's own transaction, by a store that can.
|
|
1129
|
+
await store.patch(details, (kept) => {
|
|
1130
|
+
if (staleFor(kept, chain)) throw (refused = new StaleHistoryError());
|
|
1131
|
+
});
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
throw refused ?? error;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (refused) throw refused;
|
|
1100
1137
|
}
|
|
1101
1138
|
|
|
1102
1139
|
onStep?.();
|
|
1103
1140
|
}
|
|
1141
|
+
|
|
1142
|
+
return null;
|
|
1104
1143
|
}
|
|
1105
1144
|
|
|
1106
1145
|
/**
|
|
@@ -1146,7 +1185,7 @@ export function createHistory({ archive, account, store, network = null, callFac
|
|
|
1146
1185
|
* @param {number} maxDetailRequests - 0 or more
|
|
1147
1186
|
* @param {((state: HistoryState) => void) | undefined} onStep
|
|
1148
1187
|
* @param {{ budget: { left: number }, head: { finalized: number, genesis: string } } | null} [restart]
|
|
1149
|
-
* @returns {Promise<{ state: HistoryState, requests: number, added: number }>}
|
|
1188
|
+
* @returns {Promise<{ state: HistoryState, requests: number, added: number, detailsError?: Error }>}
|
|
1150
1189
|
*/
|
|
1151
1190
|
async function run(maxRequests, maxDetailRequests, onStep, restart = null) {
|
|
1152
1191
|
/** @type {HistoryState | null} */
|
|
@@ -1154,6 +1193,9 @@ export function createHistory({ archive, account, store, network = null, callFac
|
|
|
1154
1193
|
const budget = restart?.budget ?? { left: maxRequests };
|
|
1155
1194
|
let detailsLeft = maxDetailRequests;
|
|
1156
1195
|
let added = 0;
|
|
1196
|
+
// A details request that failed: the run reads no more details, and goes on.
|
|
1197
|
+
/** @type {Error | null} */
|
|
1198
|
+
let detailsError = null;
|
|
1157
1199
|
// The chain this run reads, once its head names it, and whether it has
|
|
1158
1200
|
// seen the history kept: one cleared since isn't this run's to write.
|
|
1159
1201
|
const chain = { genesis: "", kept: state !== null };
|
|
@@ -1207,7 +1249,8 @@ export function createHistory({ archive, account, store, network = null, callFac
|
|
|
1207
1249
|
|
|
1208
1250
|
if (entries.length && added === entries.length && detailsLeft > 0) {
|
|
1209
1251
|
detailsLeft -= 1;
|
|
1210
|
-
await fillDetails({ chain, maxRequests: 1, onStep: report });
|
|
1252
|
+
detailsError = await fillDetails({ chain, lenient: true, maxRequests: 1, onStep: report });
|
|
1253
|
+
if (detailsError) detailsLeft = 0;
|
|
1211
1254
|
}
|
|
1212
1255
|
};
|
|
1213
1256
|
|
|
@@ -1237,7 +1280,21 @@ export function createHistory({ archive, account, store, network = null, callFac
|
|
|
1237
1280
|
if (state?.genesis && genesis && state.genesis !== genesis) {
|
|
1238
1281
|
if (restart) throw new Error("The store still keeps another chain's history");
|
|
1239
1282
|
|
|
1240
|
-
|
|
1283
|
+
let started = false;
|
|
1284
|
+
|
|
1285
|
+
try {
|
|
1286
|
+
// Only while it is still another chain's, checked in the clear's own
|
|
1287
|
+
// transaction by a store that can: another run may have started it
|
|
1288
|
+
// over on this chain since this one read it, and kept what it read.
|
|
1289
|
+
await store.clear((kept) => {
|
|
1290
|
+
if (kept === null || kept === undefined || kept.genesis === genesis) {
|
|
1291
|
+
started = true;
|
|
1292
|
+
throw new StaleHistoryError();
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
if (!started) throw error;
|
|
1297
|
+
}
|
|
1241
1298
|
|
|
1242
1299
|
return run(maxRequests, maxDetailRequests, onStep, { budget, head: /** @type {{ finalized: number, genesis: string }} */ (first.head) });
|
|
1243
1300
|
}
|
|
@@ -1309,9 +1366,14 @@ export function createHistory({ archive, account, store, network = null, callFac
|
|
|
1309
1366
|
await commit(range.entries, { set: { crowded: { height: lo, page: 2 }, window } });
|
|
1310
1367
|
}
|
|
1311
1368
|
|
|
1312
|
-
await fillDetails({ chain, maxRequests: detailsLeft, onStep: report });
|
|
1369
|
+
detailsError ??= await fillDetails({ chain, lenient: true, maxRequests: detailsLeft, onStep: report });
|
|
1313
1370
|
|
|
1314
|
-
return {
|
|
1371
|
+
return {
|
|
1372
|
+
added,
|
|
1373
|
+
requests: maxRequests - budget.left,
|
|
1374
|
+
state: /** @type {HistoryState} */ (state),
|
|
1375
|
+
...(detailsError ? { detailsError } : {}),
|
|
1376
|
+
};
|
|
1315
1377
|
}
|
|
1316
1378
|
|
|
1317
1379
|
return Object.freeze({
|
package/src/shielded.d.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* and the block at that height says which transaction made it: a
|
|
9
9
|
* `phoenix` event lists the nullifiers its transaction spent and the
|
|
10
10
|
* notes it made, byte for byte as the leaves hold them, and an event
|
|
11
|
-
* paying a shielded receiver from a public account or a contract
|
|
12
|
-
*
|
|
13
|
-
* for its sender, so the notes' heights hold
|
|
14
|
-
* the profile's notes too.
|
|
11
|
+
* paying a shielded receiver from a public account or a contract makes
|
|
12
|
+
* one note, and names its address and value. A transaction that spends
|
|
13
|
+
* notes always makes change for its sender, so the notes' heights hold
|
|
14
|
+
* every transaction that spent the profile's notes too.
|
|
15
15
|
* - Asking for the blocks at those heights alone would tell the node where
|
|
16
16
|
* the profile's transactions are, so each one is asked for among four
|
|
17
17
|
* other heights that hold notes, drawn from the note tree's heights. The
|
|
@@ -133,7 +133,12 @@ export type ShieldedEntry = {
|
|
|
133
133
|
/** The transaction's hash: 64 lower-case hex digits. */
|
|
134
134
|
hash: string;
|
|
135
135
|
blockHeight: number;
|
|
136
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* What it did: a transfer, a shield or an unshield, a contract call, or a
|
|
138
|
+
* stake operation. A conversion is a shield or an unshield by where its
|
|
139
|
+
* value went, as its event says, also for a profile that only got its
|
|
140
|
+
* change.
|
|
141
|
+
*/
|
|
137
142
|
kind: EntryKind;
|
|
138
143
|
/**
|
|
139
144
|
* Which way the value went for the profile's notes: "in" for what they
|
|
@@ -182,6 +187,15 @@ export type ShieldedStore = {
|
|
|
182
187
|
* the same decoys.
|
|
183
188
|
*/
|
|
184
189
|
put(record: ShieldedRecord): Promise<void> | void;
|
|
190
|
+
/**
|
|
191
|
+
* Optional: keep `change(kept)` in place of the record kept now, in one
|
|
192
|
+
* transaction, and give what was kept. With it, the decoys are drawn from
|
|
193
|
+
* those kept and kept at once, so two reads of the profile at once (two
|
|
194
|
+
* pages, a popup and a page) ask among the same decoys; without it, they
|
|
195
|
+
* are put, then read back, and two such reads can still draw their own.
|
|
196
|
+
* It rejects and passes over a history as `put` does.
|
|
197
|
+
*/
|
|
198
|
+
update?(change: (kept: ShieldedRecord | null) => ShieldedRecord): Promise<ShieldedRecord | void> | ShieldedRecord | void;
|
|
185
199
|
};
|
|
186
200
|
|
|
187
201
|
/** The note tree's distinct heights, from its first `notes` leaves. */
|
|
@@ -333,7 +347,8 @@ export declare function fetchShieldedBlocks(
|
|
|
333
347
|
* first. A transaction that spent the profile's notes sent what they held,
|
|
334
348
|
* less what came back to them (change, the fee's refund) and the fee; one
|
|
335
349
|
* that only made notes is what they received. A note no event of its block
|
|
336
|
-
* names is left out, and reverted events don't count
|
|
350
|
+
* names is left out, and reverted events don't count; two notes a block
|
|
351
|
+
* made for one address go to a transaction each, by their values.
|
|
337
352
|
*/
|
|
338
353
|
export declare function shieldedEntries(input: {
|
|
339
354
|
notes: ReadonlyArray<ValuedNote>;
|