Skip to content

Comments

PRO-3185-Gas_Abstraction#308

Merged
vignesha22 merged 13 commits intostagingfrom
PRO-3185-Gas_Abstraction
Jun 19, 2025
Merged

PRO-3185-Gas_Abstraction#308
vignesha22 merged 13 commits intostagingfrom
PRO-3185-Gas_Abstraction

Conversation

@vignesha22
Copy link
Contributor

@vignesha22 vignesha22 commented May 16, 2025

Description

  • Added Gas Abstraction for send flow

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Summary by CodeRabbit

  • New Features

    • Added support for gasless and ERC20 token fee payments in the send modal, allowing users to choose fee type and select fee assets when paymaster mode is active.
    • Introduced UI controls for fee selection and asset selection.
    • Integrated wallet portfolio fetching and paymaster filtering for fee payment options.
    • Added backend support for fetching gasless paymasters and gas price data.
    • Included environment configuration for paymaster and RPC service URLs.
  • Bug Fixes

    • Improved validation to prevent sending transactions if the user has insufficient balance for gasless transactions or unsupported paymaster actions.
  • Documentation

    • Added new labels and error messages to support the updated fee payment and error handling features.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 16, 2025

## Walkthrough

The update introduces paymaster-based fee payment to the `SendModalTokensTabView` component, enabling gasless and ERC20 token fee options. It adds state management, UI controls for fee type and asset selection, backend integration for paymaster data and gas price fetching, approval calldata computation, and validation logic. Supporting error messages and a new gasless service module are also included.

## Changes

| File(s)                                                                                     | Change Summary                                                                                                                                                                                                                                         |
|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx                         | Extended to support paymaster-based fee payment, including new state variables, UI controls for fee type/asset selection, paymaster logic, approval data calculation, Redux integration for wallet portfolio, and enhanced transaction batching logic. |
| src/translations/en.json                                                                    | Added new error messages for gasless transaction issues and new labels for fee type and asset selection.                                                                                                         |
| src/services/gasless.ts                                                                     | New module providing types and functions to fetch paymaster data and gas prices, including filtering by chain and token, plus error handling.                                                                                                   |
| .env.example                                                                               | Added environment variables for RPC URL, feature toggle, and paymaster service URL.                                                                                                                                                            |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant SendModalTokensTabView
    participant ReduxStore
    participant GaslessService
    participant EtherspotBatches

    User->>SendModalTokensTabView: Opens Send Modal
    SendModalTokensTabView->>ReduxStore: Fetch wallet portfolio
    SendModalTokensTabView->>GaslessService: getAllGaslessPaymasters(chainId, tokens)
    GaslessService-->>SendModalTokensTabView: Returns paymaster list
    SendModalTokensTabView->>User: Render fee type and fee asset selectors (if paymaster mode)
    User->>SendModalTokensTabView: Selects fee type and fee asset
    SendModalTokensTabView->>GaslessService: getGasPrice(chainId)
    GaslessService-->>SendModalTokensTabView: Returns gas price
    SendModalTokensTabView->>SendModalTokensTabView: Calculate approval calldata
    User->>SendModalTokensTabView: Confirms send
    SendModalTokensTabView->>EtherspotBatches: Submit transaction(s) with paymaster config and approval tx if needed

Possibly related PRs

Suggested reviewers

  • IAmKio

Poem

In the warren, gasless dreams come true,
With paymasters hopping into view.
Tokens or ether, pick your fee,
The modal now brings flexibility!
If errors arise, don’t feel blue—
For every new feature, we debug for you.
🐇✨


<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> npm error Exit handler never called!
> npm error This is an error with npm itself. Please report this error at:
> npm error   <https://github.com/npm/cli/issues>
> npm error A complete log of this run can be found in: /.npm/_logs/2025-06-19T13_27_43_155Z-debug-0.log
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->
<!-- internal state start -->


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

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=pillarwallet/x&utm_content=308):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented May 16, 2025

Deploying x with  Cloudflare Pages  Cloudflare Pages

Latest commit: d2bb0de
Status: ✅  Deploy successful!
Preview URL: https://dc5e7e6e.x-e62.pages.dev
Branch Preview URL: https://pro-3185-gas-abstraction.x-e62.pages.dev

View logs

useWalletAddress,
} from '@etherspot/transaction-kit';
import { BigNumber, ethers } from 'ethers';
import { BigNumber, ethers, utils } from 'ethers';

Choose a reason for hiding this comment

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

Can we replace ethers with viem? Otherwise we will end up with dependencies for both of these libraries in the PillarX binary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we can remove once we get rid of ethers from pillarX itself since we should be adding helper functions just like how we need in sdk to convert string/number to bigInt and then only we can use viem. The reason is that ethers BigNumber had a BigNumberish class which takes in both

Choose a reason for hiding this comment

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

@RanaBug @IAmKio kindly note. We can defer these changes for now. But we have to change this in future.

Copy link
Contributor

@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: 3

🧹 Nitpick comments (4)
src/translations/en.json (2)

49-51: Consider phrasing the new error keys more generically

"insufficientBalanceForGasless" and "paymasterNotSupported" are tightly coupled to the current implementation details.
If other chains / paymaster modes are introduced later these keys might need to be reused (or replaced) with different wording.
A more generic wording such as "insufficientBalanceForPaymaster" and "paymasterBatchingNotSupported" would keep the i18n surface stable.


75-76: Key naming consistency

Every other label key follows the pattern select<X>. Using "selectFeeAsset" is consistent 👌, but "feeType" breaks that pattern ("selectFeeType" would be aligned with the rest). Renaming now saves a future breaking-change in the UI.

src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (2)

212-293: Missing cleanup & race-condition guard in paymaster discovery

fetch(...).then(...setState) may attempt to update state after the component unmounts or after another asset selection outraces the first request.
Add an AbortController and an isCancelled flag to prevent memory leaks and wrong option lists.


692-701: Unsafe parsing of SelectOption.id

handleOnChange splits id on '-' assuming exactly four parts.
If a token address itself contains '-' (e.g. ENS name or future formatting) the parsing will break.

Consider serialising the payload as JSON in id (or place the extra data into value) rather than relying on string splitting.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 2add3fc and f6974b6.

📒 Files selected for processing (2)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (13 hunks)
  • src/translations/en.json (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (7)
src/components/Form/AssetSelect/index.tsx (1)
  • TokenAssetSelectOption (36-41)
src/types/api.ts (1)
  • PortfolioData (723-732)
src/apps/the-exchange/reducer/theExchangeSlice.ts (1)
  • setWalletPortfolio (107-112)
src/services/pillarXApiWalletPortfolio.ts (1)
  • convertPortfolioAPIResponseToToken (13-35)
src/services/tokensData.ts (2)
  • Token (11-21)
  • chainNameToChainIdTokensData (237-258)
src/utils/blockchain.ts (1)
  • isPolygonAssetNative (55-58)
src/components/Form/Select/index.tsx (2)
  • SelectOption (11-17)
  • Select (65-136)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages

Comment on lines +476 to +487
const amountLeft = +getAmountLeft(
selectedAsset,
amount,
selectedAssetBalance
);
if (!feeMin) return;
if (amountLeft < +feeMin) {
setErrorMessage(t`error.insufficientBalanceForGasless`);
setIsSending(false);
return;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Balance check uses the wrong token

amountLeft is calculated from the asset being sent, but the fee is paid with selectedFeeAsset.
If a user sends token A and pays fees with token B the check will be meaningless (and can block valid transfers).

-const amountLeft = +getAmountLeft(
-  selectedAsset,
-  amount,
-  selectedAssetBalance
-);
-if (!feeMin) return;
-if (amountLeft < +feeMin) {
+if (!selectedFeeAsset || !feeMin) return;
+const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+  accountAddress as string
+]?.find((b) =>
+  addressesEqual(b.token, selectedFeeAsset.token)
+  || (selectedFeeAsset.token === ethers.constants.AddressZero && b.token === null)
+)?.balance ?? 0;
+
+const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
+  feeTokenBalance,
+  selectedFeeAsset.decimals,
+);
+
+if (feeTokenBalanceFormatted < +feeMin) {
   setErrorMessage(t`error.insufficientBalanceForGasless`);
   setIsSending(false);
   return;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const amountLeft = +getAmountLeft(
selectedAsset,
amount,
selectedAssetBalance
);
if (!feeMin) return;
if (amountLeft < +feeMin) {
setErrorMessage(t`error.insufficientBalanceForGasless`);
setIsSending(false);
return;
}
}
if (!selectedFeeAsset || !feeMin) return;
const feeTokenBalance = accountBalances
?.[selectedAsset?.chainId ?? 0]
?.[accountAddress as string]
?.find(
(b) =>
addressesEqual(b.token, selectedFeeAsset.token) ||
(selectedFeeAsset.token === ethers.constants.AddressZero && b.token === null)
)
?.balance ?? 0;
const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
feeTokenBalance,
selectedFeeAsset.decimals,
);
if (feeTokenBalanceFormatted < +feeMin) {
setErrorMessage(t`error.insufficientBalanceForGasless`);
setIsSending(false);
return;
}
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 476 to 487, the balance check incorrectly compares the amount left of the
sent asset against the fee minimum, but fees are paid with selectedFeeAsset.
Update the calculation to check the balance of selectedFeeAsset instead of
selectedAsset when comparing against feeMin, ensuring the fee balance validation
is done on the correct token to avoid blocking valid transfers.

Comment on lines 187 to 210
const getGasPrice = async (chainId: number) => {
const gasRes = await fetch(
`https://rpc.etherspot.io/v2/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
}
);
gasRes.json().then((response) => {
if (response.result) {
const totalGasPrice = BigNumber.from(response.result.maxFeePerGas)
.add(response.result.maxPriorityFeePerGas)
.toString();
setGasPrice(totalGasPrice);
}
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

getGasPrice lacks error handling and can leave unresolved promises

The function fires fetch and afterwards chains .json().then(...) without a surrounding try / catch.
A network- or JSON-parsing failure will raise an uncaught Promise rejection and break React’s error boundary.

-const gasRes = await fetch(
+let gasRes: Response;
+try {
+  gasRes = await fetch(
     `https://rpc.etherspot.io/v2/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
     {
       ...
     }
   );
- gasRes.json().then((response) => {
-   if (response.result) {
-     ...
-   }
- });
+ const response = await gasRes.json();
+ if (response?.result) {
+   const totalGasPrice = BigNumber
+     .from(response.result.maxFeePerGas)
+     .add(response.result.maxPriorityFeePerGas)
+     .toString();
+   setGasPrice(totalGasPrice);
+ }
+} catch (e) {
+  console.error('Failed to fetch gas price', e);
+  // fallback – disable paymaster rather than crashing UI
+  setGasPrice(undefined);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getGasPrice = async (chainId: number) => {
const gasRes = await fetch(
`https://rpc.etherspot.io/v2/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
}
);
gasRes.json().then((response) => {
if (response.result) {
const totalGasPrice = BigNumber.from(response.result.maxFeePerGas)
.add(response.result.maxPriorityFeePerGas)
.toString();
setGasPrice(totalGasPrice);
}
});
};
const getGasPrice = async (chainId: number) => {
let gasRes: Response;
try {
gasRes = await fetch(
`https://rpc.etherspot.io/v2/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
}
);
const response = await gasRes.json();
if (response?.result) {
const totalGasPrice = BigNumber
.from(response.result.maxFeePerGas)
.add(response.result.maxPriorityFeePerGas)
.toString();
setGasPrice(totalGasPrice);
}
} catch (e) {
console.error('Failed to fetch gas price', e);
// fallback – disable paymaster rather than crashing UI
setGasPrice(undefined);
}
};
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx between
lines 187 and 210, the getGasPrice function lacks error handling around the
fetch call and the subsequent JSON parsing, which can cause uncaught promise
rejections on network or parsing failures. To fix this, wrap the entire async
operation in a try/catch block, await the fetch and json parsing properly, and
handle any errors by catching exceptions and optionally logging or setting an
error state to prevent unhandled promise rejections.

@github-actions github-actions bot temporarily deployed to Preview (PRO-3185-Gas_Abstraction) May 19, 2025 09:53 Inactive
Copy link
Contributor

@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

♻️ Duplicate comments (4)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (4)

16-16: Consider migrating from ethers to viem.

The PR introduces both ethers and viem libraries. This creates a dual dependency situation, which can lead to increased bundle size and maintenance overhead.

As noted in a previous review, consider replacing ethers with viem to standardize on a single library throughout the codebase. This would reduce bundle size and simplify maintenance.


187-210: Fix error handling in the getGasPrice function.

The getGasPrice function lacks proper error handling around the fetch call and JSON parsing, potentially causing uncaught promise rejections.

As noted in a previous review, the function needs error handling to prevent uncaught Promise rejections. Wrap the fetch and JSON parsing in try/catch and set a fallback state when errors occur.


493-505: Fix incorrect balance check for paymaster fees.

The current implementation compares the amount left of the sent asset against the fee minimum, but fees are paid with the selectedFeeAsset.

As previously noted, the balance check uses the wrong token. amountLeft is calculated from the asset being sent, but the fee is paid with selectedFeeAsset. Update the calculation to check the balance of selectedFeeAsset instead.


781-794: Reorder approve transaction to precede the primary transaction.

For ERC-20 paymaster mode, the approve transaction must come before the primary transaction, otherwise the paymaster validation will fail.

As previously noted, the approve transaction must precede the primary transaction. Reorder the transaction components so the approval comes first for the paymaster to work correctly.

🧹 Nitpick comments (5)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (5)

125-125: Consider moving this URL to a global constant.

Hardcoded URLs should be defined as constants at a global level for easier maintenance and consistency.

Move this and other service URLs to a global constants file to make them easier to update across the application.


127-145: Refactor complex state management using custom hooks.

The component has numerous state variables that could be better organized with custom hooks to improve code maintainability.

Consider creating a custom hook like usePaymasterState to encapsulate all these related state variables:

// hooks/usePaymasterState.ts
export function usePaymasterState() {
  const [isPaymaster, setIsPaymaster] = React.useState<boolean>(false);
  const [paymasterContext, setPaymasterContext] = React.useState<{
    mode: string;
    token?: string;
  } | null>({ mode: 'sponsor' });
  const [selectedPaymasterAddress, setSelectedPaymasterAddress] = React.useState<string>('');
  const [selectedFeeAsset, setSelectedFeeAsset] = React.useState<{
    token: string;
    decimals: number;
  }>();
  // ... other state variables

  return {
    isPaymaster,
    setIsPaymaster,
    paymasterContext,
    setPaymasterContext,
    // ... other variables and setters
  };
}

710-732: Improve handling of fee asset and fee type selection.

The current handlers contain logic that should be extracted into helper functions for better organization and testability.

Refactor these handlers for clarity and separation of concerns:

const handleFeeAssetSelection = (value: SelectOption) => {
  const [tokenAddress, , paymasterAddress, decimals] = value.id.split('-');
  
  setSelectedFeeAsset({
    token: tokenAddress,
    decimals: Number(decimals) || 18,
  });
  
  setSelectedPaymasterAddress(paymasterAddress);
};

const handleFeeTypeSelection = (value: SelectOption) => {
  const isGaslessMode = value.title === 'Gasless';
  
  if (isGaslessMode) {
    setPaymasterContext({
      mode: 'commonerc20',
      token: selectedFeeAsset?.token,
    });
    setIsPaymaster(true);
  } else {
    setPaymasterContext(null);
    setIsPaymaster(false);
  }
};

323-365: Extract environment variable constants to a configuration file.

The code contains multiple direct references to environment variables for gas-related constants.

Move these to a dedicated configuration file:

// config/gasConstants.ts
export const GAS_CONSUMPTION = {
  NATIVE: {
    DEFAULT: process.env.REACT_APP_NATIVE_GAS_CONSUMPTION,
    ARBITRUM: process.env.REACT_APP_NATIVE_GAS_CONSUMPTION_ARBITRUM,
  },
  TOKEN: {
    DEFAULT: process.env.REACT_APP_TOKEN_GAS_CONSUMPTION,
    ARBITRUM: process.env.REACT_APP_TOKEN_GAS_CONSUMPTION_ARBITRUM,
  },
  NFT: {
    DEFAULT: process.env.REACT_APP_NFT_GAS_CONSUMPTION,
    ARBITRUM: process.env.REACT_APP_NFT_GAS_CONSUMPTION_ARBITRUM,
  },
};

Then simplify the code using this configuration:

const chainType = selectedAsset.chainId === 42161 ? 'ARBITRUM' : 'DEFAULT';
const assetType = selectedAsset.asset.contract === ethers.constants.AddressZero ? 'NATIVE' : 'TOKEN';
gasCost = Number(GAS_CONSUMPTION[assetType][chainType]);

734-744: Simplify the effect for updating context when fee asset changes.

This effect has a simple logic that can be improved for readability.

Simplify this effect:

- useEffect(() => {
-  if (!selectedFeeAsset) return;
-  if (isPaymaster && paymasterContext?.mode === 'commonerc20') {
-    setPaymasterContext({
-      mode: 'commonerc20',
-      token: selectedFeeAsset.token,
-    });
-  }
-  // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [selectedFeeAsset]);

+ useEffect(() => {
+  if (selectedFeeAsset && isPaymaster && paymasterContext?.mode === 'commonerc20') {
+    setPaymasterContext(prev => ({
+      ...prev,
+      token: selectedFeeAsset.token,
+    }));
+  }
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedFeeAsset]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between f6974b6 and cc84f8c.

📒 Files selected for processing (1)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (13 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages

Comment on lines 141 to 142
`?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consistently handle API keys and endpoint URLs.

There are inconsistencies in how API endpoints and keys are constructed in different parts of the code.

Create utility functions for constructing API URLs and queries:

// utils/apiUtils.ts
export function getPaymasterApiUrl(endpoint: string, params?: Record<string, string>) {
  const baseUrl = process.env.REACT_APP_PAYMASTER_URL;
  const apiKey = process.env.REACT_APP_PAYMASTER_API_KEY;
  
  const queryParams = new URLSearchParams({ apiKey });
  
  if (params) {
    Object.entries(params).forEach(([key, value]) => {
      queryParams.append(key, value);
    });
  }
  
  return `${baseUrl}/${endpoint}?${queryParams.toString()}`;
}

Then use this consistently throughout the code:

const url = getPaymasterApiUrl('getAllCommonERC20PaymasterAddress');

Also applies to: 217-218, 220-220

🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 141-142 and also lines 217-218 and 220, the construction of API endpoints
and inclusion of API keys is inconsistent. To fix this, create a utility
function (e.g., getPaymasterApiUrl) that takes an endpoint and optional query
parameters, reads the base URL and API key from environment variables, and
returns a properly formatted URL with all query parameters including the API
key. Replace all direct string constructions of these URLs in the specified
lines with calls to this utility function to ensure consistency and
maintainability.

Comment on lines 219 to 291
fetch(
`${paymasterUrl}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
{
method: 'POST',
body: JSON.stringify({}),
}
).then((res) => {
res.json().then((data) => {
if (data.message) {
let paymasterObject = JSON.parse(data.message);
paymasterObject = paymasterObject.filter(
(item: { epVersion: string; chainId: number; gasToken: string }) =>
item.epVersion === 'EPV_07' &&
item.chainId === selectedAsset?.chainId &&
tokens.find(
(token: Token) =>
token.contract === item.gasToken.toLowerCase() ||
(isPolygonAssetNative(item.gasToken, item.chainId) &&
token.contract === ethers.constants.AddressZero)
)
);
const feeOptions = paymasterObject.map(
(item: {
gasToken: string;
chainId: number;
epVersion: string;
paymasterAddress: string;
// eslint-disable-next-line consistent-return, array-callback-return
}) => {
const tokenData = tokens.find(
(token: Token) => token.contract === item.gasToken.toLowerCase()
);
if (tokenData)
return {
id: `${item.gasToken}-${item.chainId}-${item.paymasterAddress}-${tokenData.decimals}`,
type: 'token',
title: tokenData.name,
imageSrc: tokenData.logo,
chainId: chainNameToChainIdTokensData(tokenData.blockchain),
value: tokenData.balance,
price: tokenData.price,
asset: {
...tokenData,
contract: item.gasToken,
decimals: tokenData.decimals,
},
};
}
);
setFeeAssetOptions(feeOptions);
if (feeOptions.length > 0) {
// get Skandha gas price
getGasPrice(selectedAsset.chainId);
setSelectedFeeAsset({
token: feeOptions[0].asset.contract,
decimals: feeOptions[0].asset.decimals,
});
setSelectedPaymasterAddress(feeOptions[0].id.split('-')[2]);
setPaymasterContext({
mode: 'commonerc20',
token: feeOptions[0].asset.contract,
});
setIsPaymaster(true);
} else {
setFeeType([]);
}
} else {
setPaymasterContext(null);
setIsPaymaster(false);
setFeeType([]);
}
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor paymaster address fetching for better error handling and cleaner code.

This block has several issues:

  1. Lacks proper error handling for fetch
  2. Contains deeply nested code that's hard to follow
  3. Uses synchronous JSON.parse on potentially large data

Refactor with async/await, proper error handling, and extract filtering logic:

- fetch(
-   `${paymasterUrl}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
-   {
-     method: 'POST',
-     body: JSON.stringify({}),
-   }
- ).then((res) => {
-   res.json().then((data) => {
-     if (data.message) {
-       let paymasterObject = JSON.parse(data.message);
-       // ... more nested code ...
-     } else {
-       setPaymasterContext(null);
-       setIsPaymaster(false);
-       setFeeType([]);
-     }
-   });
- });

+ try {
+   const res = await fetch(
+     `${paymasterUrl}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
+     {
+       method: 'POST',
+       body: JSON.stringify({}),
+     }
+   );
+   
+   const data = await res.json();
+   
+   if (!data.message) {
+     setPaymasterContext(null);
+     setIsPaymaster(false);
+     setFeeType([]);
+     return;
+   }
+   
+   const paymasterObject = JSON.parse(data.message);
+   const filteredPaymasters = filterCompatiblePaymasters(paymasterObject, selectedAsset, tokens);
+   const feeOptions = mapPaymastersToFeeOptions(filteredPaymasters, tokens);
+   
+   setFeeAssetOptions(feeOptions);
+   
+   if (feeOptions.length > 0) {
+     setupPaymasterWithFirstOption(feeOptions, selectedAsset);
+   } else {
+     setFeeType([]);
+   }
+ } catch (error) {
+   console.error('Failed to fetch paymaster addresses:', error);
+   setPaymasterContext(null);
+   setIsPaymaster(false);
+   setFeeType([]);
+ }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx between
lines 219 and 291, refactor the fetch call to use async/await syntax instead of
nested then calls for better readability. Add try/catch blocks to handle fetch
and JSON parsing errors gracefully. Extract the filtering and mapping logic for
paymaster objects into separate helper functions to reduce nesting and improve
clarity. Replace synchronous JSON.parse with safer parsing inside try/catch to
handle malformed data without crashing. Ensure all error cases update state
appropriately to reflect failure or empty results.

Comment on lines 1003 to 1011
{isPaymaster &&
selectedPaymasterAddress &&
selectedFeeAsset &&
approveData && (
<EtherspotTransaction
to={selectedFeeAsset.token}
data={approveData}
/>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure paymaster approval transaction is rendered correctly.

The approval transaction component has a potentially unnecessary check for approveData, which might not be initialized if there are errors in fetching gas prices or token prices.

Add appropriate error handling and user feedback:

{isPaymaster &&
  selectedPaymasterAddress &&
  selectedFeeAsset &&
- approveData && (
+ (
+   <>
+     {!approveData && (
+       <ErrorMessage>Error generating approval transaction. Gas price data may be unavailable.</ErrorMessage>
+     )}
+     {approveData && (
      <EtherspotTransaction
        to={selectedFeeAsset.token}
        data={approveData}
      />
+     )}
+   </>
  )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isPaymaster &&
selectedPaymasterAddress &&
selectedFeeAsset &&
approveData && (
<EtherspotTransaction
to={selectedFeeAsset.token}
data={approveData}
/>
)}
{isPaymaster &&
selectedPaymasterAddress &&
selectedFeeAsset && (
<>
{!approveData && (
<ErrorMessage>
Error generating approval transaction. Gas price data may be unavailable.
</ErrorMessage>
)}
{approveData && (
<EtherspotTransaction
to={selectedFeeAsset.token}
data={approveData}
/>
)}
</>
)}
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 1003 to 1011, the rendering of the paymaster approval transaction depends
on the presence of approveData, which may be undefined due to errors in fetching
gas or token prices. To fix this, add error handling to detect when approveData
is missing or invalid and provide user feedback such as an error message or a
loading indicator. This ensures the UI gracefully handles cases where
approveData is not available instead of silently skipping the transaction
rendering.

Comment on lines 295 to 321
const setApprovalData = async (gasCost: number) => {
if (selectedFeeAsset && gasPrice) {
const estimatedCost = Number(
utils.formatEther(BigNumber.from(gasCost).mul(gasPrice))
);
const costAsFiat = +estimatedCost * nativeAssetPrice;
const feeTokenPrice = await getPrices([selectedFeeAsset.token]);
let estimatedCostInToken;
if (feeTokenPrice) {
const feeTokenPriceInUSD = feeTokenPrice[0].usd;
estimatedCostInToken = (costAsFiat / feeTokenPriceInUSD).toFixed(
selectedFeeAsset.decimals
);
setFeeMin(estimatedCostInToken);
setApproveData(
encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [
selectedPaymasterAddress as Address,
parseUnits(estimatedCostInToken, selectedFeeAsset.decimals),
],
})
);
}
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance price fetching with error handling and confirmation.

The setApprovalData function lacks proper error handling and may calculate incorrect approval amounts if token price data is unavailable.

Add error handling and validation to the price fetching:

const setApprovalData = async (gasCost: number) => {
  if (selectedFeeAsset && gasPrice) {
    try {
      const estimatedCost = Number(
        utils.formatEther(BigNumber.from(gasCost).mul(gasPrice))
      );
      const costAsFiat = +estimatedCost * nativeAssetPrice;
      const feeTokenPrice = await getPrices([selectedFeeAsset.token]);
      
+     if (!feeTokenPrice || !feeTokenPrice[0]?.usd) {
+       console.error('Failed to fetch token price');
+       return;
+     }
      
      const feeTokenPriceInUSD = feeTokenPrice[0].usd;
      const estimatedCostInToken = (costAsFiat / feeTokenPriceInUSD).toFixed(
        selectedFeeAsset.decimals
      );
      
      setFeeMin(estimatedCostInToken);
      setApproveData(
        encodeFunctionData({
          abi: erc20Abi,
          functionName: 'approve',
          args: [
            selectedPaymasterAddress as Address,
            parseUnits(estimatedCostInToken, selectedFeeAsset.decimals),
          ],
        })
      );
+   } catch (error) {
+     console.error('Failed to generate approval data:', error);
+   }
  }
};

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx between
lines 295 and 321, the setApprovalData function fetches token prices without
error handling, risking incorrect approval amounts if price data is missing. To
fix this, add try-catch around the getPrices call to handle potential errors
gracefully, and validate that feeTokenPrice and feeTokenPrice[0].usd exist
before using them. If price data is unavailable, avoid setting approval data or
set a safe default to prevent incorrect calculations.

@github-actions github-actions bot temporarily deployed to Preview (PRO-3185-Gas_Abstraction) May 19, 2025 10:12 Inactive
Copy link
Contributor

@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: 2

♻️ Duplicate comments (5)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (5)

16-16: Consider replacing ethers with viem to standardize dependencies.

The code is currently using both ethers.js and viem libraries, which could lead to dependency bloat in the final bundle.


187-210: ⚠️ Potential issue

getGasPrice lacks error handling for fetch and JSON parsing.

The function doesn't handle network failures or JSON parsing errors, which could lead to unhandled promise rejections and break React's error boundary.

-const getGasPrice = async (chainId: number) => {
-  const gasRes = await fetch(
-    `${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
-    {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify({
-        jsonrpc: '2.0',
-        id: 1,
-        method: 'skandha_getGasPrice',
-      }),
-    }
-  );
-  gasRes.json().then((response) => {
-    if (response.result) {
-      const totalGasPrice = BigNumber.from(response.result.maxFeePerGas)
-        .add(response.result.maxPriorityFeePerGas)
-        .toString();
-      setGasPrice(totalGasPrice);
-    }
-  });
+const getGasPrice = async (chainId: number) => {
+  try {
+    const gasRes = await fetch(
+      `${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
+      {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          jsonrpc: '2.0',
+          id: 1,
+          method: 'skandha_getGasPrice',
+        }),
+      }
+    );
+    
+    const response = await gasRes.json();
+    
+    if (response?.result) {
+      const totalGasPrice = BigNumber.from(response.result.maxFeePerGas)
+        .add(response.result.maxPriorityFeePerGas)
+        .toString();
+      setGasPrice(totalGasPrice);
+    } else {
+      console.error('Invalid gas price response', response);
+      setGasPrice(undefined);
+    }
+  } catch (error) {
+    console.error('Failed to fetch gas price', error);
+    setGasPrice(undefined);
+  }
};

140-142: 🛠️ Refactor suggestion

Hard-coded API key in queryString state initialization.

The API key is directly included in the component's state initialization which isn't ideal for configuration management and security best practices.

-  const [queryString, setQueryString] = React.useState<string>(
-    `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`
-  );
+  const [queryString, setQueryString] = React.useState<string>('');
+
+  // Initialize queryString with API key from environment variables
+  useEffect(() => {
+    setQueryString(`?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`);
+  }, []);

493-505: ⚠️ Potential issue

Balance check uses the wrong token for fee validation.

The code is checking the balance of the selected asset being sent rather than the selected fee asset, which could block valid transfers.

if (isPaymaster && paymasterContext?.mode === 'commonerc20') {
-  const amountLeft = +getAmountLeft(
-    selectedAsset,
-    amount,
-    selectedAssetBalance
-  );
-  if (!feeMin) return;
-  if (amountLeft < +feeMin) {
+  if (!selectedFeeAsset || !feeMin) return;
+  const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+    accountAddress as string
+  ]?.find((b) =>
+    addressesEqual(b.token, selectedFeeAsset.token)
+    || (selectedFeeAsset.token === ethers.constants.AddressZero && b.token === null)
+  )?.balance ?? 0;
+
+  const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
+    feeTokenBalance,
+    selectedFeeAsset.decimals,
+  );
+
+  if (feeTokenBalanceFormatted < +feeMin) {
    setErrorMessage(t`error.insufficientBalanceForGasless`);
    setIsSending(false);
    return;
  }
}

295-321: 🛠️ Refactor suggestion

Improve error handling in setApprovalData function.

The function lacks proper error handling when fetching token prices and generating approval data.

const setApprovalData = async (gasCost: number) => {
  if (selectedFeeAsset && gasPrice) {
+   try {
      const estimatedCost = Number(
        utils.formatEther(BigNumber.from(gasCost).mul(gasPrice))
      );
      const costAsFiat = +estimatedCost * nativeAssetPrice;
      const feeTokenPrice = await getPrices([selectedFeeAsset.token]);
-     let estimatedCostInToken;
-     if (feeTokenPrice) {
-       const feeTokenPriceInUSD = feeTokenPrice[0].usd;
+     
+     if (!feeTokenPrice || !feeTokenPrice[0]?.usd) {
+       console.error('Failed to fetch token price or price is zero');
+       return;
+     }
+     
+     const feeTokenPriceInUSD = feeTokenPrice[0].usd;
+     if (feeTokenPriceInUSD <= 0) {
+       console.error('Invalid token price', feeTokenPriceInUSD);
+       return;
+     }
+     
      estimatedCostInToken = (costAsFiat / feeTokenPriceInUSD).toFixed(
        selectedFeeAsset.decimals
      );
      setFeeMin(estimatedCostInToken);
      setApproveData(
        encodeFunctionData({
          abi: erc20Abi,
          functionName: 'approve',
          args: [
            selectedPaymasterAddress as Address,
            parseUnits(estimatedCostInToken, selectedFeeAsset.decimals),
          ],
        })
      );
-     }
+   } catch (error) {
+     console.error('Failed to generate approval data:', error);
+     setFeeMin(undefined);
+     setApproveData('');
+   }
  }
};
🧹 Nitpick comments (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (1)

991-999: Approval transaction may not render if approveData is undefined.

The conditional check for approveData could prevent the approval transaction from rendering if there are errors in fetching gas prices or token prices.

{isPaymaster &&
  selectedPaymasterAddress &&
-  selectedFeeAsset &&
-  approveData && (
+  selectedFeeAsset && (
+  <>
+    {!approveData && (
+      <div style={{ color: 'red', fontSize: '12px', marginBottom: '10px' }}>
+        Error generating approval data. Gas price data may be unavailable.
+      </div>
+    )}
+    {approveData && (
      <EtherspotTransaction
        to={selectedFeeAsset.token}
        data={approveData}
      />
+    )}
+  </>
  )}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between cc84f8c and 944fee1.

📒 Files selected for processing (1)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (11 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (7)
src/components/Form/AssetSelect/index.tsx (1)
  • TokenAssetSelectOption (36-41)
src/types/api.ts (1)
  • PortfolioData (723-732)
src/apps/the-exchange/reducer/theExchangeSlice.ts (1)
  • setWalletPortfolio (107-112)
src/services/pillarXApiWalletPortfolio.ts (1)
  • convertPortfolioAPIResponseToToken (13-35)
src/services/tokensData.ts (2)
  • Token (11-21)
  • chainNameToChainIdTokensData (237-258)
src/utils/blockchain.ts (1)
  • isPolygonAssetNative (55-58)
src/components/Form/Select/index.tsx (2)
  • SelectOption (11-17)
  • Select (65-136)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (2)

781-793: Approve transaction is correctly placed before the primary transaction.

The implementation correctly places the approval transaction before the primary transaction when using a paymaster, which is essential for ERC-20 paymaster mode to function properly.


141-142:

Details

❓ Verification inconclusive

API key exposed in frontend code.

The API key is directly included in the frontend code, which poses a security risk as it can be exposed to clients.

Verify if this API key requires additional protection:

If the API key provides significant access, consider moving token authentication to a backend service or implementing token rotation.


🏁 Script executed:

#!/bin/bash
# Check if the API key is client-side safe or requires backend protection

# Look for any rate limiting or access control in the codebase
grep -r "REACT_APP_PAYMASTER_API_KEY" --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" .

# Check environment files for API key configuration
cat .env* 2>/dev/null | grep PAYMASTER

Length of output: 624


Confirm API Key Exposure in Client Build

It looks like REACT_APP_PAYMASTER_API_KEY is injected directly into the client bundle:

• src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx
?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY} (line 141)
?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}&chainId=${selectedAsset.chainId}&useVp=true (line 145)
${paymasterUrl}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY} (line 148)

This will expose the API key to end users. If this key grants any privileged access, please:

  • Validate its scope and privileges.
  • Proxy these requests through a secure backend or serverless function to keep the key secret.
  • Consider token rotation or scoped tokens with minimal permissions.

Please verify whether this key is safe to expose; otherwise migrate these calls to a backend layer.

Comment on lines 217 to 220
`?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}&chainId=${selectedAsset.chainId}&useVp=true`
);
fetch(
`${paymasterUrl}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inconsistent API URL construction.

The code has inconsistencies in how API endpoints and keys are constructed, which makes maintenance more difficult.

fetch(
-  `${paymasterUrl}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
+  `${paymasterUrl}/getAllCommonERC20PaymasterAddress${queryString}`,
  {
    method: 'POST',
    body: JSON.stringify({}),
  }
)

Consider creating a utility function to handle API URL construction consistently:

// utils/apiUtils.ts
export function getPaymasterApiUrl(endpoint: string, params?: Record<string, string>) {
  const baseUrl = process.env.REACT_APP_PAYMASTER_URL;
  const apiKey = process.env.REACT_APP_PAYMASTER_API_KEY;
  
  const queryParams = new URLSearchParams({ apiKey });
  
  if (params) {
    Object.entries(params).forEach(([key, value]) => {
      queryParams.append(key, value);
    });
  }
  
  return `${baseUrl}/${endpoint}?${queryParams.toString()}`;
}
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 217 to 220, the API URLs are constructed inconsistently with repeated
manual concatenation of base URLs and query parameters. To fix this, create a
utility function that accepts an endpoint and optional parameters, appends the
API key and other query parameters consistently, and returns the full URL
string. Replace all manual URL constructions in this file with calls to this
utility function to ensure uniformity and easier maintenance.

Comment on lines 217 to 291
`?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}&chainId=${selectedAsset.chainId}&useVp=true`
);
fetch(
`${paymasterUrl}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
{
method: 'POST',
body: JSON.stringify({}),
}
).then((res) => {
res.json().then((data) => {
if (data.message) {
let paymasterObject = JSON.parse(data.message);
paymasterObject = paymasterObject.filter(
(item: { epVersion: string; chainId: number; gasToken: string }) =>
item.epVersion === 'EPV_07' &&
item.chainId === selectedAsset?.chainId &&
tokens.find(
(token: Token) =>
token.contract === item.gasToken.toLowerCase() ||
(isPolygonAssetNative(item.gasToken, item.chainId) &&
token.contract === ethers.constants.AddressZero)
)
);
const feeOptions = paymasterObject.map(
(item: {
gasToken: string;
chainId: number;
epVersion: string;
paymasterAddress: string;
// eslint-disable-next-line consistent-return, array-callback-return
}) => {
const tokenData = tokens.find(
(token: Token) => token.contract === item.gasToken.toLowerCase()
);
if (tokenData)
return {
id: `${item.gasToken}-${item.chainId}-${item.paymasterAddress}-${tokenData.decimals}`,
type: 'token',
title: tokenData.name,
imageSrc: tokenData.logo,
chainId: chainNameToChainIdTokensData(tokenData.blockchain),
value: tokenData.balance,
price: tokenData.price,
asset: {
...tokenData,
contract: item.gasToken,
decimals: tokenData.decimals,
},
};
}
);
setFeeAssetOptions(feeOptions);
if (feeOptions.length > 0) {
// get Skandha gas price
getGasPrice(selectedAsset.chainId);
setSelectedFeeAsset({
token: feeOptions[0].asset.contract,
decimals: feeOptions[0].asset.decimals,
});
setSelectedPaymasterAddress(feeOptions[0].id.split('-')[2]);
setPaymasterContext({
mode: 'commonerc20',
token: feeOptions[0].asset.contract,
});
setIsPaymaster(true);
} else {
setFeeType([]);
}
} else {
setPaymasterContext(null);
setIsPaymaster(false);
setFeeType([]);
}
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor paymaster address fetching for better error handling and cleaner code.

The paymaster address fetching contains deeply nested code that lacks proper error handling.

Consider refactoring this code to use async/await with proper error handling and extracting filtering logic to separate functions:

const fetchPaymasterAddresses = async () => {
  try {
    const res = await fetch(
      `${paymasterUrl}/getAllCommonERC20PaymasterAddress${queryString}`,
      {
        method: 'POST',
        body: JSON.stringify({}),
      }
    );
    
    const data = await res.json();
    
    if (!data.message) {
      setPaymasterContext(null);
      setIsPaymaster(false);
      setFeeType([]);
      return;
    }
    
    const paymasterObject = JSON.parse(data.message);
    const filteredPaymasters = filterCompatiblePaymasters(paymasterObject, selectedAsset, tokens);
    const feeOptions = mapPaymastersToFeeOptions(filteredPaymasters, tokens);
    
    setFeeAssetOptions(feeOptions);
    
    if (feeOptions.length > 0) {
      setupPaymasterWithFirstOption(feeOptions);
    } else {
      setFeeType([]);
    }
  } catch (error) {
    console.error('Failed to fetch paymaster addresses:', error);
    setPaymasterContext(null);
    setIsPaymaster(false);
    setFeeType([]);
  }
};
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx between
lines 217 and 291, refactor the paymaster address fetching code to use
async/await instead of nested then calls for better readability and error
handling. Extract the filtering logic into separate functions like
filterCompatiblePaymasters and mapPaymastersToFeeOptions to clean up the main
function. Add a try-catch block around the fetch and JSON parsing to handle
errors gracefully by logging the error and resetting relevant state variables
(setPaymasterContext, setIsPaymaster, setFeeType). Also, encapsulate the setup
of the first fee option into a helper function (setupPaymasterWithFirstOption)
to keep the main function concise.

chainNameToChainIdTokensData,
Token,
} from '../../../services/tokensData';
import {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You are using those hooks from the deposit app. There are several useAppDispatch() and useAppSelector() used in different redux slices, please select the right folder. According to the rest of your code, you should import those from the Exchange reducer, not from the Deposit Reducer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it thanks


useEffect(() => {
if (walletPortfolioData && isWalletPortfolioDataSuccess) {
dispatch(setWalletPortfolio(walletPortfolioData?.result?.data));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think a separate slice should be created for the PillarX app, as interfering with the Exchange slice might causes issues. In my next PR I created this new PillarX slice with walletPortfolio data too. So once this is push I can make the swap between using walletPortfolio from the Exchange Slice and using it from the PillarX Slice.

dispatch(setWalletPortfolio(walletPortfolioData?.result?.data));
}
if (!isWalletPortfolioDataSuccess || walletPortfolioDataError) {
dispatch(setWalletPortfolio(undefined));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might need to add an error state here, so the user knows when there is an error with fetching the balances data from walletportfolio

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah sure will do

},
]);

const getGasPrice = async (chainId: number) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think those API calls function should be added in a separate folder and call in this component.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

if (isSendDisabled) return;
setErrorMessage('');

if (isPaymaster) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure to understand this logic of if isPaymaster is true, then show an error message?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

its only when you add a transaction to batch, it errors out just because that the paymaster can be used for the whole batch not just for a single transaction and on the transaction tab I can't see the whole batched transactions which are there. We would need to add the batched transactions to a global variable or have a mapping of all gaseless transactions and refactor the flow

@vignesha22 vignesha22 requested a review from ch4r10t33r May 20, 2025 08:59
@github-actions github-actions bot temporarily deployed to Preview (PRO-3185-Gas_Abstraction) May 21, 2025 06:37 Inactive
@vignesha22 vignesha22 requested a review from RanaBug May 21, 2025 06:37
Copy link
Contributor

@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: 2

♻️ Duplicate comments (3)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (3)

192-273: Nested fetch chain lacks error handling & readability

This block still contains deeply nested .then() chains without a try/catch, potentially leaking unhandled rejections and is hard to follow.
A previous review already suggested converting to async/await with proper error paths and helper functions.


275-301: setApprovalData assumes price data is always present

If getPrices fails or returns an empty array, feeTokenPrice[0].usd will throw.
Add try/catch and guard checks as recommended earlier to avoid silent crashes.


473-485: ⚠️ Potential issue

Balance check uses the wrong token

amountLeft is derived from the asset being sent, but when paymaster mode =commonerc20 the fee is deducted from selectedFeeAsset.
The current check can block valid transfers or let insufficient balances through.

This was pointed out previously and is still unresolved.

-      const amountLeft = +getAmountLeft(
-        selectedAsset,
-        amount,
-        selectedAssetBalance
-      );
-      if (!feeMin) return;
-      if (amountLeft < +feeMin) {
+      if (!selectedFeeAsset || !feeMin) return;
+
+      // balance of the token used to pay the fee
+      const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+        accountAddress as string
+      ]?.find((b) =>
+        addressesEqual(
+          b.token ?? ethers.constants.AddressZero,
+          selectedFeeAsset.token,
+        ),
+      )?.balance ?? 0;
+
+      const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
+        feeTokenBalance,
+        selectedFeeAsset.decimals,
+      );
+
+      if (feeTokenBalanceFormatted < +feeMin) {
         setErrorMessage(t`error.insufficientBalanceForGasless`);
         setIsSending(false);
         return;
       }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 944fee1 and b04049c.

📒 Files selected for processing (3)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (12 hunks)
  • src/translations/en.json (2 hunks)
  • src/utils/blockchain.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/translations/en.json
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages

Comment on lines 291 to 315
export const getGasPrice = async (chainId: number) => {
let gasPrice = '0';
const gasRes = await fetch(
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
}
);
gasRes.json().then((response) => {
if (response.result) {
gasPrice = BigNumber.from(response.result.maxFeePerGas)
.add(response.result.maxPriorityFeePerGas)
.toString();
}
});
return gasPrice;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

getGasPrice always resolves with '0' & lacks error handling

gasRes.json() is executed asynchronously but the function returns before the promise settles – callers always receive '0'.
Additionally, network / parsing failures will raise unhandled promise rejections.

-export const getGasPrice = async (chainId: number) => {
-  let gasPrice = '0';
-  const gasRes = await fetch(
-    `${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
-    {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify({
-        jsonrpc: '2.0',
-        id: 1,
-        method: 'skandha_getGasPrice',
-      }),
-    }
-  );
-  gasRes.json().then((response) => {
-    if (response.result) {
-      gasPrice = BigNumber.from(response.result.maxFeePerGas)
-        .add(response.result.maxPriorityFeePerGas)
-        .toString();
-    }
-  });
-  return gasPrice;
-};
+export const getGasPrice = async (chainId: number): Promise<string> => {
+  try {
+    const res = await fetch(
+      `${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
+      {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          jsonrpc: '2.0',
+          id: 1,
+          method: 'skandha_getGasPrice',
+        }),
+      },
+    );
+
+    const json = await res.json();
+    if (!json?.result) return '0';
+
+    return BigNumber.from(json.result.maxFeePerGas)
+      .add(json.result.maxPriorityFeePerGas)
+      .toString();
+  } catch (e) {
+    console.error('Failed to fetch gas price', e);
+    return '0';
+  }
+};

This ensures:

  1. The function only resolves once the value is known.
  2. Errors are caught and logged, preventing unhandled promise rejections.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getGasPrice = async (chainId: number) => {
let gasPrice = '0';
const gasRes = await fetch(
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
}
);
gasRes.json().then((response) => {
if (response.result) {
gasPrice = BigNumber.from(response.result.maxFeePerGas)
.add(response.result.maxPriorityFeePerGas)
.toString();
}
});
return gasPrice;
};
export const getGasPrice = async (chainId: number): Promise<string> => {
try {
const res = await fetch(
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
},
);
const json = await res.json();
if (!json?.result) return '0';
return BigNumber.from(json.result.maxFeePerGas)
.add(json.result.maxPriorityFeePerGas)
.toString();
} catch (e) {
console.error('Failed to fetch gas price', e);
return '0';
}
};
🤖 Prompt for AI Agents
In src/utils/blockchain.ts around lines 291 to 315, the getGasPrice function
returns '0' immediately because gasRes.json() is handled asynchronously without
awaiting its result, causing the function to resolve before the gas price is
retrieved. To fix this, await the gasRes.json() call to get the response before
processing it, and wrap the fetch and parsing logic in a try-catch block to
handle and log any errors, ensuring the function only resolves after the gas
price is obtained or logs errors if they occur.

Comment on lines +702 to +712
if (value.title === 'Gasless') {
setPaymasterContext({
mode: 'commonerc20',
token: selectedFeeAsset?.token,
});
setIsPaymaster(true);
} else {
setPaymasterContext(null);
setIsPaymaster(false);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fee-type selector maps “Gasless” to ERC-20 mode

Choosing “Gasless” should configure the paymaster with mode: 'sponsor' and clear selectedFeeAsset.
At the moment it sets mode: 'commonerc20', which:

  • Triggers unnecessary approval logic.
  • Requests a fee-token balance from the user – the opposite of a gas-free experience.
-  if (value.title === 'Gasless') {
-    setPaymasterContext({
-      mode: 'commonerc20',
-      token: selectedFeeAsset?.token,
-    });
-    setIsPaymaster(true);
-  } else {
+  if (value.title === 'Gasless') {
+    setPaymasterContext({ mode: 'sponsor' });
+    setSelectedFeeAsset(undefined);
+    setIsPaymaster(true);
+  } else {

Test matrix:

  1. Gasless selected → no approve tx rendered, paymasterContext.mode === 'sponsor'.
  2. Native Token selected → paymaster disabled.
  3. ERC-20 fee asset selected → mode === 'commonerc20' and approve tx rendered.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (value.title === 'Gasless') {
setPaymasterContext({
mode: 'commonerc20',
token: selectedFeeAsset?.token,
});
setIsPaymaster(true);
} else {
setPaymasterContext(null);
setIsPaymaster(false);
}
};
if (value.title === 'Gasless') {
// Use sponsor mode for true gasless and clear any selected fee token
setPaymasterContext({ mode: 'sponsor' });
setSelectedFeeAsset(undefined);
setIsPaymaster(true);
} else {
setPaymasterContext(null);
setIsPaymaster(false);
}
};
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 702 to 712, the "Gasless" fee-type incorrectly sets paymasterContext mode
to 'commonerc20' and does not clear selectedFeeAsset, causing unnecessary
approval logic and fee-token balance requests. Update the code so that when
"Gasless" is selected, set paymasterContext mode to 'sponsor' and clear
selectedFeeAsset to prevent approval transactions and fee-token balance checks.
For other cases, maintain the existing logic for native token and ERC-20
selections.

`?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}&chainId=${selectedAsset.chainId}&useVp=true`
);
fetch(
`${paymasterUrl}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

The whole fetch flow, the feeOptions and setApprovalData could be imported from another file, otherwise it adds too much code and logic to this component

Copy link
Contributor Author

Choose a reason for hiding this comment

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

feeOptions and setApprovalData cannot be imported since the data would change once you change the selectedAsset and it will anyways be fetched on each changes on selectedAsset. But yeah I can do fetching on api on another file

@github-actions github-actions bot temporarily deployed to Preview (PRO-3185-Gas_Abstraction) May 21, 2025 11:56 Inactive
Copy link
Contributor

@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

♻️ Duplicate comments (8)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (8)

16-16: Consider replacing ethers with viem for consistency.

You're importing both ethers and viem libraries, which adds unnecessary bundle size. Previous review comments also suggested replacing ethers with viem throughout the codebase.

What are the advantages of replacing ethers.js with viem in a React application?

962-972: Add error handling for missing approveData.

The component renders the approval transaction only if approveData is present, but it doesn't handle the case where approval data could not be generated due to errors.

  {isPaymaster &&
    selectedPaymasterAddress &&
    selectedFeeAsset &&
-   approveData && (
+   (
+     <>
+       {!approveData && (
+         <div style={{ color: 'red', marginBottom: '10px' }}>
+           {t`error.failedToGenerateApprovalTransaction`}
+         </div>
+       )}
+       {approveData && (
          <EtherspotTransaction
            to={selectedFeeAsset.token}
            data={approveData}
          />
+       )}
+     </>
    )}

196-200: Simplify query string generation with utility function.

The query string construction is duplicated and could be simplified using a utility function.

- setQueryString(
-   `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}&chainId=${selectedAsset.chainId}&useVp=true`
- );
+ setQueryString(
+   getPaymasterQueryString({
+     chainId: selectedAsset.chainId.toString(),
+     useVp: 'true'
+   })
+ );

142-144: 🛠️ Refactor suggestion

Use a utility function for API URL construction.

The API key is hardcoded in the state initialization, which isn't ideal for configuration management.

- const [queryString, setQueryString] = React.useState<string>(
-   `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`
- );
+ import { getPaymasterQueryString } from '../../../utils/apiUtils';
+ // elsewhere in the component
+ const [queryString, setQueryString] = React.useState<string>(
+   getPaymasterQueryString()
+ );

// In utils/apiUtils.ts:
export function getPaymasterQueryString(additionalParams?: Record<string, string>): string {
  const base = `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`;
  if (!additionalParams) return base;
  
  return Object.entries(additionalParams).reduce(
    (query, [key, value]) => `${query}&${key}=${value}`,
    base
  );
}

267-293: 🛠️ Refactor suggestion

Add proper error handling to setApprovalData function.

The function doesn't handle edge cases where feeTokenPrice is undefined or empty, or when price data is missing.

  const setApprovalData = async (gasCost: number) => {
    if (selectedFeeAsset && gasPrice) {
+     try {
        const estimatedCost = Number(
          utils.formatEther(BigNumber.from(gasCost).mul(gasPrice))
        );
        const costAsFiat = +estimatedCost * nativeAssetPrice;
        const feeTokenPrice = await getPrices([selectedFeeAsset.token]);
-       let estimatedCostInToken;
-       if (feeTokenPrice) {
+       
+       if (!feeTokenPrice || !feeTokenPrice[0]?.usd) {
+         console.error('Failed to fetch token price or price data is missing');
+         return;
+       }
+       
          const feeTokenPriceInUSD = feeTokenPrice[0].usd;
-         estimatedCostInToken = (costAsFiat / feeTokenPriceInUSD).toFixed(
+         const estimatedCostInToken = (costAsFiat / feeTokenPriceInUSD).toFixed(
            selectedFeeAsset.decimals
          );
          setFeeMin(estimatedCostInToken);
          setApproveData(
            encodeFunctionData({
              abi: erc20Abi,
              functionName: 'approve',
              args: [
                selectedPaymasterAddress as Address,
                parseUnits(estimatedCostInToken, selectedFeeAsset.decimals),
              ],
            })
          );
-       }
+     } catch (error) {
+       console.error('Failed to generate approval data:', error);
+     }
    }
  };

694-704: ⚠️ Potential issue

Fix incorrect paymaster mode for Gasless option.

When "Gasless" is selected, the code incorrectly sets the paymaster mode to 'commonerc20' instead of 'sponsor', which triggers unnecessary approval logic.

  if (value.title === 'Gasless') {
    setPaymasterContext({
-     mode: 'commonerc20',
-     token: selectedFeeAsset?.token,
+     mode: 'sponsor',
    });
+   setSelectedFeeAsset(undefined);
    setIsPaymaster(true);
  } else {
    setPaymasterContext(null);
    setIsPaymaster(false);
  }

465-477: ⚠️ Potential issue

Incorrect balance check for fee payment.

The current implementation checks if the remaining amount of the asset being sent is sufficient to cover the fee, but the fee should be paid with the selected fee asset, not the asset being sent.

  if (isPaymaster && paymasterContext?.mode === 'commonerc20') {
-   const amountLeft = +getAmountLeft(
-     selectedAsset,
-     amount,
-     selectedAssetBalance
-   );
    if (!feeMin) return;
-   if (amountLeft < +feeMin) {
+   if (!selectedFeeAsset) return;
+   
+   const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+     accountAddress as string
+   ]?.find((b) =>
+     addressesEqual(b.token, selectedFeeAsset.token)
+     || (selectedFeeAsset.token === ethers.constants.AddressZero && b.token === null)
+   )?.balance ?? 0;
+   
+   const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
+     feeTokenBalance,
+     selectedFeeAsset.decimals,
+   );
+   
+   if (feeTokenBalanceFormatted < +feeMin) {
      setErrorMessage(t`error.insufficientBalanceForGasless`);
      setIsSending(false);
      return;
    }
  }

240-244: 🛠️ Refactor suggestion

Improve error handling for gas price fetching.

The current implementation doesn't handle errors when fetching gas prices.

- getGasPrice(selectedAsset.chainId).then((res) => {
-   setGasPrice(res);
- });
+ try {
+   getGasPrice(selectedAsset.chainId)
+     .then((res) => {
+       setGasPrice(res);
+     })
+     .catch((error) => {
+       console.error('Failed to fetch gas price:', error);
+       setErrorMessage(t`error.failedToFetchGasPrice`);
+     });
+ } catch (error) {
+   console.error('Error initiating gas price fetch:', error);
+   setErrorMessage(t`error.failedToFetchGasPrice`);
+ }
🧹 Nitpick comments (6)
src/services/gasless.ts (3)

1-2: Unnecessary eslint disable directive.

The eslint-disable import/extensions directive doesn't seem necessary for this file since there are no direct file path imports with extensions.

-/* eslint-disable import/extensions */

16-17: Parameter name uses inconsistent naming convention.

The parameter tokens_list uses snake_case while the rest of the codebase follows camelCase convention.

export const getAllGaslessPaymasters = async (
  chainId: number,
- tokens_list: Token[]
+ tokensList: Token[]
): Promise<Paymasters[] | null> => {

46-51: Improve error handling in the catch block.

The current catch block logs the error but doesn't provide any context about what operation failed.

  } catch (err) {
-   console.error(err);
+   console.error('Failed to fetch or process paymaster data:', err);
    return null;
  }
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (3)

127-148: Create a dedicated reducer for paymaster state management.

The component introduces many state variables for paymaster functionality, making it harder to maintain and reason about. Consider using a reducer pattern to manage related state more effectively.

// Example paymaster reducer
type PaymasterState = {
  isPaymaster: boolean;
  paymasterContext: { mode: string; token?: string } | null;
  selectedPaymasterAddress: string;
  selectedFeeAsset?: { token: string; decimals: number };
  feeAssetOptions: TokenAssetSelectOption[];
  queryString: string;
  approveData: string;
  gasPrice?: string;
  feeMin?: string;
};

type PaymasterAction =
  | { type: 'SET_IS_PAYMASTER'; payload: boolean }
  | { type: 'SET_PAYMASTER_CONTEXT'; payload: { mode: string; token?: string } | null }
  | { type: 'SET_SELECTED_PAYMASTER_ADDRESS'; payload: string }
  | { type: 'SET_SELECTED_FEE_ASSET'; payload: { token: string; decimals: number } | undefined }
  | { type: 'SET_FEE_ASSET_OPTIONS'; payload: TokenAssetSelectOption[] }
  | { type: 'SET_QUERY_STRING'; payload: string }
  | { type: 'SET_APPROVE_DATA'; payload: string }
  | { type: 'SET_GAS_PRICE'; payload: string | undefined }
  | { type: 'SET_FEE_MIN'; payload: string | undefined };

const initialState: PaymasterState = {
  isPaymaster: false,
  paymasterContext: { mode: 'sponsor' },
  selectedPaymasterAddress: '',
  selectedFeeAsset: undefined,
  feeAssetOptions: [],
  queryString: `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
  approveData: '',
  gasPrice: undefined,
  feeMin: undefined,
};

function paymasterReducer(state: PaymasterState, action: PaymasterAction): PaymasterState {
  // Implementation
}

754-760: Add error handling for missing approveData in batched transactions.

Similar to the previous comment, the component doesn't handle the case where approval data could not be generated for batched transactions.

  {isPaymaster &&
    selectedPaymasterAddress &&
    selectedFeeAsset && (
+     <>
+       {!approveData && (
+         <div style={{ color: 'red', marginBottom: '10px' }}>
+           {t`error.failedToGenerateApprovalTransaction`}
+         </div>
+       )}
+       {approveData && (
          <EtherspotTransaction
            to={selectedFeeAsset.token}
            data={approveData}
          />
+       )}
+     </>
    )}

810-818: Add error handling for missing approveData in batched transactions.

Similar to earlier comments, the component needs to handle cases where approval data could not be generated for batched transactions.

  {isPaymaster &&
    selectedPaymasterAddress &&
-   approveData &&
    selectedFeeAsset && (
+     <>
+       {!approveData && (
+         <div style={{ color: 'red', marginBottom: '10px' }}>
+           {t`error.failedToGenerateApprovalTransaction`}
+         </div>
+       )}
+       {approveData && (
          <EtherspotTransaction
            to={selectedFeeAsset.token}
            data={approveData}
          />
+       )}
+     </>
    )}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between b04049c and c1bc27e.

📒 Files selected for processing (2)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (12 hunks)
  • src/services/gasless.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/services/gasless.ts (2)
src/services/tokensData.ts (1)
  • Token (11-21)
src/utils/blockchain.ts (1)
  • isPolygonAssetNative (55-58)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages

Comment on lines +32 to +45
let paymasterObject = JSON.parse(data.message);
paymasterObject = paymasterObject.filter(
(item: { epVersion: string; chainId: number; gasToken: string }) =>
item.epVersion === 'EPV_07' &&
item.chainId === chainId &&
tokens_list.find(
(token: Token) =>
token.contract === item.gasToken.toLowerCase() ||
(isPolygonAssetNative(item.gasToken, item.chainId) &&
token.contract === constants.AddressZero)
)
);
return paymasterObject;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling for JSON parsing and make filtering more robust.

The current implementation lacks error handling for JSON parsing and assumes the filtered data has the expected structure.

-     let paymasterObject = JSON.parse(data.message);
-     paymasterObject = paymasterObject.filter(
-       (item: { epVersion: string; chainId: number; gasToken: string }) =>
-         item.epVersion === 'EPV_07' &&
-         item.chainId === chainId &&
-         tokens_list.find(
-           (token: Token) =>
-             token.contract === item.gasToken.toLowerCase() ||
-             (isPolygonAssetNative(item.gasToken, item.chainId) &&
-               token.contract === constants.AddressZero)
-         )
-     );
-     return paymasterObject;
+     try {
+       let paymasterObject = JSON.parse(data.message);
+       
+       if (!Array.isArray(paymasterObject)) {
+         console.error('Expected paymaster data to be an array');
+         return null;
+       }
+       
+       paymasterObject = paymasterObject.filter(
+         (item: { epVersion?: string; chainId?: number; gasToken?: string }) =>
+           item.epVersion === 'EPV_07' &&
+           item.chainId === chainId &&
+           item.gasToken &&
+           tokensList.find(
+             (token: Token) =>
+               token.contract === item.gasToken!.toLowerCase() ||
+               (isPolygonAssetNative(item.gasToken!, item.chainId!) &&
+                 token.contract === constants.AddressZero)
+           )
+       );
+       return paymasterObject;
+     } catch (parseErr) {
+       console.error('Failed to parse paymaster data:', parseErr);
+       return null;
+     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/services/gasless.ts between lines 32 and 45, add a try-catch block around
the JSON.parse call to handle potential parsing errors gracefully. Inside the
try block, parse the JSON and perform the filtering as before. In the catch
block, log or handle the error appropriately and return an empty array or a safe
default. Additionally, before filtering, validate that the parsed data is an
array and that each item has the expected properties to avoid runtime errors
during filtering.

Comment on lines 28 to 31
const data = await res.json();
// eslint-disable-next-line no-console
console.log(data.message);
if (data.message) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove debug console log and add response validation.

Remove debugging console.log and add validation to ensure data has the expected structure before parsing.

    const data = await res.json();
-   // eslint-disable-next-line no-console
-   console.log(data.message);
-   if (data.message) {
+   if (data && typeof data.message === 'string') {
🤖 Prompt for AI Agents
In src/services/gasless.ts around lines 28 to 31, remove the debug console.log
statement to clean up the code. Add validation to check that the response data
has the expected structure, such as verifying that data is an object and
contains the message property, before accessing or parsing it to prevent runtime
errors.

Comment on lines 18 to 27
try {
// eslint-disable-next-line no-console
console.log(chainId, tokens_list);
const res = await fetch(
`${process.env.REACT_APP_PAYMASTER_URL}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
{
method: 'POST',
body: JSON.stringify({}),
}
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove console logs and improve error handling for the external API call.

Production code shouldn't contain console.log statements. Also, the fetch call lacks proper error handling and validation for the environment variables.

try {
-   // eslint-disable-next-line no-console
-   console.log(chainId, tokens_list);
+   if (!process.env.REACT_APP_PAYMASTER_URL || !process.env.REACT_APP_PAYMASTER_API_KEY) {
+     console.error('Paymaster URL or API key not configured');
+     return null;
+   }
    const res = await fetch(
      `${process.env.REACT_APP_PAYMASTER_URL}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
      {
        method: 'POST',
        body: JSON.stringify({}),
      }
    );
+   if (!res.ok) {
+     console.error(`Failed to fetch paymaster data: ${res.status} ${res.statusText}`);
+     return null;
+   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
// eslint-disable-next-line no-console
console.log(chainId, tokens_list);
const res = await fetch(
`${process.env.REACT_APP_PAYMASTER_URL}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
{
method: 'POST',
body: JSON.stringify({}),
}
);
try {
if (!process.env.REACT_APP_PAYMASTER_URL || !process.env.REACT_APP_PAYMASTER_API_KEY) {
console.error('Paymaster URL or API key not configured');
return null;
}
const res = await fetch(
`${process.env.REACT_APP_PAYMASTER_URL}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
{
method: 'POST',
body: JSON.stringify({}),
}
);
if (!res.ok) {
console.error(`Failed to fetch paymaster data: ${res.status} ${res.statusText}`);
return null;
}
🤖 Prompt for AI Agents
In src/services/gasless.ts around lines 18 to 27, remove the console.log
statement used for debugging. Add checks to ensure that the environment
variables REACT_APP_PAYMASTER_URL and REACT_APP_PAYMASTER_API_KEY are defined
before making the fetch call. Implement proper error handling for the fetch
request by catching any errors and handling non-OK HTTP responses appropriately,
such as throwing an error or returning a fallback value.

Comment on lines 200 to 263
getAllGaslessPaymasters(selectedAsset.chainId, tokens).then(
(paymasterObject) => {
// eslint-disable-next-line no-console
console.log(paymasterObject);
if (paymasterObject) {
const feeOptions = paymasterObject
.map(
(item: {
gasToken: string;
chainId: number;
epVersion: string;
paymasterAddress: string;
// eslint-disable-next-line consistent-return, array-callback-return
}) => {
const tokenData = tokens.find(
(token: Token) =>
token.contract === item.gasToken.toLowerCase()
);
if (tokenData)
return {
id: `${item.gasToken}-${item.chainId}-${item.paymasterAddress}-${tokenData.decimals}`,
type: 'token',
title: tokenData.name,
imageSrc: tokenData.logo,
chainId: chainNameToChainIdTokensData(tokenData.blockchain),
value: tokenData.balance,
price: tokenData.price,
asset: {
...tokenData,
contract: item.gasToken,
decimals: tokenData.decimals,
},
} as TokenAssetSelectOption;
}
)
.filter(
(value): value is TokenAssetSelectOption => value !== undefined
);
if (feeOptions && feeOptions.length > 0 && feeOptions[0]) {
setFeeAssetOptions(feeOptions);
// get Skandha gas price
getGasPrice(selectedAsset.chainId).then((res) => {
setGasPrice(res);
});
setSelectedFeeAsset({
token: feeOptions[0].asset.contract,
decimals: feeOptions[0].asset.decimals,
});
setSelectedPaymasterAddress(feeOptions[0].id.split('-')[2]);
setPaymasterContext({
mode: 'commonerc20',
token: feeOptions[0].asset.contract,
});
setIsPaymaster(true);
} else {
setFeeType([]);
}
} else {
setPaymasterContext(null);
setIsPaymaster(false);
setFeeType([]);
}
}
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor paymaster setup with async/await and better error handling.

The paymaster setup code uses promises with .then(), making it harder to read and maintain. Refactor using async/await and add proper error handling.

You should consider refactoring this code to use async/await for better readability and error handling. Here's a simplified example of how part of this could be structured:

const setupPaymaster = async (chainId: number, tokens: Token[]) => {
  try {
    const paymasterObject = await getAllGaslessPaymasters(chainId, tokens);
    
    if (!paymasterObject || paymasterObject.length === 0) {
      setPaymasterContext(null);
      setIsPaymaster(false);
      setFeeType([]);
      return;
    }
    
    const feeOptions = mapPaymastersToFeeOptions(paymasterObject, tokens);
    
    if (feeOptions.length === 0) {
      setFeeType([]);
      return;
    }
    
    setFeeAssetOptions(feeOptions);
    
    try {
      const gasPrice = await getGasPrice(chainId);
      setGasPrice(gasPrice);
    } catch (error) {
      console.error('Failed to fetch gas price:', error);
      setErrorMessage(t`error.failedToFetchGasPrice`);
    }
    
    // Setup initial state with first option
    setupInitialPaymasterState(feeOptions);
    
  } catch (error) {
    console.error('Failed to setup paymaster:', error);
    setPaymasterContext(null);
    setIsPaymaster(false);
    setFeeType([]);
  }
};
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 200 to 263, refactor the paymaster setup code from using .then() promise
chains to async/await syntax for improved readability and maintainability. Wrap
the entire logic in a try/catch block to handle errors gracefully. Await the
getAllGaslessPaymasters call, check for empty or null results to reset state
accordingly, then map the paymaster data to fee options. Await getGasPrice
inside a nested try/catch to handle gas price fetch errors separately. Finally,
set the initial paymaster state based on the first fee option. Remove
console.log statements and ensure all state updates happen within the async
function with proper error handling.

Comment on lines 193 to 265
useEffect(() => {
if (!walletPortfolio) return;
const tokens = convertPortfolioAPIResponseToToken(walletPortfolio);
if (!selectedAsset) return;
setQueryString(
`?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}&chainId=${selectedAsset.chainId}&useVp=true`
);
getAllGaslessPaymasters(selectedAsset.chainId, tokens).then(
(paymasterObject) => {
// eslint-disable-next-line no-console
console.log(paymasterObject);
if (paymasterObject) {
const feeOptions = paymasterObject
.map(
(item: {
gasToken: string;
chainId: number;
epVersion: string;
paymasterAddress: string;
// eslint-disable-next-line consistent-return, array-callback-return
}) => {
const tokenData = tokens.find(
(token: Token) =>
token.contract === item.gasToken.toLowerCase()
);
if (tokenData)
return {
id: `${item.gasToken}-${item.chainId}-${item.paymasterAddress}-${tokenData.decimals}`,
type: 'token',
title: tokenData.name,
imageSrc: tokenData.logo,
chainId: chainNameToChainIdTokensData(tokenData.blockchain),
value: tokenData.balance,
price: tokenData.price,
asset: {
...tokenData,
contract: item.gasToken,
decimals: tokenData.decimals,
},
} as TokenAssetSelectOption;
}
)
.filter(
(value): value is TokenAssetSelectOption => value !== undefined
);
if (feeOptions && feeOptions.length > 0 && feeOptions[0]) {
setFeeAssetOptions(feeOptions);
// get Skandha gas price
getGasPrice(selectedAsset.chainId).then((res) => {
setGasPrice(res);
});
setSelectedFeeAsset({
token: feeOptions[0].asset.contract,
decimals: feeOptions[0].asset.decimals,
});
setSelectedPaymasterAddress(feeOptions[0].id.split('-')[2]);
setPaymasterContext({
mode: 'commonerc20',
token: feeOptions[0].asset.contract,
});
setIsPaymaster(true);
} else {
setFeeType([]);
}
} else {
setPaymasterContext(null);
setIsPaymaster(false);
setFeeType([]);
}
}
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAsset, walletPortfolio]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract paymaster fetching logic into separate functions.

The useEffect for fetching and processing paymaster data is too complex and deeply nested. Extract this logic into separate, testable functions.

// Extracted functions should look something like:
const buildPaymasterQueryString = (chainId: number) => 
  `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}&chainId=${chainId}&useVp=true`;

const mapPaymasterToFeeOption = (
  paymaster: { gasToken: string; chainId: number; paymasterAddress: string },
  tokens: Token[]
): TokenAssetSelectOption | undefined => {
  const tokenData = tokens.find(
    (token: Token) => token.contract === paymaster.gasToken.toLowerCase()
  );
  
  if (!tokenData) return undefined;
  
  return {
    id: `${paymaster.gasToken}-${paymaster.chainId}-${paymaster.paymasterAddress}-${tokenData.decimals}`,
    type: 'token',
    title: tokenData.name,
    imageSrc: tokenData.logo,
    chainId: chainNameToChainIdTokensData(tokenData.blockchain),
    value: tokenData.balance,
    price: tokenData.price,
    asset: {
      ...tokenData,
      contract: paymaster.gasToken,
      decimals: tokenData.decimals,
    },
  } as TokenAssetSelectOption;
};

const setupInitialPaymasterState = (feeOptions: TokenAssetSelectOption[], chainId: number) => {
  if (feeOptions.length === 0) return;
  
  // Setup initial state
  getGasPrice(chainId).then(setGasPrice);
  
  setSelectedFeeAsset({
    token: feeOptions[0].asset.contract,
    decimals: feeOptions[0].asset.decimals,
  });
  
  setSelectedPaymasterAddress(feeOptions[0].id.split('-')[2]);
  
  setPaymasterContext({
    mode: 'commonerc20',
    token: feeOptions[0].asset.contract,
  });
  
  setIsPaymaster(true);
};
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 193 to 265, the useEffect hook contains complex and deeply nested logic
for fetching and processing paymaster data. To fix this, extract the query
string construction, mapping of paymaster objects to fee options, and the
initial paymaster state setup into separate reusable functions. Create a
function to build the paymaster query string from the chainId, another to map
each paymaster item to a TokenAssetSelectOption using the tokens array, and a
third to initialize state variables like gas price, selected fee asset,
paymaster address, context, and flags based on the fee options. Then replace the
inline logic in the useEffect with calls to these functions for improved
readability and testability.

@github-actions github-actions bot temporarily deployed to Preview (PRO-3185-Gas_Abstraction) May 21, 2025 15:19 Inactive
Copy link
Contributor

@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: 2

♻️ Duplicate comments (5)
src/services/gasless.ts (2)

18-31: 🛠️ Refactor suggestion

Remove debug logs and add environment variable validation

The production code shouldn't contain console logs and should properly validate environment variables before using them.

try {
-   // eslint-disable-next-line no-console
-   console.log(chainId, tokens_list);
+   if (!process.env.REACT_APP_PAYMASTER_URL || !process.env.REACT_APP_PAYMASTER_API_KEY) {
+     console.error('Paymaster URL or API key not configured');
+     return null;
+   }
    const res = await fetch(
      `${process.env.REACT_APP_PAYMASTER_URL}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
      {
        method: 'POST',
        body: JSON.stringify({}),
      }
    );
+   if (!res.ok) {
+     console.error(`Failed to fetch paymaster data: ${res.status} ${res.statusText}`);
+     return null;
+   }
    const data = await res.json();
-   // eslint-disable-next-line no-console
-   console.log(data.message);
-   if (data.message) {
+   if (data && typeof data.message === 'string') {

32-45: 🛠️ Refactor suggestion

Improve error handling for JSON parsing and filtering

The current implementation lacks error handling for JSON parsing and doesn't validate the paymaster data structure.

-     let paymasterObject = JSON.parse(data.message);
-     paymasterObject = paymasterObject.filter(
-       (item: { epVersion: string; chainId: number; gasToken: string }) =>
-         item.epVersion === 'EPV_07' &&
-         item.chainId === chainId &&
-         tokens_list.find(
-           (token: Token) =>
-             token.contract === item.gasToken.toLowerCase() ||
-             (isPolygonAssetNative(item.gasToken, item.chainId) &&
-               token.contract === constants.AddressZero)
-         )
-     );
-     return paymasterObject;
+     try {
+       let paymasterObject = JSON.parse(data.message);
+       
+       if (!Array.isArray(paymasterObject)) {
+         console.error('Expected paymaster data to be an array');
+         return null;
+       }
+       
+       paymasterObject = paymasterObject.filter(
+         (item: { epVersion?: string; chainId?: number; gasToken?: string }) =>
+           item.epVersion === 'EPV_07' &&
+           item.chainId === chainId &&
+           item.gasToken &&
+           tokens_list.find(
+             (token: Token) =>
+               token.contract === item.gasToken!.toLowerCase() ||
+               (isPolygonAssetNative(item.gasToken!, item.chainId!) &&
+                 token.contract === constants.AddressZero)
+           )
+       );
+       return paymasterObject;
+     } catch (parseErr) {
+       console.error('Failed to parse paymaster data:', parseErr);
+       return null;
+     }
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (3)

467-479: ⚠️ Potential issue

Balance check uses the wrong token

The current implementation checks the balance of the asset being sent, but fees are paid with the selectedFeeAsset. If a user sends token A and pays fees with token B, this check will be incorrect.

-const amountLeft = +getAmountLeft(
-  selectedAsset,
-  amount,
-  selectedAssetBalance
-);
-if (!feeMin) return;
-if (amountLeft < +feeMin) {
+if (!selectedFeeAsset || !feeMin) return;
+const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+  accountAddress as string
+]?.find((b) =>
+  addressesEqual(b.token, selectedFeeAsset.token)
+  || (selectedFeeAsset.token === ethers.constants.AddressZero && b.token === null)
+)?.balance ?? 0;
+
+const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
+  feeTokenBalance,
+  selectedFeeAsset.decimals,
+);
+
+if (feeTokenBalanceFormatted < +feeMin) {
  setErrorMessage(t`error.insufficientBalanceForGasless`);
  setIsSending(false);
  return;
}

695-706: ⚠️ Potential issue

Fee-type selector maps "Gasless" to ERC-20 mode

When "Gasless" is selected, it should configure the paymaster with mode: 'sponsor' and clear selectedFeeAsset, but it incorrectly sets mode: 'commonerc20'.

if (value.title === 'Gasless') {
  setPaymasterContext({
-   mode: 'commonerc20',
-   token: selectedFeeAsset?.token,
+   mode: 'sponsor'
  });
+ setSelectedFeeAsset(undefined);
  setIsPaymaster(true);
} else {
  setPaymasterContext(null);
  setIsPaymaster(false);
}

269-295: 🛠️ Refactor suggestion

Enhance price fetching with error handling and validation

The setApprovalData function lacks proper error handling and may calculate incorrect approval amounts if token price data is unavailable.

const setApprovalData = async (gasCost: number) => {
  if (selectedFeeAsset && gasPrice) {
+   try {
      const estimatedCost = Number(
        utils.formatEther(BigNumber.from(gasCost).mul(gasPrice))
      );
      const costAsFiat = +estimatedCost * nativeAssetPrice;
      const feeTokenPrice = await getPrices([selectedFeeAsset.token]);
-     let estimatedCostInToken;
-     if (feeTokenPrice) {
+     
+     if (!feeTokenPrice || !feeTokenPrice[0]?.usd) {
+       console.error('Failed to fetch token price');
+       return;
+     }
      
      const feeTokenPriceInUSD = feeTokenPrice[0].usd;
-     estimatedCostInToken = (costAsFiat / feeTokenPriceInUSD).toFixed(
+     const estimatedCostInToken = (costAsFiat / feeTokenPriceInUSD).toFixed(
        selectedFeeAsset.decimals
      );
      
      setFeeMin(estimatedCostInToken);
      setApproveData(
        encodeFunctionData({
          abi: erc20Abi,
          functionName: 'approve',
          args: [
            selectedPaymasterAddress as Address,
            parseUnits(estimatedCostInToken, selectedFeeAsset.decimals),
          ],
        })
      );
-     }
+   } catch (error) {
+     console.error('Failed to generate approval data:', error);
+   }
  }
};
🧹 Nitpick comments (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (1)

195-267: Refactor paymaster setup with async/await for better readability

The paymaster setup code uses promises with .then(), making it harder to read and maintain.

Consider refactoring this code to use async/await:

useEffect(() => {
  const setupPaymaster = async () => {
    if (!walletPortfolio || !selectedAsset) return;
    
    const tokens = convertPortfolioAPIResponseToToken(walletPortfolio);
    
    setQueryString(
      `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}&chainId=${selectedAsset.chainId}&useVp=true`
    );
    
    try {
      const paymasterObject = await getAllGaslessPaymasters(selectedAsset.chainId, tokens);
      
      if (!paymasterObject || paymasterObject.length === 0) {
        setPaymasterContext(null);
        setIsPaymaster(false);
        setFeeType([]);
        return;
      }
      
      const feeOptions = paymasterObject
        .map((item) => {
          // Map to fee options...
          const tokenData = tokens.find(
            (token) => token.contract === item.gasToken.toLowerCase()
          );
          
          if (!tokenData) return undefined;
          
          return {
            id: `${item.gasToken}-${item.chainId}-${item.paymasterAddress}-${tokenData.decimals}`,
            type: 'token',
            title: tokenData.name,
            // ... other properties
          } as TokenAssetSelectOption;
        })
        .filter((value): value is TokenAssetSelectOption => value !== undefined);
      
      if (feeOptions.length === 0) {
        setFeeType([]);
        return;
      }
      
      setFeeAssetOptions(feeOptions);
      
      try {
        const res = await getGasPrice(selectedAsset.chainId);
        setGasPrice(res);
      } catch (error) {
        console.error('Failed to fetch gas price:', error);
      }
      
      // Setup initial state with first option
      if (feeOptions[0]) {
        setSelectedFeeAsset({
          token: feeOptions[0].asset.contract,
          decimals: feeOptions[0].asset.decimals,
        });
        setSelectedPaymasterAddress(feeOptions[0].id.split('-')[2]);
        setPaymasterContext({
          mode: 'commonerc20',
          token: feeOptions[0].asset.contract,
        });
        setIsPaymaster(true);
      }
    } catch (error) {
      console.error('Failed to setup paymaster:', error);
      setPaymasterContext(null);
      setIsPaymaster(false);
      setFeeType([]);
    }
  };
  
  setupPaymaster();
  
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAsset, walletPortfolio]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between c1bc27e and 363388a.

📒 Files selected for processing (2)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (12 hunks)
  • src/services/gasless.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (2)

757-762: approve transaction must precede the primary transaction

For ERC-20 paymaster mode, the allowance must be set first. Currently, the approval transaction is correctly placed before the primary transaction. This is good implementation!


967-973: Ensure conditional rendering uses proper checks

The approval transaction depends on approveData being defined. This check correctly guards against rendering the transaction when approveData is undefined, which is good practice.

Comment on lines 53 to 82
export const getGasPrice = async (chainId: number) => {
let gasPrice = '0';
try {
const gasRes = await fetch(
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
}
);
gasRes.json().then((response) => {
if (response.result) {
gasPrice = BigNumber.from(response.result.maxFeePerGas)
.add(response.result.maxPriorityFeePerGas)
.toString();
}
});
return gasPrice;
} catch (err) {
console.error(err);
return gasPrice;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical issue: getGasPrice doesn't wait for the async result

The function returns the default gas price ('0') immediately without waiting for the fetch promise to complete. The actual gas price calculation happens in the .then() callback, but it's never captured by the return value.

Replace the promise chain with async/await:

export const getGasPrice = async (chainId: number) => {
  let gasPrice = '0';
  try {
    const gasRes = await fetch(
      `${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          jsonrpc: '2.0',
          id: 1,
          method: 'skandha_getGasPrice',
        }),
      }
    );
-    gasRes.json().then((response) => {
-      if (response.result) {
-        gasPrice = BigNumber.from(response.result.maxFeePerGas)
-          .add(response.result.maxPriorityFeePerGas)
-          .toString();
-      }
-    });
+    const response = await gasRes.json();
+    if (response?.result) {
+      gasPrice = BigNumber.from(response.result.maxFeePerGas)
+        .add(response.result.maxPriorityFeePerGas)
+        .toString();
+    }
    return gasPrice;
  } catch (err) {
    console.error(err);
    return gasPrice;
  }
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getGasPrice = async (chainId: number) => {
let gasPrice = '0';
try {
const gasRes = await fetch(
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
}
);
gasRes.json().then((response) => {
if (response.result) {
gasPrice = BigNumber.from(response.result.maxFeePerGas)
.add(response.result.maxPriorityFeePerGas)
.toString();
}
});
return gasPrice;
} catch (err) {
console.error(err);
return gasPrice;
}
};
export const getGasPrice = async (chainId: number) => {
let gasPrice = '0';
try {
const gasRes = await fetch(
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'skandha_getGasPrice',
}),
}
);
const response = await gasRes.json();
if (response?.result) {
gasPrice = BigNumber.from(response.result.maxFeePerGas)
.add(response.result.maxPriorityFeePerGas)
.toString();
}
return gasPrice;
} catch (err) {
console.error(err);
return gasPrice;
}
};
🤖 Prompt for AI Agents
In src/services/gasless.ts lines 53 to 82, the getGasPrice function returns the
default gas price '0' immediately because it does not await the asynchronous
JSON parsing and gas price calculation inside the .then() callback. To fix this,
replace the .then() promise chain with an await expression to wait for
gasRes.json() to resolve, then perform the gas price calculation synchronously
after that. Finally, return the calculated gas price after the await, ensuring
the function returns the correct value.

Comment on lines 144 to 146
const [queryString, setQueryString] = React.useState<string>(
`?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Hard-coded API key in queryString state

The API key is directly included in the component's state initialization, which isn't ideal for configuration management.

Extract this to a utility function:

// utils/apiHelpers.ts
export function getPaymasterQueryString(additionalParams?: Record<string, string>) {
  const base = `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`;
  if (!additionalParams) return base;
  
  return Object.entries(additionalParams).reduce(
    (query, [key, value]) => `${query}&${key}=${value}`,
    base
  );
}

Then use this utility:

-const [queryString, setQueryString] = React.useState<string>(
-  `?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`
-);
+const [queryString, setQueryString] = React.useState<string>(
+  getPaymasterQueryString()
+);
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 144 to 146, the API key is hard-coded directly in the queryString state
initialization. To fix this, create a utility function (e.g.,
getPaymasterQueryString) in a separate utils/apiHelpers.ts file that constructs
the query string including the API key and any additional parameters. Then,
replace the direct initialization of queryString with a call to this utility
function to improve configuration management and code reuse.

Copy link

@ch4r10t33r ch4r10t33r left a comment

Choose a reason for hiding this comment

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

lgtm

useWalletAddress,
} from '@etherspot/transaction-kit';
import { BigNumber, ethers } from 'ethers';
import { BigNumber, ethers, utils } from 'ethers';

Choose a reason for hiding this comment

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

@RanaBug @IAmKio kindly note. We can defer these changes for now. But we have to change this in future.

if (selectedAsset.type === 'token') {
if (selectedAsset.asset.contract === ethers.constants.AddressZero) {
gasCost = Number(
process.env.REACT_APP_NATIVE_GAS_CONSUMPTION_ARBITRUM
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do the gas consumtion values need to be an environment variable as opposed to a const definition?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes since if its const you would need code change to change every time rather you can just change the env and restart if you want to change the values in the middle

Copy link
Collaborator

Choose a reason for hiding this comment

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

Personally i don't see this as an issue (to store them in a const file or config object) as we can override this if we need to, but having these values in the control of infrastructure team means that we're dependent on them when we can just push an update ourselves, especally because it is not sensitive information. I'd recommend that we add these to a local file instead of pull them from the environment.

Comment on lines 319 to 334
process.env.REACT_APP_TOKEN_GAS_CONSUMPTION_ARBITRUM
); // estimated gas consumption for token asset transfer for undeployed wallet + 15% markup
}
} else if (selectedAsset.type === 'nft') {
gasCost = Number(process.env.REACT_APP_NFT_GAS_CONSUMPTION_ARBITRUM); // estimated gas consumption for token asset transfer for undeployed wallet + 15% markup
}
} else {
// eslint-disable-next-line no-lonely-if
if (selectedAsset.type === 'token') {
if (selectedAsset.asset.contract === ethers.constants.AddressZero) {
gasCost = Number(process.env.REACT_APP_NATIVE_GAS_CONSUMPTION); // estimated gas consumption for native asset transfer for deployed wallet + 15% markup
} else {
gasCost = Number(process.env.REACT_APP_TOKEN_GAS_CONSUMPTION); // estimated gas consumption for token asset transfer for deployed wallet + 15% markup
}
} else if (selectedAsset.type === 'nft') {
gasCost = Number(process.env.REACT_APP_NFT_GAS_CONSUMPTION); // estimated gas consumption for token asset transfer for deployed wallet + 15% markup
Copy link
Collaborator

Choose a reason for hiding this comment

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

The same applies for these process.env values

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same ans from above works here too

// eslint-disable-next-line no-console
console.log(chainId, tokens_list);
const res = await fetch(
`${process.env.REACT_APP_PAYMASTER_URL}/getAllCommonERC20PaymasterAddress?apiKey=${process.env.REACT_APP_PAYMASTER_API_KEY}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the paymaster proxy URL supposed to come from the Px API? If so is the API key needed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ohh so this PR was raised before the Px API thats why apiKey is still there will remove it now as I created an API

let gasPrice = '0';
try {
const gasRes = await fetch(
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,
`${process.env.REACT_APP_GAS_URL}/${chainId}?api-key=${process.env.REACT_APP_ETHERSPOT_BUNDLER_API_KEY}`,

What is the GAS_URL supposed to be here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it the bundler url https://rpc.etherspot.io/v2

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we make this more specific please as GAS_URL os a bit generic

@github-actions github-actions bot requested a deployment to Preview (PRO-3185-Gas_Abstraction) May 23, 2025 14:27 Abandoned
Copy link
Contributor

@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: 1

♻️ Duplicate comments (3)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (3)

468-480: ⚠️ Potential issue

Balance validation still uses incorrect token.

This is the same issue flagged in previous reviews. The balance check compares selectedAsset amount against selectedFeeAsset fee minimum, but when using paymaster mode, users pay fees with selectedFeeAsset, not selectedAsset.

-const amountLeft = +getAmountLeft(
-  selectedAsset,
-  amount,
-  selectedAssetBalance
-);
-if (!feeMin) return;
-if (amountLeft < +feeMin) {
+if (!selectedFeeAsset || !feeMin) return;
+
+const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+  accountAddress as string
+]?.find((b) =>
+  addressesEqual(b.token, selectedFeeAsset.token)
+  || (selectedFeeAsset.token === ethers.constants.AddressZero && b.token === null)
+)?.balance ?? 0;
+
+const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
+  feeTokenBalance,
+  selectedFeeAsset.decimals,
+);
+
+if (feeTokenBalanceFormatted < +feeMin) {
   setErrorMessage(t`error.insufficientBalanceForGasless`);
   setIsSending(false);
   return;
 }

717-722: ⚠️ Potential issue

"Gasless" fee type incorrectly configured.

The "Gasless" option should set paymaster mode to 'sponsor' and clear the selected fee asset, but it's currently setting mode to 'commonerc20' which triggers approval logic.

 if (value.title === 'Gasless') {
   setPaymasterContext({
-    mode: 'commonerc20',
-    token: selectedFeeAsset?.token,
+    mode: 'sponsor',
   });
+  setSelectedFeeAsset(undefined);
   setIsPaymaster(true);
 } else {

195-265: 🛠️ Refactor suggestion

Refactor complex paymaster setup with proper async/await.

This complex nested logic still uses .then() chains and lacks proper error handling, making it difficult to maintain and debug.

Extract this into a separate async function with proper error handling:

const setupPaymaster = async (chainId: number, tokens: Token[]) => {
  try {
    setQueryString(`chainId=${chainId}&useVp=true`);
    const paymasterObject = await getAllGaslessPaymasters(chainId, tokens);
    
    if (!paymasterObject || paymasterObject.length === 0) {
      setPaymasterContext(null);
      setIsPaymaster(false);
      setFeeType([]);
      return;
    }
    
    const feeOptions = paymasterObject
      .map((item) => {
        const tokenData = tokens.find(
          (token: Token) => token.contract === item.gasToken.toLowerCase()
        );
        if (tokenData) {
          return {
            id: `${item.gasToken}-${item.chainId}-${item.paymasterAddress}-${tokenData.decimals}`,
            type: 'token',
            title: tokenData.name,
            imageSrc: tokenData.logo,
            chainId: chainNameToChainIdTokensData(tokenData.blockchain),
            value: tokenData.balance,
            price: tokenData.price,
            asset: {
              ...tokenData,
              contract: item.gasToken,
              decimals: tokenData.decimals,
            },
          } as TokenAssetSelectOption;
        }
        return undefined;
      })
      .filter((value): value is TokenAssetSelectOption => value !== undefined);
    
    if (feeOptions.length > 0) {
      setFeeAssetOptions(feeOptions);
      
      const gasPrice = await getGasPrice(chainId);
      setGasPrice(gasPrice);
      
      setSelectedFeeAsset({
        token: feeOptions[0].asset.contract,
        decimals: feeOptions[0].asset.decimals,
      });
      setSelectedPaymasterAddress(feeOptions[0].id.split('-')[2]);
      setPaymasterContext({
        mode: 'commonerc20',
        token: feeOptions[0].asset.contract,
      });
      setIsPaymaster(true);
    } else {
      setFeeType([]);
    }
  } catch (error) {
    console.error('Failed to setup paymaster:', error);
    setPaymasterContext(null);
    setIsPaymaster(false);
    setFeeType([]);
  }
};

Then replace the useEffect content with:

useEffect(() => {
  if (!walletPortfolio || !selectedAsset) return;
  const tokens = convertPortfolioAPIResponseToToken(walletPortfolio);
  setupPaymaster(selectedAsset.chainId, tokens);
}, [selectedAsset, walletPortfolio]);
🧹 Nitpick comments (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (1)

131-149: Consider extracting paymaster state to custom hook.

The component has accumulated significant paymaster-related state that could be extracted into a reusable custom hook for better organization and testability.

Consider creating a usePaymaster hook:

// hooks/usePaymaster.ts
export const usePaymaster = () => {
  const [isPaymaster, setIsPaymaster] = useState<boolean>(false);
  const [paymasterContext, setPaymasterContext] = useState<{
    mode: string;
    token?: string;
  } | null>({ mode: 'sponsor' });
  const [selectedPaymasterAddress, setSelectedPaymasterAddress] = useState<string>('');
  const [selectedFeeAsset, setSelectedFeeAsset] = useState<{
    token: string;
    decimals: number;
  }>();
  const [feeAssetOptions, setFeeAssetOptions] = useState<TokenAssetSelectOption[]>([]);
  const [approveData, setApproveData] = useState<string>('');
  const [gasPrice, setGasPrice] = useState<string>();
  const [feeMin, setFeeMin] = useState<string>();
  
  // Include setup and management logic here
  
  return {
    isPaymaster,
    paymasterContext,
    selectedPaymasterAddress,
    selectedFeeAsset,
    feeAssetOptions,
    approveData,
    gasPrice,
    feeMin,
    // setup functions
  };
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 194d98d and 9c610b5.

📒 Files selected for processing (3)
  • .env.example (1 hunks)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (12 hunks)
  • src/services/gasless.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/services/gasless.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (7)
src/components/Form/AssetSelect/index.tsx (1)
  • TokenAssetSelectOption (36-41)
src/types/api.ts (1)
  • PortfolioData (723-732)
src/apps/the-exchange/reducer/theExchangeSlice.ts (1)
  • setWalletPortfolio (107-112)
src/services/pillarXApiWalletPortfolio.ts (1)
  • convertPortfolioAPIResponseToToken (13-35)
src/services/gasless.ts (3)
  • getAllGaslessPaymasters (23-56)
  • getGasPrice (58-87)
  • GasConsumptions (14-21)
src/services/tokensData.ts (2)
  • Token (11-21)
  • chainNameToChainIdTokensData (237-258)
src/components/Form/Select/index.tsx (2)
  • SelectOption (11-17)
  • Select (65-136)
🪛 Gitleaks (8.26.0)
.env.example

12-12: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Cloudflare Pages
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build

.env.example Outdated

REACT_APP_SWAP_BUTTON_SWITCH="true"
REACT_APP_PAYMASTER_URL="http://localhost:5050"
REACT_APP_PAYMASTER_API_KEY=eyJvcmciOiI2NTIzZjY5MzUwOTBmNzAwMDFiYjJkZWIiLCJpZCI6IjMxMDZiOGY2NTRhZTRhZTM4MGVjYjJiN2Q2NDMzMjM4IiwiaCI6Im11cm11cjEyOCJ9 No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace real API key with placeholder value.

The API key appears to be a real token rather than a placeholder value. Example files should contain placeholder values to avoid accidental exposure of real credentials.

-REACT_APP_PAYMASTER_API_KEY=eyJvcmciOiI2NTIzZjY5MzUwOTBmNzAwMDFiYjJkZWIiLCJpZCI6IjMxMDZiOGY2NTRhZTRhZTM4MGVjYjJiN2Q2NDMzMjM4IiwiaCI6Im11cm11cjEyOCJ9
+REACT_APP_PAYMASTER_API_KEY=your_paymaster_api_key_here
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REACT_APP_PAYMASTER_API_KEY=eyJvcmciOiI2NTIzZjY5MzUwOTBmNzAwMDFiYjJkZWIiLCJpZCI6IjMxMDZiOGY2NTRhZTRhZTM4MGVjYjJiN2Q2NDMzMjM4IiwiaCI6Im11cm11cjEyOCJ9
-REACT_APP_PAYMASTER_API_KEY=eyJvcmciOiI2NTIzZjY5MzUwOTBmNzAwMDFiYjJkZWIiLCJpZCI6IjMxMDZiOGY2NTRhZTRhZTM4MGVjYjJiN2Q2NDMzMjM4IiwiaCI6Im11cm11cjEyOCJ9
+REACT_APP_PAYMASTER_API_KEY=your_paymaster_api_key_here
🧰 Tools
🪛 Gitleaks (8.26.0)

12-12: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In the .env.example file at line 12, replace the real API key value with a
generic placeholder string such as REACT_APP_PAYMASTER_API_KEY=your_api_key_here
to prevent exposing actual credentials in example files.

@github-actions github-actions bot requested a deployment to Preview (PRO-3185-Gas_Abstraction) May 23, 2025 16:59 Abandoned
@vignesha22 vignesha22 requested a review from IAmKio May 23, 2025 16:59
Copy link
Collaborator

@IAmKio IAmKio left a comment

Choose a reason for hiding this comment

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

LGTM

@github-actions github-actions bot requested a deployment to Preview (PRO-3185-Gas_Abstraction) June 18, 2025 11:54 Abandoned
Copy link
Contributor

@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: 2

♻️ Duplicate comments (3)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (3)

523-535: Fee-balance check still uses the sent asset instead of the fee asset

The balance guard must check the user’s balance of selectedFeeAsset, not the asset being transferred.
Current logic blocks valid transfers and lets invalid ones slip through.

This was called out in a previous review and has not been addressed.

Example patch:

-const amountLeft = +getAmountLeft(
-  selectedAsset,
-  amount,
-  selectedAssetBalance
-);
+const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+  accountAddress as string
+]?.find((b) =>
+  addressesEqual(b.token, selectedFeeAsset?.token) ||
+  (selectedFeeAsset?.token === ethers.constants.AddressZero && b.token === null)
+)?.balance ?? 0;
+
+const amountLeft = +ethers.utils.formatUnits(
+  feeTokenBalance,
+  selectedFeeAsset?.decimals ?? 18,
+);

854-864: “Gasless” option still maps to commonerc20

Selecting “Gasless” should set paymasterContext.mode = 'sponsor' and clear selectedFeeAsset.
Keeping it as commonerc20 triggers the whole approval + fee-token flow — the opposite of gasless.

Issue was raised earlier but persists.


277-302: setApprovalData needs error handling around price fetch

Network / Coingecko failures will throw and break the hook. Add try/catch and guard against missing price data.

(This is the same concern reported in a previous review and still open.)

🧹 Nitpick comments (2)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (2)

211-214: Remove stray console.log before merging

A leftover console.log will pollute production logs and can leak potentially sensitive user-data.

-// eslint-disable-next-line no-console
-console.log(paymasterObject);

Delete the statement or wrap it in a debug guard.


913-950: Duplicate approval-tx blocks – consider extracting

The same approve-transaction JSX appears three times with nearly identical conditions. Extract it into a small component or helper to DRY up the markup and reduce future maintenance cost.

Also applies to: 1123-1131, 970-978

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e8d18b and ed0648b.

📒 Files selected for processing (1)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (12 hunks)
🧰 Additional context used
🪛 GitHub Actions: vignesha22 has committed new code! 🚀 Checking...
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx

[error] 318-318: Prettier formatting error: Insert newline and indentation (prettier/prettier). Run 'prettier --write' to fix code style issues.

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: unit-tests
  • GitHub Check: build
🔇 Additional comments (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (1)

141-161: ```shell
#!/bin/bash
set -e

Show how queryString is defined and used in the file

rg "queryString" -C3 src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx

Show how paymasterUrl is referenced around its usages

rg "paymasterUrl" -C3 src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx

Check if the API key environment variable is used anywhere

rg "REACT_APP_PAYMASTER_API_KEY" -C3 .


</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +913 to +918
{isPaymaster &&
selectedPaymasterAddress &&
selectedFeeAsset && (
<EtherspotTransaction
to={selectedFeeAsset.token}
data={approveData}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing approveData guard – renders empty tx

In the single-transaction path the approval tx is rendered whenever isPaymaster is true, even if approveData is an empty string.
This creates a transaction with no calldata and will revert.

-{isPaymaster &&
-  selectedPaymasterAddress &&
-  selectedFeeAsset && (
+{isPaymaster &&
+  selectedPaymasterAddress &&
+  selectedFeeAsset &&
+  approveData && (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isPaymaster &&
selectedPaymasterAddress &&
selectedFeeAsset && (
<EtherspotTransaction
to={selectedFeeAsset.token}
data={approveData}
{isPaymaster &&
selectedPaymasterAddress &&
selectedFeeAsset &&
approveData && (
<EtherspotTransaction
to={selectedFeeAsset.token}
data={approveData}
🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 913 to 918, the code renders the EtherspotTransaction component when
isPaymaster is true without checking if approveData is non-empty. This causes a
transaction with empty calldata that will revert. To fix this, add a guard to
ensure approveData is not an empty string before rendering the
EtherspotTransaction component.

@github-actions github-actions bot temporarily deployed to Preview (PRO-3185-Gas_Abstraction) June 19, 2025 12:59 Inactive
Copy link
Collaborator

@IAmKio IAmKio left a comment

Choose a reason for hiding this comment

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

Just a small comment / question

const handleOnChange = (value: SelectOption) => {
const tokenOption = feeAssetOptions.filter(
(option) => option.id === value.id
)[0] as TokenAssetSelectOption;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We're assuming we'll always have array pointer 0 here, is this correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes thats right if the user has native then it will save else 0

Copy link
Collaborator

@IAmKio IAmKio left a comment

Choose a reason for hiding this comment

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

LGTM

@github-actions github-actions bot temporarily deployed to Preview (PRO-3185-Gas_Abstraction) June 19, 2025 13:25 Inactive
Copy link
Contributor

@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: 2

♻️ Duplicate comments (5)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (5)

18-18: Technical debt: Mixed ethers and viem dependencies.

The codebase is using both ethers and viem libraries, which increases bundle size and complexity. This was flagged in previous reviews but deferred.

Consider creating a migration plan to standardize on viem across the codebase to reduce dependency overhead and improve consistency.


864-874: Critical: "Gasless" mode incorrectly configured as ERC20 paymaster.

When "Gasless" is selected, it should use sponsor mode and clear the selected fee asset. Currently it sets mode: 'commonerc20' which will trigger unnecessary approval logic and request fee tokens from users.

if (value.title === 'Gasless') {
  setPaymasterContext({
-    mode: 'commonerc20',
-    token: selectedFeeAsset?.token,
+    mode: 'sponsor',
  });
+  setSelectedFeeAsset(undefined);
   setIsPaymaster(true);
 } else {

208-281: Refactor complex useEffect with proper error handling and async/await.

This useEffect contains deeply nested promise chains, lacks error handling, and has complex logic that should be extracted into separate functions. The current implementation could cause unhandled promise rejections.

Extract the paymaster fetching logic:

useEffect(() => {
  if (!walletPortfolio) return;
  const tokens = convertPortfolioAPIResponseToToken(walletPortfolio);
  if (!selectedAsset) return;
  setQueryString(`?chainId=${selectedAsset.chainId}`);
-  getAllGaslessPaymasters(selectedAsset.chainId, tokens).then(
-    (paymasterObject) => {
-      if (paymasterObject) {
-        // ... complex nested logic
-      } else {
-        setPaymasterContext(null);
-        setIsPaymaster(false);
-        setFeeType([]);
-      }
-    }
-  );
+  setupPaymasters(selectedAsset.chainId, tokens);
}, [selectedAsset, walletPortfolio]);

+const setupPaymasters = async (chainId: number, tokens: Token[]) => {
+  try {
+    const paymasterObject = await getAllGaslessPaymasters(chainId, tokens);
+    if (!paymasterObject) {
+      resetPaymasterState();
+      return;
+    }
+    
+    const feeOptions = mapPaymastersToFeeOptions(paymasterObject, tokens);
+    if (feeOptions.length === 0) {
+      setFeeType([]);
+      return;
+    }
+    
+    await initializePaymasterState(feeOptions, chainId);
+  } catch (error) {
+    console.error('Failed to setup paymasters:', error);
+    resetPaymasterState();
+  }
+};

530-541: Critical: Balance validation uses wrong token for fee payment.

The current implementation checks the balance of the asset being sent (selectedAsset) instead of the fee asset (selectedFeeAsset). This will incorrectly block valid transactions where users send one token but pay fees with another.

-const amountLeft = +getAmountLeft(
-  selectedAsset,
-  amount,
-  selectedAssetBalance
-);
-if (!feeMin) return;
-if (amountLeft < +feeMin) {
+if (!selectedFeeAsset || !feeMin) return;
+
+const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+  accountAddress as string
+]?.find((b) =>
+  addressesEqual(b.token, selectedFeeAsset.token) ||
+  (selectedFeeAsset.token === ethers.constants.AddressZero && b.token === null)
+)?.balance ?? 0;
+
+const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
+  feeTokenBalance,
+  selectedFeeAsset.decimals,
+);
+
+if (feeTokenBalanceFormatted < +feeMin) {
   setErrorMessage(t`error.insufficientBalanceForGasless`);
   setIsSending(false);
   return;
 }

283-308: Add error handling for price fetching and token validation.

The setApprovalData function lacks error handling and doesn't validate that the selected fee asset has a valid price before calculating approval amounts.

const setApprovalData = async (gasCost: number) => {
+ try {
    if (selectedFeeAsset && gasPrice && gasCost) {
      const estimatedCost = Number(
        utils.formatEther(BigNumber.from(gasCost).mul(gasPrice))
      );
      const costAsFiat = +estimatedCost * nativeAssetPrice;
      const feeTokenPrice = selectedFeeAsset.tokenPrice;
      let estimatedCostInToken;
-     if (feeTokenPrice) {
+     if (feeTokenPrice && +feeTokenPrice > 0) {
        estimatedCostInToken = (costAsFiat / +feeTokenPrice).toFixed(
          selectedFeeAsset.decimals
        );
        setFeeMin(estimatedCostInToken);
        setApproveData(
          encodeFunctionData({
            abi: erc20Abi,
            functionName: 'approve',
            args: [
              selectedPaymasterAddress as Address,
              parseUnits(estimatedCostInToken, selectedFeeAsset.decimals),
            ],
          })
        );
+     } else {
+       console.error('Invalid fee token price:', feeTokenPrice);
+       setFeeMin(undefined);
+       setApproveData('');
      }
    }
+ } catch (error) {
+   console.error('Failed to set approval data:', error);
+   setFeeMin(undefined);
+   setApproveData('');
+ }
};
🧹 Nitpick comments (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (1)

140-159: Consider consolidating paymaster state with useReducer.

The component has many related paymaster state variables that could benefit from being managed together with useReducer for better state consistency and easier debugging.

type PaymasterState = {
  isPaymaster: boolean;
  context: { mode: string; token?: string } | null;
  selectedAddress: string;
  selectedFeeAsset?: { token: string; decimals: number; tokenPrice?: string };
  feeAssetOptions: TokenAssetSelectOption[];
  approveData: string;
  gasPrice?: string;
  feeMin?: string;
};

const paymasterReducer = (state: PaymasterState, action: PaymasterAction) => {
  // Handle state transitions
};

const [paymasterState, dispatch] = useReducer(paymasterReducer, initialState);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23b3003 and 74bb618.

📒 Files selected for processing (1)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (12 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build

Comment on lines +923 to +930
{isPaymaster &&
selectedPaymasterAddress &&
selectedFeeAsset && (
<EtherspotTransaction
to={selectedFeeAsset.token}
data={approveData}
/>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure approval transaction guards are consistent.

The batches path checks for approveData (line 982) but the single transaction path doesn't (line 925). This inconsistency could lead to empty approval transactions.

{isPaymaster &&
  selectedPaymasterAddress &&
- selectedFeeAsset && (
+ selectedFeeAsset &&
+ approveData && (
    <EtherspotTransaction
      to={selectedFeeAsset.token}
      data={approveData}
    />
  )}

Also applies to: 980-988

🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 923 to 930 and 980 to 988, the single transaction rendering does not check
for the presence of approveData before rendering the EtherspotTransaction
component, unlike the batches path which does. To fix this, add a guard
condition to ensure approveData is truthy before rendering the approval
transaction in both places, preventing empty or invalid approval transactions
from being created.

Comment on lines +256 to +258
getGasPrice(selectedAsset.chainId).then((res) => {
setGasPrice(res);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for gas price fetching.

The getGasPrice call lacks error handling and could cause unresolved promises if the API call fails.

-getGasPrice(selectedAsset.chainId).then((res) => {
-  setGasPrice(res);
-});
+try {
+  const gasPrice = await getGasPrice(selectedAsset.chainId);
+  setGasPrice(gasPrice);
+} catch (error) {
+  console.error('Failed to fetch gas price:', error);
+  setGasPrice(undefined);
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 256 to 258, the getGasPrice call does not handle errors, which can lead to
unresolved promises if the API call fails. Add a catch block to the promise
chain to handle any errors from getGasPrice, and optionally set a default gas
price or show an error message to the user. This will ensure the component
handles failures gracefully without leaving promises unresolved.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (7)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (7)

4-4: Import dependencies properly managed

The addition of viem utilities and ethers BigNumber imports are appropriate for the paymaster implementation. However, note the past review comment about eventually replacing ethers with viem to avoid dual dependencies in the PillarX binary.

Also applies to: 18-18


161-191: Add error handling for wallet portfolio fetching

The Redux integration for wallet portfolio is implemented correctly, but the error handling could be improved. When walletPortfolioDataError occurs, you're setting a generic error message but not providing specific feedback about what went wrong.

Consider enhancing error handling:

if (walletPortfolioDataError) {
- console.error(walletPortfolioDataError);
- setErrorMessage(t`error.failedWalletPortfolio`);
+ console.error('Wallet portfolio fetch error:', walletPortfolioDataError);
+ // Provide more specific error handling based on error type
+ const errorMessage = walletPortfolioDataError.status === 401 
+   ? t`error.unauthorized` 
+   : t`error.failedWalletPortfolio`;
+ setErrorMessage(errorMessage);
}

208-282: Extract paymaster setup logic for better maintainability

The useEffect for paymaster setup is complex and deeply nested. This logic should be extracted into separate functions for better readability and testability.

Consider refactoring:

const setupPaymasterForAsset = async (asset: AssetSelectOption, tokens: Token[]) => {
  try {
    const paymasterData = await getAllGaslessPaymasters(asset.chainId, tokens);
    if (!paymasterData?.length) {
      resetPaymasterState();
      return;
    }
    
    const feeOptions = buildFeeOptions(paymasterData, tokens);
    if (feeOptions.length === 0) {
      setFeeType([]);
      return;
    }
    
    setFeeAssetOptions(feeOptions);
    await initializePaymasterSettings(asset.chainId, feeOptions[0]);
  } catch (error) {
    console.error('Failed to setup paymaster:', error);
    resetPaymasterState();
  }
};

284-309: Improve error handling in setApprovalData function

The setApprovalData function lacks proper error handling and validation. The function could fail silently if price data is unavailable.

Add comprehensive error handling:

const setApprovalData = async (gasCost: number) => {
- if (selectedFeeAsset && gasPrice && gasCost) {
+ if (!selectedFeeAsset || !gasPrice || !gasCost) {
+   console.warn('Missing required data for approval calculation');
+   return;
+ }
+ 
+ try {
    const estimatedCost = Number(
      utils.formatEther(BigNumber.from(gasCost).mul(gasPrice))
    );
    const costAsFiat = +estimatedCost * nativeAssetPrice;
    const feeTokenPrice = selectedFeeAsset.tokenPrice;
-   let estimatedCostInToken;
-   if (feeTokenPrice) {
+   
+   if (!feeTokenPrice || +feeTokenPrice <= 0) {
+     console.error('Invalid or missing fee token price');
+     return;
+   }
+   
+   const estimatedCostInToken = (costAsFiat / +feeTokenPrice).toFixed(
+     selectedFeeAsset.decimals
+   );
-     estimatedCostInToken = (costAsFiat / +feeTokenPrice).toFixed(
-       selectedFeeAsset.decimals
-     );
-     setFeeMin(estimatedCostInToken);
-     setApproveData(
-       encodeFunctionData({
-         abi: erc20Abi,
-         functionName: 'approve',
-         args: [
-           selectedPaymasterAddress as Address,
-           parseUnits(estimatedCostInToken, selectedFeeAsset.decimals),
-         ],
-       })
-     );
-   }
- }
+   
+   setFeeMin(estimatedCostInToken);
+   setApproveData(
+     encodeFunctionData({
+       abi: erc20Abi,
+       functionName: 'approve',
+       args: [
+         selectedPaymasterAddress as Address,
+         parseUnits(estimatedCostInToken, selectedFeeAsset.decimals),
+       ],
+     })
+   );
+ } catch (error) {
+   console.error('Failed to generate approval data:', error);
+ }
};

530-542: Critical: Balance check still uses wrong token

The balance validation logic is still incorrect. You're checking amountLeft from the selected asset being sent, but the fee is paid with selectedFeeAsset. This was previously flagged and remains unfixed.

The previous fix suggestion should be implemented:

-const amountLeft = +getAmountLeft(
-  selectedAsset,
-  amount,
-  selectedAssetBalance
-);
-if (!feeMin) return;
-if (amountLeft < +feeMin) {
+if (!selectedFeeAsset || !feeMin) return;
+const feeTokenBalance = accountBalances?.[selectedAsset?.chainId ?? 0]?.[
+  accountAddress as string
+]?.find((b) =>
+  addressesEqual(b.token, selectedFeeAsset.token)
+  || (selectedFeeAsset.token === ethers.constants.AddressZero && b.token === null)
+)?.balance ?? 0;
+
+const feeTokenBalanceFormatted = +ethers.utils.formatUnits(
+  feeTokenBalance,
+  selectedFeeAsset.decimals,
+);
+
+if (feeTokenBalanceFormatted < +feeMin) {
   setErrorMessage(t`error.insufficientBalanceForGasless`);
   setIsSending(false);
   return;
 }

865-875: Critical: "Gasless" mode incorrectly configured

The "Gasless" fee type is still incorrectly setting mode: 'commonerc20'. This was flagged in previous reviews and remains unfixed. Gasless mode should use sponsor mode without fee asset selection.

Apply the previously suggested fix:

if (value.title === 'Gasless') {
- setPaymasterContext({
-   mode: 'commonerc20',
-   token: selectedFeeAsset?.token,
- });
- setIsPaymaster(true);
+ setPaymasterContext({ mode: 'sponsor' });
+ setSelectedFeeAsset(undefined);
+ setIsPaymaster(true);
} else {
  setPaymasterContext(null);
  setIsPaymaster(false);
}

924-931: Missing approveData guard in single transaction path

The single transaction rendering path doesn't check for approveData presence, while the batches path does. This inconsistency was previously flagged.

Add the missing guard:

{isPaymaster &&
  selectedPaymasterAddress &&
- selectedFeeAsset && (
+ selectedFeeAsset &&
+ approveData && (
    <EtherspotTransaction
      to={selectedFeeAsset.token}
      data={approveData}
    />
  )}

Also applies to: 981-989

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74bb618 and d2bb0de.

📒 Files selected for processing (1)
  • src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (12 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (9)
src/components/Form/AssetSelect/index.tsx (1)
  • TokenAssetSelectOption (36-41)
src/types/api.ts (1)
  • PortfolioData (731-740)
src/apps/the-exchange/reducer/theExchangeSlice.ts (1)
  • setWalletPortfolio (109-114)
src/services/pillarXApiWalletPortfolio.ts (1)
  • convertPortfolioAPIResponseToToken (22-46)
src/services/gasless.ts (2)
  • getAllGaslessPaymasters (23-56)
  • GasConsumptions (14-21)
src/services/tokensData.ts (2)
  • Token (11-21)
  • chainNameToChainIdTokensData (244-265)
src/apps/the-exchange/utils/wrappedTokens.ts (1)
  • isNativeToken (29-30)
src/hooks/useTransactionDebugLogger.tsx (1)
  • useTransactionDebugLogger (1-15)
src/components/Form/Select/index.tsx (2)
  • SelectOption (11-17)
  • Select (65-136)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (3)
src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx (3)

140-159: Well-structured paymaster state management

The paymaster-related state variables are logically organized and properly typed. The initialization of paymasterContext with { mode: 'sponsor' } is appropriate for the default state.


768-771: Appropriate restriction on batching with paymaster

The logic to prevent batching when paymaster is active is correct. Paymaster validation typically works at the batch level, not individual transactions.


1134-1142: Consistent approval transaction guards

The regular send modal correctly includes the approveData guard for the approval transaction, which is consistent with the batches path. Good defensive programming.

Comment on lines +1075 to +1095
{selectedAsset && feeType.length > 0 && (
<FormGroup>
<Label>{t`label.feeType`}</Label>
<Select
type="token"
onChange={handleOnChangeFeeAsset}
options={feeType}
defaultSelectedId={feeType[0].id}
/>
{paymasterContext?.mode === 'commonerc20' &&
feeAssetOptions.length > 0 && (
<>
<Label>{t`label.selectFeeAsset`}</Label>
<Select
type="token"
onChange={handleOnChange}
options={feeAssetOptions}
defaultSelectedId={feeAssetOptions[0]?.id}
/>
</>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

UI logic needs refinement for fee type selection

The fee type selection UI is rendered when feeType.length > 0, but the fee asset selection is only shown when paymasterContext?.mode === 'commonerc20'. This creates inconsistent UX when "Gasless" is selected but the mode is incorrectly set.

This will be resolved when the gasless mode configuration issue is fixed. The UI logic is otherwise sound.

🤖 Prompt for AI Agents
In src/components/BottomMenuModal/SendModal/SendModalTokensTabView.tsx around
lines 1075 to 1095, the UI logic conditionally renders the fee asset selection
only when paymasterContext?.mode is 'commonerc20', which causes inconsistent UX
if the mode is incorrectly set while "Gasless" is selected. To fix this, ensure
the paymasterContext mode is correctly configured for gasless mode outside this
component. No changes are needed in the UI logic here as it is sound; focus on
correcting the gasless mode configuration to resolve the inconsistency.

@vignesha22 vignesha22 merged commit 0d62b84 into staging Jun 19, 2025
7 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jun 23, 2025
3 tasks
This was referenced Sep 24, 2025
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.

4 participants