From f560c772d3819844bf1a62747e10005871f3e2b3 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Wed, 18 Dec 2024 08:38:37 +0000 Subject: [PATCH 01/15] fix: adjust cow amm banner position (#5205) --- .../src/common/pure/CoWAmmBannerContent/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx index bb0d9d3e08..0f7d574618 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx @@ -4,6 +4,8 @@ import { isTruthy } from '@cowprotocol/common-utils' import { TokensByAddress } from '@cowprotocol/tokens' import { LpTokenProvider } from '@cowprotocol/types' +import styled from 'styled-components/macro' + import { upToSmall, useMediaQuery } from 'legacy/hooks/useMediaQuery' import { VampireAttackContext } from 'modules/yield/types' @@ -17,6 +19,10 @@ import { CoWAmmBannerContext } from './types' import { useSafeMemoObject } from '../../hooks/useSafeMemo' +const Wrapper = styled.div` + z-index: 100; +` + interface CoWAmmBannerContentProps { id: string title: string @@ -139,7 +145,7 @@ export function CoWAmmBannerContent({ ) return ( -
+ {isTokenSelectorView ? ( {Content} @@ -159,6 +165,6 @@ export function CoWAmmBannerContent({ {Content} )} -
+ ) } From 411df4d83d5dfd52f6d1f57682e8e272200e11c7 Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 18 Dec 2024 13:44:11 +0000 Subject: [PATCH 02/15] feat: use new uni and coingecko token lists (#5225) * feat: use new uni and coingecko token lists * fix: linter errors --- .../orders/OrdersUserDetailsTable/index.tsx | 6 +++--- libs/tokens/src/const/tokensList.json | 17 +++++++++++------ libs/tokens/src/const/tokensLists.ts | 3 --- .../src/state/tokenLists/tokenListsStateAtom.ts | 16 +++++++--------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx b/apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx index 0efd2ec7e8..4710ce14af 100644 --- a/apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx +++ b/apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react' +import { Command } from '@cowprotocol/types' import { TruncatedText } from '@cowprotocol/ui/pure/TruncatedText' import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons' @@ -23,11 +24,10 @@ import { getLimitPrice } from 'utils/getLimitPrice' import { OrderSurplusDisplayStyledByRow } from './OrderSurplusTooltipStyledByRow' import { ToggleFilter } from './ToggleFilter' +import { TableState } from '../../../explorer/components/TokensTableWidget/useTable' import { SimpleTable, SimpleTableProps } from '../../common/SimpleTable' import { StatusLabel } from '../StatusLabel' import { UnsignedOrderWarning } from '../UnsignedOrderWarning' -import { TableState } from '../../../explorer/components/TokensTableWidget/useTable' -import { Command } from '@cowprotocol/types' const EXPIRED_CANCELED_STATES: OrderStatus[] = ['cancelled', 'cancelling', 'expired'] @@ -37,7 +37,7 @@ function isExpiredOrCanceled(order: Order): boolean { if (!executedSellAmount.isZero() || !executedBuyAmount.isZero()) return false // Otherwise, return if the order is expired or canceled - return EXPIRED_CANCELED_STATES.includes(order.status) + return EXPIRED_CANCELED_STATES.includes(status) } const tooltip = { diff --git a/libs/tokens/src/const/tokensList.json b/libs/tokens/src/const/tokensList.json index 0e84cad295..7c45ca369e 100644 --- a/libs/tokens/src/const/tokensList.json +++ b/libs/tokens/src/const/tokensList.json @@ -8,7 +8,7 @@ { "priority": 2, "enabledByDefault": true, - "source": "https://files.cow.fi/tokens/CoinGecko.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/CoinGecko.1.json" }, { "priority": 3, @@ -65,12 +65,12 @@ { "priority": 3, "enabledByDefault": true, - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/GnosisUniswapTokensList.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/Uniswap.100.json" }, { "priority": 4, "enabledByDefault": true, - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/GnosisCoingeckoTokensList.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/CoinGecko.100.json" }, { "priority": 5, @@ -86,12 +86,12 @@ { "priority": 2, "enabledByDefault": true, - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/ArbitrumOneUniswapTokensList.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/Uniswap.42161.json" }, { "priority": 3, "enabledByDefault": true, - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/ArbitrumOneCoingeckoTokensList.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/CoinGecko.42161.json" }, { "priority": 4, @@ -119,6 +119,11 @@ "priority": 2, "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/CoinGecko.8453.json", "enabledByDefault": true + }, + { + "priority": 3, + "enabledByDefault": true, + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/Uniswap.8453.json" } ] -} +} \ No newline at end of file diff --git a/libs/tokens/src/const/tokensLists.ts b/libs/tokens/src/const/tokensLists.ts index 138aab9db3..cc4c7251a9 100644 --- a/libs/tokens/src/const/tokensLists.ts +++ b/libs/tokens/src/const/tokensLists.ts @@ -10,6 +10,3 @@ export const LP_TOKEN_LISTS = lpTokensList as Array export const DEFAULT_TOKENS_LISTS: ListsSourcesByNetwork = mapSupportedNetworks((chainId) => tokensList[chainId]) export const UNISWAP_TOKENS_LIST = 'https://ipfs.io/ipns/tokens.uniswap.org' - -export const GNOSIS_UNISWAP_TOKENS_LIST = - 'https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/GnosisUniswapTokensList.json' diff --git a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts index a9ff33fa82..194a49a5c5 100644 --- a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts +++ b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts @@ -4,12 +4,7 @@ import { atomWithStorage } from 'jotai/utils' import { getJotaiMergerStorage } from '@cowprotocol/core' import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' -import { - DEFAULT_TOKENS_LISTS, - GNOSIS_UNISWAP_TOKENS_LIST, - LP_TOKEN_LISTS, - UNISWAP_TOKENS_LIST, -} from '../../const/tokensLists' +import { DEFAULT_TOKENS_LISTS, LP_TOKEN_LISTS, UNISWAP_TOKENS_LIST } from '../../const/tokensLists' import { ListSourceConfig, ListsSourcesByNetwork, @@ -21,9 +16,12 @@ import { environmentAtom } from '../environmentAtom' const UNISWAP_TOKEN_LIST_URL: Record = { [SupportedChainId.MAINNET]: UNISWAP_TOKENS_LIST, - [SupportedChainId.GNOSIS_CHAIN]: GNOSIS_UNISWAP_TOKENS_LIST, - [SupportedChainId.ARBITRUM_ONE]: UNISWAP_TOKENS_LIST, - [SupportedChainId.BASE]: UNISWAP_TOKENS_LIST, + [SupportedChainId.GNOSIS_CHAIN]: + 'https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/Uniswap.100.json', + [SupportedChainId.ARBITRUM_ONE]: + 'https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/Uniswap.42161.json', + [SupportedChainId.BASE]: + 'https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/Uniswap.8453.json', [SupportedChainId.SEPOLIA]: UNISWAP_TOKENS_LIST, } From a07740ced3e45db82f3fc4a5064fee81390b3537 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 19 Dec 2024 16:41:07 +0500 Subject: [PATCH 03/15] fix(limit-orders): do not override user entered price (#5232) --- .../updaters/InitialPriceUpdater/index.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/InitialPriceUpdater/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/updaters/InitialPriceUpdater/index.tsx index c8695504e2..10f8d20bd8 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/updaters/InitialPriceUpdater/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/InitialPriceUpdater/index.tsx @@ -1,26 +1,30 @@ -import { useSetAtom } from 'jotai' -import { useLayoutEffect, useState } from 'react' +import { useAtomValue, useSetAtom } from 'jotai' +import { useEffect, useLayoutEffect, useState } from 'react' import { usePrevious } from '@cowprotocol/common-hooks' import { Writeable } from 'types' -import { useGetInitialPrice } from 'modules/limitOrders/hooks/useGetInitialPrice' -import { useUpdateActiveRate } from 'modules/limitOrders/hooks/useUpdateActiveRate' -import { LimitRateState, updateLimitRateAtom } from 'modules/limitOrders/state/limitRateAtom' - -import { useLimitOrdersDerivedState } from '../../hooks/useLimitOrdersDerivedState' +import { useGetInitialPrice } from '../../hooks/useGetInitialPrice' +import { useLimitOrdersRawState } from '../../hooks/useLimitOrdersRawState' +import { useUpdateActiveRate } from '../../hooks/useUpdateActiveRate' +import { limitRateAtom, LimitRateState, updateLimitRateAtom } from '../../state/limitRateAtom' // Fetch and update initial price for the selected token pair export function InitialPriceUpdater() { - const { inputCurrency, outputCurrency } = useLimitOrdersDerivedState() + const { inputCurrencyId, outputCurrencyId } = useLimitOrdersRawState() + const { isTypedValue } = useAtomValue(limitRateAtom) const updateLimitRateState = useSetAtom(updateLimitRateAtom) const updateRate = useUpdateActiveRate() - const [isInitialPriceSet, setIsInitialPriceSet] = useState(false) + const [isInitialPriceSet, setIsInitialPriceSet] = useState(isTypedValue) const { price, isLoading } = useGetInitialPrice() const prevPrice = usePrevious(price) + useEffect(() => { + setIsInitialPriceSet(isTypedValue) + }, [isTypedValue]) + useLayoutEffect(() => { const update: Partial> = { initialRate: price, @@ -28,10 +32,6 @@ export function InitialPriceUpdater() { isLoading: isInitialPriceSet ? false : isLoading, } - if (!isInitialPriceSet) { - update.isTypedValue = false - } - updateLimitRateState(update) }, [isInitialPriceSet, price, isLoading, updateLimitRateState]) @@ -40,6 +40,7 @@ export function InitialPriceUpdater() { if (!price || isInitialPriceSet || isLoading || prevPrice?.equalTo(price)) return setIsInitialPriceSet(true) + updateRate({ activeRate: price, isInitialPriceSet: true, @@ -53,7 +54,7 @@ export function InitialPriceUpdater() { // Reset initial price set flag when any token was changed useLayoutEffect(() => { setIsInitialPriceSet(false) - }, [inputCurrency, outputCurrency]) + }, [inputCurrencyId, outputCurrencyId]) return null } From fc13cc460747bb59aba374067899d8d1c4ba986f Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 19 Dec 2024 18:14:29 +0500 Subject: [PATCH 04/15] fix: allow any safe-like apps (#5235) --- libs/wallet/src/web3-react/connection/safe.tsx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/libs/wallet/src/web3-react/connection/safe.tsx b/libs/wallet/src/web3-react/connection/safe.tsx index 98fd713a94..c200bb82d6 100644 --- a/libs/wallet/src/web3-react/connection/safe.tsx +++ b/libs/wallet/src/web3-react/connection/safe.tsx @@ -7,19 +7,7 @@ import { Web3ReactConnection } from '../types' const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector( (actions) => - new AsyncConnector( - () => - import('@web3-react/gnosis-safe').then( - (m) => - new m.GnosisSafe({ - actions, - options: { - allowedDomains: [/app\.safe\.global$/, /(.+\.)?coinshift\.global$/, /localhost:5173$/], - }, - }), - ), - actions, - ), + new AsyncConnector(() => import('@web3-react/gnosis-safe').then((m) => new m.GnosisSafe({ actions })), actions), ) export const gnosisSafeConnection: Web3ReactConnection = { connector: web3GnosisSafe, From 1626a04e2c0b530c2efae842776ba5af6014a1da Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 19 Dec 2024 14:39:02 +0000 Subject: [PATCH 05/15] fix(feature-flags): remove isBaseEnabled feature flag (#5234) --- libs/common-hooks/src/useAvailableChains.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/libs/common-hooks/src/useAvailableChains.ts b/libs/common-hooks/src/useAvailableChains.ts index 4e7c610179..82a9f8353e 100644 --- a/libs/common-hooks/src/useAvailableChains.ts +++ b/libs/common-hooks/src/useAvailableChains.ts @@ -3,8 +3,6 @@ import { useMemo } from 'react' import { getAvailableChains } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { useFeatureFlags } from './useFeatureFlags' - /** * Hook to get a list of SupportedChainId currently available/enabled * @@ -14,12 +12,12 @@ import { useFeatureFlags } from './useFeatureFlags' */ export function useAvailableChains(): SupportedChainId[] { // 1. Load feature flag for chain being enabled - const { isBaseEnabled } = useFeatureFlags() + // const { isBaseEnabled } = useFeatureFlags() return useMemo( // 2. Conditionally build a list of chain ids to exclude - // () => getAvailableChains(isArbitrumOneEnabled ? undefined : [SupportedChainId.ARBITRUM_ONE]), - () => getAvailableChains(isBaseEnabled ? undefined : [SupportedChainId.BASE]), - [isBaseEnabled], + // () => getAvailableChains(isBaseEnabled ? undefined : [SupportedChainId.BASE]), <-- example usage, kept for reference + () => getAvailableChains(), + [], ) } From 28fcda95c7002a528c538917450bc38b67c0a5eb Mon Sep 17 00:00:00 2001 From: Edouard Bougon <15703023+EdouardBougon@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:27:05 +0100 Subject: [PATCH 06/15] feat(wallets): reapply "feat(wallets): add metaMask SDK connector (#5028)" (#5215) (#5223) * Reapply "feat(wallets): add metaMask SDK connector (#5028)" (#5215) This reverts commit 889b78ed8b9e2092abbe503ed69c797e482b2edc. * chore: update MetaMask SDK to 0.31.4 --------- Co-authored-by: Leandro --- .../containers/ConnectWalletOptions.tsx | 37 +-- .../api/state/multiInjectedProvidersAtom.ts | 2 +- libs/wallet/src/api/types.ts | 1 + libs/wallet/src/api/utils/connection.ts | 3 + libs/wallet/src/index.ts | 3 +- .../web3-react/connection/injectedOptions.tsx | 30 -- .../src/web3-react/connection/metaMaskSdk.tsx | 54 +++ .../connectors/metaMaskSdk/index.ts | 307 ++++++++++++++++++ .../utils/getWeb3ReactConnection.ts | 2 + .../src/web3-react/utils/isChainAllowed.ts | 1 + package.json | 3 +- yarn.lock | 303 ++++++++++++++++- 12 files changed, 681 insertions(+), 65 deletions(-) create mode 100644 libs/wallet/src/web3-react/connection/metaMaskSdk.tsx create mode 100644 libs/wallet/src/web3-react/connectors/metaMaskSdk/index.ts diff --git a/apps/cowswap-frontend/src/modules/wallet/containers/ConnectWalletOptions.tsx b/apps/cowswap-frontend/src/modules/wallet/containers/ConnectWalletOptions.tsx index 1bf6d0bb2d..9885ea144e 100644 --- a/apps/cowswap-frontend/src/modules/wallet/containers/ConnectWalletOptions.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/containers/ConnectWalletOptions.tsx @@ -3,8 +3,7 @@ import { isMobile, isInjectedWidget } from '@cowprotocol/common-utils' import { CoinbaseWalletOption, InjectedOption as DefaultInjectedOption, - InstallMetaMaskOption, - OpenMetaMaskMobileOption, + MetaMaskSdkOption, TrezorOption, WalletConnectV2Option, getIsInjected, @@ -31,14 +30,17 @@ export function ConnectWalletOptions({ tryActivation }: { tryActivation: TryActi const connectionProps = { darkMode, selectedWallet, tryActivation } + const metaMaskSdkOption = const coinbaseWalletOption = (!hasCoinbaseEip6963 && ) ?? null const walletConnectionV2Option = ((!isInjectedMobileBrowser || isWidget) && ) ?? null const trezorOption = (!isInjectedMobileBrowser && !isMobile && ) ?? null + const injectedOption = (getIsInjected() && ) ?? null return ( <> - + {injectedOption} + {metaMaskSdkOption} {walletConnectionV2Option} {coinbaseWalletOption} {trezorOption} @@ -57,19 +59,13 @@ interface InjectedOptionsProps { } function InjectedOptions({ connectionProps, multiInjectedProviders }: InjectedOptionsProps) { - const isInjected = getIsInjected() - - if (!isInjected) { - if (!isMobile) { - return - } else { - return - } - } else { - if (multiInjectedProviders.length) { - return ( - <> - {multiInjectedProviders.map((providerInfo) => { + if (multiInjectedProviders.length) { + return ( + <> + {multiInjectedProviders + // Even if we detect the MetaMask Extension, we prefer to use the MetaMask SDK + .filter((providerInfo) => !providerInfo.info.rdns.startsWith('io.metamask')) + .map((providerInfo) => { return ( ) })} - - ) - } - - return + + ) } + + return } diff --git a/libs/wallet/src/api/state/multiInjectedProvidersAtom.ts b/libs/wallet/src/api/state/multiInjectedProvidersAtom.ts index ac5e1abd2f..c5a1a9b9f3 100644 --- a/libs/wallet/src/api/state/multiInjectedProvidersAtom.ts +++ b/libs/wallet/src/api/state/multiInjectedProvidersAtom.ts @@ -26,7 +26,7 @@ window.addEventListener('eip6963:announceProvider', (event: Event) => { jotaiStore.set(multiInjectedProvidersAtom, (prev: EIP6963ProviderDetail[]) => { const newProvider = providerEvent.detail - const existingProvider = prev.find((p) => p.info.rdns === newProvider.info.uuid) + const existingProvider = prev.find((p) => p.info.rdns === newProvider.info.rdns) if (existingProvider) return prev diff --git a/libs/wallet/src/api/types.ts b/libs/wallet/src/api/types.ts index f177ca6b11..c43a7721e9 100644 --- a/libs/wallet/src/api/types.ts +++ b/libs/wallet/src/api/types.ts @@ -8,6 +8,7 @@ export enum ConnectionType { INJECTED = 'INJECTED', WALLET_CONNECT_V2 = 'WALLET_CONNECT_V2', COINBASE_WALLET = 'COINBASE_WALLET', + METAMASK = 'METAMASK', GNOSIS_SAFE = 'GNOSIS_SAFE', TREZOR = 'TREZOR', } diff --git a/libs/wallet/src/api/utils/connection.ts b/libs/wallet/src/api/utils/connection.ts index c936123d7a..318a20404e 100644 --- a/libs/wallet/src/api/utils/connection.ts +++ b/libs/wallet/src/api/utils/connection.ts @@ -1,5 +1,6 @@ import { isMobile } from '@cowprotocol/common-utils' +import { default as MetamaskImage } from '../../api/assets/metamask.png' import CoinbaseWalletIcon from '../assets/coinbase.svg' import TrezorIcon from '../assets/trezor.svg' import WalletConnectIcon from '../assets/walletConnectIcon.svg' @@ -7,6 +8,7 @@ import { ConnectionType } from '../types' const connectionTypeToName: Record = { [ConnectionType.INJECTED]: 'Injected', + [ConnectionType.METAMASK]: 'MetaMask', [ConnectionType.COINBASE_WALLET]: 'Coinbase Wallet', [ConnectionType.WALLET_CONNECT_V2]: 'WalletConnect', [ConnectionType.NETWORK]: 'Network', @@ -18,6 +20,7 @@ const IDENTICON_KEY = 'Identicon' const connectionTypeToIcon: Record = { [ConnectionType.INJECTED]: IDENTICON_KEY, + [ConnectionType.METAMASK]: MetamaskImage, [ConnectionType.GNOSIS_SAFE]: IDENTICON_KEY, [ConnectionType.NETWORK]: IDENTICON_KEY, [ConnectionType.COINBASE_WALLET]: CoinbaseWalletIcon, diff --git a/libs/wallet/src/index.ts b/libs/wallet/src/index.ts index 68a2559d75..a781aa93ad 100644 --- a/libs/wallet/src/index.ts +++ b/libs/wallet/src/index.ts @@ -42,8 +42,6 @@ export { walletConnectConnectionV2 } from './web3-react/connection/walletConnect // Connect options export { InjectedOption, - InstallMetaMaskOption, - OpenMetaMaskMobileOption, Eip6963Option, } from './web3-react/connection/injectedOptions' @@ -51,6 +49,7 @@ export { ConnectWalletOption } from './api/pure/ConnectWalletOption' export { TrezorOption } from './web3-react/connection/trezor' export { WalletConnectV2Option } from './web3-react/connection/walletConnectV2' export { CoinbaseWalletOption } from './web3-react/connection/coinbase' +export { MetaMaskSdkOption } from './web3-react/connection/metaMaskSdk' // State // TODO: this export is discussable, however it's already used outside diff --git a/libs/wallet/src/web3-react/connection/injectedOptions.tsx b/libs/wallet/src/web3-react/connection/injectedOptions.tsx index 9b4e8cc090..e4d8c16451 100644 --- a/libs/wallet/src/web3-react/connection/injectedOptions.tsx +++ b/libs/wallet/src/web3-react/connection/injectedOptions.tsx @@ -3,7 +3,6 @@ import { useCallback } from 'react' import { injectedWalletConnection } from './injectedWallet' import { default as InjectedImage, default as InjectedImageDark } from '../../api/assets/arrow-right.svg' -import { default as MetamaskImage } from '../../api/assets/metamask.png' import { useSelectedEip6963ProviderRdns, useSetEip6963Provider } from '../../api/hooks' import { ConnectWalletOption } from '../../api/pure/ConnectWalletOption' import { ConnectionType, type EIP1193Provider, EIP6963ProviderDetail } from '../../api/types' @@ -11,14 +10,6 @@ import { getConnectionName } from '../../api/utils/connection' import { useIsActiveConnection } from '../hooks/useIsActiveConnection' import { ConnectionOptionProps, TryActivation } from '../types' -const METAMASK_DEEP_LINK = 'https://metamask.app.link/dapp/' - -const metamaskCommonOption = { - color: '#E8831D', - icon: MetamaskImage, - id: 'metamask', -} - const injectedCommon = { color: '#010101', id: 'injected', @@ -33,27 +24,6 @@ export const injectedOptionDark = { icon: InjectedImageDark, } -export const metamaskInstallOption = { - ...metamaskCommonOption, - header: 'Install MetaMask', - link: 'https://metamask.io/', -} - -export const metamaskInjectedOption = { - ...metamaskCommonOption, - header: 'MetaMask', -} - -export function InstallMetaMaskOption() { - return -} - -export function OpenMetaMaskMobileOption() { - return ( - - ) -} - export function InjectedOption({ darkMode, tryActivation, selectedWallet }: ConnectionOptionProps) { const options = darkMode ? injectedOptionDark : injectedOption diff --git a/libs/wallet/src/web3-react/connection/metaMaskSdk.tsx b/libs/wallet/src/web3-react/connection/metaMaskSdk.tsx new file mode 100644 index 0000000000..01f63209c2 --- /dev/null +++ b/libs/wallet/src/web3-react/connection/metaMaskSdk.tsx @@ -0,0 +1,54 @@ +import { RPC_URLS } from '@cowprotocol/common-const' +import { initializeConnector } from '@web3-react/core' + +import { onError } from './onError' + +import { default as MetamaskImage } from '../../api/assets/metamask.png' +import { ConnectWalletOption } from '../../api/pure/ConnectWalletOption' +import { ConnectionType } from '../../api/types' +import { getConnectionName } from '../../api/utils/connection' +import { MetaMaskSDK } from '../connectors/metaMaskSdk' +import { useIsActiveConnection } from '../hooks/useIsActiveConnection' +import { ConnectionOptionProps, Web3ReactConnection } from '../types' + +const metaMaskOption = { + color: '#E8831D', + icon: MetamaskImage, + id: 'metamask', +} + +const [web3MetaMask, web3MetaMaskHooks] = initializeConnector( + (actions) => + new MetaMaskSDK({ + actions, + options: { + dappMetadata: { + name: 'CoW Swap', + url: 'https://swap.cow.fi', + }, + readonlyRPCMap: Object.fromEntries( + Object.entries(RPC_URLS).map(([chainId, url]) => [`0x${Number(chainId).toString(16)}`, url]), + ), + }, + onError, + }), +) + +export const metaMaskSdkConnection: Web3ReactConnection = { + connector: web3MetaMask, + hooks: web3MetaMaskHooks, + type: ConnectionType.METAMASK, +} + +export function MetaMaskSdkOption({ tryActivation, selectedWallet }: ConnectionOptionProps) { + const isActive = useIsActiveConnection(selectedWallet, metaMaskSdkConnection) + + return ( + tryActivation(metaMaskSdkConnection.connector)} + header={getConnectionName(ConnectionType.METAMASK)} + /> + ) +} diff --git a/libs/wallet/src/web3-react/connectors/metaMaskSdk/index.ts b/libs/wallet/src/web3-react/connectors/metaMaskSdk/index.ts new file mode 100644 index 0000000000..69c4f15698 --- /dev/null +++ b/libs/wallet/src/web3-react/connectors/metaMaskSdk/index.ts @@ -0,0 +1,307 @@ +import type { + Actions, + AddEthereumChainParameter, + Provider, + ProviderConnectInfo, + ProviderRpcError, + WatchAssetParameters, +} from '@web3-react/types' +import { Connector } from '@web3-react/types' + +import type { MetaMaskSDK as _MetaMaskSDK, MetaMaskSDKOptions as _MetaMaskSDKOptions, SDKProvider } from '@metamask/sdk' + +/** + * MetaMaskSDK options. + */ +type MetaMaskSDKOptions = Pick<_MetaMaskSDKOptions, 'infuraAPIKey' | 'readonlyRPCMap'> & { + dappMetadata: Pick<_MetaMaskSDKOptions['dappMetadata'], 'name' | 'url' | 'iconUrl'> +} + +/** + * Listener type for MetaMaskSDK events. + */ +type Listener = Parameters[1] + +/** + * Error thrown when the MetaMaskSDK is not installed. + */ +export class NoMetaMaskSDKError extends Error { + public constructor() { + super('MetaMaskSDK not installed') + this.name = NoMetaMaskSDKError.name + Object.setPrototypeOf(this, NoMetaMaskSDKError.prototype) + } +} + +/** + * Parses a chainId from a string or number. + */ +function parseChainId(chainId: string | number) { + return typeof chainId === 'number' ? chainId : Number.parseInt(chainId, chainId.startsWith('0x') ? 16 : 10) +} + +/** + * @param options - Options to pass to `@metamask/sdk` + * @param onError - Handler to report errors thrown from eventListeners. + */ +export interface MetaMaskSDKConstructorArgs { + actions: Actions + options?: MetaMaskSDKOptions + onError?: (error: Error) => void +} + +/** + * Connector for the MetaMaskSDK. + */ +export class MetaMaskSDK extends Connector { + private sdk?: _MetaMaskSDK + provider?: SDKProvider = undefined + private readonly options: MetaMaskSDKOptions + private eagerConnection?: Promise + + /** + * @inheritdoc Connector.constructor + */ + constructor({ actions, options, onError }: MetaMaskSDKConstructorArgs) { + super(actions, onError) + + const defaultUrl = typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.host}` : '' + + this.options = { + ...options, + dappMetadata: options?.dappMetadata ?? { + url: defaultUrl, + name: defaultUrl !== '' ? undefined : 'wagmi', + }, + } + } + + /** + * Indicates whether the user is connected to the MetaMaskSDK. + */ + private async isConnected() { + try { + if (this.provider?.isConnected?.() === true) { + if (this.sdk?.isExtensionActive() === true) { + const accounts = ((await this.provider?.request({ method: 'eth_accounts' })) ?? []) as string[] + return accounts.length > 0 + } + + return true + } + } catch { + // ignore + } + + return false + } + + /** + * @inheritdoc Connector.isomorphicInitialize + */ + private async isomorphicInitialize(): Promise { + if (this.eagerConnection) return + + return (this.eagerConnection = import('@metamask/sdk').then(async (m) => { + if (!this.sdk) { + this.sdk = new m.default({ + _source: 'web3React', + useDeeplink: true, + injectProvider: false, + forceInjectProvider: false, + forceDeleteProvider: false, + ...this.options, + }) + await this.sdk.init() + } + + this.provider = this.sdk.getProvider()! + + this.provider.on('connect', (({ chainId }: ProviderConnectInfo): void => { + this.actions.update({ chainId: parseChainId(chainId) }) + }) as Listener) + + this.provider.on('disconnect', (async (error: ProviderRpcError): Promise => { + const originalError = ((error.data as any)?.originalError ?? error) as ProviderRpcError + + // If MetaMask emits a `code: 1013` error, wait for reconnection before disconnecting + // https://github.com/MetaMask/providers/pull/120 + if (error && originalError.code === 1013 && this.provider) { + const accounts = (await this.provider.request({ method: 'eth_accounts' })) as string[] + if (accounts.length > 0) return + } + + this.clearCache() + + this.actions.resetState() + this.onError?.(error) + }) as Listener) + + this.provider.on('chainChanged', ((chainId: string): void => { + this.actions.update({ chainId: parseChainId(chainId) }) + }) as Listener) + + this.provider.on('accountsChanged', ((accounts: string[]): void => { + // Disconnect if there are no accounts + if (accounts.length === 0) { + // ... and using browser extension + if (this.sdk?.isExtensionActive()) { + this.clearCache() + this.actions.resetState() + } + // FIXME(upstream): Mobile app sometimes emits invalid `accountsChanged` event with empty accounts array + else return + } else { + this.actions.update({ accounts }) + } + }) as Listener) + })) + } + + /** + * @inheritdoc Connector.connectEagerly + */ + public async connectEagerly(): Promise { + const cancelActivation = this.actions.startActivation() + + try { + await this.isomorphicInitialize() + if (!this.provider) return cancelActivation() + + // Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing + // chains; they should be requested serially, with accounts first, so that the chainId can settle. + const accounts = (await this.provider.request({ method: 'eth_accounts' })) as string[] + if (!accounts.length) throw new Error('No accounts returned') + const chainId = (await this.provider.request({ method: 'eth_chainId' })) as string + this.actions.update({ chainId: parseChainId(chainId), accounts }) + } catch { + // we should be able to use `cancelActivation` here, but on mobile, metamask emits a 'connect' + // event, meaning that chainId is updated, and cancelActivation doesn't work because an intermediary + // update has occurred, so we reset state instead + this.actions.resetState() + } + } + + /** + * Initiates a connection. + * + * @param desiredChainIdOrChainParameters - If defined, indicates the desired chain to connect to. If the user is + * already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch + * to the chain, if one of two conditions is met: either they already have it added in their extension, or the + * argument is of type AddEthereumChainParameter, in which case the user will be prompted to add the chain with the + * specified parameters first, before being prompted to switch. + */ + public async activate(desiredChainIdOrChainParameters?: number | AddEthereumChainParameter): Promise { + const [desiredChainId, desiredChain] = + typeof desiredChainIdOrChainParameters === 'number' + ? [desiredChainIdOrChainParameters, undefined] + : [desiredChainIdOrChainParameters?.chainId, desiredChainIdOrChainParameters] + + // If user already connected, only switch chain + if (this.provider && (await this.isConnected())) { + await this.switchChain(desiredChainId, desiredChain) + return + } + + // If user not connected, connect eagerly + // Then switch chain + const cancelActivation = this.actions.startActivation() + return this.isomorphicInitialize() + .then(async () => { + if (!this.provider || !this.sdk) throw new NoMetaMaskSDKError() + + const accounts = await this.sdk.connect() + const currentChainIdHex = (await this.provider.request({ method: 'eth_chainId' })) as string + const currentChainId = parseChainId(currentChainIdHex) + + await this.actions.update({ chainId: currentChainId, accounts }) + }) + .catch((error) => { + cancelActivation?.() + throw error + }) + } + + /** + * @inheritdoc Connector.deactivate + */ + public deactivate(): void { + this.sdk?.terminate() + } + + /** + * Watches an asset in the MetaMask wallet. + */ + public async watchAsset({ address, symbol, decimals, image }: WatchAssetParameters): Promise { + if (!this.provider) throw new NoMetaMaskSDKError() + + return this.provider + .request({ + method: 'wallet_watchAsset', + params: { + type: 'ERC20', // Initially only supports ERC20, but eventually more! + options: { + address, // The address that the token is at. + symbol, // A ticker symbol or shorthand, up to 5 chars. + decimals, // The number of decimals in the token + image, // A string url of the token logo + }, + }, + }) + .then((success) => { + if (!success) throw new Error('Rejected') + return true + }) + } + + /** + * Switches the chain of the MetaMask wallet. + * + * Only switches the chain if the desired chain is different from the current chain. + * Else returns the current chain id. + */ + private async switchChain(desiredChainId?: number, desiredChain?: AddEthereumChainParameter): Promise { + if (!this.provider) throw new NoMetaMaskSDKError() + + const currentChainIdHex = (await this.provider.request({ method: 'eth_chainId' })) as string + const currentChainId = parseChainId(currentChainIdHex) + + if (!desiredChainId || currentChainId === desiredChainId) return currentChainId + + const chainIdHex = `0x${desiredChainId.toString(16)}` + this.provider + .request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: chainIdHex }], + }) + .catch(async (error: ProviderRpcError) => { + const originalError = ((error.data as any)?.originalError ?? error) as ProviderRpcError + + if (originalError.code === 4902 && desiredChain !== undefined) { + if (!this.provider) throw new NoMetaMaskSDKError() + // if we're here, we can try to add a new network + return this.provider.request({ + method: 'wallet_addEthereumChain', + params: [{ ...desiredChain, chainId: chainIdHex }], + }) + } + + throw error + }) + + const newChainIdHex = (await this.provider.request({ method: 'eth_chainId' })) as string + const newChainId = parseChainId(newChainIdHex) + + return newChainId + } + + /** + * Clears the cache. + */ + private clearCache() { + localStorage.removeItem('.MMSDK_cached_address') + localStorage.removeItem('.MMSDK_cached_chainId') + localStorage.removeItem('.sdk-comm') + localStorage.removeItem('.MetaMaskSDKLng') + } +} diff --git a/libs/wallet/src/web3-react/utils/getWeb3ReactConnection.ts b/libs/wallet/src/web3-react/utils/getWeb3ReactConnection.ts index f929969914..2b8f7fb69a 100644 --- a/libs/wallet/src/web3-react/utils/getWeb3ReactConnection.ts +++ b/libs/wallet/src/web3-react/utils/getWeb3ReactConnection.ts @@ -3,6 +3,7 @@ import { Connector } from '@web3-react/types' import { ConnectionType } from '../../api/types' import { coinbaseWalletConnection } from '../connection/coinbase' import { injectedWalletConnection } from '../connection/injectedWallet' +import { metaMaskSdkConnection } from '../connection/metaMaskSdk' import { networkConnection } from '../connection/network' import { gnosisSafeConnection } from '../connection/safe' import { trezorConnection } from '../connection/trezor' @@ -11,6 +12,7 @@ import { Web3ReactConnection } from '../types' const connectionTypeToConnection: Record = { [ConnectionType.INJECTED]: injectedWalletConnection, + [ConnectionType.METAMASK]: metaMaskSdkConnection, [ConnectionType.COINBASE_WALLET]: coinbaseWalletConnection, [ConnectionType.WALLET_CONNECT_V2]: walletConnectConnectionV2, [ConnectionType.NETWORK]: networkConnection, diff --git a/libs/wallet/src/web3-react/utils/isChainAllowed.ts b/libs/wallet/src/web3-react/utils/isChainAllowed.ts index a53710fff4..86c19de13c 100644 --- a/libs/wallet/src/web3-react/utils/isChainAllowed.ts +++ b/libs/wallet/src/web3-react/utils/isChainAllowed.ts @@ -7,6 +7,7 @@ import { ConnectionType } from '../../api/types' const allowedChainsByWallet: Record = { [ConnectionType.INJECTED]: ALL_SUPPORTED_CHAIN_IDS, + [ConnectionType.METAMASK]: ALL_SUPPORTED_CHAIN_IDS, [ConnectionType.COINBASE_WALLET]: ALL_SUPPORTED_CHAIN_IDS, [ConnectionType.WALLET_CONNECT_V2]: ALL_SUPPORTED_CHAIN_IDS, [ConnectionType.NETWORK]: ALL_SUPPORTED_CHAIN_IDS, diff --git a/package.json b/package.json index a6cd57946e..6d26d07d41 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "@material-ui/core": "^4.11.0", "@metamask/eth-sig-util": "^5.0.2", "@metamask/jazzicon": "^2.0.0", + "@metamask/sdk": "^0.31.4", "@mui/icons-material": "^5.14.13", "@mui/lab": "^5.0.0-alpha.148", "@mui/material": "^5.14.13", @@ -342,4 +343,4 @@ "vite-tsconfig-paths": "~4.3.2", "vitest": "~0.32.0" } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ca3cc58390..cd5acc9218 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2063,6 +2063,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -2615,6 +2622,11 @@ mersenne-twister "^1.1.0" react-blockies "^1.4.1" +"@ecies/ciphers@^0.2.1": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@ecies/ciphers/-/ciphers-0.2.2.tgz#82a15b10a6e502b63fb30915d944b2eaf3ff17ff" + integrity sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg== + "@emnapi/runtime@^1.2.0": version "1.3.1" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60" @@ -4886,6 +4898,58 @@ "@metamask/safe-event-emitter" "^3.0.0" "@metamask/utils" "^8.3.0" +"@metamask/json-rpc-engine@^8.0.1", "@metamask/json-rpc-engine@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-8.0.2.tgz#29510a871a8edef892f838ee854db18de0bf0d14" + integrity sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA== + dependencies: + "@metamask/rpc-errors" "^6.2.1" + "@metamask/safe-event-emitter" "^3.0.0" + "@metamask/utils" "^8.3.0" + +"@metamask/json-rpc-middleware-stream@^7.0.1": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-7.0.2.tgz#2e8b2cbc38968e3c6239a9144c35bbb08a8fb57d" + integrity sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg== + dependencies: + "@metamask/json-rpc-engine" "^8.0.2" + "@metamask/safe-event-emitter" "^3.0.0" + "@metamask/utils" "^8.3.0" + readable-stream "^3.6.2" + +"@metamask/object-multiplex@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@metamask/object-multiplex/-/object-multiplex-2.1.0.tgz#5e2e908fc46aee581cbba809870eeee0e571cbb6" + integrity sha512-4vKIiv0DQxljcXwfpnbsXcfa5glMj5Zg9mqn4xpIWqkv6uJ2ma5/GtUfLFSxhlxnR8asRMv8dDmWya1Tc1sDFA== + dependencies: + once "^1.4.0" + readable-stream "^3.6.2" + +"@metamask/onboarding@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@metamask/onboarding/-/onboarding-1.0.1.tgz#14a36e1e175e2f69f09598e2008ab6dc1b3297e6" + integrity sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ== + dependencies: + bowser "^2.9.0" + +"@metamask/providers@16.1.0": + version "16.1.0" + resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-16.1.0.tgz#7da593d17c541580fa3beab8d9d8a9b9ce19ea07" + integrity sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g== + dependencies: + "@metamask/json-rpc-engine" "^8.0.1" + "@metamask/json-rpc-middleware-stream" "^7.0.1" + "@metamask/object-multiplex" "^2.0.0" + "@metamask/rpc-errors" "^6.2.1" + "@metamask/safe-event-emitter" "^3.1.1" + "@metamask/utils" "^8.3.0" + detect-browser "^5.2.0" + extension-port-stream "^3.0.0" + fast-deep-equal "^3.1.3" + is-stream "^2.0.0" + readable-stream "^3.6.2" + webextension-polyfill "^0.10.0" + "@metamask/rpc-errors@^6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-6.2.1.tgz#f5daf429ededa7cb83069dc621bd5738fe2a1d80" @@ -4904,6 +4968,54 @@ resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.1.tgz#e89b840a7af8097a8ed4953d8dc8470d1302d3ef" integrity sha512-ihb3B0T/wJm1eUuArYP4lCTSEoZsClHhuWyfo/kMX3m/odpqNcPfsz5O2A3NT7dXCAgWPGDQGPqygCpgeniKMw== +"@metamask/safe-event-emitter@^3.1.1": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.2.tgz#bfac8c7a1a149b5bbfe98f59fbfea512dfa3bad4" + integrity sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA== + +"@metamask/sdk-communication-layer@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.31.0.tgz#0acc063b62aa09d044c7aab65801712d760e53b2" + integrity sha512-V9CxdzabDPjQVgmKGHsyU3SYt4Af27g+4DbGCx0fLoHqN/i1RBDZqs/LYbJX3ykJCANzE+llz/MolMCMrzM2RA== + dependencies: + bufferutil "^4.0.8" + date-fns "^2.29.3" + debug "^4.3.4" + utf-8-validate "^5.0.2" + uuid "^8.3.2" + +"@metamask/sdk-install-modal-web@0.31.2": + version "0.31.2" + resolved "https://registry.yarnpkg.com/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.31.2.tgz#bb8c92a6844a632be8525e7bb5a35924a926d6cd" + integrity sha512-KPv36kQjmTwErU8g2neuHHSgkD5+1hp4D6ERfk5Kc2r73aOYNCdG9wDGRUmFmcY2MKkeK1EuDyZfJ4FPU30fxQ== + dependencies: + "@paulmillr/qr" "^0.2.1" + +"@metamask/sdk@^0.31.4": + version "0.31.4" + resolved "https://registry.yarnpkg.com/@metamask/sdk/-/sdk-0.31.4.tgz#2f9266e994ba838652925dc83e3409adfcae75ae" + integrity sha512-HLUN4IZGdyiy5YeebXmXi+ndpmrl6zslCQLdR2QHplIy4JmUL/eDyKNFiK7eBLVKXVVIDYFIb6g1iSEb+i8Kew== + dependencies: + "@babel/runtime" "^7.26.0" + "@metamask/onboarding" "^1.0.1" + "@metamask/providers" "16.1.0" + "@metamask/sdk-communication-layer" "0.31.0" + "@metamask/sdk-install-modal-web" "0.31.2" + "@paulmillr/qr" "^0.2.1" + bowser "^2.9.0" + cross-fetch "^4.0.0" + debug "^4.3.4" + eciesjs "^0.4.11" + eth-rpc-errors "^4.0.3" + eventemitter2 "^6.4.9" + obj-multiplex "^1.0.0" + pump "^3.0.0" + readable-stream "^3.6.2" + socket.io-client "^4.5.1" + tslib "^2.6.0" + util "^0.12.4" + uuid "^8.3.2" + "@metamask/utils@^3.0.1": version "3.6.0" resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-3.6.0.tgz#b218b969a05ca7a8093b5d1670f6625061de707d" @@ -5252,6 +5364,11 @@ dependencies: eslint-scope "5.1.1" +"@noble/ciphers@^1.0.0": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.1.3.tgz#eb27085aa7ce94d8c6eaeb64299bab0589920ec1" + integrity sha512-Ygv6WnWJHLLiW4fnNDC1z+i13bud+enXOFRBlpxI+NJliPWx5wdR+oWlTjLuBPTqjUjtHXtjkU6w3kuuH6upZA== + "@noble/curves@1.1.0", "@noble/curves@^1.0.0", "@noble/curves@~1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" @@ -5266,6 +5383,13 @@ dependencies: "@noble/hashes" "1.3.2" +"@noble/curves@^1.6.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" + integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== + dependencies: + "@noble/hashes" "1.6.0" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -5281,11 +5405,21 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" + integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== + "@noble/hashes@^1.3.1": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@^1.5.0": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" + integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== + "@noble/hashes@~1.3.2": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" @@ -5802,6 +5936,11 @@ "@parcel/watcher-win32-ia32" "2.3.0" "@parcel/watcher-win32-x64" "2.3.0" +"@paulmillr/qr@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@paulmillr/qr/-/qr-0.2.1.tgz#76ade7080be4ac4824f638146fd8b6db1805eeca" + integrity sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ== + "@phenomnomnominal/tsquery@~5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz#a2a5abc89f92c01562a32806655817516653a388" @@ -6685,6 +6824,11 @@ chalk "^2.3.0" shell-quote "^1.6.1" +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + "@solana/buffer-layout@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" @@ -11909,6 +12053,11 @@ borsh@^0.7.0: bs58 "^4.0.0" text-encoding-utf-8 "^1.0.2" +bowser@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + boxen@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" @@ -12151,6 +12300,13 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.3.0" +bufferutil@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" + integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== + dependencies: + node-gyp-build "^4.3.0" + builtin-modules@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -14308,7 +14464,7 @@ debug@^4.3.1, debug@^4.3.2: dependencies: ms "2.1.2" -debug@^4.3.6: +debug@^4.3.6, debug@~4.3.1, debug@~4.3.2: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -14545,7 +14701,7 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -detect-browser@5.3.0, detect-browser@^5.1.0, detect-browser@^5.3.0: +detect-browser@5.3.0, detect-browser@^5.1.0, detect-browser@^5.2.0, detect-browser@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.3.0.tgz#9705ef2bddf46072d0f7265a1fe300e36fe7ceca" integrity sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w== @@ -14910,6 +15066,16 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +eciesjs@^0.4.11: + version "0.4.12" + resolved "https://registry.yarnpkg.com/eciesjs/-/eciesjs-0.4.12.tgz#0ce482454953592e07b79b4824751f3b5c508b56" + integrity sha512-DGejvMCihsRAmKRFQiL6KZDE34vWVd0gvXlykFq1aEzJy/rD65AVyAIUZKZOvgvaP9ATQRcHGEZV5DfgrgjA4w== + dependencies: + "@ecies/ciphers" "^0.2.1" + "@noble/ciphers" "^1.0.0" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -15013,13 +15179,29 @@ encoding@^0.1.11, encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.1.0, end-of-stream@^1.4.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +engine.io-client@~6.6.1: + version "6.6.2" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.2.tgz#e0a09e1c90effe5d6264da1c56d7281998f1e50b" + integrity sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.1.1" + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + enhanced-resolve@^5.0.0, enhanced-resolve@^5.12.0, enhanced-resolve@^5.16.0, enhanced-resolve@^5.7.0: version "5.16.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" @@ -16269,7 +16451,7 @@ eth-rpc-errors@4.0.2: dependencies: fast-safe-stringify "^2.0.6" -eth-rpc-errors@^4.0.2: +eth-rpc-errors@^4.0.2, eth-rpc-errors@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg== @@ -16418,6 +16600,11 @@ eventemitter2@6.4.7: resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== +eventemitter2@^6.4.9: + version "6.4.9" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" + integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== + eventemitter3@4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" @@ -16621,6 +16808,14 @@ extend@^3.0.0, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +extension-port-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extension-port-stream/-/extension-port-stream-3.0.0.tgz#00a7185fe2322708a36ed24843c81bd754925fef" + integrity sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw== + dependencies: + readable-stream "^3.6.2 || ^4.4.2" + webextension-polyfill ">=0.10.0 <1.0" + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -23729,6 +23924,15 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== +obj-multiplex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/obj-multiplex/-/obj-multiplex-1.0.0.tgz#2f2ae6bfd4ae11befe742ea9ea5b36636eabffc1" + integrity sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA== + dependencies: + end-of-stream "^1.4.0" + once "^1.4.0" + readable-stream "^2.3.3" + object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -26484,7 +26688,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -26493,7 +26697,7 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.1, readable-stream@~2.3.6: +readable-stream@^2.0.1, readable-stream@^2.3.3, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -26506,6 +26710,17 @@ readable-stream@^2.0.1, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" +"readable-stream@^3.6.2 || ^4.4.2": + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-web-to-node-stream@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" @@ -27805,6 +28020,24 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" +socket.io-client@^4.5.1: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.1.tgz#1941eca135a5490b94281d0323fe2a35f6f291cb" + integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.6.1" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" @@ -28170,7 +28403,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -28187,6 +28420,15 @@ string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -28300,7 +28542,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.0.0, string_decoder@^1.1.1: +string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -28331,7 +28573,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -28345,6 +28587,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -29360,6 +29609,11 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== +tslib@^2.6.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" @@ -30931,6 +31185,16 @@ web3modal@1.9.0: styled-components "^5.1.1" tslib "^1.10.0" +"webextension-polyfill@>=0.10.0 <1.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz#f62c57d2cd42524e9fbdcee494c034cae34a3d69" + integrity sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q== + +webextension-polyfill@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" + integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -31709,7 +31973,7 @@ workbox-window@7.0.0, workbox-window@^7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -31736,6 +32000,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -31797,6 +32070,11 @@ ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" @@ -31847,6 +32125,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23" + integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ== + xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From c3529aa7e825c5747c8d00e54f6c0ed30950f932 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 19 Dec 2024 20:16:39 +0000 Subject: [PATCH 07/15] chore: merge main to dev (#5238) * fix: allow any safe-like apps * chore: release main (#5237) * feat: fix pagination for fills (#5228) * feat: fix pagination for fills * fix: remove unnecessary tag --------- Co-authored-by: Alexandr Kazachenko --- .release-please-manifest.json | 2 +- .../components/orders/OrderDetails/FillsTableWithData.tsx | 2 +- .../orders/OrderDetails/context/FillsTableContext.tsx | 2 +- apps/explorer/src/components/orders/OrderDetails/index.tsx | 4 ++-- libs/wallet/CHANGELOG.md | 7 +++++++ libs/wallet/package.json | 2 +- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 62d355c459..b2519049c8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -17,7 +17,7 @@ "libs/tokens": "1.13.1", "libs/types": "1.5.0", "libs/ui": "1.17.0", - "libs/wallet": "1.8.0", + "libs/wallet": "1.8.1", "apps/cow-fi": "1.19.3", "libs/wallet-provider": "1.0.0", "libs/ui-utils": "1.1.0", diff --git a/apps/explorer/src/components/orders/OrderDetails/FillsTableWithData.tsx b/apps/explorer/src/components/orders/OrderDetails/FillsTableWithData.tsx index 6fdf28b00a..920a3a43b8 100644 --- a/apps/explorer/src/components/orders/OrderDetails/FillsTableWithData.tsx +++ b/apps/explorer/src/components/orders/OrderDetails/FillsTableWithData.tsx @@ -18,7 +18,7 @@ type Props = { } export const FillsTableWithData: React.FC = ({ areTokensLoaded, order, isPriceInverted, invertPrice }) => { - const { trades, tableState } = useContext(FillsTableContext) + const { data: trades, tableState } = useContext(FillsTableContext) const isFirstRender = useFirstRender() return isFirstRender || !areTokensLoaded ? ( diff --git a/apps/explorer/src/components/orders/OrderDetails/context/FillsTableContext.tsx b/apps/explorer/src/components/orders/OrderDetails/context/FillsTableContext.tsx index 35dd94b813..f7ad3e361a 100644 --- a/apps/explorer/src/components/orders/OrderDetails/context/FillsTableContext.tsx +++ b/apps/explorer/src/components/orders/OrderDetails/context/FillsTableContext.tsx @@ -5,7 +5,7 @@ import { Trade } from 'api/operator' import { TableState, TableStateSetters } from '../../../../explorer/components/TokensTableWidget/useTable' type CommonState = { - trades: Trade[] + data: Trade[] isLoading: boolean tableState: TableState } & TableStateSetters diff --git a/apps/explorer/src/components/orders/OrderDetails/index.tsx b/apps/explorer/src/components/orders/OrderDetails/index.tsx index ac8e47d609..debf07668d 100644 --- a/apps/explorer/src/components/orders/OrderDetails/index.tsx +++ b/apps/explorer/src/components/orders/OrderDetails/index.tsx @@ -72,7 +72,7 @@ export type Props = { export enum TabView { OVERVIEW = 1, - FILLS, + FILLS = 2, } const DEFAULT_TAB = TabView[1] @@ -225,7 +225,7 @@ export const OrderDetails: React.FC = (props) => { ))} Date: Fri, 20 Dec 2024 17:32:55 +0000 Subject: [PATCH 08/15] feat(token-lists): remove outdated token lists (#5233) * feat: remove outdated token lists * feat: add curve's list to base * feat: add superchain list to base --- .../src/app/configurator/consts.ts | 18 +------- libs/tokens/src/const/tokensList.json | 46 ++++--------------- 2 files changed, 10 insertions(+), 54 deletions(-) diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts index 4d128e0e4c..4d91c0f1ab 100644 --- a/apps/widget-configurator/src/app/configurator/consts.ts +++ b/apps/widget-configurator/src/app/configurator/consts.ts @@ -22,28 +22,12 @@ export const TRADE_MODES = [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED, // Sourced from https://tokenlists.org/ export const DEFAULT_TOKEN_LISTS: TokenListItem[] = [ { url: 'https://files.cow.fi/tokens/CowSwap.json', enabled: true }, - { url: 'https://files.cow.fi/tokens/CoinGecko.json', enabled: true }, - { url: 'https://tokens.1inch.eth.link', enabled: false }, - { url: 'https://tokenlist.aave.eth.link', enabled: false }, - { url: 'https://datafi.theagora.eth.link', enabled: false }, - { url: 'https://defi.cmc.eth.link', enabled: false }, - { url: 'https://stablecoin.cmc.eth.link', enabled: false }, - { url: 'https://erc20.cmc.eth.link', enabled: false }, - { - url: 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json', - enabled: false, - }, - { url: 'https://tokenlist.dharma.eth.link', enabled: false }, + { url: 'https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/CoinGecko.1.json', enabled: true }, { url: 'https://www.gemini.com/uniswap/manifest.json', enabled: false }, { url: 'https://messari.io/tokenlist/messari-verified', enabled: false }, - { url: 'https://uniswap.mycryptoapi.com', enabled: false }, { url: 'https://static.optimism.io/optimism.tokenlist.json', enabled: false }, { url: 'https://app.tryroll.com/tokens.json', enabled: false }, - { url: 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json', enabled: false }, - { url: 'https://synths.snx.eth.link', enabled: false }, - { url: 'https://testnet.tokenlist.eth.link', enabled: false }, { url: 'https://ipfs.io/ipns/tokens.uniswap.org', enabled: false }, - { url: 'https://wrapped.tokensoft.eth.link', enabled: false }, ] // TODO: Move default palette to a new lib that only exposes the palette colors. // This way it can be consumed by both the configurator and the widget. diff --git a/libs/tokens/src/const/tokensList.json b/libs/tokens/src/const/tokensList.json index 7c45ca369e..c384288811 100644 --- a/libs/tokens/src/const/tokensList.json +++ b/libs/tokens/src/const/tokensList.json @@ -12,42 +12,6 @@ }, { "priority": 3, - "source": "https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json" - }, - { - "priority": 4, - "source": "tokenlist.aave.eth" - }, - { - "priority": 5, - "source": "synths.snx.eth" - }, - { - "priority": 6, - "source": "wrapped.tokensoft.eth" - }, - { - "priority": 7, - "source": "https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json" - }, - { - "priority": 8, - "source": "https://raw.githubusercontent.com/opynfinance/opyn-tokenlist/master/opyn-squeeth-tokenlist.json" - }, - { - "priority": 9, - "source": "https://app.tryroll.com/tokens.json" - }, - { - "priority": 10, - "source": "defi.cmc.eth" - }, - { - "priority": 11, - "source": "stablecoin.cmc.eth" - }, - { - "priority": 12, "source": "https://curvefi.github.io/curve-assets/ethereum.json" } ], @@ -124,6 +88,14 @@ "priority": 3, "enabledByDefault": true, "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/Uniswap.8453.json" + }, + { + "priority": 4, + "source": "https://curvefi.github.io/curve-assets/base.json" + }, + { + "priority": 5, + "source": "https://static.optimism.io/optimism.tokenlist.json" } ] -} \ No newline at end of file +} From 22b8f89453371b8406e53effa82b7e3de745f19a Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 25 Dec 2024 16:24:17 +0500 Subject: [PATCH 09/15] chore: fix failed e2e test (#5257) --- apps/cowswap-frontend-e2e/src/e2e/swap.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cowswap-frontend-e2e/src/e2e/swap.test.ts b/apps/cowswap-frontend-e2e/src/e2e/swap.test.ts index f799a523d2..b938f5f713 100644 --- a/apps/cowswap-frontend-e2e/src/e2e/swap.test.ts +++ b/apps/cowswap-frontend-e2e/src/e2e/swap.test.ts @@ -68,7 +68,7 @@ describe('Swap (custom)', () => { }) it('should accept buyAmount url param', () => { - cy.visit(`/#/${CHAIN_ID}/swap/${SELL_TOKEN}/${BUY_TOKEN}?buyAmount=0.5`) + cy.visit(`/#/${CHAIN_ID}/swap/${SELL_TOKEN}/${BUY_TOKEN}?buyAmount=0.5&orderKind=buy`) cy.get('#output-currency-input .token-amount-input').should('have.value', '0.5') }) From caeda2c6d313f112d2f8203581a3470857cb3d61 Mon Sep 17 00:00:00 2001 From: Leandro Date: Tue, 31 Dec 2024 11:38:05 +0000 Subject: [PATCH 10/15] chore: address pr5244 comments (#5263) * fix: use network instead of orderParams chainId * refactor: update comment --- .../ethFlow/steps/signEthFlowOrderStep.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/services/ethFlow/steps/signEthFlowOrderStep.ts b/apps/cowswap-frontend/src/modules/swap/services/ethFlow/steps/signEthFlowOrderStep.ts index 1c688a320d..3fda6d658c 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/ethFlow/steps/signEthFlowOrderStep.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/ethFlow/steps/signEthFlowOrderStep.ts @@ -67,19 +67,25 @@ export async function signEthFlowOrderStep( return GAS_LIMIT_DEFAULT }) - // This used to be done with a higher level of abstraction like this: + // Ensure the Eth flow contract network matches the network where you place the transaction. + // There are multiple wallet implementations, and potential race conditions that can cause the chain of the wallet to be different, + // and therefore leaving the chainId implicit might lead the user to place an order in an unwanted chain. + // This is especially dangerous for Eth Flow orders, because the contract address is different for the distinct networks, + // and this can lead to loss of funds. + // + // Thus, we are not using a higher level of abstraction as it doesn't allow to explicitly set the chainId: // const txReceipt = await ethFlowContract.createOrder(ethOrderParams, { // ...ethTxOptions, // gasLimit: calculateGasMargin(estimatedGas), // }) - // However, to **try** to prevent wallet issues, we want to explicitly send along the chainId - // But that wrapper doesn't accept it. - // So we must build the tx first, then send it using the contract's signer + // + // So we must build the tx first: const tx = await ethFlowContract.populateTransaction.createOrder(ethOrderParams, { ...ethTxOptions, gasLimit: calculateGasMargin(estimatedGas), }) - const txReceipt = await ethFlowContract.signer.sendTransaction({ ...tx, chainId: orderParams.chainId }) + // Then send the is using the contract's signer where the chainId is an acceptable parameter + const txReceipt = await ethFlowContract.signer.sendTransaction({ ...tx, chainId: network.chainId }) addInFlightOrderId(orderId) From a8efd5c641fc456f9d7369a06097bc29278d06b1 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 31 Dec 2024 18:19:50 +0500 Subject: [PATCH 11/15] ci: deploy some apps preview only when PR has label (#5258) --- README.md | 15 ++++++ tools/scripts/ignore-build-step.js | 74 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 tools/scripts/ignore-build-step.js diff --git a/README.md b/README.md index 43094caf91..f205db5385 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,21 @@ In case of problems with the service worker cache you force a reset using `emergency.js` is not cached by browser and loaded before all. +## Vercel preview build + +Since this repo includes multiple apps, we do not want to build all of them on each PR because it causes long build queues in Vercel. +Some apps (see the list bellow) are not required to be built on each PR so we run them only a PR is labeled with a specific label. +This label is defined in the project settings on Vercel in `Settings`/`Git`/`Ignored Build Step` script. +For example, the label for the widget-configurator is `preview-widget-cfg`: +``` +node tools/scripts/ignore-build-step.js --app=preview-widget-cfg +``` + +List of applications and their labels: +- widget-configurator: `preview-widget-cfg` +- cosmos: `preview-cosmos` +- sdk-tools: `preview-sdk-tools` + # 📚 Technical Documentation 1. [Oveall Architecture](docs/architecture-overview.md) diff --git a/tools/scripts/ignore-build-step.js b/tools/scripts/ignore-build-step.js new file mode 100644 index 0000000000..5977304705 --- /dev/null +++ b/tools/scripts/ignore-build-step.js @@ -0,0 +1,74 @@ +const owner = process.env.VERCEL_GIT_REPO_OWNER +const repo = process.env.VERCEL_GIT_REPO_SLUG +const pullRequestId = process.env.VERCEL_GIT_PULL_REQUEST_ID +const commitRef = process.env.VERCEL_GIT_COMMIT_REF + +const APP_ARGV = '--app=' +const appName = (() => { + const argv = process.argv.find((arg) => arg.startsWith(APP_ARGV)) + return argv ? argv.slice(APP_ARGV.length) : undefined +})() + +const PREVIEW_IGNORE_BRANCHES = ['main', 'configuration', 'release-please--branches--main'] + +/** + * Skip the build if: + * - The branch is in the list of branches to ignore + * - The app preview is configured to be deployed manually, and label is not present in the PR + */ +async function shouldSkipBuild() { + if (PREVIEW_IGNORE_BRANCHES.includes(commitRef)) { + console.log(`Skipping build for branch ${commitRef}.`) + return true + } + + if (!pullRequestId) { + console.log('No PR ID found. Proceeding with build.') + return false + } + + if (!appName) { + console.log(`No appName label: ${appName}, found. Proceeding with build.`) + return false + } + + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${pullRequestId}/labels` + + try { + const response = await fetch(url) + + if (!response.ok) { + console.error('Failed to fetch PR labels:', response.statusText) + return false // Proceed with the build in case of an error + } + + const labels = await response.json() + console.log( + 'PR Labels:', + labels.map((label) => label.name), + ) + const hasAppLabel = labels.some((label) => label.name === appName) + + if (hasAppLabel) { + console.log(`Found label: ${appName}. Proceeding with build.`) + } else { + console.log(`Label ${appName} not found. Skipping build.`) + } + + // Skip the build if the PR doesn't have the app label + return !hasAppLabel + } catch (error) { + console.error('Error fetching PR labels:', error) + return false // Proceed with the build in case of an error + } +} + +shouldSkipBuild().then((skip) => { + if (skip) { + console.log('Skipping build.') + process.exit(0) + } else { + console.log('Proceeding with build.') + process.exit(1) + } +}) From 3be8a65309048d4082f2ce25f5b39624e092dbf2 Mon Sep 17 00:00:00 2001 From: Leandro Date: Mon, 6 Jan 2025 14:49:13 +0000 Subject: [PATCH 12/15] feat: executedSurplusFee removal (#5262) * chore: bump cow-sdk to latest RC version * feat: replace executedSurplusFee with executedSuplus Also use totalFee where applicable * feat: do same as previous, but on Explorer * refactor: moved getFeeToken out and improved logic as suggested * test: add unit tests --- .../src/legacy/state/orders/actions.ts | 11 ++++-- .../src/legacy/state/orders/reducer.ts | 17 +++++---- .../pure/OrdersTableContainer/orders.mock.ts | 13 +++++++ .../pure/ReceiptModal/FeeField.tsx | 15 ++++---- .../ordersTable/utils/getFeeToken.test.ts | 37 +++++++++++++++++++ .../modules/ordersTable/utils/getFeeToken.ts | 14 +++++++ .../src/utils/orderUtils/parseOrder.ts | 12 ++++-- apps/explorer/src/api/operator/types.ts | 14 +++++-- apps/explorer/src/test/data/operator.ts | 4 +- apps/explorer/src/utils/operator.ts | 4 +- package.json | 4 +- yarn.lock | 20 ++++++++-- 12 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/ordersTable/utils/getFeeToken.test.ts create mode 100644 apps/cowswap-frontend/src/modules/ordersTable/utils/getFeeToken.ts diff --git a/apps/cowswap-frontend/src/legacy/state/orders/actions.ts b/apps/cowswap-frontend/src/legacy/state/orders/actions.ts index 4630be0d51..89bd865995 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/actions.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/actions.ts @@ -107,7 +107,9 @@ export type OrderInfoApi = Pick< | 'executedSellAmount' | 'executedSellAmountBeforeFees' | 'executedFeeAmount' - | 'executedSurplusFee' + | 'executedFee' + | 'executedFeeToken' + | 'totalFee' | 'invalidated' | 'ethflowData' | 'onchainOrderData' @@ -139,6 +141,7 @@ export interface AddPendingOrderParams { order: SerializedOrder isSafeWallet: boolean } + export type ChangeOrderStatusParams = { id: UID; chainId: ChainId } export type SetOrderCancellationHashParams = ChangeOrderStatusParams & { hash: string } @@ -177,11 +180,13 @@ export interface BatchOrdersUpdateParams { } export type PresignedOrdersParams = BatchOrdersUpdateParams + export interface UpdatePresignGnosisSafeTxParams { orderId: UID chainId: ChainId safeTransaction: SafeMultisigTransactionResponse } + export type ExpireOrdersBatchParams = BatchOrdersUpdateParams export type InvalidateOrdersBatchParams = BatchOrdersUpdateParams export type CancelOrdersBatchParams = BatchOrdersUpdateParams @@ -196,7 +201,7 @@ export const fulfillOrdersBatch = createAction('order/ export const preSignOrders = createAction('order/presignOrders') export const updatePresignGnosisSafeTx = createAction( - 'order/updatePresignGnosisSafeTx' + 'order/updatePresignGnosisSafeTx', ) export const expireOrdersBatch = createAction('order/expireOrdersBatch') @@ -214,7 +219,7 @@ export const deleteOrders = createAction('order/deleteOrders export const clearOrders = createAction<{ chainId: ChainId }>('order/clearOrders') export const updateLastCheckedBlock = createAction<{ chainId: ChainId; lastCheckedBlock: number }>( - 'order/updateLastCheckedBlock' + 'order/updateLastCheckedBlock', ) export const clearOrdersStorage = createAction('order/clearOrdersStorage') diff --git a/apps/cowswap-frontend/src/legacy/state/orders/reducer.ts b/apps/cowswap-frontend/src/legacy/state/orders/reducer.ts index 4e32f45fd1..f3068c7554 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/reducer.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/reducer.ts @@ -121,7 +121,7 @@ export function getDefaultNetworkState(chainId: ChainId): OrdersStateNetwork { // makes sure there's always an object at state[chainId], state[chainId].pending | .fulfilled function prefillState( state: Writable, - { payload: { chainId } }: PayloadAction + { payload: { chainId } }: PayloadAction, ): asserts state is Required { const stateAtChainId = state[chainId] @@ -174,7 +174,7 @@ function addOrderToState( id: string, status: OrderTypeKeys, order: SerializedOrder, - isSafeWallet: boolean + isSafeWallet: boolean, ): void { // Attempt to fix `TypeError: Cannot add property , object is not extensible` // seen on https://user-images.githubusercontent.com/34510341/138450105-bb94a2d1-656e-4e15-ae99-df9fb33c8ca4.png @@ -200,7 +200,7 @@ function cancelOrderInState( state: Required, chainId: ChainId, orderObject: OrderObject, - isSafeWallet: boolean + isSafeWallet: boolean, ) { const id = orderObject.id @@ -368,12 +368,13 @@ export default createReducer(initialState, (builder) => orderObject.order.apiAdditionalInfo = { creationDate: order.creationDate, - availableBalance: order.availableBalance, executedBuyAmount: order.executedBuyAmount, executedSellAmount: order.executedSellAmount, executedSellAmountBeforeFees: order.executedSellAmountBeforeFees, executedFeeAmount: order.executedFeeAmount, - executedSurplusFee: order.executedSurplusFee, + executedFee: order.executedFee, + executedFeeToken: order.executedFeeToken, + totalFee: order.totalFee, invalidated: order.invalidated, ethflowData: order.ethflowData, onchainOrderData: order.onchainOrderData, @@ -458,7 +459,7 @@ export default createReducer(initialState, (builder) => const allOrdersMap = flatOrdersStateNetwork(state[chainId]) const children = Object.values(allOrdersMap).filter( - (item) => item?.order.composableCowInfo?.parentId === id + (item) => item?.order.composableCowInfo?.parentId === id, ) children.forEach((child) => { @@ -544,12 +545,12 @@ export default createReducer(initialState, (builder) => orderListByChain[status] = ordersCleaned }) }) - }) + }), ) function reClassifyOrder( newOrder: SerializedOrder, - existingOrder: OrderObject | undefined + existingOrder: OrderObject | undefined, ): { status: OrderStatus; isCancelling: boolean | undefined } { // Onchain cancellations are considered final // Still, the order classification at apps/cowswap-frontend/src/legacy/state/orders/utils.ts can't tell diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/orders.mock.ts b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/orders.mock.ts index 46c3abdb6d..b6d8259577 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/orders.mock.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/orders.mock.ts @@ -100,6 +100,19 @@ export const ordersMock: ParsedOrder[] = [ signingScheme: SigningScheme.EIP712, class: OrderClass.MARKET, kind: OrderKind.SELL, + apiAdditionalInfo: { + executedFeeAmount: '1', + executedFee: '1', + executedFeeToken: USDC[chainId].address, + totalFee: '1', + creationDate: '2022-11-11T13:15:13.551Z', + executedBuyAmount: '23000000000000', + executedSellAmount: '5000300000000000', + executedSellAmountBeforeFees: '5000300000000000', + invalidated: false, + class: OrderClass.LIMIT, + signingScheme: SigningScheme.EIP712, + }, }, { id: '5', diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FeeField.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FeeField.tsx index eed9c96389..4d348db3ff 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FeeField.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FeeField.tsx @@ -1,6 +1,8 @@ import { TokenAmount } from '@cowprotocol/ui' import { CurrencyAmount } from '@uniswap/sdk-core' +import { getFeeToken } from 'modules/ordersTable/utils/getFeeToken' + import { ParsedOrder } from 'utils/orderUtils/parseOrder' import * as styledEl from './styled' @@ -8,14 +10,13 @@ import * as styledEl from './styled' export type Props = { order: ParsedOrder } export function FeeField({ order }: Props): JSX.Element | null { - const { inputToken } = order - const { executedFeeAmount, executedSurplusFee } = order.executionData + const { totalFee } = order.executionData + const feeToken = getFeeToken(order) - if (!inputToken) return + if (!feeToken) return - // TODO: use the value from SDK - const totalFee = CurrencyAmount.fromRawAmount(inputToken, (executedSurplusFee ?? executedFeeAmount) || 0) - const quoteSymbol = inputToken.symbol + const totalFeeAmount = CurrencyAmount.fromRawAmount(feeToken, totalFee || 0) + const quoteSymbol = feeToken.symbol return ( @@ -23,7 +24,7 @@ export function FeeField({ order }: Props): JSX.Element | null { - ) : ( - + )} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/utils/getFeeToken.test.ts b/apps/cowswap-frontend/src/modules/ordersTable/utils/getFeeToken.test.ts new file mode 100644 index 0000000000..0eba3524c7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/utils/getFeeToken.test.ts @@ -0,0 +1,37 @@ +import { getFeeToken } from './getFeeToken' // Adjust the import path as necessary + +import { ordersMock } from '../pure/OrdersTableContainer/orders.mock' + +const BASE_ORDER = ordersMock[3] + +describe('getFeeToken', () => { + it("should return inputToken when that's the fee token", () => { + const input = BASE_ORDER + const expectedOutput = BASE_ORDER.inputToken + + const result = getFeeToken(input) + + expect(result).toEqual(expectedOutput) + }) + + it("should return outputToken when that's the fee token", () => { + const input = { + ...BASE_ORDER, + executionData: { ...BASE_ORDER.executionData, executedFeeToken: BASE_ORDER.outputToken.address }, + } + const expectedOutput = BASE_ORDER.outputToken + + const result = getFeeToken(input) + + expect(result).toEqual(expectedOutput) + }) + + it('should return inputToken when there is no fee token', () => { + const input = { ...BASE_ORDER, executionData: { ...BASE_ORDER.executionData, executedFeeToken: null } } + const expectedOutput = BASE_ORDER.inputToken + + const result = getFeeToken(input) + + expect(result).toEqual(expectedOutput) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/utils/getFeeToken.ts b/apps/cowswap-frontend/src/modules/ordersTable/utils/getFeeToken.ts new file mode 100644 index 0000000000..a41a094bd4 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/utils/getFeeToken.ts @@ -0,0 +1,14 @@ +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +export function getFeeToken(order: ParsedOrder) { + const { inputToken, outputToken } = order + const { executedFeeToken } = order.executionData + + const feeTokenAddress = executedFeeToken?.toLowerCase() + + if (!feeTokenAddress) { + return inputToken + } + + return [inputToken, outputToken].find((token) => token?.address.toLowerCase() === feeTokenAddress) +} diff --git a/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts b/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts index 8031e391f3..f6a3d0b4e2 100644 --- a/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts +++ b/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts @@ -24,7 +24,9 @@ export interface ParsedOrderExecutionData { surplusAmount: BigNumber surplusPercentage: BigNumber executedFeeAmount: string | undefined - executedSurplusFee: string | null + executedFee: string | null + executedFeeToken: string | null + totalFee: string | null filledPercentDisplay: string executedPrice: Price | null activityId: string | undefined @@ -60,7 +62,9 @@ export const parseOrder = (order: Order): ParsedOrder => { const { executedBuyAmount, executedSellAmount } = getOrderExecutedAmounts(order) const expirationTime = new Date(Number(order.validTo) * 1000) const executedFeeAmount = order.apiAdditionalInfo?.executedFeeAmount - const executedSurplusFee = order.apiAdditionalInfo?.executedSurplusFee || null + const executedFee = order.apiAdditionalInfo?.executedFee || null + const executedFeeToken = order.apiAdditionalInfo?.executedFeeToken || null + const totalFee = order.apiAdditionalInfo?.totalFee || null const creationTime = new Date(order.creationTime) const fullyFilled = isOrderFilled(order) const partiallyFilled = isPartiallyFilled(order) @@ -80,6 +84,7 @@ export const parseOrder = (order: Order): ParsedOrder => { const activityTitle = showCreationTxLink ? 'Creation transaction' : 'Order ID' const executionData: ParsedOrderExecutionData = { + executedFeeToken, executedBuyAmount, executedSellAmount, filledAmount, @@ -88,7 +93,8 @@ export const parseOrder = (order: Order): ParsedOrder => { surplusAmount, surplusPercentage, executedFeeAmount, - executedSurplusFee, + executedFee, + totalFee, executedPrice, fullyFilled, partiallyFilled, diff --git a/apps/explorer/src/api/operator/types.ts b/apps/explorer/src/api/operator/types.ts index 8ef8bf3dba..54240f124e 100644 --- a/apps/explorer/src/api/operator/types.ts +++ b/apps/explorer/src/api/operator/types.ts @@ -18,7 +18,15 @@ export type RawOrder = EnrichedOrder */ export type Order = Pick< RawOrder, - 'owner' | 'uid' | 'appData' | 'kind' | 'partiallyFillable' | 'signature' | 'class' | 'fullAppData' + | 'owner' + | 'uid' + | 'appData' + | 'kind' + | 'partiallyFillable' + | 'signature' + | 'class' + | 'fullAppData' + | 'executedFeeToken' > & { receiver: string txHash?: string @@ -35,7 +43,7 @@ export type Order = Pick< executedSellAmount: BigNumber feeAmount: BigNumber executedFeeAmount: BigNumber - executedSurplusFee: BigNumber | null + executedFee: BigNumber | null totalFee: BigNumber cancelled: boolean status: OrderStatus @@ -60,7 +68,7 @@ export type Trade = Pick Date: Wed, 8 Jan 2025 17:27:52 +0000 Subject: [PATCH 13/15] chore: comment out seasonal feature flags (#5271) --- .../modules/application/containers/App/index.tsx | 8 +++++--- .../src/modules/sounds/utils/sound.ts | 13 ++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx index b4fbd70538..11ffafbdaf 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -1,8 +1,7 @@ import { lazy, PropsWithChildren, Suspense, useMemo } from 'react' import { ACTIVE_CUSTOM_THEME, CustomTheme } from '@cowprotocol/common-const' -import { useMediaQuery } from '@cowprotocol/common-hooks' -import { useFeatureFlags } from '@cowprotocol/common-hooks' +import { useFeatureFlags, useMediaQuery } from '@cowprotocol/common-hooks' import { isInjectedWidget } from '@cowprotocol/common-utils' import { Color, Footer, GlobalCoWDAOStyles, Media, MenuBar } from '@cowprotocol/ui' @@ -55,7 +54,10 @@ export function App() { useAnalyticsReporterCowSwap() useInitializeUtm() - const { isYieldEnabled, isChristmasEnabled, isHalloweenEnabled } = useFeatureFlags() + const { isYieldEnabled, } = useFeatureFlags() + // TODO: load them from feature flags when we want to enable again + const isChristmasEnabled = false + const isHalloweenEnabled = false const isInjectedWidgetMode = isInjectedWidget() const menuItems = useMenuItems() diff --git a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts index ca127702dc..f4557a299f 100644 --- a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts +++ b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts @@ -7,8 +7,6 @@ import { cowSwapStore } from 'legacy/state' import { injectedWidgetParamsAtom } from 'modules/injectedWidget/state/injectedWidgetParamsAtom' -import { featureFlagsAtom } from 'common/state/featureFlagsState' - type SoundType = 'SEND' | 'SUCCESS' | 'ERROR' type Sounds = Record type WidgetSounds = keyof NonNullable @@ -47,7 +45,12 @@ function isDarkMode(): boolean { } function getThemeBasedSound(type: SoundType): string { - const featureFlags = jotaiStore.get(featureFlagsAtom) as Record + // TODO: load featureFlags when enabling again + // const featureFlags = jotaiStore.get(featureFlagsAtom) as Record + // const { isChristmasEnabled, isHalloweenEnabled } = featureFlags + const isChristmasEnabled = false + const isHalloweenEnabled = false + const defaultSound = DEFAULT_COW_SOUNDS[type] const themedOptions = THEMED_SOUNDS[type] @@ -62,13 +65,13 @@ function getThemeBasedSound(type: SoundType): string { return defaultSound } - if (ACTIVE_CUSTOM_THEME === CustomTheme.CHRISTMAS && featureFlags.isChristmasEnabled && themedOptions.winterSound) { + if (ACTIVE_CUSTOM_THEME === CustomTheme.CHRISTMAS && isChristmasEnabled && themedOptions.winterSound) { return themedOptions.winterSound } if ( ACTIVE_CUSTOM_THEME === CustomTheme.HALLOWEEN && - featureFlags.isHalloweenEnabled && + isHalloweenEnabled && themedOptions.halloweenSound && isDarkMode() ) { From f080ffdb098612e729f3a3f829410ce78697979f Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 8 Jan 2025 22:52:28 +0500 Subject: [PATCH 14/15] feat(swap): partial approve (#5256) * feat(swap): add settings option for partial approve * feat(swap): add sell amount to regular approve tx * feat(swap): add sell amount to permit value * chore: fix build * fix: cache permit taking amount into account * feat(swap): take partial approves into account for sc wallets * fix: ignore account agnostic permit in hooks details * fix: take permit amount into account when caching * fix: skip partial permits in widgets besides swap * chore: fix permit hook description * chore: fix conditions * fix: disable partial approve for Hooks store * fix: support partial approve it classic eth flow * fix: do not use infinite approvals in swap when partial approve mode * chore: fix circular dependency * chore: add a dot * chore: fix tooltips * chore: adjust approve tooltip * fix: display hook details only in Hooks store confirm modal * feat(partial-approvals): partial approve v2 (#5269) * refactor: expose UndelinedLinkStyledButton component * chore: removed unused React import * refactor: export useOpenSettingsTab hook * feat: expose needsApproval flag for all token types, not just permittable * feat: add PartialApprovalBanner * feat: update settings name Partial Approve to Minimal approve and tooltip * fix: fix typo and padding on settings link * fix: reworked minimal approvals title and tooltip * fix: lint issues * fix: cosmos build * fix: fix grammar * fix: do not show approval banner when selling native * chore: move partial approval banner after TWAP warning * feat: simplify text * feat: remove isApprovalNeeded prop * feat: remove banner from swap warnings * feat: add banner to top of Swap widget * chore: remove settings from cosmos --------- Co-authored-by: Leandro --- .../containers/OrderHooksDetails/index.tsx | 41 +++++++++- .../TradeApprove/TradeApproveButton.tsx | 5 +- .../TradeApprove/useTradeApproveCallback.ts | 9 ++- .../src/common/hooks/useApproveCallback.ts | 15 ++-- .../src/common/pure/ApproveButton/index.tsx | 1 - .../common/pure/OrderProgressBarV2/index.tsx | 15 +++- .../common/pure/OrderProgressBarV2/styled.ts | 12 +-- .../src/common/utils/parsePermitData.ts | 15 ++++ .../src/legacy/state/user/hooks.tsx | 19 +++++ .../src/legacy/state/user/reducer.ts | 7 +- .../src/lib/hooks/useApproval.ts | 74 +------------------ .../operations/bundle/buildApproveTx.ts | 5 +- .../permit/hooks/useGeneratePermitHook.ts | 10 ++- .../permit/hooks/useGetCachedPermit.ts | 5 +- .../modules/permit/state/permitCacheAtom.ts | 31 ++++++-- .../src/modules/permit/types.ts | 4 +- .../src/modules/permit/utils/handlePermit.ts | 3 +- .../ConfirmSwapModalSetup/index.tsx | 8 +- .../modules/swap/containers/EthFlow/index.tsx | 8 +- .../swap/containers/SwapWidget/index.tsx | 10 ++- .../swap/hooks/useHandleSwapOrEthFlow.ts | 8 +- .../swap/hooks/useSwapButtonContext.ts | 4 + .../modules/swap/hooks/useSwapFlowContext.ts | 6 +- .../EthFlow/EthFlowModalContent/configs.ts | 4 +- .../swap/pure/SwapButtons/index.cosmos.tsx | 1 + .../modules/swap/pure/SwapButtons/index.tsx | 3 +- .../pure/banners/PartialApprovalBanner.tsx | 31 ++++++++ .../trade/pure/TradeConfirmation/index.tsx | 18 ++++- .../tradeFlow/hooks/useTradeFlowContext.ts | 8 +- .../safeBundleFlow/safeBundleApprovalFlow.ts | 11 ++- .../safeBundleFlow/safeBundleEthFlow.ts | 10 ++- .../tradeFlow/services/swapFlow/index.ts | 1 + .../tradeFlow/types/TradeFlowContext.ts | 1 + .../containers/SettingsTab/index.tsx | 41 +++++++++- .../pure/Row/RowSlippageContent/index.tsx | 7 +- .../state/settingsTabState.ts | 8 +- .../cowswap-frontend/src/pages/Swap/index.tsx | 5 +- .../utils/orderUtils/getOrderPermitAmount.ts | 7 +- libs/hook-dapp-lib/src/hookDappsRegistry.json | 2 +- .../src/lib/generatePermitHook.ts | 14 +++- libs/permit-utils/src/types.ts | 1 + libs/ui/src/containers/InlineBanner/index.tsx | 2 +- libs/ui/src/pure/LinkStyledButton/index.tsx | 9 +++ 43 files changed, 350 insertions(+), 149 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/utils/parsePermitData.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/pure/banners/PartialApprovalBanner.tsx diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index 3c8a623983..67e607b666 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -3,6 +3,8 @@ import { ReactElement, useEffect, useMemo, useState } from 'react' import { latest } from '@cowprotocol/app-data' import { CowHookDetails, HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' import { InfoTooltip } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { ChevronDown, ChevronUp } from 'react-feather' @@ -14,15 +16,27 @@ import { HookItem } from './HookItem' import * as styledEl from './styled' import { CircleCount } from './styled' +import { parsePermitData } from '../../utils/parsePermitData' + interface OrderHooksDetailsProps { appData: string | AppDataInfo children: (content: ReactElement) => ReactElement margin?: string isTradeConfirmation?: boolean + slippageAdjustedSellAmount?: CurrencyAmount + isPartialApprove?: boolean } -export function OrderHooksDetails({ appData, children, margin, isTradeConfirmation }: OrderHooksDetailsProps) { +export function OrderHooksDetails({ + appData, + children, + margin, + isTradeConfirmation, + slippageAdjustedSellAmount, + isPartialApprove, +}: OrderHooksDetailsProps) { const [isOpen, setOpen] = useState(false) + const { account } = useWalletInfo() const appDataDoc = useMemo(() => { return typeof appData === 'string' ? decodeAppData(appData) : appData.doc }, [appData]) @@ -41,9 +55,32 @@ export function OrderHooksDetails({ appData, children, margin, isTradeConfirmati const metadata = appDataDoc.metadata as latest.Metadata + /** + * AppData might include a hook with account agnostic permit which is used to fetch a quote. + * This hook should be ignored. + * Moreover, any hook with a permit which has owner !== current account will be excluded. + * We also remove the permit from appData before order signing (see filterPermitSignerPermit). + */ + const preHooks = account + ? metadata.hooks?.pre?.filter((hook) => { + try { + const permitHookData = parsePermitData(hook.callData) + const isOwnerMatched = permitHookData.owner.toLowerCase() === account.toLowerCase() + + // If the hook is a partial approve, we need to check if the value is equal to the slippageAdjustedSellAmount + // Because there might be a hook with an "infinite" permit from other widget + return isPartialApprove && slippageAdjustedSellAmount + ? isOwnerMatched && permitHookData.value.eq(slippageAdjustedSellAmount.quotient.toString()) + : isOwnerMatched + } catch { + return true + } + }) + : metadata.hooks?.pre + const hasSomeFailedSimulation = isTradeConfirmation && Object.values(data || {}).some((hook) => !hook.status) - const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || [], preCustomHookDapps) + const preHooksToDapp = matchHooksToDappsRegistry(preHooks || [], preCustomHookDapps) const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || [], postCustomHookDapps) if (!preHooksToDapp.length && !postHooksToDapp.length) return null diff --git a/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx b/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx index 68e68ef2b3..3e31105e1d 100644 --- a/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx +++ b/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx @@ -14,15 +14,16 @@ export interface TradeApproveButtonProps { amountToApprove: CurrencyAmount children?: React.ReactNode isDisabled?: boolean + isPartialApprove?: boolean } export function TradeApproveButton(props: TradeApproveButtonProps) { - const { amountToApprove, children, isDisabled } = props + const { amountToApprove, children, isDisabled, isPartialApprove } = props const currency = amountToApprove.currency const { state: approvalState } = useApproveState(amountToApprove) - const tradeApproveCallback = useTradeApproveCallback(amountToApprove) + const tradeApproveCallback = useTradeApproveCallback(amountToApprove, isPartialApprove) const shouldZeroApprove = useShouldZeroApprove(amountToApprove) const zeroApprove = useZeroApprove(amountToApprove.currency) diff --git a/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts b/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts index 3e5136344c..552a32d42b 100644 --- a/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts +++ b/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts @@ -19,13 +19,16 @@ export interface TradeApproveCallback { (params?: TradeApproveCallbackParams): Promise } -export function useTradeApproveCallback(amountToApprove?: CurrencyAmount): TradeApproveCallback { +export function useTradeApproveCallback( + amountToApprove?: CurrencyAmount, + isPartialApprove?: boolean, +): TradeApproveCallback { const updateTradeApproveState = useUpdateTradeApproveState() const spender = useTradeSpenderAddress() const currency = amountToApprove?.currency const symbol = currency?.symbol - const approveCallback = useApproveCallback(amountToApprove, spender) + const approveCallback = useApproveCallback(amountToApprove, spender, isPartialApprove) return useCallback( async ({ useModals = true }: TradeApproveCallbackParams = { useModals: true }) => { @@ -58,6 +61,6 @@ export function useTradeApproveCallback(amountToApprove?: CurrencyAmount, + isPartialApprove?: boolean, ): Promise<{ approveAmount: BigNumber | string gasLimit: BigNumber }> { + const approveAmount = + isPartialApprove && amountToApprove ? BigNumber.from(amountToApprove.quotient.toString()) : MaxUint256 + try { return { - approveAmount: MaxUint256, - gasLimit: await tokenContract.estimateGas.approve(spender, MaxUint256), + approveAmount, + gasLimit: await tokenContract.estimateGas.approve(spender, approveAmount), } } catch { // Fallback: Attempt to set an approval for the maximum wallet balance (instead of the MaxUint256). @@ -45,7 +49,7 @@ export async function estimateApprove( ) return { - approveAmount: MaxUint256, + approveAmount, gasLimit: GAS_LIMIT_DEFAULT, } } @@ -55,6 +59,7 @@ export async function estimateApprove( export function useApproveCallback( amountToApprove?: CurrencyAmount, spender?: string, + isPartialApprove?: boolean, ): (summary?: string) => Promise { const { chainId } = useWalletInfo() const currency = amountToApprove?.currency @@ -68,7 +73,7 @@ export function useApproveCallback( return } - const estimation = await estimateApprove(tokenContract, spender, amountToApprove) + const estimation = await estimateApprove(tokenContract, spender, amountToApprove, isPartialApprove) return tokenContract .approve(spender, estimation.approveAmount, { gasLimit: calculateGasMargin(estimation.gasLimit), @@ -81,5 +86,5 @@ export function useApproveCallback( }) return response }) - }, [chainId, token, tokenContract, amountToApprove, spender, addTransaction]) + }, [chainId, token, tokenContract, amountToApprove, spender, addTransaction, isPartialApprove]) } diff --git a/apps/cowswap-frontend/src/common/pure/ApproveButton/index.tsx b/apps/cowswap-frontend/src/common/pure/ApproveButton/index.tsx index 13273df82e..68bda945b0 100644 --- a/apps/cowswap-frontend/src/common/pure/ApproveButton/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/ApproveButton/index.tsx @@ -50,7 +50,6 @@ export function ApproveButton(props: ApproveButtonProps) { content={ You must give the CoW Protocol smart contracts permission to use your . - If you approve the default amount, you will only have to do this once per token. } > diff --git a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx index 81d1eaafc2..605eb3faf3 100644 --- a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx @@ -22,7 +22,16 @@ import { ExplorerDataType, getExplorerLink, getRandomInt, isSellOrder, shortenAd import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' import { TokenLogo } from '@cowprotocol/tokens' import { Command } from '@cowprotocol/types' -import { Confetti, ExternalLink, InfoTooltip, ProductLogo, ProductVariant, TokenAmount, UI } from '@cowprotocol/ui' +import { + Confetti, + ExternalLink, + InfoTooltip, + ProductLogo, + ProductVariant, + TokenAmount, + UI, + UnderlinedLinkStyledButton, +} from '@cowprotocol/ui' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { AnimatePresence, motion } from 'framer-motion' @@ -1123,14 +1132,14 @@ function ExpiredStep(props: OrderProgressBarV2Props) {

The good news

Unlike on other exchanges, you won't be charged for this! Feel free to{' '} - { props.navigateToNewOrder?.() trackNewOrderClick() }} > place a new order - {' '} + {' '} without worry.

diff --git a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts index 198fe338ff..cd1a6a3de0 100644 --- a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts +++ b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts @@ -1,6 +1,6 @@ import IMAGE_STAR_SHINE from '@cowprotocol/assets/cow-swap/star-shine.svg' import { SingleLetterLogoWrapper } from '@cowprotocol/tokens' -import { ButtonPrimary, Font, LinkStyledButton, Media, UI } from '@cowprotocol/ui' +import { ButtonPrimary, Font, Media, UI } from '@cowprotocol/ui' import styled, { css, keyframes } from 'styled-components/macro' @@ -66,6 +66,7 @@ export const StepsContainer = styled.div<{ $height: number; $minHeight?: string; padding: 0; // implement a gradient to hide the bottom of the steps container using white to opacity white using pseudo element + &::after { content: ${({ bottomGradient }) => (bottomGradient ? '""' : 'none')}; position: absolute; @@ -143,15 +144,6 @@ export const CancelButton = styled(CancelButtonOriginal)` } ` -export const Button = styled(LinkStyledButton)` - font-size: 14px; - text-decoration: underline; - - &:hover { - text-decoration: none; - } -` - export const ProgressImageWrapper = styled.div<{ bgColor?: string; padding?: string; height?: string; gap?: string }>` width: 100%; height: ${({ height }) => height || '246px'}; diff --git a/apps/cowswap-frontend/src/common/utils/parsePermitData.ts b/apps/cowswap-frontend/src/common/utils/parsePermitData.ts new file mode 100644 index 0000000000..6ba16f7c1a --- /dev/null +++ b/apps/cowswap-frontend/src/common/utils/parsePermitData.ts @@ -0,0 +1,15 @@ +import { Erc20__factory } from '@cowprotocol/abis' +import type { BigNumber } from '@ethersproject/bignumber' + +const erc20Interface = Erc20__factory.createInterface() + +export interface PermitParameters { + owner: string + spender: string + value: BigNumber + deadline: BigNumber +} + +export function parsePermitData(callData: string): PermitParameters { + return erc20Interface.decodeFunctionData('permit', callData) as unknown as PermitParameters +} diff --git a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx index 2f86b41a32..24aeb1e878 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx +++ b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx @@ -8,8 +8,11 @@ import { Currency } from '@uniswap/sdk-core' import { shallowEqual } from 'react-redux' +import { useIsHooksTradeType } from 'modules/trade/hooks/useIsHooksTradeType' + import { updateHooksEnabled, + updatePartialApprove, updateRecipientToggleVisible, updateUserDarkMode, updateUserDeadline, @@ -118,6 +121,22 @@ export function useUserTransactionTTL(): [number, (slippage: number) => void] { return [deadline, setUserDeadline] } +export function usePartialApprove(): [boolean, (value: boolean) => void] { + const dispatch = useAppDispatch() + const isHookTradeType = useIsHooksTradeType() + const partialApprove = useAppSelector((state) => state.user.partialApprove) + + const setPartialApprove = useCallback( + (partialApprove: boolean) => { + dispatch(updatePartialApprove({ partialApprove })) + }, + [dispatch], + ) + + // Partial approve is disabled for Hooks store + return [isHookTradeType ? false : partialApprove, setPartialApprove] +} + export function useSelectedWallet(): string | undefined { return useAppSelector(({ user: { selectedWallet } }) => selectedWallet) } diff --git a/apps/cowswap-frontend/src/legacy/state/user/reducer.ts b/apps/cowswap-frontend/src/legacy/state/user/reducer.ts index a9470d45b3..395811fa84 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/reducer.ts +++ b/apps/cowswap-frontend/src/legacy/state/user/reducer.ts @@ -18,6 +18,7 @@ export interface UserState { // TODO: mod, shouldn't be here recipientToggleVisible: boolean hooksEnabled: boolean + partialApprove: boolean // deadline set by user in minutes, used in all txns userDeadline: number @@ -27,9 +28,9 @@ export const initialState: UserState = { selectedWallet: undefined, matchesDarkMode: false, userDarkMode: null, - // TODO: mod, shouldn't be here recipientToggleVisible: false, hooksEnabled: false, + partialApprove: false, userLocale: null, userDeadline: DEFAULT_DEADLINE_FROM_NOW, } @@ -56,6 +57,9 @@ const userSlice = createSlice({ updateUserDeadline(state, action) { state.userDeadline = action.payload.userDeadline }, + updatePartialApprove(state, action) { + state.partialApprove = action.payload.partialApprove + }, updateRecipientToggleVisible(state, action) { state.recipientToggleVisible = action.payload.recipientToggleVisible }, @@ -70,5 +74,6 @@ export const { updateUserDeadline, updateUserLocale, updateRecipientToggleVisible, + updatePartialApprove, } = userSlice.actions export default userSlice.reducer diff --git a/apps/cowswap-frontend/src/lib/hooks/useApproval.ts b/apps/cowswap-frontend/src/lib/hooks/useApproval.ts index 285b4afb2a..bfe49fa6c2 100644 --- a/apps/cowswap-frontend/src/lib/hooks/useApproval.ts +++ b/apps/cowswap-frontend/src/lib/hooks/useApproval.ts @@ -1,9 +1,7 @@ -import { useCallback, useMemo } from 'react' +import { useMemo } from 'react' -import { calculateGasMargin, getIsNativeToken } from '@cowprotocol/common-utils' +import { getIsNativeToken } from '@cowprotocol/common-utils' import { useWalletInfo } from '@cowprotocol/wallet' -import { MaxUint256 } from '@ethersproject/constants' -import { TransactionResponse } from '@ethersproject/providers' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { Nullish } from 'types' @@ -11,7 +9,6 @@ import { Nullish } from 'types' import { useTokenAllowance } from 'legacy/hooks/useTokenAllowance' import { ApprovalState } from 'common/hooks/useApproveState' -import { useTokenContract } from 'common/hooks/useContract' export interface ApprovalStateForSpenderResult { approvalState: ApprovalState @@ -22,7 +19,7 @@ function toApprovalState( amountToApprove: Nullish>, spender: string | undefined, currentAllowance?: CurrencyAmount, - pendingApproval?: boolean + pendingApproval?: boolean, ): ApprovalState { // Unknown amount or spender if (!amountToApprove || !spender) { @@ -50,7 +47,7 @@ function toApprovalState( export function useApprovalStateForSpender( amountToApprove: Nullish>, spender: string | undefined, - useIsPendingApproval: (token?: Token, spender?: string) => boolean + useIsPendingApproval: (token?: Token, spender?: string) => boolean, ): ApprovalStateForSpenderResult { const { account } = useWalletInfo() const currency = amountToApprove?.currency @@ -64,66 +61,3 @@ export function useApprovalStateForSpender( return { approvalState, currentAllowance } }, [amountToApprove, currentAllowance, pendingApproval, spender]) } - -export function useApproval( - amountToApprove: CurrencyAmount | undefined, - spender: string | undefined, - useIsPendingApproval: (token?: Token, spender?: string) => boolean -): [ - ApprovalState, - () => Promise<{ response: TransactionResponse; tokenAddress: string; spenderAddress: string } | undefined> -] { - const { chainId } = useWalletInfo() - const currency = amountToApprove?.currency - const token = currency && !getIsNativeToken(currency) ? currency : undefined - - // check the current approval status - const approvalState = useApprovalStateForSpender(amountToApprove, spender, useIsPendingApproval).approvalState - - const tokenContract = useTokenContract(token?.address) - - const approve = useCallback(async () => { - function logFailure(error: Error | string): undefined { - console.warn(`${token?.symbol || 'Token'} approval failed:`, error) - return - } - - // Bail early if there is an issue. - if (approvalState !== ApprovalState.NOT_APPROVED) { - return logFailure('approve was called unnecessarily') - } else if (!chainId) { - return logFailure('no chainId') - } else if (!token) { - return logFailure('no token') - } else if (!tokenContract) { - return logFailure('tokenContract is null') - } else if (!amountToApprove) { - return logFailure('missing amount to approve') - } else if (!spender) { - return logFailure('no spender') - } - - let useExact = false - const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => { - // general fallback for tokens which restrict approval amounts - useExact = true - return tokenContract.estimateGas.approve(spender, amountToApprove.quotient.toString()) - }) - - return tokenContract - .approve(spender, useExact ? amountToApprove.quotient.toString() : MaxUint256, { - gasLimit: calculateGasMargin(estimatedGas), - }) - .then((response) => ({ - response, - tokenAddress: token.address, - spenderAddress: spender, - })) - .catch((error: Error) => { - logFailure(error) - throw error - }) - }, [approvalState, token, tokenContract, amountToApprove, spender, chainId]) - - return [approvalState, approve] -} diff --git a/apps/cowswap-frontend/src/modules/operations/bundle/buildApproveTx.ts b/apps/cowswap-frontend/src/modules/operations/bundle/buildApproveTx.ts index abd22db931..51aba39eea 100644 --- a/apps/cowswap-frontend/src/modules/operations/bundle/buildApproveTx.ts +++ b/apps/cowswap-frontend/src/modules/operations/bundle/buildApproveTx.ts @@ -7,15 +7,16 @@ export type BuildApproveTxParams = { erc20Contract: Erc20 spender: string amountToApprove: CurrencyAmount + isPartialApprove?: boolean } /** * Builds the approval tx, without sending it */ export async function buildApproveTx(params: BuildApproveTxParams) { - const { erc20Contract, spender, amountToApprove } = params + const { erc20Contract, spender, amountToApprove, isPartialApprove } = params - const estimatedAmount = await estimateApprove(erc20Contract, spender, amountToApprove) + const estimatedAmount = await estimateApprove(erc20Contract, spender, amountToApprove, isPartialApprove) return erc20Contract.populateTransaction.approve(spender, estimatedAmount.approveAmount) } diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts index 86d6b6bf65..2fbf6ba422 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts @@ -19,7 +19,7 @@ import { GeneratePermitHook, GeneratePermitHookParams } from '../types' /** * Hook that returns callback to generate permit hook data */ -export function useGeneratePermitHook(): GeneratePermitHook { +export function useGeneratePermitHook(isPartialApprove?: boolean): GeneratePermitHook { const { chainId } = useWalletInfo() const storePermit = useSetAtom(storePermitCacheAtom) const getCachedPermit = useGetCachedPermit() @@ -44,14 +44,15 @@ export function useGeneratePermitHook(): GeneratePermitHook { const eip2162Utils = getPermitUtilsInstance(chainId, provider, account) const spender = customSpender || COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId] + const amount = isPartialApprove ? params.amount : undefined // Always get the nonce for the real account, to know whether the cache should be invalidated // Static account should never need to pre-check the nonce as it'll never change once cached const nonce = account ? await eip2162Utils.getTokenNonce(inputToken.address, account) : undefined - const permitParams = { chainId, tokenAddress: inputToken.address, account, nonce } + const permitParams = { chainId, tokenAddress: inputToken.address, account, nonce, amount } - const cachedPermit = await getCachedPermit(inputToken.address, spender) + const cachedPermit = await getCachedPermit(inputToken.address, spender, amount) if (cachedPermit) { return cachedPermit @@ -66,12 +67,13 @@ export function useGeneratePermitHook(): GeneratePermitHook { eip2162Utils, account, nonce, + amount, }) hookData && storePermit({ ...permitParams, hookData, spender }) return hookData }, - [provider, chainId, getCachedPermit, storePermit], + [provider, chainId, getCachedPermit, storePermit, isPartialApprove], ) } diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useGetCachedPermit.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useGetCachedPermit.ts index 4f0ba0dc7a..4476e8c7c4 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useGetCachedPermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useGetCachedPermit.ts @@ -13,13 +13,14 @@ import { getPermitCacheAtom } from '../state/permitCacheAtom' export function useGetCachedPermit(): ( tokenAddress: Nullish, customSpender?: string, + amount?: bigint, ) => Promise { const { chainId, account } = useWalletInfo() const provider = useWalletProvider() const getCachedPermit = useSetAtom(getPermitCacheAtom) return useCallback( - async (tokenAddress: Nullish, customSpender?: string) => { + async (tokenAddress: Nullish, customSpender?: string, amount?: bigint) => { if (!provider || !account || !tokenAddress) { return } @@ -32,7 +33,7 @@ export function useGetCachedPermit(): ( // Static account should never need to pre-check the nonce as it'll never change once cached const nonce = account ? await eip2162Utils.getTokenNonce(tokenAddress, account) : undefined - const permitParams = { chainId, tokenAddress, account, nonce, spender } + const permitParams = { chainId, tokenAddress, account, nonce, spender, amount } return getCachedPermit(permitParams) } catch (e) { diff --git a/apps/cowswap-frontend/src/modules/permit/state/permitCacheAtom.ts b/apps/cowswap-frontend/src/modules/permit/state/permitCacheAtom.ts index e09aa938f4..1ab0198f52 100644 --- a/apps/cowswap-frontend/src/modules/permit/state/permitCacheAtom.ts +++ b/apps/cowswap-frontend/src/modules/permit/state/permitCacheAtom.ts @@ -1,6 +1,12 @@ import { atom } from 'jotai' import { atomWithStorage } from 'jotai/utils' +import { MaxUint256 } from '@ethersproject/constants' + +import { TradeType, tradeTypeAtom } from 'modules/trade' + +import { Routes } from 'common/constants/routes' + import { CachedPermitData, GetPermitCacheParams, @@ -14,14 +20,14 @@ import { * Should never change once it has been created. * Used exclusively for quote requests */ -export const staticPermitCacheAtom = atomWithStorage('staticPermitCache:v3', {}) +export const staticPermitCacheAtom = atomWithStorage('staticPermitCache:v4', {}) /** * Atom that stores permit data for user permit requests. * Should be updated whenever the permit nonce is updated. * Used exclusively for order requests */ -export const userPermitCacheAtom = atomWithStorage('userPermitCache:v1', {}) +export const userPermitCacheAtom = atomWithStorage('userPermitCache:v2', {}) /** * Atom to add/update permit cache data @@ -36,6 +42,7 @@ export const storePermitCacheAtom = atom(null, (get, set, params: StorePermitCac const dataToCache: CachedPermitData = { hookData: params.hookData, nonce: params.nonce, + amount: params.amount?.toString(), } set(atomToUpdate, (permitCache) => ({ ...permitCache, [key]: JSON.stringify(dataToCache) })) @@ -53,6 +60,10 @@ export const getPermitCacheAtom = atom(null, (get, set, params: GetPermitCachePa const atomToUpdate = params.account ? userPermitCacheAtom : staticPermitCacheAtom const permitCache = get(atomToUpdate) + const tradeType = get(tradeTypeAtom) + + const isSwap = tradeType?.tradeType === TradeType.SWAP && tradeType.route === Routes.SWAP + const key = buildKey(params) const cachedData = permitCache[key] @@ -61,7 +72,7 @@ export const getPermitCacheAtom = atom(null, (get, set, params: GetPermitCachePa } try { - const { hookData, nonce: storedNonce }: CachedPermitData = JSON.parse(cachedData) + const { hookData, nonce: storedNonce, amount }: CachedPermitData = JSON.parse(cachedData) if (params.account !== undefined) { // User type permit cache, check the nonce @@ -76,6 +87,16 @@ export const getPermitCacheAtom = atom(null, (get, set, params: GetPermitCachePa return undefined } + + // Only Swap might create partial amount permits + // Because of that, we skip cached permits with partial amount in other widgets + if (!isSwap && amount && amount !== MaxUint256.toString()) { + return undefined + } + + if (params.amount && params.amount.toString() !== amount) { + return undefined + } } // Cache hit for both static and user permit types @@ -91,8 +112,8 @@ export const getPermitCacheAtom = atom(null, (get, set, params: GetPermitCachePa } }) -function buildKey({ chainId, tokenAddress, account, spender }: PermitCacheKeyParams) { - const base = `${chainId}-${tokenAddress.toLowerCase()}-${spender.toLowerCase()}` +function buildKey({ chainId, tokenAddress, account, spender, amount }: PermitCacheKeyParams) { + const base = `${chainId}-${tokenAddress.toLowerCase()}-${spender.toLowerCase()}-${amount ? amount.toString() : ''}` return account ? `${base}-${account.toLowerCase()}` : base } diff --git a/apps/cowswap-frontend/src/modules/permit/types.ts b/apps/cowswap-frontend/src/modules/permit/types.ts index c1b9cd215d..2a42f1c941 100644 --- a/apps/cowswap-frontend/src/modules/permit/types.ts +++ b/apps/cowswap-frontend/src/modules/permit/types.ts @@ -14,7 +14,7 @@ export type AddPermitTokenParams = { permitInfo: PermitInfo } -export type GeneratePermitHookParams = Pick & { +export type GeneratePermitHookParams = Pick & { customSpender?: string } @@ -33,6 +33,7 @@ export type PermitCache = Record export type CachedPermitData = { hookData: PermitHookData nonce: number | undefined + amount: string | undefined } export type PermitCacheKeyParams = { @@ -41,6 +42,7 @@ export type PermitCacheKeyParams = { account: string | undefined nonce: number | undefined spender: string + amount: bigint | undefined } export type StorePermitCacheParams = PermitCacheKeyParams & { hookData: PermitHookData } diff --git a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts index 1a13cb033c..1be97e5ee9 100644 --- a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts @@ -22,7 +22,7 @@ import { HandlePermitParams } from '../types' * Returns the updated appData */ export async function handlePermit(params: HandlePermitParams): Promise { - const { permitInfo, inputToken, account, appData, typedHooks, generatePermitHook } = params + const { permitInfo, inputToken, account, appData, typedHooks, generatePermitHook, amount } = params if (isSupportedPermitInfo(permitInfo) && !getIsNativeToken(inputToken)) { // permitInfo will only be set if there's NOT enough allowance @@ -31,6 +31,7 @@ export async function handlePermit(params: HandlePermitParams): Promise {(restContent) => ( <> diff --git a/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx index a8b9760bd0..150a7eea22 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx @@ -9,6 +9,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useSingleActivityDescriptor } from 'legacy/hooks/useRecentActivity' import { WrapUnwrapCallback } from 'legacy/hooks/useWrapCallback' +import { usePartialApprove } from 'legacy/state/user/hooks' import { getDerivedEthFlowState } from 'modules/swap/containers/EthFlow/utils/getDerivedEthFlowState' import { EthFlowModalContent } from 'modules/swap/pure/EthFlow/EthFlowModalContent' @@ -25,7 +26,6 @@ import { useEthFlowActions } from './hooks/useEthFlowActions' import useRemainingNativeTxsAndCosts from './hooks/useRemainingNativeTxsAndCosts' import { useSetupEthFlow } from './hooks/useSetupEthFlow' - export interface EthFlowProps { nativeInput?: CurrencyAmount hasEnoughWrappedBalanceForSwap: boolean @@ -45,10 +45,12 @@ export function EthFlowModal({ const native = useNativeCurrency() const wrapped = useWrappedToken() const { state: approvalState } = useApproveState(nativeInput || null) + const [isPartialApprove] = usePartialApprove() const ethFlowContext = useAtomValue(ethFlowContextAtom) const approveCallback = useTradeApproveCallback( - (nativeInput && currencyAmountToTokenAmount(nativeInput)) || undefined + (nativeInput && currencyAmountToTokenAmount(nativeInput)) || undefined, + isPartialApprove, ) const ethFlowActions = useEthFlowActions({ wrap: wrapCallback, @@ -86,7 +88,7 @@ export function EthFlowModal({ approvalState, approveActivity, wrapActivity, - onDismiss + onDismiss, }) return ( diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx index a49a039628..d4fff58b3d 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -14,7 +14,12 @@ import { NetworkAlert } from 'legacy/components/NetworkAlert/NetworkAlert' import { useModalIsOpen } from 'legacy/state/application/hooks' import { ApplicationModal } from 'legacy/state/application/reducer' import { Field } from 'legacy/state/types' -import { useHooksEnabledManager, useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/user/hooks' +import { + useHooksEnabledManager, + usePartialApprove, + useRecipientToggleManager, + useUserTransactionTTL, +} from 'legacy/state/user/hooks' import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' @@ -85,6 +90,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const recipientToggleState = useRecipientToggleManager() const hooksEnabledState = useHooksEnabledManager() const deadlineState = useUserTransactionTTL() + const partialApproveState = usePartialApprove() const isHookTradeType = useIsHooksTradeType() const isTradePriceUpdating = useTradePricesUpdate() @@ -238,6 +244,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { recipientToggleState={recipientToggleState} hooksEnabledState={hooksEnabledState} deadlineState={deadlineState} + // Partial approve is disabled for Hooks store + partialApproveState={isHookTradeType ? undefined : partialApproveState} /> ), diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts index f550702e03..275a2798c3 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react' import { Field } from 'legacy/state/types' -import { useUserTransactionTTL } from 'legacy/state/user/hooks' +import { usePartialApprove, useUserTransactionTTL } from 'legacy/state/user/hooks' import { TradeWidgetActions, useTradePriceImpact } from 'modules/trade' import { logTradeFlow } from 'modules/trade/utils/logger' @@ -25,7 +25,11 @@ export function useHandleSwapOrEthFlow(actions: TradeWidgetActions) { const { onUserInput, onChangeRecipient } = actions const [deadline] = useUserTransactionTTL() - const { callback: handleSwap, contextIsReady } = useHandleSwap(useSafeMemoObject({ deadline }), actions) + const [isPartialApprove] = usePartialApprove() + const { callback: handleSwap, contextIsReady } = useHandleSwap( + useSafeMemoObject({ deadline, isPartialApprove }), + actions, + ) const callback = useCallback(async () => { if (!swapFlowContext) return diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index 91fe6ec473..19906be3f4 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -14,6 +14,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useToggleWalletModal } from 'legacy/state/application/hooks' import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/hooks' import { Field } from 'legacy/state/types' +import { usePartialApprove } from 'legacy/state/user/hooks' import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' @@ -63,6 +64,7 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge const tradeConfirmActions = useTradeConfirmActions() const { standaloneMode } = useInjectedWidgetParams() const isHooksStore = useIsHooksTradeType() + const [isPartialApprove] = usePartialApprove() const currencyIn = currencies[Field.INPUT] const currencyOut = currencies[Field.OUTPUT] @@ -143,6 +145,7 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge onCurrencySelection, widgetStandaloneMode: standaloneMode, quoteDeadlineParams, + isPartialApprove, }), [ swapButtonState, @@ -159,6 +162,7 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge onCurrencySelection, standaloneMode, quoteDeadlineParams, + isPartialApprove, ], ) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index afed5d89d7..bc70dfb18f 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -1,4 +1,4 @@ -import { useUserTransactionTTL } from 'legacy/state/user/hooks' +import { usePartialApprove, useUserTransactionTTL } from 'legacy/state/user/hooks' import { useTradeFlowContext } from 'modules/tradeFlow' @@ -6,5 +6,7 @@ import { useSafeMemoObject } from 'common/hooks/useSafeMemo' export function useSwapFlowContext() { const [deadline] = useUserTransactionTTL() - return useTradeFlowContext(useSafeMemoObject({ deadline })) + const [isPartialApprove] = usePartialApprove() + + return useTradeFlowContext(useSafeMemoObject({ deadline, isPartialApprove })) } diff --git a/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowModalContent/configs.ts b/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowModalContent/configs.ts index b5648ccb99..e599b3e3f3 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowModalContent/configs.ts +++ b/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowModalContent/configs.ts @@ -68,9 +68,7 @@ export const ethFlowConfigs: { [EthFlowState.ApproveNeeded]: ({ wrappedSymbol }) => ({ title: `Approve ${wrappedSymbol}`, buttonText: `Approve ${wrappedSymbol}`, - descriptions: [ - `It is required to do a one-time approval of ${wrappedSymbol} via an on-chain ERC20 Approve transaction.`, - ], + descriptions: [`It is required to do an approval of ${wrappedSymbol} via an on-chain ERC20 Approve transaction.`], }), [EthFlowState.SwapReady]: ({ wrappedSymbol }) => ({ title: `Continue swap with ${wrappedSymbol}`, diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx index 110dd701f1..c64663d107 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx @@ -26,6 +26,7 @@ const swapButtonsContext: SwapButtonsContext = { openSwapConfirm: () => void 0, toggleWalletModal: () => void 0, hasEnoughWrappedBalanceForSwap: true, + isPartialApprove: false, quoteDeadlineParams: { validFor: 0, quoteValidTo: 0, diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx index 28c5618757..a0dd7e1e41 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx @@ -40,6 +40,7 @@ export interface SwapButtonsContext { onCurrencySelection: (field: Field, currency: Currency) => void widgetStandaloneMode?: boolean quoteDeadlineParams: QuoteDeadlineParams + isPartialApprove: boolean } const swapButtonStateMap: { [key in SwapButtonState]: (props: SwapButtonsContext) => JSX.Element } = { @@ -126,7 +127,7 @@ const swapButtonStateMap: { [key in SwapButtonState]: (props: SwapButtonsContext {props.inputAmount && ( - + Swap diff --git a/apps/cowswap-frontend/src/modules/swap/pure/banners/PartialApprovalBanner.tsx b/apps/cowswap-frontend/src/modules/swap/pure/banners/PartialApprovalBanner.tsx new file mode 100644 index 0000000000..caddfb37b5 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/pure/banners/PartialApprovalBanner.tsx @@ -0,0 +1,31 @@ +import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg' +import { Command } from '@cowprotocol/types' +import { BannerOrientation, ClosableBanner, InlineBanner, UnderlinedLinkStyledButton } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +const BANNER_STORAGE_KEY = 'partialPermitBannerKey:v0' + +type PartialApprovalBannerProps = { + openSettings: Command +} + +export function PartialApprovalBanner({ openSettings }: PartialApprovalBannerProps) { + return ClosableBanner(BANNER_STORAGE_KEY, (onClose) => ( + +

+ NEW: You can now choose to do minimal token approvals in the settings. +

+
+ )) +} + +const Link = styled(UnderlinedLinkStyledButton)` + padding: 0; +` diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx index 979c49913d..6edebb05e2 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx @@ -9,6 +9,7 @@ import { CustomRecipientWarningBanner, LongLoadText, } from '@cowprotocol/ui' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' import ms from 'ms.macro' @@ -49,6 +50,9 @@ export interface TradeConfirmationProps { recipient?: string | null buttonText?: React.ReactNode children?: (restContent: ReactElement) => ReactElement + slippageAdjustedSellAmount?: CurrencyAmount + isPartialApprove?: boolean + displayHookDetails?: boolean } export function TradeConfirmation(props: TradeConfirmationProps) { @@ -76,6 +80,9 @@ export function TradeConfirmation(props: TradeConfirmationProps) { recipient, isPriceStatic, appData, + isPartialApprove, + slippageAdjustedSellAmount, + displayHookDetails, } = frozenProps || props /** @@ -126,15 +133,20 @@ export function TradeConfirmation(props: TradeConfirmationProps) { onConfirm() } - const hookDetailsElement = ( + const hookDetailsElement = displayHookDetails ? ( <> {appData && ( - + {(hookChildren) => hookChildren} )} - ) + ) : null return ( e.key === 'Escape' && onDismiss()}> diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts index fafa6e7594..945cf69e0f 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -28,9 +28,10 @@ import { TradeFlowContext } from '../types/TradeFlowContext' export interface TradeFlowParams { deadline: number + isPartialApprove?: boolean } -export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowContext | null { +export function useTradeFlowContext({ deadline, isPartialApprove }: TradeFlowParams): TradeFlowContext | null { const { chainId, account } = useWalletInfo() const provider = useWalletProvider() const { allowsOffchainSigning } = useWalletDetails() @@ -49,7 +50,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon const networkFee = receiveAmountInfo?.costs.networkFee.amountInSellCurrency const permitInfo = usePermitInfo(sellCurrency, tradeType) - const generatePermitHook = useGeneratePermitHook() + const generatePermitHook = useGeneratePermitHook(isPartialApprove) const getCachedPermit = useGetCachedPermit() const closeModals = useCloseModals() const dispatch = useDispatch() @@ -122,6 +123,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon deadline, orderKind, uiOrderType, + isPartialApprove, ] : null, ([ @@ -152,6 +154,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon deadline, orderKind, uiOrderType, + isPartialApprove, ]) => { return { context: { @@ -162,6 +165,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon }, flags: { allowsOffchainSigning, + isPartialApprove, }, callbacks: { closeModals, diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts index cae474c566..eafae28aff 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts @@ -33,7 +33,15 @@ export async function safeBundleApprovalFlow( return false } - const { context, callbacks, orderParams, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext + const { + context, + callbacks, + orderParams, + swapFlowAnalyticsContext, + tradeConfirmActions, + typedHooks, + flags: { isPartialApprove }, + } = tradeContext const { spender, settlementContract, safeAppsSdk, erc20Contract } = safeBundleContext @@ -52,6 +60,7 @@ export async function safeBundleApprovalFlow( erc20Contract, spender, amountToApprove: context.inputAmount, + isPartialApprove, }) orderParams.appData = await removePermitHookFromAppData(orderParams.appData, typedHooks) diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts index 0247b09d84..c9b09bd002 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts @@ -35,7 +35,14 @@ export async function safeBundleEthFlow( return false } - const { context, callbacks, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext + const { + context, + callbacks, + swapFlowAnalyticsContext, + tradeConfirmActions, + typedHooks, + flags: { isPartialApprove }, + } = tradeContext const { spender, settlementContract, safeAppsSdk, needsApproval, wrappedNativeContract } = safeBundleContext @@ -73,6 +80,7 @@ export async function safeBundleEthFlow( erc20Contract: wrappedNativeContract as unknown as Erc20, spender, amountToApprove: inputAmount, + isPartialApprove, }) txs.push({ diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts index d26e22b9d4..1a8589d56c 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts @@ -59,6 +59,7 @@ export async function swapFlow( inputToken: inputCurrency, permitInfo, generatePermitHook, + amount: BigInt(inputAmount.quotient.toString()), }) if (callDataContainsPermitSigner(orderParams.appData.fullAppData)) { diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts index 16f6dc07b8..c129b37df6 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts @@ -26,6 +26,7 @@ export interface TradeFlowContext { } flags: { allowsOffchainSigning: boolean + isPartialApprove?: boolean } callbacks: { closeModals: Command diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx index fc7273935d..7090446fdb 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -27,12 +27,20 @@ interface SettingsTabProps { className?: string recipientToggleState: StatefulValue hooksEnabledState?: StatefulValue + partialApproveState?: StatefulValue deadlineState: StatefulValue } -export function SettingsTab({ className, recipientToggleState, hooksEnabledState, deadlineState }: SettingsTabProps) { +export function SettingsTab({ + className, + recipientToggleState, + hooksEnabledState, + deadlineState, + partialApproveState, +}: SettingsTabProps) { const menuButtonRef = useRef(null) + const [isPartialApprove, setPartialApprove] = partialApproveState || [null, null] const [recipientToggleVisible, toggleRecipientVisibilityAux] = recipientToggleState const toggleRecipientVisibility = useCallback( (value?: boolean) => { @@ -104,7 +112,7 @@ export function SettingsTab({ className, recipientToggleState, hooksEnabledState Experimental: {' '} - Add DeFI interactions before and after your trade + Add DeFI interactions before and after your trade. } /> @@ -112,6 +120,35 @@ export function SettingsTab({ className, recipientToggleState, hooksEnabledState )} + + {isPartialApprove !== null && setPartialApprove && ( + + + + Minimal Approvals + + + By default, token approvals & permits are for an unlimited amount, which ensures you don't pay extra for subsequent trades. +
+
+ When this setting is enabled, approvals & permits will be for the minimum amount instead of unlimited. + This incurs additional costs on every trade. +
+
+ Existing approvals must be revoked manually before you can re-approve. + + } + /> +
+ setPartialApprove(!isPartialApprove)} + /> +
+ )}
diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx index 741bc17006..4a9c0ec9d9 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx @@ -1,4 +1,3 @@ -import { useSetAtom } from 'jotai' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' @@ -10,7 +9,7 @@ import styled from 'styled-components/macro' import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' -import { settingsTabStateAtom } from '../../../state/settingsTabState' +import { useOpenSettingsTab } from '../../../state/settingsTabState' import { RowStyleProps, StyledInfoIcon, StyledRowBetween, TextWrapper, TransactionText } from '../styled' const DefaultSlippage = styled.span` @@ -65,9 +64,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) { isSmartSlippageLoading, } = props - const setSettingTabState = useSetAtom(settingsTabStateAtom) - - const openSettings = () => setSettingTabState({ open: true }) + const openSettings = useOpenSettingsTab() const tooltipContent = slippageTooltip || diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts index 659fb6321f..61b3e2561a 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts @@ -1,3 +1,9 @@ -import { atom } from 'jotai' +import { atom, useSetAtom } from 'jotai' export const settingsTabStateAtom = atom({ open: false }) + +export function useOpenSettingsTab() { + const setSettingTabState = useSetAtom(settingsTabStateAtom) + + return () => setSettingTabState({ open: true }) +} diff --git a/apps/cowswap-frontend/src/pages/Swap/index.tsx b/apps/cowswap-frontend/src/pages/Swap/index.tsx index d672663c10..c90c727003 100644 --- a/apps/cowswap-frontend/src/pages/Swap/index.tsx +++ b/apps/cowswap-frontend/src/pages/Swap/index.tsx @@ -5,13 +5,16 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { Navigate, useLocation, useParams } from 'react-router-dom' import { SwapUpdaters, SwapWidget } from 'modules/swap' +import { PartialApprovalBanner } from 'modules/swap/pure/banners/PartialApprovalBanner' import { getDefaultTradeRawState } from 'modules/trade/types/TradeRawState' import { parameterizeTradeRoute } from 'modules/trade/utils/parameterizeTradeRoute' +import { useOpenSettingsTab } from 'modules/tradeWidgetAddons/state/settingsTabState' import { Routes } from 'common/constants/routes' export function SwapPage() { const params = useParams() + const openSettings = useOpenSettingsTab() if (!params.chainId) { return @@ -20,7 +23,7 @@ export function SwapPage() { return ( <> - + } /> ) } diff --git a/apps/cowswap-frontend/src/utils/orderUtils/getOrderPermitAmount.ts b/apps/cowswap-frontend/src/utils/orderUtils/getOrderPermitAmount.ts index d579add099..ab4c3d7c8f 100644 --- a/apps/cowswap-frontend/src/utils/orderUtils/getOrderPermitAmount.ts +++ b/apps/cowswap-frontend/src/utils/orderUtils/getOrderPermitAmount.ts @@ -1,11 +1,10 @@ -import { Erc20__factory } from '@cowprotocol/abis' import type { LatestAppDataDocVersion } from '@cowprotocol/app-data' import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' import { BigNumber } from '@ethersproject/bignumber' -import { ParsedOrder } from './parseOrder' +import { parsePermitData } from 'common/utils/parsePermitData' -const erc20Interface = Erc20__factory.createInterface() +import { ParsedOrder } from './parseOrder' export function getOrderPermitAmount(chainId: SupportedChainId, order: ParsedOrder): BigNumber | null { if (!order.fullAppData) return null @@ -20,7 +19,7 @@ export function getOrderPermitAmount(chainId: SupportedChainId, order: ParsedOrd const permitData = preHooks .map((hook) => { try { - return erc20Interface.decodeFunctionData('permit', hook.callData) + return parsePermitData(hook.callData) } catch { return null } diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json index 4ce8c5e3e2..7f973f7768 100644 --- a/libs/hook-dapp-lib/src/hookDappsRegistry.json +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -24,7 +24,7 @@ "PERMIT_TOKEN": { "type": "INTERNAL", "name": "Permit a token", - "descriptionShort": "Infinite permit an address to spend one token on your behalf.", + "descriptionShort": "Permit an address to spend one token on your behalf.", "description": "This hook allows you to permit an address to spend your tokens on your behalf. This is useful for allowing a smart contract to spend your tokens without needing to approve each transaction.", "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/refs/heads/develop/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/icon.png", "version": "v0.1.0", diff --git a/libs/permit-utils/src/lib/generatePermitHook.ts b/libs/permit-utils/src/lib/generatePermitHook.ts index e9f2e32675..f121a67d83 100644 --- a/libs/permit-utils/src/lib/generatePermitHook.ts +++ b/libs/permit-utils/src/lib/generatePermitHook.ts @@ -37,7 +37,17 @@ export async function generatePermitHook(params: PermitHookParams): Promise { - const { inputToken, spender, chainId, permitInfo, provider, account, eip2162Utils, nonce: preFetchedNonce } = params + const { + inputToken, + spender, + chainId, + permitInfo, + provider, + account, + eip2162Utils, + nonce: preFetchedNonce, + amount, + } = params const tokenAddress = inputToken.address // TODO: remove the need for `name` from input token. Should come from permitInfo instead @@ -58,7 +68,7 @@ async function generatePermitHookRaw(params: PermitHookParams): Promise Date: Thu, 9 Jan 2025 10:44:00 +0000 Subject: [PATCH 15/15] Revert "feat(swap): partial approve (#5256)" This reverts commit f080ffdb098612e729f3a3f829410ce78697979f. --- .../containers/OrderHooksDetails/index.tsx | 41 +--------- .../TradeApprove/TradeApproveButton.tsx | 5 +- .../TradeApprove/useTradeApproveCallback.ts | 9 +-- .../src/common/hooks/useApproveCallback.ts | 15 ++-- .../src/common/pure/ApproveButton/index.tsx | 1 + .../common/pure/OrderProgressBarV2/index.tsx | 15 +--- .../common/pure/OrderProgressBarV2/styled.ts | 12 ++- .../src/common/utils/parsePermitData.ts | 15 ---- .../src/legacy/state/user/hooks.tsx | 19 ----- .../src/legacy/state/user/reducer.ts | 7 +- .../src/lib/hooks/useApproval.ts | 74 ++++++++++++++++++- .../operations/bundle/buildApproveTx.ts | 5 +- .../permit/hooks/useGeneratePermitHook.ts | 10 +-- .../permit/hooks/useGetCachedPermit.ts | 5 +- .../modules/permit/state/permitCacheAtom.ts | 31 ++------ .../src/modules/permit/types.ts | 4 +- .../src/modules/permit/utils/handlePermit.ts | 3 +- .../ConfirmSwapModalSetup/index.tsx | 8 +- .../modules/swap/containers/EthFlow/index.tsx | 8 +- .../swap/containers/SwapWidget/index.tsx | 10 +-- .../swap/hooks/useHandleSwapOrEthFlow.ts | 8 +- .../swap/hooks/useSwapButtonContext.ts | 4 - .../modules/swap/hooks/useSwapFlowContext.ts | 6 +- .../EthFlow/EthFlowModalContent/configs.ts | 4 +- .../swap/pure/SwapButtons/index.cosmos.tsx | 1 - .../modules/swap/pure/SwapButtons/index.tsx | 3 +- .../pure/banners/PartialApprovalBanner.tsx | 31 -------- .../trade/pure/TradeConfirmation/index.tsx | 18 +---- .../tradeFlow/hooks/useTradeFlowContext.ts | 8 +- .../safeBundleFlow/safeBundleApprovalFlow.ts | 11 +-- .../safeBundleFlow/safeBundleEthFlow.ts | 10 +-- .../tradeFlow/services/swapFlow/index.ts | 1 - .../tradeFlow/types/TradeFlowContext.ts | 1 - .../containers/SettingsTab/index.tsx | 41 +--------- .../pure/Row/RowSlippageContent/index.tsx | 7 +- .../state/settingsTabState.ts | 8 +- .../cowswap-frontend/src/pages/Swap/index.tsx | 5 +- .../utils/orderUtils/getOrderPermitAmount.ts | 7 +- libs/hook-dapp-lib/src/hookDappsRegistry.json | 2 +- .../src/lib/generatePermitHook.ts | 14 +--- libs/permit-utils/src/types.ts | 1 - libs/ui/src/containers/InlineBanner/index.tsx | 2 +- libs/ui/src/pure/LinkStyledButton/index.tsx | 9 --- 43 files changed, 149 insertions(+), 350 deletions(-) delete mode 100644 apps/cowswap-frontend/src/common/utils/parsePermitData.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/banners/PartialApprovalBanner.tsx diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index 67e607b666..3c8a623983 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -3,8 +3,6 @@ import { ReactElement, useEffect, useMemo, useState } from 'react' import { latest } from '@cowprotocol/app-data' import { CowHookDetails, HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' import { InfoTooltip } from '@cowprotocol/ui' -import { useWalletInfo } from '@cowprotocol/wallet' -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { ChevronDown, ChevronUp } from 'react-feather' @@ -16,27 +14,15 @@ import { HookItem } from './HookItem' import * as styledEl from './styled' import { CircleCount } from './styled' -import { parsePermitData } from '../../utils/parsePermitData' - interface OrderHooksDetailsProps { appData: string | AppDataInfo children: (content: ReactElement) => ReactElement margin?: string isTradeConfirmation?: boolean - slippageAdjustedSellAmount?: CurrencyAmount - isPartialApprove?: boolean } -export function OrderHooksDetails({ - appData, - children, - margin, - isTradeConfirmation, - slippageAdjustedSellAmount, - isPartialApprove, -}: OrderHooksDetailsProps) { +export function OrderHooksDetails({ appData, children, margin, isTradeConfirmation }: OrderHooksDetailsProps) { const [isOpen, setOpen] = useState(false) - const { account } = useWalletInfo() const appDataDoc = useMemo(() => { return typeof appData === 'string' ? decodeAppData(appData) : appData.doc }, [appData]) @@ -55,32 +41,9 @@ export function OrderHooksDetails({ const metadata = appDataDoc.metadata as latest.Metadata - /** - * AppData might include a hook with account agnostic permit which is used to fetch a quote. - * This hook should be ignored. - * Moreover, any hook with a permit which has owner !== current account will be excluded. - * We also remove the permit from appData before order signing (see filterPermitSignerPermit). - */ - const preHooks = account - ? metadata.hooks?.pre?.filter((hook) => { - try { - const permitHookData = parsePermitData(hook.callData) - const isOwnerMatched = permitHookData.owner.toLowerCase() === account.toLowerCase() - - // If the hook is a partial approve, we need to check if the value is equal to the slippageAdjustedSellAmount - // Because there might be a hook with an "infinite" permit from other widget - return isPartialApprove && slippageAdjustedSellAmount - ? isOwnerMatched && permitHookData.value.eq(slippageAdjustedSellAmount.quotient.toString()) - : isOwnerMatched - } catch { - return true - } - }) - : metadata.hooks?.pre - const hasSomeFailedSimulation = isTradeConfirmation && Object.values(data || {}).some((hook) => !hook.status) - const preHooksToDapp = matchHooksToDappsRegistry(preHooks || [], preCustomHookDapps) + const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || [], preCustomHookDapps) const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || [], postCustomHookDapps) if (!preHooksToDapp.length && !postHooksToDapp.length) return null diff --git a/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx b/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx index 3e31105e1d..68e68ef2b3 100644 --- a/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx +++ b/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx @@ -14,16 +14,15 @@ export interface TradeApproveButtonProps { amountToApprove: CurrencyAmount children?: React.ReactNode isDisabled?: boolean - isPartialApprove?: boolean } export function TradeApproveButton(props: TradeApproveButtonProps) { - const { amountToApprove, children, isDisabled, isPartialApprove } = props + const { amountToApprove, children, isDisabled } = props const currency = amountToApprove.currency const { state: approvalState } = useApproveState(amountToApprove) - const tradeApproveCallback = useTradeApproveCallback(amountToApprove, isPartialApprove) + const tradeApproveCallback = useTradeApproveCallback(amountToApprove) const shouldZeroApprove = useShouldZeroApprove(amountToApprove) const zeroApprove = useZeroApprove(amountToApprove.currency) diff --git a/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts b/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts index 552a32d42b..3e5136344c 100644 --- a/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts +++ b/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts @@ -19,16 +19,13 @@ export interface TradeApproveCallback { (params?: TradeApproveCallbackParams): Promise } -export function useTradeApproveCallback( - amountToApprove?: CurrencyAmount, - isPartialApprove?: boolean, -): TradeApproveCallback { +export function useTradeApproveCallback(amountToApprove?: CurrencyAmount): TradeApproveCallback { const updateTradeApproveState = useUpdateTradeApproveState() const spender = useTradeSpenderAddress() const currency = amountToApprove?.currency const symbol = currency?.symbol - const approveCallback = useApproveCallback(amountToApprove, spender, isPartialApprove) + const approveCallback = useApproveCallback(amountToApprove, spender) return useCallback( async ({ useModals = true }: TradeApproveCallbackParams = { useModals: true }) => { @@ -61,6 +58,6 @@ export function useTradeApproveCallback( return undefined }) }, - [symbol, approveCallback, updateTradeApproveState, currency], + [symbol, approveCallback, updateTradeApproveState, currency] ) } diff --git a/apps/cowswap-frontend/src/common/hooks/useApproveCallback.ts b/apps/cowswap-frontend/src/common/hooks/useApproveCallback.ts index 8a07a9ef95..6fce1a2444 100644 --- a/apps/cowswap-frontend/src/common/hooks/useApproveCallback.ts +++ b/apps/cowswap-frontend/src/common/hooks/useApproveCallback.ts @@ -18,18 +18,14 @@ export async function estimateApprove( tokenContract: Erc20, spender: string, amountToApprove: CurrencyAmount, - isPartialApprove?: boolean, ): Promise<{ approveAmount: BigNumber | string gasLimit: BigNumber }> { - const approveAmount = - isPartialApprove && amountToApprove ? BigNumber.from(amountToApprove.quotient.toString()) : MaxUint256 - try { return { - approveAmount, - gasLimit: await tokenContract.estimateGas.approve(spender, approveAmount), + approveAmount: MaxUint256, + gasLimit: await tokenContract.estimateGas.approve(spender, MaxUint256), } } catch { // Fallback: Attempt to set an approval for the maximum wallet balance (instead of the MaxUint256). @@ -49,7 +45,7 @@ export async function estimateApprove( ) return { - approveAmount, + approveAmount: MaxUint256, gasLimit: GAS_LIMIT_DEFAULT, } } @@ -59,7 +55,6 @@ export async function estimateApprove( export function useApproveCallback( amountToApprove?: CurrencyAmount, spender?: string, - isPartialApprove?: boolean, ): (summary?: string) => Promise { const { chainId } = useWalletInfo() const currency = amountToApprove?.currency @@ -73,7 +68,7 @@ export function useApproveCallback( return } - const estimation = await estimateApprove(tokenContract, spender, amountToApprove, isPartialApprove) + const estimation = await estimateApprove(tokenContract, spender, amountToApprove) return tokenContract .approve(spender, estimation.approveAmount, { gasLimit: calculateGasMargin(estimation.gasLimit), @@ -86,5 +81,5 @@ export function useApproveCallback( }) return response }) - }, [chainId, token, tokenContract, amountToApprove, spender, addTransaction, isPartialApprove]) + }, [chainId, token, tokenContract, amountToApprove, spender, addTransaction]) } diff --git a/apps/cowswap-frontend/src/common/pure/ApproveButton/index.tsx b/apps/cowswap-frontend/src/common/pure/ApproveButton/index.tsx index 68bda945b0..13273df82e 100644 --- a/apps/cowswap-frontend/src/common/pure/ApproveButton/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/ApproveButton/index.tsx @@ -50,6 +50,7 @@ export function ApproveButton(props: ApproveButtonProps) { content={ You must give the CoW Protocol smart contracts permission to use your . + If you approve the default amount, you will only have to do this once per token. } > diff --git a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx index 605eb3faf3..81d1eaafc2 100644 --- a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx @@ -22,16 +22,7 @@ import { ExplorerDataType, getExplorerLink, getRandomInt, isSellOrder, shortenAd import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' import { TokenLogo } from '@cowprotocol/tokens' import { Command } from '@cowprotocol/types' -import { - Confetti, - ExternalLink, - InfoTooltip, - ProductLogo, - ProductVariant, - TokenAmount, - UI, - UnderlinedLinkStyledButton, -} from '@cowprotocol/ui' +import { Confetti, ExternalLink, InfoTooltip, ProductLogo, ProductVariant, TokenAmount, UI } from '@cowprotocol/ui' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { AnimatePresence, motion } from 'framer-motion' @@ -1132,14 +1123,14 @@ function ExpiredStep(props: OrderProgressBarV2Props) {

The good news

Unlike on other exchanges, you won't be charged for this! Feel free to{' '} - { props.navigateToNewOrder?.() trackNewOrderClick() }} > place a new order - {' '} + {' '} without worry.

diff --git a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts index cd1a6a3de0..198fe338ff 100644 --- a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts +++ b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/styled.ts @@ -1,6 +1,6 @@ import IMAGE_STAR_SHINE from '@cowprotocol/assets/cow-swap/star-shine.svg' import { SingleLetterLogoWrapper } from '@cowprotocol/tokens' -import { ButtonPrimary, Font, Media, UI } from '@cowprotocol/ui' +import { ButtonPrimary, Font, LinkStyledButton, Media, UI } from '@cowprotocol/ui' import styled, { css, keyframes } from 'styled-components/macro' @@ -66,7 +66,6 @@ export const StepsContainer = styled.div<{ $height: number; $minHeight?: string; padding: 0; // implement a gradient to hide the bottom of the steps container using white to opacity white using pseudo element - &::after { content: ${({ bottomGradient }) => (bottomGradient ? '""' : 'none')}; position: absolute; @@ -144,6 +143,15 @@ export const CancelButton = styled(CancelButtonOriginal)` } ` +export const Button = styled(LinkStyledButton)` + font-size: 14px; + text-decoration: underline; + + &:hover { + text-decoration: none; + } +` + export const ProgressImageWrapper = styled.div<{ bgColor?: string; padding?: string; height?: string; gap?: string }>` width: 100%; height: ${({ height }) => height || '246px'}; diff --git a/apps/cowswap-frontend/src/common/utils/parsePermitData.ts b/apps/cowswap-frontend/src/common/utils/parsePermitData.ts deleted file mode 100644 index 6ba16f7c1a..0000000000 --- a/apps/cowswap-frontend/src/common/utils/parsePermitData.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Erc20__factory } from '@cowprotocol/abis' -import type { BigNumber } from '@ethersproject/bignumber' - -const erc20Interface = Erc20__factory.createInterface() - -export interface PermitParameters { - owner: string - spender: string - value: BigNumber - deadline: BigNumber -} - -export function parsePermitData(callData: string): PermitParameters { - return erc20Interface.decodeFunctionData('permit', callData) as unknown as PermitParameters -} diff --git a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx index 24aeb1e878..2f86b41a32 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx +++ b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx @@ -8,11 +8,8 @@ import { Currency } from '@uniswap/sdk-core' import { shallowEqual } from 'react-redux' -import { useIsHooksTradeType } from 'modules/trade/hooks/useIsHooksTradeType' - import { updateHooksEnabled, - updatePartialApprove, updateRecipientToggleVisible, updateUserDarkMode, updateUserDeadline, @@ -121,22 +118,6 @@ export function useUserTransactionTTL(): [number, (slippage: number) => void] { return [deadline, setUserDeadline] } -export function usePartialApprove(): [boolean, (value: boolean) => void] { - const dispatch = useAppDispatch() - const isHookTradeType = useIsHooksTradeType() - const partialApprove = useAppSelector((state) => state.user.partialApprove) - - const setPartialApprove = useCallback( - (partialApprove: boolean) => { - dispatch(updatePartialApprove({ partialApprove })) - }, - [dispatch], - ) - - // Partial approve is disabled for Hooks store - return [isHookTradeType ? false : partialApprove, setPartialApprove] -} - export function useSelectedWallet(): string | undefined { return useAppSelector(({ user: { selectedWallet } }) => selectedWallet) } diff --git a/apps/cowswap-frontend/src/legacy/state/user/reducer.ts b/apps/cowswap-frontend/src/legacy/state/user/reducer.ts index 395811fa84..a9470d45b3 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/reducer.ts +++ b/apps/cowswap-frontend/src/legacy/state/user/reducer.ts @@ -18,7 +18,6 @@ export interface UserState { // TODO: mod, shouldn't be here recipientToggleVisible: boolean hooksEnabled: boolean - partialApprove: boolean // deadline set by user in minutes, used in all txns userDeadline: number @@ -28,9 +27,9 @@ export const initialState: UserState = { selectedWallet: undefined, matchesDarkMode: false, userDarkMode: null, + // TODO: mod, shouldn't be here recipientToggleVisible: false, hooksEnabled: false, - partialApprove: false, userLocale: null, userDeadline: DEFAULT_DEADLINE_FROM_NOW, } @@ -57,9 +56,6 @@ const userSlice = createSlice({ updateUserDeadline(state, action) { state.userDeadline = action.payload.userDeadline }, - updatePartialApprove(state, action) { - state.partialApprove = action.payload.partialApprove - }, updateRecipientToggleVisible(state, action) { state.recipientToggleVisible = action.payload.recipientToggleVisible }, @@ -74,6 +70,5 @@ export const { updateUserDeadline, updateUserLocale, updateRecipientToggleVisible, - updatePartialApprove, } = userSlice.actions export default userSlice.reducer diff --git a/apps/cowswap-frontend/src/lib/hooks/useApproval.ts b/apps/cowswap-frontend/src/lib/hooks/useApproval.ts index bfe49fa6c2..285b4afb2a 100644 --- a/apps/cowswap-frontend/src/lib/hooks/useApproval.ts +++ b/apps/cowswap-frontend/src/lib/hooks/useApproval.ts @@ -1,7 +1,9 @@ -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' -import { getIsNativeToken } from '@cowprotocol/common-utils' +import { calculateGasMargin, getIsNativeToken } from '@cowprotocol/common-utils' import { useWalletInfo } from '@cowprotocol/wallet' +import { MaxUint256 } from '@ethersproject/constants' +import { TransactionResponse } from '@ethersproject/providers' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { Nullish } from 'types' @@ -9,6 +11,7 @@ import { Nullish } from 'types' import { useTokenAllowance } from 'legacy/hooks/useTokenAllowance' import { ApprovalState } from 'common/hooks/useApproveState' +import { useTokenContract } from 'common/hooks/useContract' export interface ApprovalStateForSpenderResult { approvalState: ApprovalState @@ -19,7 +22,7 @@ function toApprovalState( amountToApprove: Nullish>, spender: string | undefined, currentAllowance?: CurrencyAmount, - pendingApproval?: boolean, + pendingApproval?: boolean ): ApprovalState { // Unknown amount or spender if (!amountToApprove || !spender) { @@ -47,7 +50,7 @@ function toApprovalState( export function useApprovalStateForSpender( amountToApprove: Nullish>, spender: string | undefined, - useIsPendingApproval: (token?: Token, spender?: string) => boolean, + useIsPendingApproval: (token?: Token, spender?: string) => boolean ): ApprovalStateForSpenderResult { const { account } = useWalletInfo() const currency = amountToApprove?.currency @@ -61,3 +64,66 @@ export function useApprovalStateForSpender( return { approvalState, currentAllowance } }, [amountToApprove, currentAllowance, pendingApproval, spender]) } + +export function useApproval( + amountToApprove: CurrencyAmount | undefined, + spender: string | undefined, + useIsPendingApproval: (token?: Token, spender?: string) => boolean +): [ + ApprovalState, + () => Promise<{ response: TransactionResponse; tokenAddress: string; spenderAddress: string } | undefined> +] { + const { chainId } = useWalletInfo() + const currency = amountToApprove?.currency + const token = currency && !getIsNativeToken(currency) ? currency : undefined + + // check the current approval status + const approvalState = useApprovalStateForSpender(amountToApprove, spender, useIsPendingApproval).approvalState + + const tokenContract = useTokenContract(token?.address) + + const approve = useCallback(async () => { + function logFailure(error: Error | string): undefined { + console.warn(`${token?.symbol || 'Token'} approval failed:`, error) + return + } + + // Bail early if there is an issue. + if (approvalState !== ApprovalState.NOT_APPROVED) { + return logFailure('approve was called unnecessarily') + } else if (!chainId) { + return logFailure('no chainId') + } else if (!token) { + return logFailure('no token') + } else if (!tokenContract) { + return logFailure('tokenContract is null') + } else if (!amountToApprove) { + return logFailure('missing amount to approve') + } else if (!spender) { + return logFailure('no spender') + } + + let useExact = false + const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => { + // general fallback for tokens which restrict approval amounts + useExact = true + return tokenContract.estimateGas.approve(spender, amountToApprove.quotient.toString()) + }) + + return tokenContract + .approve(spender, useExact ? amountToApprove.quotient.toString() : MaxUint256, { + gasLimit: calculateGasMargin(estimatedGas), + }) + .then((response) => ({ + response, + tokenAddress: token.address, + spenderAddress: spender, + })) + .catch((error: Error) => { + logFailure(error) + throw error + }) + }, [approvalState, token, tokenContract, amountToApprove, spender, chainId]) + + return [approvalState, approve] +} diff --git a/apps/cowswap-frontend/src/modules/operations/bundle/buildApproveTx.ts b/apps/cowswap-frontend/src/modules/operations/bundle/buildApproveTx.ts index 51aba39eea..abd22db931 100644 --- a/apps/cowswap-frontend/src/modules/operations/bundle/buildApproveTx.ts +++ b/apps/cowswap-frontend/src/modules/operations/bundle/buildApproveTx.ts @@ -7,16 +7,15 @@ export type BuildApproveTxParams = { erc20Contract: Erc20 spender: string amountToApprove: CurrencyAmount - isPartialApprove?: boolean } /** * Builds the approval tx, without sending it */ export async function buildApproveTx(params: BuildApproveTxParams) { - const { erc20Contract, spender, amountToApprove, isPartialApprove } = params + const { erc20Contract, spender, amountToApprove } = params - const estimatedAmount = await estimateApprove(erc20Contract, spender, amountToApprove, isPartialApprove) + const estimatedAmount = await estimateApprove(erc20Contract, spender, amountToApprove) return erc20Contract.populateTransaction.approve(spender, estimatedAmount.approveAmount) } diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts index 2fbf6ba422..86d6b6bf65 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts @@ -19,7 +19,7 @@ import { GeneratePermitHook, GeneratePermitHookParams } from '../types' /** * Hook that returns callback to generate permit hook data */ -export function useGeneratePermitHook(isPartialApprove?: boolean): GeneratePermitHook { +export function useGeneratePermitHook(): GeneratePermitHook { const { chainId } = useWalletInfo() const storePermit = useSetAtom(storePermitCacheAtom) const getCachedPermit = useGetCachedPermit() @@ -44,15 +44,14 @@ export function useGeneratePermitHook(isPartialApprove?: boolean): GeneratePermi const eip2162Utils = getPermitUtilsInstance(chainId, provider, account) const spender = customSpender || COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId] - const amount = isPartialApprove ? params.amount : undefined // Always get the nonce for the real account, to know whether the cache should be invalidated // Static account should never need to pre-check the nonce as it'll never change once cached const nonce = account ? await eip2162Utils.getTokenNonce(inputToken.address, account) : undefined - const permitParams = { chainId, tokenAddress: inputToken.address, account, nonce, amount } + const permitParams = { chainId, tokenAddress: inputToken.address, account, nonce } - const cachedPermit = await getCachedPermit(inputToken.address, spender, amount) + const cachedPermit = await getCachedPermit(inputToken.address, spender) if (cachedPermit) { return cachedPermit @@ -67,13 +66,12 @@ export function useGeneratePermitHook(isPartialApprove?: boolean): GeneratePermi eip2162Utils, account, nonce, - amount, }) hookData && storePermit({ ...permitParams, hookData, spender }) return hookData }, - [provider, chainId, getCachedPermit, storePermit, isPartialApprove], + [provider, chainId, getCachedPermit, storePermit], ) } diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useGetCachedPermit.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useGetCachedPermit.ts index 4476e8c7c4..4f0ba0dc7a 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useGetCachedPermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useGetCachedPermit.ts @@ -13,14 +13,13 @@ import { getPermitCacheAtom } from '../state/permitCacheAtom' export function useGetCachedPermit(): ( tokenAddress: Nullish, customSpender?: string, - amount?: bigint, ) => Promise { const { chainId, account } = useWalletInfo() const provider = useWalletProvider() const getCachedPermit = useSetAtom(getPermitCacheAtom) return useCallback( - async (tokenAddress: Nullish, customSpender?: string, amount?: bigint) => { + async (tokenAddress: Nullish, customSpender?: string) => { if (!provider || !account || !tokenAddress) { return } @@ -33,7 +32,7 @@ export function useGetCachedPermit(): ( // Static account should never need to pre-check the nonce as it'll never change once cached const nonce = account ? await eip2162Utils.getTokenNonce(tokenAddress, account) : undefined - const permitParams = { chainId, tokenAddress, account, nonce, spender, amount } + const permitParams = { chainId, tokenAddress, account, nonce, spender } return getCachedPermit(permitParams) } catch (e) { diff --git a/apps/cowswap-frontend/src/modules/permit/state/permitCacheAtom.ts b/apps/cowswap-frontend/src/modules/permit/state/permitCacheAtom.ts index 1ab0198f52..e09aa938f4 100644 --- a/apps/cowswap-frontend/src/modules/permit/state/permitCacheAtom.ts +++ b/apps/cowswap-frontend/src/modules/permit/state/permitCacheAtom.ts @@ -1,12 +1,6 @@ import { atom } from 'jotai' import { atomWithStorage } from 'jotai/utils' -import { MaxUint256 } from '@ethersproject/constants' - -import { TradeType, tradeTypeAtom } from 'modules/trade' - -import { Routes } from 'common/constants/routes' - import { CachedPermitData, GetPermitCacheParams, @@ -20,14 +14,14 @@ import { * Should never change once it has been created. * Used exclusively for quote requests */ -export const staticPermitCacheAtom = atomWithStorage('staticPermitCache:v4', {}) +export const staticPermitCacheAtom = atomWithStorage('staticPermitCache:v3', {}) /** * Atom that stores permit data for user permit requests. * Should be updated whenever the permit nonce is updated. * Used exclusively for order requests */ -export const userPermitCacheAtom = atomWithStorage('userPermitCache:v2', {}) +export const userPermitCacheAtom = atomWithStorage('userPermitCache:v1', {}) /** * Atom to add/update permit cache data @@ -42,7 +36,6 @@ export const storePermitCacheAtom = atom(null, (get, set, params: StorePermitCac const dataToCache: CachedPermitData = { hookData: params.hookData, nonce: params.nonce, - amount: params.amount?.toString(), } set(atomToUpdate, (permitCache) => ({ ...permitCache, [key]: JSON.stringify(dataToCache) })) @@ -60,10 +53,6 @@ export const getPermitCacheAtom = atom(null, (get, set, params: GetPermitCachePa const atomToUpdate = params.account ? userPermitCacheAtom : staticPermitCacheAtom const permitCache = get(atomToUpdate) - const tradeType = get(tradeTypeAtom) - - const isSwap = tradeType?.tradeType === TradeType.SWAP && tradeType.route === Routes.SWAP - const key = buildKey(params) const cachedData = permitCache[key] @@ -72,7 +61,7 @@ export const getPermitCacheAtom = atom(null, (get, set, params: GetPermitCachePa } try { - const { hookData, nonce: storedNonce, amount }: CachedPermitData = JSON.parse(cachedData) + const { hookData, nonce: storedNonce }: CachedPermitData = JSON.parse(cachedData) if (params.account !== undefined) { // User type permit cache, check the nonce @@ -87,16 +76,6 @@ export const getPermitCacheAtom = atom(null, (get, set, params: GetPermitCachePa return undefined } - - // Only Swap might create partial amount permits - // Because of that, we skip cached permits with partial amount in other widgets - if (!isSwap && amount && amount !== MaxUint256.toString()) { - return undefined - } - - if (params.amount && params.amount.toString() !== amount) { - return undefined - } } // Cache hit for both static and user permit types @@ -112,8 +91,8 @@ export const getPermitCacheAtom = atom(null, (get, set, params: GetPermitCachePa } }) -function buildKey({ chainId, tokenAddress, account, spender, amount }: PermitCacheKeyParams) { - const base = `${chainId}-${tokenAddress.toLowerCase()}-${spender.toLowerCase()}-${amount ? amount.toString() : ''}` +function buildKey({ chainId, tokenAddress, account, spender }: PermitCacheKeyParams) { + const base = `${chainId}-${tokenAddress.toLowerCase()}-${spender.toLowerCase()}` return account ? `${base}-${account.toLowerCase()}` : base } diff --git a/apps/cowswap-frontend/src/modules/permit/types.ts b/apps/cowswap-frontend/src/modules/permit/types.ts index 2a42f1c941..c1b9cd215d 100644 --- a/apps/cowswap-frontend/src/modules/permit/types.ts +++ b/apps/cowswap-frontend/src/modules/permit/types.ts @@ -14,7 +14,7 @@ export type AddPermitTokenParams = { permitInfo: PermitInfo } -export type GeneratePermitHookParams = Pick & { +export type GeneratePermitHookParams = Pick & { customSpender?: string } @@ -33,7 +33,6 @@ export type PermitCache = Record export type CachedPermitData = { hookData: PermitHookData nonce: number | undefined - amount: string | undefined } export type PermitCacheKeyParams = { @@ -42,7 +41,6 @@ export type PermitCacheKeyParams = { account: string | undefined nonce: number | undefined spender: string - amount: bigint | undefined } export type StorePermitCacheParams = PermitCacheKeyParams & { hookData: PermitHookData } diff --git a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts index 1be97e5ee9..1a13cb033c 100644 --- a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts @@ -22,7 +22,7 @@ import { HandlePermitParams } from '../types' * Returns the updated appData */ export async function handlePermit(params: HandlePermitParams): Promise { - const { permitInfo, inputToken, account, appData, typedHooks, generatePermitHook, amount } = params + const { permitInfo, inputToken, account, appData, typedHooks, generatePermitHook } = params if (isSupportedPermitInfo(permitInfo) && !getIsNativeToken(inputToken)) { // permitInfo will only be set if there's NOT enough allowance @@ -31,7 +31,6 @@ export async function handlePermit(params: HandlePermitParams): Promise {(restContent) => ( <> diff --git a/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx index 150a7eea22..a8b9760bd0 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx @@ -9,7 +9,6 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useSingleActivityDescriptor } from 'legacy/hooks/useRecentActivity' import { WrapUnwrapCallback } from 'legacy/hooks/useWrapCallback' -import { usePartialApprove } from 'legacy/state/user/hooks' import { getDerivedEthFlowState } from 'modules/swap/containers/EthFlow/utils/getDerivedEthFlowState' import { EthFlowModalContent } from 'modules/swap/pure/EthFlow/EthFlowModalContent' @@ -26,6 +25,7 @@ import { useEthFlowActions } from './hooks/useEthFlowActions' import useRemainingNativeTxsAndCosts from './hooks/useRemainingNativeTxsAndCosts' import { useSetupEthFlow } from './hooks/useSetupEthFlow' + export interface EthFlowProps { nativeInput?: CurrencyAmount hasEnoughWrappedBalanceForSwap: boolean @@ -45,12 +45,10 @@ export function EthFlowModal({ const native = useNativeCurrency() const wrapped = useWrappedToken() const { state: approvalState } = useApproveState(nativeInput || null) - const [isPartialApprove] = usePartialApprove() const ethFlowContext = useAtomValue(ethFlowContextAtom) const approveCallback = useTradeApproveCallback( - (nativeInput && currencyAmountToTokenAmount(nativeInput)) || undefined, - isPartialApprove, + (nativeInput && currencyAmountToTokenAmount(nativeInput)) || undefined ) const ethFlowActions = useEthFlowActions({ wrap: wrapCallback, @@ -88,7 +86,7 @@ export function EthFlowModal({ approvalState, approveActivity, wrapActivity, - onDismiss, + onDismiss }) return ( diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx index d4fff58b3d..a49a039628 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -14,12 +14,7 @@ import { NetworkAlert } from 'legacy/components/NetworkAlert/NetworkAlert' import { useModalIsOpen } from 'legacy/state/application/hooks' import { ApplicationModal } from 'legacy/state/application/reducer' import { Field } from 'legacy/state/types' -import { - useHooksEnabledManager, - usePartialApprove, - useRecipientToggleManager, - useUserTransactionTTL, -} from 'legacy/state/user/hooks' +import { useHooksEnabledManager, useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/user/hooks' import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' @@ -90,7 +85,6 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const recipientToggleState = useRecipientToggleManager() const hooksEnabledState = useHooksEnabledManager() const deadlineState = useUserTransactionTTL() - const partialApproveState = usePartialApprove() const isHookTradeType = useIsHooksTradeType() const isTradePriceUpdating = useTradePricesUpdate() @@ -244,8 +238,6 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { recipientToggleState={recipientToggleState} hooksEnabledState={hooksEnabledState} deadlineState={deadlineState} - // Partial approve is disabled for Hooks store - partialApproveState={isHookTradeType ? undefined : partialApproveState} /> ), diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts index 275a2798c3..f550702e03 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react' import { Field } from 'legacy/state/types' -import { usePartialApprove, useUserTransactionTTL } from 'legacy/state/user/hooks' +import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { TradeWidgetActions, useTradePriceImpact } from 'modules/trade' import { logTradeFlow } from 'modules/trade/utils/logger' @@ -25,11 +25,7 @@ export function useHandleSwapOrEthFlow(actions: TradeWidgetActions) { const { onUserInput, onChangeRecipient } = actions const [deadline] = useUserTransactionTTL() - const [isPartialApprove] = usePartialApprove() - const { callback: handleSwap, contextIsReady } = useHandleSwap( - useSafeMemoObject({ deadline, isPartialApprove }), - actions, - ) + const { callback: handleSwap, contextIsReady } = useHandleSwap(useSafeMemoObject({ deadline }), actions) const callback = useCallback(async () => { if (!swapFlowContext) return diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index 19906be3f4..91fe6ec473 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -14,7 +14,6 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useToggleWalletModal } from 'legacy/state/application/hooks' import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/hooks' import { Field } from 'legacy/state/types' -import { usePartialApprove } from 'legacy/state/user/hooks' import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' @@ -64,7 +63,6 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge const tradeConfirmActions = useTradeConfirmActions() const { standaloneMode } = useInjectedWidgetParams() const isHooksStore = useIsHooksTradeType() - const [isPartialApprove] = usePartialApprove() const currencyIn = currencies[Field.INPUT] const currencyOut = currencies[Field.OUTPUT] @@ -145,7 +143,6 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge onCurrencySelection, widgetStandaloneMode: standaloneMode, quoteDeadlineParams, - isPartialApprove, }), [ swapButtonState, @@ -162,7 +159,6 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge onCurrencySelection, standaloneMode, quoteDeadlineParams, - isPartialApprove, ], ) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index bc70dfb18f..afed5d89d7 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -1,4 +1,4 @@ -import { usePartialApprove, useUserTransactionTTL } from 'legacy/state/user/hooks' +import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { useTradeFlowContext } from 'modules/tradeFlow' @@ -6,7 +6,5 @@ import { useSafeMemoObject } from 'common/hooks/useSafeMemo' export function useSwapFlowContext() { const [deadline] = useUserTransactionTTL() - const [isPartialApprove] = usePartialApprove() - - return useTradeFlowContext(useSafeMemoObject({ deadline, isPartialApprove })) + return useTradeFlowContext(useSafeMemoObject({ deadline })) } diff --git a/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowModalContent/configs.ts b/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowModalContent/configs.ts index e599b3e3f3..b5648ccb99 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowModalContent/configs.ts +++ b/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowModalContent/configs.ts @@ -68,7 +68,9 @@ export const ethFlowConfigs: { [EthFlowState.ApproveNeeded]: ({ wrappedSymbol }) => ({ title: `Approve ${wrappedSymbol}`, buttonText: `Approve ${wrappedSymbol}`, - descriptions: [`It is required to do an approval of ${wrappedSymbol} via an on-chain ERC20 Approve transaction.`], + descriptions: [ + `It is required to do a one-time approval of ${wrappedSymbol} via an on-chain ERC20 Approve transaction.`, + ], }), [EthFlowState.SwapReady]: ({ wrappedSymbol }) => ({ title: `Continue swap with ${wrappedSymbol}`, diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx index c64663d107..110dd701f1 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.cosmos.tsx @@ -26,7 +26,6 @@ const swapButtonsContext: SwapButtonsContext = { openSwapConfirm: () => void 0, toggleWalletModal: () => void 0, hasEnoughWrappedBalanceForSwap: true, - isPartialApprove: false, quoteDeadlineParams: { validFor: 0, quoteValidTo: 0, diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx index a0dd7e1e41..28c5618757 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx @@ -40,7 +40,6 @@ export interface SwapButtonsContext { onCurrencySelection: (field: Field, currency: Currency) => void widgetStandaloneMode?: boolean quoteDeadlineParams: QuoteDeadlineParams - isPartialApprove: boolean } const swapButtonStateMap: { [key in SwapButtonState]: (props: SwapButtonsContext) => JSX.Element } = { @@ -127,7 +126,7 @@ const swapButtonStateMap: { [key in SwapButtonState]: (props: SwapButtonsContext {props.inputAmount && ( - + Swap diff --git a/apps/cowswap-frontend/src/modules/swap/pure/banners/PartialApprovalBanner.tsx b/apps/cowswap-frontend/src/modules/swap/pure/banners/PartialApprovalBanner.tsx deleted file mode 100644 index caddfb37b5..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/banners/PartialApprovalBanner.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg' -import { Command } from '@cowprotocol/types' -import { BannerOrientation, ClosableBanner, InlineBanner, UnderlinedLinkStyledButton } from '@cowprotocol/ui' - -import styled from 'styled-components/macro' - -const BANNER_STORAGE_KEY = 'partialPermitBannerKey:v0' - -type PartialApprovalBannerProps = { - openSettings: Command -} - -export function PartialApprovalBanner({ openSettings }: PartialApprovalBannerProps) { - return ClosableBanner(BANNER_STORAGE_KEY, (onClose) => ( - -

- NEW: You can now choose to do minimal token approvals in the settings. -

-
- )) -} - -const Link = styled(UnderlinedLinkStyledButton)` - padding: 0; -` diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx index 6edebb05e2..979c49913d 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx @@ -9,7 +9,6 @@ import { CustomRecipientWarningBanner, LongLoadText, } from '@cowprotocol/ui' -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' import ms from 'ms.macro' @@ -50,9 +49,6 @@ export interface TradeConfirmationProps { recipient?: string | null buttonText?: React.ReactNode children?: (restContent: ReactElement) => ReactElement - slippageAdjustedSellAmount?: CurrencyAmount - isPartialApprove?: boolean - displayHookDetails?: boolean } export function TradeConfirmation(props: TradeConfirmationProps) { @@ -80,9 +76,6 @@ export function TradeConfirmation(props: TradeConfirmationProps) { recipient, isPriceStatic, appData, - isPartialApprove, - slippageAdjustedSellAmount, - displayHookDetails, } = frozenProps || props /** @@ -133,20 +126,15 @@ export function TradeConfirmation(props: TradeConfirmationProps) { onConfirm() } - const hookDetailsElement = displayHookDetails ? ( + const hookDetailsElement = ( <> {appData && ( - + {(hookChildren) => hookChildren} )} - ) : null + ) return ( e.key === 'Escape' && onDismiss()}> diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts index 945cf69e0f..fafa6e7594 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -28,10 +28,9 @@ import { TradeFlowContext } from '../types/TradeFlowContext' export interface TradeFlowParams { deadline: number - isPartialApprove?: boolean } -export function useTradeFlowContext({ deadline, isPartialApprove }: TradeFlowParams): TradeFlowContext | null { +export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowContext | null { const { chainId, account } = useWalletInfo() const provider = useWalletProvider() const { allowsOffchainSigning } = useWalletDetails() @@ -50,7 +49,7 @@ export function useTradeFlowContext({ deadline, isPartialApprove }: TradeFlowPar const networkFee = receiveAmountInfo?.costs.networkFee.amountInSellCurrency const permitInfo = usePermitInfo(sellCurrency, tradeType) - const generatePermitHook = useGeneratePermitHook(isPartialApprove) + const generatePermitHook = useGeneratePermitHook() const getCachedPermit = useGetCachedPermit() const closeModals = useCloseModals() const dispatch = useDispatch() @@ -123,7 +122,6 @@ export function useTradeFlowContext({ deadline, isPartialApprove }: TradeFlowPar deadline, orderKind, uiOrderType, - isPartialApprove, ] : null, ([ @@ -154,7 +152,6 @@ export function useTradeFlowContext({ deadline, isPartialApprove }: TradeFlowPar deadline, orderKind, uiOrderType, - isPartialApprove, ]) => { return { context: { @@ -165,7 +162,6 @@ export function useTradeFlowContext({ deadline, isPartialApprove }: TradeFlowPar }, flags: { allowsOffchainSigning, - isPartialApprove, }, callbacks: { closeModals, diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts index eafae28aff..cae474c566 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts @@ -33,15 +33,7 @@ export async function safeBundleApprovalFlow( return false } - const { - context, - callbacks, - orderParams, - swapFlowAnalyticsContext, - tradeConfirmActions, - typedHooks, - flags: { isPartialApprove }, - } = tradeContext + const { context, callbacks, orderParams, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext const { spender, settlementContract, safeAppsSdk, erc20Contract } = safeBundleContext @@ -60,7 +52,6 @@ export async function safeBundleApprovalFlow( erc20Contract, spender, amountToApprove: context.inputAmount, - isPartialApprove, }) orderParams.appData = await removePermitHookFromAppData(orderParams.appData, typedHooks) diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts index c9b09bd002..0247b09d84 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts @@ -35,14 +35,7 @@ export async function safeBundleEthFlow( return false } - const { - context, - callbacks, - swapFlowAnalyticsContext, - tradeConfirmActions, - typedHooks, - flags: { isPartialApprove }, - } = tradeContext + const { context, callbacks, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext const { spender, settlementContract, safeAppsSdk, needsApproval, wrappedNativeContract } = safeBundleContext @@ -80,7 +73,6 @@ export async function safeBundleEthFlow( erc20Contract: wrappedNativeContract as unknown as Erc20, spender, amountToApprove: inputAmount, - isPartialApprove, }) txs.push({ diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts index 1a8589d56c..d26e22b9d4 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts @@ -59,7 +59,6 @@ export async function swapFlow( inputToken: inputCurrency, permitInfo, generatePermitHook, - amount: BigInt(inputAmount.quotient.toString()), }) if (callDataContainsPermitSigner(orderParams.appData.fullAppData)) { diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts index c129b37df6..16f6dc07b8 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts @@ -26,7 +26,6 @@ export interface TradeFlowContext { } flags: { allowsOffchainSigning: boolean - isPartialApprove?: boolean } callbacks: { closeModals: Command diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx index 7090446fdb..fc7273935d 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -27,20 +27,12 @@ interface SettingsTabProps { className?: string recipientToggleState: StatefulValue hooksEnabledState?: StatefulValue - partialApproveState?: StatefulValue deadlineState: StatefulValue } -export function SettingsTab({ - className, - recipientToggleState, - hooksEnabledState, - deadlineState, - partialApproveState, -}: SettingsTabProps) { +export function SettingsTab({ className, recipientToggleState, hooksEnabledState, deadlineState }: SettingsTabProps) { const menuButtonRef = useRef(null) - const [isPartialApprove, setPartialApprove] = partialApproveState || [null, null] const [recipientToggleVisible, toggleRecipientVisibilityAux] = recipientToggleState const toggleRecipientVisibility = useCallback( (value?: boolean) => { @@ -112,7 +104,7 @@ export function SettingsTab({ Experimental: {' '} - Add DeFI interactions before and after your trade. + Add DeFI interactions before and after your trade } /> @@ -120,35 +112,6 @@ export function SettingsTab({ )} - - {isPartialApprove !== null && setPartialApprove && ( - - - - Minimal Approvals - - - By default, token approvals & permits are for an unlimited amount, which ensures you don't pay extra for subsequent trades. -
-
- When this setting is enabled, approvals & permits will be for the minimum amount instead of unlimited. - This incurs additional costs on every trade. -
-
- Existing approvals must be revoked manually before you can re-approve. - - } - /> -
- setPartialApprove(!isPartialApprove)} - /> -
- )}
diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx index 4a9c0ec9d9..741bc17006 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx @@ -1,3 +1,4 @@ +import { useSetAtom } from 'jotai' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' @@ -9,7 +10,7 @@ import styled from 'styled-components/macro' import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' -import { useOpenSettingsTab } from '../../../state/settingsTabState' +import { settingsTabStateAtom } from '../../../state/settingsTabState' import { RowStyleProps, StyledInfoIcon, StyledRowBetween, TextWrapper, TransactionText } from '../styled' const DefaultSlippage = styled.span` @@ -64,7 +65,9 @@ export function RowSlippageContent(props: RowSlippageContentProps) { isSmartSlippageLoading, } = props - const openSettings = useOpenSettingsTab() + const setSettingTabState = useSetAtom(settingsTabStateAtom) + + const openSettings = () => setSettingTabState({ open: true }) const tooltipContent = slippageTooltip || diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts index 61b3e2561a..659fb6321f 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts @@ -1,9 +1,3 @@ -import { atom, useSetAtom } from 'jotai' +import { atom } from 'jotai' export const settingsTabStateAtom = atom({ open: false }) - -export function useOpenSettingsTab() { - const setSettingTabState = useSetAtom(settingsTabStateAtom) - - return () => setSettingTabState({ open: true }) -} diff --git a/apps/cowswap-frontend/src/pages/Swap/index.tsx b/apps/cowswap-frontend/src/pages/Swap/index.tsx index c90c727003..d672663c10 100644 --- a/apps/cowswap-frontend/src/pages/Swap/index.tsx +++ b/apps/cowswap-frontend/src/pages/Swap/index.tsx @@ -5,16 +5,13 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { Navigate, useLocation, useParams } from 'react-router-dom' import { SwapUpdaters, SwapWidget } from 'modules/swap' -import { PartialApprovalBanner } from 'modules/swap/pure/banners/PartialApprovalBanner' import { getDefaultTradeRawState } from 'modules/trade/types/TradeRawState' import { parameterizeTradeRoute } from 'modules/trade/utils/parameterizeTradeRoute' -import { useOpenSettingsTab } from 'modules/tradeWidgetAddons/state/settingsTabState' import { Routes } from 'common/constants/routes' export function SwapPage() { const params = useParams() - const openSettings = useOpenSettingsTab() if (!params.chainId) { return @@ -23,7 +20,7 @@ export function SwapPage() { return ( <> - } /> + ) } diff --git a/apps/cowswap-frontend/src/utils/orderUtils/getOrderPermitAmount.ts b/apps/cowswap-frontend/src/utils/orderUtils/getOrderPermitAmount.ts index ab4c3d7c8f..d579add099 100644 --- a/apps/cowswap-frontend/src/utils/orderUtils/getOrderPermitAmount.ts +++ b/apps/cowswap-frontend/src/utils/orderUtils/getOrderPermitAmount.ts @@ -1,11 +1,12 @@ +import { Erc20__factory } from '@cowprotocol/abis' import type { LatestAppDataDocVersion } from '@cowprotocol/app-data' import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' import { BigNumber } from '@ethersproject/bignumber' -import { parsePermitData } from 'common/utils/parsePermitData' - import { ParsedOrder } from './parseOrder' +const erc20Interface = Erc20__factory.createInterface() + export function getOrderPermitAmount(chainId: SupportedChainId, order: ParsedOrder): BigNumber | null { if (!order.fullAppData) return null @@ -19,7 +20,7 @@ export function getOrderPermitAmount(chainId: SupportedChainId, order: ParsedOrd const permitData = preHooks .map((hook) => { try { - return parsePermitData(hook.callData) + return erc20Interface.decodeFunctionData('permit', hook.callData) } catch { return null } diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json index 7f973f7768..4ce8c5e3e2 100644 --- a/libs/hook-dapp-lib/src/hookDappsRegistry.json +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -24,7 +24,7 @@ "PERMIT_TOKEN": { "type": "INTERNAL", "name": "Permit a token", - "descriptionShort": "Permit an address to spend one token on your behalf.", + "descriptionShort": "Infinite permit an address to spend one token on your behalf.", "description": "This hook allows you to permit an address to spend your tokens on your behalf. This is useful for allowing a smart contract to spend your tokens without needing to approve each transaction.", "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/refs/heads/develop/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/icon.png", "version": "v0.1.0", diff --git a/libs/permit-utils/src/lib/generatePermitHook.ts b/libs/permit-utils/src/lib/generatePermitHook.ts index f121a67d83..e9f2e32675 100644 --- a/libs/permit-utils/src/lib/generatePermitHook.ts +++ b/libs/permit-utils/src/lib/generatePermitHook.ts @@ -37,17 +37,7 @@ export async function generatePermitHook(params: PermitHookParams): Promise { - const { - inputToken, - spender, - chainId, - permitInfo, - provider, - account, - eip2162Utils, - nonce: preFetchedNonce, - amount, - } = params + const { inputToken, spender, chainId, permitInfo, provider, account, eip2162Utils, nonce: preFetchedNonce } = params const tokenAddress = inputToken.address // TODO: remove the need for `name` from input token. Should come from permitInfo instead @@ -68,7 +58,7 @@ async function generatePermitHookRaw(params: PermitHookParams): Promise