Skip to content

♻️ app: prefer sendCalls, fix chain handling#879

Open
dieguezguille wants to merge 12 commits intomainfrom
calls
Open

♻️ app: prefer sendCalls, fix chain handling#879
dieguezguille wants to merge 12 commits intomainfrom
calls

Conversation

@dieguezguille
Copy link
Member

@dieguezguille dieguezguille commented Mar 12, 2026


Open with Devin

Summary by CodeRabbit

  • Bug Fixes

    • Fixed owner wallet detection, swap query invalidation, bridge fallback, forward chain handling, and pay simulation pending-state issues.
    • Resolved cross-chain/repay edge cases and transfer simulation errors.
  • Improvements

    • App-wide multi-chain awareness for data, simulations, and transaction routing.
    • Unified, more reliable send/swap execution with better error reporting and status tracking.
    • Streamlined swap flows with simplified success/pending/failure screens and added web padding.
  • Style

    • Adjusted repay amount selector font styling.

@changeset-bot
Copy link

changeset-bot bot commented Mar 12, 2026

🦋 Changeset detected

Latest 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

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds chain-scoped context to many on-chain hooks, converts direct contract writes to mutation-driven sendCalls + waitForCallsStatus (paymaster integration), refactors Bridge to use source, restructures swap screens, and extends account client to accept optional chainId and perform chain switching.

Changes

Cohort / File(s) Summary
Changeset entries
​.changeset/bright-foxes-swim.md, ​.changeset/cool-icons-grow.md, ​.changeset/curly-pumas-jam.md, ​.changeset/cyan-flies-camp.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.md, ​.changeset/funny-aliens-mix.md, ​.changeset/humble-cities-drop.md
Add patch changeset metadata files for @exactly/mobile; no runtime code changes.
Bridge component
src/components/add-funds/Bridge.tsx
Replace effectiveSource with source across selection/queries/approval/transfer logic; add explicit error handling and move some transaction waits to waitForCallsStatus for batched submissions.
Mutation-driven on-chain flows
src/components/pay-mode/Pay.tsx, src/components/roll-debt/RollDebt.tsx, src/components/send-funds/Amount.tsx, src/components/swaps/Swaps.tsx
Replace direct writeContract calls with useMutation + encodeFunctionData + useSendCalls + waitForCallsStatus; integrate Alchemy/paymaster settings, consolidate send/propose paths and simulations.
Swap screens & UI
src/components/swaps/Failure.tsx, src/components/swaps/Pending.tsx, src/components/swaps/Success.tsx, src/components/swaps/Swaps.tsx
Simplify layouts to SafeView/ScrollView/YStack, centralize close/query invalidation, add external prop to Success, and route swap execution through new mutation/sendCalls path.
ChainId propagation across UI/hooks
src/components/.../*.tsx (card, home, loans, pay-mode, shared, etc.), src/utils/useAsset.ts, src/utils/usePendingOperations.ts, src/utils/usePortfolio.ts, src/utils/useSimulateProposal.ts, src/utils/usePendingOperations.ts
Add default chain import and pass chainId: chain.id into numerous hooks (useBytecode, useReadPreviewerExactly, installed-plugins, proposals, previewer snapshots) to scope queries by chain.
Account client / sendCalls handling
src/utils/accountClient.ts
Extend wallet_sendCalls payload to accept optional chainId, include chainId when calling sendCalls/getCallsStatus, add switchChain and encodeFunctionData handling to execute and recover from chain mismatches.
Misc UI & minor logic
src/components/pay-mode/RepayAmountSelector.tsx, src/utils/useAuth.ts, src/components/shared/InstallmentSelector.tsx, src/components/shared/PluginUpgrade.tsx, src/components/shared/Success.tsx, src/components/card/..., src/components/home/*, src/components/loans/*, src/utils/useSimulateProposal.ts
Style prop adjustments, SIWE connection caching, hook option updates to include chainId, small layout and translation additions, and re-exports/imports for chain-scoped behavior.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • cruzdanilo
  • franm91
  • nfmelendez
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: refactoring to prefer sendCalls and fixing chain handling across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch calls
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cruzdanilo cruzdanilo changed the title ♻️ app: prefer sendCalls, fix chain handling ♻️ app: prefer sendCalls, fix chain handling Mar 12, 2026
@gemini-code-assist
Copy link

Summary of Changes

Hello, 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 sendCalls for a more unified and reliable approach to interacting with smart accounts. It introduces explicit chain ID handling across various components and hooks, resolving several issues related to cross-chain operations and transaction simulations. The changes aim to enhance the application's stability, particularly for complex DeFi interactions like bridging, repaying loans, and asset swaps, while also delivering minor UI refinements.

Highlights

  • Migration to sendCalls: Many transaction flows, including repay, rollover debt, and send funds, have been migrated to use the sendCalls mechanism, improving transaction handling and consistency.
  • Chain ID Handling: Explicit chainId parameters have been added to numerous wagmi hooks and transaction functions across the application to ensure correct chain context and prevent issues.
  • Bridge Functionality Enhancements: The bridge component now correctly forwards chain IDs, fixes simulation fallbacks, and includes robust error handling with waitForCallsStatus for bridge transactions.
  • Bug Fixes and UI Improvements: Several bugs were addressed, including owner wallet detection in SIWE authentication, swaps query invalidation, and UI layout adjustments for swap and repay screens.
Changelog
  • @exactly/mobile
    • 💄 restructure swap state screen layouts
    • 🐛 forward chain id in account client
    • 🐛 fix owner wallet detection in siwe auth
    • 🐛 fix swaps query invalidation
    • 🐛 fix bridge simulation falling back to default account
    • 🐛 pass chain id to bridge calls
    • 💄 fix repay amount selector font style
    • ♻️ migrate remaining flows to send calls
    • 💄 add bottom padding to swaps screen on web
    • ♻️ pass explicit chain id to read hooks
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@sentry
Copy link

sentry bot commented Mar 12, 2026

Codecov Report

❌ Patch coverage is 70.71429% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.94%. Comparing base (19fba55) to head (20f2f6d).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/components/pay-mode/Pay.tsx 56.25% 21 Missing ⚠️
src/components/send-funds/Amount.tsx 77.77% 10 Missing ⚠️
src/utils/accountClient.ts 25.00% 6 Missing ⚠️
src/components/roll-debt/RollDebt.tsx 84.21% 3 Missing ⚠️
...rc/components/home/card-upgrade/UpgradeAccount.tsx 0.00% 1 Missing ⚠️
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              
Flag Coverage Δ
e2e 70.94% <70.71%> (+4.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +165 to 173
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({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

sentry[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@dieguezguille dieguezguille force-pushed the calls branch 2 times, most recently from 991a102 to 80f63cc Compare March 12, 2026 16:29
coderabbitai[bot]

This comment was marked as resolved.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Add chainId: chain.id to external asset mutateSendCalls invocation.

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 | 🟠 Major

Rewrite or drop this stale changeset entry.

This note still says “add bottom padding to swaps screen on web”, but this PR is about sendCalls and 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/*.md must start with a gitmoji and follow the format <emoji> <message> (no scope).

src/components/shared/PluginUpgrade.tsx (1)

33-46: ⚠️ Potential issue | 🔴 Critical

Pin the upgrade write path to chain.id as 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 | 🔴 Critical

Pin 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 | 🟠 Major

Add chainId: chain.id to the mutateSendCalls invocation.

The mutation uses chain-scoped simulation and polling, but mutateSendCalls is called without an explicit chainId. 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 | 🟠 Major

Add chainId: chain.id to the mutateSendCalls invocation.

All simulations in this component are chain-scoped, but mutateSendCalls is called without an explicit chainId. This can result in the transaction being submitted on the wallet's currently connected chain instead of the intended chain. The subsequent waitForCallsStatus call uses the single-chain exa config, 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 | 🟠 Major

Add chainId: chain.id to mutateSendCalls invocation.

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 while waitForCallsStatus polls on exa'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

📥 Commits

Reviewing files that changed from the base of the PR and between 80f63cc and 70e9366.

📒 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.md
  • src/components/add-funds/Bridge.tsx
  • src/components/card/Card.tsx
  • src/components/card/exa-card/CardContents.tsx
  • src/components/defi/DeFi.tsx
  • src/components/getting-started/GettingStarted.tsx
  • src/components/home/AssetList.tsx
  • src/components/home/CardLimits.tsx
  • src/components/home/Home.tsx
  • src/components/home/HomeActions.tsx
  • src/components/home/Portfolio.tsx
  • src/components/home/card-upgrade/UpgradeAccount.tsx
  • src/components/loans/Amount.tsx
  • src/components/loans/Asset.tsx
  • src/components/loans/CreditLine.tsx
  • src/components/loans/LoanSummary.tsx
  • src/components/loans/Loans.tsx
  • src/components/loans/Review.tsx
  • src/components/pay-mode/OverduePayments.tsx
  • src/components/pay-mode/Pay.tsx
  • src/components/pay-mode/PayMode.tsx
  • src/components/pay-mode/PaySelector.tsx
  • src/components/pay-mode/RepayAmountSelector.tsx
  • src/components/pay-mode/UpcomingPayments.tsx
  • src/components/roll-debt/RollDebt.tsx
  • src/components/send-funds/Amount.tsx
  • src/components/shared/InstallmentSelector.tsx
  • src/components/shared/PluginUpgrade.tsx
  • src/components/swaps/Failure.tsx
  • src/components/swaps/Pending.tsx
  • src/components/swaps/Success.tsx
  • src/components/swaps/Swaps.tsx
  • src/utils/accountClient.ts
  • src/utils/useAsset.ts
  • src/utils/useAuth.ts
  • src/utils/usePendingOperations.ts
  • src/utils/usePortfolio.ts
  • src/utils/useSimulateProposal.ts

Comment on lines +1 to +5
---
"@exactly/mobile": patch
---

♻️ migrate remaining flows to send calls
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines 403 to 405
const showWarning = fromToken && !fromToken.external && fromAmount > 0n && (caution || danger);
const disabled = isSimulating || !!simulationError || danger;
const disabled = !route || isSimulating || !!simulationError || danger;
const buttonLabel = useMemo(() => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +165 to +167
const { calls, chainId, from, id } = params[0] as {
calls: readonly Call[];
chainId?: Hex;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Thread chainId into this shared hook instead of pinning it here.

useSimulateProposal is still hardcoded to the generated default chain, but its callers in src/components/pay-mode/Pay.tsx:288-317 and src/components/swaps/Swaps.tsx:252-275 do 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 | 🟠 Major

Guard borrowMaturity before converting it to BigInt.

If maturity is missing or invalid, safeParse fails but this render path still evaluates BigInt(borrowMaturity) inside the preview hook. That turns the intended return null on 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 | 🟠 Major

Pass chainId to waitForCallsStatus too.

mutateSendCalls is now pinned to chain.id, but the status poll on Line 86 still is not. waitForCallsStatus also accepts chainId, 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 | 🟠 Major

Keep the swap CTA disabled on insufficient balance.

Both simulation hooks are explicitly disabled when isInsufficientBalance is true, so this button can still call swap() and the mutation then fails with 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");

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

📥 Commits

Reviewing files that changed from the base of the PR and between 70e9366 and 20f2f6d.

📒 Files selected for processing (35)
  • .changeset/funny-aliens-mix.md
  • .changeset/humble-cities-drop.md
  • .changeset/wide-cats-hug.md
  • src/components/card/Card.tsx
  • src/components/card/exa-card/CardContents.tsx
  • src/components/defi/DeFi.tsx
  • src/components/getting-started/GettingStarted.tsx
  • src/components/home/AssetList.tsx
  • src/components/home/CardLimits.tsx
  • src/components/home/Home.tsx
  • src/components/home/HomeActions.tsx
  • src/components/home/Portfolio.tsx
  • src/components/home/card-upgrade/UpgradeAccount.tsx
  • src/components/loans/Amount.tsx
  • src/components/loans/Asset.tsx
  • src/components/loans/CreditLine.tsx
  • src/components/loans/LoanSummary.tsx
  • src/components/loans/Loans.tsx
  • src/components/loans/Review.tsx
  • src/components/pay-mode/OverduePayments.tsx
  • src/components/pay-mode/Pay.tsx
  • src/components/pay-mode/PayMode.tsx
  • src/components/pay-mode/PaySelector.tsx
  • src/components/pay-mode/PaymentSheet.tsx
  • src/components/pay-mode/UpcomingPayments.tsx
  • src/components/roll-debt/RollDebt.tsx
  • src/components/send-funds/Amount.tsx
  • src/components/shared/InstallmentSelector.tsx
  • src/components/shared/PluginUpgrade.tsx
  • src/components/shared/Success.tsx
  • src/components/swaps/Swaps.tsx
  • src/utils/useAsset.ts
  • src/utils/usePendingOperations.ts
  • src/utils/usePortfolio.ts
  • src/utils/useSimulateProposal.ts

Comment on lines +558 to 560
const needsRoute = mode === "crossRepay" || mode === "legacyCrossRepay" || mode === "external";
const disabled = isSimulating || !!simulationError || (needsRoute && !route) || repayAssets > maxRepayInput;
const loading = isSimulating || isPending || (selectedAsset.external && isRoutePending);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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

Comment on lines +282 to +318
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 to wallet_sendCalls, then wrapped via getTransactionError). (app.unpkg.com)
  • viem classifies UserRejectedRequestError under its RPC/provider error set (the kind you get for EIP-1193 code 4001 user 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:


🏁 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

Comment on lines +381 to +384
const { status } = await waitForCallsStatus(exaConfig, { id });
if (status === "failure") throw new Error("failed to swap");
await queryClient.invalidateQueries({ queryKey: ["lifi", "tokenBalances"] });
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants