@bananapus/nana-sdk-core 3.5.2 → 4.0.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/bin/wallet-check.mjs +489 -0
- package/dist/cjs/actions/suckerPairs.d.ts +5 -3
- package/dist/cjs/actions/suckerPairs.d.ts.map +1 -1
- package/dist/cjs/actions/suckerPairs.js +6 -26
- package/dist/cjs/actions/suckerPairs.js.map +1 -1
- package/dist/cjs/pause.d.ts +2 -0
- package/dist/cjs/pause.d.ts.map +1 -1
- package/dist/cjs/pause.js +17 -0
- package/dist/cjs/pause.js.map +1 -1
- package/dist/cjs/review/confirm.d.ts +52 -0
- package/dist/cjs/review/confirm.d.ts.map +1 -0
- package/dist/cjs/review/confirm.js +88 -0
- package/dist/cjs/review/confirm.js.map +1 -0
- package/dist/cjs/review/contractWrite.d.ts +5 -3
- package/dist/cjs/review/contractWrite.d.ts.map +1 -1
- package/dist/cjs/review/contractWrite.js +8 -7
- package/dist/cjs/review/contractWrite.js.map +1 -1
- package/dist/cjs/review/decode.d.ts +19 -1
- package/dist/cjs/review/decode.d.ts.map +1 -1
- package/dist/cjs/review/decode.js +30 -11
- package/dist/cjs/review/decode.js.map +1 -1
- package/dist/cjs/review/decodeCall.d.ts +21 -0
- package/dist/cjs/review/decodeCall.d.ts.map +1 -0
- package/dist/cjs/review/decodeCall.js +183 -0
- package/dist/cjs/review/decodeCall.js.map +1 -0
- package/dist/cjs/review/heldWrite.d.ts.map +1 -1
- package/dist/cjs/review/heldWrite.js +5 -4
- package/dist/cjs/review/heldWrite.js.map +1 -1
- package/dist/cjs/review/index.d.ts +11 -0
- package/dist/cjs/review/index.d.ts.map +1 -1
- package/dist/cjs/review/index.js +24 -0
- package/dist/cjs/review/index.js.map +1 -1
- package/dist/cjs/review/intents.d.ts +10 -0
- package/dist/cjs/review/intents.d.ts.map +1 -1
- package/dist/cjs/review/intents.js +13 -0
- package/dist/cjs/review/intents.js.map +1 -1
- package/dist/cjs/review/lineError.d.ts +34 -0
- package/dist/cjs/review/lineError.d.ts.map +1 -0
- package/dist/cjs/review/lineError.js +42 -0
- package/dist/cjs/review/lineError.js.map +1 -0
- package/dist/cjs/review/messages.d.ts +15 -3
- package/dist/cjs/review/messages.d.ts.map +1 -1
- package/dist/cjs/review/messages.js +16 -4
- package/dist/cjs/review/messages.js.map +1 -1
- package/dist/cjs/review/model.d.ts +149 -0
- package/dist/cjs/review/model.d.ts.map +1 -0
- package/dist/cjs/review/model.js +750 -0
- package/dist/cjs/review/model.js.map +1 -0
- package/dist/cjs/review/plan.d.ts +63 -0
- package/dist/cjs/review/plan.d.ts.map +1 -0
- package/dist/cjs/review/plan.js +149 -0
- package/dist/cjs/review/plan.js.map +1 -0
- package/dist/cjs/review/react/index.d.ts +1 -0
- package/dist/cjs/review/react/index.d.ts.map +1 -1
- package/dist/cjs/review/react/index.js +1 -0
- package/dist/cjs/review/react/index.js.map +1 -1
- package/dist/cjs/review/react/useReviewedRun.d.ts +96 -0
- package/dist/cjs/review/react/useReviewedRun.d.ts.map +1 -0
- package/dist/cjs/review/react/useReviewedRun.js +261 -0
- package/dist/cjs/review/react/useReviewedRun.js.map +1 -0
- package/dist/cjs/review/receipt.d.ts +4 -1
- package/dist/cjs/review/receipt.d.ts.map +1 -1
- package/dist/cjs/review/receipt.js +7 -3
- package/dist/cjs/review/receipt.js.map +1 -1
- package/dist/cjs/review/relayr.d.ts +13 -4
- package/dist/cjs/review/relayr.d.ts.map +1 -1
- package/dist/cjs/review/relayr.js +51 -8
- package/dist/cjs/review/relayr.js.map +1 -1
- package/dist/cjs/review/relayrSend.d.ts +135 -0
- package/dist/cjs/review/relayrSend.d.ts.map +1 -0
- package/dist/cjs/review/relayrSend.js +943 -0
- package/dist/cjs/review/relayrSend.js.map +1 -0
- package/dist/cjs/review/reviewQueue.d.ts +10 -9
- package/dist/cjs/review/reviewQueue.d.ts.map +1 -1
- package/dist/cjs/review/reviewQueue.js +19 -29
- package/dist/cjs/review/reviewQueue.js.map +1 -1
- package/dist/cjs/review/reviewRequests.d.ts +86 -0
- package/dist/cjs/review/reviewRequests.d.ts.map +1 -0
- package/dist/cjs/review/reviewRequests.js +266 -0
- package/dist/cjs/review/reviewRequests.js.map +1 -0
- package/dist/cjs/review/safeRelayr.d.ts +3 -1
- package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
- package/dist/cjs/review/safeRelayr.js +5 -1
- package/dist/cjs/review/safeRelayr.js.map +1 -1
- package/dist/cjs/review/safeSend.d.ts +69 -0
- package/dist/cjs/review/safeSend.d.ts.map +1 -0
- package/dist/cjs/review/safeSend.js +308 -0
- package/dist/cjs/review/safeSend.js.map +1 -0
- package/dist/cjs/review/send.d.ts +241 -0
- package/dist/cjs/review/send.d.ts.map +1 -0
- package/dist/cjs/review/send.js +1017 -0
- package/dist/cjs/review/send.js.map +1 -0
- package/dist/cjs/review/sequence.d.ts +77 -0
- package/dist/cjs/review/sequence.d.ts.map +1 -0
- package/dist/cjs/review/sequence.js +232 -0
- package/dist/cjs/review/sequence.js.map +1 -0
- package/dist/cjs/review/sign.d.ts +109 -0
- package/dist/cjs/review/sign.d.ts.map +1 -0
- package/dist/cjs/review/sign.js +374 -0
- package/dist/cjs/review/sign.js.map +1 -0
- package/dist/cjs/review/simulation.d.ts +41 -2
- package/dist/cjs/review/simulation.d.ts.map +1 -1
- package/dist/cjs/review/simulation.js +99 -29
- package/dist/cjs/review/simulation.js.map +1 -1
- package/dist/cjs/review/transaction-message.d.ts +13 -0
- package/dist/cjs/review/transaction-message.d.ts.map +1 -1
- package/dist/cjs/review/transaction-message.js +25 -0
- package/dist/cjs/review/transaction-message.js.map +1 -1
- package/dist/cjs/review/transactionReview.d.ts +27 -19
- package/dist/cjs/review/transactionReview.d.ts.map +1 -1
- package/dist/cjs/review/transactionReview.js +22 -63
- package/dist/cjs/review/transactionReview.js.map +1 -1
- package/dist/cjs/review/wallet.d.ts +82 -0
- package/dist/cjs/review/wallet.d.ts.map +1 -0
- package/dist/cjs/review/wallet.js +157 -0
- package/dist/cjs/review/wallet.js.map +1 -0
- package/dist/cjs/review/writeHold.d.ts +100 -12
- package/dist/cjs/review/writeHold.d.ts.map +1 -1
- package/dist/cjs/review/writeHold.js +61 -13
- package/dist/cjs/review/writeHold.js.map +1 -1
- package/dist/cjs/review/writeRecovery.d.ts +6 -1
- package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
- package/dist/cjs/review/writeRecovery.js +108 -40
- package/dist/cjs/review/writeRecovery.js.map +1 -1
- package/dist/cjs/safeService.d.ts +27 -4
- package/dist/cjs/safeService.d.ts.map +1 -1
- package/dist/cjs/safeService.js +120 -3
- package/dist/cjs/safeService.js.map +1 -1
- package/dist/cjs/testing.d.ts +24 -0
- package/dist/cjs/testing.d.ts.map +1 -1
- package/dist/cjs/testing.js +26 -1
- package/dist/cjs/testing.js.map +1 -1
- package/dist/cjs/v6/defifa.d.ts.map +1 -1
- package/dist/cjs/v6/defifa.js +24 -23
- package/dist/cjs/v6/defifa.js.map +1 -1
- package/dist/cjs/v6/executionErrors.d.ts.map +1 -1
- package/dist/cjs/v6/executionErrors.js +3 -2
- package/dist/cjs/v6/executionErrors.js.map +1 -1
- package/dist/cjs/v6/pay.d.ts +25 -8
- package/dist/cjs/v6/pay.d.ts.map +1 -1
- package/dist/cjs/v6/pay.js +117 -23
- package/dist/cjs/v6/pay.js.map +1 -1
- package/dist/cjs/v6/suckers.d.ts +30 -0
- package/dist/cjs/v6/suckers.d.ts.map +1 -1
- package/dist/cjs/v6/suckers.js +69 -5
- package/dist/cjs/v6/suckers.js.map +1 -1
- package/dist/esm/actions/suckerPairs.d.ts +5 -3
- package/dist/esm/actions/suckerPairs.d.ts.map +1 -1
- package/dist/esm/actions/suckerPairs.js +7 -27
- package/dist/esm/actions/suckerPairs.js.map +1 -1
- package/dist/esm/pause.d.ts +2 -0
- package/dist/esm/pause.d.ts.map +1 -1
- package/dist/esm/pause.js +16 -0
- package/dist/esm/pause.js.map +1 -1
- package/dist/esm/review/confirm.d.ts +52 -0
- package/dist/esm/review/confirm.d.ts.map +1 -0
- package/dist/esm/review/confirm.js +83 -0
- package/dist/esm/review/confirm.js.map +1 -0
- package/dist/esm/review/contractWrite.d.ts +5 -3
- package/dist/esm/review/contractWrite.d.ts.map +1 -1
- package/dist/esm/review/contractWrite.js +8 -7
- package/dist/esm/review/contractWrite.js.map +1 -1
- package/dist/esm/review/decode.d.ts +19 -1
- package/dist/esm/review/decode.d.ts.map +1 -1
- package/dist/esm/review/decode.js +29 -14
- package/dist/esm/review/decode.js.map +1 -1
- package/dist/esm/review/decodeCall.d.ts +21 -0
- package/dist/esm/review/decodeCall.d.ts.map +1 -0
- package/dist/esm/review/decodeCall.js +180 -0
- package/dist/esm/review/decodeCall.js.map +1 -0
- package/dist/esm/review/heldWrite.d.ts.map +1 -1
- package/dist/esm/review/heldWrite.js +5 -4
- package/dist/esm/review/heldWrite.js.map +1 -1
- package/dist/esm/review/index.d.ts +11 -0
- package/dist/esm/review/index.d.ts.map +1 -1
- package/dist/esm/review/index.js +11 -0
- package/dist/esm/review/index.js.map +1 -1
- package/dist/esm/review/intents.d.ts +10 -0
- package/dist/esm/review/intents.d.ts.map +1 -1
- package/dist/esm/review/intents.js +12 -0
- package/dist/esm/review/intents.js.map +1 -1
- package/dist/esm/review/lineError.d.ts +34 -0
- package/dist/esm/review/lineError.d.ts.map +1 -0
- package/dist/esm/review/lineError.js +36 -0
- package/dist/esm/review/lineError.js.map +1 -0
- package/dist/esm/review/messages.d.ts +15 -3
- package/dist/esm/review/messages.d.ts.map +1 -1
- package/dist/esm/review/messages.js +15 -3
- package/dist/esm/review/messages.js.map +1 -1
- package/dist/esm/review/model.d.ts +149 -0
- package/dist/esm/review/model.d.ts.map +1 -0
- package/dist/esm/review/model.js +746 -0
- package/dist/esm/review/model.js.map +1 -0
- package/dist/esm/review/plan.d.ts +63 -0
- package/dist/esm/review/plan.d.ts.map +1 -0
- package/dist/esm/review/plan.js +141 -0
- package/dist/esm/review/plan.js.map +1 -0
- package/dist/esm/review/react/index.d.ts +1 -0
- package/dist/esm/review/react/index.d.ts.map +1 -1
- package/dist/esm/review/react/index.js +1 -0
- package/dist/esm/review/react/index.js.map +1 -1
- package/dist/esm/review/react/useReviewedRun.d.ts +96 -0
- package/dist/esm/review/react/useReviewedRun.d.ts.map +1 -0
- package/dist/esm/review/react/useReviewedRun.js +257 -0
- package/dist/esm/review/react/useReviewedRun.js.map +1 -0
- package/dist/esm/review/receipt.d.ts +4 -1
- package/dist/esm/review/receipt.d.ts.map +1 -1
- package/dist/esm/review/receipt.js +6 -2
- package/dist/esm/review/receipt.js.map +1 -1
- package/dist/esm/review/relayr.d.ts +13 -4
- package/dist/esm/review/relayr.d.ts.map +1 -1
- package/dist/esm/review/relayr.js +50 -8
- package/dist/esm/review/relayr.js.map +1 -1
- package/dist/esm/review/relayrSend.d.ts +135 -0
- package/dist/esm/review/relayrSend.d.ts.map +1 -0
- package/dist/esm/review/relayrSend.js +934 -0
- package/dist/esm/review/relayrSend.js.map +1 -0
- package/dist/esm/review/reviewQueue.d.ts +10 -9
- package/dist/esm/review/reviewQueue.d.ts.map +1 -1
- package/dist/esm/review/reviewQueue.js +17 -26
- package/dist/esm/review/reviewQueue.js.map +1 -1
- package/dist/esm/review/reviewRequests.d.ts +86 -0
- package/dist/esm/review/reviewRequests.d.ts.map +1 -0
- package/dist/esm/review/reviewRequests.js +263 -0
- package/dist/esm/review/reviewRequests.js.map +1 -0
- package/dist/esm/review/safeRelayr.d.ts +3 -1
- package/dist/esm/review/safeRelayr.d.ts.map +1 -1
- package/dist/esm/review/safeRelayr.js +5 -1
- package/dist/esm/review/safeRelayr.js.map +1 -1
- package/dist/esm/review/safeSend.d.ts +69 -0
- package/dist/esm/review/safeSend.d.ts.map +1 -0
- package/dist/esm/review/safeSend.js +303 -0
- package/dist/esm/review/safeSend.js.map +1 -0
- package/dist/esm/review/send.d.ts +241 -0
- package/dist/esm/review/send.d.ts.map +1 -0
- package/dist/esm/review/send.js +1011 -0
- package/dist/esm/review/send.js.map +1 -0
- package/dist/esm/review/sequence.d.ts +77 -0
- package/dist/esm/review/sequence.d.ts.map +1 -0
- package/dist/esm/review/sequence.js +223 -0
- package/dist/esm/review/sequence.js.map +1 -0
- package/dist/esm/review/sign.d.ts +109 -0
- package/dist/esm/review/sign.d.ts.map +1 -0
- package/dist/esm/review/sign.js +368 -0
- package/dist/esm/review/sign.js.map +1 -0
- package/dist/esm/review/simulation.d.ts +41 -2
- package/dist/esm/review/simulation.d.ts.map +1 -1
- package/dist/esm/review/simulation.js +95 -29
- package/dist/esm/review/simulation.js.map +1 -1
- package/dist/esm/review/transaction-message.d.ts +13 -0
- package/dist/esm/review/transaction-message.d.ts.map +1 -1
- package/dist/esm/review/transaction-message.js +23 -0
- package/dist/esm/review/transaction-message.js.map +1 -1
- package/dist/esm/review/transactionReview.d.ts +27 -19
- package/dist/esm/review/transactionReview.d.ts.map +1 -1
- package/dist/esm/review/transactionReview.js +19 -56
- package/dist/esm/review/transactionReview.js.map +1 -1
- package/dist/esm/review/wallet.d.ts +82 -0
- package/dist/esm/review/wallet.d.ts.map +1 -0
- package/dist/esm/review/wallet.js +150 -0
- package/dist/esm/review/wallet.js.map +1 -0
- package/dist/esm/review/writeHold.d.ts +100 -12
- package/dist/esm/review/writeHold.d.ts.map +1 -1
- package/dist/esm/review/writeHold.js +59 -13
- package/dist/esm/review/writeHold.js.map +1 -1
- package/dist/esm/review/writeRecovery.d.ts +6 -1
- package/dist/esm/review/writeRecovery.d.ts.map +1 -1
- package/dist/esm/review/writeRecovery.js +111 -43
- package/dist/esm/review/writeRecovery.js.map +1 -1
- package/dist/esm/safeService.d.ts +27 -4
- package/dist/esm/safeService.d.ts.map +1 -1
- package/dist/esm/safeService.js +119 -4
- package/dist/esm/safeService.js.map +1 -1
- package/dist/esm/testing.d.ts +24 -0
- package/dist/esm/testing.d.ts.map +1 -1
- package/dist/esm/testing.js +23 -1
- package/dist/esm/testing.js.map +1 -1
- package/dist/esm/v6/defifa.d.ts.map +1 -1
- package/dist/esm/v6/defifa.js +24 -23
- package/dist/esm/v6/defifa.js.map +1 -1
- package/dist/esm/v6/executionErrors.d.ts.map +1 -1
- package/dist/esm/v6/executionErrors.js +3 -2
- package/dist/esm/v6/executionErrors.js.map +1 -1
- package/dist/esm/v6/pay.d.ts +25 -8
- package/dist/esm/v6/pay.d.ts.map +1 -1
- package/dist/esm/v6/pay.js +115 -22
- package/dist/esm/v6/pay.js.map +1 -1
- package/dist/esm/v6/suckers.d.ts +30 -0
- package/dist/esm/v6/suckers.d.ts.map +1 -1
- package/dist/esm/v6/suckers.js +67 -5
- package/dist/esm/v6/suckers.js.map +1 -1
- package/package.json +11 -2
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
import { concat, decodeFunctionData, decodeFunctionResult, encodeFunctionData, formatEther, } from "viem";
|
|
2
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
import { erc2771ForwarderAbi, jbV6ContractAddress, } from "../generated/juicebox.js";
|
|
4
|
+
import { multiSendCallsOf } from "../safe.js";
|
|
5
|
+
import { findPendingSafeAppProposal, hasSafeService, SAFE_EXEC_ABI, } from "../safeService.js";
|
|
6
|
+
import { promptReview } from "./confirm.js";
|
|
7
|
+
import { SubmittedContractWriteError, submitReviewedContractWrite, } from "./contractWrite.js";
|
|
8
|
+
import { decodeReviewCall } from "./decodeCall.js";
|
|
9
|
+
import { buildReviewModel } from "./model.js";
|
|
10
|
+
import { plannedRelayrFee, relayrStandInSender, spendPlan, spendPlannedRelayrRound, } from "./plan.js";
|
|
11
|
+
import { waitForTrackedReceipt } from "./receipt.js";
|
|
12
|
+
import { bindRelayrQuote, FORWARD_REQUEST_TYPES, MAX_RELAYR_SENT_PAYMENTS, quoteExpired, readRelayrBundle, RELAYR_API, RELAYR_PAYMENT_ADDRESS, RELAYR_PAYMENT_GAS, RELAYR_PAYMENT_SELECTOR, relayrBundleRequest, relayrForwarder, relayrForwarderDeadline, relayrForwardRequest, relayrPaymentChains, relayrPaymentDetails, relayrPaymentOptions, RelayrPaymentNotSentError, relayrWalletPaymentError, relayrStateIsFailed, relayrStateIsSuccess, relayrSupportsChains, requireRelayrBundleUnpaid, requireRelayrPaymentRuntime, requireRelayrRetry, sentRelayrPayment, simulateRelayrPayment, TRUSTED_FORWARDER_ABI, verifyRelayrPayment, } from "./relayr.js";
|
|
13
|
+
import { ReviewedCallRevertedError, ReviewLineError } from "./lineError.js";
|
|
14
|
+
import { plannedCallsOf, prepareSignature, signPrepared, } from "./sign.js";
|
|
15
|
+
import { simulateStateChangingTransaction } from "./simulation.js";
|
|
16
|
+
import { unsupportedWalletRequest, walletReady, } from "./wallet.js";
|
|
17
|
+
import { openStepHold, WriteHeldError } from "./writeHold.js";
|
|
18
|
+
import { browserLocks } from "./writeRecovery.js";
|
|
19
|
+
import { timeout } from "../pause.js";
|
|
20
|
+
/**
|
|
21
|
+
* The margin over an estimated Relayr fee that the review shows as its cap,
|
|
22
|
+
* in basis points (11,000 = the estimate plus 10%). Identical requests quoted
|
|
23
|
+
* minutes apart on every Relayr chain differed by at most 1.4%, so a signed
|
|
24
|
+
* quote above the cap, which asks again, should be rare.
|
|
25
|
+
*/
|
|
26
|
+
export const RELAYR_FEE_MARGIN_BPS = 11000n;
|
|
27
|
+
/**
|
|
28
|
+
* Relayr's fee over a request's gas limit at the chain's gas price, in basis
|
|
29
|
+
* points, for bounding a fee when no stand-in quote can be had: the highest
|
|
30
|
+
* ratio measured on a Relayr chain (7.95, Ethereum at 100,000 gas), rounded
|
|
31
|
+
* up. Most of a fee is fixed per chain, so the ratio falls as gas rises.
|
|
32
|
+
*/
|
|
33
|
+
export const RELAYR_FEE_MARKUP_BPS = 80000n;
|
|
34
|
+
/** The gas a forwarder's `execute` adds around the request's own. */
|
|
35
|
+
const FORWARDER_OVERHEAD_GAS = 100000n;
|
|
36
|
+
const QUOTE_TIMEOUT_MS = 45_000;
|
|
37
|
+
/** How long the review waits on a stand-in quote before it bounds the fee locally. */
|
|
38
|
+
const STAND_IN_TIMEOUT_MS = 10_000;
|
|
39
|
+
const POLL_INTERVAL_MS = 2_500;
|
|
40
|
+
const POLL_TIMEOUT_MS = 5 * 60_000;
|
|
41
|
+
/** How many times an expired quote is replaced before the action stops. */
|
|
42
|
+
const QUOTE_ATTEMPTS = 3;
|
|
43
|
+
const BALANCE_OF_ABI = [
|
|
44
|
+
{
|
|
45
|
+
type: "function",
|
|
46
|
+
name: "balanceOf",
|
|
47
|
+
stateMutability: "view",
|
|
48
|
+
inputs: [{ name: "owner", type: "address" }],
|
|
49
|
+
outputs: [{ type: "uint256" }],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
const RELAYR_PAYMENT_ABI = [
|
|
53
|
+
{
|
|
54
|
+
type: "function",
|
|
55
|
+
name: "prepayment",
|
|
56
|
+
stateMutability: "payable",
|
|
57
|
+
inputs: [
|
|
58
|
+
{ name: "payment_uuid", type: "bytes16" },
|
|
59
|
+
{ name: "deadline", type: "uint40" },
|
|
60
|
+
],
|
|
61
|
+
outputs: [],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const ACCOUNT_CHANGED = "The connected account changed. Review again.";
|
|
65
|
+
const ORDINARY_WALLET = "Pay for relayed transactions from an ordinary wallet. Nothing was sent.";
|
|
66
|
+
const CHANGED_REQUEST = "The relay authorization or trusted forwarder changed. Review the original action again.";
|
|
67
|
+
/** The quote's payment deadline passed before the wallet opened. */
|
|
68
|
+
class QuoteExpiredError extends Error {
|
|
69
|
+
constructor() {
|
|
70
|
+
super("This Relayr quote expired. Review the action again for a new quote.");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function forwarderOn(chainId) {
|
|
74
|
+
const forwarder = relayrForwarder(chainId);
|
|
75
|
+
if (!forwarder) {
|
|
76
|
+
throw new Error(`There is no Juicebox forwarder on chain ${chainId}.`);
|
|
77
|
+
}
|
|
78
|
+
return forwarder;
|
|
79
|
+
}
|
|
80
|
+
/** The connected wallet, as long as it is an ordinary one on `account` (or, with `safe`, a Safe app). */
|
|
81
|
+
async function requireOrdinaryWallet(adapter, account, safe = false) {
|
|
82
|
+
await walletReady(adapter);
|
|
83
|
+
if (adapter.kind !== "wallet")
|
|
84
|
+
throw unsupportedWalletRequest();
|
|
85
|
+
const connected = adapter.account();
|
|
86
|
+
if (!connected)
|
|
87
|
+
throw new Error("Connect a wallet first.");
|
|
88
|
+
if (connected.toLowerCase() !== account.toLowerCase()) {
|
|
89
|
+
throw new Error(ACCOUNT_CHANGED);
|
|
90
|
+
}
|
|
91
|
+
// Signed requests and their proof need an ordinary wallet's own transaction.
|
|
92
|
+
if (adapter.isSafe() && !safe)
|
|
93
|
+
throw new Error(ORDINARY_WALLET);
|
|
94
|
+
}
|
|
95
|
+
/** Whether `target` on `chainId` trusts the chain's canonical forwarder, so the real sender is kept. */
|
|
96
|
+
async function trustsForwarder(client, chainId, target) {
|
|
97
|
+
const forwarder = relayrForwarder(chainId);
|
|
98
|
+
if (!forwarder)
|
|
99
|
+
return false;
|
|
100
|
+
try {
|
|
101
|
+
const data = await simulateStateChangingTransaction(client, {
|
|
102
|
+
from: forwarder,
|
|
103
|
+
to: target,
|
|
104
|
+
data: encodeFunctionData({
|
|
105
|
+
abi: TRUSTED_FORWARDER_ABI,
|
|
106
|
+
functionName: "isTrustedForwarder",
|
|
107
|
+
args: [forwarder],
|
|
108
|
+
}),
|
|
109
|
+
gas: 100000n,
|
|
110
|
+
});
|
|
111
|
+
return decodeFunctionResult({
|
|
112
|
+
abi: TRUSTED_FORWARDER_ABI,
|
|
113
|
+
functionName: "isTrustedForwarder",
|
|
114
|
+
data,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The forward request each call is signed as: from `account`, with the
|
|
123
|
+
* forwarder nonce read now and the default deadline. Refuses more than one
|
|
124
|
+
* call per chain, chains outside one Relayr network family, a gas limit that
|
|
125
|
+
* is not positive, and a destination that does not trust the forwarder.
|
|
126
|
+
*/
|
|
127
|
+
async function forwardRequests(calls, account, clientFor) {
|
|
128
|
+
const read = [...calls].map(({ chainId, target, data, value, gas }) => ({
|
|
129
|
+
chainId,
|
|
130
|
+
target,
|
|
131
|
+
data,
|
|
132
|
+
value: value ?? 0n,
|
|
133
|
+
gas,
|
|
134
|
+
}));
|
|
135
|
+
if (!read.length)
|
|
136
|
+
throw new Error("Choose at least one chain.");
|
|
137
|
+
const chainIds = read.map((call) => call.chainId);
|
|
138
|
+
if (!relayrSupportsChains(chainIds)) {
|
|
139
|
+
throw new Error("Relayr runs one call per supported destination, all mainnets or all testnets.");
|
|
140
|
+
}
|
|
141
|
+
if (read.some((call) => typeof call.gas !== "bigint" || call.gas <= 0n)) {
|
|
142
|
+
throw new Error("Each relayed call needs a positive gas limit.");
|
|
143
|
+
}
|
|
144
|
+
const deadline = relayrForwarderDeadline();
|
|
145
|
+
return Promise.all(read.map(async (call) => {
|
|
146
|
+
const client = clientFor(call.chainId);
|
|
147
|
+
if (!(await trustsForwarder(client, call.chainId, call.target))) {
|
|
148
|
+
throw new Error(`The target on chain ${call.chainId} does not trust the canonical forwarder. Send this action directly.`);
|
|
149
|
+
}
|
|
150
|
+
const nonce = await client.readContract({
|
|
151
|
+
address: forwarderOn(call.chainId),
|
|
152
|
+
abi: erc2771ForwarderAbi,
|
|
153
|
+
functionName: "nonces",
|
|
154
|
+
args: [account],
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
chainId: call.chainId,
|
|
158
|
+
request: {
|
|
159
|
+
from: account,
|
|
160
|
+
to: call.target,
|
|
161
|
+
value: call.value,
|
|
162
|
+
gas: call.gas,
|
|
163
|
+
nonce,
|
|
164
|
+
deadline,
|
|
165
|
+
data: call.data,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
/** The bundle entry that runs `request` with `signature` through the chain's forwarder. */
|
|
171
|
+
function entryOf(chainId, request, signature) {
|
|
172
|
+
const { from, to, value, gas, deadline, data } = request;
|
|
173
|
+
return {
|
|
174
|
+
chain: chainId,
|
|
175
|
+
target: forwarderOn(chainId),
|
|
176
|
+
data: encodeFunctionData({
|
|
177
|
+
abi: erc2771ForwarderAbi,
|
|
178
|
+
functionName: "execute",
|
|
179
|
+
args: [{ from, to, value, gas, deadline, data, signature }],
|
|
180
|
+
}),
|
|
181
|
+
value: value.toString(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Before posting or paying, each signed entry must still be what was
|
|
186
|
+
* signed: from `account`, executable for two more minutes, to a destination
|
|
187
|
+
* that trusts the forwarder, accepted by the forwarder's `verify`, and
|
|
188
|
+
* simulating from `account` without a revert.
|
|
189
|
+
*/
|
|
190
|
+
async function verifyForwardedEntries(entries, account, clientFor) {
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
const request = relayrForwardRequest(entry);
|
|
193
|
+
if (!request)
|
|
194
|
+
throw new Error("Invalid relay authorization.");
|
|
195
|
+
const client = clientFor(entry.chain);
|
|
196
|
+
if (request.from.toLowerCase() !== account.toLowerCase() ||
|
|
197
|
+
request.value !== BigInt(entry.value) ||
|
|
198
|
+
request.deadline <= Math.floor(Date.now() / 1_000) + 120 ||
|
|
199
|
+
!(await trustsForwarder(client, entry.chain, request.to)) ||
|
|
200
|
+
!(await client.readContract({
|
|
201
|
+
address: entry.target,
|
|
202
|
+
abi: erc2771ForwarderAbi,
|
|
203
|
+
functionName: "verify",
|
|
204
|
+
args: [request],
|
|
205
|
+
}))) {
|
|
206
|
+
throw new Error(CHANGED_REQUEST);
|
|
207
|
+
}
|
|
208
|
+
await simulateStateChangingTransaction(client, {
|
|
209
|
+
from: account,
|
|
210
|
+
to: entry.target,
|
|
211
|
+
data: entry.data,
|
|
212
|
+
value: request.value,
|
|
213
|
+
gas: request.gas + request.gas / 63n + FORWARDER_OVERHEAD_GAS,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Post `entries` to Relayr and bind its quote to them (see {@link bindRelayrQuote}). */
|
|
218
|
+
async function postBundle(entries) {
|
|
219
|
+
const request = relayrBundleRequest(entries);
|
|
220
|
+
const response = await globalThis.fetch(`${RELAYR_API}/v1/bundle/prepaid`, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
body: JSON.stringify(request),
|
|
224
|
+
signal: AbortSignal.timeout(QUOTE_TIMEOUT_MS),
|
|
225
|
+
});
|
|
226
|
+
return bindRelayrQuote(response, request);
|
|
227
|
+
}
|
|
228
|
+
/** Refuses while a payment held under `intent` may still land, or, for `paidOnce`, once one succeeded. */
|
|
229
|
+
function requireIntentClear(deps, account, intent) {
|
|
230
|
+
for (const chainId of intent.funding) {
|
|
231
|
+
const state = openStepHold(deps.hold, account, false, {
|
|
232
|
+
chainId,
|
|
233
|
+
to: RELAYR_PAYMENT_ADDRESS,
|
|
234
|
+
data: "0x",
|
|
235
|
+
intent: intent.key,
|
|
236
|
+
}).peek();
|
|
237
|
+
if (state.status !== "clear")
|
|
238
|
+
throw new WriteHeldError(state);
|
|
239
|
+
if (intent.paidOnce && state.settled?.result === "success") {
|
|
240
|
+
throw new Error("This Relayr payment already succeeded. Do not pay again.");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** The quote's authenticated payment on `chainId`. */
|
|
245
|
+
function paymentOn(quote, chainId) {
|
|
246
|
+
const destinationChainIds = quote.expectedTransactions.map((binding) => binding.chain);
|
|
247
|
+
const option = relayrPaymentOptions(quote, destinationChainIds).find((payment) => payment.chain === chainId);
|
|
248
|
+
if (!option &&
|
|
249
|
+
relayrPaymentOptions(quote, destinationChainIds, 0).some((payment) => payment.chain === chainId)) {
|
|
250
|
+
// Offered, but already too close to its deadline to pay.
|
|
251
|
+
throw new QuoteExpiredError();
|
|
252
|
+
}
|
|
253
|
+
if (!option) {
|
|
254
|
+
throw new Error(`Relayr returned no payment option on chain ${chainId}. Nothing was paid.`);
|
|
255
|
+
}
|
|
256
|
+
return relayrPaymentDetails(option, {
|
|
257
|
+
bundleUuid: quote.bundle_uuid,
|
|
258
|
+
destinationChainIds,
|
|
259
|
+
expectedTransactions: quote.expectedTransactions,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/** The calls a quote's bundle runs, as a plan holds them; a raw value that can't be read matches none. */
|
|
263
|
+
function quotedCalls(quote) {
|
|
264
|
+
return quote.expectedTransactions.map(({ chain, entry }) => {
|
|
265
|
+
const request = relayrForwardRequest(entry);
|
|
266
|
+
if (request) {
|
|
267
|
+
const { from, to, data, value, gas } = request;
|
|
268
|
+
return { chainId: chain, from, to, data, value, gas };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
chainId: chain,
|
|
272
|
+
from: "relayr",
|
|
273
|
+
to: entry.target,
|
|
274
|
+
data: entry.data,
|
|
275
|
+
value: /^\d+$/u.test(entry.value) ? BigInt(entry.value) : -1n,
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function withMargin(fee) {
|
|
280
|
+
return (fee * RELAYR_FEE_MARGIN_BPS + 9999n) / 10000n;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Relayr's fee for `calls` signed by a key made for this quote alone: the
|
|
284
|
+
* same chains, gas limits and values, so Relayr prices the same work. The key
|
|
285
|
+
* lives only in memory for this call and leaves it only inside the requests'
|
|
286
|
+
* signatures.
|
|
287
|
+
*/
|
|
288
|
+
async function standInFee(calls, paymentChainId, deadline) {
|
|
289
|
+
const signer = privateKeyToAccount(generatePrivateKey());
|
|
290
|
+
const entries = await Promise.all(calls.map(async ({ chainId, target, data, value, gas, raw }) => {
|
|
291
|
+
// Anyone may run a raw call: it is its own stand-in.
|
|
292
|
+
if (raw)
|
|
293
|
+
return { chain: chainId, target, data, value: value.toString() };
|
|
294
|
+
const request = {
|
|
295
|
+
from: signer.address,
|
|
296
|
+
to: target,
|
|
297
|
+
value,
|
|
298
|
+
gas,
|
|
299
|
+
deadline,
|
|
300
|
+
data,
|
|
301
|
+
};
|
|
302
|
+
const signature = await signer.signTypedData({
|
|
303
|
+
domain: {
|
|
304
|
+
name: "Juicebox",
|
|
305
|
+
version: "1",
|
|
306
|
+
chainId,
|
|
307
|
+
verifyingContract: forwarderOn(chainId),
|
|
308
|
+
},
|
|
309
|
+
types: FORWARD_REQUEST_TYPES,
|
|
310
|
+
primaryType: "ForwardRequest",
|
|
311
|
+
// A key made now has used no forwarder nonce.
|
|
312
|
+
message: { ...request, nonce: 0n },
|
|
313
|
+
});
|
|
314
|
+
return entryOf(chainId, request, signature);
|
|
315
|
+
}));
|
|
316
|
+
return paymentOn(await postBundle(entries), paymentChainId).amount;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The fee cap the review shows: Relayr's quote for stand-ins of `calls`, plus
|
|
320
|
+
* {@link RELAYR_FEE_MARGIN_BPS}. A call that runs only for the real sender
|
|
321
|
+
* (an owner's action) can't be stood in, so a call with the same chain, gas
|
|
322
|
+
* and calldata length that anyone can run (JBProjects' `balanceOf`) is quoted
|
|
323
|
+
* instead, with the calls' values added. Without either quote in time, the
|
|
324
|
+
* fee is bounded locally: each call's gas estimate through the forwarder as
|
|
325
|
+
* `account`, at the chain's gas price, times {@link RELAYR_FEE_MARKUP_BPS}.
|
|
326
|
+
*/
|
|
327
|
+
export async function relayrFeeCap(calls, account, paymentChainId, clientFor) {
|
|
328
|
+
const read = calls.map((call) => ({ ...call, value: call.value ?? 0n }));
|
|
329
|
+
const deadline = relayrForwarderDeadline();
|
|
330
|
+
const values = read.reduce((sum, call) => sum + call.value, 0n);
|
|
331
|
+
const quoted = await timeout(standInFee(read, paymentChainId, deadline).catch(async () => {
|
|
332
|
+
const filler = privateKeyToAccount(generatePrivateKey()).address;
|
|
333
|
+
return ((await standInFee(read.map((call) => {
|
|
334
|
+
const probe = encodeFunctionData({
|
|
335
|
+
abi: BALANCE_OF_ABI,
|
|
336
|
+
functionName: "balanceOf",
|
|
337
|
+
args: [filler],
|
|
338
|
+
});
|
|
339
|
+
const bytes = Math.max((call.data.length - probe.length) / 2, 0);
|
|
340
|
+
return {
|
|
341
|
+
...call,
|
|
342
|
+
raw: false,
|
|
343
|
+
target: jbV6ContractAddress.JBProjects[call.chainId],
|
|
344
|
+
data: `${probe}${"00".repeat(bytes)}`,
|
|
345
|
+
value: 0n,
|
|
346
|
+
};
|
|
347
|
+
}), paymentChainId, deadline)) + values);
|
|
348
|
+
}), STAND_IN_TIMEOUT_MS).catch(() => null);
|
|
349
|
+
if (quoted !== null)
|
|
350
|
+
return withMargin(quoted);
|
|
351
|
+
let bound = values;
|
|
352
|
+
for (const call of read) {
|
|
353
|
+
const client = clientFor(call.chainId);
|
|
354
|
+
const estimate = await client
|
|
355
|
+
.estimateGas(call.raw
|
|
356
|
+
? { to: call.target, data: call.data, value: call.value }
|
|
357
|
+
: {
|
|
358
|
+
account: forwarderOn(call.chainId),
|
|
359
|
+
to: call.target,
|
|
360
|
+
// ERC-2771: the forwarder appends the real sender.
|
|
361
|
+
data: concat([call.data, account]),
|
|
362
|
+
value: call.value,
|
|
363
|
+
})
|
|
364
|
+
.catch(() => call.gas);
|
|
365
|
+
const gas = (estimate > call.gas ? estimate : call.gas) +
|
|
366
|
+
(call.raw ? 0n : FORWARDER_OVERHEAD_GAS);
|
|
367
|
+
bound +=
|
|
368
|
+
(gas * (await client.getGasPrice()) * RELAYR_FEE_MARKUP_BPS) / 10000n;
|
|
369
|
+
}
|
|
370
|
+
return withMargin(bound);
|
|
371
|
+
}
|
|
372
|
+
/** The payment as the review shows it: one call to Relayr's pinned payment contract. */
|
|
373
|
+
function paymentReviewCall(details, account, safe = false) {
|
|
374
|
+
const { functionName, args } = decodeFunctionData({
|
|
375
|
+
abi: RELAYR_PAYMENT_ABI,
|
|
376
|
+
data: details.calldata,
|
|
377
|
+
});
|
|
378
|
+
return {
|
|
379
|
+
chainId: details.chainId,
|
|
380
|
+
from: account,
|
|
381
|
+
to: RELAYR_PAYMENT_ADDRESS,
|
|
382
|
+
data: details.calldata,
|
|
383
|
+
value: details.amount,
|
|
384
|
+
// A Safe signs the sent gas as safeTxGas; 0 makes a failed payment revert.
|
|
385
|
+
...(safe ? { safeTxGas: 0n } : { gas: RELAYR_PAYMENT_GAS }),
|
|
386
|
+
abi: RELAYR_PAYMENT_ABI,
|
|
387
|
+
functionName,
|
|
388
|
+
args,
|
|
389
|
+
label: "Pay network fee",
|
|
390
|
+
contractName: "Relayr payment",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
async function reviewPayment(details, account, action, safe = false) {
|
|
394
|
+
const model = buildReviewModel({
|
|
395
|
+
calls: [paymentReviewCall(details, account, safe)],
|
|
396
|
+
title: action.title ?? "Review network fee",
|
|
397
|
+
...(action.context ? { context: action.context } : {}),
|
|
398
|
+
});
|
|
399
|
+
await promptReview(model, Promise.resolve({ status: "passed" }), Promise.resolve(model));
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Send one reviewed payment without asking again: Relayr's pinned contract
|
|
403
|
+
* and selector only, from `account`, after the runtime and simulation checks,
|
|
404
|
+
* under the payment's write hold (or `intent`'s). The payment must be proven
|
|
405
|
+
* from the chain before the hash it was mined under comes back.
|
|
406
|
+
*/
|
|
407
|
+
async function sendPayment(details, account, deps, hooks = {}) {
|
|
408
|
+
const { adapter } = deps;
|
|
409
|
+
const safe = hooks.safe ?? false;
|
|
410
|
+
await requireOrdinaryWallet(adapter, account, safe);
|
|
411
|
+
const client = deps.clientFor(details.chainId);
|
|
412
|
+
const call = {
|
|
413
|
+
chainId: details.chainId,
|
|
414
|
+
to: RELAYR_PAYMENT_ADDRESS,
|
|
415
|
+
data: details.calldata,
|
|
416
|
+
value: details.amount,
|
|
417
|
+
...(hooks.intent ? { intent: hooks.intent.key } : {}),
|
|
418
|
+
};
|
|
419
|
+
const handle = openStepHold(deps.hold, account, safe, call);
|
|
420
|
+
/** The attempt is saved (`onSending`), and the wallet not yet asked. */
|
|
421
|
+
let saved = false;
|
|
422
|
+
let invoked = false;
|
|
423
|
+
/** A proposal of exactly this payment its Safe already queued, sent in its place. */
|
|
424
|
+
let queued = null;
|
|
425
|
+
const attempt = () => {
|
|
426
|
+
const held = handle.peek();
|
|
427
|
+
if (held.status !== "clear")
|
|
428
|
+
throw new WriteHeldError(held);
|
|
429
|
+
return handle.send(client, (gate) => submitReviewedContractWrite({
|
|
430
|
+
request: call,
|
|
431
|
+
expectedAccount: account,
|
|
432
|
+
review: () => Promise.resolve(),
|
|
433
|
+
switchChain: async (chainId) => {
|
|
434
|
+
if (adapter.chainId() !== chainId)
|
|
435
|
+
await adapter.switchChain(chainId);
|
|
436
|
+
},
|
|
437
|
+
currentAccount: () => adapter.account(),
|
|
438
|
+
reverify: async () => {
|
|
439
|
+
await hooks.recheck?.();
|
|
440
|
+
// A lapsed payment would only revert in simulation.
|
|
441
|
+
if (quoteExpired(details.deadline))
|
|
442
|
+
throw new QuoteExpiredError();
|
|
443
|
+
await requireRelayrPaymentRuntime(client);
|
|
444
|
+
},
|
|
445
|
+
simulate: async () => {
|
|
446
|
+
const [, found] = await Promise.all([
|
|
447
|
+
simulateRelayrPayment(client, {
|
|
448
|
+
from: account,
|
|
449
|
+
payment: details,
|
|
450
|
+
}),
|
|
451
|
+
// A Safe never proposes again a payment it already queued exactly.
|
|
452
|
+
safe && hasSafeService(details.chainId)
|
|
453
|
+
? findPendingSafeAppProposal(client, details.chainId, account, {
|
|
454
|
+
to: RELAYR_PAYMENT_ADDRESS,
|
|
455
|
+
data: details.calldata,
|
|
456
|
+
value: details.amount,
|
|
457
|
+
}, deps.hold.safeService, true)
|
|
458
|
+
: null,
|
|
459
|
+
]);
|
|
460
|
+
queued = found;
|
|
461
|
+
return details;
|
|
462
|
+
},
|
|
463
|
+
beforeWrite: async () => {
|
|
464
|
+
const { intent } = hooks;
|
|
465
|
+
// Only the check and the reservation are under the lock: never a
|
|
466
|
+
// chain switch, simulation or wallet that might not answer.
|
|
467
|
+
await (intent
|
|
468
|
+
? browserLocks().request(`nana-sdk:relayr-payment:${account.toLowerCase()}:${intent.key}`, { mode: "exclusive" }, async () => {
|
|
469
|
+
requireIntentClear(deps, account, intent);
|
|
470
|
+
await gate.beforeSend();
|
|
471
|
+
})
|
|
472
|
+
: gate.beforeSend());
|
|
473
|
+
try {
|
|
474
|
+
await hooks.onSending?.();
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
gate.onRejected();
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
saved = true;
|
|
481
|
+
},
|
|
482
|
+
beforeSend: () => {
|
|
483
|
+
try {
|
|
484
|
+
if (details.target.toLowerCase() !== RELAYR_PAYMENT_ADDRESS ||
|
|
485
|
+
details.calldata.slice(0, 10).toLowerCase() !==
|
|
486
|
+
RELAYR_PAYMENT_SELECTOR) {
|
|
487
|
+
throw new Error("The Relayr payment is not to its pinned contract.");
|
|
488
|
+
}
|
|
489
|
+
if (quoteExpired(details.deadline))
|
|
490
|
+
throw new QuoteExpiredError();
|
|
491
|
+
if (adapter.isSafe() !== safe)
|
|
492
|
+
throw new Error(safe
|
|
493
|
+
? "Wallet connection changed. Review the payment again."
|
|
494
|
+
: ORDINARY_WALLET);
|
|
495
|
+
if (adapter.chainId() !== details.chainId) {
|
|
496
|
+
throw new Error("Your wallet changed networks. Review the payment again.");
|
|
497
|
+
}
|
|
498
|
+
// Last, after every callback above could have run.
|
|
499
|
+
if (adapter.account()?.toLowerCase() !== account.toLowerCase()) {
|
|
500
|
+
throw new Error(ACCOUNT_CHANGED);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
throw new RelayrPaymentNotSentError(error);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
onBeforeWriteAborted: () => gate.onRejected(),
|
|
508
|
+
onWriteRejected: () => gate.onRejected(),
|
|
509
|
+
onWriteSubmitted: async (submitted) => {
|
|
510
|
+
gate.afterSubmit(submitted, queued?.tx);
|
|
511
|
+
await hooks.onSent?.(submitted);
|
|
512
|
+
},
|
|
513
|
+
// The pinned contract and gas, never the quote's word.
|
|
514
|
+
write: async ({ chainId, calldata, amount }) => {
|
|
515
|
+
// An adopted proposal is already with the Safe: never "not sent".
|
|
516
|
+
invoked = true;
|
|
517
|
+
if (queued)
|
|
518
|
+
return queued.proposalHash;
|
|
519
|
+
try {
|
|
520
|
+
return await adapter.sendTransaction({
|
|
521
|
+
chainId,
|
|
522
|
+
to: RELAYR_PAYMENT_ADDRESS,
|
|
523
|
+
data: calldata,
|
|
524
|
+
value: amount,
|
|
525
|
+
gas: safe ? 0n : RELAYR_PAYMENT_GAS,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
// Only the wallet's own 4001 says it declined.
|
|
530
|
+
throw relayrWalletPaymentError(error);
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
}));
|
|
534
|
+
};
|
|
535
|
+
let hash;
|
|
536
|
+
try {
|
|
537
|
+
if (hooks.intent)
|
|
538
|
+
requireIntentClear(deps, account, hooks.intent);
|
|
539
|
+
hash = await attempt();
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
// Saved as sending, refused by a final gate, never given to the wallet.
|
|
543
|
+
if (saved && !invoked && !(error instanceof RelayrPaymentNotSentError)) {
|
|
544
|
+
throw new RelayrPaymentNotSentError(error);
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
// A proposal: its hold follows it to the Safe's execution.
|
|
549
|
+
if (safe)
|
|
550
|
+
return hash;
|
|
551
|
+
const receipt = await waitForTrackedReceipt(client, hash);
|
|
552
|
+
// The hold checks the receipt itself and settles, so a reverted payment can
|
|
553
|
+
// be reviewed and sent again without waiting on a view.
|
|
554
|
+
await handle.state(client).catch(() => undefined);
|
|
555
|
+
// A wallet that sped the payment up mined it under another hash.
|
|
556
|
+
const mined = receipt.transactionHash;
|
|
557
|
+
if (mined.toLowerCase() !== hash.toLowerCase()) {
|
|
558
|
+
try {
|
|
559
|
+
await hooks.onSent?.(mined);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
throw new SubmittedContractWriteError(mined, error);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (receipt.status === "reverted")
|
|
566
|
+
throw new ReviewedCallRevertedError(0, mined, receipt);
|
|
567
|
+
await verifyRelayrPayment(client, {
|
|
568
|
+
hash: mined,
|
|
569
|
+
from: account,
|
|
570
|
+
payment: details,
|
|
571
|
+
});
|
|
572
|
+
return mined;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Signs `calls` as ERC-2771 forward requests after one review of the
|
|
576
|
+
* requests, decoded, and returns the Relayr bundle entries that run them.
|
|
577
|
+
* Each destination must trust the chain's canonical forwarder. Nothing is
|
|
578
|
+
* posted or paid.
|
|
579
|
+
*/
|
|
580
|
+
export async function signReviewedForwardRequests(calls, action, deps) {
|
|
581
|
+
const { account } = action;
|
|
582
|
+
await requireOrdinaryWallet(deps.adapter, account);
|
|
583
|
+
const requests = await forwardRequests(calls, account, deps.clientFor);
|
|
584
|
+
const prepared = requests.map(({ chainId, request }) => prepareSignature({ kind: "forward-request", chainId, request }, account));
|
|
585
|
+
const model = buildReviewModel({
|
|
586
|
+
calls: [],
|
|
587
|
+
signatures: prepared.map(({ review }) => review),
|
|
588
|
+
title: action.title ?? "Review relayed transactions",
|
|
589
|
+
...(action.context ? { context: action.context } : {}),
|
|
590
|
+
});
|
|
591
|
+
await promptReview(model, Promise.resolve({ status: "passed" }), Promise.resolve(model));
|
|
592
|
+
const signatures = await signPrepared(prepared, account, deps.adapter);
|
|
593
|
+
return requests.map(({ chainId, request }, index) => entryOf(chainId, request, signatures[index]));
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Pays a Relayr quote after one review of the payment: the option on
|
|
597
|
+
* `chainId`, authenticated against the quote, sent to Relayr's pinned
|
|
598
|
+
* contract and proven from the chain. For a payment resumed apart from its
|
|
599
|
+
* signatures. A Safe app pays by proposal: `safeTxHash` comes back, held
|
|
600
|
+
* until the Safe runs it. `onSending` runs after the hold reserves, right
|
|
601
|
+
* before the wallet opens (a throw sends nothing); `onSent` gets every hash
|
|
602
|
+
* the payment is known by. With `plan` and `relayrRound`, a quote of exactly
|
|
603
|
+
* that round's calls spends the round's reviewed fee bound once, as
|
|
604
|
+
* {@link runRelayrReviewed} does: at most the bound pays with no prompt, and
|
|
605
|
+
* a higher fee, a spent round or another chain is asked.
|
|
606
|
+
*/
|
|
607
|
+
export async function payRelayrReviewed(quote, chainId, action, deps) {
|
|
608
|
+
const { account } = action;
|
|
609
|
+
await walletReady(deps.adapter);
|
|
610
|
+
const safe = deps.adapter.isSafe();
|
|
611
|
+
await requireOrdinaryWallet(deps.adapter, account, safe);
|
|
612
|
+
const details = paymentOn(quote, chainId);
|
|
613
|
+
const intent = {
|
|
614
|
+
key: `relayr:${details.bundleUuid}`,
|
|
615
|
+
funding: relayrPaymentChains(quote.expectedTransactions.map((binding) => binding.chain)),
|
|
616
|
+
paidOnce: true,
|
|
617
|
+
};
|
|
618
|
+
requireIntentClear(deps, account, intent);
|
|
619
|
+
if (action.plan && action.relayrRound !== undefined) {
|
|
620
|
+
const cap = spendPlannedRelayrRound(action.plan, account, action.relayrRound, quotedCalls(quote), details.chainId);
|
|
621
|
+
if (cap === undefined || details.amount > cap)
|
|
622
|
+
await reviewPayment(details, account, {
|
|
623
|
+
title: cap ? "The network fee went up" : "Review network fee",
|
|
624
|
+
...(action.context ? { context: action.context } : {}),
|
|
625
|
+
}, safe);
|
|
626
|
+
}
|
|
627
|
+
else if (action.plan)
|
|
628
|
+
spendPlan(action.plan, account, [
|
|
629
|
+
{
|
|
630
|
+
chainId: details.chainId,
|
|
631
|
+
from: account,
|
|
632
|
+
to: RELAYR_PAYMENT_ADDRESS,
|
|
633
|
+
data: details.calldata,
|
|
634
|
+
value: details.amount,
|
|
635
|
+
},
|
|
636
|
+
]);
|
|
637
|
+
else
|
|
638
|
+
await reviewPayment(details, account, action, safe);
|
|
639
|
+
const hash = await sendPayment(details, account, deps, {
|
|
640
|
+
intent,
|
|
641
|
+
// Relayr's own word too, whatever this browser kept of earlier payments.
|
|
642
|
+
recheck: () => requireRelayrBundleUnpaid(details.bundleUuid),
|
|
643
|
+
...(action.onSending ? { onSending: action.onSending } : {}),
|
|
644
|
+
...(action.onSent ? { onSent: action.onSent } : {}),
|
|
645
|
+
safe,
|
|
646
|
+
});
|
|
647
|
+
return safe ? { safeTxHash: hash } : { hash };
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Runs `calls` through Relayr under one review. Before the review it quotes
|
|
651
|
+
* stand-ins of the calls ({@link relayrFeeCap}), and the review shows the
|
|
652
|
+
* forward requests, decoded, with the fee cap as a row; a `raw` call shows as
|
|
653
|
+
* sent by Relayr, unsigned. After confirm it signs the requests, posts them
|
|
654
|
+
* for the quote that binds them, and pays it without asking again when its
|
|
655
|
+
* fee is at most the cap. A higher fee opens one fresh review of the payment
|
|
656
|
+
* first. A quote that expires before the wallet opens is replaced the same
|
|
657
|
+
* way. `scope`, the site's stable key for the action, is required: it holds
|
|
658
|
+
* the payment on every funding chain, so the action is never paid twice,
|
|
659
|
+
* even after a paid run. With `plan` (`reviewPlan`), the calls are spent from
|
|
660
|
+
* it instead of reviewed, and the payment is reviewed on its own.
|
|
661
|
+
*/
|
|
662
|
+
export async function runRelayrReviewed(calls, action, deps) {
|
|
663
|
+
const { adapter, clientFor } = deps;
|
|
664
|
+
const { account, scope, plan } = action;
|
|
665
|
+
if (typeof scope !== "string" || !scope.trim()) {
|
|
666
|
+
throw new Error("A relayed action needs its scope. Nothing was sent.");
|
|
667
|
+
}
|
|
668
|
+
await requireOrdinaryWallet(adapter, account);
|
|
669
|
+
/** Each call as reviewed, read once. */
|
|
670
|
+
const read = [...calls].map(({ chainId, target, data, value, gas, raw }) => ({
|
|
671
|
+
chainId,
|
|
672
|
+
target,
|
|
673
|
+
data,
|
|
674
|
+
value: value ?? 0n,
|
|
675
|
+
gas,
|
|
676
|
+
raw: raw === true,
|
|
677
|
+
}));
|
|
678
|
+
const chainIds = read.map(({ chainId }) => chainId);
|
|
679
|
+
if (!relayrSupportsChains(chainIds)) {
|
|
680
|
+
throw new Error("Relayr runs one call per supported destination, all mainnets or all testnets.");
|
|
681
|
+
}
|
|
682
|
+
// Relayr's executor is not known before it quotes, so a raw call can't be simulated carrying ETH.
|
|
683
|
+
if (read.some(({ raw, value }) => raw && value !== 0n))
|
|
684
|
+
throw new Error("A call Relayr sends on its own can't carry ETH.");
|
|
685
|
+
const forwarded = read.filter(({ raw }) => !raw);
|
|
686
|
+
const requests = forwarded.length
|
|
687
|
+
? await forwardRequests(forwarded, account, clientFor)
|
|
688
|
+
: [];
|
|
689
|
+
const funding = relayrPaymentChains(chainIds);
|
|
690
|
+
const walletChain = adapter.chainId();
|
|
691
|
+
/** What the plan holds for these calls: forward requests by their call and gas, raw calls as Relayr's. */
|
|
692
|
+
const planned = read.map(({ chainId, target, data, value, gas, raw }) => ({
|
|
693
|
+
chainId,
|
|
694
|
+
from: raw ? "relayr" : account,
|
|
695
|
+
to: target,
|
|
696
|
+
data,
|
|
697
|
+
value,
|
|
698
|
+
...(raw ? {} : { gas }),
|
|
699
|
+
}));
|
|
700
|
+
const paymentChainId = action.paymentChainId ??
|
|
701
|
+
// Under a plan, the chain its fee bound was reviewed for.
|
|
702
|
+
(plan && plannedRelayrFee(plan, planned)?.chainId) ??
|
|
703
|
+
(walletChain !== undefined && funding.includes(walletChain)
|
|
704
|
+
? walletChain
|
|
705
|
+
: chainIds[0]);
|
|
706
|
+
if (!funding.includes(paymentChainId)) {
|
|
707
|
+
throw new Error("Choose a Relayr funding chain in the same network family as these destinations.");
|
|
708
|
+
}
|
|
709
|
+
const intent = { key: scope, funding, paidOnce: true };
|
|
710
|
+
// An earlier payment of this action may still land, or landed: nothing is asked.
|
|
711
|
+
requireIntentClear(deps, account, intent);
|
|
712
|
+
const signatures = [
|
|
713
|
+
...requests.map(({ chainId, request }) => ({
|
|
714
|
+
kind: "forward-request",
|
|
715
|
+
chainId,
|
|
716
|
+
request,
|
|
717
|
+
})),
|
|
718
|
+
...(action.signatures ?? []),
|
|
719
|
+
];
|
|
720
|
+
const prepared = signatures.map((signature) => prepareSignature(signature, account));
|
|
721
|
+
/** The fee the person reviewed, if any: a payment above it, or with none, is asked. */
|
|
722
|
+
let cap;
|
|
723
|
+
if (plan) {
|
|
724
|
+
// The round's fee bound its review showed, or none: then the payment is asked.
|
|
725
|
+
cap = spendPlan(plan, account, [
|
|
726
|
+
...planned.filter(({ from }) => from === "relayr"),
|
|
727
|
+
...prepared.flatMap(plannedCallsOf),
|
|
728
|
+
], paymentChainId);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
cap = await relayrFeeCap(read, account, paymentChainId, clientFor);
|
|
732
|
+
const model = buildReviewModel({
|
|
733
|
+
calls: read.flatMap(({ chainId, target, data, value, gas, raw }) => raw
|
|
734
|
+
? [
|
|
735
|
+
{
|
|
736
|
+
...decodeReviewCall({ chainId, to: target, data, value, gas }),
|
|
737
|
+
label: "Sent by Relayr",
|
|
738
|
+
},
|
|
739
|
+
]
|
|
740
|
+
: []),
|
|
741
|
+
signatures: prepared.map(({ review }) => review),
|
|
742
|
+
title: action.title ?? "Review relayed transactions",
|
|
743
|
+
context: [
|
|
744
|
+
...(action.context ?? []),
|
|
745
|
+
{
|
|
746
|
+
label: "Network fee",
|
|
747
|
+
value: `Up to ${formatEther(cap)} ETH`,
|
|
748
|
+
strong: true,
|
|
749
|
+
},
|
|
750
|
+
{ label: "Fee paid on", value: `Chain ${paymentChainId}` },
|
|
751
|
+
{ label: "Fee paid to", value: RELAYR_PAYMENT_ADDRESS, mono: true },
|
|
752
|
+
],
|
|
753
|
+
});
|
|
754
|
+
await promptReview(model, Promise.resolve({ status: "passed" }), Promise.resolve(model));
|
|
755
|
+
}
|
|
756
|
+
const signed = await signPrepared(prepared, account, adapter);
|
|
757
|
+
let next = 0;
|
|
758
|
+
const entries = read.map(({ chainId, target, data, value, raw }) => {
|
|
759
|
+
if (raw)
|
|
760
|
+
return { chain: chainId, target, data, value: value.toString() };
|
|
761
|
+
const { request } = requests[next];
|
|
762
|
+
return entryOf(chainId, request, signed[next++]);
|
|
763
|
+
});
|
|
764
|
+
const extra = signed.slice(requests.length);
|
|
765
|
+
/** Every call still as reviewed, each signed entry still valid, each raw call still running. */
|
|
766
|
+
const recheck = async () => {
|
|
767
|
+
const now = [...calls];
|
|
768
|
+
if (now.length !== read.length ||
|
|
769
|
+
now.some((call, index) => call.chainId !== read[index].chainId ||
|
|
770
|
+
call.target.toLowerCase() !== read[index].target.toLowerCase() ||
|
|
771
|
+
call.data.toLowerCase() !== read[index].data.toLowerCase() ||
|
|
772
|
+
(call.value ?? 0n) !== read[index].value ||
|
|
773
|
+
(call.raw === true) !== read[index].raw))
|
|
774
|
+
throw new ReviewLineError("Transaction data changed after review. Nothing was sent; review it again.");
|
|
775
|
+
await verifyForwardedEntries(entries.filter((_entry, index) => !read[index].raw), account, clientFor);
|
|
776
|
+
for (const { chainId, target, data, value, raw } of read)
|
|
777
|
+
if (raw)
|
|
778
|
+
await simulateStateChangingTransaction(clientFor(chainId), {
|
|
779
|
+
from: relayrStandInSender(),
|
|
780
|
+
to: target,
|
|
781
|
+
data,
|
|
782
|
+
value,
|
|
783
|
+
});
|
|
784
|
+
};
|
|
785
|
+
await recheck();
|
|
786
|
+
for (let attempt = 1;; attempt += 1) {
|
|
787
|
+
try {
|
|
788
|
+
const quote = await postBundle(entries);
|
|
789
|
+
const details = paymentOn(quote, paymentChainId);
|
|
790
|
+
if (cap === undefined || details.amount > cap) {
|
|
791
|
+
await reviewPayment(details, account, {
|
|
792
|
+
title: cap ? "The network fee went up" : "Review network fee",
|
|
793
|
+
...(action.context ? { context: action.context } : {}),
|
|
794
|
+
});
|
|
795
|
+
cap = details.amount;
|
|
796
|
+
}
|
|
797
|
+
const paymentHash = await sendPayment(details, account, deps, {
|
|
798
|
+
intent,
|
|
799
|
+
recheck,
|
|
800
|
+
});
|
|
801
|
+
return {
|
|
802
|
+
uuid: quote.bundle_uuid,
|
|
803
|
+
paymentHash,
|
|
804
|
+
...(extra.length ? { signatures: extra } : {}),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
const expired = error instanceof QuoteExpiredError ||
|
|
809
|
+
(error instanceof RelayrPaymentNotSentError &&
|
|
810
|
+
error.cause instanceof QuoteExpiredError);
|
|
811
|
+
if (!expired || attempt >= QUOTE_ATTEMPTS)
|
|
812
|
+
throw error;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Relayr's bundle once every one of its `expectedCount` transactions reports
|
|
818
|
+
* success. Only `failed`, in any case, is final; every other state, and a
|
|
819
|
+
* read that fails, is polled again every 2.5 seconds, for up to 5 minutes.
|
|
820
|
+
* Relayr's word is not proof: verify the destinations from the chain.
|
|
821
|
+
*/
|
|
822
|
+
export async function pollRelayrBundle(uuid, expectedCount, options = {}) {
|
|
823
|
+
if (!Number.isSafeInteger(expectedCount) || expectedCount < 1) {
|
|
824
|
+
throw new Error("Relayr polling requires the exact destination count.");
|
|
825
|
+
}
|
|
826
|
+
const { signal } = options;
|
|
827
|
+
const until = Date.now() + POLL_TIMEOUT_MS;
|
|
828
|
+
for (;;) {
|
|
829
|
+
signal?.throwIfAborted();
|
|
830
|
+
const bundle = await readRelayrBundle(uuid, {
|
|
831
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
832
|
+
}).catch(() => null);
|
|
833
|
+
const records = Array.isArray(bundle?.transactions)
|
|
834
|
+
? bundle.transactions
|
|
835
|
+
: [];
|
|
836
|
+
const failed = records.filter((record) => relayrStateIsFailed(record?.status?.state)).length;
|
|
837
|
+
if (failed) {
|
|
838
|
+
throw new Error(`Relayr could not run this on ${failed} chain${failed === 1 ? "" : "s"}.`);
|
|
839
|
+
}
|
|
840
|
+
if (bundle &&
|
|
841
|
+
records.length === expectedCount &&
|
|
842
|
+
records.every((record) => relayrStateIsSuccess(record?.status?.state))) {
|
|
843
|
+
return bundle;
|
|
844
|
+
}
|
|
845
|
+
if (Date.now() >= until) {
|
|
846
|
+
throw new Error(`Relayr is still processing paid bundle ${uuid}. Do not submit this action again; check it later.`);
|
|
847
|
+
}
|
|
848
|
+
await new Promise((resolve, reject) => {
|
|
849
|
+
const timer = setTimeout(resolve, POLL_INTERVAL_MS);
|
|
850
|
+
signal?.addEventListener("abort", () => {
|
|
851
|
+
clearTimeout(timer);
|
|
852
|
+
reject(signal.reason);
|
|
853
|
+
}, { once: true });
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/** A Safe execution as the review shows it: the Safe's `execTransaction`, with the call or MultiSend inside. */
|
|
858
|
+
function safeExecutionCall(entry) {
|
|
859
|
+
const decoded = decodeFunctionData({ abi: SAFE_EXEC_ABI, data: entry.data });
|
|
860
|
+
if (decoded.functionName !== "execTransaction") {
|
|
861
|
+
throw new Error("The saved call is not a Safe execution.");
|
|
862
|
+
}
|
|
863
|
+
const [to, value, data, operation] = decoded.args;
|
|
864
|
+
const inner = multiSendCallsOf({ to, data, operation }) ?? [
|
|
865
|
+
{ to, value, data },
|
|
866
|
+
];
|
|
867
|
+
return {
|
|
868
|
+
chainId: entry.chain,
|
|
869
|
+
to: entry.target,
|
|
870
|
+
data: entry.data,
|
|
871
|
+
value: BigInt(entry.value),
|
|
872
|
+
abi: SAFE_EXEC_ABI,
|
|
873
|
+
functionName: "execTransaction",
|
|
874
|
+
args: decoded.args,
|
|
875
|
+
contractName: "Safe",
|
|
876
|
+
calls: inner.map((call) => decodeReviewCall({ chainId: entry.chain, from: entry.target, ...call })),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* The wallet side of {@link createSafeRelayrController}: `review` shows the
|
|
881
|
+
* frozen Safe executions in core's review, and `sendPayment` reviews and
|
|
882
|
+
* sends the selected payment as {@link payRelayrReviewed} does, with the
|
|
883
|
+
* controller's checks and journal callbacks around the wallet. A quote paid
|
|
884
|
+
* before is paid again only when {@link requireRelayrRetry} clears every
|
|
885
|
+
* earlier payment.
|
|
886
|
+
*/
|
|
887
|
+
export function safeRelayrWalletDeps(deps) {
|
|
888
|
+
return {
|
|
889
|
+
review: async (executions, { resumed }) => {
|
|
890
|
+
const model = buildReviewModel({
|
|
891
|
+
calls: executions.map(({ entry }) => safeExecutionCall(entry)),
|
|
892
|
+
title: `Review ${executions.length} Safe execution${executions.length === 1 ? "" : "s"}`,
|
|
893
|
+
confirmLabel: resumed
|
|
894
|
+
? "Use saved quote"
|
|
895
|
+
: "Review & get payment options",
|
|
896
|
+
});
|
|
897
|
+
await promptReview(model, Promise.resolve({ status: "passed" }), Promise.resolve(model));
|
|
898
|
+
},
|
|
899
|
+
sendPayment: async ({ session, payment, beforeSend, onSending, onSent, }) => {
|
|
900
|
+
const { account } = session;
|
|
901
|
+
await requireOrdinaryWallet(deps.adapter, account);
|
|
902
|
+
if (session.payments.length >= MAX_RELAYR_SENT_PAYMENTS) {
|
|
903
|
+
throw new Error("This Relayr quote was paid too many times to pay again. Keep it pending; do not pay again.");
|
|
904
|
+
}
|
|
905
|
+
if (!session.quote)
|
|
906
|
+
throw new Error("This Safe action has no quote to pay.");
|
|
907
|
+
const details = relayrPaymentDetails(payment, {
|
|
908
|
+
bundleUuid: session.quote.bundle_uuid,
|
|
909
|
+
destinationChainIds: session.executions.map(({ entry }) => entry.chain),
|
|
910
|
+
});
|
|
911
|
+
await reviewPayment(details, account, {});
|
|
912
|
+
const payments = [...session.payments];
|
|
913
|
+
const hash = await sendPayment(details, account, deps, {
|
|
914
|
+
recheck: async () => {
|
|
915
|
+
await beforeSend();
|
|
916
|
+
if (session.payments.length) {
|
|
917
|
+
await requireRelayrRetry(deps.clientFor, {
|
|
918
|
+
payments: session.payments,
|
|
919
|
+
from: account,
|
|
920
|
+
bundleUuid: details.bundleUuid,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
onSending,
|
|
925
|
+
onSent: async (sent) => {
|
|
926
|
+
payments.push(sentRelayrPayment(details, sent));
|
|
927
|
+
await onSent([...payments]);
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
return { hash, payments };
|
|
931
|
+
},
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
//# sourceMappingURL=relayrSend.js.map
|