@bananapus/nana-sdk-core 2.36.0 → 2.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/review/index.d.ts +1 -0
- package/dist/cjs/review/index.d.ts.map +1 -1
- package/dist/cjs/review/index.js +1 -0
- package/dist/cjs/review/index.js.map +1 -1
- package/dist/cjs/review/intents.d.ts +71 -0
- package/dist/cjs/review/intents.d.ts.map +1 -0
- package/dist/cjs/review/intents.js +110 -0
- package/dist/cjs/review/intents.js.map +1 -0
- package/dist/cjs/review/relayr.d.ts +22 -5
- package/dist/cjs/review/relayr.d.ts.map +1 -1
- package/dist/cjs/review/relayr.js +56 -29
- package/dist/cjs/review/relayr.js.map +1 -1
- package/dist/cjs/review/safeRelayr.d.ts +31 -2
- package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
- package/dist/cjs/review/safeRelayr.js +346 -75
- package/dist/cjs/review/safeRelayr.js.map +1 -1
- package/dist/cjs/review/writeHold.d.ts.map +1 -1
- package/dist/cjs/review/writeHold.js +10 -2
- package/dist/cjs/review/writeHold.js.map +1 -1
- package/dist/cjs/review/writeRecovery.d.ts +5 -0
- package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
- package/dist/cjs/review/writeRecovery.js +21 -5
- package/dist/cjs/review/writeRecovery.js.map +1 -1
- package/dist/cjs/testing.d.ts +4 -1
- package/dist/cjs/testing.d.ts.map +1 -1
- package/dist/cjs/testing.js +12 -3
- package/dist/cjs/testing.js.map +1 -1
- package/dist/esm/review/index.d.ts +1 -0
- package/dist/esm/review/index.d.ts.map +1 -1
- package/dist/esm/review/index.js +1 -0
- package/dist/esm/review/index.js.map +1 -1
- package/dist/esm/review/intents.d.ts +71 -0
- package/dist/esm/review/intents.d.ts.map +1 -0
- package/dist/esm/review/intents.js +100 -0
- package/dist/esm/review/intents.js.map +1 -0
- package/dist/esm/review/relayr.d.ts +22 -5
- package/dist/esm/review/relayr.d.ts.map +1 -1
- package/dist/esm/review/relayr.js +55 -29
- package/dist/esm/review/relayr.js.map +1 -1
- package/dist/esm/review/safeRelayr.d.ts +31 -2
- package/dist/esm/review/safeRelayr.d.ts.map +1 -1
- package/dist/esm/review/safeRelayr.js +346 -75
- package/dist/esm/review/safeRelayr.js.map +1 -1
- package/dist/esm/review/writeHold.d.ts.map +1 -1
- package/dist/esm/review/writeHold.js +10 -2
- package/dist/esm/review/writeHold.js.map +1 -1
- package/dist/esm/review/writeRecovery.d.ts +5 -0
- package/dist/esm/review/writeRecovery.d.ts.map +1 -1
- package/dist/esm/review/writeRecovery.js +21 -5
- package/dist/esm/review/writeRecovery.js.map +1 -1
- package/dist/esm/testing.d.ts +4 -1
- package/dist/esm/testing.d.ts.map +1 -1
- package/dist/esm/testing.js +12 -3
- package/dist/esm/testing.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodeFunctionData, encodeFunctionData, isAddress, keccak256, toHex, parseAbi, zeroAddress, } from "viem";
|
|
2
2
|
import { readBoundedSafeNonce } from "../safe.js";
|
|
3
3
|
import { SAFE_EXEC_ABI, canonicalSafeTxHash, requireSafeExecutionSuccess, safeExecutionResult, } from "../safeService.js";
|
|
4
|
-
import { RELAYR_API, RELAYR_UUID_RE, MAX_RELAYR_SENT_PAYMENTS, atCanonicalFinalizedBlock, bindRelayrQuote, readRelayrBundle, relayrBundleRequest,
|
|
4
|
+
import { RELAYR_API, RELAYR_UUID_RE, MAX_RELAYR_SENT_PAYMENTS, atCanonicalFinalizedBlock, bindRelayrQuote, readRelayrBundle, relayrBundleRequest, relayrPaymentOptions, relayrPaymentDetails, relayrPaymentAttemptOutcome, relayrDestinationHash, relayrDestinationHashes, relayrRetryOption, relayrSentPaymentsSnapshot, relayrStateIsPending, relayrStateIsFailed, relayrDeadlinePassed, relayrQuotedOptions, relayrQuoteUnfundable, RelayrPaymentRetryError, requireRelayrBundleUnpaid, requireRelayrRetry, revertedRelayrQuote, verifyRelayrDestinations, verifyRelayrDestination, verifyRelayrPayment, RelayrPaymentRevertedError, RelayrProofUnavailableError, } from "./relayr.js";
|
|
5
5
|
import { TAB, tabShared } from "./tabState.js";
|
|
6
6
|
function hasFundingRecords(records) {
|
|
7
7
|
if (records === undefined)
|
|
@@ -62,6 +62,28 @@ export function canReplaceSafeRelayrQuote(session) {
|
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
/** The line a Safe action shows when its payment's status couldn't be read. */
|
|
66
|
+
export const SAFE_RELAYR_STATUS_UNKNOWN_LINE = "Couldn't check this payment's status. Try again in a moment.";
|
|
67
|
+
/** The line a Safe action shows when its quote can no longer be paid but hasn't expired onchain. */
|
|
68
|
+
export const SAFE_RELAYR_QUOTE_CLOSING_LINE = "This quote's payment window has closed. Try again in a few minutes for a new quote.";
|
|
69
|
+
/** The line a Safe action shows with its warned exit while Relayr's funding report is unresolved. */
|
|
70
|
+
export const SAFE_RELAYR_FUNDING_EXIT_LINE = "Relayr reported a payment for this quote. Check your wallet and Relayr before paying again.";
|
|
71
|
+
/** How long after funding was last reported its warned exit waits: an hour. */
|
|
72
|
+
const FUNDING_EXIT_MS = 60 * 60_000;
|
|
73
|
+
/** The line a Safe action shows with its warned exit (see {@link SafeRelayrRecovery.exit}). */
|
|
74
|
+
export const SAFE_RELAYR_UNCONFIRMED_LINE = "We couldn't confirm this payment's status. If your wallet shows no payment, get a new quote.";
|
|
75
|
+
/** A status read's failure that says nothing: no refusal Relayr gave. */
|
|
76
|
+
function statusUnknown(error) {
|
|
77
|
+
return (!(error instanceof RelayrPaymentRetryError) || error.reason === "unknown");
|
|
78
|
+
}
|
|
79
|
+
/** A refusal saying Relayr reports the bundle paid, or a call running or run. */
|
|
80
|
+
function fundingReported(error) {
|
|
81
|
+
return (error instanceof RelayrPaymentRetryError &&
|
|
82
|
+
(error.reason === "paid" || error.reason === "running"));
|
|
83
|
+
}
|
|
84
|
+
/** Reads of a bundle's payment status before an unknown answer is reported. */
|
|
85
|
+
const STATUS_READS = 3;
|
|
86
|
+
const STATUS_RETRY_MS = 250;
|
|
65
87
|
export class SafeRelayrRecoveryError extends Error {
|
|
66
88
|
constructor(session, message = "This Safe action already has a payment in progress. Check it before paying again.", recovery) {
|
|
67
89
|
super(message);
|
|
@@ -266,7 +288,31 @@ function requireSessionQuote(session) {
|
|
|
266
288
|
}
|
|
267
289
|
/** Shared Safe Relayr state machine. It never signs Safe proposals or submits on prepare/check. */
|
|
268
290
|
export function createSafeRelayrController(options) {
|
|
269
|
-
|
|
291
|
+
/**
|
|
292
|
+
* The app's store, with funding evidence sticky in its one save path: a
|
|
293
|
+
* save from an older snapshot never drops `fundingObserved` or an older
|
|
294
|
+
* `fundingObservedAt` another flow saved. Nothing ever clears them; an
|
|
295
|
+
* exit keeps them for reconciliation.
|
|
296
|
+
*/
|
|
297
|
+
const store = {
|
|
298
|
+
get scope() {
|
|
299
|
+
return options.store.scope;
|
|
300
|
+
},
|
|
301
|
+
list: (account) => options.store.list(account),
|
|
302
|
+
withLock: (account, run) => options.store.withLock(account, run),
|
|
303
|
+
async save(next) {
|
|
304
|
+
const saved = (await options.store.list(next.account)).find((session) => session.id === next.id);
|
|
305
|
+
const at = [saved?.fundingObservedAt, next.fundingObservedAt].filter((value) => typeof value === "number");
|
|
306
|
+
// Once seen, funding stays, and its clock only moves forward.
|
|
307
|
+
return options.store.save(saved?.fundingObserved || next.fundingObserved
|
|
308
|
+
? {
|
|
309
|
+
...next,
|
|
310
|
+
fundingObserved: true,
|
|
311
|
+
...(at.length ? { fundingObservedAt: Math.max(...at) } : {}),
|
|
312
|
+
}
|
|
313
|
+
: next);
|
|
314
|
+
},
|
|
315
|
+
};
|
|
270
316
|
const fetchRelayr = options.fetch ?? globalThis.fetch;
|
|
271
317
|
const displayedExecutions = new Map();
|
|
272
318
|
const notify = (progress, rechecking = false) => {
|
|
@@ -454,18 +500,14 @@ export function createSafeRelayrController(options) {
|
|
|
454
500
|
checks,
|
|
455
501
|
});
|
|
456
502
|
if (session.bundleUuid) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
catch {
|
|
503
|
+
const [status, read] = await bundleUnpaid(session);
|
|
504
|
+
session = read;
|
|
505
|
+
if (status !== "unpaid")
|
|
463
506
|
return pending({
|
|
464
507
|
reason: "funding-unresolved",
|
|
465
508
|
message: "Every saved Safe nonce has been used, but the existing Relayr bundle has not been proven unpaid with all calls pending. Keep its payment evidence and inspect the original bundle before replacing this selection.",
|
|
466
509
|
checks,
|
|
467
510
|
});
|
|
468
|
-
}
|
|
469
511
|
}
|
|
470
512
|
session = {
|
|
471
513
|
...session,
|
|
@@ -490,6 +532,192 @@ export function createSafeRelayrController(options) {
|
|
|
490
532
|
checks,
|
|
491
533
|
});
|
|
492
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* `read` of the bundle's status, tried up to {@link STATUS_READS} times
|
|
537
|
+
* while its failure is unknown. Every status read goes through it.
|
|
538
|
+
*/
|
|
539
|
+
async function statusRead(read) {
|
|
540
|
+
for (let attempt = 1;; attempt += 1) {
|
|
541
|
+
try {
|
|
542
|
+
return await read();
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
if (!statusUnknown(error) || attempt >= STATUS_READS)
|
|
546
|
+
throw error;
|
|
547
|
+
await new Promise((resolve) => setTimeout(resolve, STATUS_RETRY_MS));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/** `session`, saved with the funding Relayr reported. */
|
|
552
|
+
async function observed(session) {
|
|
553
|
+
session = {
|
|
554
|
+
...session,
|
|
555
|
+
fundingObserved: true,
|
|
556
|
+
fundingObservedAt: Date.now(),
|
|
557
|
+
};
|
|
558
|
+
await store.save(session);
|
|
559
|
+
return session;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* The bundle's payment status, three-valued: proven unpaid, funded (Relayr
|
|
563
|
+
* reports it paid or running, saved as observed funding), or unknown when
|
|
564
|
+
* it couldn't be read. Unknown is never paid, and never free to pay again.
|
|
565
|
+
*/
|
|
566
|
+
async function bundleUnpaid(session) {
|
|
567
|
+
try {
|
|
568
|
+
await statusRead(() => requireRelayrBundleUnpaid(session.bundleUuid, { fetch: fetchRelayr }));
|
|
569
|
+
return ["unpaid", session];
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
if (!fundingReported(error))
|
|
573
|
+
return ["unknown", session];
|
|
574
|
+
return ["funded", await observed(session)];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* What a quote whose payments didn't fund it allows
|
|
579
|
+
* ({@link revertedRelayrQuote}), read after {@link bundleUnpaid} proved it
|
|
580
|
+
* unpaid; its own reads are confirmations. Funding they report is saved.
|
|
581
|
+
* Null while unproven, or with `rethrow`, its reason thrown.
|
|
582
|
+
*/
|
|
583
|
+
async function disposed(session, quote, chains, rethrow = false) {
|
|
584
|
+
let state;
|
|
585
|
+
/** The last read before release failed after its retries: unknown. */
|
|
586
|
+
let unconfirmed = false;
|
|
587
|
+
try {
|
|
588
|
+
({ state } = await revertedRelayrQuote(options.clientFor, {
|
|
589
|
+
bundleUuid: quote.bundle_uuid,
|
|
590
|
+
payments: session.payments,
|
|
591
|
+
options: quote.payment_info,
|
|
592
|
+
destinationChainIds: chains,
|
|
593
|
+
account: session.account,
|
|
594
|
+
}, {
|
|
595
|
+
fetch: fetchRelayr,
|
|
596
|
+
// Every read of its bundle, through the same retries.
|
|
597
|
+
statusRead: (read) => statusRead(read).catch((error) => {
|
|
598
|
+
unconfirmed = statusUnknown(error);
|
|
599
|
+
throw error;
|
|
600
|
+
}),
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
if (unconfirmed)
|
|
605
|
+
return { state: "unknown", session };
|
|
606
|
+
if (rethrow)
|
|
607
|
+
throw error;
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
state,
|
|
612
|
+
session: state === "funded" ? await observed(session) : session,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Held while reported funding is unresolved: never a release or a retry.
|
|
617
|
+
* Once every deadline is final and an hour has passed since funding was
|
|
618
|
+
* last reported, the user may leave it, strongly warned: a spurious
|
|
619
|
+
* report is no dead end.
|
|
620
|
+
*/
|
|
621
|
+
async function fundingUnresolved(session) {
|
|
622
|
+
const exit = await fundingExitDue(session, session.quote);
|
|
623
|
+
return {
|
|
624
|
+
...result("pending", session),
|
|
625
|
+
recovery: exit
|
|
626
|
+
? {
|
|
627
|
+
reason: "funding-unresolved",
|
|
628
|
+
message: SAFE_RELAYR_FUNDING_EXIT_LINE,
|
|
629
|
+
exit: true,
|
|
630
|
+
}
|
|
631
|
+
: {
|
|
632
|
+
reason: "funding-unresolved",
|
|
633
|
+
message: "This saved selection has reported funding or execution activity. Check the existing bundle and its funding transactions before submitting another payment.",
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/** Reported funding's warned exit is due: an hour since it was last reported, and every deadline final. */
|
|
638
|
+
async function fundingExitDue(session, quote) {
|
|
639
|
+
return (Date.now() - Number(session.fundingObservedAt) >= FUNDING_EXIT_MS &&
|
|
640
|
+
(await deadlinesFinal(session, quote)));
|
|
641
|
+
}
|
|
642
|
+
/** Every deadline of the quote's options and payments passed at a canonical finalized block. */
|
|
643
|
+
async function deadlinesFinal(session, quote) {
|
|
644
|
+
const deadlines = [
|
|
645
|
+
...relayrQuotedOptions(quote, session.executions.map(({ entry }) => entry.chain)).map(({ details }) => details),
|
|
646
|
+
...session.payments,
|
|
647
|
+
];
|
|
648
|
+
for (const { chainId, deadline } of deadlines) {
|
|
649
|
+
const client = options.clientFor(chainId);
|
|
650
|
+
if (!client || (await relayrDeadlinePassed(client, deadline)) !== true)
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* A held result that says why: in progress, unknown, or closing. Unknown
|
|
657
|
+
* offers the warned exit once nothing shown can fund the quote.
|
|
658
|
+
*/
|
|
659
|
+
async function held(why, session) {
|
|
660
|
+
if (why === "funded")
|
|
661
|
+
return result("pending", session);
|
|
662
|
+
if (why === "closing")
|
|
663
|
+
return {
|
|
664
|
+
...result("pending", session),
|
|
665
|
+
recovery: {
|
|
666
|
+
reason: "quote-closing",
|
|
667
|
+
message: SAFE_RELAYR_QUOTE_CLOSING_LINE,
|
|
668
|
+
retryable: true,
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
const quote = session.quote;
|
|
672
|
+
// With funding reported and unresolved, the exit also waits for the
|
|
673
|
+
// funding exit's rule (see fundingUnresolved), with its stronger warning.
|
|
674
|
+
const funded = hasObservedFunding(session);
|
|
675
|
+
const spent = !!quote &&
|
|
676
|
+
(!funded || (await fundingExitDue(session, quote))) &&
|
|
677
|
+
(await relayrQuoteUnfundable(options.clientFor, {
|
|
678
|
+
bundleUuid: quote.bundle_uuid,
|
|
679
|
+
payments: session.payments,
|
|
680
|
+
options: quote.payment_info,
|
|
681
|
+
destinationChainIds: session.executions.map(({ entry }) => entry.chain),
|
|
682
|
+
account: session.account,
|
|
683
|
+
}, { unseen: true }).catch(() => null)) === true;
|
|
684
|
+
return {
|
|
685
|
+
...result("pending", session),
|
|
686
|
+
recovery: spent
|
|
687
|
+
? {
|
|
688
|
+
reason: "status-unknown",
|
|
689
|
+
message: funded
|
|
690
|
+
? SAFE_RELAYR_FUNDING_EXIT_LINE
|
|
691
|
+
: SAFE_RELAYR_UNCONFIRMED_LINE,
|
|
692
|
+
retryable: true,
|
|
693
|
+
exit: true,
|
|
694
|
+
}
|
|
695
|
+
: {
|
|
696
|
+
reason: "status-unknown",
|
|
697
|
+
message: SAFE_RELAYR_STATUS_UNKNOWN_LINE,
|
|
698
|
+
retryable: true,
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/** The status-unknown refusal, for a status read outside an inspection. */
|
|
703
|
+
const unknownStatus = (session) => new SafeRelayrRecoveryError(session, SAFE_RELAYR_STATUS_UNKNOWN_LINE, {
|
|
704
|
+
reason: "status-unknown",
|
|
705
|
+
message: SAFE_RELAYR_STATUS_UNKNOWN_LINE,
|
|
706
|
+
retryable: true,
|
|
707
|
+
});
|
|
708
|
+
/**
|
|
709
|
+
* Why an overlapping quote is kept, or null when it may be replaced: it is
|
|
710
|
+
* unused (see {@link canReplaceSafeRelayrQuote}) and, once quoted, a status
|
|
711
|
+
* read now proves it unpaid. Funding it reports is saved; unknown keeps it.
|
|
712
|
+
*/
|
|
713
|
+
async function unreplaceable(saved) {
|
|
714
|
+
if (!canReplaceSafeRelayrQuote(saved))
|
|
715
|
+
return result("pending", saved);
|
|
716
|
+
if (!saved.bundleUuid)
|
|
717
|
+
return null;
|
|
718
|
+
const [status, read] = await bundleUnpaid(saved);
|
|
719
|
+
return status === "unpaid" ? null : held(status, read);
|
|
720
|
+
}
|
|
493
721
|
async function inspect(session, signal) {
|
|
494
722
|
const report = (progress, rechecking = false) => {
|
|
495
723
|
if (!signal?.aborted)
|
|
@@ -501,23 +729,28 @@ export function createSafeRelayrController(options) {
|
|
|
501
729
|
return result("released", session);
|
|
502
730
|
if (session.state === "complete")
|
|
503
731
|
return result("complete", session);
|
|
504
|
-
if (hasObservedFunding(session) &&
|
|
505
|
-
session
|
|
732
|
+
if (hasObservedFunding(session) &&
|
|
733
|
+
(session.fundingObserved !== true ||
|
|
734
|
+
session.fundingObservedAt === undefined)) {
|
|
735
|
+
// A record saved before the clock existed starts it now.
|
|
736
|
+
session = {
|
|
737
|
+
...session,
|
|
738
|
+
fundingObserved: true,
|
|
739
|
+
fundingObservedAt: session.fundingObservedAt ?? Date.now(),
|
|
740
|
+
};
|
|
506
741
|
await store.save(session);
|
|
507
742
|
}
|
|
508
743
|
if (!session.bundleUuid || !session.quote)
|
|
509
744
|
return inspectMissingQuote(session);
|
|
510
745
|
const quote = requireSessionQuote(session);
|
|
511
746
|
const chains = session.executions.map(({ entry }) => entry.chain);
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
hasFundingRecords(bundle.transactions))
|
|
518
|
-
session =
|
|
519
|
-
await store.save(session);
|
|
520
|
-
}
|
|
747
|
+
const bundleUuid = session.bundleUuid;
|
|
748
|
+
const bundle = await statusRead(() => readRelayrBundle(bundleUuid, { fetch: fetchRelayr })).catch(() => null);
|
|
749
|
+
if (!bundle)
|
|
750
|
+
return held("unknown", session);
|
|
751
|
+
if (bundle.payment_received === true ||
|
|
752
|
+
hasFundingRecords(bundle.transactions))
|
|
753
|
+
session = await observed(session);
|
|
521
754
|
if (!Array.isArray(bundle.transactions))
|
|
522
755
|
throw new SafeRelayrRecoveryError(session, "Relayr has not returned this bundle's transactions. Check the existing bundle again.");
|
|
523
756
|
const records = bundle.transactions;
|
|
@@ -683,18 +916,28 @@ export function createSafeRelayrController(options) {
|
|
|
683
916
|
everyReverted = false;
|
|
684
917
|
}
|
|
685
918
|
}
|
|
919
|
+
// Funding reported once stays unresolved until the chain proves a
|
|
920
|
+
// payment funded it (above) or the calls ran: a later unpaid read,
|
|
921
|
+
// or this session's own payments reverting, can't exclude another
|
|
922
|
+
// payment. No release, no retry.
|
|
923
|
+
if (hasObservedFunding(session))
|
|
924
|
+
return await fundingUnresolved(session);
|
|
686
925
|
if (!everyReverted) {
|
|
687
926
|
// A payment through a wallet contract that reverted is no
|
|
688
927
|
// RelayrPaymentRevertedError. Once nothing can fund the quote, it
|
|
689
|
-
// is released like an expired one.
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
928
|
+
// is released like an expired one. A payment that can't be shown
|
|
929
|
+
// reverted (dropped, or unreadable) is unknown: never released on
|
|
930
|
+
// its own, since no node can prove a transaction doesn't exist;
|
|
931
|
+
// the warned exit is offered once the chain says it has none.
|
|
932
|
+
const [status, read] = await bundleUnpaid(session);
|
|
933
|
+
session = read;
|
|
934
|
+
if (status === "funded")
|
|
935
|
+
return held("funded", session);
|
|
936
|
+
const disposition = status === "unpaid" ? await disposed(session, quote, chains) : null;
|
|
937
|
+
if (!disposition || disposition.state === "unknown")
|
|
938
|
+
return held("unknown", session);
|
|
939
|
+
session = disposition.session;
|
|
940
|
+
if (disposition.state !== "released")
|
|
698
941
|
return result("pending", session);
|
|
699
942
|
session = {
|
|
700
943
|
...session,
|
|
@@ -707,13 +950,15 @@ export function createSafeRelayrController(options) {
|
|
|
707
950
|
}
|
|
708
951
|
session = { ...session, paymentStatus: "reverted" };
|
|
709
952
|
await store.save(session);
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
953
|
+
const [status, read] = await bundleUnpaid(session);
|
|
954
|
+
session = read;
|
|
955
|
+
if (status !== "unpaid")
|
|
956
|
+
return held(status, session);
|
|
957
|
+
// Unpaid, its release may still be unproven: it says why.
|
|
958
|
+
const disposition = (await disposed(session, quote, chains, true));
|
|
959
|
+
session = disposition.session;
|
|
960
|
+
if (disposition.state === "unknown")
|
|
961
|
+
return held("unknown", session);
|
|
717
962
|
if (disposition.state === "funded")
|
|
718
963
|
return result("pending", session);
|
|
719
964
|
if (disposition.state === "released") {
|
|
@@ -731,13 +976,7 @@ export function createSafeRelayrController(options) {
|
|
|
731
976
|
session.fundingObserved) {
|
|
732
977
|
// A wallet invocation without a known hash may still have broadcast.
|
|
733
978
|
return session.fundingObserved && !known.length
|
|
734
|
-
?
|
|
735
|
-
...result("pending", session),
|
|
736
|
-
recovery: {
|
|
737
|
-
reason: "funding-unresolved",
|
|
738
|
-
message: "This saved selection has reported funding or execution activity. Check the existing bundle and its funding transactions before submitting another payment.",
|
|
739
|
-
},
|
|
740
|
-
}
|
|
979
|
+
? await fundingUnresolved(session)
|
|
741
980
|
: result("pending", session);
|
|
742
981
|
}
|
|
743
982
|
const quoted = relayrQuotedOptions(quote, chains);
|
|
@@ -748,18 +987,15 @@ export function createSafeRelayrController(options) {
|
|
|
748
987
|
? [false]
|
|
749
988
|
: await Promise.all(quoted.map(async ({ details }) => {
|
|
750
989
|
const client = options.clientFor(details.chainId);
|
|
751
|
-
return
|
|
752
|
-
|
|
990
|
+
return client
|
|
991
|
+
? relayrDeadlinePassed(client, details.deadline)
|
|
992
|
+
: false;
|
|
753
993
|
}));
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
catch {
|
|
761
|
-
return result("pending", session);
|
|
762
|
-
}
|
|
994
|
+
const [unpaid, read] = await bundleUnpaid(session);
|
|
995
|
+
session = read;
|
|
996
|
+
if (unpaid !== "unpaid")
|
|
997
|
+
return held(unpaid, session);
|
|
998
|
+
if (expired.every((passed) => passed === true)) {
|
|
763
999
|
session = {
|
|
764
1000
|
...session,
|
|
765
1001
|
state: "released",
|
|
@@ -769,15 +1005,11 @@ export function createSafeRelayrController(options) {
|
|
|
769
1005
|
await store.save(session);
|
|
770
1006
|
return result("released", session);
|
|
771
1007
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
catch {
|
|
778
|
-
return result("pending", session);
|
|
779
|
-
}
|
|
780
|
-
const allowed = session.payments.length && payments.length
|
|
1008
|
+
// No option the browser's clock still allows, though the chain hasn't
|
|
1009
|
+
// shown the quote expired: closing, or unknown if the chain couldn't say.
|
|
1010
|
+
if (!payments.length)
|
|
1011
|
+
return held(expired.some((passed) => passed === null) ? "unknown" : "closing", session);
|
|
1012
|
+
const allowed = session.payments.length
|
|
781
1013
|
? [relayrRetryOption(session.payments, payments)]
|
|
782
1014
|
: payments;
|
|
783
1015
|
return allowed.length
|
|
@@ -796,9 +1028,12 @@ export function createSafeRelayrController(options) {
|
|
|
796
1028
|
return store.withLock(account, async () => {
|
|
797
1029
|
checkAccount(account, signal);
|
|
798
1030
|
const reservations = (await store.list(account)).filter((session) => reserves(session, executions));
|
|
799
|
-
for (const
|
|
800
|
-
|
|
1031
|
+
for (const found of reservations) {
|
|
1032
|
+
const kept = await unreplaceable(found);
|
|
1033
|
+
if (!kept)
|
|
801
1034
|
continue;
|
|
1035
|
+
// What its status read saved, a funding report included.
|
|
1036
|
+
const saved = kept.session;
|
|
802
1037
|
let checked;
|
|
803
1038
|
try {
|
|
804
1039
|
checked = await inspect(saved, signal);
|
|
@@ -834,8 +1069,9 @@ export function createSafeRelayrController(options) {
|
|
|
834
1069
|
for (const saved of await store.list(account)) {
|
|
835
1070
|
if (!reserves(saved, executions))
|
|
836
1071
|
continue;
|
|
837
|
-
|
|
838
|
-
|
|
1072
|
+
const kept = await unreplaceable(saved);
|
|
1073
|
+
if (kept)
|
|
1074
|
+
throw new SafeRelayrRecoveryError(kept.session, kept.recovery?.message, kept.recovery);
|
|
839
1075
|
await store.save({
|
|
840
1076
|
...saved,
|
|
841
1077
|
state: "released",
|
|
@@ -930,17 +1166,21 @@ export function createSafeRelayrController(options) {
|
|
|
930
1166
|
phase("payment-checking", session, signal);
|
|
931
1167
|
await revalidate(session.executions, account, onStatus, true, signal);
|
|
932
1168
|
await assertUnreserved(account, session.executions, session.id);
|
|
933
|
-
|
|
934
|
-
|
|
1169
|
+
await statusRead(() => session.payments.length
|
|
1170
|
+
? requireRelayrRetry(options.clientFor, {
|
|
935
1171
|
payments: session.payments,
|
|
936
1172
|
from: account,
|
|
937
1173
|
bundleUuid: session.bundleUuid,
|
|
938
|
-
}, { fetch: fetchRelayr })
|
|
939
|
-
|
|
940
|
-
else
|
|
941
|
-
await requireRelayrBundleUnpaid(session.bundleUuid, {
|
|
1174
|
+
}, { fetch: fetchRelayr })
|
|
1175
|
+
: requireRelayrBundleUnpaid(session.bundleUuid, {
|
|
942
1176
|
fetch: fetchRelayr,
|
|
943
|
-
})
|
|
1177
|
+
})).catch(async (error) => {
|
|
1178
|
+
if (statusUnknown(error))
|
|
1179
|
+
throw unknownStatus(session);
|
|
1180
|
+
if (fundingReported(error))
|
|
1181
|
+
session = await observed(session);
|
|
1182
|
+
throw error;
|
|
1183
|
+
});
|
|
944
1184
|
checkAccount(account, signal);
|
|
945
1185
|
requireOpenPayment();
|
|
946
1186
|
verified = true;
|
|
@@ -949,6 +1189,17 @@ export function createSafeRelayrController(options) {
|
|
|
949
1189
|
checkAccount(account, signal);
|
|
950
1190
|
if (!verified || sending)
|
|
951
1191
|
throw new Error("Recheck the Safe execution before funding it.");
|
|
1192
|
+
// The saved record, read again just before the wallet opens: funding
|
|
1193
|
+
// another flow reported, or any change to its payment, refuses.
|
|
1194
|
+
const stored = await load(account, session.id);
|
|
1195
|
+
if (hasObservedFunding(stored) ||
|
|
1196
|
+
stored.state !== session.state ||
|
|
1197
|
+
stored.paymentStatus !== session.paymentStatus ||
|
|
1198
|
+
stored.bundleUuid !== session.bundleUuid ||
|
|
1199
|
+
JSON.stringify(stored.payments) !== JSON.stringify(session.payments))
|
|
1200
|
+
throw new SafeRelayrRecoveryError(stored, hasObservedFunding(stored)
|
|
1201
|
+
? SAFE_RELAYR_FUNDING_EXIT_LINE
|
|
1202
|
+
: "This saved selection changed while it was being paid. Check it again before paying.");
|
|
952
1203
|
session = { ...session, paymentStatus: "sending" };
|
|
953
1204
|
await store.save(session);
|
|
954
1205
|
try {
|
|
@@ -1106,6 +1357,26 @@ export function createSafeRelayrController(options) {
|
|
|
1106
1357
|
signal?.removeEventListener("abort", stop);
|
|
1107
1358
|
}
|
|
1108
1359
|
}
|
|
1109
|
-
|
|
1360
|
+
/**
|
|
1361
|
+
* The user's warned exit from a quote whose status stays unknown (see
|
|
1362
|
+
* {@link SafeRelayrRecovery.exit}): rechecked now, then released, its
|
|
1363
|
+
* record kept for reconciliation, so a new quote can follow.
|
|
1364
|
+
*/
|
|
1365
|
+
async function exit({ account, sessionId, signal, }) {
|
|
1366
|
+
return store.withLock(account, async () => {
|
|
1367
|
+
checkAccount(account, signal);
|
|
1368
|
+
const checked = await inspect(await load(account, sessionId), signal);
|
|
1369
|
+
if (!checked.recovery?.exit)
|
|
1370
|
+
throw new SafeRelayrRecoveryError(checked.session, checked.recovery?.message, checked.recovery);
|
|
1371
|
+
const session = {
|
|
1372
|
+
...checked.session,
|
|
1373
|
+
state: "released",
|
|
1374
|
+
releaseReason: "payment-unconfirmed",
|
|
1375
|
+
};
|
|
1376
|
+
await store.save(session);
|
|
1377
|
+
return result("released", session);
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
return { prepare, check, fund, watch, exit };
|
|
1110
1381
|
}
|
|
1111
1382
|
//# sourceMappingURL=safeRelayr.js.map
|