@bananapus/nana-sdk-core 2.24.1 → 2.24.3
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/dist/cjs/review/relayr.d.ts +21 -7
- package/dist/cjs/review/relayr.d.ts.map +1 -1
- package/dist/cjs/review/relayr.js +120 -17
- package/dist/cjs/review/relayr.js.map +1 -1
- package/dist/cjs/review/safeRelayr.d.ts +31 -3
- package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
- package/dist/cjs/review/safeRelayr.js +410 -121
- package/dist/cjs/review/safeRelayr.js.map +1 -1
- package/dist/esm/review/relayr.d.ts +21 -7
- package/dist/esm/review/relayr.d.ts.map +1 -1
- package/dist/esm/review/relayr.js +117 -17
- package/dist/esm/review/relayr.js.map +1 -1
- package/dist/esm/review/safeRelayr.d.ts +31 -3
- package/dist/esm/review/safeRelayr.d.ts.map +1 -1
- package/dist/esm/review/safeRelayr.js +410 -122
- package/dist/esm/review/safeRelayr.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SafeRelayrRecoveryError = void 0;
|
|
4
|
+
exports.canReplaceSafeRelayrQuote = canReplaceSafeRelayrQuote;
|
|
4
5
|
exports.safeRelayrReservationKey = safeRelayrReservationKey;
|
|
5
6
|
exports.sameSafeRelayrIntents = sameSafeRelayrIntents;
|
|
6
7
|
exports.requireSafeRelayrExecution = requireSafeRelayrExecution;
|
|
@@ -12,6 +13,65 @@ const safe_js_1 = require("../safe.js");
|
|
|
12
13
|
const safeService_js_1 = require("../safeService.js");
|
|
13
14
|
const contractWrite_js_1 = require("./contractWrite.js");
|
|
14
15
|
const relayr_js_1 = require("./relayr.js");
|
|
16
|
+
function hasFundingRecords(records) {
|
|
17
|
+
if (records === undefined)
|
|
18
|
+
return false;
|
|
19
|
+
if (!Array.isArray(records))
|
|
20
|
+
return true;
|
|
21
|
+
return records.some((record) => {
|
|
22
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
23
|
+
return true;
|
|
24
|
+
const status = record.status;
|
|
25
|
+
if (status === undefined)
|
|
26
|
+
return false;
|
|
27
|
+
if (!status || typeof status !== "object" || Array.isArray(status))
|
|
28
|
+
return true;
|
|
29
|
+
const data = status.data;
|
|
30
|
+
if (data !== undefined &&
|
|
31
|
+
data !== null &&
|
|
32
|
+
(typeof data !== "object" || Array.isArray(data)))
|
|
33
|
+
return true;
|
|
34
|
+
const transaction = data?.transaction;
|
|
35
|
+
if (transaction !== undefined &&
|
|
36
|
+
(transaction === null ||
|
|
37
|
+
typeof transaction !== "object" ||
|
|
38
|
+
Array.isArray(transaction)))
|
|
39
|
+
return true;
|
|
40
|
+
if (status.data?.hash !== undefined ||
|
|
41
|
+
status.data?.transaction?.hash !== undefined)
|
|
42
|
+
return true;
|
|
43
|
+
return status.state !== undefined && !(0, relayr_js_1.relayrStateIsPending)(status.state);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function hasObservedFunding(session) {
|
|
47
|
+
return ((session.fundingObserved !== undefined &&
|
|
48
|
+
session.fundingObserved !== false) ||
|
|
49
|
+
hasFundingRecords(session.records) ||
|
|
50
|
+
hasFundingRecords(session.quote?.transactions));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Safe signatures already authorize a nonce-protected call. Publishing or losing
|
|
54
|
+
* an unused quote is not a funding attempt. Only this Safe-specific owner may
|
|
55
|
+
* replace such a quote; raw/forwarded authorizations keep their existing rules.
|
|
56
|
+
*/
|
|
57
|
+
function canReplaceSafeRelayrQuote(session) {
|
|
58
|
+
if ((session.state !== "publishing" && session.state !== "active") ||
|
|
59
|
+
session.paymentStatus !== "unfunded" ||
|
|
60
|
+
!Array.isArray(session.payments) ||
|
|
61
|
+
session.payments.length !== 0 ||
|
|
62
|
+
(session.reservationKeys !== undefined &&
|
|
63
|
+
!Array.isArray(session.reservationKeys)) ||
|
|
64
|
+
hasObservedFunding(session))
|
|
65
|
+
return false;
|
|
66
|
+
try {
|
|
67
|
+
validateExecutions(session.executions);
|
|
68
|
+
const identities = new Set(session.executions.map(safeRelayrReservationKey));
|
|
69
|
+
return !session.reservationKeys?.some((key) => !identities.has(key.toLowerCase()));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
15
75
|
class SafeRelayrRecoveryError extends Error {
|
|
16
76
|
constructor(session, message = "This Safe execution has an existing Relayr bundle. Check the existing bundle before submitting another payment.", recovery) {
|
|
17
77
|
super(message);
|
|
@@ -172,6 +232,13 @@ function requireSafeRelayrRecords(bindings, records) {
|
|
|
172
232
|
throw new Error("Relayr's record does not match the saved Safe transaction binding.");
|
|
173
233
|
}
|
|
174
234
|
}
|
|
235
|
+
function requireSafeLanding(execution, { receipt }) {
|
|
236
|
+
const { safe, safeTxHash } = execution;
|
|
237
|
+
(0, safeService_js_1.requireSafeExecutionSuccess)(receipt, safe, safeTxHash);
|
|
238
|
+
const event = (0, safeService_js_1.safeExecutionResult)(receipt, safe, safeTxHash);
|
|
239
|
+
if (event.status !== "success" || event.payment !== 0n)
|
|
240
|
+
throw new Error("The Safe execution reimbursed its executor. Keep the existing bundle for verification.");
|
|
241
|
+
}
|
|
175
242
|
/** Canonical exact destination receipts plus matching, refund-free Safe success events. */
|
|
176
243
|
async function verifySafeRelayrLanding(clientFor, { executions, bindings, records, }) {
|
|
177
244
|
validateExecutions(executions);
|
|
@@ -184,13 +251,7 @@ async function verifySafeRelayrLanding(clientFor, { executions, bindings, record
|
|
|
184
251
|
bindings,
|
|
185
252
|
records,
|
|
186
253
|
});
|
|
187
|
-
verified.forEach((
|
|
188
|
-
const { safe, safeTxHash } = executions[index];
|
|
189
|
-
(0, safeService_js_1.requireSafeExecutionSuccess)(receipt, safe, safeTxHash);
|
|
190
|
-
const event = (0, safeService_js_1.safeExecutionResult)(receipt, safe, safeTxHash);
|
|
191
|
-
if (event.status !== "success" || event.payment !== 0n)
|
|
192
|
-
throw new Error("The Safe execution reimbursed its executor. Keep the existing bundle for verification.");
|
|
193
|
-
});
|
|
254
|
+
verified.forEach((item, index) => requireSafeLanding(executions[index], item));
|
|
194
255
|
return verified;
|
|
195
256
|
}
|
|
196
257
|
/** Validate persisted bindings again; local storage is not authenticated evidence. */
|
|
@@ -216,6 +277,49 @@ function requireSessionQuote(session) {
|
|
|
216
277
|
function createSafeRelayrController(options) {
|
|
217
278
|
const { store } = options;
|
|
218
279
|
const fetchRelayr = options.fetch ?? globalThis.fetch;
|
|
280
|
+
const displayedExecutions = new Map();
|
|
281
|
+
const notify = (progress, rechecking = false) => {
|
|
282
|
+
if (progress.type === "execution") {
|
|
283
|
+
const key = `${progress.session.id}:${progress.index}`;
|
|
284
|
+
const display = JSON.stringify([
|
|
285
|
+
progress.status,
|
|
286
|
+
progress.hash,
|
|
287
|
+
progress.message,
|
|
288
|
+
]);
|
|
289
|
+
const previous = displayedExecutions.get(key);
|
|
290
|
+
// Rechecking does not undo a proven display result. A later unavailable,
|
|
291
|
+
// contradictory, or changed-hash result explicitly updates it below.
|
|
292
|
+
if (previous === display ||
|
|
293
|
+
(rechecking &&
|
|
294
|
+
previous === JSON.stringify(["executed", progress.hash, undefined])))
|
|
295
|
+
return;
|
|
296
|
+
displayedExecutions.set(key, display);
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
// A view must not mutate frozen calls or turn a submitted payment into a
|
|
300
|
+
// rejected operation. Also consume rejection from an async UI callback.
|
|
301
|
+
void Promise.resolve(options.onProgress?.(structuredClone(progress))).catch(() => undefined);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// Display observers cannot change the durable execution lifecycle.
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const phase = (phase, session, signal) => {
|
|
308
|
+
if (!signal?.aborted)
|
|
309
|
+
notify({ type: "phase", phase, session });
|
|
310
|
+
};
|
|
311
|
+
const savedPhase = (session, signal) => {
|
|
312
|
+
if (session.state === "released")
|
|
313
|
+
return;
|
|
314
|
+
if (session.state === "complete")
|
|
315
|
+
phase("complete", session, signal);
|
|
316
|
+
else if (session.paymentStatus === "confirmed" ||
|
|
317
|
+
hasObservedFunding(session))
|
|
318
|
+
phase("executing", session, signal);
|
|
319
|
+
else if (session.paymentStatus === "submitted" ||
|
|
320
|
+
session.paymentStatus === "sending")
|
|
321
|
+
phase("payment-confirming", session, signal);
|
|
322
|
+
};
|
|
219
323
|
const checkAccount = (account, signal) => {
|
|
220
324
|
signal?.throwIfAborted();
|
|
221
325
|
if (!(0, viem_1.isAddress)(account) ||
|
|
@@ -273,10 +377,12 @@ function createSafeRelayrController(options) {
|
|
|
273
377
|
};
|
|
274
378
|
const unidentified = (session) => !session.executions.length && !session.reservationKeys?.length;
|
|
275
379
|
const unresolved = (session) => session.state !== "complete" && session.state !== "released";
|
|
380
|
+
const reserves = (session, executions) => unresolved(session) &&
|
|
381
|
+
(store.scope === "single-session" ||
|
|
382
|
+
unidentified(session) ||
|
|
383
|
+
overlap(session, executions));
|
|
276
384
|
const assertUnreserved = async (account, executions, exceptId) => {
|
|
277
|
-
const conflict = (await store.list(account)).find((session) => session.id !== exceptId &&
|
|
278
|
-
unresolved(session) &&
|
|
279
|
-
(unidentified(session) || overlap(session, executions)));
|
|
385
|
+
const conflict = (await store.list(account)).find((session) => session.id !== exceptId && reserves(session, executions));
|
|
280
386
|
if (conflict)
|
|
281
387
|
throw new SafeRelayrRecoveryError(conflict);
|
|
282
388
|
};
|
|
@@ -349,7 +455,8 @@ function createSafeRelayrController(options) {
|
|
|
349
455
|
// session per Safe and replacing it would erase the unresolved evidence.
|
|
350
456
|
if (session.paymentStatus !== "unfunded" ||
|
|
351
457
|
!Array.isArray(session.payments) ||
|
|
352
|
-
session.payments.length
|
|
458
|
+
session.payments.length ||
|
|
459
|
+
hasObservedFunding(session))
|
|
353
460
|
return pending({
|
|
354
461
|
reason: "funding-unresolved",
|
|
355
462
|
message: "Every saved Safe nonce has been used, but this selection still has unresolved payment evidence. Inspect the original Safe proposals and funding transactions before replacing it. No refund or successful execution has been confirmed.",
|
|
@@ -392,77 +499,256 @@ function createSafeRelayrController(options) {
|
|
|
392
499
|
checks,
|
|
393
500
|
});
|
|
394
501
|
}
|
|
395
|
-
async function inspect(session) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
records.every((record) => record.status?.data?.hash || record.status?.data?.transaction?.hash)) {
|
|
417
|
-
const verified = await verifySafeRelayrLanding(options.clientFor, {
|
|
418
|
-
executions: session.executions,
|
|
419
|
-
bindings: quote.expectedTransactions,
|
|
420
|
-
records,
|
|
502
|
+
async function inspect(session, signal) {
|
|
503
|
+
const report = (progress, rechecking = false) => {
|
|
504
|
+
if (!signal?.aborted)
|
|
505
|
+
notify(progress, rechecking);
|
|
506
|
+
};
|
|
507
|
+
try {
|
|
508
|
+
savedPhase(session, signal);
|
|
509
|
+
if (session.state === "released")
|
|
510
|
+
return result("released", session);
|
|
511
|
+
if (session.state === "complete")
|
|
512
|
+
return result("complete", session);
|
|
513
|
+
if (hasObservedFunding(session) && session.fundingObserved !== true) {
|
|
514
|
+
session = { ...session, fundingObserved: true };
|
|
515
|
+
await store.save(session);
|
|
516
|
+
}
|
|
517
|
+
if (!session.bundleUuid || !session.quote)
|
|
518
|
+
return inspectMissingQuote(session);
|
|
519
|
+
const quote = requireSessionQuote(session);
|
|
520
|
+
const chains = session.executions.map(({ entry }) => entry.chain);
|
|
521
|
+
const bundle = await (0, relayr_js_1.readRelayrBundle)(session.bundleUuid, {
|
|
522
|
+
fetch: fetchRelayr,
|
|
421
523
|
});
|
|
422
|
-
|
|
423
|
-
|
|
524
|
+
if (hasObservedFunding(session) ||
|
|
525
|
+
bundle.payment_received === true ||
|
|
526
|
+
hasFundingRecords(bundle.transactions)) {
|
|
527
|
+
session = { ...session, fundingObserved: true };
|
|
528
|
+
await store.save(session);
|
|
529
|
+
}
|
|
530
|
+
if (!Array.isArray(bundle.transactions))
|
|
531
|
+
throw new SafeRelayrRecoveryError(session, "Relayr has not returned this bundle's transactions. Check the existing bundle again.");
|
|
532
|
+
const records = bundle.transactions;
|
|
533
|
+
requireSafeRelayrRecords(quote.expectedTransactions, records);
|
|
534
|
+
session = { ...session, records };
|
|
424
535
|
await store.save(session);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return result("pending", session);
|
|
536
|
+
savedPhase(session, signal);
|
|
537
|
+
// Each hash is only a pointer. Check available chains independently, using
|
|
538
|
+
// the same complete binding validator and exact receipt proof as completion.
|
|
539
|
+
const indexed = quote.expectedTransactions.map((binding, index) => ({
|
|
540
|
+
binding,
|
|
541
|
+
index,
|
|
542
|
+
record: records.find((record) => record.tx_uuid?.toLowerCase() === binding.txUuid.toLowerCase()),
|
|
543
|
+
}));
|
|
544
|
+
const known = indexed.filter(({ record }) => record.status?.data?.hash !== undefined ||
|
|
545
|
+
record.status?.data?.transaction?.hash !== undefined);
|
|
546
|
+
if (known.length) {
|
|
437
547
|
try {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
payment,
|
|
548
|
+
(0, relayr_js_1.relayrDestinationHashes)({
|
|
549
|
+
bindings: known.map(({ binding }) => binding),
|
|
550
|
+
records: known.map(({ record }) => record),
|
|
442
551
|
});
|
|
443
|
-
session = { ...session, paymentStatus: "confirmed" };
|
|
444
|
-
await store.save(session);
|
|
445
|
-
return result("pending", session);
|
|
446
552
|
}
|
|
447
553
|
catch (error) {
|
|
448
|
-
|
|
449
|
-
|
|
554
|
+
for (const { index, record } of known)
|
|
555
|
+
report({
|
|
556
|
+
type: "execution",
|
|
557
|
+
session,
|
|
558
|
+
index,
|
|
559
|
+
execution: session.executions[index],
|
|
560
|
+
status: "failed",
|
|
561
|
+
hash: (0, relayr_js_1.relayrDestinationHash)(record) ?? undefined,
|
|
562
|
+
message: error instanceof Error
|
|
563
|
+
? error.message
|
|
564
|
+
: "The destination records do not match this quote.",
|
|
565
|
+
});
|
|
566
|
+
throw new SafeRelayrRecoveryError(session, error instanceof Error ? error.message : undefined);
|
|
450
567
|
}
|
|
568
|
+
phase("executing", session, signal);
|
|
451
569
|
}
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
570
|
+
if (known.length ||
|
|
571
|
+
session.paymentStatus !== "unfunded" ||
|
|
572
|
+
session.fundingObserved) {
|
|
573
|
+
for (const { index, record } of indexed) {
|
|
574
|
+
const hash = (0, relayr_js_1.relayrDestinationHash)(record) ?? undefined;
|
|
575
|
+
report({
|
|
576
|
+
type: "execution",
|
|
577
|
+
session,
|
|
578
|
+
index,
|
|
579
|
+
execution: session.executions[index],
|
|
580
|
+
status: hash
|
|
581
|
+
? "confirming"
|
|
582
|
+
: (0, relayr_js_1.relayrStateIsFailed)(record.status?.state)
|
|
583
|
+
? "failed"
|
|
584
|
+
: "pending",
|
|
585
|
+
hash,
|
|
586
|
+
...(!hash && (0, relayr_js_1.relayrStateIsFailed)(record.status?.state)
|
|
587
|
+
? {
|
|
588
|
+
message: "Relayr reported this execution failed. Its onchain result has not been verified.",
|
|
589
|
+
}
|
|
590
|
+
: {}),
|
|
591
|
+
}, true);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const landed = await Promise.allSettled(known.map(async ({ binding, index, record }) => {
|
|
595
|
+
const execution = session.executions[index];
|
|
596
|
+
const hash = (0, relayr_js_1.relayrDestinationHash)(record);
|
|
597
|
+
const client = options.clientFor(binding.chain);
|
|
598
|
+
if (!client) {
|
|
599
|
+
report({
|
|
600
|
+
type: "execution",
|
|
601
|
+
session,
|
|
602
|
+
index,
|
|
603
|
+
execution,
|
|
604
|
+
status: "confirming",
|
|
605
|
+
hash,
|
|
606
|
+
message: "Waiting for a connection to this network.",
|
|
607
|
+
});
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const receipt = await (0, relayr_js_1.verifyRelayrDestination)(client, {
|
|
612
|
+
entry: binding.entry,
|
|
613
|
+
hash,
|
|
614
|
+
});
|
|
615
|
+
const verified = {
|
|
616
|
+
txUuid: binding.txUuid,
|
|
617
|
+
chainId: binding.chain,
|
|
618
|
+
receipt,
|
|
619
|
+
};
|
|
620
|
+
requireSafeLanding(execution, verified);
|
|
621
|
+
await options.afterVerified?.(execution, verified);
|
|
622
|
+
report({
|
|
623
|
+
type: "execution",
|
|
624
|
+
session,
|
|
625
|
+
index,
|
|
626
|
+
execution,
|
|
627
|
+
status: "executed",
|
|
628
|
+
hash,
|
|
629
|
+
});
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
if (error instanceof relayr_js_1.RelayrProofUnavailableError) {
|
|
634
|
+
report({
|
|
635
|
+
type: "execution",
|
|
636
|
+
session,
|
|
637
|
+
index,
|
|
638
|
+
execution,
|
|
639
|
+
status: "confirming",
|
|
640
|
+
hash,
|
|
641
|
+
message: "Waiting for the network to confirm this transaction.",
|
|
642
|
+
});
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
report({
|
|
646
|
+
type: "execution",
|
|
647
|
+
session,
|
|
648
|
+
index,
|
|
649
|
+
execution,
|
|
650
|
+
status: "failed",
|
|
651
|
+
hash,
|
|
652
|
+
message: error instanceof Error
|
|
653
|
+
? error.message
|
|
654
|
+
: "The Safe execution could not be verified.",
|
|
655
|
+
});
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
}));
|
|
659
|
+
const failure = landed.find((item) => item.status === "rejected");
|
|
660
|
+
if (failure?.status === "rejected")
|
|
661
|
+
throw new SafeRelayrRecoveryError(session, failure.reason instanceof Error ? failure.reason.message : undefined);
|
|
662
|
+
if (known.length === session.executions.length &&
|
|
663
|
+
landed.every((item) => item.status === "fulfilled" && item.value)) {
|
|
664
|
+
session = { ...session, state: "complete" };
|
|
665
|
+
await store.save(session);
|
|
666
|
+
phase("complete", session, signal);
|
|
667
|
+
return result("complete", session);
|
|
668
|
+
}
|
|
669
|
+
if (session.paymentStatus === "sending")
|
|
464
670
|
return result("pending", session);
|
|
465
|
-
if (
|
|
671
|
+
if (session.payments.some((payment) => payment.bundleUuid !== session.bundleUuid))
|
|
672
|
+
throw new SafeRelayrRecoveryError(session, "A saved payment belongs to another bundle. Keep the existing bundle for verification.");
|
|
673
|
+
if (session.payments.length) {
|
|
674
|
+
let everyReverted = true;
|
|
675
|
+
for (const payment of session.payments) {
|
|
676
|
+
const client = options.clientFor(payment.chainId);
|
|
677
|
+
if (!client)
|
|
678
|
+
return result("pending", session);
|
|
679
|
+
try {
|
|
680
|
+
await (0, relayr_js_1.verifyRelayrPayment)(client, {
|
|
681
|
+
hash: payment.hash,
|
|
682
|
+
from: session.account,
|
|
683
|
+
payment,
|
|
684
|
+
});
|
|
685
|
+
session = { ...session, paymentStatus: "confirmed" };
|
|
686
|
+
await store.save(session);
|
|
687
|
+
phase("executing", session, signal);
|
|
688
|
+
return result("pending", session);
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
if (!(error instanceof relayr_js_1.RelayrPaymentRevertedError))
|
|
692
|
+
everyReverted = false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (!everyReverted)
|
|
696
|
+
return result("pending", session);
|
|
697
|
+
session = { ...session, paymentStatus: "reverted" };
|
|
698
|
+
await store.save(session);
|
|
699
|
+
const disposition = await (0, relayr_js_1.revertedRelayrQuote)(options.clientFor, {
|
|
700
|
+
bundleUuid: quote.bundle_uuid,
|
|
701
|
+
payments: session.payments,
|
|
702
|
+
options: quote.payment_info,
|
|
703
|
+
destinationChainIds: chains,
|
|
704
|
+
account: session.account,
|
|
705
|
+
}, { fetch: fetchRelayr });
|
|
706
|
+
if (disposition.state === "funded")
|
|
707
|
+
return result("pending", session);
|
|
708
|
+
if (disposition.state === "released") {
|
|
709
|
+
session = {
|
|
710
|
+
...session,
|
|
711
|
+
state: "released",
|
|
712
|
+
paymentStatus: "expired",
|
|
713
|
+
releaseReason: "quote-expired",
|
|
714
|
+
};
|
|
715
|
+
await store.save(session);
|
|
716
|
+
return result("released", session);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else if (session.paymentStatus !== "unfunded" ||
|
|
720
|
+
session.fundingObserved) {
|
|
721
|
+
// A wallet invocation without a known hash may still have broadcast.
|
|
722
|
+
return session.fundingObserved && !known.length
|
|
723
|
+
? {
|
|
724
|
+
...result("pending", session),
|
|
725
|
+
recovery: {
|
|
726
|
+
reason: "funding-unresolved",
|
|
727
|
+
message: "This saved selection has reported funding or execution activity. Check the existing bundle and its funding transactions before submitting another payment.",
|
|
728
|
+
},
|
|
729
|
+
}
|
|
730
|
+
: result("pending", session);
|
|
731
|
+
}
|
|
732
|
+
const quoted = (0, relayr_js_1.relayrQuotedOptions)(quote, chains);
|
|
733
|
+
if (!quoted.length)
|
|
734
|
+
return inspectMissingQuote(session);
|
|
735
|
+
const payments = (0, relayr_js_1.relayrPaymentOptions)(quote, chains);
|
|
736
|
+
const expired = payments.length
|
|
737
|
+
? [false]
|
|
738
|
+
: await Promise.all(quoted.map(async ({ details }) => {
|
|
739
|
+
const client = options.clientFor(details.chainId);
|
|
740
|
+
return (!!client &&
|
|
741
|
+
(await (0, relayr_js_1.relayrDeadlinePassed)(client, details.deadline).catch(() => false)));
|
|
742
|
+
}));
|
|
743
|
+
if (expired.every(Boolean)) {
|
|
744
|
+
try {
|
|
745
|
+
await (0, relayr_js_1.requireRelayrBundleUnpaid)(quote.bundle_uuid, {
|
|
746
|
+
fetch: fetchRelayr,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return result("pending", session);
|
|
751
|
+
}
|
|
466
752
|
session = {
|
|
467
753
|
...session,
|
|
468
754
|
state: "released",
|
|
@@ -472,23 +758,6 @@ function createSafeRelayrController(options) {
|
|
|
472
758
|
await store.save(session);
|
|
473
759
|
return result("released", session);
|
|
474
760
|
}
|
|
475
|
-
}
|
|
476
|
-
else if (session.paymentStatus !== "unfunded") {
|
|
477
|
-
// A wallet invocation without a known hash may still have broadcast.
|
|
478
|
-
return result("pending", session);
|
|
479
|
-
}
|
|
480
|
-
const quoted = (0, relayr_js_1.relayrQuotedOptions)(quote, chains);
|
|
481
|
-
if (!quoted.length)
|
|
482
|
-
return inspectMissingQuote(session);
|
|
483
|
-
const payments = (0, relayr_js_1.relayrPaymentOptions)(quote, chains);
|
|
484
|
-
const expired = payments.length
|
|
485
|
-
? [false]
|
|
486
|
-
: await Promise.all(quoted.map(async ({ details }) => {
|
|
487
|
-
const client = options.clientFor(details.chainId);
|
|
488
|
-
return (!!client &&
|
|
489
|
-
(await (0, relayr_js_1.relayrDeadlinePassed)(client, details.deadline).catch(() => false)));
|
|
490
|
-
}));
|
|
491
|
-
if (expired.every(Boolean)) {
|
|
492
761
|
try {
|
|
493
762
|
await (0, relayr_js_1.requireRelayrBundleUnpaid)(quote.bundle_uuid, {
|
|
494
763
|
fetch: fetchRelayr,
|
|
@@ -497,43 +766,35 @@ function createSafeRelayrController(options) {
|
|
|
497
766
|
catch {
|
|
498
767
|
return result("pending", session);
|
|
499
768
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
await store.save(session);
|
|
507
|
-
return result("released", session);
|
|
508
|
-
}
|
|
509
|
-
try {
|
|
510
|
-
await (0, relayr_js_1.requireRelayrBundleUnpaid)(quote.bundle_uuid, {
|
|
511
|
-
fetch: fetchRelayr,
|
|
512
|
-
});
|
|
769
|
+
const allowed = session.payments.length && payments.length
|
|
770
|
+
? [(0, relayr_js_1.relayrRetryOption)(session.payments, payments)]
|
|
771
|
+
: payments;
|
|
772
|
+
return allowed.length
|
|
773
|
+
? result("ready", session, allowed)
|
|
774
|
+
: result("pending", session);
|
|
513
775
|
}
|
|
514
|
-
catch {
|
|
515
|
-
|
|
776
|
+
catch (error) {
|
|
777
|
+
if (error instanceof SafeRelayrRecoveryError)
|
|
778
|
+
throw error;
|
|
779
|
+
throw new SafeRelayrRecoveryError(session, error instanceof Error ? error.message : undefined);
|
|
516
780
|
}
|
|
517
|
-
const allowed = session.payments.length && payments.length
|
|
518
|
-
? [(0, relayr_js_1.relayrRetryOption)(session.payments, payments)]
|
|
519
|
-
: payments;
|
|
520
|
-
return allowed.length
|
|
521
|
-
? result("ready", session, allowed)
|
|
522
|
-
: result("pending", session);
|
|
523
781
|
}
|
|
524
782
|
async function prepare({ account, executions: input, signal, onStatus, }) {
|
|
525
783
|
const executions = structuredClone([...input]);
|
|
526
784
|
validateExecutions(executions);
|
|
527
785
|
return store.withLock(account, async () => {
|
|
528
786
|
checkAccount(account, signal);
|
|
529
|
-
const reservations = (await store.list(account)).filter((session) =>
|
|
530
|
-
(unidentified(session) || overlap(session, executions)));
|
|
787
|
+
const reservations = (await store.list(account)).filter((session) => reserves(session, executions));
|
|
531
788
|
for (const saved of reservations) {
|
|
789
|
+
if (canReplaceSafeRelayrQuote(saved))
|
|
790
|
+
continue;
|
|
532
791
|
let checked;
|
|
533
792
|
try {
|
|
534
|
-
checked = await inspect(saved);
|
|
793
|
+
checked = await inspect(saved, signal);
|
|
535
794
|
}
|
|
536
795
|
catch (error) {
|
|
796
|
+
if (error instanceof SafeRelayrRecoveryError)
|
|
797
|
+
throw error;
|
|
537
798
|
throw new SafeRelayrRecoveryError(saved, error instanceof Error ? error.message : undefined);
|
|
538
799
|
}
|
|
539
800
|
if (checked.state === "released" &&
|
|
@@ -547,14 +808,29 @@ function createSafeRelayrController(options) {
|
|
|
547
808
|
throw new SafeRelayrRecoveryError(checked.session, checked.recovery?.message, checked.recovery);
|
|
548
809
|
await assertUnreserved(account, executions, saved.id);
|
|
549
810
|
await revalidate(saved.executions, account, onStatus, false, signal);
|
|
811
|
+
phase("reviewing", checked.session, signal);
|
|
550
812
|
await options.review(saved.executions, { resumed: true });
|
|
551
813
|
checkAccount(account, signal);
|
|
552
814
|
rememberReview(checked.session);
|
|
553
815
|
return { ...checked, state: "ready", resumed: true };
|
|
554
816
|
}
|
|
555
817
|
await revalidate(executions, account, onStatus, false, signal);
|
|
818
|
+
phase("reviewing", undefined, signal);
|
|
556
819
|
await options.review(executions, { resumed: false });
|
|
557
820
|
checkAccount(account, signal);
|
|
821
|
+
// Re-read under the shared lock after review. A rejected review leaves the
|
|
822
|
+
// old quote untouched; a recorded wallet invocation can never be retired.
|
|
823
|
+
for (const saved of await store.list(account)) {
|
|
824
|
+
if (!reserves(saved, executions))
|
|
825
|
+
continue;
|
|
826
|
+
if (!canReplaceSafeRelayrQuote(saved))
|
|
827
|
+
throw new SafeRelayrRecoveryError(saved);
|
|
828
|
+
await store.save({
|
|
829
|
+
...saved,
|
|
830
|
+
state: "released",
|
|
831
|
+
releaseReason: "quote-replaced",
|
|
832
|
+
});
|
|
833
|
+
}
|
|
558
834
|
await assertUnreserved(account, executions);
|
|
559
835
|
let session = {
|
|
560
836
|
id: options.createId?.() ?? globalThis.crypto.randomUUID(),
|
|
@@ -565,8 +841,8 @@ function createSafeRelayrController(options) {
|
|
|
565
841
|
payments: [],
|
|
566
842
|
createdAt: Date.now(),
|
|
567
843
|
};
|
|
568
|
-
// Persist before POST
|
|
569
|
-
//
|
|
844
|
+
// Persist before POST so the quote can be recovered or retired later.
|
|
845
|
+
// Safe publication alone is not evidence that the wallet funded it.
|
|
570
846
|
await store.save(session);
|
|
571
847
|
try {
|
|
572
848
|
checkAccount(account, signal);
|
|
@@ -576,6 +852,7 @@ function createSafeRelayrController(options) {
|
|
|
576
852
|
throw error;
|
|
577
853
|
}
|
|
578
854
|
const request = (0, relayr_js_1.relayrBundleRequest)(executions.map(({ entry }) => entry));
|
|
855
|
+
phase("quoting", session, signal);
|
|
579
856
|
const response = await fetchRelayr(`${relayr_js_1.RELAYR_API}/v1/bundle/prepaid`, {
|
|
580
857
|
method: "POST",
|
|
581
858
|
headers: { "content-type": "application/json" },
|
|
@@ -604,14 +881,18 @@ function createSafeRelayrController(options) {
|
|
|
604
881
|
return { state: "ready", session, payments, resumed: false };
|
|
605
882
|
});
|
|
606
883
|
}
|
|
607
|
-
async function check({ account, sessionId, }) {
|
|
608
|
-
return store.withLock(account, async () =>
|
|
884
|
+
async function check({ account, sessionId, signal, }) {
|
|
885
|
+
return store.withLock(account, async () => {
|
|
886
|
+
signal?.throwIfAborted();
|
|
887
|
+
const session = await load(account, sessionId);
|
|
888
|
+
return inspect(session, signal);
|
|
889
|
+
});
|
|
609
890
|
}
|
|
610
891
|
async function fund({ account, sessionId, paymentChainId, signal, onStatus, }) {
|
|
611
892
|
return store.withLock(account, async () => {
|
|
612
893
|
checkAccount(account, signal);
|
|
613
894
|
let session = await load(account, sessionId);
|
|
614
|
-
const checked = await inspect(session);
|
|
895
|
+
const checked = await inspect(session, signal);
|
|
615
896
|
session = checked.session;
|
|
616
897
|
if (checked.state !== "ready")
|
|
617
898
|
throw new SafeRelayrRecoveryError(session, checked.recovery?.message, checked.recovery);
|
|
@@ -622,6 +903,7 @@ function createSafeRelayrController(options) {
|
|
|
622
903
|
throw new Error("Choose a current payment option from this saved quote.");
|
|
623
904
|
await assertUnreserved(account, session.executions, session.id);
|
|
624
905
|
if (!reviewedIntents.has(reviewKey(session))) {
|
|
906
|
+
phase("reviewing", session, signal);
|
|
625
907
|
await options.review(session.executions, { resumed: true });
|
|
626
908
|
checkAccount(account, signal);
|
|
627
909
|
rememberReview(session);
|
|
@@ -669,6 +951,7 @@ function createSafeRelayrController(options) {
|
|
|
669
951
|
throw error;
|
|
670
952
|
}
|
|
671
953
|
sending = true;
|
|
954
|
+
phase("payment-submitting", session, signal);
|
|
672
955
|
};
|
|
673
956
|
const onSent = async (payments) => {
|
|
674
957
|
const snapshot = (0, relayr_js_1.relayrSentPaymentsSnapshot)(payments);
|
|
@@ -706,8 +989,10 @@ function createSafeRelayrController(options) {
|
|
|
706
989
|
paymentStatus: "submitted",
|
|
707
990
|
};
|
|
708
991
|
await store.save(session);
|
|
992
|
+
phase("payment-confirming", session, signal);
|
|
709
993
|
};
|
|
710
994
|
try {
|
|
995
|
+
phase("payment-review", session, signal);
|
|
711
996
|
const sent = await options.sendPayment({
|
|
712
997
|
session: structuredClone(session),
|
|
713
998
|
payment,
|
|
@@ -728,10 +1013,14 @@ function createSafeRelayrController(options) {
|
|
|
728
1013
|
paymentStatus: checked.session.paymentStatus,
|
|
729
1014
|
};
|
|
730
1015
|
await store.save(session);
|
|
1016
|
+
throw error;
|
|
731
1017
|
}
|
|
1018
|
+
if (sending ||
|
|
1019
|
+
session.payments.length > checked.session.payments.length)
|
|
1020
|
+
throw new SafeRelayrRecoveryError(session, error instanceof Error ? error.message : undefined);
|
|
732
1021
|
throw error;
|
|
733
1022
|
}
|
|
734
|
-
return inspect(session);
|
|
1023
|
+
return inspect(session, signal);
|
|
735
1024
|
});
|
|
736
1025
|
}
|
|
737
1026
|
/** Read-only progress polling; each read releases its lock before the wait. */
|
|
@@ -744,7 +1033,7 @@ function createSafeRelayrController(options) {
|
|
|
744
1033
|
const started = Date.now();
|
|
745
1034
|
while (true) {
|
|
746
1035
|
signal?.throwIfAborted();
|
|
747
|
-
const next = await check({ account, sessionId });
|
|
1036
|
+
const next = await check({ account, sessionId, signal });
|
|
748
1037
|
signal?.throwIfAborted();
|
|
749
1038
|
await onUpdate?.(next);
|
|
750
1039
|
const remaining = timeoutMs - (Date.now() - started);
|