diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po
index c740d59cfbf..18a1031a8e2 100644
--- a/apps/cowswap-frontend/src/locales/en-US.po
+++ b/apps/cowswap-frontend/src/locales/en-US.po
@@ -1510,6 +1510,7 @@ msgstr "Edit code"
msgid "Select an {accountProxyLabelString} to check for available refunds {chain}"
msgstr "Select an {accountProxyLabelString} to check for available refunds {chain}"
+#: apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/index.tsx
#: apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/index.tsx
msgid "Unsupported wallet"
msgstr "Unsupported wallet"
diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx
index 80a5d411488..2eed4569164 100644
--- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx
+++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx
@@ -72,7 +72,10 @@ export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: T
return (
<>
{(() => {
- if (localFormValidation === TwapFormState.TX_BUNDLING_NOT_SUPPORTED) {
+ if (
+ localFormValidation === TwapFormState.WALLET_NOT_SUPPORTED ||
+ localFormValidation === TwapFormState.TX_BUNDLING_NOT_SUPPORTED
+ ) {
return
}
diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx
index 721afc3d403..457d1a42830 100644
--- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx
+++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx
@@ -1,20 +1,23 @@
+import { ReactNode } from 'react'
+
import { getSafeAccountUrl } from '@cowprotocol/core'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { ExternalLink, InlineBanner, StatusColorVariant } from '@cowprotocol/ui'
import { Trans } from '@lingui/react/macro'
-import { UNSUPPORTED_WALLET_LINK } from 'modules/twap/const'
+import { UNSUPPORTED_WALLET_LINK } from 'modules/twap'
export interface UnsupportedWalletWarningProps {
chainId: SupportedChainId
account?: string
+ /**
+ * If true, we want to suggest the user to try the Safe web app, because their wallet connection did not confirm tx bundling.
+ */
isSafeViaWc: boolean
}
-// TODO: Add proper return type annotation
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function UnsupportedWalletWarning({ isSafeViaWc, chainId, account }: UnsupportedWalletWarningProps) {
+export function UnsupportedWalletWarning({ isSafeViaWc, chainId, account }: UnsupportedWalletWarningProps): ReactNode {
if (isSafeViaWc && account) {
return (
diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx
index db846a170ad..4c8d8c6df0c 100644
--- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx
+++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx
@@ -129,7 +129,7 @@ export function TwapFormWidget({ tradeWarnings }: TwapFormWidget): ReactNode {
useEffect(() => {
if (account && verification) {
- if (localFormValidation === TwapFormState.TX_BUNDLING_NOT_SUPPORTED) {
+ if (localFormValidation === TwapFormState.WALLET_NOT_SUPPORTED) {
cowAnalytics.sendEvent({
category: CowSwapAnalyticsCategory.TWAP,
action: 'non-compatible',
diff --git a/apps/cowswap-frontend/src/modules/twap/hooks/useTwapFormState.ts b/apps/cowswap-frontend/src/modules/twap/hooks/useTwapFormState.ts
index b741ae10b9b..2af9f640eca 100644
--- a/apps/cowswap-frontend/src/modules/twap/hooks/useTwapFormState.ts
+++ b/apps/cowswap-frontend/src/modules/twap/hooks/useTwapFormState.ts
@@ -1,6 +1,6 @@
import { useAtomValue } from 'jotai'
-import { useIsTxBundlingSupported, useWalletInfo } from '@cowprotocol/wallet'
+import { isSafeAppAtom, isSafeViaWcAtom, useIsTxBundlingSupported, useWalletInfo } from '@cowprotocol/wallet'
import { useGetReceiveAmountInfo } from 'modules/trade'
import { tradeFormValidationContextAtom } from 'modules/tradeFormValidation'
@@ -26,9 +26,13 @@ export function useTwapFormState(): TwapFormState | null {
const tradeFormValidationContext = useAtomValue(tradeFormValidationContextAtom)
const verification = useFallbackHandlerVerification()
+ const isSafeApp = useAtomValue(isSafeAppAtom)
+ const isSafeViaWc = useAtomValue(isSafeViaWcAtom)
const isTxBundlingSupported = useIsTxBundlingSupported()
+ const isWalletSupported = isSafeApp === null || isSafeViaWc === null ? null : isSafeApp || isSafeViaWc
return getTwapFormState({
+ isWalletSupported,
isTxBundlingSupported,
verification,
twapOrder,
diff --git a/apps/cowswap-frontend/src/modules/twap/index.ts b/apps/cowswap-frontend/src/modules/twap/index.ts
index 0486398f7bb..09286babe3d 100644
--- a/apps/cowswap-frontend/src/modules/twap/index.ts
+++ b/apps/cowswap-frontend/src/modules/twap/index.ts
@@ -9,6 +9,7 @@ export * from './hooks/useMapTwapCurrencyInfo'
export * from './hooks/useScaledReceiveAmountInfo'
export * from './hooks/useTwapFormState'
export * from './hooks/useTwapSlippage'
+export * from './const'
export { useIsFallbackHandlerRequired } from './hooks/useFallbackHandlerVerification'
export { SetupFallbackHandlerWarning } from './containers/SetupFallbackHandlerWarning'
export * from './updaters/index'
diff --git a/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/getTwapFormState.test.ts b/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/getTwapFormState.test.ts
index 2748d653242..f9058e3ff81 100644
--- a/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/getTwapFormState.test.ts
+++ b/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/getTwapFormState.test.ts
@@ -25,9 +25,42 @@ const twapOrder: TWAPOrder = {
}
describe('getTwapFormState()', () => {
+ it('returns WALLET_NOT_SUPPORTED for a non-Safe wallet', () => {
+ const result = getTwapFormState({
+ isWalletSupported: false,
+ isTxBundlingSupported: true,
+ verification: ExtensibleFallbackVerification.HAS_NOTHING,
+ twapOrder,
+ sellAmountPartFiat: null,
+ chainId: SupportedChainId.SEPOLIA,
+ partTime: undefined,
+ numberOfPartsValue: 1,
+ tradeFormValidationContext: null,
+ })
+
+ expect(result).toEqual(TwapFormState.WALLET_NOT_SUPPORTED)
+ })
+
+ it('returns TX_BUNDLING_NOT_SUPPORTED for a Safe without batching support', () => {
+ const result = getTwapFormState({
+ isWalletSupported: true,
+ isTxBundlingSupported: false,
+ verification: ExtensibleFallbackVerification.HAS_NOTHING,
+ twapOrder,
+ sellAmountPartFiat: null,
+ chainId: SupportedChainId.SEPOLIA,
+ partTime: undefined,
+ numberOfPartsValue: 1,
+ tradeFormValidationContext: null,
+ })
+
+ expect(result).toEqual(TwapFormState.TX_BUNDLING_NOT_SUPPORTED)
+ })
+
describe('When sell fiat amount is under threshold', () => {
it('And order has buy amount, then should return SELL_AMOUNT_TOO_SMALL', () => {
const result = getTwapFormState({
+ isWalletSupported: true,
isTxBundlingSupported: true,
verification: ExtensibleFallbackVerification.HAS_DOMAIN_VERIFIER,
twapOrder: { ...twapOrder },
@@ -43,6 +76,7 @@ describe('getTwapFormState()', () => {
it('And order does NOT have buy amount, then should return null', () => {
const result = getTwapFormState({
+ isWalletSupported: true,
isTxBundlingSupported: true,
verification: ExtensibleFallbackVerification.HAS_DOMAIN_VERIFIER,
twapOrder: { ...twapOrder, buyAmount: CurrencyAmount.fromRawAmount(COW_SEPOLIA, 0) },
diff --git a/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/getTwapFormState.tsx b/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/getTwapFormState.tsx
index dd87b5265ba..5f8c91e0ce3 100644
--- a/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/getTwapFormState.tsx
+++ b/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/getTwapFormState.tsx
@@ -14,6 +14,7 @@ import { isPartTimeIntervalTooShort } from '../../utils/isPartTimeIntervalTooSho
import { isSellAmountTooSmall } from '../../utils/isSellAmountTooSmall'
export interface TwapFormStateParams {
+ isWalletSupported: boolean | null
isTxBundlingSupported: boolean | null
verification: ExtensibleFallbackVerification | null
twapOrder: TWAPOrder | null
@@ -26,6 +27,7 @@ export interface TwapFormStateParams {
export enum TwapFormState {
LOADING_SAFE_INFO = 'LOADING_SAFE_INFO',
+ WALLET_NOT_SUPPORTED = 'WALLET_NOT_SUPPORTED',
TX_BUNDLING_NOT_SUPPORTED = 'TX_BUNDLING_NOT_SUPPORTED',
SELL_AMOUNT_TOO_SMALL = 'SELL_AMOUNT_TOO_SMALL',
PART_TIME_INTERVAL_TOO_SHORT = 'PART_TIME_INTERVAL_TOO_SHORT',
@@ -35,6 +37,7 @@ export enum TwapFormState {
export function getTwapFormState(props: TwapFormStateParams): TwapFormState | null {
const {
+ isWalletSupported,
twapOrder,
isTxBundlingSupported,
verification,
@@ -45,9 +48,12 @@ export function getTwapFormState(props: TwapFormStateParams): TwapFormState | nu
numberOfPartsValue,
} = props
+ if (isWalletSupported === false) return TwapFormState.WALLET_NOT_SUPPORTED
if (isTxBundlingSupported === false) return TwapFormState.TX_BUNDLING_NOT_SUPPORTED
- if (verification === null || isTxBundlingSupported === null) return TwapFormState.LOADING_SAFE_INFO
+ if (verification === null || isTxBundlingSupported === null || isWalletSupported === null) {
+ return TwapFormState.LOADING_SAFE_INFO
+ }
if (!isFractionFalsy(twapOrder?.buyAmount) && isSellAmountTooSmall(sellAmountPartFiat, chainId)) {
return TwapFormState.SELL_AMOUNT_TOO_SMALL
diff --git a/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/index.tsx b/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/index.tsx
index e4fd361d09d..27b3bfdab11 100644
--- a/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/index.tsx
+++ b/apps/cowswap-frontend/src/modules/twap/pure/PrimaryActionButton/index.tsx
@@ -18,6 +18,11 @@ const buttonsMap: RecordLoading...
),
+ [TwapFormState.WALLET_NOT_SUPPORTED]: () => (
+
+ Unsupported wallet
+
+ ),
[TwapFormState.TX_BUNDLING_NOT_SUPPORTED]: () => (
Unsupported wallet
diff --git a/libs/wallet/package.json b/libs/wallet/package.json
index 4d2743f52c0..e9a37fd8814 100644
--- a/libs/wallet/package.json
+++ b/libs/wallet/package.json
@@ -41,7 +41,6 @@
"@cowprotocol/iframe-transport": "workspace:*",
"@cowprotocol/types": "workspace:*",
"@cowprotocol/ui": "workspace:*",
- "@cowprotocol/wallet-provider": "workspace:*",
"@ethereumjs/util": "10.1.0",
"@metamask/jazzicon": "2.0.0",
"@metamask/sdk": "0.31.4",
diff --git a/libs/wallet/src/api/state/walletCapabilitiesAtom.test.ts b/libs/wallet/src/api/state/walletCapabilitiesAtom.test.ts
index 7a08371c851..45b3b5bafbf 100644
--- a/libs/wallet/src/api/state/walletCapabilitiesAtom.test.ts
+++ b/libs/wallet/src/api/state/walletCapabilitiesAtom.test.ts
@@ -1,39 +1,22 @@
import { createStore, type WritableAtom } from 'jotai'
-import type { EIP1193Provider, PublicClient } from 'viem'
import type { Connector } from 'wagmi'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
-import { AccountType } from '@cowprotocol/types'
import {
- getShouldSkipCapabilitiesCheck,
isAtomicBatchSupportedAtom,
isAtomicBatchSupportedAsyncAtom,
isAtomicBatchSupportedLoadableAtom,
- resolveCapabilitiesForChain,
walletCapabilitiesAtom,
} from './walletCapabilitiesAtom'
-import {
- accountTypeAtom,
- isSafeAppAtom,
- isSafeViaWcAtom,
- isSafeWalletAtom,
- isSmartContractWalletAtom,
-} from '../../wagmi/state/walletMetadata.atoms'
+import { isSafeAppAtom, isSafeViaWcAtom } from '../../wagmi/state/walletMetadata.atoms'
import { walletInfoAtom } from '../state'
/** Mocked module exports writable primitives; Jest widens atom types, so casts are required for store.set. */
const writableIsSafeAppAtom = isSafeAppAtom as WritableAtom
const writableIsSafeViaWcAtom = isSafeViaWcAtom as WritableAtom
-const writableAccountTypeAtom = accountTypeAtom as WritableAtom
-const writableIsSmartContractWalletAtom = isSmartContractWalletAtom as WritableAtom<
- boolean | null,
- [boolean | null],
- unknown
->
-const writableIsSafeWalletAtom = isSafeWalletAtom as WritableAtom
import type { WalletCapabilities } from './walletCapabilitiesAtom'
import type { WalletInfo } from '../types'
@@ -44,9 +27,6 @@ jest.mock('../../wagmi/state/walletMetadata.atoms', () => {
return {
isSafeAppAtom: jotai.atom(false),
isSafeViaWcAtom: jotai.atom(false),
- accountTypeAtom: jotai.atom('EOA'),
- isSmartContractWalletAtom: jotai.atom(false),
- isSafeWalletAtom: jotai.atom(false),
}
})
@@ -55,150 +35,34 @@ const MOCK_CHAIN_ID = SupportedChainId.MAINNET
const MOCK_CONNECTOR = { type: 'injected' } as Connector
const mockGetCapabilities = jest.fn()
-const mockWagmiConfigGetClient = jest.fn()
-const mockGetIsWalletConnect = jest.fn()
-const mockIsMobile = { value: false }
-
-jest.mock('@cowprotocol/common-utils', () => {
- const actual = jest.requireActual('@cowprotocol/common-utils')
-
- return {
- ...actual,
- getCurrentChainIdFromUrl: () => 1,
- get isMobile() {
- return mockIsMobile.value
- },
- }
-})
-jest.mock('../../wagmi/hooks/useIsWalletConnect', () => ({
- getIsWalletConnect: (...args: unknown[]) => mockGetIsWalletConnect(...args),
-}))
-
-jest.mock('viem/actions', () => ({
+jest.mock('wagmi/actions', () => ({
getCapabilities: (...args: unknown[]) => mockGetCapabilities(...args),
}))
jest.mock('../../wagmi/config', () => ({
- wagmiConfig: {
- getClient: (...args: unknown[]) => mockWagmiConfigGetClient(...args),
- },
+ wagmiConfig: {},
}))
-function createMockEip1193Provider(request = jest.fn()): EIP1193Provider {
- return {
- request,
- on: jest.fn(),
- removeListener: jest.fn(),
- } as unknown as EIP1193Provider
-}
-
-function seedResolvedWalletMetadata(
- store: ReturnType,
- overrides: Partial<{
- accountType: AccountType | null
- isSafeWallet: boolean
- isSmartContractWallet: boolean | null
- isSafeViaWc: boolean
- isSafeApp: boolean
- }> = {},
-): void {
- store.set(writableAccountTypeAtom, overrides.accountType ?? AccountType.EOA)
- store.set(writableIsSafeWalletAtom, overrides.isSafeWallet ?? false)
- store.set(
- writableIsSmartContractWalletAtom,
- 'isSmartContractWallet' in overrides ? (overrides.isSmartContractWallet ?? null) : false,
- )
- store.set(writableIsSafeViaWcAtom, overrides.isSafeViaWc ?? false)
- store.set(writableIsSafeAppAtom, overrides.isSafeApp ?? false)
-}
-
function setWalletInfo(
store: ReturnType,
overrides: Partial<{
account: string
chainId: SupportedChainId
connector: Connector
- provider: NonNullable
}> = {},
): void {
store.set(walletInfoAtom, {
chainId: overrides.chainId ?? MOCK_CHAIN_ID,
account: overrides.account ?? MOCK_ACCOUNT,
connector: overrides.connector ?? MOCK_CONNECTOR,
- provider: overrides.provider ?? createMockEip1193Provider(),
})
}
-describe('resolveCapabilitiesForChain', () => {
- const capabilities: WalletCapabilities = { atomic: { status: 'supported' } }
-
- it('matches numeric chain id key', () => {
- expect(resolveCapabilitiesForChain({ [MOCK_CHAIN_ID]: capabilities }, MOCK_CHAIN_ID)).toEqual(capabilities)
- })
-
- it('matches hex chain id key', () => {
- expect(resolveCapabilitiesForChain({ '0x1': capabilities }, MOCK_CHAIN_ID)).toEqual(capabilities)
- })
-
- it('falls back to the first entry when no chain key matches', () => {
- expect(resolveCapabilitiesForChain({ '0x64': capabilities }, MOCK_CHAIN_ID)).toEqual(capabilities)
- })
-
- it('returns null for empty capabilities', () => {
- expect(resolveCapabilitiesForChain({}, MOCK_CHAIN_ID)).toBeNull()
- })
-})
-
-describe('getShouldSkipCapabilitiesCheck', () => {
- beforeEach(() => {
- mockIsMobile.value = false
- mockGetIsWalletConnect.mockReturnValue(false)
- })
-
- it('returns false on desktop', async () => {
- expect(await getShouldSkipCapabilitiesCheck(MOCK_CONNECTOR, createMockEip1193Provider())).toBe(false)
- })
-
- it('returns true on mobile WalletConnect', async () => {
- mockIsMobile.value = true
- mockGetIsWalletConnect.mockReturnValue(true)
-
- expect(await getShouldSkipCapabilitiesCheck(MOCK_CONNECTOR, createMockEip1193Provider())).toBe(true)
- })
-})
-
describe('walletCapabilitiesAtom', () => {
beforeEach(() => {
jest.clearAllMocks()
- mockIsMobile.value = false
- mockGetIsWalletConnect.mockReturnValue(false)
mockGetCapabilities.mockResolvedValue({})
- mockWagmiConfigGetClient.mockReturnValue({ chainId: MOCK_CHAIN_ID })
- })
-
- it('returns null when isSafeViaWc is still loading', async () => {
- const store = createStore()
- store.set(writableIsSafeViaWcAtom, null)
- setWalletInfo(store)
-
- const result = await store.get(walletCapabilitiesAtom)
-
- expect(result).toBeNull()
- expect(mockGetCapabilities).not.toHaveBeenCalled()
- })
-
- it('returns null on mobile WalletConnect without fetching capabilities', async () => {
- mockIsMobile.value = true
- mockGetIsWalletConnect.mockReturnValue(true)
-
- const store = createStore()
- setWalletInfo(store)
-
- const result = await store.get(walletCapabilitiesAtom)
-
- expect(result).toBeNull()
- expect(mockGetCapabilities).not.toHaveBeenCalled()
})
describe('walletInfoAtom state: missing required fields', () => {
@@ -207,7 +71,6 @@ describe('walletCapabilitiesAtom', () => {
store.set(walletInfoAtom, {
chainId: MOCK_CHAIN_ID,
connector: MOCK_CONNECTOR,
- provider: createMockEip1193Provider(),
})
const result = await store.get(walletCapabilitiesAtom)
@@ -221,7 +84,6 @@ describe('walletCapabilitiesAtom', () => {
store.set(walletInfoAtom, {
account: MOCK_ACCOUNT,
connector: MOCK_CONNECTOR,
- provider: createMockEip1193Provider(),
} as WalletInfo)
const result = await store.get(walletCapabilitiesAtom)
@@ -235,21 +97,6 @@ describe('walletCapabilitiesAtom', () => {
store.set(walletInfoAtom, {
chainId: MOCK_CHAIN_ID,
account: MOCK_ACCOUNT,
- provider: createMockEip1193Provider(),
- })
-
- const result = await store.get(walletCapabilitiesAtom)
-
- expect(result).toBeNull()
- expect(mockGetCapabilities).not.toHaveBeenCalled()
- })
-
- it('returns null when provider is missing', async () => {
- const store = createStore()
- store.set(walletInfoAtom, {
- chainId: MOCK_CHAIN_ID,
- account: MOCK_ACCOUNT,
- connector: MOCK_CONNECTOR,
})
const result = await store.get(walletCapabilitiesAtom)
@@ -259,7 +106,7 @@ describe('walletCapabilitiesAtom', () => {
})
})
- describe('getCapabilities (viem)', () => {
+ describe('getCapabilities (wagmi)', () => {
it('returns capabilities when getCapabilities resolves for the current chain', async () => {
const capabilities: WalletCapabilities = { atomic: { status: 'supported' } }
mockGetCapabilities.mockResolvedValue(capabilities)
@@ -270,8 +117,14 @@ describe('walletCapabilitiesAtom', () => {
const result = await store.get(walletCapabilitiesAtom)
expect(result).toEqual(capabilities)
- expect(mockGetCapabilities).toHaveBeenCalled()
- expect(mockWagmiConfigGetClient).toHaveBeenCalledWith({ chainId: MOCK_CHAIN_ID })
+ expect(mockGetCapabilities).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ account: MOCK_ACCOUNT,
+ chainId: MOCK_CHAIN_ID,
+ connector: MOCK_CONNECTOR,
+ }),
+ )
})
it('returns empty capabilities object when getCapabilities resolves with no fields', async () => {
@@ -285,111 +138,23 @@ describe('walletCapabilitiesAtom', () => {
expect(result).toEqual({})
})
- it('returns null when getCapabilities and wallet_getCapabilities both fail', async () => {
- mockGetCapabilities.mockRejectedValue(new Error('viem error'))
- const mockRequest = jest.fn().mockRejectedValue(new Error('legacy error'))
+ it('returns null when getCapabilities fails', async () => {
+ mockGetCapabilities.mockRejectedValue(new Error('wagmi error'))
const store = createStore()
- setWalletInfo(store, { provider: createMockEip1193Provider(mockRequest) })
-
- const result = await store.get(walletCapabilitiesAtom)
-
- expect(result).toBeNull()
- expect(mockRequest).toHaveBeenCalledWith({
- method: 'wallet_getCapabilities',
- params: [MOCK_ACCOUNT],
- })
- })
-
- it('returns null when getCapabilities fails and provider does not support EIP-1193 requests', async () => {
- const error = new Error('viem error')
- mockGetCapabilities.mockRejectedValue(error)
-
- const store = createStore()
- setWalletInfo(store, { provider: {} as PublicClient })
+ setWalletInfo(store)
const result = await store.get(walletCapabilitiesAtom)
expect(result).toBeNull()
})
- it('returns null when getCapabilities times out and provider does not support EIP-1193 requests', async () => {
+ it('returns null when getCapabilities times out', async () => {
jest.useFakeTimers()
mockGetCapabilities.mockImplementation(() => new Promise(() => undefined))
const store = createStore()
- setWalletInfo(store, { provider: {} as PublicClient })
-
- try {
- const resultPromise = store.get(walletCapabilitiesAtom)
- await jest.advanceTimersByTimeAsync(5_000)
- const result = await resultPromise
-
- expect(result).toBeNull()
- } finally {
- jest.useRealTimers()
- }
- })
- })
-
- describe('wallet_getCapabilities legacy fallback', () => {
- it('returns capabilities resolved from hex chain id key when viem throws', async () => {
- const capabilities: WalletCapabilities = { atomic: { status: 'supported' } }
- mockGetCapabilities.mockRejectedValue(new Error('viem error'))
- const mockRequest = jest.fn().mockResolvedValue({ '0x1': capabilities })
-
- const store = createStore()
- setWalletInfo(store, { provider: createMockEip1193Provider(mockRequest) })
-
- const result = await store.get(walletCapabilitiesAtom)
-
- expect(result).toEqual(capabilities)
- })
-
- it('falls back to the first legacy capability entry when chain key is missing', async () => {
- const capabilities: WalletCapabilities = { atomic: { status: 'supported' } }
- mockGetCapabilities.mockRejectedValue(new Error('viem error'))
- const mockRequest = jest.fn().mockResolvedValue({ '0x64': capabilities })
-
- const store = createStore()
- setWalletInfo(store, { provider: createMockEip1193Provider(mockRequest) })
-
- const result = await store.get(walletCapabilitiesAtom)
-
- expect(result).toEqual(capabilities)
- })
-
- it('uses legacy fallback when getCapabilities times out', async () => {
- jest.useFakeTimers()
- const capabilities: WalletCapabilities = { atomic: { status: 'supported' } }
- mockGetCapabilities.mockImplementation(() => new Promise(() => undefined))
- const mockRequest = jest.fn().mockResolvedValue({ '0x1': capabilities })
-
- const store = createStore()
- setWalletInfo(store, { provider: createMockEip1193Provider(mockRequest) })
-
- try {
- const resultPromise = store.get(walletCapabilitiesAtom)
- await jest.advanceTimersByTimeAsync(5_000)
- const result = await resultPromise
-
- expect(result).toEqual(capabilities)
- expect(mockRequest).toHaveBeenCalledWith({
- method: 'wallet_getCapabilities',
- params: [MOCK_ACCOUNT],
- })
- } finally {
- jest.useRealTimers()
- }
- })
-
- it('returns null when legacy fallback times out', async () => {
- jest.useFakeTimers()
- mockGetCapabilities.mockRejectedValue(new Error('viem error'))
- const mockRequest = jest.fn().mockImplementation(() => new Promise(() => undefined))
-
- const store = createStore()
- setWalletInfo(store, { provider: createMockEip1193Provider(mockRequest) })
+ setWalletInfo(store)
try {
const resultPromise = store.get(walletCapabilitiesAtom)
@@ -407,15 +172,11 @@ describe('walletCapabilitiesAtom', () => {
describe('isAtomicBatchSupportedAsyncAtom', () => {
beforeEach(() => {
jest.clearAllMocks()
- mockIsMobile.value = false
- mockGetIsWalletConnect.mockReturnValue(false)
mockGetCapabilities.mockResolvedValue({})
- mockWagmiConfigGetClient.mockReturnValue({ chainId: MOCK_CHAIN_ID })
})
it('returns false when walletInfoAtom yields no capabilities (disconnected)', async () => {
const store = createStore()
- seedResolvedWalletMetadata(store)
store.set(walletInfoAtom, { chainId: MOCK_CHAIN_ID } as WalletInfo)
const result = await store.get(isAtomicBatchSupportedAsyncAtom)
@@ -455,7 +216,7 @@ describe('isAtomicBatchSupportedAsyncAtom', () => {
expect(result).toBe(true)
})
- it('returns false when isSafeViaWcAtom and capabilities atomic status is ready', async () => {
+ it('returns true when isSafeViaWcAtom and capabilities atomic status is ready', async () => {
const store = createStore()
store.set(writableIsSafeViaWcAtom, true)
mockGetCapabilities.mockResolvedValue({ atomic: { status: 'ready' } })
@@ -463,7 +224,7 @@ describe('isAtomicBatchSupportedAsyncAtom', () => {
const result = await store.get(isAtomicBatchSupportedAsyncAtom)
- expect(result).toBe(false)
+ expect(result).toBe(true)
})
it('returns true when atomicBatch.supported is true', async () => {
@@ -497,65 +258,12 @@ describe('isAtomicBatchSupportedAsyncAtom', () => {
expect(result).toBe(true)
})
-
- it('does not wait on Safe info for EOA when gnosisSafeInfo is still undefined', async () => {
- const store = createStore()
- seedResolvedWalletMetadata(store, { accountType: AccountType.EOA, isSafeWallet: false })
- mockGetCapabilities.mockResolvedValue({ atomic: { status: 'supported' } })
- setWalletInfo(store)
-
- const result = await store.get(isAtomicBatchSupportedAsyncAtom)
-
- expect(result).toBe(true)
- expect(mockGetCapabilities).toHaveBeenCalled()
- })
-
- it('returns false for smart contract wallet before Safe info confirms a Safe', async () => {
- const store = createStore()
- seedResolvedWalletMetadata(store, { isSmartContractWallet: true, isSafeWallet: false })
- mockGetCapabilities.mockResolvedValue({ atomic: { status: 'supported' } })
- setWalletInfo(store)
-
- const result = await store.get(isAtomicBatchSupportedAsyncAtom)
-
- expect(result).toBe(false)
- expect(mockGetCapabilities).not.toHaveBeenCalled()
- })
-
- it('returns false for non-Safe smart contract wallet before checking capabilities', async () => {
- const store = createStore()
- store.set(writableIsSmartContractWalletAtom, true)
- store.set(writableIsSafeWalletAtom, false)
- mockGetCapabilities.mockResolvedValue({ atomic: { status: 'supported' } })
- setWalletInfo(store)
-
- const result = await store.get(isAtomicBatchSupportedAsyncAtom)
-
- expect(result).toBe(false)
- expect(mockGetCapabilities).not.toHaveBeenCalled()
- })
-
- it('returns false for EIP-7702 account before checking capabilities', async () => {
- const store = createStore()
- store.set(writableAccountTypeAtom, AccountType.EIP7702EOA)
- store.set(writableIsSafeWalletAtom, false)
- mockGetCapabilities.mockResolvedValue({ atomic: { status: 'supported' } })
- setWalletInfo(store)
-
- const result = await store.get(isAtomicBatchSupportedAsyncAtom)
-
- expect(result).toBe(false)
- expect(mockGetCapabilities).not.toHaveBeenCalled()
- })
})
describe('isAtomicBatchSupportedLoadableAtom and isAtomicBatchSupportedAtom', () => {
beforeEach(() => {
jest.clearAllMocks()
- mockIsMobile.value = false
- mockGetIsWalletConnect.mockReturnValue(false)
mockGetCapabilities.mockResolvedValue({})
- mockWagmiConfigGetClient.mockReturnValue({ chainId: MOCK_CHAIN_ID })
})
it('isAtomicBatchSupportedAtom returns null while loading', async () => {
@@ -582,9 +290,7 @@ describe('isAtomicBatchSupportedLoadableAtom and isAtomicBatchSupportedAtom', ()
store.set(writableIsSafeViaWcAtom, false)
store.set(writableIsSafeAppAtom, false)
mockGetCapabilities.mockRejectedValue(new Error('network error'))
- setWalletInfo(store, {
- provider: createMockEip1193Provider(jest.fn().mockRejectedValue(new Error('legacy error'))),
- })
+ setWalletInfo(store)
store.get(isAtomicBatchSupportedLoadableAtom)
const asyncResult = await store.get(isAtomicBatchSupportedAsyncAtom)
diff --git a/libs/wallet/src/api/state/walletCapabilitiesAtom.ts b/libs/wallet/src/api/state/walletCapabilitiesAtom.ts
index 24d34f3e7fe..5cdd08d5772 100644
--- a/libs/wallet/src/api/state/walletCapabilitiesAtom.ts
+++ b/libs/wallet/src/api/state/walletCapabilitiesAtom.ts
@@ -1,171 +1,38 @@
import { atom } from 'jotai'
import { loadable } from 'jotai/utils'
-import { EIP1193Provider, numberToHex, PublicClient } from 'viem'
-import { Connector } from 'wagmi'
+import { getCapabilities, type GetCapabilitiesReturnType } from 'wagmi/actions'
-import { isMobile, logWallet, normalizeError, TimeoutError, withTimeout } from '@cowprotocol/common-utils'
-import { ProviderMetaInfoPayload, WidgetEthereumProvider } from '@cowprotocol/iframe-transport'
-import { AccountType } from '@cowprotocol/types'
+import { logWallet, normalizeError, TimeoutError, withTimeout } from '@cowprotocol/common-utils'
import ms from 'ms.macro'
-import { getCapabilities, type GetCapabilitiesReturnType } from 'viem/actions'
import { wagmiConfig } from '../../wagmi/config'
-import { getIsWalletConnect } from '../../wagmi/hooks/useIsWalletConnect'
-import {
- isSafeAppAtom,
- isSafeViaWcAtom,
- accountTypeAtom,
- isSmartContractWalletAtom,
- isSafeWalletAtom,
-} from '../../wagmi/state/walletMetadata.atoms'
-import { isEip1193Provider } from '../../wagmi/utils/isEip1193Provider.utils'
+import { isSafeAppAtom, isSafeViaWcAtom } from '../../wagmi/state/walletMetadata.atoms'
import { walletInfoAtom } from '../state'
export type WalletCapabilities = GetCapabilitiesReturnType[number]
const REQUEST_TIMEOUT_MS = ms`5s`
-/**
- * WalletConnect in mobile browsers initiates a request with confirmation to the wallet
- * to get the capabilities. It breaks the flow with perpetual requests.
- */
-export async function getShouldSkipCapabilitiesCheck(
- connector: Connector,
- provider: EIP1193Provider | WidgetEthereumProvider | PublicClient,
-): Promise {
- if (!isMobile) return false
-
- const isWalletConnect = getIsWalletConnect(connector)
-
- if (isWalletConnect) return true
-
- const widgetProviderMetaInfo = await fetchWidgetProviderMetaInfo(provider)
- const isWalletConnectViaWidget = !!widgetProviderMetaInfo?.providerWcMetadata
-
- return isWalletConnectViaWidget
-}
-
-/**
- * Safe WC returns EIP-5792 capabilities keyed by hex chain id (e.g. "0xaa36a7")
- * while walletInfoAtom.chainId is numeric (e.g. 11155111). Numeric lookup alone misses them.
- */
-export function resolveCapabilitiesForChain(
- capabilities: Record,
- chainId: number,
-): WalletCapabilities | null {
- const capabilitiesForChain = capabilities[chainId] ?? capabilities[numberToHex(chainId)]
-
- if (capabilitiesForChain) return capabilitiesForChain
-
- // This fallback was initially here for Safe via WC, even thought it should return a value as shown in the example in
- // the long TSDoc comment below. `Object.values` should not be needed, as we are already parsing this properly with
- // `numberToHex` above:
-
- return Object.values(capabilities)[0] || null
-}
-
-async function fetchWidgetProviderMetaInfo(
- provider: EIP1193Provider | WidgetEthereumProvider | PublicClient,
-): Promise {
- if (provider instanceof WidgetEthereumProvider) {
- const providerMetaInfo = new Promise((resolve) => {
- provider.onProviderMetaInfo((data) => {
- provider.clearProviderMetaInfoListener()
- resolve(data)
- })
- })
-
- return withTimeout(providerMetaInfo, {
- timeout: REQUEST_TIMEOUT_MS,
- timeoutMessage: `Widget provider meta info request timed out after ${REQUEST_TIMEOUT_MS}ms`,
- }).catch(() => {
- provider.clearProviderMetaInfoListener()
- return null
- })
- }
-
- return Promise.resolve(null)
-}
-
/**
* Async atom that fetches wallet capabilities (EIP-5792) via wagmi/viem.
* Returns capabilities for the current account and chain, or null when disconnected or on error.
*/
-// eslint-disable-next-line complexity
export const walletCapabilitiesAtom = atom(async (get): Promise => {
- const { account, chainId, provider, connector } = get(walletInfoAtom)
- const isSafeViaWc = get(isSafeViaWcAtom)
+ const { account, chainId, connector } = get(walletInfoAtom)
- if (!account || !chainId || !connector || !provider || isSafeViaWc === null) return null
-
- let capabilities: WalletCapabilities | null = null
+ if (!account || !chainId || !connector) return null
try {
- const shouldSkipCapabilitiesCheck = await getShouldSkipCapabilitiesCheck(connector, provider)
-
- if (shouldSkipCapabilitiesCheck) {
- return null
- }
-
- /**
- * Viem takes care here of getting the capabilities for the exact chainId, so we don't need to do it manually.
- * However, keep in mind this branch MUST run for Safe via WC, but getCapabilities() will throw an error. However,
- * using `wallet_getCapabilities` directly does work. You can test this with:
- *
- * ```
- * const allCapabilities = await getCapabilities(wagmiConfig.getClient({ chainId }), {
- * account: account as `0x${string}`,
- * }).catch((error) => {
- * console.error('Cannot fetchallCapabilities', error)
- * return {} as GetCapabilitiesReturnType
- * })
- *
- * const capabilitiesForChain = await getCapabilities(wagmiConfig.getClient({ chainId }), {
- * account: account as `0x${string}`,
- * chainId,
- * }).catch((error) => {
- * console.error('Cannot fetch capabilitiesForChain', error)
- * return {} as GetCapabilitiesReturnType
- * })
- *
- * const legacyCapabilities = isEip1193Provider(provider)
- * ? await provider
- * .request({
- * method: 'wallet_getCapabilities',
- * params: [account],
- * })
- * .catch((error) => {
- * console.error('Cannot fetch legacyCapabilities', error)
- * return {} as GetCapabilitiesReturnType
- * })
- * : null
- * ```
- *
- * The last one should return:
- *
- * ```json
- * {
- * "0xaa36a7": {
- * "atomicBatch": {
- * "supported": true
- * },
- * "atomic": {
- * "status": "supported"
- * }
- * }
- * }
- * ```
- */
-
logWallet.debug('Fetching wallet capabilities', { account, chainId })
- capabilities = await withTimeout(
- getCapabilities(wagmiConfig.getClient({ chainId }), {
+ const capabilities = await withTimeout(
+ getCapabilities(wagmiConfig, {
account: account as `0x${string}`,
chainId,
+ connector,
}),
{
timeout: REQUEST_TIMEOUT_MS,
@@ -174,81 +41,45 @@ export const walletCapabilitiesAtom = atom(async (get): Promise => {
const isSafeApp = get(isSafeAppAtom)
-
- if (isSafeApp === null) return null
-
- if (isSafeApp) return true
-
- const accountType = get(accountTypeAtom)
- const isSmartContractWallet = get(isSmartContractWalletAtom)
- const isSafeWallet = get(isSafeWalletAtom)
const isSafeViaWc = get(isSafeViaWcAtom)
- if (accountType === null || isSmartContractWallet === null || isSafeViaWc === null) return null
-
- // Smart accounts (ERC-4337, Coinbase Smart Wallet, EIP-7702, etc.) that are not a Safe lack the
- // fallback handler mechanism TWAP requires, so we treat them as unsupported.
- // Note: useIsSmartContractWallet() only detects AccountType.SMART_CONTRACT, not EIP-7702 accounts
- // (which keep the same EOA address but have delegation bytecode). We check both explicitly.
- if ((isSmartContractWallet || accountType === AccountType.EIP7702EOA) && !isSafeWallet && !isSafeViaWc) return false
+ /**
+ * A SafeWallet connected through SafeApp is assumed to have support.
+ * A SafeWallet connected through WC or any other provider needs to pass the capabilities check.
+ */
+ if (isSafeApp) return true
+ if (isSafeApp === null || isSafeViaWc === null) return null
const walletCapabilities = await get(walletCapabilitiesAtom)
- // If `walletCapabilitiesAtom` returns `null` it's because `shouldSkipCapabilitiesCheck === true`,
- // or because some kind of API empty response or error. So, if we cannot check, then we must be false,
+ // If `walletCapabilitiesAtom` returns `null` it's because some kind of API empty response
+ // or error. So, if we cannot check, then we must be false,
// not null (as some components/functions like `validateTradeForm` treat `null` as loading):
- if (!walletCapabilities) return false
+ if (walletCapabilities === null) return false
- const status = walletCapabilities.atomic?.status || ''
- const supported = walletCapabilities?.atomicBatch?.supported
+ const status = walletCapabilities.atomic?.status
+ const isLegacyAtomicBatchSupported = Boolean(walletCapabilities?.atomicBatch?.supported)
// See https://www.eip5792.xyz/getting-started:
// - supported: The wallet will execute all calls atomically and contiguously
// - ready: The wallet is able to upgrade to supported pending user approval (e.g. via EIP-7702)
- return status === 'supported' || !!supported
- // return status === 'supported' || status === 'ready'
+ return status === 'supported' || status === 'ready' || isLegacyAtomicBatchSupported
})
export const isAtomicBatchSupportedLoadableAtom = loadable(isAtomicBatchSupportedAsyncAtom)
diff --git a/libs/wallet/src/api/types.ts b/libs/wallet/src/api/types.ts
index c227ae7b967..f450ff65504 100644
--- a/libs/wallet/src/api/types.ts
+++ b/libs/wallet/src/api/types.ts
@@ -1,9 +1,7 @@
-import { EIP1193Provider, PublicClient } from 'viem'
import { Connector as WagmiConnector } from 'wagmi'
import { injected, walletConnect, coinbaseWallet, safe } from 'wagmi/connectors'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
-import { WidgetEthereumProvider } from '@cowprotocol/iframe-transport'
import type { SafeInfoResponse } from '@safe-global/api-kit'
export const ConnectionType = {
@@ -41,8 +39,6 @@ export interface WalletInfo {
account?: string
active?: boolean
connector?: WagmiConnector
- provider?: EIP1193Provider | WidgetEthereumProvider | PublicClient
- isConnectionRestoring?: boolean
}
export enum WalletType {
diff --git a/libs/wallet/src/wagmi/updater.ts b/libs/wallet/src/wagmi/updater.ts
index 8d45df69b77..185274e2456 100644
--- a/libs/wallet/src/wagmi/updater.ts
+++ b/libs/wallet/src/wagmi/updater.ts
@@ -8,7 +8,6 @@ import { getCurrentChainIdFromUrl, getRawCurrentChainIdFromUrl, logSafeApi } fro
import { getSafeInfo, normalizeSafeError, SAFE_RATE_LIMIT_MSG } from '@cowprotocol/core'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { AccountType } from '@cowprotocol/types'
-import { useWalletProvider } from '@cowprotocol/wallet-provider'
import type { SafeInfoResponse } from '@safe-global/api-kit'
import type { SafeInfoExtended } from '@safe-global/safe-apps-sdk'
@@ -47,8 +46,7 @@ function useBrowserUrlKey(): string {
function useWalletInfo(): WalletInfo {
// TODO: Replace urlKey with locationNetworkAtom, which will also trigger the useMemo below less often.
const urlKey = useBrowserUrlKey()
- const { address, chainId, isConnected, status, connector } = useAccountState()
- const isConnectionRestoring = status === 'reconnecting'
+ const { address, chainId, isConnected, connector } = useAccountState()
const isChainIdUnsupported = !!chainId && !(chainId in SupportedChainId)
const [lastStableChainId, setLastStableChainId] = useState(undefined)
const [lastResolvedChainId, setLastResolvedChainId] = useState(() => getCurrentChainIdFromUrl())
@@ -84,19 +82,8 @@ function useWalletInfo(): WalletInfo {
active: isConnected,
account: address,
connector,
- isConnectionRestoring,
}
- }, [
- address,
- chainId,
- isConnected,
- connector,
- isChainIdUnsupported,
- isConnectionRestoring,
- lastStableChainId,
- lastResolvedChainId,
- urlKey,
- ])
+ }, [address, chainId, isConnected, connector, isChainIdUnsupported, lastStableChainId, lastResolvedChainId, urlKey])
useEffect(() => {
setLastResolvedChainId(walletInfo.chainId)
@@ -141,7 +128,7 @@ let shortSafeInfoInterval: ReturnType | null = null
let longSafeInfoInterval: ReturnType | null = null
export function WalletUpdater(): null {
- const { chainId, active, account, connector, isConnectionRestoring } = useWalletInfo()
+ const { chainId, active, account, connector } = useWalletInfo()
const walletDetails = useWalletDetails(account)
const gnosisSafeInfo = useSafeInfo()
@@ -150,11 +137,9 @@ export function WalletUpdater(): null {
const setWalletDetails = useSetAtom(walletDetailsAtom)
const setGnosisSafeInfo = useSetAtom(gnosisSafeInfoAtom)
- const provider = useWalletProvider()
-
useEffect(() => {
- setWalletInfo({ chainId, active, account, connector, isConnectionRestoring, provider })
- }, [chainId, active, account, connector, isConnectionRestoring, provider, setWalletInfo])
+ setWalletInfo({ chainId, active, account, connector })
+ }, [chainId, active, account, connector, setWalletInfo])
useEffect(() => {
const walletType = getWalletType({ gnosisSafeInfo, isSmartContractWallet: walletDetails.isSmartContractWallet })
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0dec49b5fa4..3eef0a76891 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2106,9 +2106,6 @@ importers:
'@cowprotocol/ui':
specifier: workspace:*
version: link:../ui
- '@cowprotocol/wallet-provider':
- specifier: workspace:*
- version: link:../wallet-provider
'@ethereumjs/util':
specifier: 10.1.0
version: 10.1.0