@bananapus/nana-sdk-core 2.16.0 → 2.17.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.
@@ -0,0 +1,894 @@
1
+ import { decodeFunctionData, encodeFunctionData, isAddress, keccak256, } from "viem";
2
+ import { erc2771ForwarderAbi, jbContractAddress, } from "../generated/juicebox.js";
3
+ import { simulateStateChangingTransaction } from "./simulation.js";
4
+ // Relayr runs a bundle of transactions on several chains for one prepaid
5
+ // native payment on one of them. Everything its API returns is untrusted:
6
+ // these helpers bind a quote to the exact transactions that were posted,
7
+ // authenticate the payment against Relayr's immutable payment contract, and
8
+ // prove payments and destination transactions from the chain itself. None of
9
+ // them signs or sends.
10
+ /** Relayr's HTTP API. */
11
+ export const RELAYR_API = "https://api.relayr.ba5ed.com";
12
+ /**
13
+ * Relayr's immutable prepaid-native payment contract. A quote is untrusted
14
+ * HTTP input, so paying any other target or calldata would turn the quote
15
+ * service into a wallet transaction oracle.
16
+ */
17
+ export const RELAYR_PAYMENT_ADDRESS = "0x1c05f7841379d4393574c0ffa17908ec40ffd97d";
18
+ /**
19
+ * The payment function's selector. Its arguments are the bundle UUID as a
20
+ * right-padded bytes16 word and a uint40 deadline.
21
+ */
22
+ export const RELAYR_PAYMENT_SELECTOR = "0x103903a7";
23
+ /** keccak256 of the payment contract's runtime code. */
24
+ export const RELAYR_PAYMENT_CODE_HASH = "0x6006b5acadb4cd60aa5c00cb844c34563e182dff83d4f4ff4fde226f7df16fa6";
25
+ /** The token Relayr quotes for a payment in the chain's native currency. */
26
+ export const RELAYR_NATIVE_TOKEN = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
27
+ /** The gas a payment is simulated and sent with. */
28
+ export const RELAYR_PAYMENT_GAS = 150000n;
29
+ /**
30
+ * How long a signed ERC-2771 ForwardRequest stays executable: 47 hours from
31
+ * signing. The forwarder rejects it afterwards, even if the bundle was paid.
32
+ */
33
+ export const RELAYR_FORWARDER_DEADLINE_SECONDS = 47 * 60 * 60;
34
+ /** ERC2771Context's view on a destination contract. */
35
+ export const TRUSTED_FORWARDER_ABI = [
36
+ {
37
+ type: "function",
38
+ name: "isTrustedForwarder",
39
+ stateMutability: "view",
40
+ inputs: [{ name: "forwarder", type: "address" }],
41
+ outputs: [{ type: "bool" }],
42
+ },
43
+ ];
44
+ /** EIP-712 types of an ERC2771Forwarder ForwardRequest. */
45
+ export const FORWARD_REQUEST_TYPES = {
46
+ ForwardRequest: [
47
+ { name: "from", type: "address" },
48
+ { name: "to", type: "address" },
49
+ { name: "value", type: "uint256" },
50
+ { name: "gas", type: "uint256" },
51
+ { name: "nonce", type: "uint256" },
52
+ { name: "deadline", type: "uint48" },
53
+ { name: "data", type: "bytes" },
54
+ ],
55
+ };
56
+ const MAINNETS = [1, 10, 8453, 42161];
57
+ const TESTNETS = [11155111, 11155420, 84532, 421614];
58
+ const MAX_UINT256 = (1n << 256n) - 1n;
59
+ const PAYMENT_CODE_MAX_BYTES = 2_048;
60
+ const BUNDLE_READ_TIMEOUT_MS = 15_000;
61
+ const HTTP_DETAIL_CHARACTERS = 240;
62
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
63
+ const TRANSACTION_HASH = /^0x[0-9a-f]{64}$/iu;
64
+ // RFC 3339 with an explicit offset. A time without one is read in the
65
+ // machine's timezone, which would make the same quote pass or fail by place.
66
+ const DEADLINE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/iu;
67
+ const UNBOUND = "Relayr did not bind every quoted transaction to a unique ID. Nothing was paid.";
68
+ const UNRETURNED = "Relayr did not return the quoted transactions. Nothing was paid.";
69
+ const NO_PROOF = "This saved Relayr bundle lacks exact destination proof. Keep it pending and verify the original transactions; do not pay again.";
70
+ const NOT_IDENTIFIED = "Relayr has not identified every exact destination transaction. Keep checking the original bundle; do not pay again.";
71
+ const EXPIRED = "This Relayr quote expired. Review the action again for a new quote.";
72
+ const UNKNOWN_PAYMENT = "Relayr has not said whether this bundle is paid. Do not pay again yet; check it later.";
73
+ /**
74
+ * The chain or Relayr contradicts the expected transaction: this evidence can
75
+ * never prove it. Keep the action pending and never pay for it again.
76
+ */
77
+ export class RelayrProofError extends Error {
78
+ constructor() {
79
+ super(...arguments);
80
+ this.name = "RelayrProofError";
81
+ }
82
+ }
83
+ /**
84
+ * The payment at `hash` is exactly the reviewed payment, canonically included,
85
+ * and reverted, so that one transaction paid nothing. It does not show that
86
+ * the bundle is unpaid: the payment contract keeps no state, and Relayr keeps
87
+ * every payment it receives for a bundle, so another payment may have funded
88
+ * it. Only {@link requireRelayrPaymentRetry} clears the quote to be paid
89
+ * again.
90
+ */
91
+ export class RelayrPaymentRevertedError extends RelayrProofError {
92
+ constructor(message, hash, chainId) {
93
+ super(message);
94
+ this.hash = hash;
95
+ this.chainId = chainId;
96
+ this.name = "RelayrPaymentRevertedError";
97
+ }
98
+ }
99
+ /**
100
+ * The destination at `hash` is exactly the signed call, canonically included,
101
+ * and reverted: the call failed on its chain. Whether its authorization can
102
+ * still be used again (an unchanged forwarder nonce) is the caller's to check.
103
+ */
104
+ export class RelayrDestinationRevertedError extends RelayrProofError {
105
+ constructor(message, hash, chainId) {
106
+ super(message);
107
+ this.hash = hash;
108
+ this.chainId = chainId;
109
+ this.name = "RelayrDestinationRevertedError";
110
+ }
111
+ }
112
+ /** Like a native error cause, but set on ES2021 and never enumerable: an RPC error's URL can carry a key. */
113
+ function errorWithCause(message, cause) {
114
+ const error = new Error(message);
115
+ Object.defineProperty(error, "cause", {
116
+ value: cause,
117
+ writable: true,
118
+ enumerable: false,
119
+ configurable: true,
120
+ });
121
+ return error;
122
+ }
123
+ function isAddressLike(value) {
124
+ return typeof value === "string" && isAddress(value, { strict: false });
125
+ }
126
+ function sameAddress(value, address) {
127
+ return isAddressLike(value) && value.toLowerCase() === address.toLowerCase();
128
+ }
129
+ function isHexBytes(value) {
130
+ return typeof value === "string" && /^0x(?:[0-9a-f]{2})*$/iu.test(value);
131
+ }
132
+ /** A uint256 given as decimal or 0x-hex digits, a safe integer or a bigint. */
133
+ function uint256(value) {
134
+ let parsed;
135
+ if (typeof value === "bigint")
136
+ parsed = value;
137
+ else if (typeof value === "number" && Number.isSafeInteger(value)) {
138
+ parsed = BigInt(value);
139
+ }
140
+ else if (typeof value === "string" &&
141
+ /^(?:0x[0-9a-f]+|\d+)$/iu.test(value)) {
142
+ parsed = BigInt(value);
143
+ }
144
+ else
145
+ return null;
146
+ return parsed >= 0n && parsed <= MAX_UINT256 ? parsed : null;
147
+ }
148
+ function uuidOf(value) {
149
+ if (typeof value !== "string")
150
+ return null;
151
+ const uuid = value.toLowerCase();
152
+ return UUID.test(uuid) ? uuid : null;
153
+ }
154
+ function readEntry(entry) {
155
+ if (!entry || typeof entry !== "object")
156
+ return null;
157
+ const { chain, target, data, value } = entry;
158
+ const amount = uint256(value);
159
+ return typeof chain === "number" &&
160
+ Number.isSafeInteger(chain) &&
161
+ chain > 0 &&
162
+ isAddressLike(target) &&
163
+ isHexBytes(data) &&
164
+ amount !== null
165
+ ? { chain, target, data, value: amount }
166
+ : null;
167
+ }
168
+ /**
169
+ * Whether a record's request is the posted transaction: the same chain,
170
+ * target, calldata and value, and the same virtual nonce (always when
171
+ * `nonce` is "required", else only when both carry one).
172
+ */
173
+ function isRequestFor(request, entry, nonce) {
174
+ const read = readEntry(request);
175
+ const expected = readEntry(entry);
176
+ if (!read || !expected)
177
+ return false;
178
+ const quoted = request.virtual_nonce;
179
+ return (read.chain === expected.chain &&
180
+ read.target.toLowerCase() === expected.target.toLowerCase() &&
181
+ read.data.toLowerCase() === expected.data.toLowerCase() &&
182
+ read.value === expected.value &&
183
+ (nonce === "required"
184
+ ? quoted === entry.virtual_nonce
185
+ : typeof quoted !== "number" ||
186
+ typeof entry.virtual_nonce !== "number" ||
187
+ quoted === entry.virtual_nonce));
188
+ }
189
+ /** Relayr runs bundles on Ethereum, Optimism, Base and Arbitrum, and on their Sepolia testnets. */
190
+ export function relayrSupportsChain(chainId) {
191
+ return MAINNETS.includes(chainId) || TESTNETS.includes(chainId);
192
+ }
193
+ /**
194
+ * One destination per chain, every one in a single network family: all
195
+ * mainnets or all testnets.
196
+ */
197
+ export function relayrSupportsChains(chainIds) {
198
+ return (chainIds.length > 0 &&
199
+ new Set(chainIds).size === chainIds.length &&
200
+ [MAINNETS, TESTNETS].some((family) => chainIds.every((chainId) => family.includes(chainId))));
201
+ }
202
+ /**
203
+ * The chains that may fund a bundle with these destinations: their network
204
+ * family, so a testnet action never spends mainnet ETH. Empty when the
205
+ * destinations are unsupported or mix families.
206
+ */
207
+ export function relayrPaymentChains(chainIds) {
208
+ const unique = [...new Set(chainIds)];
209
+ if (!relayrSupportsChains(unique))
210
+ return [];
211
+ return [...(MAINNETS.includes(unique[0]) ? MAINNETS : TESTNETS)];
212
+ }
213
+ /**
214
+ * The body to post to `${RELAYR_API}/v1/bundle/prepaid`. Each chain's
215
+ * transactions run in order: their virtual nonces count up from 0 per chain,
216
+ * independently of other chains. Throws unless every transaction is
217
+ * well-formed and the destinations share one network family.
218
+ */
219
+ export function relayrBundleRequest(entries) {
220
+ if (!relayrSupportsChains([...new Set(entries.map((entry) => entry.chain))])) {
221
+ throw new Error("Choose supported destinations from one network family: mainnets or testnets.");
222
+ }
223
+ const nextNonce = new Map();
224
+ const transactions = entries.map((entry) => {
225
+ const read = readEntry(entry);
226
+ if (!read)
227
+ throw new Error("A Relayr bundle transaction is malformed.");
228
+ const virtual_nonce = nextNonce.get(read.chain) ?? 0;
229
+ nextNonce.set(read.chain, virtual_nonce + 1);
230
+ return {
231
+ chain: read.chain,
232
+ target: read.target,
233
+ data: read.data,
234
+ value: read.value.toString(),
235
+ virtual_nonce,
236
+ };
237
+ });
238
+ return { transactions, virtual_nonce_mode: "ChainIndependent" };
239
+ }
240
+ /**
241
+ * Relayr's `GET /v1/bundle/{uuid}` for exactly this bundle, or `unavailable`.
242
+ * Never from a cache: a stale answer could say a paid bundle is unpaid.
243
+ */
244
+ async function readBundle(fetchBundle, bundleUuid, unavailable) {
245
+ let bundle;
246
+ try {
247
+ const response = await fetchBundle(`${RELAYR_API}/v1/bundle/${bundleUuid}`, {
248
+ cache: "no-store",
249
+ signal: AbortSignal.timeout(BUNDLE_READ_TIMEOUT_MS),
250
+ });
251
+ if (!response.ok)
252
+ throw new Error(`Relayr HTTP ${response.status}`);
253
+ bundle = await response.json();
254
+ }
255
+ catch (cause) {
256
+ throw errorWithCause(unavailable, cause);
257
+ }
258
+ if (!bundle || uuidOf(bundle.bundle_uuid) !== bundleUuid) {
259
+ throw new Error(unavailable);
260
+ }
261
+ return bundle;
262
+ }
263
+ /**
264
+ * Authenticate Relayr's response to posting `request` (from
265
+ * {@link relayrBundleRequest}). Relayr lists transaction IDs out of request
266
+ * order, so each posted transaction is bound to the quoted ID whose record
267
+ * carries its exact request; the records are read from the bundle (with
268
+ * `fetch`) when the response leaves them out. The records must be exactly the
269
+ * quoted IDs, one each, as {@link verifyRelayrDestinations} will require of
270
+ * Relayr's status. Throws, with nothing paid, unless every posted transaction
271
+ * is bound to its own ID.
272
+ */
273
+ export async function bindRelayrQuote(response, request, { fetch: fetchBundle = globalThis.fetch, } = {}) {
274
+ if (!response.ok) {
275
+ const detail = await response.text().catch(() => "");
276
+ throw new Error(`Relayr HTTP ${response.status}${detail ? `: ${detail.slice(0, HTTP_DETAIL_CHARACTERS)}` : ""}`);
277
+ }
278
+ let body;
279
+ try {
280
+ body = await response.json();
281
+ }
282
+ catch {
283
+ body = null;
284
+ }
285
+ if (!body || typeof body !== "object") {
286
+ throw new Error("Relayr returned an unreadable quote. Nothing was paid.");
287
+ }
288
+ const bundleUuid = uuidOf(body.bundle_uuid);
289
+ if (!bundleUuid) {
290
+ throw new Error("Relayr returned no valid bundle ID. Nothing was paid.");
291
+ }
292
+ const current = Array.isArray(body.tx_uuids) ? body.tx_uuids : null;
293
+ const legacy = Array.isArray(body.txn_uuids) ? body.txn_uuids : null;
294
+ if (current && legacy && JSON.stringify(current) !== JSON.stringify(legacy)) {
295
+ throw new Error("Relayr returned conflicting transaction IDs. Nothing was paid.");
296
+ }
297
+ const { transactions } = request;
298
+ const ids = (current ?? legacy ?? []).map(uuidOf);
299
+ if (!transactions.length ||
300
+ transactions.some((entry) => !Number.isSafeInteger(entry.virtual_nonce) || entry.virtual_nonce < 0) ||
301
+ ids.length !== transactions.length ||
302
+ ids.some((id) => id === null) ||
303
+ new Set(ids).size !== ids.length ||
304
+ !Array.isArray(body.payment_info)) {
305
+ throw new Error(UNBOUND);
306
+ }
307
+ const quoted = new Set(ids);
308
+ const idOf = (record) => record && typeof record === "object"
309
+ ? uuidOf(record.tx_uuid)
310
+ : null;
311
+ let records = Array.isArray(body.transactions)
312
+ ? body.transactions
313
+ : [];
314
+ if (records.length !== transactions.length ||
315
+ records.some((record) => idOf(record) === null || !record.request)) {
316
+ const bundle = await readBundle(fetchBundle, bundleUuid, UNRETURNED);
317
+ if (!Array.isArray(bundle.transactions))
318
+ throw new Error(UNRETURNED);
319
+ records = bundle.transactions;
320
+ }
321
+ // Every record carries one of the quoted IDs, once. With every posted
322
+ // transaction bound to its own record below, the records are then exactly
323
+ // the quoted IDs, as the destination proof requires of Relayr's status.
324
+ const recordIds = records.map(idOf);
325
+ if (recordIds.some((id) => id === null || !quoted.has(id)) ||
326
+ new Set(recordIds).size !== records.length) {
327
+ throw new Error(UNBOUND);
328
+ }
329
+ const bound = transactions.map((entry) => {
330
+ const matches = records.filter((record) => isRequestFor(record.request, entry, "required"));
331
+ if (matches.length !== 1)
332
+ throw new Error(UNBOUND);
333
+ return matches[0];
334
+ });
335
+ const expectedTransactions = bound.map((record, index) => ({
336
+ txUuid: idOf(record),
337
+ chain: transactions[index].chain,
338
+ entry: { ...transactions[index] },
339
+ }));
340
+ if (new Set(expectedTransactions.map((binding) => binding.txUuid)).size !==
341
+ transactions.length) {
342
+ throw new Error(UNBOUND);
343
+ }
344
+ return {
345
+ bundle_uuid: bundleUuid,
346
+ payment_info: body.payment_info,
347
+ transactions: bound,
348
+ expectedTransactions,
349
+ };
350
+ }
351
+ /** A quoted deadline in seconds: integer seconds, or an RFC 3339 time with an offset. */
352
+ function deadlineSeconds(value) {
353
+ if (typeof value === "number") {
354
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
355
+ }
356
+ if (typeof value !== "string")
357
+ return null;
358
+ if (/^\d+$/u.test(value)) {
359
+ const seconds = Number(value);
360
+ return Number.isSafeInteger(seconds) ? seconds : null;
361
+ }
362
+ const milliseconds = DEADLINE_TIME.test(value) ? Date.parse(value) : NaN;
363
+ return Number.isFinite(milliseconds) && milliseconds >= 0
364
+ ? Math.floor(milliseconds / 1_000)
365
+ : null;
366
+ }
367
+ /** The deadline word of payment calldata. */
368
+ function calldataDeadline(calldata) {
369
+ return BigInt(`0x${calldata.slice(74)}`);
370
+ }
371
+ /** A quote is dead once its deadline is 15 seconds away or less. */
372
+ function quoteExpired(deadline, nowSeconds) {
373
+ return deadline <= BigInt(Math.floor(nowSeconds)) + 15n;
374
+ }
375
+ /** Payment calldata: the selector, the bundle UUID as a right-padded bytes16 word, and a uint40 deadline word. */
376
+ function paymentCalldataFor(calldata, bundleUuid) {
377
+ return (typeof calldata === "string" &&
378
+ /^0x[0-9a-f]{136}$/iu.test(calldata) &&
379
+ calldata.slice(0, 10).toLowerCase() === RELAYR_PAYMENT_SELECTOR &&
380
+ calldata.slice(10, 74).toLowerCase() ===
381
+ `${bundleUuid.replaceAll("-", "")}${"0".repeat(32)}` &&
382
+ calldataDeadline(calldata) <= 0xffffffffffn);
383
+ }
384
+ /**
385
+ * Authenticate one of Relayr's payment options against its bundle: a chain in
386
+ * the destinations' network family, Relayr's payment contract and native
387
+ * token, calldata that pays for exactly `bundleUuid` with the quoted
388
+ * deadline, and a deadline more than 15 seconds away. Throws otherwise.
389
+ */
390
+ export function relayrPaymentDetails(payment, { bundleUuid, destinationChainIds, nowSeconds = Date.now() / 1_000, }) {
391
+ const chainId = payment?.chain;
392
+ if (typeof chainId !== "number" || !relayrSupportsChain(chainId)) {
393
+ throw new Error("Relayr returned an unsupported payment chain.");
394
+ }
395
+ if (!relayrPaymentChains(destinationChainIds).includes(chainId)) {
396
+ throw new Error("Choose a supported Relayr funding chain in the same network family as these destinations.");
397
+ }
398
+ if (!sameAddress(payment.target, RELAYR_PAYMENT_ADDRESS)) {
399
+ throw new Error("Relayr returned an unrecognized payment contract.");
400
+ }
401
+ if (!sameAddress(payment.token, RELAYR_NATIVE_TOKEN)) {
402
+ throw new Error("Relayr returned an unsupported payment token.");
403
+ }
404
+ const amount = uint256(payment.amount);
405
+ if (amount === null) {
406
+ throw new Error("Relayr returned an invalid payment amount.");
407
+ }
408
+ const uuid = uuidOf(bundleUuid);
409
+ if (!uuid)
410
+ throw new Error("Relayr returned an invalid bundle ID.");
411
+ const calldata = typeof payment.calldata === "string" ? payment.calldata.toLowerCase() : "";
412
+ if (!/^0x[0-9a-f]{136}$/u.test(calldata)) {
413
+ throw new Error("Relayr returned invalid payment calldata.");
414
+ }
415
+ if (calldata.slice(0, 10) !== RELAYR_PAYMENT_SELECTOR) {
416
+ throw new Error("Relayr returned an unrecognized payment function.");
417
+ }
418
+ if (calldata.slice(10, 74) !== `${uuid.replaceAll("-", "")}${"0".repeat(32)}`) {
419
+ throw new Error("Relayr payment calldata does not match this bundle.");
420
+ }
421
+ const deadline = calldataDeadline(calldata);
422
+ if (deadline > 0xffffffffffn) {
423
+ throw new Error("Relayr returned an invalid payment deadline.");
424
+ }
425
+ const quoted = deadlineSeconds(payment.payment_deadline);
426
+ if (quoted === null || BigInt(quoted) !== deadline) {
427
+ throw new Error("Relayr payment calldata does not match the quote deadline.");
428
+ }
429
+ if (quoteExpired(deadline, nowSeconds))
430
+ throw new Error(EXPIRED);
431
+ return {
432
+ chainId,
433
+ target: RELAYR_PAYMENT_ADDRESS,
434
+ amount,
435
+ calldata: calldata,
436
+ bundleUuid: uuid,
437
+ deadline,
438
+ };
439
+ }
440
+ /**
441
+ * The payment options to offer: each passes {@link relayrPaymentDetails} for
442
+ * the quote's bundle and destinations, one per chain (Relayr's first), copied
443
+ * so the quote's owner cannot change one under a later review.
444
+ */
445
+ export function relayrPaymentOptions(quote, destinationChainIds, nowSeconds) {
446
+ const chains = new Set();
447
+ const options = [];
448
+ for (const payment of Array.isArray(quote.payment_info)
449
+ ? quote.payment_info
450
+ : []) {
451
+ let chainId;
452
+ try {
453
+ ({ chainId } = relayrPaymentDetails(payment, {
454
+ bundleUuid: quote.bundle_uuid,
455
+ destinationChainIds,
456
+ nowSeconds,
457
+ }));
458
+ }
459
+ catch {
460
+ continue;
461
+ }
462
+ if (chains.has(chainId))
463
+ continue;
464
+ chains.add(chainId);
465
+ options.push({ ...payment });
466
+ }
467
+ return options;
468
+ }
469
+ /**
470
+ * Prove the payment chain runs Relayr's payment contract: the code at
471
+ * {@link RELAYR_PAYMENT_ADDRESS} hashes to {@link RELAYR_PAYMENT_CODE_HASH}.
472
+ */
473
+ export async function requireRelayrPaymentRuntime(client) {
474
+ let code;
475
+ try {
476
+ code = await client.getCode({
477
+ address: RELAYR_PAYMENT_ADDRESS,
478
+ blockTag: "latest",
479
+ });
480
+ }
481
+ catch (cause) {
482
+ throw errorWithCause("Could not authenticate the Relayr payment contract.", cause);
483
+ }
484
+ if (typeof code !== "string" ||
485
+ !/^0x(?:[0-9a-f]{2})+$/iu.test(code) ||
486
+ (code.length - 2) / 2 > PAYMENT_CODE_MAX_BYTES) {
487
+ throw new Error("Could not authenticate the Relayr payment contract.");
488
+ }
489
+ if (keccak256(code) !== RELAYR_PAYMENT_CODE_HASH) {
490
+ throw new Error("Relayr payment contract code is not recognized.");
491
+ }
492
+ }
493
+ /**
494
+ * The node's own words for a failure: a viem error's `details` in its first
495
+ * eight causes. Its `message` is never read, since over HTTP it quotes the
496
+ * RPC URL. Only an error from outside viem is read by its message.
497
+ */
498
+ function nodeDetail(error) {
499
+ let current = error;
500
+ for (let depth = 0; depth < 8 && current && typeof current === "object"; depth += 1) {
501
+ const { details, cause } = current;
502
+ if (typeof details === "string" && details.trim()) {
503
+ return details.trim().split("\n")[0].slice(0, HTTP_DETAIL_CHARACTERS);
504
+ }
505
+ current = cause;
506
+ }
507
+ return error instanceof Error && !("shortMessage" in error)
508
+ ? error.message.split("\n")[0].slice(0, HTTP_DETAIL_CHARACTERS)
509
+ : "the node refused the call";
510
+ }
511
+ /**
512
+ * Simulate the exact payment from `from` with the gas it is sent with,
513
+ * without CCIP-read. Relayr's contract returns nothing, so a revert or any
514
+ * return data refuses the payment.
515
+ */
516
+ export async function simulateRelayrPayment(client, { from, payment, }) {
517
+ let result;
518
+ try {
519
+ result = await simulateStateChangingTransaction(client, {
520
+ from,
521
+ to: payment.target,
522
+ data: payment.calldata,
523
+ value: payment.amount,
524
+ gas: RELAYR_PAYMENT_GAS,
525
+ });
526
+ }
527
+ catch (cause) {
528
+ throw errorWithCause(`The Relayr payment would fail: ${nodeDetail(cause)}`, cause);
529
+ }
530
+ if (result !== "0x") {
531
+ throw new Error("Relayr payment simulation returned an unexpected result.");
532
+ }
533
+ }
534
+ /** A status label as Relayr's states are compared: trimmed, in any case. */
535
+ function stateLabel(state) {
536
+ return typeof state === "string" ? state.trim().toLowerCase() : "";
537
+ }
538
+ /** Success is `success` or `completed`, in any case. */
539
+ export function relayrStateIsSuccess(state) {
540
+ const label = stateLabel(state);
541
+ return label === "success" || label === "completed";
542
+ }
543
+ /** `pending`, in any case: Relayr has not run the transaction. */
544
+ function relayrStateIsPending(state) {
545
+ return stateLabel(state) === "pending";
546
+ }
547
+ /**
548
+ * Only `failed`, in any case, is a failure. Relayr's other states say nothing
549
+ * final; receipts stay the proof either way.
550
+ */
551
+ export function relayrStateIsFailed(state) {
552
+ return stateLabel(state) === "failed";
553
+ }
554
+ /** Counts by Relayr's labels, with rows it has not reported yet counted as pending. */
555
+ export function relayrProgress(records, expectedCount = records.length) {
556
+ const total = Math.max(expectedCount, records.length);
557
+ const confirmed = records.filter((record) => relayrStateIsSuccess(record?.status?.state)).length;
558
+ const failed = records.filter((record) => relayrStateIsFailed(record?.status?.state)).length;
559
+ return {
560
+ confirmed,
561
+ failed,
562
+ pending: Math.max(total - confirmed - failed, 0),
563
+ total,
564
+ };
565
+ }
566
+ /** The destination transaction hash a record reports, if it is a transaction hash. */
567
+ export function relayrDestinationHash(record) {
568
+ const data = record?.status?.data;
569
+ const hash = data?.hash ?? data?.transaction?.hash;
570
+ return typeof hash === "string" && TRANSACTION_HASH.test(hash) ? hash : null;
571
+ }
572
+ /** A record's destination chain. Relayr's status nests it under `request`. */
573
+ export function relayrRecordChain(record) {
574
+ const chain = record?.request?.chain ?? record?.chain;
575
+ return typeof chain === "number" && Number.isSafeInteger(chain) && chain > 0
576
+ ? chain
577
+ : null;
578
+ }
579
+ function forwarderOn(chainId) {
580
+ return relayrSupportsChain(chainId)
581
+ ? jbContractAddress["6"].ERC2771Forwarder[chainId]
582
+ : undefined;
583
+ }
584
+ /**
585
+ * The ERC-2771 request an entry carries: an `execute` call on its chain's
586
+ * canonical Juicebox V6 forwarder whose calldata re-encodes byte for byte.
587
+ * Null for anything else: a raw call, another forwarder, another function or
588
+ * a dirty encoding.
589
+ */
590
+ export function relayrForwardRequest(entry) {
591
+ const forwarder = forwarderOn(entry?.chain);
592
+ if (!forwarder ||
593
+ !sameAddress(entry.target, forwarder) ||
594
+ !isHexBytes(entry.data)) {
595
+ return null;
596
+ }
597
+ try {
598
+ const decoded = decodeFunctionData({
599
+ abi: erc2771ForwarderAbi,
600
+ data: entry.data,
601
+ });
602
+ if (decoded.functionName !== "execute")
603
+ return null;
604
+ const [request] = decoded.args;
605
+ const encoded = encodeFunctionData({
606
+ abi: erc2771ForwarderAbi,
607
+ functionName: "execute",
608
+ args: [request],
609
+ });
610
+ return encoded.toLowerCase() === entry.data.toLowerCase()
611
+ ? { ...request }
612
+ : null;
613
+ }
614
+ catch {
615
+ return null;
616
+ }
617
+ }
618
+ /**
619
+ * Read `expected.hash` and prove it is exactly the expected transaction,
620
+ * canonically included. Returns its receipt, successful or reverted. A
621
+ * transaction hash commits to its sender, target, calldata, value and chain,
622
+ * so any difference there is a {@link RelayrProofError}. A receipt,
623
+ * transaction and block that disagree (a lagging node, a reorg) prove nothing
624
+ * either way.
625
+ */
626
+ async function proveTransaction(client, expected, words) {
627
+ const hash = expected.hash.toLowerCase();
628
+ let transaction;
629
+ let receipt;
630
+ try {
631
+ [transaction, receipt] = await Promise.all([
632
+ client.getTransaction({ hash: expected.hash }),
633
+ client.getTransactionReceipt({ hash: expected.hash }),
634
+ ]);
635
+ }
636
+ catch (cause) {
637
+ throw errorWithCause(words.unavailable, cause);
638
+ }
639
+ if (transaction?.hash?.toLowerCase() !== hash ||
640
+ receipt?.transactionHash?.toLowerCase() !== hash) {
641
+ throw new Error(words.unavailable);
642
+ }
643
+ if (transaction.chainId !== expected.chainId ||
644
+ !sameAddress(transaction.to, expected.to) ||
645
+ typeof transaction.input !== "string" ||
646
+ transaction.input.toLowerCase() !== expected.data.toLowerCase() ||
647
+ transaction.value !== expected.value ||
648
+ (expected.from !== undefined &&
649
+ !sameAddress(transaction.from, expected.from))) {
650
+ throw new RelayrProofError(words.mismatch);
651
+ }
652
+ if (!sameAddress(receipt.to, expected.to) ||
653
+ typeof receipt.blockHash !== "string" ||
654
+ !TRANSACTION_HASH.test(receipt.blockHash) ||
655
+ transaction.blockHash?.toLowerCase() !== receipt.blockHash.toLowerCase() ||
656
+ typeof receipt.blockNumber !== "bigint" ||
657
+ transaction.blockNumber !== receipt.blockNumber ||
658
+ (receipt.status !== "success" && receipt.status !== "reverted")) {
659
+ throw new Error(words.unavailable);
660
+ }
661
+ let block;
662
+ try {
663
+ block = await client.getBlock({ blockNumber: receipt.blockNumber });
664
+ }
665
+ catch (cause) {
666
+ throw errorWithCause(words.unavailable, cause);
667
+ }
668
+ if (block?.hash?.toLowerCase() !== receipt.blockHash.toLowerCase()) {
669
+ throw new Error(words.notCanonical);
670
+ }
671
+ return receipt;
672
+ }
673
+ /** A reviewed payment's parts, or null unless it is an authenticated Relayr payment. */
674
+ function readReviewedPayment(payment) {
675
+ const bundleUuid = uuidOf(payment?.bundleUuid);
676
+ const amount = uint256(payment?.amount);
677
+ if (!relayrSupportsChain(payment?.chainId) ||
678
+ !sameAddress(payment.target, RELAYR_PAYMENT_ADDRESS) ||
679
+ amount === null ||
680
+ bundleUuid === null ||
681
+ !paymentCalldataFor(payment.calldata, bundleUuid)) {
682
+ return null;
683
+ }
684
+ return {
685
+ chainId: payment.chainId,
686
+ amount,
687
+ bundleUuid,
688
+ calldata: payment.calldata,
689
+ deadline: calldataDeadline(payment.calldata),
690
+ };
691
+ }
692
+ /**
693
+ * Prove a Relayr payment from the chain: the transaction at `hash` is exactly
694
+ * `payment` sent by `from` (chain, payment contract, calldata, value),
695
+ * canonically included, and successful. Pass the hash that was mined: when a
696
+ * wallet speeds a payment up, viem's receipt is the replacement's, so verify
697
+ * its `transactionHash`, not the hash the wallet first returned.
698
+ *
699
+ * Throws {@link RelayrPaymentRevertedError} when it canonically reverted. That
700
+ * transaction paid nothing, but the bundle may still be paid by another, so
701
+ * only {@link requireRelayrPaymentRetry} clears the quote to be paid again.
702
+ * {@link RelayrProofError} means the transaction is a different one: never pay
703
+ * again. Any other error means the proof is unavailable for now.
704
+ */
705
+ export async function verifyRelayrPayment(client, { hash, from, payment, }) {
706
+ const reviewed = readReviewedPayment(payment);
707
+ if (typeof hash !== "string" ||
708
+ !TRANSACTION_HASH.test(hash) ||
709
+ !isAddressLike(from) ||
710
+ !reviewed) {
711
+ throw new Error("Only an authenticated Relayr payment with its transaction hash can be verified.");
712
+ }
713
+ const receipt = await proveTransaction(client, {
714
+ hash,
715
+ chainId: reviewed.chainId,
716
+ from,
717
+ to: RELAYR_PAYMENT_ADDRESS,
718
+ data: reviewed.calldata,
719
+ value: reviewed.amount,
720
+ }, {
721
+ unavailable: `Could not read Relayr payment ${hash} on chain ${reviewed.chainId}. Do not pay again; check it later.`,
722
+ mismatch: "The funding transaction does not match the reviewed Relayr payment. Do not pay again; inspect the wallet's activity and the saved bundle.",
723
+ notCanonical: "The Relayr funding receipt is no longer canonical. Do not pay again; check it later.",
724
+ });
725
+ if (receipt.status === "reverted") {
726
+ throw new RelayrPaymentRevertedError("The Relayr funding transaction reverted onchain.", hash, reviewed.chainId);
727
+ }
728
+ return receipt;
729
+ }
730
+ /**
731
+ * Clear a saved quote to be paid once more after its payments reverted.
732
+ * `hashes` is every payment this session sent for the quote, each as mined
733
+ * (see {@link verifyRelayrPayment}). A bundle ID belongs to one quote, and a
734
+ * quote to the session on the device that requested it, so the session's own
735
+ * hashes are every payment that could have funded the bundle. Resolves only
736
+ * when:
737
+ *
738
+ * - every one of them is exactly the reviewed payment, canonically reverted;
739
+ * - the quote's deadline is more than 15 seconds away (`nowSeconds`, by
740
+ * default the clock);
741
+ * - Relayr's bundle, read with `fetch` and never from a cache, reports
742
+ * `payment_received: false`, and every record is still pending with no
743
+ * destination hash. Relayr runs only paid bundles.
744
+ *
745
+ * The payment contract keeps no state and Relayr keeps every payment it
746
+ * receives, so anything else rules a new payment out, and an empty list never
747
+ * clears one. A payment still in flight from elsewhere is not visible yet.
748
+ * Throws in every other case, including when an answer is unavailable. Never
749
+ * pay on an error.
750
+ */
751
+ export async function requireRelayrPaymentRetry(client, { hashes, from, payment, }, { fetch: fetchBundle = globalThis.fetch, nowSeconds = Date.now() / 1_000, } = {}) {
752
+ if (!Array.isArray(hashes) || !hashes.length) {
753
+ throw new Error("Name every payment this session sent for the quote before paying it again.");
754
+ }
755
+ const reviewed = readReviewedPayment(payment);
756
+ if (reviewed && quoteExpired(reviewed.deadline, nowSeconds)) {
757
+ throw new Error(EXPIRED);
758
+ }
759
+ for (const hash of hashes) {
760
+ try {
761
+ await verifyRelayrPayment(client, { hash, from, payment });
762
+ }
763
+ catch (error) {
764
+ if (error instanceof RelayrPaymentRevertedError)
765
+ continue;
766
+ throw error;
767
+ }
768
+ throw new Error("This Relayr payment succeeded onchain, so its bundle is paid. Do not pay again.");
769
+ }
770
+ const { payment_received, transactions } = await readBundle(fetchBundle, reviewed.bundleUuid, UNKNOWN_PAYMENT);
771
+ if (payment_received === true) {
772
+ throw new Error("Relayr already reports a payment for this bundle. Do not pay again.");
773
+ }
774
+ if (payment_received !== false ||
775
+ !Array.isArray(transactions) ||
776
+ !transactions.length) {
777
+ throw new Error(UNKNOWN_PAYMENT);
778
+ }
779
+ if (transactions.some((record) => relayrDestinationHash(record) !== null ||
780
+ !relayrStateIsPending(record?.status?.state))) {
781
+ throw new Error("Relayr reports a transaction of this bundle as running or run. Do not pay again.");
782
+ }
783
+ }
784
+ function destinationWords(hash, chainId) {
785
+ return {
786
+ unavailable: `Could not read destination transaction ${hash} on chain ${chainId}. Keep the original bundle pending; do not pay again.`,
787
+ mismatch: "The destination transaction does not prove the signed Relayr call. Keep the original bundle pending; do not pay again.",
788
+ notCanonical: "The destination receipt is no longer canonical. Keep the original bundle pending.",
789
+ };
790
+ }
791
+ /**
792
+ * Prove one destination from the chain: the transaction at `hash` is exactly
793
+ * `entry` (chain, target, calldata, value), canonically included, and
794
+ * successful. Throws {@link RelayrDestinationRevertedError} when it
795
+ * canonically reverted, {@link RelayrProofError} when it is a different
796
+ * transaction, and any other error while the proof is unavailable.
797
+ */
798
+ export async function verifyRelayrDestination(client, { entry, hash }) {
799
+ const read = readEntry(entry);
800
+ if (!read || typeof hash !== "string" || !TRANSACTION_HASH.test(hash)) {
801
+ throw new Error(NO_PROOF);
802
+ }
803
+ const receipt = await proveTransaction(client, {
804
+ hash,
805
+ chainId: read.chain,
806
+ to: read.target,
807
+ data: read.data,
808
+ value: read.value,
809
+ }, destinationWords(hash, read.chain));
810
+ if (receipt.status === "reverted") {
811
+ throw new RelayrDestinationRevertedError("The destination receipt does not prove the signed Relayr call succeeded: it reverted onchain. Keep the original bundle pending; do not pay again.", hash, read.chain);
812
+ }
813
+ return receipt;
814
+ }
815
+ /**
816
+ * Prove every destination of a bundle from the chain. `bindings` are the
817
+ * quote's {@link RelayrQuote.expectedTransactions}; `records` are Relayr's
818
+ * status records, which only say which hash to check. Every record must carry
819
+ * one of the quote's IDs, once. Each binding then needs exactly one record on
820
+ * its chain carrying its exact request; only a record without a request is
821
+ * paired by its ID, since bindings saved before request binding paired IDs by
822
+ * position. Every hash must prove its binding onchain with
823
+ * {@link verifyRelayrDestination}.
824
+ *
825
+ * With `account`, every entry must also be that account's ERC-2771 request
826
+ * on its chain's canonical forwarder, for the entry's own value.
827
+ *
828
+ * Returns the receipts in binding order. Throws
829
+ * {@link RelayrDestinationRevertedError} when a destination canonically
830
+ * reverted, another {@link RelayrProofError} when Relayr or the chain
831
+ * contradicts a binding, and any other error while proof is unavailable.
832
+ * Never pay again on any error.
833
+ */
834
+ export async function verifyRelayrDestinations(clientFor, { bindings, records, account, }) {
835
+ const ids = bindings.map((binding) => uuidOf(binding?.txUuid));
836
+ if (!bindings.length ||
837
+ ids.some((id) => id === null) ||
838
+ new Set(ids).size !== bindings.length ||
839
+ bindings.some((binding) => readEntry(binding.entry)?.chain !== binding.chain)) {
840
+ throw new Error(NO_PROOF);
841
+ }
842
+ if (account !== undefined) {
843
+ for (const { entry } of bindings) {
844
+ const request = relayrForwardRequest(entry);
845
+ if (!isAddressLike(account) ||
846
+ !request ||
847
+ !sameAddress(request.from, account) ||
848
+ request.value !== uint256(entry.value)) {
849
+ throw new Error("The saved relay authorization does not match its account or value.");
850
+ }
851
+ }
852
+ }
853
+ const quoted = new Set(ids);
854
+ const recordIds = records.map((record) => uuidOf(record?.tx_uuid));
855
+ if (records.length !== bindings.length)
856
+ throw new Error(NOT_IDENTIFIED);
857
+ if (recordIds.some((id) => id === null || !quoted.has(id)) ||
858
+ new Set(recordIds).size !== records.length) {
859
+ throw new RelayrProofError("Relayr's status names a transaction this quote did not bind. Keep the original bundle pending; do not pay again.");
860
+ }
861
+ const hashes = bindings.map((binding, index) => {
862
+ const matches = records.filter((record, recordIndex) => relayrRecordChain(record) === binding.chain &&
863
+ (record.request !== undefined && record.request !== null
864
+ ? isRequestFor(record.request, binding.entry, "when-present")
865
+ : recordIds[recordIndex] === ids[index]));
866
+ if (matches.length !== 1) {
867
+ throw new RelayrProofError("Relayr's destination call does not match the signed request. Keep the original bundle pending; do not pay again.");
868
+ }
869
+ const hash = relayrDestinationHash(matches[0]);
870
+ if (!hash)
871
+ throw new Error(NOT_IDENTIFIED);
872
+ return hash;
873
+ });
874
+ if (new Set(hashes.map((hash) => hash.toLowerCase())).size !== hashes.length) {
875
+ throw new RelayrProofError("Relayr reported one destination transaction for two signed calls. Keep the original bundle pending; do not pay again.");
876
+ }
877
+ const verified = [];
878
+ for (const [index, binding] of bindings.entries()) {
879
+ const client = clientFor(binding.chain);
880
+ if (!client) {
881
+ throw new Error(`No RPC is available for chain ${binding.chain}. Keep the original bundle pending; do not pay again.`);
882
+ }
883
+ verified.push({
884
+ txUuid: ids[index],
885
+ chainId: binding.chain,
886
+ receipt: await verifyRelayrDestination(client, {
887
+ entry: binding.entry,
888
+ hash: hashes[index],
889
+ }),
890
+ });
891
+ }
892
+ return verified;
893
+ }
894
+ //# sourceMappingURL=relayr.js.map