@bananapus/nana-sdk-core 3.5.2 → 5.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 +495 -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 +56 -0
- package/dist/cjs/review/confirm.d.ts.map +1 -0
- package/dist/cjs/review/confirm.js +99 -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 +10 -8
- 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 +4 -0
- package/dist/cjs/review/heldWrite.d.ts.map +1 -1
- package/dist/cjs/review/heldWrite.js +35 -31
- package/dist/cjs/review/heldWrite.js.map +1 -1
- package/dist/cjs/review/index.d.ts +12 -1
- package/dist/cjs/review/index.d.ts.map +1 -1
- package/dist/cjs/review/index.js +29 -1
- 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 +72 -0
- package/dist/cjs/review/lineError.d.ts.map +1 -0
- package/dist/cjs/review/lineError.js +76 -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 +61 -0
- package/dist/cjs/review/plan.d.ts.map +1 -0
- package/dist/cjs/review/plan.js +143 -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/useHeldSend.js +3 -4
- package/dist/cjs/review/react/useHeldSend.js.map +1 -1
- package/dist/cjs/review/react/useHeldWrite.d.ts.map +1 -1
- package/dist/cjs/review/react/useHeldWrite.js +2 -1
- package/dist/cjs/review/react/useHeldWrite.js.map +1 -1
- package/dist/cjs/review/react/useReviewedRun.d.ts +122 -0
- package/dist/cjs/review/react/useReviewedRun.d.ts.map +1 -0
- package/dist/cjs/review/react/useReviewedRun.js +443 -0
- package/dist/cjs/review/react/useReviewedRun.js.map +1 -0
- package/dist/cjs/review/receipt.d.ts +37 -1
- package/dist/cjs/review/receipt.d.ts.map +1 -1
- package/dist/cjs/review/receipt.js +76 -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 +52 -8
- package/dist/cjs/review/relayr.js.map +1 -1
- package/dist/cjs/review/relayrSend.d.ts +106 -0
- package/dist/cjs/review/relayrSend.d.ts.map +1 -0
- package/dist/cjs/review/relayrSend.js +795 -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 +9 -9
- 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 +306 -0
- package/dist/cjs/review/safeSend.js.map +1 -0
- package/dist/cjs/review/send.d.ts +272 -0
- package/dist/cjs/review/send.d.ts.map +1 -0
- package/dist/cjs/review/send.js +1163 -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 +111 -0
- package/dist/cjs/review/sign.d.ts.map +1 -0
- package/dist/cjs/review/sign.js +378 -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 +98 -0
- package/dist/cjs/review/wallet.d.ts.map +1 -0
- package/dist/cjs/review/wallet.js +234 -0
- package/dist/cjs/review/wallet.js.map +1 -0
- package/dist/cjs/review/writeHold.d.ts +138 -13
- package/dist/cjs/review/writeHold.d.ts.map +1 -1
- package/dist/cjs/review/writeHold.js +98 -18
- package/dist/cjs/review/writeHold.js.map +1 -1
- package/dist/cjs/review/writeRecovery.d.ts +17 -1
- package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
- package/dist/cjs/review/writeRecovery.js +146 -45
- 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 +56 -0
- package/dist/esm/review/confirm.d.ts.map +1 -0
- package/dist/esm/review/confirm.js +93 -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 +10 -8
- 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 +4 -0
- package/dist/esm/review/heldWrite.d.ts.map +1 -1
- package/dist/esm/review/heldWrite.js +34 -30
- package/dist/esm/review/heldWrite.js.map +1 -1
- package/dist/esm/review/index.d.ts +12 -1
- package/dist/esm/review/index.d.ts.map +1 -1
- package/dist/esm/review/index.js +12 -1
- 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 +72 -0
- package/dist/esm/review/lineError.d.ts.map +1 -0
- package/dist/esm/review/lineError.js +68 -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 +61 -0
- package/dist/esm/review/plan.d.ts.map +1 -0
- package/dist/esm/review/plan.js +136 -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/useHeldSend.js +3 -4
- package/dist/esm/review/react/useHeldSend.js.map +1 -1
- package/dist/esm/review/react/useHeldWrite.d.ts.map +1 -1
- package/dist/esm/review/react/useHeldWrite.js +2 -1
- package/dist/esm/review/react/useHeldWrite.js.map +1 -1
- package/dist/esm/review/react/useReviewedRun.d.ts +122 -0
- package/dist/esm/review/react/useReviewedRun.d.ts.map +1 -0
- package/dist/esm/review/react/useReviewedRun.js +439 -0
- package/dist/esm/review/react/useReviewedRun.js.map +1 -0
- package/dist/esm/review/receipt.d.ts +37 -1
- package/dist/esm/review/receipt.d.ts.map +1 -1
- package/dist/esm/review/receipt.js +72 -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 +51 -8
- package/dist/esm/review/relayr.js.map +1 -1
- package/dist/esm/review/relayrSend.d.ts +106 -0
- package/dist/esm/review/relayrSend.d.ts.map +1 -0
- package/dist/esm/review/relayrSend.js +787 -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 +9 -9
- 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 +301 -0
- package/dist/esm/review/safeSend.js.map +1 -0
- package/dist/esm/review/send.d.ts +272 -0
- package/dist/esm/review/send.d.ts.map +1 -0
- package/dist/esm/review/send.js +1156 -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 +111 -0
- package/dist/esm/review/sign.d.ts.map +1 -0
- package/dist/esm/review/sign.js +372 -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 +98 -0
- package/dist/esm/review/wallet.d.ts.map +1 -0
- package/dist/esm/review/wallet.js +227 -0
- package/dist/esm/review/wallet.js.map +1 -0
- package/dist/esm/review/writeHold.d.ts +138 -13
- package/dist/esm/review/writeHold.d.ts.map +1 -1
- package/dist/esm/review/writeHold.js +96 -19
- package/dist/esm/review/writeHold.js.map +1 -1
- package/dist/esm/review/writeRecovery.d.ts +17 -1
- package/dist/esm/review/writeRecovery.d.ts.map +1 -1
- package/dist/esm/review/writeRecovery.js +149 -48
- 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,1156 @@
|
|
|
1
|
+
import { ReviewedCallRevertedError, ReviewedReceiptUnverifiedError, ReviewLineError, WalletNotInvokedError, } from "./lineError.js";
|
|
2
|
+
import { encodeFunctionData, formatEther, } from "viem";
|
|
3
|
+
import { failedSimulation, promptReview, } from "./confirm.js";
|
|
4
|
+
import { isDefiniteWalletRejection, SubmittedContractWriteError, submitReviewedContractWrite, } from "./contractWrite.js";
|
|
5
|
+
import { JB_CHAINS } from "../constants.js";
|
|
6
|
+
import { gasWithHeadroom } from "./gas.js";
|
|
7
|
+
import { FOLLOW_MS, followHeldWrite, RECEIPT_RETRY_MS } from "./heldWrite.js";
|
|
8
|
+
import { TransactionReviewCancelledError } from "./reviewQueue.js";
|
|
9
|
+
import { pause } from "../pause.js";
|
|
10
|
+
import { findPendingSafeAppProposal, hasSafeService, isSafeExecTransaction, } from "../safeService.js";
|
|
11
|
+
import { REVIEWED_ACCOUNT_CHANGED_LINE, WRITE_HOLD_CALL_FAILED_LINE, WRITE_HOLD_SAFE_UNCONFIRMED_LINE, } from "./messages.js";
|
|
12
|
+
import { buildReviewModel, enrichReviewModel, } from "./model.js";
|
|
13
|
+
import { mintPlan, relayrStandInSender, REVIEWED_PLAN_REFUSED_LINE, spendPlan, } from "./plan.js";
|
|
14
|
+
import { heldBlock, heldReceipt, tell, waitForTrackedReceipt, } from "./receipt.js";
|
|
15
|
+
import { relayrPaymentChains } from "./relayr.js";
|
|
16
|
+
import { relayrFeeCap } from "./relayrSend.js";
|
|
17
|
+
import { assertSavableCalls, readReviewedSequence, removeReviewedSequence, REVIEWED_SEQUENCE_MOVED_LINE, REVIEWED_SEQUENCE_UNCERTAIN_LINE, REVIEWED_SEQUENCE_UNFINISHED_LINE, saveReviewedSequence, } from "./sequence.js";
|
|
18
|
+
import { plannedCallsOf, prepareSignature, signPrepared, } from "./sign.js";
|
|
19
|
+
import { isRevert, REVERT_UNKNOWN_LINE, revertDetail, revertReason, simulateCallSequence, simulateStateChangingTransaction, simulationFailure, } from "./simulation.js";
|
|
20
|
+
import { unsupportedWalletRequest, walletReady, } from "./wallet.js";
|
|
21
|
+
import { openStepHold, safeBatchCall, WriteHeldError, WriteHoldStaleReviewError, WriteHoldUnacknowledgedSuccessError, } from "./writeHold.js";
|
|
22
|
+
import { createHeldWriteStorage, } from "./writeRecovery.js";
|
|
23
|
+
const CHANGED = "Transaction data changed after review. Nothing was sent; review it again.";
|
|
24
|
+
const MISMATCHED = "This call's data does not match its arguments. Nothing was sent.";
|
|
25
|
+
const BAD_GAS = "This call's gas limit is not valid. Nothing was sent.";
|
|
26
|
+
const BAD_ADOPT = "This call's adoption rule is not valid. Nothing was sent.";
|
|
27
|
+
const ACCOUNT_CHANGED = REVIEWED_ACCOUNT_CHANGED_LINE;
|
|
28
|
+
const NOT_SAVED = "This action's progress could not be saved. Nothing was sent; review it again.";
|
|
29
|
+
export const SAFE_BATCH_UNSUPPORTED_LINE = "This Safe connection can't propose these calls as one transaction. Nothing was sent.";
|
|
30
|
+
export const SAFE_BATCH_UNFOLLOWABLE_LINE = "Your Safe took this batch, but it can't be followed from here. Check your Safe before sending again.";
|
|
31
|
+
const SIMULATION_UNAVAILABLE = "Simulation unavailable. This could not be checked before you confirm; it is checked again before your wallet opens.";
|
|
32
|
+
/**
|
|
33
|
+
* `call` as it encodes now, each field read once, so a getter cannot show one
|
|
34
|
+
* value and send another. Refuses `data` that is not its arguments' encoding.
|
|
35
|
+
*/
|
|
36
|
+
function exact(call) {
|
|
37
|
+
const { chainId, to, data, value, gas, abi, functionName, args } = call;
|
|
38
|
+
const { label, contractName } = call;
|
|
39
|
+
const ignoring = call.adoptIgnoringArgs;
|
|
40
|
+
if (gas !== undefined && (typeof gas !== "bigint" || gas <= 0n)) {
|
|
41
|
+
throw new ReviewLineError(BAD_GAS);
|
|
42
|
+
}
|
|
43
|
+
if (ignoring !== undefined && !Array.isArray(ignoring)) {
|
|
44
|
+
throw new ReviewLineError(BAD_ADOPT);
|
|
45
|
+
}
|
|
46
|
+
const adoptIgnoringArgs = ignoring && Object.freeze([...ignoring]);
|
|
47
|
+
if (adoptIgnoringArgs &&
|
|
48
|
+
(!abi ||
|
|
49
|
+
!functionName ||
|
|
50
|
+
new Set(adoptIgnoringArgs).size !== adoptIgnoringArgs.length ||
|
|
51
|
+
adoptIgnoringArgs.some((index) => !Number.isInteger(index) || index < 0 || index >= (args?.length ?? 0)))) {
|
|
52
|
+
throw new ReviewLineError(BAD_ADOPT);
|
|
53
|
+
}
|
|
54
|
+
if (abi && functionName) {
|
|
55
|
+
const encoded = encodeFunctionData({
|
|
56
|
+
abi,
|
|
57
|
+
functionName,
|
|
58
|
+
args: args ?? [],
|
|
59
|
+
});
|
|
60
|
+
if (encoded.toLowerCase() !== data.toLowerCase()) {
|
|
61
|
+
throw new ReviewLineError(MISMATCHED);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
sent: Object.freeze({
|
|
66
|
+
chainId,
|
|
67
|
+
to,
|
|
68
|
+
data,
|
|
69
|
+
value: value ?? 0n,
|
|
70
|
+
...(gas === undefined ? {} : { gas }),
|
|
71
|
+
}),
|
|
72
|
+
shown: {
|
|
73
|
+
abi,
|
|
74
|
+
functionName,
|
|
75
|
+
args,
|
|
76
|
+
label,
|
|
77
|
+
contractName,
|
|
78
|
+
...(adoptIgnoringArgs ? { adoptIgnoringArgs } : {}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** What a Safe app may adopt a queued proposal by, as reviewed. */
|
|
83
|
+
function adopting({ abi, adoptIgnoringArgs, }) {
|
|
84
|
+
return abi && adoptIgnoringArgs
|
|
85
|
+
? { abi, ignoring: adoptIgnoringArgs }
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
function sameCall(a, b) {
|
|
89
|
+
return (a.chainId === b.chainId &&
|
|
90
|
+
a.to.toLowerCase() === b.to.toLowerCase() &&
|
|
91
|
+
a.data.toLowerCase() === b.data.toLowerCase() &&
|
|
92
|
+
a.value === b.value &&
|
|
93
|
+
a.gas === b.gas);
|
|
94
|
+
}
|
|
95
|
+
/** The chain's name in a line the person reads, never only its id when core knows it. */
|
|
96
|
+
function chainNameOf(client, chainId) {
|
|
97
|
+
return (client.chain?.name ??
|
|
98
|
+
JB_CHAINS[chainId]?.name ??
|
|
99
|
+
`chain ${chainId}`);
|
|
100
|
+
}
|
|
101
|
+
/** A call as the review shows it, from `from` (none: Relayr's own account runs it). */
|
|
102
|
+
function shownCall({ sent: { gas, ...sent }, shown }, from, safe) {
|
|
103
|
+
return {
|
|
104
|
+
...sent,
|
|
105
|
+
...(from ? { from } : {}),
|
|
106
|
+
// A Safe app signs safeTxGas 0, never the app's gas.
|
|
107
|
+
...(safe ? { safeTxGas: 0n } : gas === undefined ? {} : { gas }),
|
|
108
|
+
...(shown.abi ? { abi: shown.abi } : {}),
|
|
109
|
+
...(shown.functionName ? { functionName: shown.functionName } : {}),
|
|
110
|
+
...(shown.args ? { args: shown.args } : {}),
|
|
111
|
+
...(shown.label
|
|
112
|
+
? { label: shown.label }
|
|
113
|
+
: from
|
|
114
|
+
? {}
|
|
115
|
+
: { label: "Sent by Relayr" }),
|
|
116
|
+
...(shown.contractName ? { contractName: shown.contractName } : {}),
|
|
117
|
+
...(shown.adoptIgnoringArgs
|
|
118
|
+
? { adoptIgnoringArgs: shown.adoptIgnoringArgs }
|
|
119
|
+
: {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function labelOf(call, index) {
|
|
123
|
+
return call.label ?? call.functionName ?? `Step ${index + 1}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The latest block, or `minimum` while the node has not reached it: never
|
|
127
|
+
* state from before a prerequisite took effect, never stale state after it.
|
|
128
|
+
*/
|
|
129
|
+
async function notBefore(client, minimum) {
|
|
130
|
+
const head = await client.getBlockNumber();
|
|
131
|
+
return head > minimum ? head : minimum;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The review-time simulation of every call, per chain: a sequence through
|
|
135
|
+
* {@link simulateCallSequence}, a lone call through
|
|
136
|
+
* {@link simulateStateChangingTransaction}. A lone call that needs a step on
|
|
137
|
+
* another chain is left to the simulation before its send. It never rejects:
|
|
138
|
+
* a revert is `failed`, anything else `unavailable`.
|
|
139
|
+
*/
|
|
140
|
+
async function reviewSimulation(fromOf, calls, sent, blocks, clientFor,
|
|
141
|
+
/** Steps whose value someone else pays: the sender's balance is topped up by it. */
|
|
142
|
+
funded) {
|
|
143
|
+
const chains = [...new Set(sent.map((call) => call.chainId))];
|
|
144
|
+
const perChain = chains.map(async (chainId) => {
|
|
145
|
+
const steps = sent.flatMap((call, index) => call.chainId === chainId ? [index] : []);
|
|
146
|
+
const paid = steps.reduce((sum, index) => sum + (funded?.(index) ? (sent[index].value ?? 0n) : 0n), 0n);
|
|
147
|
+
let chainName = `chain ${chainId}`;
|
|
148
|
+
try {
|
|
149
|
+
const client = clientFor(chainId);
|
|
150
|
+
chainName = chainNameOf(client, chainId);
|
|
151
|
+
const from = fromOf(steps[0]);
|
|
152
|
+
const balance = paid
|
|
153
|
+
? { balance: (await client.getBalance({ address: from })) + paid }
|
|
154
|
+
: {};
|
|
155
|
+
if (steps.length > 1) {
|
|
156
|
+
// A sequence runs from one sender; mixed senders are checked before each send.
|
|
157
|
+
if (steps.some((index) => fromOf(index) !== from))
|
|
158
|
+
return { status: "unavailable", message: SIMULATION_UNAVAILABLE };
|
|
159
|
+
await simulateCallSequence(client, {
|
|
160
|
+
from,
|
|
161
|
+
chainName,
|
|
162
|
+
calls: steps.map((index) => ({
|
|
163
|
+
to: sent[index].to,
|
|
164
|
+
data: sent[index].data,
|
|
165
|
+
value: sent[index].value,
|
|
166
|
+
gas: sent[index].gas,
|
|
167
|
+
label: labelOf(calls[index], index),
|
|
168
|
+
dependsOnPrior: calls[index].dependsOnPrior,
|
|
169
|
+
funded: funded?.(index) === true,
|
|
170
|
+
})),
|
|
171
|
+
...balance,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else if (!(calls[steps[0]].dependsOnPrior && steps[0] > 0)) {
|
|
175
|
+
const minimum = blocks[steps[0]];
|
|
176
|
+
await simulateStateChangingTransaction(client, {
|
|
177
|
+
from,
|
|
178
|
+
to: sent[steps[0]].to,
|
|
179
|
+
data: sent[steps[0]].data,
|
|
180
|
+
value: sent[steps[0]].value,
|
|
181
|
+
gas: sent[steps[0]].gas,
|
|
182
|
+
...balance,
|
|
183
|
+
...(minimum === undefined
|
|
184
|
+
? {}
|
|
185
|
+
: { blockNumber: await notBefore(client, minimum) }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return { status: "passed" };
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (!isRevert(error))
|
|
192
|
+
return { status: "unavailable", message: SIMULATION_UNAVAILABLE };
|
|
193
|
+
const reason = revertReason(error);
|
|
194
|
+
// A sequence's failure is the SDK's line naming its call.
|
|
195
|
+
const failure = error instanceof ReviewLineError
|
|
196
|
+
? error
|
|
197
|
+
: simulationFailure(`${labelOf(calls[steps[0]], steps[0])} cannot run on ${chainName}: ${reason ?? REVERT_UNKNOWN_LINE}`, error);
|
|
198
|
+
return failedSimulation({
|
|
199
|
+
status: "failed",
|
|
200
|
+
message: failure.message,
|
|
201
|
+
explained: reason !== null,
|
|
202
|
+
}, failure);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// A revert on one chain is reported at once, whatever the others still wait on.
|
|
206
|
+
const failed = new Promise((resolve) => {
|
|
207
|
+
for (const result of perChain) {
|
|
208
|
+
void result.then((settled) => {
|
|
209
|
+
if (settled.status === "failed")
|
|
210
|
+
resolve(settled);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
const all = Promise.all(perChain).then((results) => results.find((result) => result.status === "unavailable") ?? {
|
|
215
|
+
status: "passed",
|
|
216
|
+
});
|
|
217
|
+
return Promise.race([failed, all]);
|
|
218
|
+
}
|
|
219
|
+
function simulationWarning(simulation) {
|
|
220
|
+
if (simulation.status === "failed") {
|
|
221
|
+
return { code: "simulation-failed", text: simulation.message };
|
|
222
|
+
}
|
|
223
|
+
if (simulation.status === "unavailable") {
|
|
224
|
+
return { code: "simulation-unavailable", text: SIMULATION_UNAVAILABLE };
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
/** One prompt over `model`, with the review-time simulation of `run` as its warning. */
|
|
229
|
+
async function promptSimulated(model, fromOf, calls, run, blocks, clientFor, signal, funded) {
|
|
230
|
+
const simulation = reviewSimulation(fromOf, calls, run, blocks, clientFor, funded);
|
|
231
|
+
const enriched = Promise.all([
|
|
232
|
+
enrichReviewModel(model, clientFor).catch(() => model),
|
|
233
|
+
simulation,
|
|
234
|
+
]).then(([read, simulated]) => {
|
|
235
|
+
const warning = simulationWarning(simulated);
|
|
236
|
+
return warning ? { ...read, warnings: [...read.warnings, warning] } : read;
|
|
237
|
+
});
|
|
238
|
+
await promptReview(model, simulation, enriched, signal);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* One review of calls an app then sends one by one, across chains and
|
|
242
|
+
* paths: core's prompt over exactly `action.calls`, simulated as their
|
|
243
|
+
* senders, and nothing else. The plan it returns lets {@link sendReviewed},
|
|
244
|
+
* `signReviewed`, `proposeReviewedSafeTransaction` and `payRelayrReviewed`
|
|
245
|
+
* skip their prompt for a call identical to one of its
|
|
246
|
+
* calls (account, chain, sender, target, data, value and gas: a call's
|
|
247
|
+
* exact gas limit, or a forward request's signed gas, is set before the
|
|
248
|
+
* review), each spent once, for {@link REVIEWED_PLAN_TTL_MS} after confirm.
|
|
249
|
+
* Anything else is refused.
|
|
250
|
+
*/
|
|
251
|
+
export async function reviewPlan(action, deps) {
|
|
252
|
+
const { account } = action;
|
|
253
|
+
const calls = [...action.calls];
|
|
254
|
+
if (!calls.length)
|
|
255
|
+
throw new ReviewLineError("There is no transaction to review.");
|
|
256
|
+
const froms = calls.map(({ from }) => from ?? account);
|
|
257
|
+
const snapshots = calls.map(exact);
|
|
258
|
+
const roundOf = calls.map(({ relayrRound }) => relayrRound);
|
|
259
|
+
const fees = new Map();
|
|
260
|
+
for (const round of new Set(roundOf)) {
|
|
261
|
+
if (round === undefined)
|
|
262
|
+
continue;
|
|
263
|
+
const members = snapshots.flatMap(({ sent }, index) => roundOf[index] === round
|
|
264
|
+
? [{ ...sent, raw: froms[index] === "relayr" }]
|
|
265
|
+
: []);
|
|
266
|
+
if (members.some(({ raw, gas }) => !raw && gas === undefined))
|
|
267
|
+
throw new ReviewLineError("A relayed call needs the gas limit its request signs.");
|
|
268
|
+
const chainId = action.relayrRounds?.[round]?.paymentChainId ?? members[0].chainId;
|
|
269
|
+
if (!relayrPaymentChains(members.map((member) => member.chainId)).includes(chainId))
|
|
270
|
+
throw new ReviewLineError("Choose a Relayr funding chain in the same network family as these destinations.");
|
|
271
|
+
fees.set(round, {
|
|
272
|
+
chainId,
|
|
273
|
+
cap: await relayrFeeCap(members.map(({ chainId, to, data, value, gas, raw }) => ({
|
|
274
|
+
chainId,
|
|
275
|
+
target: to,
|
|
276
|
+
data,
|
|
277
|
+
value,
|
|
278
|
+
gas: gas ?? 0n,
|
|
279
|
+
raw,
|
|
280
|
+
})), account, chainId, deps.clientFor),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const feeRows = [...fees.values()].map(({ chainId, cap }, index) => ({
|
|
284
|
+
label: fees.size > 1 ? `Network fee ${index + 1}` : "Network fee",
|
|
285
|
+
value: `Up to ${formatEther(cap)} ETH on ${chainNameOf(deps.clientFor(chainId), chainId)}`,
|
|
286
|
+
strong: true,
|
|
287
|
+
}));
|
|
288
|
+
const context = [...(action.context ?? []), ...feeRows];
|
|
289
|
+
const model = buildReviewModel({
|
|
290
|
+
calls: snapshots.map((snapshot, index) => shownCall(snapshot, froms[index] === "relayr" ? undefined : froms[index], false)),
|
|
291
|
+
...(action.title ? { title: action.title } : {}),
|
|
292
|
+
...(action.description ? { description: action.description } : {}),
|
|
293
|
+
...(action.confirmLabel ? { confirmLabel: action.confirmLabel } : {}),
|
|
294
|
+
...(context.length ? { context } : {}),
|
|
295
|
+
});
|
|
296
|
+
const relayr = relayrStandInSender();
|
|
297
|
+
await promptSimulated(model, (index) => (froms[index] === "relayr" ? relayr : froms[index]), calls, snapshots.map(({ sent }) => sent), calls.map((call) => call.simulationBlockNumber), deps.clientFor, action.signal,
|
|
298
|
+
// Relayr pays a relayed call's value: its signer needs none of its own.
|
|
299
|
+
(index) => roundOf[index] !== undefined && froms[index] !== "relayr");
|
|
300
|
+
return mintPlan(account, snapshots.map(({ sent: { chainId, to, data, value, gas } }, index) => ({
|
|
301
|
+
chainId,
|
|
302
|
+
from: froms[index],
|
|
303
|
+
to,
|
|
304
|
+
data,
|
|
305
|
+
value,
|
|
306
|
+
// Relayr's own call: its gas bounds only the fee.
|
|
307
|
+
...(gas === undefined || froms[index] === "relayr" ? {} : { gas }),
|
|
308
|
+
...(roundOf[index] === undefined ? {} : { relayrRound: roundOf[index] }),
|
|
309
|
+
})), fees);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Sends one reviewed action: one call, or a sequence (approve then pay, say),
|
|
313
|
+
* reviewed once and sent without asking again.
|
|
314
|
+
*
|
|
315
|
+
* Core builds one model over every call and asks the registered review
|
|
316
|
+
* handler once ({@link registerReviewHandler}); nothing is sent unless the
|
|
317
|
+
* handler returns the confirmation core minted for that model. The
|
|
318
|
+
* review-time simulation runs while the review is open, and confirm never
|
|
319
|
+
* waits on it. Then, for each call in order: `reverify`, a check that the call
|
|
320
|
+
* still encodes to exactly the reviewed bytes, a simulation (at the previous
|
|
321
|
+
* step's receipt block when `dependsOnPrior`), and the send of those bytes
|
|
322
|
+
* through the adapter under the call's write hold, by
|
|
323
|
+
* `submitReviewedContractWrite`. The connected account must stay
|
|
324
|
+
* `action.account`, and the wallet on the call's chain. Each step waits for
|
|
325
|
+
* its receipt; a revert stops the sequence. A Safe-connected wallet's first
|
|
326
|
+
* step becomes a proposal, and the sequence stops there.
|
|
327
|
+
*
|
|
328
|
+
* The action's `signatures` show in the same model and prompt, and are
|
|
329
|
+
* signed after confirm and before the first send, as `signReviewed` signs.
|
|
330
|
+
*
|
|
331
|
+
* Only an explicit wallet rejection (EIP-1193 4001) releases a step's hold;
|
|
332
|
+
* any other failure once the wallet opened keeps it held.
|
|
333
|
+
*
|
|
334
|
+
* A self-reviewing adapter (Signa) is never prompted: `onPreview` gets the
|
|
335
|
+
* model, and the adapter gets the raw calls to review on its own page. An
|
|
336
|
+
* action with `signatures` is refused for it with EIP-1193 4200.
|
|
337
|
+
*/
|
|
338
|
+
export async function sendReviewed(action, deps) {
|
|
339
|
+
action.guard?.();
|
|
340
|
+
await walletReady(deps.adapter);
|
|
341
|
+
return send(action, deps);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Finish a sequence {@link sendReviewed} saved under `record.scope`: the step
|
|
345
|
+
* whose wallet call opened is decided by its write hold first (still held is
|
|
346
|
+
* refused; a settled success counts as completed, its receipt's hash
|
|
347
|
+
* recorded), then every remaining step, and only those, is reviewed once
|
|
348
|
+
* more as one fresh model and sent as `sendReviewed` sends. `reverify` gets
|
|
349
|
+
* each step's original index in the action. Only `record.account` can resume
|
|
350
|
+
* it. The result's `hashes` are every completed step's, `receipts` this
|
|
351
|
+
* run's.
|
|
352
|
+
*/
|
|
353
|
+
export async function resumeReviewed(record, action, deps) {
|
|
354
|
+
action.guard?.();
|
|
355
|
+
await walletReady(deps.adapter);
|
|
356
|
+
const storage = deps.storage ?? createHeldWriteStorage();
|
|
357
|
+
const { raw, record: saved } = readReviewedSequence(record.scope, record.account, storage);
|
|
358
|
+
if (!saved || saved.id !== record.id)
|
|
359
|
+
throw new ReviewLineError(REVIEWED_SEQUENCE_MOVED_LINE);
|
|
360
|
+
let expected = raw;
|
|
361
|
+
const connected = deps.adapter.account();
|
|
362
|
+
if (!connected)
|
|
363
|
+
throw new ReviewLineError("Connect a wallet first.");
|
|
364
|
+
if (connected.toLowerCase() !== saved.account.toLowerCase())
|
|
365
|
+
throw new ReviewLineError(ACCOUNT_CHANGED);
|
|
366
|
+
if (saved.sending) {
|
|
367
|
+
const { step, reservation } = saved.sending;
|
|
368
|
+
const call = saved.calls[step];
|
|
369
|
+
const handle = openStepHold(deps.hold, saved.account, deps.adapter.isSafe(), call);
|
|
370
|
+
const state = await handle.state(deps.clientFor(call.chainId));
|
|
371
|
+
if (state.status !== "clear")
|
|
372
|
+
throw new WriteHeldError(state);
|
|
373
|
+
// Only the step's own reservation's outcome decides it: a later one of
|
|
374
|
+
// the same call or intent hides it.
|
|
375
|
+
if (state.reservation !== reservation)
|
|
376
|
+
throw new ReviewLineError(REVIEWED_SEQUENCE_UNCERTAIN_LINE);
|
|
377
|
+
// Clear with no settlement: a settlement that could not be saved, which
|
|
378
|
+
// may have run, unless the user released it by its exit ("I checked my
|
|
379
|
+
// wallet") or nothing was sent: then the step is reviewed and sent again.
|
|
380
|
+
if (!state.settled && !handle.released(reservation))
|
|
381
|
+
throw new ReviewLineError(REVIEWED_SEQUENCE_UNCERTAIN_LINE);
|
|
382
|
+
const { settled } = state;
|
|
383
|
+
// Read before anything advances: a failed read leaves the step to resume again.
|
|
384
|
+
const receipt = await confirmedReceipt(settled, deps.clientFor(call.chainId));
|
|
385
|
+
// This sequence's own step: it is recorded, told, and only then
|
|
386
|
+
// acknowledged, so it is never offered again.
|
|
387
|
+
const kept = await recordSending(saved, settled, storage, expected);
|
|
388
|
+
await tellConfirmed(action, handle, call, step, settled, receipt);
|
|
389
|
+
if (settled?.result === "success")
|
|
390
|
+
await handle.acknowledge(settled);
|
|
391
|
+
if (kept === null) {
|
|
392
|
+
const total = saved.calls.length;
|
|
393
|
+
tell(deps.onProgress, { step: total - 1, total, phase: "done" });
|
|
394
|
+
return { status: "done", hashes: saved.completed, receipts: [] };
|
|
395
|
+
}
|
|
396
|
+
expected = kept;
|
|
397
|
+
}
|
|
398
|
+
return send({
|
|
399
|
+
...action,
|
|
400
|
+
account: saved.account,
|
|
401
|
+
calls: saved.calls.slice(saved.completed.length),
|
|
402
|
+
scope: saved.scope,
|
|
403
|
+
}, deps, { record: saved, storage, raw: expected });
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Ends `saved`'s step in flight as its hold settled it: a success is
|
|
407
|
+
* recorded as completed. Removes a finished sequence (null) or saves it over
|
|
408
|
+
* `expected`, returning the value written.
|
|
409
|
+
*/
|
|
410
|
+
async function recordSending(saved, settled, storage, expected) {
|
|
411
|
+
delete saved.sending;
|
|
412
|
+
delete saved.proposed;
|
|
413
|
+
if (settled?.result === "success")
|
|
414
|
+
saved.completed.push(settled.hash);
|
|
415
|
+
if (saved.completed.length < saved.calls.length)
|
|
416
|
+
return saveReviewedSequence(saved, storage, expected);
|
|
417
|
+
await removeReviewedSequence(saved, storage, expected);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* The receipt of a step its hold settled ({@link heldReceipt}), checked
|
|
422
|
+
* before anything advances: a read that fails or disagrees with the hold
|
|
423
|
+
* throws. Null when the settlement has no transaction (an expiry).
|
|
424
|
+
*/
|
|
425
|
+
async function confirmedReceipt(settled, client, given) {
|
|
426
|
+
return settled && "hash" in settled
|
|
427
|
+
? heldReceipt(settled, client, given)
|
|
428
|
+
: null;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Tells `onConfirmed` of a step's verified success, once per settlement:
|
|
432
|
+
* only the completion (a run, `completeReviewedStep`, `resumeReviewed`, in
|
|
433
|
+
* any tab) that claims its notice on the hold tells it.
|
|
434
|
+
*/
|
|
435
|
+
async function tellConfirmed(action, handle, call, step, settled, receipt) {
|
|
436
|
+
if (settled?.result === "success" &&
|
|
437
|
+
receipt &&
|
|
438
|
+
(await handle.claimConfirmed(settled)))
|
|
439
|
+
await tell(action.onConfirmed, call, step, receipt);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* A sent step's settlement by its own reservation, and the receipt its hold
|
|
443
|
+
* verified ({@link heldReceipt}), read again until it reads. Null on `follow`'s
|
|
444
|
+
* timeout or signal, or when the hold cleared without settling it (its exit).
|
|
445
|
+
*/
|
|
446
|
+
async function followSent(handle, client, reserved, follow) {
|
|
447
|
+
const stop = new AbortController();
|
|
448
|
+
const timer = setTimeout(() => stop.abort(), follow.timeoutMs ?? FOLLOW_MS);
|
|
449
|
+
const signal = follow.signal
|
|
450
|
+
? AbortSignal.any([stop.signal, follow.signal])
|
|
451
|
+
: stop.signal;
|
|
452
|
+
// Every read races the follow's end: a read that answers after it is ignored.
|
|
453
|
+
const ended = new Promise((resolve) => {
|
|
454
|
+
signal.addEventListener("abort", () => resolve(null), { once: true });
|
|
455
|
+
});
|
|
456
|
+
const read = (work) => Promise.race([work, ended]);
|
|
457
|
+
try {
|
|
458
|
+
while (!signal.aborted) {
|
|
459
|
+
const state = await read(followHeldWrite(handle, client, { signal }));
|
|
460
|
+
if (signal.aborted)
|
|
461
|
+
return null;
|
|
462
|
+
if (state?.status !== "clear" ||
|
|
463
|
+
state.reservation !== reserved ||
|
|
464
|
+
!state.settled ||
|
|
465
|
+
!("hash" in state.settled))
|
|
466
|
+
return null;
|
|
467
|
+
const { settled } = state;
|
|
468
|
+
try {
|
|
469
|
+
const receipt = await read(heldReceipt(settled, client));
|
|
470
|
+
return signal.aborted || !receipt ? null : { settled, receipt };
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
await pause(RECEIPT_RETRY_MS, signal, () => null).catch(() => null);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
clearTimeout(timer);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Completes a sent step (one a run left at its receipt,
|
|
484
|
+
* {@link ReviewedReceiptUnverifiedError}, or a proposal: its step and its
|
|
485
|
+
* `held` call and reservation) once its hold settled that send, as the run
|
|
486
|
+
* itself would have: the saved sequence under `scope` records it, a success
|
|
487
|
+
* is told to `onConfirmed` with the receipt its hold verified (`receipt`,
|
|
488
|
+
* the hold view's, say, when it is that one) and only then acknowledged, the
|
|
489
|
+
* one acknowledgement of a followed step. Returns the settlement and that
|
|
490
|
+
* checked receipt (a failure's too); null until then. A read that fails or disagrees with the hold throws with
|
|
491
|
+
* nothing advanced: call it again. Call it once per settlement:
|
|
492
|
+
* `onConfirmed` is told each time.
|
|
493
|
+
*/
|
|
494
|
+
export async function completeReviewedStep(unverified, action, deps, receipt) {
|
|
495
|
+
const { step, held } = unverified;
|
|
496
|
+
if (!held)
|
|
497
|
+
return null;
|
|
498
|
+
const { call, reservation } = held;
|
|
499
|
+
const client = deps.clientFor(call.chainId);
|
|
500
|
+
const handle = openStepHold(deps.hold, action.account, false, call);
|
|
501
|
+
const state = await handle.state(client);
|
|
502
|
+
if (state.status !== "clear" ||
|
|
503
|
+
state.reservation !== reservation ||
|
|
504
|
+
!state.settled)
|
|
505
|
+
return null;
|
|
506
|
+
const { settled } = state;
|
|
507
|
+
const confirmed = await confirmedReceipt(settled, client, receipt);
|
|
508
|
+
if (action.scope !== undefined) {
|
|
509
|
+
const storage = deps.storage ?? createHeldWriteStorage();
|
|
510
|
+
const { raw, record } = readReviewedSequence(action.scope, action.account, storage);
|
|
511
|
+
if (record?.sending?.step === step &&
|
|
512
|
+
record.sending.reservation === reservation)
|
|
513
|
+
await recordSending(record, settled, storage, raw);
|
|
514
|
+
}
|
|
515
|
+
await tellConfirmed(action, handle, call, step, settled, confirmed);
|
|
516
|
+
if (settled.result === "success")
|
|
517
|
+
await handle.acknowledge(settled);
|
|
518
|
+
return { settled, receipt: confirmed };
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The hash a wallet returned, even when the site's record of it failed
|
|
522
|
+
* afterwards: the step was sent.
|
|
523
|
+
*/
|
|
524
|
+
function sentHash(error) {
|
|
525
|
+
if (error instanceof SubmittedContractWriteError)
|
|
526
|
+
return error.hash;
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
async function send(action, deps, resuming) {
|
|
530
|
+
const { adapter, clientFor, hold } = deps;
|
|
531
|
+
const onProgress = (progress) => tell(deps.onProgress, progress);
|
|
532
|
+
action.guard?.();
|
|
533
|
+
const { account } = action;
|
|
534
|
+
const calls = [...action.calls];
|
|
535
|
+
if (!calls.length)
|
|
536
|
+
throw new ReviewLineError("There is no transaction to review.");
|
|
537
|
+
const snapshots = calls.map(exact);
|
|
538
|
+
const sent = snapshots.map(({ sent }) => sent);
|
|
539
|
+
/** Each step's simulation block, read once; a resumed run's prerequisites are long confirmed. */
|
|
540
|
+
const blocks = calls.map((call) => resuming ? undefined : call.simulationBlockNumber);
|
|
541
|
+
/** A site's release callback never stops core's own release. */
|
|
542
|
+
const tellUnsent = (call, step) => tell(action.onUnsent, call, step);
|
|
543
|
+
const connected = adapter.account();
|
|
544
|
+
if (!connected)
|
|
545
|
+
throw new ReviewLineError("Connect a wallet first.");
|
|
546
|
+
if (connected.toLowerCase() !== account.toLowerCase()) {
|
|
547
|
+
throw new ReviewLineError(ACCOUNT_CHANGED);
|
|
548
|
+
}
|
|
549
|
+
// A resumed sequence signs none: its signatures went with its first run.
|
|
550
|
+
const prepared = (resuming ? [] : [...(action.signatures ?? [])]).map((signature) => prepareSignature(signature, account));
|
|
551
|
+
// Signa has no reviewed-signature path yet.
|
|
552
|
+
if (prepared.length && adapter.kind === "self-reviewing") {
|
|
553
|
+
throw unsupportedWalletRequest();
|
|
554
|
+
}
|
|
555
|
+
const request = (safe) => ({
|
|
556
|
+
calls: snapshots.map((snapshot) => shownCall(snapshot, account, safe)),
|
|
557
|
+
...(prepared.length
|
|
558
|
+
? { signatures: prepared.map(({ review }) => review) }
|
|
559
|
+
: {}),
|
|
560
|
+
...(action.title ? { title: action.title } : {}),
|
|
561
|
+
...(action.description ? { description: action.description } : {}),
|
|
562
|
+
...(action.confirmLabel ? { confirmLabel: action.confirmLabel } : {}),
|
|
563
|
+
...(action.context ? { context: action.context } : {}),
|
|
564
|
+
});
|
|
565
|
+
if (adapter.kind === "self-reviewing") {
|
|
566
|
+
if (!adapter.submitToReviewer) {
|
|
567
|
+
throw new ReviewLineError("This wallet cannot review this transaction.");
|
|
568
|
+
}
|
|
569
|
+
deps.onPreview?.(buildReviewModel(request(false)));
|
|
570
|
+
/** The steps whose beforeWrite began, and whether the wallet was handed the calls. */
|
|
571
|
+
let began = 0;
|
|
572
|
+
let submitting = false;
|
|
573
|
+
let hash;
|
|
574
|
+
try {
|
|
575
|
+
for (const [step, call] of calls.entries()) {
|
|
576
|
+
began += 1;
|
|
577
|
+
await action.beforeWallet?.(call, step);
|
|
578
|
+
}
|
|
579
|
+
// The site's hooks may have awaited: the gates once more, right before the wallet.
|
|
580
|
+
action.guard?.();
|
|
581
|
+
action.assertWriteState?.();
|
|
582
|
+
if (adapter.account()?.toLowerCase() !== account.toLowerCase())
|
|
583
|
+
throw new ReviewLineError(ACCOUNT_CHANGED);
|
|
584
|
+
submitting = true;
|
|
585
|
+
hash = await adapter.submitToReviewer({
|
|
586
|
+
account,
|
|
587
|
+
calls: sent.map((call) => ({ ...call })),
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
// Only the site's own refusal or an explicit rejection proves nothing was sent.
|
|
592
|
+
if (!submitting || isDefiniteWalletRejection(error))
|
|
593
|
+
for (const [step, call] of calls.slice(0, began).entries())
|
|
594
|
+
await tellUnsent(call, step);
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
// Every step is told; a site's failure to record it never changes what was sent.
|
|
598
|
+
for (const [step, call] of calls.entries())
|
|
599
|
+
await tell(action.onSubmitted, call, step, hash, {
|
|
600
|
+
safe: false,
|
|
601
|
+
adopted: false,
|
|
602
|
+
});
|
|
603
|
+
return { status: "done", hashes: [hash], receipts: [] };
|
|
604
|
+
}
|
|
605
|
+
const { scope } = action;
|
|
606
|
+
const storage = scope === undefined
|
|
607
|
+
? undefined
|
|
608
|
+
: (resuming?.storage ?? deps.storage ?? createHeldWriteStorage());
|
|
609
|
+
/** The journal's saved value as this run last read or wrote it. */
|
|
610
|
+
let expected = resuming?.raw ?? null;
|
|
611
|
+
if (storage && !resuming) {
|
|
612
|
+
const saved = readReviewedSequence(scope, account, storage);
|
|
613
|
+
// A fresh run refuses while an earlier run of the same action is unfinished.
|
|
614
|
+
if (saved.record)
|
|
615
|
+
throw new ReviewLineError(REVIEWED_SEQUENCE_UNFINISHED_LINE);
|
|
616
|
+
assertSavableCalls(calls);
|
|
617
|
+
expected = saved.raw;
|
|
618
|
+
}
|
|
619
|
+
/** Where the remaining steps start in the whole action. */
|
|
620
|
+
const offset = resuming?.record.completed.length ?? 0;
|
|
621
|
+
const safe = adapter.isSafe();
|
|
622
|
+
/** Through a Safe app, the one MultiSend call every step goes as. */
|
|
623
|
+
const batched =
|
|
624
|
+
// A resumed sequence is journaled step by step: never folded into one proposal.
|
|
625
|
+
action.batch && safe && calls.length > 1 && !resuming
|
|
626
|
+
? safeBatchCall(account, calls.map((call, step) => ({ ...call, ...sent[step] })))
|
|
627
|
+
: undefined;
|
|
628
|
+
if (batched &&
|
|
629
|
+
(!adapter.sendCalls ||
|
|
630
|
+
sent.some((call) => call.chainId !== batched.chainId)))
|
|
631
|
+
throw new ReviewLineError(SAFE_BATCH_UNSUPPORTED_LINE);
|
|
632
|
+
const handles = (batched ? [batched] : calls).map((call, step) => openStepHold(hold, account, safe, {
|
|
633
|
+
intent: call.intent,
|
|
634
|
+
legacy: call.legacy,
|
|
635
|
+
...(batched ?? sent[step]),
|
|
636
|
+
}));
|
|
637
|
+
// A step its hold already holds is refused before the review opens.
|
|
638
|
+
for (const handle of handles) {
|
|
639
|
+
const state = handle.peek();
|
|
640
|
+
if (state.status !== "clear")
|
|
641
|
+
throw new WriteHeldError(state);
|
|
642
|
+
}
|
|
643
|
+
// So is one whose last success nothing showed: a resend may pay twice.
|
|
644
|
+
const away = handles[handles.length - 1].unacknowledgedSuccess();
|
|
645
|
+
if (away)
|
|
646
|
+
throw new WriteHoldUnacknowledgedSuccessError(away);
|
|
647
|
+
/**
|
|
648
|
+
* Each step's latest reservation as the review opened. Another prompt of
|
|
649
|
+
* the same action that reserved meanwhile, whether or not it has settled,
|
|
650
|
+
* makes this review stale.
|
|
651
|
+
*/
|
|
652
|
+
const generation = (handle) => handle.peek().reservation ?? null;
|
|
653
|
+
const reviewedGenerations = handles.map(generation);
|
|
654
|
+
const model = buildReviewModel(request(safe));
|
|
655
|
+
/** What each step runs as: a Safe app's call is proposed with safeTxGas 0, not the app's gas. */
|
|
656
|
+
const run = safe
|
|
657
|
+
? sent.map(({ gas: _gas, ...call }) => call)
|
|
658
|
+
: sent;
|
|
659
|
+
if (action.plan) {
|
|
660
|
+
// A plan binds bytes, never an adoption rule; a Safe app's unbatched
|
|
661
|
+
// sequence stops at its first proposal, so its other calls would be spent unsent.
|
|
662
|
+
if (snapshots.some(({ shown }) => shown.adoptIgnoringArgs) ||
|
|
663
|
+
(safe && !batched && calls.length > 1))
|
|
664
|
+
throw new ReviewLineError(REVIEWED_PLAN_REFUSED_LINE);
|
|
665
|
+
spendPlan(action.plan, account, [
|
|
666
|
+
...sent.map(({ chainId, to, data, value, gas }) => ({
|
|
667
|
+
chainId,
|
|
668
|
+
from: account,
|
|
669
|
+
to,
|
|
670
|
+
data,
|
|
671
|
+
value,
|
|
672
|
+
...(gas === undefined ? {} : { gas }),
|
|
673
|
+
})),
|
|
674
|
+
...prepared.flatMap(plannedCallsOf),
|
|
675
|
+
]);
|
|
676
|
+
}
|
|
677
|
+
else
|
|
678
|
+
await promptSimulated(model, () => account, calls, run, blocks, clientFor, action.signal);
|
|
679
|
+
let signed = {};
|
|
680
|
+
if (prepared.length) {
|
|
681
|
+
// The action's own gates, last before the wallet opens for each signature.
|
|
682
|
+
const stillReviewed = () => {
|
|
683
|
+
action.guard?.();
|
|
684
|
+
action.assertWriteState?.();
|
|
685
|
+
calls.forEach((call, step) => {
|
|
686
|
+
let now;
|
|
687
|
+
try {
|
|
688
|
+
now = exact(call).sent;
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
throw new ReviewLineError(CHANGED);
|
|
692
|
+
}
|
|
693
|
+
if (!sameCall(now, sent[step]))
|
|
694
|
+
throw new ReviewLineError(CHANGED);
|
|
695
|
+
});
|
|
696
|
+
handles.forEach((handle, step) => {
|
|
697
|
+
if (generation(handle) !== reviewedGenerations[step]) {
|
|
698
|
+
throw new WriteHoldStaleReviewError();
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
signed = {
|
|
703
|
+
signatures: await signPrepared(prepared, account, adapter, stillReviewed),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
if (batched) {
|
|
707
|
+
const client = clientFor(batched.chainId);
|
|
708
|
+
const unchanged = () => calls.forEach((call, step) => {
|
|
709
|
+
let now;
|
|
710
|
+
try {
|
|
711
|
+
now = exact(call).sent;
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
throw new ReviewLineError(CHANGED);
|
|
715
|
+
}
|
|
716
|
+
if (!sameCall(now, sent[step]))
|
|
717
|
+
throw new ReviewLineError(CHANGED);
|
|
718
|
+
});
|
|
719
|
+
const progress = (phase, hash) => onProgress({
|
|
720
|
+
step: 0,
|
|
721
|
+
total: calls.length,
|
|
722
|
+
phase,
|
|
723
|
+
...(hash ? { hash } : {}),
|
|
724
|
+
});
|
|
725
|
+
/** The steps whose `beforeWallet` began. */
|
|
726
|
+
let began = 0;
|
|
727
|
+
const hash = await handles[0]
|
|
728
|
+
.send(client, (gate) => {
|
|
729
|
+
if (generation(handles[0]) !== reviewedGenerations[0]) {
|
|
730
|
+
throw new WriteHoldStaleReviewError();
|
|
731
|
+
}
|
|
732
|
+
const released = async () => {
|
|
733
|
+
gate.onRejected();
|
|
734
|
+
for (const [step, call] of calls.slice(0, began).entries())
|
|
735
|
+
await tellUnsent(call, step);
|
|
736
|
+
};
|
|
737
|
+
return submitReviewedContractWrite({
|
|
738
|
+
request: batched,
|
|
739
|
+
expectedAccount: account,
|
|
740
|
+
review: () => Promise.resolve(),
|
|
741
|
+
switchChain: async (chainId) => {
|
|
742
|
+
if (adapter.chainId() !== chainId)
|
|
743
|
+
await adapter.switchChain(chainId);
|
|
744
|
+
},
|
|
745
|
+
currentAccount: () => adapter.account(),
|
|
746
|
+
reverify: async () => {
|
|
747
|
+
for (const [step, call] of calls.entries())
|
|
748
|
+
await action.reverify?.(call, step, undefined);
|
|
749
|
+
unchanged();
|
|
750
|
+
},
|
|
751
|
+
simulate: async (sending) => {
|
|
752
|
+
await simulateCallSequence(client, {
|
|
753
|
+
from: account,
|
|
754
|
+
chainName: chainNameOf(client, batched.chainId),
|
|
755
|
+
calls: run.map((call, step) => ({
|
|
756
|
+
...call,
|
|
757
|
+
label: labelOf(calls[step], step),
|
|
758
|
+
dependsOnPrior: calls[step].dependsOnPrior,
|
|
759
|
+
})),
|
|
760
|
+
});
|
|
761
|
+
return sending;
|
|
762
|
+
},
|
|
763
|
+
beforeWrite: async () => {
|
|
764
|
+
await gate.beforeSend();
|
|
765
|
+
try {
|
|
766
|
+
for (const [step, call] of calls.entries()) {
|
|
767
|
+
began = step + 1;
|
|
768
|
+
await action.beforeWallet?.(call, step);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
await released();
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
beforeSend: () => {
|
|
777
|
+
action.assertWriteState?.();
|
|
778
|
+
unchanged();
|
|
779
|
+
if (!adapter.isSafe())
|
|
780
|
+
throw new ReviewLineError("Wallet connection changed. Review the transaction again.");
|
|
781
|
+
if (adapter.chainId() !== batched.chainId)
|
|
782
|
+
throw new ReviewLineError("Your wallet changed networks. Review the transaction again.");
|
|
783
|
+
if (adapter.account()?.toLowerCase() !== account.toLowerCase())
|
|
784
|
+
throw new ReviewLineError(ACCOUNT_CHANGED);
|
|
785
|
+
},
|
|
786
|
+
onBeforeWriteAborted: released,
|
|
787
|
+
onWriteRejected: released,
|
|
788
|
+
onWriteSubmitted: async (submitted) => {
|
|
789
|
+
gate.afterSubmit(submitted);
|
|
790
|
+
// Every step went: each is told, whatever an earlier callback does.
|
|
791
|
+
for (const [step, call] of calls.entries())
|
|
792
|
+
await tell(action.onSubmitted, call, step, submitted, {
|
|
793
|
+
safe: true,
|
|
794
|
+
adopted: false,
|
|
795
|
+
});
|
|
796
|
+
},
|
|
797
|
+
write: async () => {
|
|
798
|
+
const id = await adapter.sendCalls(batched.chainId, sent.map(({ to, data, value }) => ({ to, data, value })));
|
|
799
|
+
// A Safe app answers with the proposal's safeTxHash; anything else can't be followed.
|
|
800
|
+
if (typeof id !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(id))
|
|
801
|
+
throw new ReviewLineError(SAFE_BATCH_UNFOLLOWABLE_LINE);
|
|
802
|
+
return id;
|
|
803
|
+
},
|
|
804
|
+
onPhase: (phase) => progress(phase),
|
|
805
|
+
guard: action.guard,
|
|
806
|
+
});
|
|
807
|
+
})
|
|
808
|
+
.catch(sentHash);
|
|
809
|
+
progress("proposed", hash);
|
|
810
|
+
return { status: "proposed", step: 0, safeTxHash: hash, ...signed };
|
|
811
|
+
}
|
|
812
|
+
// Saved before the first wallet call, once the review is confirmed.
|
|
813
|
+
let journal;
|
|
814
|
+
if (storage) {
|
|
815
|
+
if (resuming)
|
|
816
|
+
journal = { ...resuming.record, modelId: model.id };
|
|
817
|
+
else
|
|
818
|
+
journal = {
|
|
819
|
+
version: 1,
|
|
820
|
+
id: globalThis.crypto.randomUUID(),
|
|
821
|
+
account,
|
|
822
|
+
scope: scope,
|
|
823
|
+
modelId: model.id,
|
|
824
|
+
// What was reviewed, read once above.
|
|
825
|
+
calls: calls.map((call, step) => ({
|
|
826
|
+
...call,
|
|
827
|
+
...snapshots[step].shown,
|
|
828
|
+
...snapshots[step].sent,
|
|
829
|
+
})),
|
|
830
|
+
completed: [],
|
|
831
|
+
};
|
|
832
|
+
// Refused when another window changed it meanwhile.
|
|
833
|
+
expected = await saveReviewedSequence(journal, storage, expected);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Progress after a send is saved as it can be: the step's saved
|
|
837
|
+
* `sending` mark already lets its hold decide it on resume.
|
|
838
|
+
*/
|
|
839
|
+
const keep = async () => {
|
|
840
|
+
try {
|
|
841
|
+
if (journal.completed.length === journal.calls.length)
|
|
842
|
+
await removeReviewedSequence(journal, storage, expected);
|
|
843
|
+
else
|
|
844
|
+
expected = await saveReviewedSequence(journal, storage, expected);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
/* Another window owns the sequence now; the hold still holds the step. */
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
const hashes = [];
|
|
851
|
+
const receipts = [];
|
|
852
|
+
const total = offset + calls.length;
|
|
853
|
+
for (const [step, call] of calls.entries()) {
|
|
854
|
+
const reviewed = sent[step];
|
|
855
|
+
/** This step's index in the whole action. */
|
|
856
|
+
const at = offset + step;
|
|
857
|
+
const progress = (phase, hash) => onProgress({ step: at, total, phase, ...(hash ? { hash } : {}) });
|
|
858
|
+
const unchanged = () => {
|
|
859
|
+
let now;
|
|
860
|
+
try {
|
|
861
|
+
now = exact(call);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
throw new ReviewLineError(CHANGED);
|
|
865
|
+
}
|
|
866
|
+
if (!sameCall(now.sent, reviewed) ||
|
|
867
|
+
String(now.shown.adoptIgnoringArgs) !==
|
|
868
|
+
String(snapshots[step].shown.adoptIgnoringArgs))
|
|
869
|
+
throw new ReviewLineError(CHANGED);
|
|
870
|
+
};
|
|
871
|
+
const client = clientFor(reviewed.chainId);
|
|
872
|
+
const prior = receipts[step - 1];
|
|
873
|
+
/** Where the step before this one confirmed: this run's receipt, or a resumed run's last completed step. */
|
|
874
|
+
const confirmedBefore = async () => {
|
|
875
|
+
if (prior)
|
|
876
|
+
return {
|
|
877
|
+
chainId: sent[step - 1].chainId,
|
|
878
|
+
blockNumber: prior.blockNumber,
|
|
879
|
+
blockHash: prior.blockHash,
|
|
880
|
+
};
|
|
881
|
+
const done = resuming?.record;
|
|
882
|
+
if (step > 0 || !done?.completed.length)
|
|
883
|
+
return undefined;
|
|
884
|
+
const last = done.calls[done.completed.length - 1];
|
|
885
|
+
const hash = done.completed[done.completed.length - 1];
|
|
886
|
+
// Its hold's settlement, while it is still the call's latest, kept
|
|
887
|
+
// the block it verified; else the hash's receipt, canonical and final.
|
|
888
|
+
const held = openStepHold(hold, account, adapter.isSafe(), last).peek();
|
|
889
|
+
const settled = held.status === "clear" &&
|
|
890
|
+
held.settled &&
|
|
891
|
+
"hash" in held.settled &&
|
|
892
|
+
held.settled.hash.toLowerCase() === hash.toLowerCase()
|
|
893
|
+
? held.settled
|
|
894
|
+
: { result: "success", hash };
|
|
895
|
+
// One try: a block that can't be read or verified now is no block.
|
|
896
|
+
const block = await heldBlock(settled, clientFor(last.chainId)).catch(() => undefined);
|
|
897
|
+
return block && { chainId: last.chainId, ...block };
|
|
898
|
+
};
|
|
899
|
+
const priorBlock = call.dependsOnPrior &&
|
|
900
|
+
prior &&
|
|
901
|
+
sent[step - 1].chainId === reviewed.chainId
|
|
902
|
+
? prior.blockNumber
|
|
903
|
+
: undefined;
|
|
904
|
+
/** The reservation this step's send took. */
|
|
905
|
+
let reserved;
|
|
906
|
+
/** A proposal of exactly this call its Safe already queued, sent in its place. */
|
|
907
|
+
let queued;
|
|
908
|
+
/** The caller's end of the run, once this run sent a step. */
|
|
909
|
+
const runEnd = hashes.length ? action.follow?.signal : undefined;
|
|
910
|
+
/** The site's failed record of this step's hash: the step was sent, and is held unverified. */
|
|
911
|
+
let unrecorded;
|
|
912
|
+
const hash = await handles[step]
|
|
913
|
+
.send(client, (gate) => {
|
|
914
|
+
// Under the hold's own check of the call, before anything reserves.
|
|
915
|
+
if (generation(handles[step]) !== reviewedGenerations[step]) {
|
|
916
|
+
throw new WriteHoldStaleReviewError();
|
|
917
|
+
}
|
|
918
|
+
/** Nothing was sent: the hold lets go, and the step is no longer in flight. */
|
|
919
|
+
const released = async () => {
|
|
920
|
+
gate.onRejected();
|
|
921
|
+
if (journal) {
|
|
922
|
+
delete journal.sending;
|
|
923
|
+
await keep();
|
|
924
|
+
}
|
|
925
|
+
await tellUnsent(call, at);
|
|
926
|
+
};
|
|
927
|
+
return submitReviewedContractWrite({
|
|
928
|
+
request: reviewed,
|
|
929
|
+
expectedAccount: account,
|
|
930
|
+
// Confirmed once, above, for every step.
|
|
931
|
+
review: () => Promise.resolve(),
|
|
932
|
+
switchChain: async (chainId) => {
|
|
933
|
+
if (adapter.chainId() !== chainId)
|
|
934
|
+
await adapter.switchChain(chainId);
|
|
935
|
+
},
|
|
936
|
+
currentAccount: () => adapter.account(),
|
|
937
|
+
reverify: async () => {
|
|
938
|
+
if (action.reverify)
|
|
939
|
+
await action.reverify(call, at, await confirmedBefore());
|
|
940
|
+
unchanged();
|
|
941
|
+
},
|
|
942
|
+
simulate: async (sending) => {
|
|
943
|
+
const blockNumber = priorBlock ??
|
|
944
|
+
(blocks[step] === undefined
|
|
945
|
+
? undefined
|
|
946
|
+
: await notBefore(client, blocks[step]));
|
|
947
|
+
const tx = {
|
|
948
|
+
to: sending.to,
|
|
949
|
+
data: sending.data,
|
|
950
|
+
value: sending.value,
|
|
951
|
+
...(blockNumber === undefined ? {} : { blockNumber }),
|
|
952
|
+
};
|
|
953
|
+
const { gas } = run[step];
|
|
954
|
+
const [, estimate, found] = await Promise.all([
|
|
955
|
+
simulateStateChangingTransaction(client, {
|
|
956
|
+
from: account,
|
|
957
|
+
...tx,
|
|
958
|
+
gas,
|
|
959
|
+
}).catch((error) => {
|
|
960
|
+
throw simulationFailure(`${labelOf(call, at)} cannot run on ${chainNameOf(client, sending.chainId)}: ${revertDetail(error)}`, error);
|
|
961
|
+
}),
|
|
962
|
+
// A Safe app signs the sent gas as safeTxGas; 0 makes a failed call revert.
|
|
963
|
+
safe || gas !== undefined
|
|
964
|
+
? undefined
|
|
965
|
+
: client.estimateGas({ account, ...tx }),
|
|
966
|
+
// A Safe app never proposes again a call its Safe already queued exactly.
|
|
967
|
+
safe && hasSafeService(sending.chainId)
|
|
968
|
+
? findPendingSafeAppProposal(client, sending.chainId, account, sending, hold.safeService, true, adopting(snapshots[step].shown))
|
|
969
|
+
: null,
|
|
970
|
+
]);
|
|
971
|
+
queued = found;
|
|
972
|
+
return {
|
|
973
|
+
...sending,
|
|
974
|
+
gas: safe ? 0n : (gas ?? gasWithHeadroom(estimate)),
|
|
975
|
+
};
|
|
976
|
+
},
|
|
977
|
+
beforeWrite: async () => {
|
|
978
|
+
// An adopted proposal is held as the call it runs.
|
|
979
|
+
await gate.beforeSend(queued?.call);
|
|
980
|
+
reserved = handles[step].peek().reservation;
|
|
981
|
+
if (journal)
|
|
982
|
+
try {
|
|
983
|
+
const { reservation } = handles[step].peek();
|
|
984
|
+
if (reservation === undefined)
|
|
985
|
+
throw new ReviewLineError(NOT_SAVED);
|
|
986
|
+
journal.sending = { step: at, reservation };
|
|
987
|
+
expected = await saveReviewedSequence(journal, storage, expected);
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
// Reserved, and the wallet never opened.
|
|
991
|
+
delete journal.sending;
|
|
992
|
+
gate.onRejected();
|
|
993
|
+
throw error;
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
await action.beforeWallet?.(call, at);
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
await released();
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
},
|
|
1003
|
+
beforeSend: () => {
|
|
1004
|
+
// The caller ended the run (a reset, an unmount) after it sent a
|
|
1005
|
+
// step: nothing more is sent.
|
|
1006
|
+
if (hashes.length && action.follow?.signal?.aborted)
|
|
1007
|
+
throw new TransactionReviewCancelledError();
|
|
1008
|
+
action.assertWriteState?.();
|
|
1009
|
+
unchanged();
|
|
1010
|
+
if (adapter.isSafe() !== safe) {
|
|
1011
|
+
throw new ReviewLineError("Wallet connection changed. Review the transaction again.");
|
|
1012
|
+
}
|
|
1013
|
+
if (adapter.chainId() !== reviewed.chainId) {
|
|
1014
|
+
throw new ReviewLineError("Your wallet changed networks. Review the transaction again.");
|
|
1015
|
+
}
|
|
1016
|
+
// Last, after every callback and getter above could have run.
|
|
1017
|
+
if (adapter.account()?.toLowerCase() !== account.toLowerCase()) {
|
|
1018
|
+
throw new ReviewLineError(ACCOUNT_CHANGED);
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
onBeforeWriteAborted: released,
|
|
1022
|
+
onWriteRejected: released,
|
|
1023
|
+
// An adopted proposal's nonce lets the hold prove its execution onchain.
|
|
1024
|
+
onWriteSubmitted: async (submitted) => {
|
|
1025
|
+
gate.afterSubmit(submitted, queued?.tx);
|
|
1026
|
+
await tell(action.onSubmitted, call, at, submitted, {
|
|
1027
|
+
safe,
|
|
1028
|
+
adopted: !!queued,
|
|
1029
|
+
});
|
|
1030
|
+
},
|
|
1031
|
+
write: async ({ chainId, to, data, value, gas }) => queued?.proposalHash ??
|
|
1032
|
+
adapter.sendTransaction({
|
|
1033
|
+
chainId,
|
|
1034
|
+
to,
|
|
1035
|
+
data,
|
|
1036
|
+
value,
|
|
1037
|
+
gas,
|
|
1038
|
+
// After a step was sent, the run's end refuses this one, unsent,
|
|
1039
|
+
// as late as the wallet's own last check.
|
|
1040
|
+
...(runEnd ? { signal: runEnd } : {}),
|
|
1041
|
+
}),
|
|
1042
|
+
onPhase: (phase) => progress(phase),
|
|
1043
|
+
guard: action.guard,
|
|
1044
|
+
});
|
|
1045
|
+
})
|
|
1046
|
+
.catch((error) => {
|
|
1047
|
+
// Refused unsent because the caller ended the run: a cancel, not a failure.
|
|
1048
|
+
if (runEnd?.aborted && error instanceof WalletNotInvokedError)
|
|
1049
|
+
throw new TransactionReviewCancelledError();
|
|
1050
|
+
unrecorded = error;
|
|
1051
|
+
return sentHash(error);
|
|
1052
|
+
});
|
|
1053
|
+
hashes.push(hash);
|
|
1054
|
+
// A later step held under this step's key (the same call, or the same
|
|
1055
|
+
// intent) was reserved by this send, not by another prompt.
|
|
1056
|
+
for (const [later, handle] of handles.entries()) {
|
|
1057
|
+
const sameKey = call.intent
|
|
1058
|
+
? calls[later].intent === call.intent &&
|
|
1059
|
+
sent[later].chainId === reviewed.chainId
|
|
1060
|
+
: !calls[later].intent && sameCall(sent[later], reviewed);
|
|
1061
|
+
if (later > step && sameKey) {
|
|
1062
|
+
reviewedGenerations[later] = generation(handle);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (safe) {
|
|
1066
|
+
if (journal) {
|
|
1067
|
+
journal.proposed = { step: at, safeTxHash: hash };
|
|
1068
|
+
await keep();
|
|
1069
|
+
}
|
|
1070
|
+
progress("proposed", hash);
|
|
1071
|
+
return { status: "proposed", step: at, safeTxHash: hash, ...signed };
|
|
1072
|
+
}
|
|
1073
|
+
// From here the step was sent: it ends done, failed as its hold
|
|
1074
|
+
// verified, or unverified with what resumes it, never any other way.
|
|
1075
|
+
const unverified = (cause) => new ReviewedReceiptUnverifiedError(at, hash, isSafeExecTransaction(reviewed.data)
|
|
1076
|
+
? WRITE_HOLD_SAFE_UNCONFIRMED_LINE
|
|
1077
|
+
: undefined, {
|
|
1078
|
+
call,
|
|
1079
|
+
reservation: reserved,
|
|
1080
|
+
// This run's sends, its own last: only the steps before it completed.
|
|
1081
|
+
hashes: journal ? [...journal.completed] : hashes.slice(0, -1),
|
|
1082
|
+
...signed,
|
|
1083
|
+
}, cause === undefined ? {} : { cause });
|
|
1084
|
+
// The site could not record the hash: held as sent, for it to resume.
|
|
1085
|
+
if (unrecorded !== undefined)
|
|
1086
|
+
throw unverified(unrecorded);
|
|
1087
|
+
// Its hold's verified settlement is the only word on how it ended: its
|
|
1088
|
+
// result, its transaction (a speed-up's, when the wallet replaced the
|
|
1089
|
+
// send) and that transaction's receipt in the block the hold verified.
|
|
1090
|
+
// A watcher's receipt only says when to ask.
|
|
1091
|
+
let receipt;
|
|
1092
|
+
let settled;
|
|
1093
|
+
try {
|
|
1094
|
+
progress("receipt", hash);
|
|
1095
|
+
const seen = await waitForTrackedReceipt(client, hash);
|
|
1096
|
+
// Only this send's own reservation's settlement speaks for it: a later
|
|
1097
|
+
// send of the same call or intent may have settled since.
|
|
1098
|
+
settled = await handles[step]
|
|
1099
|
+
.state(client)
|
|
1100
|
+
.then((state) => state.status === "clear" &&
|
|
1101
|
+
reserved !== undefined &&
|
|
1102
|
+
state.reservation === reserved &&
|
|
1103
|
+
state.settled &&
|
|
1104
|
+
"hash" in state.settled
|
|
1105
|
+
? state.settled
|
|
1106
|
+
: undefined)
|
|
1107
|
+
.catch(() => undefined);
|
|
1108
|
+
receipt = settled
|
|
1109
|
+
? await heldReceipt(settled, client, seen).catch(() => undefined)
|
|
1110
|
+
: undefined;
|
|
1111
|
+
if (!receipt && action.follow) {
|
|
1112
|
+
tell(action.follow.onConfirming, at);
|
|
1113
|
+
const ended = await followSent(handles[step], client, reserved, action.follow);
|
|
1114
|
+
({ settled, receipt } = ended ?? { settled, receipt });
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
throw unverified(error);
|
|
1119
|
+
}
|
|
1120
|
+
if (!settled || !receipt)
|
|
1121
|
+
throw unverified();
|
|
1122
|
+
if (settled.result === "failed" && receipt.status === "success") {
|
|
1123
|
+
// It ran, and the hold proved it failed: a Safe's call that failed.
|
|
1124
|
+
if (journal) {
|
|
1125
|
+
delete journal.sending;
|
|
1126
|
+
await keep();
|
|
1127
|
+
}
|
|
1128
|
+
throw new ReviewLineError(WRITE_HOLD_CALL_FAILED_LINE);
|
|
1129
|
+
}
|
|
1130
|
+
const mined = settled.hash;
|
|
1131
|
+
hashes[hashes.length - 1] = mined;
|
|
1132
|
+
if (receipt.status !== "success") {
|
|
1133
|
+
// It ran and reverted: nothing of it to resume.
|
|
1134
|
+
if (journal) {
|
|
1135
|
+
delete journal.sending;
|
|
1136
|
+
await keep();
|
|
1137
|
+
}
|
|
1138
|
+
throw new ReviewedCallRevertedError(at, mined, receipt);
|
|
1139
|
+
}
|
|
1140
|
+
receipts.push(receipt);
|
|
1141
|
+
if (journal) {
|
|
1142
|
+
delete journal.sending;
|
|
1143
|
+
journal.completed.push(mined);
|
|
1144
|
+
await keep();
|
|
1145
|
+
}
|
|
1146
|
+
await tellConfirmed(action, handles[step], call, at, settled, receipt);
|
|
1147
|
+
}
|
|
1148
|
+
onProgress({ step: total - 1, total, phase: "done" });
|
|
1149
|
+
return {
|
|
1150
|
+
status: "done",
|
|
1151
|
+
hashes: journal ? [...journal.completed] : hashes,
|
|
1152
|
+
receipts,
|
|
1153
|
+
...signed,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
//# sourceMappingURL=send.js.map
|