@bananapus/nana-sdk-core 2.24.2 → 2.24.4
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/activityGroups.d.ts +24 -0
- package/dist/cjs/activityGroups.d.ts.map +1 -0
- package/dist/cjs/activityGroups.js +36 -0
- package/dist/cjs/activityGroups.js.map +1 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/review/relayr.d.ts +19 -7
- package/dist/cjs/review/relayr.d.ts.map +1 -1
- package/dist/cjs/review/relayr.js +119 -17
- package/dist/cjs/review/relayr.js.map +1 -1
- package/dist/cjs/review/safeRelayr.d.ts +20 -2
- package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
- package/dist/cjs/review/safeRelayr.js +325 -132
- package/dist/cjs/review/safeRelayr.js.map +1 -1
- package/dist/esm/activityGroups.d.ts +24 -0
- package/dist/esm/activityGroups.d.ts.map +1 -0
- package/dist/esm/activityGroups.js +33 -0
- package/dist/esm/activityGroups.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/review/relayr.d.ts +19 -7
- package/dist/esm/review/relayr.d.ts.map +1 -1
- package/dist/esm/review/relayr.js +116 -16
- package/dist/esm/review/relayr.js.map +1 -1
- package/dist/esm/review/safeRelayr.d.ts +20 -2
- package/dist/esm/review/safeRelayr.d.ts.map +1 -1
- package/dist/esm/review/safeRelayr.js +326 -133
- package/dist/esm/review/safeRelayr.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { decodeFunctionData, encodeFunctionData, isAddress, keccak256, toHex, pa
|
|
|
2
2
|
import { readBoundedSafeNonce } from "../safe.js";
|
|
3
3
|
import { SAFE_EXEC_ABI, canonicalSafeTxHash, requireSafeExecutionSuccess, safeExecutionResult, } from "../safeService.js";
|
|
4
4
|
import { isDefiniteWalletRejection } from "./contractWrite.js";
|
|
5
|
-
import { RELAYR_API, RELAYR_UUID_RE, MAX_RELAYR_SENT_PAYMENTS, atCanonicalFinalizedBlock, bindRelayrQuote, readRelayrBundle, relayrBundleRequest, relayrDeadlinePassed, relayrPaymentOptions, relayrPaymentDetails, relayrRetryOption, relayrSentPaymentsSnapshot, relayrStateIsPending, relayrQuotedOptions, requireRelayrBundleUnpaid, requireRelayrRetry, revertedRelayrQuote, verifyRelayrDestinations, verifyRelayrPayment, RelayrPaymentRevertedError, } from "./relayr.js";
|
|
5
|
+
import { RELAYR_API, RELAYR_UUID_RE, MAX_RELAYR_SENT_PAYMENTS, atCanonicalFinalizedBlock, bindRelayrQuote, readRelayrBundle, relayrBundleRequest, relayrDeadlinePassed, relayrPaymentOptions, relayrPaymentDetails, relayrDestinationHash, relayrDestinationHashes, relayrRetryOption, relayrSentPaymentsSnapshot, relayrStateIsPending, relayrStateIsFailed, relayrQuotedOptions, requireRelayrBundleUnpaid, requireRelayrRetry, revertedRelayrQuote, verifyRelayrDestinations, verifyRelayrDestination, verifyRelayrPayment, RelayrPaymentRevertedError, RelayrProofUnavailableError, } from "./relayr.js";
|
|
6
6
|
function hasFundingRecords(records) {
|
|
7
7
|
if (records === undefined)
|
|
8
8
|
return false;
|
|
@@ -221,6 +221,13 @@ function requireSafeRelayrRecords(bindings, records) {
|
|
|
221
221
|
throw new Error("Relayr's record does not match the saved Safe transaction binding.");
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
+
function requireSafeLanding(execution, { receipt }) {
|
|
225
|
+
const { safe, safeTxHash } = execution;
|
|
226
|
+
requireSafeExecutionSuccess(receipt, safe, safeTxHash);
|
|
227
|
+
const event = safeExecutionResult(receipt, safe, safeTxHash);
|
|
228
|
+
if (event.status !== "success" || event.payment !== 0n)
|
|
229
|
+
throw new Error("The Safe execution reimbursed its executor. Keep the existing bundle for verification.");
|
|
230
|
+
}
|
|
224
231
|
/** Canonical exact destination receipts plus matching, refund-free Safe success events. */
|
|
225
232
|
export async function verifySafeRelayrLanding(clientFor, { executions, bindings, records, }) {
|
|
226
233
|
validateExecutions(executions);
|
|
@@ -233,13 +240,7 @@ export async function verifySafeRelayrLanding(clientFor, { executions, bindings,
|
|
|
233
240
|
bindings,
|
|
234
241
|
records,
|
|
235
242
|
});
|
|
236
|
-
verified.forEach((
|
|
237
|
-
const { safe, safeTxHash } = executions[index];
|
|
238
|
-
requireSafeExecutionSuccess(receipt, safe, safeTxHash);
|
|
239
|
-
const event = safeExecutionResult(receipt, safe, safeTxHash);
|
|
240
|
-
if (event.status !== "success" || event.payment !== 0n)
|
|
241
|
-
throw new Error("The Safe execution reimbursed its executor. Keep the existing bundle for verification.");
|
|
242
|
-
});
|
|
243
|
+
verified.forEach((item, index) => requireSafeLanding(executions[index], item));
|
|
243
244
|
return verified;
|
|
244
245
|
}
|
|
245
246
|
/** Validate persisted bindings again; local storage is not authenticated evidence. */
|
|
@@ -265,6 +266,49 @@ function requireSessionQuote(session) {
|
|
|
265
266
|
export function createSafeRelayrController(options) {
|
|
266
267
|
const { store } = options;
|
|
267
268
|
const fetchRelayr = options.fetch ?? globalThis.fetch;
|
|
269
|
+
const displayedExecutions = new Map();
|
|
270
|
+
const notify = (progress, rechecking = false) => {
|
|
271
|
+
if (progress.type === "execution") {
|
|
272
|
+
const key = `${progress.session.id}:${progress.index}`;
|
|
273
|
+
const display = JSON.stringify([
|
|
274
|
+
progress.status,
|
|
275
|
+
progress.hash,
|
|
276
|
+
progress.message,
|
|
277
|
+
]);
|
|
278
|
+
const previous = displayedExecutions.get(key);
|
|
279
|
+
// Rechecking does not undo a proven display result. A later unavailable,
|
|
280
|
+
// contradictory, or changed-hash result explicitly updates it below.
|
|
281
|
+
if (previous === display ||
|
|
282
|
+
(rechecking &&
|
|
283
|
+
previous === JSON.stringify(["executed", progress.hash, undefined])))
|
|
284
|
+
return;
|
|
285
|
+
displayedExecutions.set(key, display);
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
// A view must not mutate frozen calls or turn a submitted payment into a
|
|
289
|
+
// rejected operation. Also consume rejection from an async UI callback.
|
|
290
|
+
void Promise.resolve(options.onProgress?.(structuredClone(progress))).catch(() => undefined);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Display observers cannot change the durable execution lifecycle.
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const phase = (phase, session, signal) => {
|
|
297
|
+
if (!signal?.aborted)
|
|
298
|
+
notify({ type: "phase", phase, session });
|
|
299
|
+
};
|
|
300
|
+
const savedPhase = (session, signal) => {
|
|
301
|
+
if (session.state === "released")
|
|
302
|
+
return;
|
|
303
|
+
if (session.state === "complete")
|
|
304
|
+
phase("complete", session, signal);
|
|
305
|
+
else if (session.paymentStatus === "confirmed" ||
|
|
306
|
+
hasObservedFunding(session))
|
|
307
|
+
phase("executing", session, signal);
|
|
308
|
+
else if (session.paymentStatus === "submitted" ||
|
|
309
|
+
session.paymentStatus === "sending")
|
|
310
|
+
phase("payment-confirming", session, signal);
|
|
311
|
+
};
|
|
268
312
|
const checkAccount = (account, signal) => {
|
|
269
313
|
signal?.throwIfAborted();
|
|
270
314
|
if (!isAddress(account) ||
|
|
@@ -444,87 +488,256 @@ export function createSafeRelayrController(options) {
|
|
|
444
488
|
checks,
|
|
445
489
|
});
|
|
446
490
|
}
|
|
447
|
-
async function inspect(session) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
session
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
if (!Array.isArray(bundle.transactions))
|
|
470
|
-
throw new SafeRelayrRecoveryError(session, "Relayr has not returned this bundle's transactions. Check the existing bundle again.");
|
|
471
|
-
const records = bundle.transactions;
|
|
472
|
-
requireSafeRelayrRecords(quote.expectedTransactions, records);
|
|
473
|
-
session = { ...session, records };
|
|
474
|
-
await store.save(session);
|
|
475
|
-
// A reported hash is only a pointer. Completion requires canonical exact
|
|
476
|
-
// calldata/value receipts AND the matching Safe ExecutionSuccess event.
|
|
477
|
-
if (records.length &&
|
|
478
|
-
records.every((record) => record.status?.data?.hash || record.status?.data?.transaction?.hash)) {
|
|
479
|
-
const verified = await verifySafeRelayrLanding(options.clientFor, {
|
|
480
|
-
executions: session.executions,
|
|
481
|
-
bindings: quote.expectedTransactions,
|
|
482
|
-
records,
|
|
491
|
+
async function inspect(session, signal) {
|
|
492
|
+
const report = (progress, rechecking = false) => {
|
|
493
|
+
if (!signal?.aborted)
|
|
494
|
+
notify(progress, rechecking);
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
savedPhase(session, signal);
|
|
498
|
+
if (session.state === "released")
|
|
499
|
+
return result("released", session);
|
|
500
|
+
if (session.state === "complete")
|
|
501
|
+
return result("complete", session);
|
|
502
|
+
if (hasObservedFunding(session) && session.fundingObserved !== true) {
|
|
503
|
+
session = { ...session, fundingObserved: true };
|
|
504
|
+
await store.save(session);
|
|
505
|
+
}
|
|
506
|
+
if (!session.bundleUuid || !session.quote)
|
|
507
|
+
return inspectMissingQuote(session);
|
|
508
|
+
const quote = requireSessionQuote(session);
|
|
509
|
+
const chains = session.executions.map(({ entry }) => entry.chain);
|
|
510
|
+
const bundle = await readRelayrBundle(session.bundleUuid, {
|
|
511
|
+
fetch: fetchRelayr,
|
|
483
512
|
});
|
|
484
|
-
|
|
485
|
-
|
|
513
|
+
if (hasObservedFunding(session) ||
|
|
514
|
+
bundle.payment_received === true ||
|
|
515
|
+
hasFundingRecords(bundle.transactions)) {
|
|
516
|
+
session = { ...session, fundingObserved: true };
|
|
517
|
+
await store.save(session);
|
|
518
|
+
}
|
|
519
|
+
if (!Array.isArray(bundle.transactions))
|
|
520
|
+
throw new SafeRelayrRecoveryError(session, "Relayr has not returned this bundle's transactions. Check the existing bundle again.");
|
|
521
|
+
const records = bundle.transactions;
|
|
522
|
+
requireSafeRelayrRecords(quote.expectedTransactions, records);
|
|
523
|
+
session = { ...session, records };
|
|
486
524
|
await store.save(session);
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return result("pending", session);
|
|
525
|
+
savedPhase(session, signal);
|
|
526
|
+
// Each hash is only a pointer. Check available chains independently, using
|
|
527
|
+
// the same complete binding validator and exact receipt proof as completion.
|
|
528
|
+
const indexed = quote.expectedTransactions.map((binding, index) => ({
|
|
529
|
+
binding,
|
|
530
|
+
index,
|
|
531
|
+
record: records.find((record) => record.tx_uuid?.toLowerCase() === binding.txUuid.toLowerCase()),
|
|
532
|
+
}));
|
|
533
|
+
const known = indexed.filter(({ record }) => record.status?.data?.hash !== undefined ||
|
|
534
|
+
record.status?.data?.transaction?.hash !== undefined);
|
|
535
|
+
if (known.length) {
|
|
499
536
|
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
payment,
|
|
537
|
+
relayrDestinationHashes({
|
|
538
|
+
bindings: known.map(({ binding }) => binding),
|
|
539
|
+
records: known.map(({ record }) => record),
|
|
504
540
|
});
|
|
505
|
-
session = { ...session, paymentStatus: "confirmed" };
|
|
506
|
-
await store.save(session);
|
|
507
|
-
return result("pending", session);
|
|
508
541
|
}
|
|
509
542
|
catch (error) {
|
|
510
|
-
|
|
511
|
-
|
|
543
|
+
for (const { index, record } of known)
|
|
544
|
+
report({
|
|
545
|
+
type: "execution",
|
|
546
|
+
session,
|
|
547
|
+
index,
|
|
548
|
+
execution: session.executions[index],
|
|
549
|
+
status: "failed",
|
|
550
|
+
hash: relayrDestinationHash(record) ?? undefined,
|
|
551
|
+
message: error instanceof Error
|
|
552
|
+
? error.message
|
|
553
|
+
: "The destination records do not match this quote.",
|
|
554
|
+
});
|
|
555
|
+
throw new SafeRelayrRecoveryError(session, error instanceof Error ? error.message : undefined);
|
|
512
556
|
}
|
|
557
|
+
phase("executing", session, signal);
|
|
513
558
|
}
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
559
|
+
if (known.length ||
|
|
560
|
+
session.paymentStatus !== "unfunded" ||
|
|
561
|
+
session.fundingObserved) {
|
|
562
|
+
for (const { index, record } of indexed) {
|
|
563
|
+
const hash = relayrDestinationHash(record) ?? undefined;
|
|
564
|
+
report({
|
|
565
|
+
type: "execution",
|
|
566
|
+
session,
|
|
567
|
+
index,
|
|
568
|
+
execution: session.executions[index],
|
|
569
|
+
status: hash
|
|
570
|
+
? "confirming"
|
|
571
|
+
: relayrStateIsFailed(record.status?.state)
|
|
572
|
+
? "failed"
|
|
573
|
+
: "pending",
|
|
574
|
+
hash,
|
|
575
|
+
...(!hash && relayrStateIsFailed(record.status?.state)
|
|
576
|
+
? {
|
|
577
|
+
message: "Relayr reported this execution failed. Its onchain result has not been verified.",
|
|
578
|
+
}
|
|
579
|
+
: {}),
|
|
580
|
+
}, true);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const landed = await Promise.allSettled(known.map(async ({ binding, index, record }) => {
|
|
584
|
+
const execution = session.executions[index];
|
|
585
|
+
const hash = relayrDestinationHash(record);
|
|
586
|
+
const client = options.clientFor(binding.chain);
|
|
587
|
+
if (!client) {
|
|
588
|
+
report({
|
|
589
|
+
type: "execution",
|
|
590
|
+
session,
|
|
591
|
+
index,
|
|
592
|
+
execution,
|
|
593
|
+
status: "confirming",
|
|
594
|
+
hash,
|
|
595
|
+
message: "Waiting for a connection to this network.",
|
|
596
|
+
});
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const receipt = await verifyRelayrDestination(client, {
|
|
601
|
+
entry: binding.entry,
|
|
602
|
+
hash,
|
|
603
|
+
});
|
|
604
|
+
const verified = {
|
|
605
|
+
txUuid: binding.txUuid,
|
|
606
|
+
chainId: binding.chain,
|
|
607
|
+
receipt,
|
|
608
|
+
};
|
|
609
|
+
requireSafeLanding(execution, verified);
|
|
610
|
+
await options.afterVerified?.(execution, verified);
|
|
611
|
+
report({
|
|
612
|
+
type: "execution",
|
|
613
|
+
session,
|
|
614
|
+
index,
|
|
615
|
+
execution,
|
|
616
|
+
status: "executed",
|
|
617
|
+
hash,
|
|
618
|
+
});
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
if (error instanceof RelayrProofUnavailableError) {
|
|
623
|
+
report({
|
|
624
|
+
type: "execution",
|
|
625
|
+
session,
|
|
626
|
+
index,
|
|
627
|
+
execution,
|
|
628
|
+
status: "confirming",
|
|
629
|
+
hash,
|
|
630
|
+
message: "Waiting for the network to confirm this transaction.",
|
|
631
|
+
});
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
report({
|
|
635
|
+
type: "execution",
|
|
636
|
+
session,
|
|
637
|
+
index,
|
|
638
|
+
execution,
|
|
639
|
+
status: "failed",
|
|
640
|
+
hash,
|
|
641
|
+
message: error instanceof Error
|
|
642
|
+
? error.message
|
|
643
|
+
: "The Safe execution could not be verified.",
|
|
644
|
+
});
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
}));
|
|
648
|
+
const failure = landed.find((item) => item.status === "rejected");
|
|
649
|
+
if (failure?.status === "rejected")
|
|
650
|
+
throw new SafeRelayrRecoveryError(session, failure.reason instanceof Error ? failure.reason.message : undefined);
|
|
651
|
+
if (known.length === session.executions.length &&
|
|
652
|
+
landed.every((item) => item.status === "fulfilled" && item.value)) {
|
|
653
|
+
session = { ...session, state: "complete" };
|
|
654
|
+
await store.save(session);
|
|
655
|
+
phase("complete", session, signal);
|
|
656
|
+
return result("complete", session);
|
|
657
|
+
}
|
|
658
|
+
if (session.paymentStatus === "sending")
|
|
526
659
|
return result("pending", session);
|
|
527
|
-
if (
|
|
660
|
+
if (session.payments.some((payment) => payment.bundleUuid !== session.bundleUuid))
|
|
661
|
+
throw new SafeRelayrRecoveryError(session, "A saved payment belongs to another bundle. Keep the existing bundle for verification.");
|
|
662
|
+
if (session.payments.length) {
|
|
663
|
+
let everyReverted = true;
|
|
664
|
+
for (const payment of session.payments) {
|
|
665
|
+
const client = options.clientFor(payment.chainId);
|
|
666
|
+
if (!client)
|
|
667
|
+
return result("pending", session);
|
|
668
|
+
try {
|
|
669
|
+
await verifyRelayrPayment(client, {
|
|
670
|
+
hash: payment.hash,
|
|
671
|
+
from: session.account,
|
|
672
|
+
payment,
|
|
673
|
+
});
|
|
674
|
+
session = { ...session, paymentStatus: "confirmed" };
|
|
675
|
+
await store.save(session);
|
|
676
|
+
phase("executing", session, signal);
|
|
677
|
+
return result("pending", session);
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
if (!(error instanceof RelayrPaymentRevertedError))
|
|
681
|
+
everyReverted = false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (!everyReverted)
|
|
685
|
+
return result("pending", session);
|
|
686
|
+
session = { ...session, paymentStatus: "reverted" };
|
|
687
|
+
await store.save(session);
|
|
688
|
+
const disposition = await revertedRelayrQuote(options.clientFor, {
|
|
689
|
+
bundleUuid: quote.bundle_uuid,
|
|
690
|
+
payments: session.payments,
|
|
691
|
+
options: quote.payment_info,
|
|
692
|
+
destinationChainIds: chains,
|
|
693
|
+
account: session.account,
|
|
694
|
+
}, { fetch: fetchRelayr });
|
|
695
|
+
if (disposition.state === "funded")
|
|
696
|
+
return result("pending", session);
|
|
697
|
+
if (disposition.state === "released") {
|
|
698
|
+
session = {
|
|
699
|
+
...session,
|
|
700
|
+
state: "released",
|
|
701
|
+
paymentStatus: "expired",
|
|
702
|
+
releaseReason: "quote-expired",
|
|
703
|
+
};
|
|
704
|
+
await store.save(session);
|
|
705
|
+
return result("released", session);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else if (session.paymentStatus !== "unfunded" ||
|
|
709
|
+
session.fundingObserved) {
|
|
710
|
+
// A wallet invocation without a known hash may still have broadcast.
|
|
711
|
+
return session.fundingObserved && !known.length
|
|
712
|
+
? {
|
|
713
|
+
...result("pending", session),
|
|
714
|
+
recovery: {
|
|
715
|
+
reason: "funding-unresolved",
|
|
716
|
+
message: "This saved selection has reported funding or execution activity. Check the existing bundle and its funding transactions before submitting another payment.",
|
|
717
|
+
},
|
|
718
|
+
}
|
|
719
|
+
: result("pending", session);
|
|
720
|
+
}
|
|
721
|
+
const quoted = relayrQuotedOptions(quote, chains);
|
|
722
|
+
if (!quoted.length)
|
|
723
|
+
return inspectMissingQuote(session);
|
|
724
|
+
const payments = relayrPaymentOptions(quote, chains);
|
|
725
|
+
const expired = payments.length
|
|
726
|
+
? [false]
|
|
727
|
+
: await Promise.all(quoted.map(async ({ details }) => {
|
|
728
|
+
const client = options.clientFor(details.chainId);
|
|
729
|
+
return (!!client &&
|
|
730
|
+
(await relayrDeadlinePassed(client, details.deadline).catch(() => false)));
|
|
731
|
+
}));
|
|
732
|
+
if (expired.every(Boolean)) {
|
|
733
|
+
try {
|
|
734
|
+
await requireRelayrBundleUnpaid(quote.bundle_uuid, {
|
|
735
|
+
fetch: fetchRelayr,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return result("pending", session);
|
|
740
|
+
}
|
|
528
741
|
session = {
|
|
529
742
|
...session,
|
|
530
743
|
state: "released",
|
|
@@ -534,32 +747,6 @@ export function createSafeRelayrController(options) {
|
|
|
534
747
|
await store.save(session);
|
|
535
748
|
return result("released", session);
|
|
536
749
|
}
|
|
537
|
-
}
|
|
538
|
-
else if (session.paymentStatus !== "unfunded" ||
|
|
539
|
-
session.fundingObserved) {
|
|
540
|
-
// A wallet invocation without a known hash may still have broadcast.
|
|
541
|
-
return session.fundingObserved
|
|
542
|
-
? {
|
|
543
|
-
...result("pending", session),
|
|
544
|
-
recovery: {
|
|
545
|
-
reason: "funding-unresolved",
|
|
546
|
-
message: "This saved selection has reported funding or execution activity. Check the existing bundle and its funding transactions before submitting another payment.",
|
|
547
|
-
},
|
|
548
|
-
}
|
|
549
|
-
: result("pending", session);
|
|
550
|
-
}
|
|
551
|
-
const quoted = relayrQuotedOptions(quote, chains);
|
|
552
|
-
if (!quoted.length)
|
|
553
|
-
return inspectMissingQuote(session);
|
|
554
|
-
const payments = relayrPaymentOptions(quote, chains);
|
|
555
|
-
const expired = payments.length
|
|
556
|
-
? [false]
|
|
557
|
-
: await Promise.all(quoted.map(async ({ details }) => {
|
|
558
|
-
const client = options.clientFor(details.chainId);
|
|
559
|
-
return (!!client &&
|
|
560
|
-
(await relayrDeadlinePassed(client, details.deadline).catch(() => false)));
|
|
561
|
-
}));
|
|
562
|
-
if (expired.every(Boolean)) {
|
|
563
750
|
try {
|
|
564
751
|
await requireRelayrBundleUnpaid(quote.bundle_uuid, {
|
|
565
752
|
fetch: fetchRelayr,
|
|
@@ -568,29 +755,18 @@ export function createSafeRelayrController(options) {
|
|
|
568
755
|
catch {
|
|
569
756
|
return result("pending", session);
|
|
570
757
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
await store.save(session);
|
|
578
|
-
return result("released", session);
|
|
579
|
-
}
|
|
580
|
-
try {
|
|
581
|
-
await requireRelayrBundleUnpaid(quote.bundle_uuid, {
|
|
582
|
-
fetch: fetchRelayr,
|
|
583
|
-
});
|
|
758
|
+
const allowed = session.payments.length && payments.length
|
|
759
|
+
? [relayrRetryOption(session.payments, payments)]
|
|
760
|
+
: payments;
|
|
761
|
+
return allowed.length
|
|
762
|
+
? result("ready", session, allowed)
|
|
763
|
+
: result("pending", session);
|
|
584
764
|
}
|
|
585
|
-
catch {
|
|
586
|
-
|
|
765
|
+
catch (error) {
|
|
766
|
+
if (error instanceof SafeRelayrRecoveryError)
|
|
767
|
+
throw error;
|
|
768
|
+
throw new SafeRelayrRecoveryError(session, error instanceof Error ? error.message : undefined);
|
|
587
769
|
}
|
|
588
|
-
const allowed = session.payments.length && payments.length
|
|
589
|
-
? [relayrRetryOption(session.payments, payments)]
|
|
590
|
-
: payments;
|
|
591
|
-
return allowed.length
|
|
592
|
-
? result("ready", session, allowed)
|
|
593
|
-
: result("pending", session);
|
|
594
770
|
}
|
|
595
771
|
async function prepare({ account, executions: input, signal, onStatus, }) {
|
|
596
772
|
const executions = structuredClone([...input]);
|
|
@@ -603,9 +779,11 @@ export function createSafeRelayrController(options) {
|
|
|
603
779
|
continue;
|
|
604
780
|
let checked;
|
|
605
781
|
try {
|
|
606
|
-
checked = await inspect(saved);
|
|
782
|
+
checked = await inspect(saved, signal);
|
|
607
783
|
}
|
|
608
784
|
catch (error) {
|
|
785
|
+
if (error instanceof SafeRelayrRecoveryError)
|
|
786
|
+
throw error;
|
|
609
787
|
throw new SafeRelayrRecoveryError(saved, error instanceof Error ? error.message : undefined);
|
|
610
788
|
}
|
|
611
789
|
if (checked.state === "released" &&
|
|
@@ -619,12 +797,14 @@ export function createSafeRelayrController(options) {
|
|
|
619
797
|
throw new SafeRelayrRecoveryError(checked.session, checked.recovery?.message, checked.recovery);
|
|
620
798
|
await assertUnreserved(account, executions, saved.id);
|
|
621
799
|
await revalidate(saved.executions, account, onStatus, false, signal);
|
|
800
|
+
phase("reviewing", checked.session, signal);
|
|
622
801
|
await options.review(saved.executions, { resumed: true });
|
|
623
802
|
checkAccount(account, signal);
|
|
624
803
|
rememberReview(checked.session);
|
|
625
804
|
return { ...checked, state: "ready", resumed: true };
|
|
626
805
|
}
|
|
627
806
|
await revalidate(executions, account, onStatus, false, signal);
|
|
807
|
+
phase("reviewing", undefined, signal);
|
|
628
808
|
await options.review(executions, { resumed: false });
|
|
629
809
|
checkAccount(account, signal);
|
|
630
810
|
// Re-read under the shared lock after review. A rejected review leaves the
|
|
@@ -661,6 +841,7 @@ export function createSafeRelayrController(options) {
|
|
|
661
841
|
throw error;
|
|
662
842
|
}
|
|
663
843
|
const request = relayrBundleRequest(executions.map(({ entry }) => entry));
|
|
844
|
+
phase("quoting", session, signal);
|
|
664
845
|
const response = await fetchRelayr(`${RELAYR_API}/v1/bundle/prepaid`, {
|
|
665
846
|
method: "POST",
|
|
666
847
|
headers: { "content-type": "application/json" },
|
|
@@ -689,14 +870,18 @@ export function createSafeRelayrController(options) {
|
|
|
689
870
|
return { state: "ready", session, payments, resumed: false };
|
|
690
871
|
});
|
|
691
872
|
}
|
|
692
|
-
async function check({ account, sessionId, }) {
|
|
693
|
-
return store.withLock(account, async () =>
|
|
873
|
+
async function check({ account, sessionId, signal, }) {
|
|
874
|
+
return store.withLock(account, async () => {
|
|
875
|
+
signal?.throwIfAborted();
|
|
876
|
+
const session = await load(account, sessionId);
|
|
877
|
+
return inspect(session, signal);
|
|
878
|
+
});
|
|
694
879
|
}
|
|
695
880
|
async function fund({ account, sessionId, paymentChainId, signal, onStatus, }) {
|
|
696
881
|
return store.withLock(account, async () => {
|
|
697
882
|
checkAccount(account, signal);
|
|
698
883
|
let session = await load(account, sessionId);
|
|
699
|
-
const checked = await inspect(session);
|
|
884
|
+
const checked = await inspect(session, signal);
|
|
700
885
|
session = checked.session;
|
|
701
886
|
if (checked.state !== "ready")
|
|
702
887
|
throw new SafeRelayrRecoveryError(session, checked.recovery?.message, checked.recovery);
|
|
@@ -707,6 +892,7 @@ export function createSafeRelayrController(options) {
|
|
|
707
892
|
throw new Error("Choose a current payment option from this saved quote.");
|
|
708
893
|
await assertUnreserved(account, session.executions, session.id);
|
|
709
894
|
if (!reviewedIntents.has(reviewKey(session))) {
|
|
895
|
+
phase("reviewing", session, signal);
|
|
710
896
|
await options.review(session.executions, { resumed: true });
|
|
711
897
|
checkAccount(account, signal);
|
|
712
898
|
rememberReview(session);
|
|
@@ -754,6 +940,7 @@ export function createSafeRelayrController(options) {
|
|
|
754
940
|
throw error;
|
|
755
941
|
}
|
|
756
942
|
sending = true;
|
|
943
|
+
phase("payment-submitting", session, signal);
|
|
757
944
|
};
|
|
758
945
|
const onSent = async (payments) => {
|
|
759
946
|
const snapshot = relayrSentPaymentsSnapshot(payments);
|
|
@@ -791,8 +978,10 @@ export function createSafeRelayrController(options) {
|
|
|
791
978
|
paymentStatus: "submitted",
|
|
792
979
|
};
|
|
793
980
|
await store.save(session);
|
|
981
|
+
phase("payment-confirming", session, signal);
|
|
794
982
|
};
|
|
795
983
|
try {
|
|
984
|
+
phase("payment-review", session, signal);
|
|
796
985
|
const sent = await options.sendPayment({
|
|
797
986
|
session: structuredClone(session),
|
|
798
987
|
payment,
|
|
@@ -813,10 +1002,14 @@ export function createSafeRelayrController(options) {
|
|
|
813
1002
|
paymentStatus: checked.session.paymentStatus,
|
|
814
1003
|
};
|
|
815
1004
|
await store.save(session);
|
|
1005
|
+
throw error;
|
|
816
1006
|
}
|
|
1007
|
+
if (sending ||
|
|
1008
|
+
session.payments.length > checked.session.payments.length)
|
|
1009
|
+
throw new SafeRelayrRecoveryError(session, error instanceof Error ? error.message : undefined);
|
|
817
1010
|
throw error;
|
|
818
1011
|
}
|
|
819
|
-
return inspect(session);
|
|
1012
|
+
return inspect(session, signal);
|
|
820
1013
|
});
|
|
821
1014
|
}
|
|
822
1015
|
/** Read-only progress polling; each read releases its lock before the wait. */
|
|
@@ -829,7 +1022,7 @@ export function createSafeRelayrController(options) {
|
|
|
829
1022
|
const started = Date.now();
|
|
830
1023
|
while (true) {
|
|
831
1024
|
signal?.throwIfAborted();
|
|
832
|
-
const next = await check({ account, sessionId });
|
|
1025
|
+
const next = await check({ account, sessionId, signal });
|
|
833
1026
|
signal?.throwIfAborted();
|
|
834
1027
|
await onUpdate?.(next);
|
|
835
1028
|
const remaining = timeoutMs - (Date.now() - started);
|