@dusk-network/app-core 0.4.0 → 0.4.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 +10 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/bridge.d.ts +9 -5
- package/src/bridge.js +15 -10
- package/src/duskevm-withdrawals.d.ts +13 -2
- package/src/duskevm-withdrawals.js +139 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are listed here. It follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [0.4.1]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- `/duskevm-withdrawals`: a withdrawal's receipt is kept in `cache` once its DuskEVM block is final, and read once
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `/bridge`: a memo of bytes that aren't text (`memoBinary`) gives no payout address, and names no migration
|
|
14
|
+
|
|
5
15
|
## [0.4.0]
|
|
6
16
|
|
|
7
17
|
### Added
|
package/README.md
CHANGED
|
@@ -206,8 +206,8 @@ arrivalsOf(arrivals); // Map { hash => { hash: "0x029e…", at: 1793080333000 }
|
|
|
206
206
|
- A transfer's time is its Dusk block's, never the device's clock. One without it yet isn't looked for. One sent more than a week ago (`waitMs`) is unknown, and isn't asked about.
|
|
207
207
|
- A deposit is followed on DuskEVM's explorer (`via: "explorer"`, the default): the recipient's incoming calls, newest first, read once for all its deposits, back past the oldest, 5 pages at most. Those calls don't say who deposited, so a payment is a deposit's only when it can't be another's: within 2 minutes either side of its block, once those minutes are over, with no other payment of its amount there (a failed one counts) that isn't another given deposit's. Nothing is asked before then.
|
|
208
208
|
- On DuskEVM's RPC (`via: "rpc"`), a deposit is found by DuskEVM's bridge event from its sender's DuskEVM address. With `/history`'s archive, each sender's equal deposits in the hours before its own are read from its history and paid first; a deposit there whose call can't be read leaves the order unclear. Without it, the RPC's credits are taken as the explorer's are. A deposit given without its sender takes no credit from the sender of another equal one given.
|
|
209
|
-
- A migration is paid by a transfer to the account from its route's payout account, with the migration's transaction hash as the memo, of exactly its amount: from the account's entries, less any `refund` paid to the account with it, or from the archive's memo index, which gives each payout's own amount. One from that account of another amount leaves it unsure, not waiting: the value of an entry an earlier version kept holds any gas refund paid to the account. With `entries`, the payouts there of migrations the app didn't list come too (`unlisted`), each with its `payer`; where one account pays out more than one route, a payout of a migration not listed doesn't say which: its `route` is null.
|
|
210
|
-
- A transfer out is read from the chain: what the route's account received, the address its memo gives as the route's bridge reads it (0x and 40 hex digits, any case, nothing around them), and its block. It arrives with the DUSK token's `Transfer` from its route's payout address to that address, of what the account received less the fee. The account's history lists every transfer that named the same address, in any case: equal ones in the hours before (a given one's by its own fee) are paid first, and one that can't be read leaves the order unclear. One that pays nothing out is `no-payout`, with its `problem`. `serviceTransferOf` gives what left the account (`sent`), and what the account received (`amount`) only where no gas was left to refund: an entry's value holds any refund paid to another account.
|
|
209
|
+
- A migration is paid by a transfer to the account from its route's payout account, with the migration's transaction hash as the memo, as text, of exactly its amount: from the account's entries, less any `refund` paid to the account with it, or from the archive's memo index, which gives each payout's own amount. One from that account of another amount leaves it unsure, not waiting: the value of an entry an earlier version kept holds any gas refund paid to the account. With `entries`, the payouts there of migrations the app didn't list come too (`unlisted`), each with its `payer`; where one account pays out more than one route, a payout of a migration not listed doesn't say which: its `route` is null.
|
|
210
|
+
- A transfer out is read from the chain: what the route's account received, the address its memo gives as the route's bridge reads it (0x and 40 hex digits, any case, nothing around them, as text: a memo of bytes that aren't text gives none), and its block. It arrives with the DUSK token's `Transfer` from its route's payout address to that address, of what the account received less the fee. The account's history lists every transfer that named the same address, in any case: equal ones in the hours before (a given one's by its own fee) are paid first, and one that can't be read leaves the order unclear. One that pays nothing out is `no-payout`, with its `problem`. `serviceTransferOf` gives what left the account (`sent`), and what the account received (`amount`) only where no gas was left to refund: an entry's value holds any refund paid to another account.
|
|
211
211
|
- With the archive, each transfer reads only its own hours of its account's history (its sender's for a deposit, the route's account's for a transfer out), further back while equal ones before it run on, and only once the archive lists the transfer there. A call reads 4 pages of it (`HISTORY_PAGES`) for each: a transfer whose history is longer waits, and the next call reads on from its `scan`.
|
|
212
212
|
- A route without its payout account or address in the network's file pays out nothing that can be told apart: `no-route`.
|
|
213
213
|
- An RPC search starts at a block from before the transfer, 10 minutes before it as the chain's pace says, checked by its time and stepped back until it is, and reads 8 windows at most: of 5,000 blocks, twice as many after each one the RPC answers whole, up to 100,000, and fewer where it asks for it, the width kept in the search's `scan` from call to call. A call reads a chain's head and pace once for all its searches. Once a deposit's windows are over and read, a search without its sender's history reads no further. It reads each block more than once: its window at the head reaches back 1,000 blocks, so the newest blocks are read again on each call, and once it has reached the head with a transfer not arrived, it reads its blocks again from the first, a pass at a time with the windows a call has left (one at least while a pass is under way, so 9 where the search ahead takes all 8): the first a minute later, each one after as long again as it has been since it reached the head. Where a later equal transfer, still without its credit, could have had the credit an earlier one takes, the earlier one waits until the blocks are read again 8 times since the credit was found, one pass after the other. A result that isn't arrived says where the next search starts, with what it searched for and the credits found so far (`scan`): the app keeps it with the arrivals it was made with, and passes both back. A search for another sender or amount starts afresh. The block times the app keeps (`blockTimes`) are a chain's: for transfers out, a cache a chain by its chain ID, or one cache where the network's service routes are all on one chain.
|
|
@@ -258,7 +258,7 @@ const claimCall = await withdrawals.claimCall(txHash, { submitter: account }); /
|
|
|
258
258
|
### Reads and caches
|
|
259
259
|
|
|
260
260
|
- The Portal and the dispute game factory are read with the app's drivers; each game's hub and the anchor state registry raw. Reads go to the node's contracts through the app's `fetch` (its Multicall for Dusk batcher), sent again twice at most when the node answers 429, 502, 503 or 504; a refusal carries the contract's own words. `maxBatch` is how many reads one query holds (32 by default), each game checked taking 4: its status, the Portal's blacklist, and the registry's two answers. A game's status is read on its own hub, which the factory names: a game type's hub can change, and the games made before stay where they were.
|
|
261
|
-
- What a deployment doesn't change is read once: each game's hub, each game type's hub, each hub's clock limit. `cache`, a `Map` of text by default, is where they're kept: an app can keep them across sessions.
|
|
261
|
+
- What a deployment doesn't change is read once: each game's hub, each game type's hub, each hub's clock limit, and a withdrawal's receipt once its DuskEVM block is final. A receipt is read after DuskEVM's finalized block, one read for the receipts read at the same time, and kept when its block is at or under that one; one that isn't final yet, or isn't a withdrawal's, is read again each time. `cache`, a `Map` of text by default, is where they're kept: an app can keep them across sessions.
|
|
262
262
|
- Dusk's latest block comes from `latestBlock`, or the node's GraphQL. Without it, a proved withdrawal's status fails: its date can't be worked out, and no claim is built.
|
|
263
263
|
|
|
264
264
|
## License
|
package/package.json
CHANGED
package/src/bridge.d.ts
CHANGED
|
@@ -409,6 +409,8 @@ export type HistoryLike = {
|
|
|
409
409
|
/** Of `value`, a gas refund paid to another account than the sender. */
|
|
410
410
|
refund?: bigint | string | null;
|
|
411
411
|
memo: string;
|
|
412
|
+
/** True when `memo` is bytes that aren't text, given as 0x hex: no address or hash is read from it. */
|
|
413
|
+
memoBinary?: boolean;
|
|
412
414
|
payer: boolean;
|
|
413
415
|
timestamp?: number | null;
|
|
414
416
|
success?: boolean | null;
|
|
@@ -433,7 +435,7 @@ export type ServiceTransferFacts = {
|
|
|
433
435
|
/** What the route's account received: known only where no gas was left to refund; null otherwise. */
|
|
434
436
|
amount: bigint | null;
|
|
435
437
|
fee: bigint;
|
|
436
|
-
/** The address its memo gives, exactly as the bridge reads it; null when it isn't one. */
|
|
438
|
+
/** The address its memo gives, exactly as the bridge reads it; null when it isn't one, or the memo isn't text. */
|
|
437
439
|
to: string | null;
|
|
438
440
|
at: number | null;
|
|
439
441
|
height: number | null;
|
|
@@ -444,16 +446,18 @@ export type ServiceTransferFacts = {
|
|
|
444
446
|
/**
|
|
445
447
|
* A transfer the account sent to a service route's account, as its entry
|
|
446
448
|
* says it; null for any other entry. The memo is read as the route's bridge
|
|
447
|
-
* reads it: 0x and 40 hex digits, any case, nothing around them.
|
|
449
|
+
* reads it: 0x and 40 hex digits, any case, nothing around them, as text. A
|
|
450
|
+
* memo of bytes that aren't text (`memoBinary`) gives no address.
|
|
448
451
|
*/
|
|
449
452
|
export declare function serviceTransferOf(entry: HistoryLike, network: NetworkRef): ServiceTransferFacts | null;
|
|
450
453
|
|
|
451
454
|
/**
|
|
452
455
|
* A payout of a migration, from its entry: a transfer the account received
|
|
453
456
|
* from a migration route's payout account, with a transaction hash as the
|
|
454
|
-
* memo, of its value less any gas `refund` paid to the account with
|
|
455
|
-
* route is null where that account pays out more than one route.
|
|
456
|
-
* any other entry,
|
|
457
|
+
* memo as text, of its value less any gas `refund` paid to the account with
|
|
458
|
+
* it. Its route is null where that account pays out more than one route.
|
|
459
|
+
* Null for any other entry, a memo of bytes that aren't text included, and
|
|
460
|
+
* on a route without its payout account.
|
|
457
461
|
*/
|
|
458
462
|
export declare function migrationPayoutOf(entry: HistoryLike, network: NetworkRef): MigrationPayout | null;
|
|
459
463
|
|
package/src/bridge.js
CHANGED
|
@@ -1278,19 +1278,24 @@ function exactJson(text) {
|
|
|
1278
1278
|
return JSON.parse(text.replace(TOKEN, (match, string) => string ?? (/^-?\d+$/.test(match) && !Number.isSafeInteger(Number(match)) ? `"${match}"` : match)));
|
|
1279
1279
|
}
|
|
1280
1280
|
|
|
1281
|
-
const utf8 = new TextDecoder();
|
|
1281
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
1282
1282
|
|
|
1283
1283
|
/**
|
|
1284
|
-
* A memo's bytes, given as hex, as text
|
|
1285
|
-
*
|
|
1286
|
-
* character; "" for none.
|
|
1284
|
+
* A memo's bytes, given as hex, as UTF-8 text; "" for none, and for bytes
|
|
1285
|
+
* that aren't UTF-8: an address or a hash is read from text only.
|
|
1287
1286
|
*
|
|
1288
1287
|
* @param {unknown} hex
|
|
1289
1288
|
*/
|
|
1290
1289
|
function memoOf(hex) {
|
|
1291
1290
|
const text = String(hex ?? "").replace(/^0x/i, "");
|
|
1292
1291
|
|
|
1293
|
-
|
|
1292
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(text)) return "";
|
|
1293
|
+
|
|
1294
|
+
try {
|
|
1295
|
+
return utf8.decode(Uint8Array.from(text.match(/../g) ?? [], (byte) => parseInt(byte, 16)));
|
|
1296
|
+
} catch {
|
|
1297
|
+
return "";
|
|
1298
|
+
}
|
|
1294
1299
|
}
|
|
1295
1300
|
|
|
1296
1301
|
/**
|
|
@@ -2451,8 +2456,9 @@ export function serviceTransferOf(entry, network) {
|
|
|
2451
2456
|
const spent = luxOf(entry.gasSpent);
|
|
2452
2457
|
const price = luxOf(entry.gasPrice);
|
|
2453
2458
|
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
|
-
|
|
2459
|
+
// The address the memo gives, exactly as the bridge reads it: from text.
|
|
2460
|
+
// A memo of bytes that aren't text, which /history gives as 0x hex, gives none.
|
|
2461
|
+
const memo = typeof entry.memo === "string" && entry.memoBinary !== true ? entry.memo : "";
|
|
2456
2462
|
const to = EVM_ADDRESS.test(memo) ? memo : null;
|
|
2457
2463
|
const problem = entry.success === false ? "failed" : value <= route.feeLux ? "under-fee" : to === null ? "no-address" : null;
|
|
2458
2464
|
|
|
@@ -2470,9 +2476,8 @@ export function migrationPayoutOf(entry, network) {
|
|
|
2470
2476
|
const { at, hash, height, value } = factsOf(entry);
|
|
2471
2477
|
// What the transfer paid: a gas refund paid to the account with it aside.
|
|
2472
2478
|
const refund = entry?.refund === undefined || entry.refund === null ? 0n : luxOf(entry.refund);
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
.toLowerCase();
|
|
2479
|
+
// The migration's hash, as text: a memo of bytes that aren't text names none.
|
|
2480
|
+
const memo = entry?.memoBinary === true ? "" : String(entry?.memo ?? "").trim().toLowerCase();
|
|
2476
2481
|
|
|
2477
2482
|
if (!paying.length || entry.success === false || !hash || height === null || value === null || refund === null || refund > value || !EVM_HASH.test(memo)) {
|
|
2478
2483
|
return null;
|
|
@@ -359,7 +359,11 @@ export type WithdrawalsOptions = {
|
|
|
359
359
|
maxBatch?: number;
|
|
360
360
|
/** How many of the newest games a proof is looked for among: 64 by default, and for anything but a positive whole number. */
|
|
361
361
|
gameSearchDepth?: number | bigint;
|
|
362
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* Where the deployment's fixed facts are kept: each game's hub, each game
|
|
364
|
+
* type's hub, each hub's clock limit, and each withdrawal's receipt once
|
|
365
|
+
* its DuskEVM block is final.
|
|
366
|
+
*/
|
|
363
367
|
cache?: FactCache;
|
|
364
368
|
/** The clock, in milliseconds. */
|
|
365
369
|
now?: () => number;
|
|
@@ -368,7 +372,14 @@ export type WithdrawalsOptions = {
|
|
|
368
372
|
/** A network's withdrawals, read on its Dusk node and DuskEVM. */
|
|
369
373
|
export type Withdrawals = {
|
|
370
374
|
readonly network: Network;
|
|
371
|
-
/**
|
|
375
|
+
/**
|
|
376
|
+
* A DuskEVM transaction's withdrawal, from its receipt; refuses as
|
|
377
|
+
* `parseWithdrawalReceipt` does. The receipt is read after DuskEVM's
|
|
378
|
+
* finalized block, which the receipts read at the same time share, and
|
|
379
|
+
* kept in `cache` once its block is at or under that one: a final
|
|
380
|
+
* receipt is read once. One that isn't final yet, or isn't a
|
|
381
|
+
* withdrawal's, is read again each time.
|
|
382
|
+
*/
|
|
372
383
|
receipt(txHash: string): Promise<ReceiptWithdrawal>;
|
|
373
384
|
/**
|
|
374
385
|
* Where a withdrawal is, for `submitter` (a Dusk account, or its
|
|
@@ -773,6 +773,20 @@ function evmRpc(client, net) {
|
|
|
773
773
|
};
|
|
774
774
|
}
|
|
775
775
|
|
|
776
|
+
/**
|
|
777
|
+
* A block number as DuskEVM's RPC gives it, a hex quantity, or as a whole
|
|
778
|
+
* number; null for anything else.
|
|
779
|
+
*
|
|
780
|
+
* @param {unknown} value
|
|
781
|
+
* @returns {bigint | null}
|
|
782
|
+
*/
|
|
783
|
+
function blockNumberOf(value) {
|
|
784
|
+
if (typeof value === "bigint") return value >= 0n ? value : null;
|
|
785
|
+
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
|
|
786
|
+
|
|
787
|
+
return typeof value === "string" && /^0x[0-9a-fA-F]+$/.test(value) ? BigInt(value) : null;
|
|
788
|
+
}
|
|
789
|
+
|
|
776
790
|
// ---------------------------------------------------------------------------
|
|
777
791
|
// The bridge's contracts on Dusk
|
|
778
792
|
// ---------------------------------------------------------------------------
|
|
@@ -1223,13 +1237,137 @@ export function createWithdrawals(options) {
|
|
|
1223
1237
|
]).then(([status, blacklisted, proper, respected]) => ({ blacklisted, proper, respected, status }));
|
|
1224
1238
|
|
|
1225
1239
|
/**
|
|
1240
|
+
* Where a final receipt is kept: by the network, its Portal and the
|
|
1241
|
+
* transaction's hash, so another network's or deployment's stays apart.
|
|
1242
|
+
*
|
|
1243
|
+
* @param {string} hash - 0x and 64 lower-case hex digits
|
|
1244
|
+
*/
|
|
1245
|
+
const receiptKey = (hash) => `receipt:${net.chainId}:${contracts.portal}:${hash}`;
|
|
1246
|
+
|
|
1247
|
+
/** @type {Promise<bigint | null> | null} */
|
|
1248
|
+
let finalizing = null;
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* DuskEVM's finalized block's height, or null where it can't be read;
|
|
1252
|
+
* the receipts read at the same time share one read.
|
|
1253
|
+
*
|
|
1254
|
+
* @returns {Promise<bigint | null>}
|
|
1255
|
+
*/
|
|
1256
|
+
function finalizedHeight() {
|
|
1257
|
+
if (!finalizing) {
|
|
1258
|
+
const reading = Promise.resolve()
|
|
1259
|
+
.then(() => rpc("eth_getBlockByNumber", ["finalized", false]))
|
|
1260
|
+
.then(
|
|
1261
|
+
(block) => blockNumberOf(block?.number),
|
|
1262
|
+
() => null
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
finalizing = reading;
|
|
1266
|
+
reading.then(() => {
|
|
1267
|
+
if (finalizing === reading) finalizing = null;
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
return finalizing;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/** @type {Map<string, Promise<any>>} */
|
|
1275
|
+
const receiptReads = new Map();
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* The withdrawal a kept receipt records, as when it was kept; null when
|
|
1279
|
+
* none is kept, or what is kept isn't this transaction's withdrawal.
|
|
1280
|
+
*
|
|
1281
|
+
* @param {string} hash
|
|
1282
|
+
* @returns {ReceiptWithdrawal | null}
|
|
1283
|
+
*/
|
|
1284
|
+
function keptReceipt(hash) {
|
|
1285
|
+
try {
|
|
1286
|
+
const text = cache.get(receiptKey(hash));
|
|
1287
|
+
|
|
1288
|
+
if (typeof text !== "string" || !text) return null;
|
|
1289
|
+
|
|
1290
|
+
const withdrawal = parseWithdrawalReceipt(net, JSON.parse(text));
|
|
1291
|
+
|
|
1292
|
+
return withdrawal.transactionHash === hash ? withdrawal : null;
|
|
1293
|
+
} catch {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Keeps a receipt that can't change any more: a withdrawal's, of this
|
|
1300
|
+
* transaction, in a block at or under `final`, the finalized height read
|
|
1301
|
+
* before the receipt was. A failed or missing receipt, or one that isn't
|
|
1302
|
+
* a withdrawal's, isn't kept. Kept as the RPC gave it, once its text
|
|
1303
|
+
* reads back as the same withdrawal.
|
|
1304
|
+
*
|
|
1305
|
+
* @param {string} hash
|
|
1306
|
+
* @param {any} raw
|
|
1307
|
+
* @param {bigint} final
|
|
1308
|
+
*/
|
|
1309
|
+
function keepIfFinal(hash, raw, final) {
|
|
1310
|
+
try {
|
|
1311
|
+
const withdrawal = parseWithdrawalReceipt(net, raw);
|
|
1312
|
+
const block = blockNumberOf(raw?.blockNumber);
|
|
1313
|
+
|
|
1314
|
+
if (block === null || block !== withdrawal.blockNumber || block > final || withdrawal.transactionHash !== hash) return;
|
|
1315
|
+
|
|
1316
|
+
const text = JSON.stringify(raw);
|
|
1317
|
+
const again = parseWithdrawalReceipt(net, JSON.parse(text));
|
|
1318
|
+
|
|
1319
|
+
if (again.withdrawalHash !== withdrawal.withdrawalHash || again.blockNumber !== block || again.transactionHash !== hash) return;
|
|
1320
|
+
|
|
1321
|
+
cache.set(receiptKey(hash), text);
|
|
1322
|
+
} catch {
|
|
1323
|
+
// Not kept: it's read again next time.
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* A transaction's receipt as DuskEVM gives it, read after DuskEVM's
|
|
1329
|
+
* finalized block: a receipt read once its block is final is the final
|
|
1330
|
+
* chain's, and is kept. The callers asking for one receipt at the same
|
|
1331
|
+
* time share one read.
|
|
1332
|
+
*
|
|
1333
|
+
* @param {string} hash
|
|
1334
|
+
* @returns {Promise<any>}
|
|
1335
|
+
*/
|
|
1336
|
+
function readReceipt(hash) {
|
|
1337
|
+
let reading = receiptReads.get(hash);
|
|
1338
|
+
|
|
1339
|
+
if (!reading) {
|
|
1340
|
+
reading = (async () => {
|
|
1341
|
+
const final = await finalizedHeight();
|
|
1342
|
+
const raw = await rpc("eth_getTransactionReceipt", [hash]);
|
|
1343
|
+
|
|
1344
|
+
if (final !== null) keepIfFinal(hash, raw, final);
|
|
1345
|
+
|
|
1346
|
+
return raw;
|
|
1347
|
+
})();
|
|
1348
|
+
receiptReads.set(hash, reading);
|
|
1349
|
+
reading.then(
|
|
1350
|
+
() => receiptReads.delete(hash),
|
|
1351
|
+
() => receiptReads.delete(hash)
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
return reading;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* A transaction's withdrawal, from its receipt: the one kept once its
|
|
1360
|
+
* block was final, else the one DuskEVM gives now.
|
|
1361
|
+
*
|
|
1226
1362
|
* @param {string} txHash
|
|
1227
1363
|
* @returns {Promise<ReceiptWithdrawal>}
|
|
1228
1364
|
*/
|
|
1229
1365
|
async function receipt(txHash) {
|
|
1230
1366
|
if (typeof txHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(txHash)) throw new WithdrawalError("bad-hash");
|
|
1231
1367
|
|
|
1232
|
-
|
|
1368
|
+
const hash = txHash.toLowerCase();
|
|
1369
|
+
|
|
1370
|
+
return keptReceipt(hash) ?? parseWithdrawalReceipt(net, await readReceipt(hash));
|
|
1233
1371
|
}
|
|
1234
1372
|
|
|
1235
1373
|
/**
|