♻️ app: prefer sendCalls, fix chain handling#879
♻️ app: prefer sendCalls, fix chain handling#879dieguezguille wants to merge 12 commits intomainfrom
sendCalls, fix chain handling#879Conversation
🦋 Changeset detectedLatest commit: 20f2f6d The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds chain-scoped context to many on-chain hooks, converts direct contract writes to mutation-driven sendCalls + waitForCallsStatus (paymaster integration), refactors Bridge to use Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Mobile Client
participant Mutate as useMutation (swap/send)
participant SendCalls as useSendCalls
participant Paymaster as Alchemy Paymaster
participant Chain as Blockchain Network
participant Status as waitForCallsStatus
Client->>Mutate: trigger swap/proposal (encoded calls + chainId)
Mutate->>SendCalls: submit calls (paymaster policy, chainId)
SendCalls->>Paymaster: request sponsored submission
Paymaster->>Chain: submit transaction
Chain->>Chain: execute calls
Mutate->>Status: poll call status with chainId
Status->>Chain: query receipt/status
Chain->>Status: return status
Status->>Mutate: resolve success/failure
Mutate->>Client: return result
sequenceDiagram
participant Component as UI Component
participant ChainObj as chain (generated)
participant Hook as Data Hook
participant Query as On-chain Query Layer
Component->>ChainObj: import chain
Component->>Hook: call hook with { ..., chainId: chain.id }
Hook->>Query: execute query scoped by chainId
Query->>Hook: return chain-specific data
Hook->>Component: provide scoped results
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
sendCalls, fix chain handling
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request refactors core transaction logic to leverage Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #879 +/- ##
==========================================
- Coverage 71.23% 70.94% -0.30%
==========================================
Files 212 212
Lines 8378 8424 +46
Branches 2741 2756 +15
==========================================
+ Hits 5968 5976 +8
- Misses 2132 2170 +38
Partials 278 278
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| const { calls, chainId, from, id } = params[0] as { | ||
| calls: readonly Call[]; | ||
| chainId?: Hex; | ||
| from?: Address; | ||
| id?: string; | ||
| }; | ||
| if (from && from !== accountAddress) throw new Error("bad account"); | ||
| if (queryClient.getQueryData<AuthMethod>(["method"]) === "webauthn") { | ||
| const { hash } = await client.sendUserOperation({ |
There was a problem hiding this comment.
🚩 webauthn path in accountClient ignores chainId parameter — safe for current callers
The wallet_sendCalls handler at src/utils/accountClient.ts:165 now extracts chainId from params but the webauthn path (lines 172-176) ignores it entirely, always sending user operations on the default chain and encoding chain.id in the response. This is currently safe because all callers that go through the smart account config (exa) don't pass explicit chainId — the bridge uses ownerConfig directly. However, if a future caller passes a non-default chainId through the exa config with webauthn auth, the transaction would silently execute on the wrong chain.
(Refers to lines 165-176)
Was this helpful? React with 👍 or 👎 to provide feedback.
991a102 to
80f63cc
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/pay-mode/Pay.tsx (1)
473-501:⚠️ Potential issue | 🟠 MajorAdd
chainId: chain.idto external assetmutateSendCallsinvocation.Same issue as the other mutation: the external repay path should also specify the chain to prevent cross-chain mismatches.
🔧 Proposed fix
const { id } = await mutateSendCalls({ + chainId: chain.id, calls: [ { to: selectedAsset.address, abi: erc20Abi, functionName: "approve", args: [swapperAddress, route.fromAmount], }, // ... rest of calls ], capabilities: { paymasterService: { url: `${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, context: { policyId: alchemyGasPolicyId }, }, }, });
♻️ Duplicate comments (6)
.changeset/open-beds-stand.md (1)
1-5:⚠️ Potential issue | 🟠 MajorRewrite or drop this stale changeset entry.
This note still says “add bottom padding to swaps screen on web”, but this PR is about
sendCallsand chain handling. If the change is internal, this should be an empty changeset; otherwise, rewrite it to the actual user-visible fix.Based on learnings, empty changesets are required for non-user-facing changes, and changeset summaries in
.changeset/*.mdmust start with a gitmoji and follow the format<emoji> <message>(no scope).src/components/shared/PluginUpgrade.tsx (1)
33-46:⚠️ Potential issue | 🔴 CriticalPin the upgrade write path to
chain.idas well.These reads are now scoped to
chain.id, but the batch submission and status polling below still default to the active wallet chain. If the wallet is on another network, the upgrade can be submitted or polled against a different chain than the one you just simulated.🔧 Minimal fix
const { id } = await mutateSendCalls({ + chainId: chain.id, calls: [ { ...uninstallPluginSimulation.request, to: address }, { to: address, @@ capabilities: { paymasterService: { url: `${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, context: { policyId: alchemyGasPolicyId }, }, }, }); - const { status } = await waitForCallsStatus(exa, { id }); + const { status } = await waitForCallsStatus(exa, { id, chainId: chain.id });In wagmi, do useSendCalls/sendCalls mutation variables and waitForCallsStatus accept a chainId parameter, and should it be passed when reads/simulations are already pinned to a specific chain?Also applies to: 59-85
src/components/swaps/Swaps.tsx (1)
339-380:⚠️ Potential issue | 🔴 CriticalPin the swap submission and status polling to
chain.id.The simulations above are scoped to
chain.id, but this batch write path still uses the wallet’s active chain by default. That can send the swap on a different network than the one you just simulated, or poll the wrong chain for status.🔧 Minimal fix
const { id } = await mutateSendCalls({ + chainId: chain.id, calls: [call], capabilities: { paymasterService: { url: `${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, context: { policyId: alchemyGasPolicyId }, }, }, }); - const { status } = await waitForCallsStatus(exaConfig, { id }); + const { status } = await waitForCallsStatus(exaConfig, { id, chainId: chain.id });In wagmi, do useSendCalls/sendCalls mutation variables and waitForCallsStatus accept a chainId parameter, and should it be passed when reads/simulations are already pinned to a specific chain?src/components/roll-debt/RollDebt.tsx (1)
290-300:⚠️ Potential issue | 🟠 MajorAdd
chainId: chain.idto themutateSendCallsinvocation.The mutation uses chain-scoped simulation and polling, but
mutateSendCallsis called without an explicitchainId. This can cause the transaction to be submitted on the wallet's currently connected chain rather than the intended chain. The past review comment flagged this issue as addressed, but the fix appears to be missing.🔧 Proposed fix
const { id } = await mutateSendCalls({ + chainId: chain.id, calls: [{ to, data: encodeFunctionData({ abi, functionName, args }) }], capabilities: { paymasterService: { url: `${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, context: { policyId: alchemyGasPolicyId }, }, }, });src/components/send-funds/Amount.tsx (1)
165-178:⚠️ Potential issue | 🟠 MajorAdd
chainId: chain.idto themutateSendCallsinvocation.All simulations in this component are chain-scoped, but
mutateSendCallsis called without an explicitchainId. This can result in the transaction being submitted on the wallet's currently connected chain instead of the intended chain. The subsequentwaitForCallsStatuscall uses the single-chainexaconfig, which may fail to poll status correctly if chains mismatch.🔧 Proposed fix
const sendCalls = async (calls: readonly { data?: `0x${string}`; to: `0x${string}`; value?: bigint }[]) => { const { id } = await mutateSendCalls({ + chainId: chain.id, calls, capabilities: { paymasterService: { url: `${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, context: { policyId: alchemyGasPolicyId }, }, }, });src/components/pay-mode/Pay.tsx (1)
427-435:⚠️ Potential issue | 🟠 MajorAdd
chainId: chain.idtomutateSendCallsinvocation.All reads and simulations in this component explicitly pin to
chain.id, but this mutation executes calls without specifying the chain. A wallet connected to a different network could submit the transaction on the wrong chain whilewaitForCallsStatuspolls onexa's configured chain.🔧 Proposed fix
const { id } = await mutateSendCalls({ + chainId: chain.id, calls: [call], capabilities: { paymasterService: { url: `${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, context: { policyId: alchemyGasPolicyId }, }, }, });
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f49faff4-bf2f-4d46-a9e9-1b0dacece46f
📒 Files selected for processing (48)
.changeset/bright-foxes-swim.md.changeset/cool-icons-grow.md.changeset/curly-pumas-jam.md.changeset/cyan-flies-camp.md.changeset/funny-aliens-mix.md.changeset/gold-cow-eat.md.changeset/great-dryers-kick.md.changeset/jolly-teeth-flow.md.changeset/loose-papers-take.md.changeset/open-beds-stand.md.changeset/wide-cats-hug.mdsrc/components/add-funds/Bridge.tsxsrc/components/card/Card.tsxsrc/components/card/exa-card/CardContents.tsxsrc/components/defi/DeFi.tsxsrc/components/getting-started/GettingStarted.tsxsrc/components/home/AssetList.tsxsrc/components/home/CardLimits.tsxsrc/components/home/Home.tsxsrc/components/home/HomeActions.tsxsrc/components/home/Portfolio.tsxsrc/components/home/card-upgrade/UpgradeAccount.tsxsrc/components/loans/Amount.tsxsrc/components/loans/Asset.tsxsrc/components/loans/CreditLine.tsxsrc/components/loans/LoanSummary.tsxsrc/components/loans/Loans.tsxsrc/components/loans/Review.tsxsrc/components/pay-mode/OverduePayments.tsxsrc/components/pay-mode/Pay.tsxsrc/components/pay-mode/PayMode.tsxsrc/components/pay-mode/PaySelector.tsxsrc/components/pay-mode/RepayAmountSelector.tsxsrc/components/pay-mode/UpcomingPayments.tsxsrc/components/roll-debt/RollDebt.tsxsrc/components/send-funds/Amount.tsxsrc/components/shared/InstallmentSelector.tsxsrc/components/shared/PluginUpgrade.tsxsrc/components/swaps/Failure.tsxsrc/components/swaps/Pending.tsxsrc/components/swaps/Success.tsxsrc/components/swaps/Swaps.tsxsrc/utils/accountClient.tssrc/utils/useAsset.tssrc/utils/useAuth.tssrc/utils/usePendingOperations.tssrc/utils/usePortfolio.tssrc/utils/useSimulateProposal.ts
| --- | ||
| "@exactly/mobile": patch | ||
| --- | ||
|
|
||
| ♻️ migrate remaining flows to send calls |
There was a problem hiding this comment.
Make this changeset empty unless the refactor is user-visible.
migrate remaining flows to send calls reads like an internal implementation detail, not a release note. If this is only transaction-plumbing work, this should be an empty changeset; otherwise, rewrite it in user-facing terms. Based on learnings, empty changesets are required in this repo for non-user-facing changes that do not warrant release notes.
| const showWarning = fromToken && !fromToken.external && fromAmount > 0n && (caution || danger); | ||
| const disabled = isSimulating || !!simulationError || danger; | ||
| const disabled = !route || isSimulating || !!simulationError || danger; | ||
| const buttonLabel = useMemo(() => { |
There was a problem hiding this comment.
Disable the swap CTA when the balance is insufficient.
route can still be present while isInsufficientBalance prevents both simulations from running. In that state, Line 560 still calls swap(), and the mutation falls into no external swap simulation / no swap proposal simulation instead of blocking the action.
🔧 Minimal fix
- const disabled = !route || isSimulating || !!simulationError || danger;
+ const disabled = !route || isSimulating || !!simulationError || isInsufficientBalance || danger;
const buttonLabel = useMemo(() => {
- if (isSimulating && route) return isInsufficientBalance ? t("Insufficient balance") : t("Please wait...");
+ if (isInsufficientBalance) return t("Insufficient balance");
+ if (isSimulating && route) return t("Please wait...");
if (simulationError) return t("Cannot proceed");
if (danger) return t("Enter a lower amount to swap");
if (fromToken && toToken) {
return t("Swap {{from}} for {{to}}", { from: fromToken.token.symbol, to: toToken.token.symbol });
}Also applies to: 559-566
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 20f2f6d3e7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const { calls, chainId, from, id } = params[0] as { | ||
| calls: readonly Call[]; | ||
| chainId?: Hex; |
There was a problem hiding this comment.
Respect requested chain in webauthn sendCalls
wallet_sendCalls now accepts a chainId, but this code path does not apply it for webauthn: it always sends via the default chain client and returns an id encoded with chain.id. If a caller requests a different network, webauthn users will submit on the wrong chain (or poll status against the wrong chain metadata), which breaks chain-aware send flows. Please either reject non-default chainId for webauthn explicitly or execute against a client configured for the requested chain.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/utils/useSimulateProposal.ts (1)
18-23:⚠️ Potential issue | 🟠 MajorThread
chainIdinto this shared hook instead of pinning it here.
useSimulateProposalis still hardcoded to the generated default chain, but its callers insrc/components/pay-mode/Pay.tsx:288-317andsrc/components/swaps/Swaps.tsx:252-275do not pass any chain context. That means these simulations can still run against the wrong network when the caller/account has switched elsewhere, and the returned request can be invalid at execution time.🔧 Minimal direction
export default function useSimulateProposal({ account, amount, + chainId = chain.id, market, enabled = true, ...proposal }: { account: Address | undefined; amount: bigint | undefined; + chainId?: number; enabled?: boolean; market: Address | undefined; } & ( ... )) { const { data: deployed } = useBytecode({ address: account, - chainId: chain.id, + chainId, query: { enabled: enabled && !!account }, }); const propose = useSimulateContract({ account, address: account, - chainId: chain.id, + chainId, ... }); const { data: proposalDelay } = useReadProposalManagerDelay({ address: proposalManagerAddress, - chainId: chain.id, + chainId, query: { enabled }, }); const { data: assets } = useReadExaPreviewerAssets({ address: exaPreviewerAddress, - chainId: chain.id, + chainId, query: { enabled }, }); const { data: nonce } = useReadProposalManagerQueueNonces({ address: proposalManagerAddress, - chainId: chain.id, + chainId, ... }); const executeProposal = useSimulateContract({ account, address: account, - chainId: chain.id, + chainId, ... });Also applies to: 191-223, 318-321
src/components/roll-debt/RollDebt.tsx (1)
48-65:⚠️ Potential issue | 🟠 MajorGuard
borrowMaturitybefore converting it toBigInt.If
maturityis missing or invalid,safeParsefails but this render path still evaluatesBigInt(borrowMaturity)inside the preview hook. That turns the intendedreturn nullon Line 68 into a render-time crash for malformed deep links.🔧 Proposed fix
const timestamp = Math.floor(Date.now() / 1000); const nextMaturity = timestamp - (timestamp % MATURITY_INTERVAL) + MATURITY_INTERVAL; - const borrowMaturity = Number(repayMaturity) < timestamp ? nextMaturity : Number(repayMaturity) + MATURITY_INTERVAL; + const parsedRepayMaturity = success ? Number(repayMaturity) : undefined; + const borrowMaturity = + parsedRepayMaturity === undefined + ? undefined + : parsedRepayMaturity < timestamp + ? nextMaturity + : parsedRepayMaturity + MATURITY_INTERVAL; const borrow = exaUSDC?.fixedBorrowPositions.find((b) => b.maturity === BigInt(success ? repayMaturity : 0)); const rolloverMaturityBorrow = exaUSDC?.fixedBorrowPositions.find((b) => b.maturity === BigInt(borrowMaturity)); @@ const { data: borrowPreview } = useReadPreviewerPreviewBorrowAtMaturity({ address: previewerAddress, chainId: chain.id, - args: [marketUSDCAddress, BigInt(borrowMaturity), borrow?.previewValue ?? 0n], - query: { enabled: !!bytecode && !!exaUSDC && !!borrow && !!address && !!borrowMaturity }, + args: borrowMaturity === undefined ? undefined : [marketUSDCAddress, BigInt(borrowMaturity), borrow?.previewValue ?? 0n], + query: { enabled: !!bytecode && !!exaUSDC && !!borrow && !!address && borrowMaturity !== undefined }, });
♻️ Duplicate comments (2)
src/components/shared/PluginUpgrade.tsx (1)
59-86:⚠️ Potential issue | 🟠 MajorPass
chainIdtowaitForCallsStatustoo.
mutateSendCallsis now pinned tochain.id, but the status poll on Line 86 still is not.waitForCallsStatusalso acceptschainId, and EIP-5792 batches are chain-scoped, so omitting it leaves this follow-up dependent on ambient chain selection if the wallet/network changes between submission and polling. (tessl.io)🔧 Proposed fix
- const { status } = await waitForCallsStatus(exa, { id }); + const { status } = await waitForCallsStatus(exa, { id, chainId: chain.id });Does wagmi's `waitForCallsStatus` accept an optional `chainId`, and should it match the `chainId` passed to `sendCalls` for an EIP-5792 batch?src/components/swaps/Swaps.tsx (1)
405-414:⚠️ Potential issue | 🟠 MajorKeep the swap CTA disabled on insufficient balance.
Both simulation hooks are explicitly disabled when
isInsufficientBalanceis true, so this button can still callswap()and the mutation then fails withno external swap simulation/no swap proposal simulationinstead of blocking the action.🔧 Minimal fix
- const disabled = !route || isSimulating || !!simulationError || danger; + const disabled = !route || isSimulating || !!simulationError || isInsufficientBalance || danger; const buttonLabel = useMemo(() => { - if (isSimulating && route) return isInsufficientBalance ? t("Insufficient balance") : t("Please wait..."); + if (isInsufficientBalance) return t("Insufficient balance"); + if (isSimulating && route) return t("Please wait..."); if (simulationError) return t("Cannot proceed"); if (danger) return t("Enter a lower amount to swap");Also applies to: 560-567
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c0eca9a4-2d1a-467e-9330-583f698205fd
📒 Files selected for processing (35)
.changeset/funny-aliens-mix.md.changeset/humble-cities-drop.md.changeset/wide-cats-hug.mdsrc/components/card/Card.tsxsrc/components/card/exa-card/CardContents.tsxsrc/components/defi/DeFi.tsxsrc/components/getting-started/GettingStarted.tsxsrc/components/home/AssetList.tsxsrc/components/home/CardLimits.tsxsrc/components/home/Home.tsxsrc/components/home/HomeActions.tsxsrc/components/home/Portfolio.tsxsrc/components/home/card-upgrade/UpgradeAccount.tsxsrc/components/loans/Amount.tsxsrc/components/loans/Asset.tsxsrc/components/loans/CreditLine.tsxsrc/components/loans/LoanSummary.tsxsrc/components/loans/Loans.tsxsrc/components/loans/Review.tsxsrc/components/pay-mode/OverduePayments.tsxsrc/components/pay-mode/Pay.tsxsrc/components/pay-mode/PayMode.tsxsrc/components/pay-mode/PaySelector.tsxsrc/components/pay-mode/PaymentSheet.tsxsrc/components/pay-mode/UpcomingPayments.tsxsrc/components/roll-debt/RollDebt.tsxsrc/components/send-funds/Amount.tsxsrc/components/shared/InstallmentSelector.tsxsrc/components/shared/PluginUpgrade.tsxsrc/components/shared/Success.tsxsrc/components/swaps/Swaps.tsxsrc/utils/useAsset.tssrc/utils/usePendingOperations.tssrc/utils/usePortfolio.tssrc/utils/useSimulateProposal.ts
| const needsRoute = mode === "crossRepay" || mode === "legacyCrossRepay" || mode === "external"; | ||
| const disabled = isSimulating || !!simulationError || (needsRoute && !route) || repayAssets > maxRepayInput; | ||
| const loading = isSimulating || isPending || (selectedAsset.external && isRoutePending); |
There was a problem hiding this comment.
Disable the CTA until the repay mutation can actually run.
disabled still allows presses when repayAssets === 0n or mode === "none". In those states, Line 846 can still trigger a mutation path that immediately throws (no route / unexpected mode) and sends the user into the failure flow for an action that should have been blocked.
🔧 Minimal fix
const needsRoute = mode === "crossRepay" || mode === "legacyCrossRepay" || mode === "external";
- const disabled = isSimulating || !!simulationError || (needsRoute && !route) || repayAssets > maxRepayInput;
+ const disabled =
+ mode === "none" ||
+ repayAssets === 0n ||
+ isSimulating ||
+ !!simulationError ||
+ (needsRoute && !route) ||
+ repayAssets > maxRepayInput;
@@
- onPress={selectedAsset.external ? () => repayWithExternalAsset() : () => repay()}
+ onPress={() => {
+ if (disabled) return;
+ if (selectedAsset.external) repayWithExternalAsset();
+ else repay();
+ }}Also applies to: 842-846
| mutate: proposeRollDebt, | ||
| isPending: isProposeRollDebtPending, | ||
| error: proposeRollDebtError, | ||
| } = useWriteContract({ | ||
| mutation: { | ||
| onSuccess: () => { | ||
| toast.show(t("Processing rollover"), { | ||
| native: true, | ||
| duration: 1000, | ||
| burntOptions: { haptic: "success", preset: "done" }, | ||
| }); | ||
| if (address && bytecode) refetchPendingProposals().catch(reportError); | ||
| router.dismissTo("/activity"); | ||
| }, | ||
| onError: (error) => { | ||
| toast.show(t("Rollover failed"), { | ||
| native: true, | ||
| duration: 1000, | ||
| burntOptions: { haptic: "error", preset: "error" }, | ||
| }); | ||
| reportError(error); | ||
| }, | ||
| } = useMutation({ | ||
| async mutationFn() { | ||
| if (!address) throw new Error("no address"); | ||
| if (!proposeSimulation) throw new Error("no propose roll debt simulation"); | ||
| const { address: to, abi, functionName, args } = proposeSimulation.request; | ||
| const { id } = await mutateSendCalls({ | ||
| chainId: chain.id, | ||
| calls: [{ to, data: encodeFunctionData({ abi, functionName, args }) }], | ||
| capabilities: { | ||
| paymasterService: { | ||
| url: `${chain.rpcUrls.alchemy.http[0]}/${alchemyAPIKey}`, | ||
| context: { policyId: alchemyGasPolicyId }, | ||
| }, | ||
| }, | ||
| }); | ||
| const { status } = await waitForCallsStatus(exa, { id }); | ||
| if (status === "failure") throw new Error("failed to propose rollover"); | ||
| }, | ||
| onSuccess() { | ||
| toast.show(t("Processing rollover"), { | ||
| native: true, | ||
| duration: 1000, | ||
| burntOptions: { haptic: "success", preset: "done" }, | ||
| }); | ||
| if (address && bytecode) refetchPendingProposals().catch(reportError); | ||
| router.dismissTo("/activity"); | ||
| }, | ||
| onError(error) { | ||
| toast.show(t("Rollover failed"), { | ||
| native: true, | ||
| duration: 1000, | ||
| burntOptions: { haptic: "error", preset: "error" }, | ||
| }); | ||
| reportError(error); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In the current viem/wagmi docs, what error type is raised when a wallet rejects sendCalls/wallet_sendCalls? Is it UserRejectedRequestErrorrather thanContractFunctionExecutionError?
💡 Result:
For sendCalls / wallet_sendCalls (EIP-5792), a user “reject” is treated as a request/provider rejection, i.e. UserRejectedRequestError, not a contract execution/revert error.
- In viem’s EIP-5792 implementation,
SendCallsErrorType = RequestErrorType | ErrorType(it’s an RPC request towallet_sendCalls, then wrapped viagetTransactionError). (app.unpkg.com) - viem classifies
UserRejectedRequestErrorunder its RPC/provider error set (the kind you get for EIP-1193 code4001user rejection). (viem.sh)
So yes: when the wallet rejects wallet_sendCalls, you should expect UserRejectedRequestError (or a wrapped error whose cause is that), not ContractFunctionExecutionError.
Citations:
- 1: https://app.unpkg.com/viem%402.18.2/files/experimental/eip5792/actions/sendCalls.ts
- 2: https://viem.sh/docs/glossary/errors
🏁 Script executed:
fd -t f "RollDebt.tsx" --exec wc -l {} \;Repository: exactly/exa
Length of output: 98
🏁 Script executed:
fd -t f "RollDebt.tsx" --exec sed -n '280,320p' {} \;Repository: exactly/exa
Length of output: 1519
🏁 Script executed:
fd -t f "RollDebt.tsx" --exec sed -n '325,340p' {} \;Repository: exactly/exa
Length of output: 568
🏁 Script executed:
fd -t f "RollDebt.tsx" --exec head -30 {} \;Repository: exactly/exa
Length of output: 1441
Check for UserRejectedRequestError instead of ContractFunctionExecutionError for wallet rejections in sendCalls.
After switching from contract write to sendCalls, the error handling must use UserRejectedRequestError (the wallet action rejection type) instead of ContractFunctionExecutionError. Currently, wallet cancellations trigger the error toast and reportError, then leave isError true, which keeps the button disabled even though the user intentionally rejected—they cannot retry without navigating away.
🔧 Proposed fix
-import { ContractFunctionExecutionError, encodeAbiParameters, encodeFunctionData } from "viem";
+import {
+ ContractFunctionExecutionError,
+ UserRejectedRequestError,
+ encodeAbiParameters,
+ encodeFunctionData,
+} from "viem";
@@
onError(error) {
+ if (error instanceof UserRejectedRequestError) return;
toast.show(t("Rollover failed"), {
native: true,
duration: 1000,
burntOptions: { haptic: "error", preset: "error" },
});
reportError(error);
},
});
@@
const isError =
proposeRollDebtError &&
- !(
- proposeRollDebtError instanceof ContractFunctionExecutionError &&
- proposeRollDebtError.shortMessage === "User rejected the request."
- );
+ !(proposeRollDebtError instanceof UserRejectedRequestError) &&
+ !(proposeRollDebtError instanceof ContractFunctionExecutionError && proposeRollDebtError.shortMessage === "User rejected the request.");Also applies to: 329-337
| const { status } = await waitForCallsStatus(exaConfig, { id }); | ||
| if (status === "failure") throw new Error("failed to swap"); | ||
| await queryClient.invalidateQueries({ queryKey: ["lifi", "tokenBalances"] }); | ||
| }, |
There was a problem hiding this comment.
Refresh previewer-backed balances after a successful swap.
Only ["lifi", "tokenBalances"] is invalidated here, but both swap paths also mutate the data behind useReadPreviewerExactly/usePortfolio. After closing the success screen, protocol balances, collateral usage, and available assets can stay stale until a manual refresh because those reads are still cached.
Summary by CodeRabbit
Bug Fixes
Improvements
Style