Skip to content

Commit a622864

Browse files
committed
feat(checkout): restrict to EVM chains and hide native gas token
- Force chains.types to EVM-only; cascades to chains, tokens, routes, wallets - Hide native gas token in transfer/exchange/cash flows (IF can't accept it) - Wallet menu now honors chains.types ecosystem allow/deny config
1 parent ed274b4 commit a622864

9 files changed

Lines changed: 115 additions & 11 deletions

File tree

.changeset/checkout-evm-only.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@lifi/widget-checkout": minor
3+
"@lifi/widget": minor
4+
---
5+
6+
Restrict checkout to EVM chains and tokens, and hide the native gas token in deposit flows.
7+
8+
Checkout now forces `chains.types` to EVM-only, so chain lists, token lists, route quotes, and wallet/recipient selection surface only EVM chains and their native + ERC20 tokens. The native gas token is hidden from source-token selection in the transfer/exchange/cash (Intent Factory) flows, which cannot accept it; the wallet flow keeps full token support.
9+
10+
`@lifi/widget`'s wallet menu now honors the `chains.types` allow/deny config, so a restricted ecosystem set only offers wallets for the allowed chain types.

packages/widget-checkout/src/pages/SelectSourcePage/SelectSourcePage.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
DEFAULT_FROM_CHAIN_ID,
3939
DEFAULT_FROM_TOKEN_ADDRESS,
4040
} from '../../utils/checkoutDefaults.js'
41+
import { isNativeToken } from '../../utils/nativeToken.js'
4142
import { checkoutNavigationRoutes } from '../../utils/navigationRoutes.js'
4243
import { CheckoutActivitySection } from './CheckoutActivitySection.js'
4344
import { SelectSourceFundingOptions } from './SelectSourceFundingOptions.js'
@@ -164,8 +165,23 @@ export const SelectSourcePage: React.FC = () => {
164165
const handleTransferCrypto = useCallback(() => {
165166
overrideExchanges([...INTENT_FACTORY_ONLY])
166167
setFundingSource('transfer')
168+
// IF deposits can't accept the native gas token; reset a carried-over pick.
169+
if (isNativeToken(prevTokenAddress)) {
170+
setFieldValue(FormKeyHelper.getChainKey('from'), DEFAULT_FROM_CHAIN_ID)
171+
setFieldValue(
172+
FormKeyHelper.getTokenKey('from'),
173+
DEFAULT_FROM_TOKEN_ADDRESS
174+
)
175+
setFieldValue(FormKeyHelper.getAmountKey('from'), '')
176+
}
167177
goToToken()
168-
}, [goToToken, overrideExchanges, setFundingSource])
178+
}, [
179+
goToToken,
180+
overrideExchanges,
181+
prevTokenAddress,
182+
setFieldValue,
183+
setFundingSource,
184+
])
169185

170186
const pinExchangeSource = useCallback(() => {
171187
overrideExchanges([...INTENT_FACTORY_ONLY])

packages/widget-checkout/src/pages/SelectTokenPage/SelectTokenList.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Box } from '@mui/material'
1717
import type { RefObject } from 'react'
1818
import { type FC, memo, useCallback, useEffect, useMemo, useRef } from 'react'
1919
import { useCheckoutFlowStore } from '../../stores/useCheckoutFlowStore.js'
20+
import { isNativeToken } from '../../utils/nativeToken.js'
2021

2122
export interface SelectTokenListProps {
2223
formType: FormType
@@ -38,6 +39,8 @@ type SharedListProps = Omit<SelectTokenListProps, 'isWalletFunded'> & {
3839
selectedTokenAddress?: string
3940
isAllNetworks: boolean
4041
tokenSearchFilter?: string
42+
// IF deposit flows can't accept the native gas token; only the wallet flow keeps it.
43+
excludeNative: boolean
4144
}
4245

4346
type TokenListResult = ReturnType<typeof useTokenBalances>
@@ -47,6 +50,7 @@ const TokenListView: FC<SharedListProps & TokenListResult> = ({
4750
headerRef,
4851
afterTokenSelect,
4952
allowedSymbols,
53+
excludeNative,
5054
selectedChainId,
5155
selectedTokenAddress,
5256
isAllNetworks,
@@ -75,15 +79,18 @@ const TokenListView: FC<SharedListProps & TokenListResult> = ({
7579
)
7680

7781
const filteredTokens = useMemo(() => {
82+
const withoutNative = excludeNative
83+
? tokens.filter((token) => !isNativeToken(token.address))
84+
: tokens
7885
if (!allowedSymbols || allowedSymbols.size === 0) {
79-
return tokens
86+
return withoutNative
8087
}
8188
// Strip on-wallet amounts — the curated list funds from an exchange,
8289
// not the connected wallet — regardless of which hook fed the list.
83-
return tokens
90+
return withoutNative
8491
.filter((token) => allowedSymbols.has(token.symbol.toUpperCase()))
8592
.map((token) => ({ ...token, amount: undefined }))
86-
}, [tokens, allowedSymbols])
93+
}, [tokens, allowedSymbols, excludeNative])
8794

8895
const showCategories =
8996
!allowedSymbols && withCategories && !tokenSearchFilter && !isAllNetworks
@@ -170,6 +177,7 @@ export const SelectTokenList: FC<SelectTokenListProps> = memo(
170177
headerRef,
171178
afterTokenSelect,
172179
allowedSymbols,
180+
excludeNative: !isWalletFunded,
173181
selectedChainId,
174182
selectedTokenAddress,
175183
isAllNetworks,

packages/widget-checkout/src/pages/SelectTokenPage/SelectTokenPage.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,8 @@ export const SelectTokenPage: React.FC = () => {
3434
const fundingSource = useCheckoutFlowStore((s) => s.fundingSource)
3535
const isWalletFunded = useIsWalletFundedFlow()
3636
const isExchangeFlow = fundingSource === 'exchange'
37-
const exchangeAllowedSymbols = useMemo(
38-
() => new Set(['USDC', 'USDT', 'ETH']),
39-
[]
40-
)
37+
// IF deposits can't accept the native gas token, so the curated set is ERC20-only.
38+
const exchangeAllowedSymbols = useMemo(() => new Set(['USDC', 'USDT']), [])
4139
const hideChainSelect = hiddenUI?.chainSelect || isExchangeFlow
4240

4341
const isMobile = useMediaQuery((theme: Theme) =>

packages/widget-checkout/src/utils/checkoutToWidgetConfig.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ChainType } from '@lifi/sdk'
12
import { describe, expect, it } from 'vitest'
23
import type { CheckoutConfig } from '../types/config.js'
34
import { checkoutConfigToWidgetConfig } from './checkoutToWidgetConfig.js'
@@ -57,4 +58,26 @@ describe('checkoutConfigToWidgetConfig', () => {
5758
const result = checkoutConfigToWidgetConfig(minimalCheckout)
5859
expect(result.integrator).toBe('test-integrator')
5960
})
61+
62+
it('forces EVM-only chain types', () => {
63+
const result = checkoutConfigToWidgetConfig(minimalCheckout)
64+
expect(result.chains?.types?.allow).toEqual([ChainType.EVM])
65+
})
66+
67+
it('preserves integrator chain-id allow alongside the forced EVM type lock', () => {
68+
const result = checkoutConfigToWidgetConfig({
69+
integrator: 'x',
70+
config: { chains: { allow: [1, 137] } },
71+
})
72+
expect(result.chains?.allow).toEqual([1, 137])
73+
expect(result.chains?.types?.allow).toEqual([ChainType.EVM])
74+
})
75+
76+
it('overrides an integrator-supplied non-EVM chain type', () => {
77+
const result = checkoutConfigToWidgetConfig({
78+
integrator: 'x',
79+
config: { chains: { types: { allow: [ChainType.SVM] } } },
80+
})
81+
expect(result.chains?.types?.allow).toEqual([ChainType.EVM])
82+
})
6083
})

packages/widget-checkout/src/utils/checkoutToWidgetConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ChainType } from '@lifi/sdk'
12
import type { WidgetConfig } from '@lifi/widget/shared'
23
import type { CheckoutConfig } from '../types/config.js'
34

@@ -15,6 +16,11 @@ export function checkoutConfigToWidgetConfig(
1516
// toChain/toToken/toAddress are required config; CheckoutConfigGuard blocks when missing.
1617
return {
1718
...merged,
19+
// EVM-only for now; cascades to chains, tokens, routes, and the wallet menu.
20+
chains: {
21+
...merged.chains,
22+
types: { allow: [ChainType.EVM] },
23+
},
1824
hiddenUI: {
1925
...merged.hiddenUI,
2026
toToken: true,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { isNativeToken, NATIVE_TOKEN_ADDRESS } from './nativeToken.js'
3+
4+
describe('isNativeToken', () => {
5+
it('matches the zero sentinel address', () => {
6+
expect(isNativeToken(NATIVE_TOKEN_ADDRESS)).toBe(true)
7+
})
8+
9+
it('is case-insensitive', () => {
10+
expect(isNativeToken('0x0000000000000000000000000000000000000000')).toBe(
11+
true
12+
)
13+
expect(isNativeToken('0X0000000000000000000000000000000000000000')).toBe(
14+
true
15+
)
16+
})
17+
18+
it('returns false for ERC20 addresses', () => {
19+
expect(isNativeToken('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')).toBe(
20+
false
21+
)
22+
})
23+
24+
it('returns false for undefined', () => {
25+
expect(isNativeToken(undefined)).toBe(false)
26+
})
27+
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const NATIVE_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'
2+
3+
export const isNativeToken = (address?: string): boolean =>
4+
address?.toLowerCase() === NATIVE_TOKEN_ADDRESS

packages/widget/src/providers/WalletProvider/WalletProvider.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { useTranslation } from 'react-i18next'
1111
import { useAvailableChains } from '../../hooks/useAvailableChains.js'
1212
import { useInitializeSDKProviders } from '../../hooks/useInitializeSDKProviders.js'
13+
import { getConfigItemSets, isItemAllowedForSets } from '../../utils/item.js'
1314
import { useWidgetConfig } from '../WidgetProvider/WidgetProvider.js'
1415
import { useExternalWalletProvider } from './useExternalWalletProvider.js'
1516

@@ -21,12 +22,23 @@ export const WalletProvider = ({
2122
children,
2223
providers,
2324
}: PropsWithChildren<WalletProviderProps>): JSX.Element => {
24-
const { walletConfig } = useWidgetConfig()
25+
const { walletConfig, chains: chainsConfig } = useWidgetConfig()
2526
const { chains } = useAvailableChains()
2627
const { i18n } = useTranslation()
2728
const { useExternalWalletProvidersOnly, internalChainTypes } =
2829
useExternalWalletProvider()
2930

31+
// Restrict offered wallets to the ecosystems allowed by chains.types config.
32+
const enabledChainTypes = useMemo(() => {
33+
const chainTypeSets = getConfigItemSets(
34+
chainsConfig?.types,
35+
(items) => new Set(items)
36+
)
37+
return internalChainTypes.filter((chainType) =>
38+
isItemAllowedForSets(chainType, chainTypeSets)
39+
)
40+
}, [internalChainTypes, chainsConfig?.types])
41+
3042
if (
3143
!providers.length &&
3244
!useExternalWalletProvidersOnly &&
@@ -40,12 +52,12 @@ export const WalletProvider = ({
4052
const walletManagementConfig = useMemo(
4153
() => ({
4254
locale: i18n.resolvedLanguage as never,
43-
enabledChainTypes: internalChainTypes,
55+
enabledChainTypes,
4456
walletEcosystemsOrder: walletConfig?.walletEcosystemsOrder,
4557
}),
4658
[
4759
i18n.resolvedLanguage,
48-
internalChainTypes,
60+
enabledChainTypes,
4961
walletConfig?.walletEcosystemsOrder,
5062
]
5163
)

0 commit comments

Comments
 (0)