From f055957af450967b4bc4d58a15fc7a7b80f0aa77 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:19:02 +0100 Subject: [PATCH 001/116] fix(tokens-selector): fix tokens displaying on mobile view (#4929) * fix: rendering of tokens on mobile tokenselector * fix: center align small orders warning text * fix: mobile layout issue on token selector * fix: change tokenitem layout to properly display * fix: proper styling of token import * feat: pass account higher up with props * chore: fix build --------- Co-authored-by: Alexandr Kazachenko --- .../containers/SelectTokenWidget/index.tsx | 5 ++++- .../containers/TokenSearchResults/index.tsx | 5 +++++ .../tokensList/pure/ImportTokenItem/styled.ts | 6 +++++- .../tokensList/pure/ImportTokenModal/styled.ts | 5 +++++ .../pure/SelectTokenModal/index.cosmos.tsx | 1 + .../tokensList/pure/SelectTokenModal/index.tsx | 4 +++- .../modules/tokensList/pure/TokenInfo/index.tsx | 4 ++-- .../modules/tokensList/pure/TokenInfo/styled.ts | 9 +++++++-- .../tokensList/pure/TokenListItem/index.tsx | 10 ++++++++-- .../tokensList/pure/TokenListItem/styled.ts | 5 +++-- .../tokensList/pure/TokensVirtualList/index.tsx | 7 ++++++- libs/tokens/src/pure/TokenLogo/index.tsx | 4 ++++ libs/ui/src/containers/InlineBanner/index.tsx | 1 + libs/ui/src/pure/TokenName/index.tsx | 15 ++++++++++----- libs/ui/src/theme/baseTheme.tsx | 8 ++++---- 15 files changed, 68 insertions(+), 21 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index 79157afa46..e4296a5f64 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -13,6 +13,7 @@ import { useUnsupportedTokens, useUserAddedTokens, } from '@cowprotocol/tokens' +import { useWalletInfo } from '@cowprotocol/wallet' import styled from 'styled-components/macro' @@ -42,6 +43,7 @@ export function SelectTokenWidget() { const [isManageWidgetOpen, setIsManageWidgetOpen] = useState(false) const updateSelectTokenWidget = useUpdateSelectTokenWidgetState() + const { account } = useWalletInfo() const addCustomTokenLists = useAddList((source) => addListAnalytics('Success', source)) const importTokenCallback = useAddUserToken() @@ -146,7 +148,8 @@ export function SelectTokenWidget() { onDismiss={onDismiss} onOpenManageWidget={() => setIsManageWidgetOpen(true)} hideFavoriteTokensTooltip={isInjectedWidgetMode} - > + account={account} + /> ) })()} diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx index 2b9bc78b4e..9b413b167a 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx @@ -4,6 +4,7 @@ import { TokenWithLogo } from '@cowprotocol/common-const' import { doesTokenMatchSymbolOrAddress } from '@cowprotocol/common-utils' import { useSearchToken } from '@cowprotocol/tokens' import { BannerOrientation, ExternalLink, InlineBanner, LINK_GUIDE_ADD_CUSTOM_TOKEN, Loader } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' import { useNetworkName } from 'common/hooks/useNetworkName' @@ -31,6 +32,9 @@ export function TokenSearchResults({ unsupportedTokens, permitCompatibleTokens, }: TokenSearchResultsProps) { + const { account } = useWalletInfo() + const isWalletConnected = !!account + const searchResults = useSearchToken(searchInput) const { values: balances } = balancesState @@ -121,6 +125,7 @@ export function TokenSearchResults({ token={token} balance={balances ? balances[token.address.toLowerCase()] : undefined} onSelectToken={onSelectToken} + isWalletConnected={isWalletConnected} /> ) })} diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenItem/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenItem/styled.ts index 99d0a9c536..068ff64547 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenItem/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenItem/styled.ts @@ -1,4 +1,4 @@ -import { UI } from '@cowprotocol/ui' +import { Media, UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' @@ -10,6 +10,10 @@ export const Wrapper = styled.div` padding: 0 20px; margin-bottom: 20px; + ${Media.upToSmall()} { + padding: 0 14px; + } + &:last-child { margin-bottom: 0; } diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenModal/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenModal/styled.ts index 4c39806439..411930c870 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenModal/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/ImportTokenModal/styled.ts @@ -33,6 +33,11 @@ export const TokenInfo = styled.div` gap: 10px; font-size: 14px; margin-bottom: 20px; + max-width: 100%; + + > a { + word-break: break-all; + } &:last-child { margin-bottom: 0; diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx index f40cc0a9dc..2123dca8c6 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx @@ -26,6 +26,7 @@ const balances = allTokensMock.reduce((acc, token) => { }, {}) const defaultProps: SelectTokenModalProps = { + account: undefined, permitCompatibleTokens: {}, unsupportedTokens, allTokens: allTokensMock, diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index cb8ef213c4..1dba5f772b 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -24,6 +24,7 @@ export interface SelectTokenModalProps { selectedToken?: string permitCompatibleTokens: PermitCompatibleTokens hideFavoriteTokensTooltip?: boolean + account: string | undefined onSelectToken(token: TokenWithLogo): void @@ -50,6 +51,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) { onDismiss, onOpenManageWidget, onInputPressEnter, + account, } = props const [inputValue, setInputValue] = useState(defaultInputValue) @@ -90,7 +92,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) { {inputValue.trim() ? ( ) : ( - + )}
diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx index 13a4964582..f057fb9fc8 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx @@ -15,12 +15,12 @@ export function TokenInfo(props: TokenInfoProps) { return ( -
+ -
+
) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/styled.ts index f81b7717e4..bbf49481e1 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/styled.ts @@ -4,14 +4,12 @@ import styled from 'styled-components/macro' export const Wrapper = styled.div` display: flex; - flex-direction: row; text-align: left; gap: 16px; font-weight: 500; ${Media.upToSmall()} { gap: 10px; - flex: 1 1 auto; } ` @@ -21,3 +19,10 @@ export const TokenName = styled.div` color: inherit; opacity: 0.6; ` + +export const TokenDetails = styled.div` + display: flex; + flex-flow: column wrap; + flex: 1 1 100%; + gap: 4px; +` diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx index 356343381c..7823fb7dc6 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx @@ -19,6 +19,7 @@ export interface TokenListItemProps { virtualRow?: VirtualItem isUnsupported: boolean isPermitCompatible: boolean + isWalletConnected: boolean } export function TokenListItem(props: TokenListItemProps) { @@ -31,6 +32,7 @@ export function TokenListItem(props: TokenListItemProps) { isUnsupported, isPermitCompatible, measureElement, + isWalletConnected, } = props const isTokenSelected = token.address.toLowerCase() === selectedToken?.toLowerCase() @@ -47,8 +49,12 @@ export function TokenListItem(props: TokenListItemProps) { onClick={() => onSelectToken(token)} > - {balanceAmount && } - + {isWalletConnected && ( + <> + {balanceAmount && } + + + )} ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/styled.ts index 2d255068b6..2a25e391eb 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/styled.ts @@ -23,12 +23,13 @@ export const TokenItem = styled.button` padding: 10px 20px; margin-bottom: 10px; opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; - transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out; + transition: + background var(${UI.ANIMATION_DURATION}) ease-in-out, + color var(${UI.ANIMATION_DURATION}) ease-in-out; ${Media.upToSmall()} { font-size: 14px; padding: 10px 15px; - justify-content: flex-end; } &:last-child { diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index 59e409900b..16e91c70a8 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -25,12 +25,16 @@ const scrollDelay = ms`400ms` export interface TokensVirtualListProps extends SelectTokenContext { allTokens: TokenWithLogo[] + account: string | undefined } export function TokensVirtualList(props: TokensVirtualListProps) { - const { allTokens, selectedToken, balancesState, onSelectToken, unsupportedTokens, permitCompatibleTokens } = props + const { allTokens, selectedToken, balancesState, onSelectToken, unsupportedTokens, permitCompatibleTokens, account } = + props const { values: balances, isLoading: balancesLoading } = balancesState + const isWalletConnected = !!account + const scrollTimeoutRef = useRef() const parentRef = useRef(null) const wrapperRef = useRef(null) @@ -83,6 +87,7 @@ export function TokensVirtualList(props: TokensVirtualListProps) { selectedToken={selectedToken} balance={balance} onSelectToken={onSelectToken} + isWalletConnected={isWalletConnected} /> ) })} diff --git a/libs/tokens/src/pure/TokenLogo/index.tsx b/libs/tokens/src/pure/TokenLogo/index.tsx index 1faebe401a..8a37abef83 100644 --- a/libs/tokens/src/pure/TokenLogo/index.tsx +++ b/libs/tokens/src/pure/TokenLogo/index.tsx @@ -26,6 +26,8 @@ export const TokenLogoWrapper = styled.div<{ size?: number; sizeMobile?: number border-radius: ${({ size }) => size ?? defaultSize}px; width: ${({ size }) => size ?? defaultSize}px; height: ${({ size }) => size ?? defaultSize}px; + min-width: ${({ size }) => size ?? defaultSize}px; + min-height: ${({ size }) => size ?? defaultSize}px; font-size: ${({ size }) => size ?? defaultSize}px; overflow: hidden; @@ -44,6 +46,8 @@ export const TokenLogoWrapper = styled.div<{ size?: number; sizeMobile?: number border-radius: ${sizeMobile}px; width: ${sizeMobile}px; height: ${sizeMobile}px; + min-width: ${sizeMobile}px; + min-height: ${sizeMobile}px; font-size: ${sizeMobile}px; > img, diff --git a/libs/ui/src/containers/InlineBanner/index.tsx b/libs/ui/src/containers/InlineBanner/index.tsx index de208bb189..0d994b1490 100644 --- a/libs/ui/src/containers/InlineBanner/index.tsx +++ b/libs/ui/src/containers/InlineBanner/index.tsx @@ -117,6 +117,7 @@ const Wrapper = styled.span<{ > span > span > strong { display: flex; align-items: center; + text-align: center; gap: 6px; color: ${({ colorEnums }) => `var(${colorEnums.text})`}; } diff --git a/libs/ui/src/pure/TokenName/index.tsx b/libs/ui/src/pure/TokenName/index.tsx index af99c4ac78..21e00b06f2 100644 --- a/libs/ui/src/pure/TokenName/index.tsx +++ b/libs/ui/src/pure/TokenName/index.tsx @@ -2,6 +2,8 @@ import styled from 'styled-components/macro' import { sanitizeTokenName } from './sanitizeTokenName' +import { Media } from '../../consts' + export type TokenNameProps = { token: { name?: string } | undefined className?: string @@ -10,23 +12,26 @@ export type TokenNameProps = { const Wrapper = styled.span<{ length?: number }>` display: inline-block; - width: ${({ length }) => length}px; + width: ${({ length }) => length ?? 200}px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - :hover { - white-space: inherit; + ${Media.upToSmall()} { + white-space: normal; + width: 100%; + word-break: break-word; + padding: 0 5px 0 0; } ` -export function TokenName({ token, className, length = 200 }: TokenNameProps) { +export function TokenName({ token, className, length }: TokenNameProps) { const { name } = token || {} if (!name) return null return ( - + {sanitizeTokenName(name)} ) diff --git a/libs/ui/src/theme/baseTheme.tsx b/libs/ui/src/theme/baseTheme.tsx index 2ab245b779..f0160ac232 100644 --- a/libs/ui/src/theme/baseTheme.tsx +++ b/libs/ui/src/theme/baseTheme.tsx @@ -83,7 +83,7 @@ function colors(darkMode: boolean): Colors { gradient1: `linear-gradient(145deg, ${paper}, ${background})`, gradient2: `linear-gradient(250deg, ${transparentize(alert, 0.92)} 10%, ${transparentize( success, - 0.92 + 0.92, )} 50%, ${transparentize(success, 0.92)} 100%);`, boxShadow1: darkMode ? '0 24px 32px rgba(0, 0, 0, 0.06)' : '0 12px 12px rgba(5, 43, 101, 0.06)', boxShadow2: '0 4px 12px 0 rgb(0 0 0 / 15%)', @@ -109,7 +109,7 @@ function utils(darkMode: boolean) { } `, colorScrollbar: css` - scrollbar-color: var(${UI.COLOR_PAPER_DARKEST}), var(${UI.COLOR_PAPER_DARKER}); + scrollbar-color: var(${UI.COLOR_PAPER_DARKEST}), var(${UI.COLOR_TEXT_OPACITY_10}); scroll-behavior: smooth; &::-webkit-scrollbar { @@ -119,8 +119,8 @@ function utils(darkMode: boolean) { } &::-webkit-scrollbar-thumb { - background: var(${UI.COLOR_PAPER_DARKEST}); - border: 3px solid var(${UI.COLOR_PAPER_DARKEST}); + background: var(${UI.COLOR_TEXT_OPACITY_10}); + border: 3px solid var(${UI.COLOR_TEXT_OPACITY_10}); border-radius: 14px; background-clip: padding-box; } From 7111756e359a8e52daa674068f99217efe27ee5b Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 2 Oct 2024 18:43:11 +0500 Subject: [PATCH 002/116] feat(hooks-store): add dapp id to hook callData (#4920) --- .../containers/HookRegistryList/index.tsx | 9 ++-- .../hooksStore/dapps/AirdropHookApp/hook.tsx | 34 ------------ .../hooksStore/dapps/BuildHookApp/hook.tsx | 22 -------- .../hooksStore/dapps/ClaimGnoHookApp/const.ts | 8 +++ .../hooksStore/dapps/ClaimGnoHookApp/hook.tsx | 29 ---------- .../dapps/ClaimGnoHookApp/index.tsx | 33 +++++++----- .../ClaimGnoHookApp/useSBCDepositContract.ts | 16 ------ .../hooksStore/dapps/PermitHookApp/hook.tsx | 17 ------ .../src/modules/hooksStore/hookRegistry.tsx | 41 +++++++------- .../modules/hooksStore/hooks/useAddHook.ts | 13 +++-- .../hooksStore/hooks/useAllHookDapps.ts | 10 ++-- .../modules/hooksStore/hooks/useEditHook.ts | 13 ++++- .../hooksStore/hooks/useInternalHookDapps.ts | 26 +++++++++ .../hooksStore/hooks/useMatchHooksToDapps.ts | 21 ++++++++ .../CustomDappLoader/index.tsx | 37 ++++++++----- .../hooksStore/pure/HookDappDetails/index.tsx | 6 ++- .../hooksStore/state/customHookDappsAtom.ts | 3 +- .../src/modules/hooksStore/types/hooks.ts | 30 ++--------- .../src/modules/hooksStore/utils.ts | 19 +++---- .../hook-dapp-omnibridge/public/manifest.json | 6 ++- libs/hook-dapp-lib/package.json | 1 - libs/hook-dapp-lib/src/consts.ts | 11 ++++ libs/hook-dapp-lib/src/hookDappsRegistry.json | 53 +++++++++++++++++++ libs/hook-dapp-lib/src/index.ts | 4 ++ libs/hook-dapp-lib/src/types.ts | 24 ++++++++- libs/hook-dapp-lib/src/utils.ts | 27 ++++++++++ 26 files changed, 286 insertions(+), 227 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hook.tsx delete mode 100644 apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/hook.tsx delete mode 100644 apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/hook.tsx delete mode 100644 apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/useSBCDepositContract.ts delete mode 100644 apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/hook.tsx create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/hooks/useInternalHookDapps.ts create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/hooks/useMatchHooksToDapps.ts create mode 100644 libs/hook-dapp-lib/src/consts.ts create mode 100644 libs/hook-dapp-lib/src/hookDappsRegistry.json create mode 100644 libs/hook-dapp-lib/src/utils.ts diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx index 3107eb5ab1..8953a4b5ce 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx @@ -3,16 +3,16 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' import { Command } from '@cowprotocol/types' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' -import { useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet' +import { useIsSmartContractWallet } from '@cowprotocol/wallet' import { NewModal } from 'common/pure/NewModal' import { EmptyList, HookDappsList, Wrapper } from './styled' -import { POST_HOOK_REGISTRY, PRE_HOOK_REGISTRY } from '../../hookRegistry' import { useAddCustomHookDapp } from '../../hooks/useAddCustomHookDapp' import { useCustomHookDapps } from '../../hooks/useCustomHookDapps' import { useHookById } from '../../hooks/useHookById' +import { useInternalHookDapps } from '../../hooks/useInternalHookDapps' import { useRemoveCustomHookDapp } from '../../hooks/useRemoveCustomHookDapp' import { AddCustomHookForm } from '../../pure/AddCustomHookForm' import { HookDappDetails } from '../../pure/HookDappDetails' @@ -31,7 +31,6 @@ interface HookStoreModal { } export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStoreModal) { - const { chainId } = useWalletInfo() const [selectedDapp, setSelectedDapp] = useState(null) const [dappDetails, setDappDetails] = useState(null) @@ -51,9 +50,7 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore setSearchQuery('') }, []) - const internalHookDapps = useMemo(() => { - return (isPreHook ? PRE_HOOK_REGISTRY[chainId] : POST_HOOK_REGISTRY[chainId]) || [] - }, [isPreHook, chainId]) + const internalHookDapps = useInternalHookDapps(isPreHook) const currentDapps = useMemo(() => { return isAllHooksTab ? internalHookDapps.concat(customHookDapps) : customHookDapps diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hook.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hook.tsx deleted file mode 100644 index 99c847b421..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hook.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { HookDappInternal, HookDappType, HookDappWalletCompatibility } from 'modules/hooksStore/types/hooks' - -import airdropImage from './airdrop.svg' - -import { AirdropHookApp } from './index' - -const Description = () => { - return ( - <> -

- Effortless Airdrop Claims! - The Claim COW Airdrop feature simplifies the process of collecting free COW tokens before or after your swap, - seamlessly integrating into the CoW Swap platform. -

-
-

- Whether you're claiming new airdrops or exploring CoW on a new network, this tool ensures you get your rewards - quickly and easily. -

- - ) -} - -export const AIRDROP_HOOK_APP: HookDappInternal = { - name: 'Claim COW Airdrop', - description: , - descriptionShort: 'Retrieve COW tokens before or after a swap', - type: HookDappType.INTERNAL, - image: airdropImage, - component: (props) => , - version: '0.1.0', - website: 'https://github.com/bleu/cow-airdrop-contract-deployer', - walletCompatibility: [HookDappWalletCompatibility.EOA, HookDappWalletCompatibility.SMART_CONTRACT], -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/hook.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/hook.tsx deleted file mode 100644 index dd2f973f44..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/hook.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import buildImg from './build.png' - -import { HookDappInternal, HookDappType, HookDappWalletCompatibility } from '../../types/hooks' - -import { BuildHookApp } from './index' - -const getAppDetails = (isPreHook: boolean): HookDappInternal => { - return { - name: `Build your own ${isPreHook ? 'Pre' : 'Post'}-hook`, - descriptionShort: 'Call any smart contract with your own parameters', - description: `Didn't find a suitable hook? You can always create your own! To do this, you need to specify which smart contract you want to call, the parameters for the call and the gas limit.`, - type: HookDappType.INTERNAL, - image: buildImg, - component: (props) => , - version: 'v0.1.0', - website: 'https://docs.cow.fi/cow-protocol/reference/core/intents/hooks', - walletCompatibility: [HookDappWalletCompatibility.SMART_CONTRACT, HookDappWalletCompatibility.EOA], - } -} - -export const PRE_BUILD = getAppDetails(true) -export const POST_BUILD = getAppDetails(false) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/const.ts b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/const.ts index e6ea31fd70..a316307aae 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/const.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/const.ts @@ -1 +1,9 @@ +import { SBCDepositContract as SBCDepositContractType, SBCDepositContractAbi } from '@cowprotocol/abis' +import { Contract } from '@ethersproject/contracts' + export const SBC_DEPOSIT_CONTRACT_ADDRESS = '0x0B98057eA310F4d31F2a452B414647007d1645d9' + +export const SBCDepositContract = new Contract( + SBC_DEPOSIT_CONTRACT_ADDRESS, + SBCDepositContractAbi, +) as SBCDepositContractType diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/hook.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/hook.tsx deleted file mode 100644 index a1f7178a76..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/hook.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import gnoLogo from '@cowprotocol/assets/cow-swap/network-gnosis-chain-logo.svg' - -import { HookDappInternal, HookDappType, HookDappWalletCompatibility } from '../../types/hooks' - -import { ClaimGnoHookApp } from './index' - -export const PRE_CLAIM_GNO: HookDappInternal = { - name: 'Claim GNO from validators', - descriptionShort: 'Withdraw rewards from your Gnosis validators.', - description: ( - <> - This hook allows you to withdraw rewards from your Gnosis Chain validators through CoW Swap. It automates the - process of interacting with the Gnosis Deposit Contract, enabling you to claim any available rewards directly to - your specified withdrawal address. -
-
- The hook monitors your validator's accrued rewards and triggers the claimWithdrawals function when rewards are - ready for withdrawal. This simplifies the management of Gnosis validator earnings without requiring ready for - withdrawal. This simplifies the management of Gnosis validator earnings without requiring manual contract - interaction, providing a smoother and more efficient experience for users. - - ), - type: HookDappType.INTERNAL, - component: (props) => , - image: gnoLogo, - version: 'v0.1.1', - website: 'https://www.gnosis.io/', - walletCompatibility: [HookDappWalletCompatibility.SMART_CONTRACT, HookDappWalletCompatibility.EOA], -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx index 1b2ac92766..6c9be054d0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx @@ -1,17 +1,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react' +import { SupportedChainId } from '@cowprotocol/cow-sdk' import { ButtonPrimary } from '@cowprotocol/ui' import { UI } from '@cowprotocol/ui' +import { useWalletProvider } from '@cowprotocol/wallet-provider' import { BigNumber } from '@ethersproject/bignumber' import { formatUnits } from 'ethers/lib/utils' -import { SBC_DEPOSIT_CONTRACT_ADDRESS } from './const' -import { useSBCDepositContract } from './useSBCDepositContract' +import { SBC_DEPOSIT_CONTRACT_ADDRESS, SBCDepositContract } from './const' import { HookDappProps } from '../../types/hooks' import { ContentWrapper, Text, LoadingLabel, Wrapper } from '../styled' +const SbcDepositContractInterface = SBCDepositContract.interface + /** * Dapp that creates the hook to the connected wallet GNO Rewards. * @@ -20,42 +23,44 @@ import { ContentWrapper, Text, LoadingLabel, Wrapper } from '../styled' * - Master: 0x4fef25519256e24a1fc536f7677152da742fe3ef */ export function ClaimGnoHookApp({ context }: HookDappProps) { - const SbcDepositContract = useSBCDepositContract() + const provider = useWalletProvider() const [claimable, setClaimable] = useState(undefined) const [gasLimit, setGasLimit] = useState(undefined) const [error, setError] = useState(false) const loading = (!gasLimit || !claimable) && !error - const SbcDepositContractInterface = SbcDepositContract?.interface + const account = context?.account + const callData = useMemo(() => { - if (!SbcDepositContractInterface || !context?.account) { + if (!account) { return null } - return SbcDepositContractInterface.encodeFunctionData('claimWithdrawal', [context.account]) - }, [SbcDepositContractInterface, context]) + return SbcDepositContractInterface.encodeFunctionData('claimWithdrawal', [account]) + }, [context]) useEffect(() => { - if (!SbcDepositContract || !context?.account) { + if (!account || !provider) { return } + const handleError = (e: any) => { console.error('[ClaimGnoHookApp] Error getting balance/gasEstimation', e) setError(true) } // Get balance - SbcDepositContract.withdrawableAmount(context.account) + SBCDepositContract.connect(provider) + .withdrawableAmount(account) .then((claimable) => { console.log('[ClaimGnoHookApp] get claimable', claimable) setClaimable(claimable) }) .catch(handleError) - // Get gas estimation - SbcDepositContract.estimateGas.claimWithdrawal(context.account).then(setGasLimit).catch(handleError) - }, [SbcDepositContract, setClaimable, context]) + SBCDepositContract.connect(provider).estimateGas.claimWithdrawal(account).then(setGasLimit).catch(handleError) + }, [setClaimable, account, provider]) const clickOnAddHook = useCallback(() => { if (!callData || !gasLimit || !context || !claimable) { @@ -85,9 +90,9 @@ export function ClaimGnoHookApp({ context }: HookDappProps) { return ( - {!SbcDepositContractInterface ? ( + {context.chainId !== SupportedChainId.GNOSIS_CHAIN ? ( 'Unsupported network. Please change to Gnosis Chain' - ) : !context?.account ? ( + ) : !account ? ( 'Connect your wallet first' ) : ( <> diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/useSBCDepositContract.ts b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/useSBCDepositContract.ts deleted file mode 100644 index 0724f8bf91..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/useSBCDepositContract.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { SBCDepositContract, SBCDepositContractAbi } from '@cowprotocol/abis' -import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { useWalletInfo } from '@cowprotocol/wallet' - -import { useContract } from 'common/hooks/useContract' - -import { SBC_DEPOSIT_CONTRACT_ADDRESS } from './const' - -export function useSBCDepositContract(): SBCDepositContract | null { - const { chainId } = useWalletInfo() - return useContract( - chainId === SupportedChainId.GNOSIS_CHAIN ? SBC_DEPOSIT_CONTRACT_ADDRESS : undefined, - SBCDepositContractAbi, - true, - ) -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/hook.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/hook.tsx deleted file mode 100644 index 9f9d6fed74..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/hook.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import permitImg from './icon.png' - -import { HookDappInternal, HookDappType, HookDappWalletCompatibility } from '../../types/hooks' - -import { PermitHookApp } from './index' - -export const PERMIT_HOOK: HookDappInternal = { - name: `Permit a token`, - 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.`, - type: HookDappType.INTERNAL, - image: permitImg, - component: (props) => , - version: 'v0.1.0', - website: 'https://docs.cow.fi/cow-protocol/reference/core/intents/hooks', - walletCompatibility: [HookDappWalletCompatibility.EOA], -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index 93fa2b9975..0fcf02b699 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -1,21 +1,26 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { hookDappsRegistry } from '@cowprotocol/hook-dapp-lib' -import { AIRDROP_HOOK_APP } from './dapps/AirdropHookApp/hook' -import { PRE_BUILD, POST_BUILD } from './dapps/BuildHookApp/hook' -import { PRE_CLAIM_GNO } from './dapps/ClaimGnoHookApp/hook' -import { PERMIT_HOOK } from './dapps/PermitHookApp/hook' +import { AirdropHookApp } from './dapps/AirdropHookApp' +import { BuildHookApp } from './dapps/BuildHookApp' +import { ClaimGnoHookApp } from './dapps/ClaimGnoHookApp' +import { PermitHookApp } from './dapps/PermitHookApp' import { HookDapp } from './types/hooks' -export const PRE_HOOK_REGISTRY: Record = { - [SupportedChainId.MAINNET]: [PRE_BUILD], - [SupportedChainId.GNOSIS_CHAIN]: [PRE_CLAIM_GNO, PRE_BUILD], - [SupportedChainId.SEPOLIA]: [PRE_BUILD, PERMIT_HOOK, AIRDROP_HOOK_APP], - [SupportedChainId.ARBITRUM_ONE]: [PRE_BUILD], -} - -export const POST_HOOK_REGISTRY: Record = { - [SupportedChainId.MAINNET]: [POST_BUILD], - [SupportedChainId.GNOSIS_CHAIN]: [POST_BUILD], - [SupportedChainId.SEPOLIA]: [POST_BUILD, AIRDROP_HOOK_APP, PERMIT_HOOK], - [SupportedChainId.ARBITRUM_ONE]: [POST_BUILD], -} +export const ALL_HOOK_DAPPS = [ + { + ...hookDappsRegistry.BUILD_CUSTOM_HOOK, + component: (props) => , + }, + { + ...hookDappsRegistry.CLAIM_GNO_FROM_VALIDATORS, + component: (props) => , + }, + { + ...hookDappsRegistry.PERMIT_TOKEN, + component: (props) => , + }, + { + ...hookDappsRegistry.CLAIM_COW_AIRDROP, + component: (props) => , + }, +] as HookDapp[] diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts index b8bf146d11..8b57cf575a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid' import { setHooksAtom } from '../state/hookDetailsAtom' import { AddHook, CowHookDetailsSerialized, HookDapp } from '../types/hooks' -import { getHookDappId } from '../utils' +import { appendDappIdToCallData } from '../utils' export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { const updateHooks = useSetAtom(setHooksAtom) @@ -16,8 +16,15 @@ export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { const uuid = uuidv4() const hookDetails: CowHookDetailsSerialized = { - hookDetails: { ...hookToAdd, uuid }, - dappId: getHookDappId(dapp), + hookDetails: { + ...hookToAdd, + uuid, + hook: { + ...hookToAdd.hook, + callData: appendDappIdToCallData(hookToAdd.hook.callData, dapp.id), + }, + }, + dappId: dapp.id, } updateHooks((hooks) => { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts index f441010de0..0edeb39916 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts @@ -1,17 +1,15 @@ import { useMemo } from 'react' -import { useWalletInfo } from '@cowprotocol/wallet' - import { useCustomHookDapps } from './useCustomHookDapps' +import { useInternalHookDapps } from './useInternalHookDapps' -import { POST_HOOK_REGISTRY, PRE_HOOK_REGISTRY } from '../hookRegistry' import { HookDapp } from '../types/hooks' export function useAllHookDapps(isPreHook: boolean): HookDapp[] { - const { chainId } = useWalletInfo() + const internalHookDapps = useInternalHookDapps(isPreHook) const customHookDapps = useCustomHookDapps(isPreHook) return useMemo(() => { - return (isPreHook ? PRE_HOOK_REGISTRY : POST_HOOK_REGISTRY)[chainId].concat(customHookDapps) - }, [customHookDapps, chainId]) + return internalHookDapps.concat(customHookDapps) + }, [customHookDapps, internalHookDapps]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts index 8d30ecac43..c6f5e3d27f 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts @@ -5,6 +5,7 @@ import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { setHooksAtom } from '../state/hookDetailsAtom' import { EditHook } from '../types/hooks' +import { appendDappIdToCallData } from '../utils' export function useEditHook(isPreHook: boolean): EditHook { const updateHooks = useSetAtom(setHooksAtom) @@ -18,9 +19,17 @@ export function useEditHook(isPreHook: boolean): EditHook { if (hookIndex < 0) return state const typeState = [...state[type]] + const hookDetails = typeState[hookIndex] + typeState[hookIndex] = { - ...typeState[hookIndex], - hookDetails: update, + ...hookDetails, + hookDetails: { + ...update, + hook: { + ...update.hook, + callData: appendDappIdToCallData(update.hook.callData, hookDetails.dappId), + }, + }, } return { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useInternalHookDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useInternalHookDapps.ts new file mode 100644 index 0000000000..1d374ea50e --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useInternalHookDapps.ts @@ -0,0 +1,26 @@ +import { useMemo } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { ALL_HOOK_DAPPS } from '../hookRegistry' +import { HookDapp } from '../types/hooks' + +export function useInternalHookDapps(isPreHook: boolean): HookDapp[] { + const { chainId } = useWalletInfo() + + return useMemo(() => { + return ALL_HOOK_DAPPS.filter((dapp) => { + const position = dapp?.conditions?.position + const supportedNetworks = dapp?.conditions?.supportedNetworks + + if (supportedNetworks && !supportedNetworks.includes(chainId)) return false + + if (position) { + if (isPreHook && position !== 'pre') return false + if (!isPreHook && position !== 'post') return false + } + + return true + }) + }, [chainId, isPreHook]) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useMatchHooksToDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useMatchHooksToDapps.ts new file mode 100644 index 0000000000..bf06d1604f --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useMatchHooksToDapps.ts @@ -0,0 +1,21 @@ +import { useCallback, useMemo } from 'react' + +import { CowHook, matchHooksToDapps } from '@cowprotocol/hook-dapp-lib' + +import { useAllHookDapps } from './useAllHookDapps' + +export function useMatchHooksToDapps() { + const allPreHookDapps = useAllHookDapps(true) + const allPostHookDapps = useAllHookDapps(false) + + const allHookDapps = useMemo(() => { + return allPreHookDapps.concat(allPostHookDapps) + }, [allPreHookDapps, allPostHookDapps]) + + return useCallback( + (hooks: CowHook[]) => { + return matchHooksToDapps(hooks, allHookDapps) + }, + [allHookDapps], + ) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx index c24890d833..9d0832a918 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx @@ -1,19 +1,20 @@ import { Dispatch, SetStateAction, useEffect } from 'react' -import { HookDappBase, HookDappIframe, HookDappType } from '../../../types/hooks' +import { + HOOK_DAPP_ID_LENGTH, + HookDappBase, + HookDappType, + HookDappWalletCompatibility, +} from '@cowprotocol/hook-dapp-lib' +import { useWalletInfo } from '@cowprotocol/wallet' -interface HookDappConditions { - position?: 'post' | 'pre' - smartContractWalletSupported?: boolean -} +import { HookDappIframe } from '../../../types/hooks' -type HookDappBaseInfo = Omit +type HookDappBaseInfo = Omit -type HookDappManifest = HookDappBaseInfo & { - conditions?: HookDappConditions -} +const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['id', 'name', 'image', 'version', 'website'] -const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['name', 'image', 'version', 'website'] +const isHex = (val: string) => Boolean(val.match(/^[0-9a-f]+$/i)) interface ExternalDappLoaderProps { input: string @@ -32,6 +33,8 @@ export function ExternalDappLoader({ isSmartContractWallet, isPreHook, }: ExternalDappLoaderProps) { + const { chainId } = useWalletInfo() + useEffect(() => { let isRequestRelevant = true @@ -42,7 +45,7 @@ export function ExternalDappLoader({ .then((data) => { if (!isRequestRelevant) return - const { conditions = {}, ...dapp } = data.cow_hook_dapp as HookDappManifest + const { conditions = {}, ...dapp } = data.cow_hook_dapp as HookDappBase if (dapp) { const emptyFields = MANDATORY_DAPP_FIELDS.filter((field) => typeof dapp[field] === 'undefined') @@ -50,8 +53,16 @@ export function ExternalDappLoader({ if (emptyFields.length > 0) { setManifestError(`${emptyFields.join(',')} fields are no set.`) } else { - if (conditions.smartContractWalletSupported === false && isSmartContractWallet === true) { + if ( + isSmartContractWallet === true && + conditions.walletCompatibility && + !conditions.walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT) + ) { setManifestError('The app does not support smart-contract wallets.') + } else if (!isHex(dapp.id) || dapp.id.length !== HOOK_DAPP_ID_LENGTH) { + setManifestError(

Hook dapp id must be a hex with length 64.

) + } else if (conditions.supportedNetworks && !conditions.supportedNetworks.includes(chainId)) { + setManifestError(

This app/hook doesn't support current network (chainId={chainId}).

) } else if (conditions.position === 'post' && isPreHook) { setManifestError(

@@ -92,7 +103,7 @@ export function ExternalDappLoader({ return () => { isRequestRelevant = false } - }, [input]) + }, [input, chainId]) return null } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx index 0107376e85..c292d66ed9 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx @@ -1,11 +1,12 @@ import { useMemo } from 'react' +import { HookDappType, HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { Command } from '@cowprotocol/types' import { HelpTooltip } from '@cowprotocol/ui' import * as styled from './styled' -import { HookDapp, HookDappType, HookDappWalletCompatibility } from '../../types/hooks' +import { HookDapp } from '../../types/hooks' import { HookDetailHeader } from '../HookDetailHeader' interface HookDappDetailsProps { @@ -15,7 +16,8 @@ interface HookDappDetailsProps { export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) { const tags = useMemo(() => { - const { version, website, type, walletCompatibility = [] } = dapp + const { version, website, type, conditions } = dapp + const walletCompatibility = conditions?.walletCompatibility || [] const getWalletCompatibilityTooltip = () => { const isSmartContract = walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts index 1610661e01..d00c525a10 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts @@ -8,7 +8,6 @@ import { walletInfoAtom } from '@cowprotocol/wallet' import { setHooksAtom } from './hookDetailsAtom' import { HookDappIframe } from '../types/hooks' -import { getHookDappId } from '../utils' type CustomHookDapps = Record @@ -69,7 +68,7 @@ export const removeCustomHookDappAtom = atom(null, (get, set, dapp: HookDappIfra [chainId]: currentState, }) - const hookDappId = getHookDappId(dapp) + const hookDappId = dapp.id // Delete applied hooks along with the deleting hook-dapp set(setHooksAtom, (hooksState) => ({ diff --git a/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts b/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts index dea723d383..7091944e34 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts @@ -1,44 +1,22 @@ import type { ReactNode } from 'react' -import type { +import { CowHook, CowHookCreation, HookDappOrderParams, CoWHookDappActions, HookDappContext as GenericHookDappContext, CowHookDetails, + HookDappBase, + HookDappType, } from '@cowprotocol/hook-dapp-lib' import type { Signer } from '@ethersproject/abstract-signer' export type { CowHook, CowHookCreation, HookDappOrderParams } -export enum HookDappType { - INTERNAL = 'INTERNAL', - IFRAME = 'IFRAME', -} - -export enum HookDappWalletCompatibility { - EOA = 'EOA', - SMART_CONTRACT = 'Smart contract', -} - -export interface HookDappBase { - name: string - descriptionShort?: string - description?: ReactNode | string - type: HookDappType - version: string - website: string - image: string - walletCompatibility: HookDappWalletCompatibility[] -} - -export type DappId = `${HookDappType}:::${HookDappBase['name']}` - export interface HookDappInternal extends HookDappBase { type: HookDappType.INTERNAL component: (props: HookDappProps) => ReactNode - walletCompatibility: HookDappWalletCompatibility[] } export interface HookDappIframe extends HookDappBase { @@ -50,7 +28,7 @@ export type HookDapp = HookDappInternal | HookDappIframe export interface CowHookDetailsSerialized { hookDetails: CowHookDetails - dappId: DappId + dappId: string } export type AddHook = CoWHookDappActions['addHook'] diff --git a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts index f3b14c81c5..0d379e5abf 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts @@ -1,21 +1,16 @@ -import { CowHookDetailsSerialized, DappId, HookDapp, HookDappBase, HookDappIframe, HookDappType } from './types/hooks' +import { HookDappType } from '@cowprotocol/hook-dapp-lib' + +import { CowHookDetailsSerialized, HookDapp, HookDappIframe } from './types/hooks' // Do a safe guard assertion that receives a HookDapp and asserts is a HookDappIframe export function isHookDappIframe(dapp: HookDapp): dapp is HookDappIframe { return dapp.type === HookDappType.IFRAME } -export const getHookDappId = (dapp: HookDapp): DappId => `${dapp.type}:::${dapp.name}` -export function parseDappId(id: DappId): Pick { - const [type, name] = id.split(':::') - - return { type: type as HookDappType, name } -} - export function findHookDappById(dapps: HookDapp[], hookDetails: CowHookDetailsSerialized): HookDapp | undefined { - return dapps.find((i) => { - const { type, name } = parseDappId(hookDetails.dappId) + return dapps.find((i) => i.id === hookDetails.dappId) +} - return i.type === type && i.name === name - }) +export function appendDappIdToCallData(callData: string, dappId: string): string { + return callData.endsWith(dappId) ? callData : callData + dappId } diff --git a/apps/hook-dapp-omnibridge/public/manifest.json b/apps/hook-dapp-omnibridge/public/manifest.json index aaf2080e3a..ff04cb43e3 100644 --- a/apps/hook-dapp-omnibridge/public/manifest.json +++ b/apps/hook-dapp-omnibridge/public/manifest.json @@ -23,15 +23,17 @@ "start_url": ".", "theme_color": "#ffffff", "cow_hook_dapp": { + "id": "75716a3cb48fdbb43ebdff58ce6c541f6a2c269be690513131355800367f2da2", "name": "Omnibridge", "descriptionShort": "Bridge from Gnosis Chain to Mainnet", "description": "The Omnibridge can be used to bridge ERC-20 tokens between Ethereum and Gnosis. The first time a token is bridged, a new ERC677 token contract is deployed on GC with an additional suffix to differentiate the token. It will say \"token name on xDai\", as this was the original chain name prior to re-branding. If a token has been bridged previously, the previously deployed contract is used. The requested token amount is minted and sent to the account initiating the transfer (or an alternative receiver account specified by the sender).", "version": "0.0.1", "website": "https://omni.legacy.gnosischain.com", - "image": "http://localhost:3000/hook-dapp-omnibridge/apple-touch-icon.png", + "image": "http://localhost:4317/hook-dapp-omnibridge/apple-touch-icon.png", "conditions": { "position": "post", - "smartContractWalletSupported": false + "smartContractWalletSupported": false, + "supportedNetworks": [100] } } } diff --git a/libs/hook-dapp-lib/package.json b/libs/hook-dapp-lib/package.json index dbec39ed41..808cf0db50 100644 --- a/libs/hook-dapp-lib/package.json +++ b/libs/hook-dapp-lib/package.json @@ -21,7 +21,6 @@ "hook-dapp-lib" ], "dependencies": { - "@cowprotocol/cow-sdk": "^5.4.1", "@cowprotocol/iframe-transport": "^1.0.0" } } diff --git a/libs/hook-dapp-lib/src/consts.ts b/libs/hook-dapp-lib/src/consts.ts new file mode 100644 index 0000000000..b09a9ce29d --- /dev/null +++ b/libs/hook-dapp-lib/src/consts.ts @@ -0,0 +1,11 @@ +export enum HookDappType { + INTERNAL = 'INTERNAL', + IFRAME = 'IFRAME', +} + +export enum HookDappWalletCompatibility { + EOA = 'EOA', + SMART_CONTRACT = 'SMART_CONTRACT', +} + +export const HOOK_DAPP_ID_LENGTH = 64 diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json new file mode 100644 index 0000000000..ce98edb976 --- /dev/null +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -0,0 +1,53 @@ +{ + "BUILD_CUSTOM_HOOK": { + "id": "c768665aa144bcf18c14eea0249b6322050e5daeba046d7e94df743a2e504586", + "type": "INTERNAL", + "name": "Build your own hook", + "descriptionShort": "Call any smart contract with your own parameters", + "description": "Didn't find a suitable hook? You can always create your own! To do this, you need to specify which smart contract you want to call, the parameters for the call and the gas limit.", + "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/refs/heads/develop/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/build.png", + "version": "v0.1.0", + "website": "https://docs.cow.fi/cow-protocol/tutorials/hook-dapp" + }, + "CLAIM_GNO_FROM_VALIDATORS": { + "id": "ee4a6b1065cda592972b9ff7448ec111f29a566f137fef101ead7fbf8b01dd0b", + "type": "INTERNAL", + "name": "Claim GNO from validators", + "descriptionShort": "Withdraw rewards from your Gnosis validators.", + "description": "This hook allows you to withdraw rewards from your Gnosis Chain validators through CoW Swap. It automates the process of interacting with the Gnosis Deposit Contract, enabling you to claim any available rewards directly to your specified withdrawal address. The hook monitors your validator's accrued rewards and triggers the claimWithdrawals function when rewards are ready for withdrawal. This simplifies the management of Gnosis validator earnings without requiring ready for withdrawal. This simplifies the management of Gnosis validator earnings without requiring manual contract interaction, providing a smoother and more efficient experience for users.", + "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/897ce91ca60a6b2d3823e6a002c3bf64c5384afe/libs/assets/src/cow-swap/network-gnosis-chain-logo.svg", + "version": "v0.1.1", + "website": "https://www.gnosis.io", + "conditions": { + "supportedNetworks": [100], + "position": "pre" + } + }, + "PERMIT_TOKEN": { + "id": "1db4bacb661a90fb6b475fd5b585acba9745bc373573c65ecc3e8f5bfd5dee1f", + "type": "INTERNAL", + "name": "Permit a token", + "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", + "website": "https://docs.cow.fi/cow-protocol/tutorials/hook-dapp", + "conditions": { + "walletCompatibility": ["EOA"], + "supportedNetworks": [11155111] + } + }, + "CLAIM_COW_AIRDROP": { + "id": "40ed08569519f3b58c410ba35a8e684612663a7c9b58025e0a9c3a54551fb0ff", + "type": "INTERNAL", + "name": "Claim COW Airdrop", + "descriptionShort": "Retrieve COW tokens before or after a swap.", + "description": "Effortless Airdrop Claims! The Claim COW Airdrop feature simplifies the process of collecting free COW tokens before or after your swap, seamlessly integrating into the CoW Swap platform. Whether you're claiming new airdrops or exploring CoW on a new network, this tool ensures you get your rewards quickly and easily.", + "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/897ce91ca60a6b2d3823e6a002c3bf64c5384afe/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/airdrop.svg", + "version": "v0.1.0", + "website": "https://github.com/bleu/cow-airdrop-contract-deployer", + "conditions": { + "supportedNetworks": [11155111] + } + } +} diff --git a/libs/hook-dapp-lib/src/index.ts b/libs/hook-dapp-lib/src/index.ts index f5d3fe208d..aecd63a181 100644 --- a/libs/hook-dapp-lib/src/index.ts +++ b/libs/hook-dapp-lib/src/index.ts @@ -1,3 +1,7 @@ export { initCoWHookDapp } from './initCoWHookDapp' export * from './hookDappIframeTransport' export * from './types' +export * from './consts' +export * from './utils' +import * as hookDappsRegistry from './hookDappsRegistry.json' +export { hookDappsRegistry } diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index 32201d1725..da26daa099 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -1,4 +1,6 @@ -import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import type { ReactNode } from 'react' + +import { HookDappType, HookDappWalletCompatibility } from './consts' export interface CowHook { target: string @@ -6,6 +8,12 @@ export interface CowHook { gasLimit: string } +export interface HookDappConditions { + position?: 'post' | 'pre' + walletCompatibility?: HookDappWalletCompatibility[] + supportedNetworks?: number[] +} + export interface CowHookCreation { hook: CowHook recipientOverride?: string @@ -33,7 +41,7 @@ export interface HookDappOrderParams { } export interface HookDappContext { - chainId: SupportedChainId + chainId: number account?: string orderParams: HookDappOrderParams | null hookToEdit?: CowHookDetails @@ -41,3 +49,15 @@ export interface HookDappContext { isPreHook: boolean isDarkMode: boolean } + +export interface HookDappBase { + id: string + name: string + descriptionShort?: string + description?: ReactNode | string + type: HookDappType + version: string + website: string + image: string + conditions?: HookDappConditions +} diff --git a/libs/hook-dapp-lib/src/utils.ts b/libs/hook-dapp-lib/src/utils.ts new file mode 100644 index 0000000000..4cbfc1a902 --- /dev/null +++ b/libs/hook-dapp-lib/src/utils.ts @@ -0,0 +1,27 @@ +import { HOOK_DAPP_ID_LENGTH } from './consts' +import * as hookDappsRegistry from './hookDappsRegistry.json' +import { CowHook, HookDappBase } from './types' + +export interface HookToDappMatch { + dapp: HookDappBase | null + hook: CowHook +} + +export function matchHooksToDapps(hooks: CowHook[], dapps: HookDappBase[]): HookToDappMatch[] { + const dappsMap = dapps.reduce( + (acc, dapp) => { + acc[dapp.id] = dapp + return acc + }, + {} as Record, + ) + + return hooks.map((hook) => ({ + hook, + dapp: dappsMap[hook.callData.slice(-HOOK_DAPP_ID_LENGTH)] || null, + })) +} + +export function matchHooksToDappsRegistry(hooks: CowHook[]): HookToDappMatch[] { + return matchHooksToDapps(hooks, Object.values(hookDappsRegistry) as HookDappBase[]) +} From 9c364bd81f2e392a8cece06f6470734ee3d7623c Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 2 Oct 2024 18:46:08 +0500 Subject: [PATCH 003/116] feat(explorer): display order hooks details (#4921) --- .../src/components/AppData/DecodeAppData.tsx | 111 +++--------------- .../components/orders/DetailsTable/index.tsx | 15 +++ .../OrderHooksDetails/HookItem/index.tsx | 20 ++++ .../OrderHooksDetails/HookItem/styled.tsx | 19 +++ .../orders/OrderHooksDetails/index.tsx | 55 +++++++++ .../orders/OrderHooksDetails/styled.tsx | 7 ++ apps/explorer/src/hooks/useAppData.ts | 96 ++++++++++----- apps/explorer/tsconfig.json | 2 + libs/hook-dapp-lib/package.json | 2 +- 9 files changed, 200 insertions(+), 127 deletions(-) create mode 100644 apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx create mode 100644 apps/explorer/src/components/orders/OrderHooksDetails/HookItem/styled.tsx create mode 100644 apps/explorer/src/components/orders/OrderHooksDetails/index.tsx create mode 100644 apps/explorer/src/components/orders/OrderHooksDetails/styled.tsx diff --git a/apps/explorer/src/components/AppData/DecodeAppData.tsx b/apps/explorer/src/components/AppData/DecodeAppData.tsx index 7c3d9b692f..8532bc352b 100644 --- a/apps/explorer/src/components/AppData/DecodeAppData.tsx +++ b/apps/explorer/src/components/AppData/DecodeAppData.tsx @@ -1,114 +1,33 @@ -import { useCallback, useEffect } from 'react' - -import { AnyAppDataDocVersion } from '@cowprotocol/app-data' +import { useState } from 'react' import AppDataWrapper from 'components/common/AppDataWrapper' import { RowWithCopyButton } from 'components/common/RowWithCopyButton' import Spinner from 'components/common/Spinner' import { Notification } from 'components/Notification' -import { DEFAULT_IPFS_READ_URI, IPFS_INVALID_APP_IDS } from 'const' -import { appDataHexToCid, fetchDocFromAppDataHex } from 'hooks/useAppData' -import useSafeState from 'hooks/useSafeState' +import { useAppData } from 'hooks/useAppData' import styled from 'styled-components/macro' -import { decodeFullAppData } from 'utils/decodeFullAppData' - type Props = { appData: string fullAppData?: string showExpanded?: boolean } -async function _getDecodedAppData( - appData: string, - isLegacyAppDataHex: boolean, - fullAppData?: string, -): Promise<{ decodedAppData?: void | AnyAppDataDocVersion; isError: boolean }> { - // If the full appData is available, we try to parse it as JSON - if (fullAppData) { - try { - const decodedAppData = decodeFullAppData(fullAppData, true) - return { decodedAppData, isError: false } - } catch (error) { - console.error('Error parsing fullAppData from the API', { fullAppData }, error) - } - } - - if (IPFS_INVALID_APP_IDS.includes(appData.toString())) { - return { isError: true } - } - - const decodedAppData = await fetchDocFromAppDataHex(appData.toString(), isLegacyAppDataHex) - return { isError: false, decodedAppData } -} - const DecodeAppData = (props: Props): React.ReactNode => { const { appData, showExpanded = false, fullAppData } = props - const [appDataLoading, setAppDataLoading] = useSafeState(false) - const [appDataError, setAppDataError] = useSafeState(false) - const [decodedAppData, setDecodedAppData] = useSafeState(undefined) - const [ipfsUri, setIpfsUri] = useSafeState('') - - const [showDecodedAppData, setShowDecodedAppData] = useSafeState(showExpanded) + const { + isLoading: appDataLoading, + appDataDoc: decodedAppData, + ipfsUri, + hasError: appDataError, + } = useAppData(appData, fullAppData) - // Old AppData use a different way to derive the CID (we know is old if fullAppData is not available) const isLegacyAppDataHex = fullAppData === undefined - - useEffect(() => { - const fetchIPFS = async (): Promise => { - try { - const cid = await appDataHexToCid(appData.toString(), isLegacyAppDataHex) - setIpfsUri(`${DEFAULT_IPFS_READ_URI}/${cid}`) - } catch { - setAppDataError(true) - } - } - - fetchIPFS() - }, [appData, setAppDataError, setIpfsUri, isLegacyAppDataHex]) - - const handleDecodedAppData = useCallback( - async (isOpen?: boolean): Promise => { - if (!isOpen) { - setShowDecodedAppData(!showDecodedAppData) - } - if (decodedAppData) return - - setAppDataLoading(true) - try { - const { isError, decodedAppData } = await _getDecodedAppData(appData, isLegacyAppDataHex, fullAppData) - if (isError) { - setAppDataError(true) - } else { - setDecodedAppData(decodedAppData) - } - } catch { - setDecodedAppData(undefined) - setAppDataError(true) - } finally { - setAppDataLoading(false) - } - }, - [ - appData, - fullAppData, - decodedAppData, - setAppDataError, - setAppDataLoading, - setDecodedAppData, - setShowDecodedAppData, - showDecodedAppData, - isLegacyAppDataHex, - ], - ) - - useEffect(() => { - if (showExpanded) { - handleDecodedAppData(showExpanded) - } - }, [showExpanded, handleDecodedAppData]) + const [showDecodedAppData, setShowDecodedAppData] = useState(showExpanded) const renderAppData = (): React.ReactNode | null => { + const appDataString = JSON.stringify(decodedAppData, null, 2) + if (appDataLoading) return if (showDecodedAppData) { if (appDataError) @@ -122,8 +41,8 @@ const DecodeAppData = (props: Props): React.ReactNode => { ) return ( {JSON.stringify(decodedAppData, null, 2)}} + textToCopy={appDataString} + contentsToDisplay={

{appDataString}
} /> ) } @@ -137,7 +56,7 @@ const DecodeAppData = (props: Props): React.ReactNode => { {appData} ) : isLegacyAppDataHex ? ( {appData} @@ -150,7 +69,7 @@ const DecodeAppData = (props: Props): React.ReactNode => { appData )}   - => handleDecodedAppData(false)}> + setShowDecodedAppData((state) => !state)}> {showDecodedAppData ? '[-] Show less' : '[+] Show more'} diff --git a/apps/explorer/src/components/orders/DetailsTable/index.tsx b/apps/explorer/src/components/orders/DetailsTable/index.tsx index cf66f3a58b..08756c8a4c 100644 --- a/apps/explorer/src/components/orders/DetailsTable/index.tsx +++ b/apps/explorer/src/components/orders/DetailsTable/index.tsx @@ -30,6 +30,8 @@ import { capitalize } from 'utils' import { Order } from 'api/operator' import { getUiOrderType } from 'utils/getUiOrderType' +import { OrderHooksDetails } from '../OrderHooksDetails' + const tooltip = { orderID: 'A unique identifier ID for this order.', from: 'The account address which signed the order.', @@ -38,6 +40,7 @@ const tooltip = { appData: 'The AppData hash for this order. It can denote encoded metadata with info on the app, environment and more, although not all interfaces follow the same pattern. Show more will try to decode that information.', status: 'The order status is either Open, Filled, Expired or Canceled.', + hooks: 'Hooks are interactions before/after order execution.', submission: 'The date and time at which the order was submitted. The timezone is based on the browser locale settings.', expiration: @@ -413,6 +416,18 @@ export function DetailsTable(props: Props): React.ReactNode | null { + + {(content) => ( + + + + Hooks + + + {content} + + )} + diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx new file mode 100644 index 0000000000..ceb64112af --- /dev/null +++ b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx @@ -0,0 +1,20 @@ +import { HookToDappMatch } from '@cowprotocol/hook-dapp-lib' + +import { Item, Wrapper } from './styled' + +export function HookItem({ item }: { item: HookToDappMatch }) { + return ( + + {item.dapp ? ( + + {item.dapp.name} +

+ {item.dapp.name} ({item.dapp.version}) +

+
+ ) : ( +
Unknown hook dapp
+ )} +
+ ) +} diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/styled.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/styled.tsx new file mode 100644 index 0000000000..1a16cca1a6 --- /dev/null +++ b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/styled.tsx @@ -0,0 +1,19 @@ +import styled from 'styled-components/macro' + +export const Item = styled.li` + list-style: none; + margin: 0; + padding: 0; +` + +export const Wrapper = styled.a` + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; + + > img { + width: 30px; + height: 30px; + } +` diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx new file mode 100644 index 0000000000..d7a1bf543b --- /dev/null +++ b/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx @@ -0,0 +1,55 @@ +import { ReactElement } from 'react' + +import { latest } from '@cowprotocol/app-data' +import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' + +import { HookItem } from './HookItem' +import { HooksList } from './styled' + +import { useAppData } from '../../../hooks/useAppData' + +interface OrderHooksDetailsProps { + appData: string + fullAppData: string | undefined + children: (content: ReactElement) => ReactElement +} + +export function OrderHooksDetails({ appData, fullAppData, children }: OrderHooksDetailsProps) { + const { appDataDoc } = useAppData(appData, fullAppData) + + if (!appDataDoc) return null + + const metadata = appDataDoc.metadata as latest.Metadata + + const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || []) + const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || []) + + return children( + <> + + + , + ) +} + +interface HooksInfoProps { + data: HookToDappMatch[] + title: string +} + +function HooksInfo({ data, title }: HooksInfoProps) { + return ( + <> + {data.length && ( +
+

{title}

+ + {data.map((item) => { + return + })} + +
+ )} + + ) +} diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/styled.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/styled.tsx new file mode 100644 index 0000000000..660fe8c1e3 --- /dev/null +++ b/apps/explorer/src/components/orders/OrderHooksDetails/styled.tsx @@ -0,0 +1,7 @@ +import styled from 'styled-components/macro' + +export const HooksList = styled.ul` + margin: 0; + padding: 0; + padding-left: 10px; +` diff --git a/apps/explorer/src/hooks/useAppData.ts b/apps/explorer/src/hooks/useAppData.ts index bb4e771530..f406c68e7c 100644 --- a/apps/explorer/src/hooks/useAppData.ts +++ b/apps/explorer/src/hooks/useAppData.ts @@ -1,41 +1,53 @@ -import { useEffect, useMemo, useState } from 'react' - import { AnyAppDataDocVersion } from '@cowprotocol/app-data' -import { DEFAULT_IPFS_READ_URI } from 'const' +import { DEFAULT_IPFS_READ_URI, IPFS_INVALID_APP_IDS } from 'const' import { metadataApiSDK } from 'cowSdk' -import { useNetworkId } from 'state/network' - -export const useAppData = ( - appDataHash: string, - isLegacyAppDataHex: boolean -): { isLoading: boolean; appDataDoc: AnyAppDataDocVersion | void | undefined } => { - const network = useNetworkId() || undefined - const [isLoading, setLoading] = useState(false) - const [appDataDoc, setAppDataDoc] = useState() - useEffect(() => { - async function getAppDataDoc(): Promise { - setLoading(true) - try { - const decodedAppData = await fetchDocFromAppDataHex(appDataHash, isLegacyAppDataHex) - setAppDataDoc(decodedAppData) - } catch (e) { - const msg = `Failed to fetch appData document` - console.error(msg, e) - } finally { - setLoading(false) - setAppDataDoc(undefined) - } - } - getAppDataDoc() - }, [appDataHash, network, isLegacyAppDataHex]) +import useSWR from 'swr' + +import { decodeFullAppData } from '../utils/decodeFullAppData' + +interface AppDataDecodingResult { + isLoading: boolean + appDataDoc: AnyAppDataDocVersion | undefined + hasError: boolean + ipfsUri: string | undefined +} + +export const useAppData = (appData: string, fullAppData?: string): AppDataDecodingResult => { + // Old AppData use a different way to derive the CID (we know is old if fullAppData is not available) + const isLegacyAppDataHex = fullAppData === undefined + + const { + error: ipfsError, + isLoading: isIpfsLoading, + data: ipfsUri, + } = useSWR(['appDataHexToCid', appData, isLegacyAppDataHex], async ([_, appData, isLegacyAppDataHex]) => { + const cid = await appDataHexToCid(appData.toString(), isLegacyAppDataHex) + + return `${DEFAULT_IPFS_READ_URI}/${cid}` + }) - return useMemo(() => ({ isLoading, appDataDoc }), [isLoading, appDataDoc]) + const { + error, + isLoading, + data: appDataDoc, + } = useSWR( + ['getDecodedAppData', appData, fullAppData, isLegacyAppDataHex], + async ([_, appData, fullAppData, isLegacyAppDataHex]) => { + const { error, decodedAppData } = await getDecodedAppData(appData, isLegacyAppDataHex, fullAppData) + + if (error) throw error + + return decodedAppData || undefined + }, + ) + + return { isLoading: isLoading || isIpfsLoading, hasError: !!(ipfsError || error), appDataDoc, ipfsUri } } export const fetchDocFromAppDataHex = ( appDataHex: string, - isLegacyAppDataHex: boolean + isLegacyAppDataHex: boolean, ): Promise => { const method = isLegacyAppDataHex ? 'fetchDocFromAppDataHexLegacy' : 'fetchDocFromAppDataHex' return metadataApiSDK[method](appDataHex, DEFAULT_IPFS_READ_URI) @@ -45,3 +57,27 @@ export const appDataHexToCid = (appDataHash: string, isLegacyAppDataHex: boolean const method = isLegacyAppDataHex ? 'appDataHexToCidLegacy' : 'appDataHexToCid' return metadataApiSDK[method](appDataHash) } + +async function getDecodedAppData( + appData: string, + isLegacyAppDataHex: boolean, + fullAppData?: string, +): Promise<{ decodedAppData?: void | AnyAppDataDocVersion; error?: Error }> { + // If the full appData is available, we try to parse it as JSON + if (fullAppData) { + try { + const decodedAppData = decodeFullAppData(fullAppData, true) + return { decodedAppData } + } catch (error) { + console.error('Error parsing fullAppData from the API', { fullAppData }, error) + return { error } + } + } + + if (IPFS_INVALID_APP_IDS.includes(appData.toString())) { + return { error: new Error('Invalid app id') } + } + + const decodedAppData = await fetchDocFromAppDataHex(appData.toString(), isLegacyAppDataHex) + return { decodedAppData } +} diff --git a/apps/explorer/tsconfig.json b/apps/explorer/tsconfig.json index 00f80a13a9..e897af0bf8 100644 --- a/apps/explorer/tsconfig.json +++ b/apps/explorer/tsconfig.json @@ -21,6 +21,8 @@ "@cowprotocol/analytics": ["../../../libs/analytics/src/index.ts"], "@cowprotocol/assets": ["../../../libs/assets/src/index.ts"], "@cowprotocol/types": ["../../../libs/types/src/index.ts"], + "@cowprotocol/hook-dapp-lib": ["../../../libs/hook-dapp-lib/src/index.ts"], + "@cowprotocol/iframe-transport": ["../../../libs/iframe-transport/src/index.ts"], "@cowprotocol/wallet": ["../../../libs/wallet/src/index.ts"] } }, diff --git a/libs/hook-dapp-lib/package.json b/libs/hook-dapp-lib/package.json index 808cf0db50..96aae7a478 100644 --- a/libs/hook-dapp-lib/package.json +++ b/libs/hook-dapp-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/hook-dapp-lib", - "version": "1.0.0", + "version": "1.0.0-RC1", "type": "commonjs", "description": "CoW Swap Hook Dapp Library. Allows you to develop pre/post hooks dapps for CoW Protocol.", "main": "index.js", From 302fa35387ddd1b9647063baea1fef47582dddeb Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 3 Oct 2024 17:10:23 +0500 Subject: [PATCH 004/116] chore: change airdrop contract (#4939) --- .../src/modules/hooksStore/dapps/AirdropHookApp/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/index.tsx index 01f3453338..2225379cbb 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/index.tsx @@ -18,7 +18,7 @@ const COW_AIRDROP = { name: 'COW', dataBaseUrl: 'https://raw.githubusercontent.com/bleu/cow-airdrop-contract-deployer/example/mock-airdrop-data/', chainId: SupportedChainId.SEPOLIA, - address: '0x0D6361f70f54b0e63A34D3F2D2C2552a21F100Fc', + address: '0x06Ca512F7d35A35Dfa49aa69F12cFB2a9166a95b', token: TokenWithLogo.fromToken( new Token(cowSepolia.chainId, cowSepolia.address, cowSepolia.decimals, cowSepolia.symbol, cowSepolia.name), cowSepolia.logoURI, From 1e776fc4f6dfb28eebf881e79bb45dbfd693e472 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 4 Oct 2024 13:02:28 +0500 Subject: [PATCH 005/116] feat(swap): display order hooks details (#4925) --- .../OrderHooksDetails/HookItem/index.tsx | 20 ++ .../OrderHooksDetails/HookItem/styled.tsx | 19 ++ .../containers/OrderHooksDetails/index.tsx | 77 ++++++++ .../containers/OrderHooksDetails/styled.tsx | 33 ++++ .../Transaction/ActivityDetails.tsx | 15 +- .../src/modules/appData/index.ts | 1 + .../containers/HookDappContainer/index.tsx | 1 + .../containers/HooksStoreWidget/index.tsx | 2 + .../hooksStore/hooks/useAddCustomHookDapp.ts | 4 +- .../CustomDappLoader/index.tsx | 67 ++----- .../hooksStore/state/customHookDappsAtom.ts | 2 +- .../updaters/iframeDappsManifestUpdater.tsx | 81 ++++++++ .../hooksStore/validateHookDappManifest.tsx | 55 ++++++ .../ConfirmSwapModalSetup/index.tsx | 14 +- .../swap/containers/SwapUpdaters/index.tsx | 9 +- .../hooks/useBaseSafeBundleFlowContext.ts | 8 +- .../modules/swap/hooks/useEthFlowContext.ts | 15 +- .../src/modules/swap/hooks/useFlowContext.ts | 180 ++---------------- .../hooks/useSafeBundleApprovalFlowContext.ts | 3 +- .../swap/hooks/useSafeBundleEthFlowContext.ts | 3 +- .../modules/swap/hooks/useSwapFlowContext.ts | 36 +--- .../src/modules/swap/services/types.ts | 3 +- .../swap/state/baseFlowContextSourceAtom.ts | 5 + .../src/modules/swap/types/flowContext.ts | 51 +++++ .../swap/updaters/BaseFlowContextUpdater.tsx | 147 ++++++++++++++ .../trade/pure/TradeConfirmation/index.tsx | 15 ++ libs/hook-dapp-lib/src/utils.ts | 27 ++- 27 files changed, 619 insertions(+), 274 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx create mode 100644 apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx create mode 100644 apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx create mode 100644 apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/validateHookDappManifest.tsx create mode 100644 apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/types/flowContext.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx new file mode 100644 index 0000000000..ceb64112af --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx @@ -0,0 +1,20 @@ +import { HookToDappMatch } from '@cowprotocol/hook-dapp-lib' + +import { Item, Wrapper } from './styled' + +export function HookItem({ item }: { item: HookToDappMatch }) { + return ( + + {item.dapp ? ( + + {item.dapp.name} +

+ {item.dapp.name} ({item.dapp.version}) +

+
+ ) : ( +
Unknown hook dapp
+ )} +
+ ) +} diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx new file mode 100644 index 0000000000..1a16cca1a6 --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx @@ -0,0 +1,19 @@ +import styled from 'styled-components/macro' + +export const Item = styled.li` + list-style: none; + margin: 0; + padding: 0; +` + +export const Wrapper = styled.a` + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; + + > img { + width: 30px; + height: 30px; + } +` diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx new file mode 100644 index 0000000000..abf8be100a --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -0,0 +1,77 @@ +import { ReactElement, useMemo, useState } from 'react' + +import { latest } from '@cowprotocol/app-data' +import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' + +import { ChevronDown, ChevronUp } from 'react-feather' + +import { AppDataInfo, decodeAppData } from 'modules/appData' + +import { HookItem } from './HookItem' +import { HooksList, InfoWrapper, ToggleButton, Wrapper } from './styled' + +interface OrderHooksDetailsProps { + appData: string | AppDataInfo + children: (content: ReactElement) => ReactElement +} + +export function OrderHooksDetails({ appData, children }: OrderHooksDetailsProps) { + const [isOpen, setOpen] = useState(false) + const appDataDoc = useMemo(() => { + return typeof appData === 'string' ? decodeAppData(appData) : appData.doc + }, [appData]) + + if (!appDataDoc) return null + + const metadata = appDataDoc.metadata as latest.Metadata + + const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || []) + const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || []) + + if (!preHooksToDapp.length && !postHooksToDapp.length) return null + + return children( + isOpen ? ( + + setOpen(false)}> + + + + + + ) : ( + + + {preHooksToDapp.length ? `Pre: ${preHooksToDapp.length}` : ''} + {preHooksToDapp.length && postHooksToDapp.length ? ' | ' : ''} + {postHooksToDapp.length ? `Post: ${postHooksToDapp.length}` : ''} + + setOpen(true)}> + + + + ), + ) +} + +interface HooksInfoProps { + data: HookToDappMatch[] + title: string +} + +function HooksInfo({ data, title }: HooksInfoProps) { + return ( + <> + {data.length ? ( + +

{title}

+ + {data.map((item) => { + return + })} + +
+ ) : null} + + ) +} diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx new file mode 100644 index 0000000000..c7a331922b --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components/macro' + +export const Wrapper = styled.div` + position: relative; + padding-right: 30px; +` + +export const HooksList = styled.ul` + margin: 0; + padding: 0; + padding-left: 10px; +` + +export const ToggleButton = styled.button` + cursor: pointer; + background: none; + border: 0; + outline: 0; + padding: 0; + margin: 0; + position: absolute; + right: 0; + top: -4px; + + &:hover { + opacity: 0.7; + } +` +export const InfoWrapper = styled.div` + h3 { + margin: 0; + } +` diff --git a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx index 927d342abc..89a101695b 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx @@ -25,6 +25,7 @@ import { useToggleAccountModal } from 'modules/account' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { EthFlowStepper } from 'modules/swap/containers/EthFlowStepper' +import { OrderHooksDetails } from 'common/containers/OrderHooksDetails' import { useCancelOrder } from 'common/hooks/useCancelOrder' import { isPending } from 'common/hooks/useCategorizeRecentActivity' import { useGetSurplusData } from 'common/hooks/useGetSurplusFiatValue' @@ -193,6 +194,7 @@ export function ActivityDetails(props: { const getShowCancellationModal = useCancelOrder() const isSwap = order && getUiOrderType(order) === UiOrderType.SWAP + const appData = !!order && order.fullAppData const { disableProgressBar } = useInjectedWidgetParams() @@ -390,9 +392,20 @@ export function ActivityDetails(props: { )} + + {appData && ( + + {(children) => ( + + Hooks + {children} + + )} + + )} ) : ( - summary ?? id + (summary ?? id) )} {activityLinkUrl && enhancedTransaction?.replacementType !== 'replaced' && ( diff --git a/apps/cowswap-frontend/src/modules/appData/index.ts b/apps/cowswap-frontend/src/modules/appData/index.ts index 2d10f8bb41..da9816331c 100644 --- a/apps/cowswap-frontend/src/modules/appData/index.ts +++ b/apps/cowswap-frontend/src/modules/appData/index.ts @@ -2,6 +2,7 @@ export { getAppData } from './utils/fullAppData' export * from './updater/AppDataUpdater' export { useAppData, useAppDataHooks, useUploadAppData } from './hooks' export { filterPermitSignerPermit } from './utils/appDataFilter' +export { decodeAppData } from './utils/decodeAppData' export { replaceHooksOnAppData, buildAppData, removePermitHookFromAppData } from './utils/buildAppData' export { buildAppDataHooks } from './utils/buildAppDataHooks' export * from './utils/getAppDataHooks' diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index 339bc4aa22..ae3815e1b5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -77,6 +77,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho tradeNavigate, inputCurrencyId, outputCurrencyId, + isDarkMode, ]) const dappProps = useMemo(() => ({ context, dapp, isPreHook }), [context, dapp, isPreHook]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index d0e1862643..7cb991d41a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -10,6 +10,7 @@ import { useIsSellNative } from 'modules/trade' import { useSetRecipientOverride } from '../../hooks/useSetRecipientOverride' import { useSetupHooksStoreOrderParams } from '../../hooks/useSetupHooksStoreOrderParams' +import { IframeDappsManifestUpdater } from '../../updaters/iframeDappsManifestUpdater' import { HookRegistryList } from '../HookRegistryList' import { PostHookButton } from '../PostHookButton' import { PreHookButton } from '../PreHookButton' @@ -81,6 +82,7 @@ export function HooksStoreWidget() { + {isHookSelectionOpen && ( )} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts index d0998491d9..51623a63b0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts @@ -1,11 +1,11 @@ import { useSetAtom } from 'jotai' import { useCallback } from 'react' -import { addCustomHookDappAtom } from '../state/customHookDappsAtom' +import { upsertCustomHookDappAtom } from '../state/customHookDappsAtom' import { HookDappIframe } from '../types/hooks' export function useAddCustomHookDapp(isPreHook: boolean) { - const setState = useSetAtom(addCustomHookDappAtom) + const setState = useSetAtom(upsertCustomHookDappAtom) return useCallback( (dapp: HookDappIframe) => { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx index 9d0832a918..9d904c1d9e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx @@ -1,20 +1,10 @@ import { Dispatch, SetStateAction, useEffect } from 'react' -import { - HOOK_DAPP_ID_LENGTH, - HookDappBase, - HookDappType, - HookDappWalletCompatibility, -} from '@cowprotocol/hook-dapp-lib' +import { HookDappBase, HookDappType } from '@cowprotocol/hook-dapp-lib' import { useWalletInfo } from '@cowprotocol/wallet' import { HookDappIframe } from '../../../types/hooks' - -type HookDappBaseInfo = Omit - -const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['id', 'name', 'image', 'version', 'website'] - -const isHex = (val: string) => Boolean(val.match(/^[0-9a-f]+$/i)) +import { validateHookDappManifest } from '../../../validateHookDappManifest' interface ExternalDappLoaderProps { input: string @@ -45,47 +35,24 @@ export function ExternalDappLoader({ .then((data) => { if (!isRequestRelevant) return - const { conditions = {}, ...dapp } = data.cow_hook_dapp as HookDappBase + const dapp = data.cow_hook_dapp as HookDappBase - if (dapp) { - const emptyFields = MANDATORY_DAPP_FIELDS.filter((field) => typeof dapp[field] === 'undefined') + const validationError = validateHookDappManifest( + data.cow_hook_dapp as HookDappBase, + chainId, + isPreHook, + isSmartContractWallet, + ) - if (emptyFields.length > 0) { - setManifestError(`${emptyFields.join(',')} fields are no set.`) - } else { - if ( - isSmartContractWallet === true && - conditions.walletCompatibility && - !conditions.walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT) - ) { - setManifestError('The app does not support smart-contract wallets.') - } else if (!isHex(dapp.id) || dapp.id.length !== HOOK_DAPP_ID_LENGTH) { - setManifestError(

Hook dapp id must be a hex with length 64.

) - } else if (conditions.supportedNetworks && !conditions.supportedNetworks.includes(chainId)) { - setManifestError(

This app/hook doesn't support current network (chainId={chainId}).

) - } else if (conditions.position === 'post' && isPreHook) { - setManifestError( -

- This app/hook can only be used as a post-hook and cannot be added as a pre-hook. -

, - ) - } else if (conditions.position === 'pre' && !isPreHook) { - setManifestError( -

- This app/hook can only be used as a pre-hook and cannot be added as a post-hook. -

, - ) - } else { - setManifestError(null) - setDappInfo({ - ...dapp, - type: HookDappType.IFRAME, - url: input, - }) - } - } + if (validationError) { + setManifestError(validationError) } else { - setManifestError('Manifest does not contain "cow_hook_dapp" property.') + setManifestError(null) + setDappInfo({ + ...dapp, + type: HookDappType.IFRAME, + url: input, + }) } }) .catch((error) => { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts index d00c525a10..eb0abc8cf6 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts @@ -39,7 +39,7 @@ export const customPostHookDappsAtom = atom((get) => { return Object.values(get(customHookDappsAtom).post) as HookDappIframe[] }) -export const addCustomHookDappAtom = atom(null, (get, set, isPreHook: boolean, dapp: HookDappIframe) => { +export const upsertCustomHookDappAtom = atom(null, (get, set, isPreHook: boolean, dapp: HookDappIframe) => { const { chainId } = get(walletInfoAtom) const state = get(customHookDappsInner) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx b/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx new file mode 100644 index 0000000000..6a5d950898 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx @@ -0,0 +1,81 @@ +import { useSetAtom } from 'jotai' +import { useAtomValue } from 'jotai/index' +import { useCallback, useEffect, useMemo } from 'react' + +import { HookDappBase, HookDappType } from '@cowprotocol/hook-dapp-lib' +import { useWalletInfo } from '@cowprotocol/wallet' + +import ms from 'ms.macro' + +import { customHookDappsAtom, upsertCustomHookDappAtom } from '../state/customHookDappsAtom' +import { validateHookDappManifest } from '../validateHookDappManifest' + +const UPDATE_TIME_KEY = 'HOOK_DAPPS_UPDATE_TIME' +const HOOK_DAPPS_UPDATE_INTERVAL = ms`6h` + +const getLastUpdateTimestamp = () => { + const lastUpdate = localStorage.getItem(UPDATE_TIME_KEY) + return lastUpdate ? +lastUpdate : null +} + +export function IframeDappsManifestUpdater() { + const hooksState = useAtomValue(customHookDappsAtom) + const upsertCustomHookDapp = useSetAtom(upsertCustomHookDappAtom) + const { chainId } = useWalletInfo() + + const [preHooks, postHooks] = useMemo( + () => [Object.values(hooksState.pre), Object.values(hooksState.post)], + [hooksState], + ) + + const fetchAndUpdateHookDapp = useCallback( + (url: string, isPreHook: boolean) => { + return fetch(`${url}/manifest.json`) + .then((res) => res.json()) + .then((data) => { + const dapp = data.cow_hook_dapp as HookDappBase + + // Don't pass parameters that are not needed for validation + // In order to skip validation of the already added hook-dapp + const validationError = validateHookDappManifest( + data.cow_hook_dapp as HookDappBase, + undefined, + undefined, + undefined, + ) + + if (validationError) { + console.error('Cannot update iframe hook dapp:', validationError) + } else { + upsertCustomHookDapp(isPreHook, { + ...dapp, + type: HookDappType.IFRAME, + url, + }) + } + }) + }, + [chainId, upsertCustomHookDapp], + ) + + /** + * Update iframe hook dapps not more often than every 6 hours + */ + useEffect(() => { + if (!preHooks.length && !postHooks.length) return + + const lastUpdate = getLastUpdateTimestamp() + const shouldUpdate = !lastUpdate || lastUpdate + HOOK_DAPPS_UPDATE_INTERVAL < Date.now() + + if (!shouldUpdate) return + + console.debug('Updating iframe hook dapps...', { preHooks, postHooks }) + + localStorage.setItem(UPDATE_TIME_KEY, Date.now().toString()) + + preHooks.forEach((hook) => fetchAndUpdateHookDapp(hook.url, true)) + postHooks.forEach((hook) => fetchAndUpdateHookDapp(hook.url, false)) + }, [preHooks, postHooks, fetchAndUpdateHookDapp]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/validateHookDappManifest.tsx b/apps/cowswap-frontend/src/modules/hooksStore/validateHookDappManifest.tsx new file mode 100644 index 0000000000..b0370b4473 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/validateHookDappManifest.tsx @@ -0,0 +1,55 @@ +import { ReactElement } from 'react' + +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { HOOK_DAPP_ID_LENGTH, HookDappBase, HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' + +type HookDappBaseInfo = Omit + +const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['id', 'name', 'image', 'version', 'website'] + +const isHex = (val: string) => Boolean(val.match(/^[0-9a-f]+$/i)) + +export function validateHookDappManifest( + data: HookDappBase, + chainId: SupportedChainId | undefined, + isPreHook: boolean | undefined, + isSmartContractWallet: boolean | undefined, +): ReactElement | string | null { + const { conditions = {}, ...dapp } = data + + if (dapp) { + const emptyFields = MANDATORY_DAPP_FIELDS.filter((field) => typeof dapp[field] === 'undefined') + + if (emptyFields.length > 0) { + return `${emptyFields.join(',')} fields are no set.` + } else { + if ( + isSmartContractWallet === true && + conditions.walletCompatibility && + !conditions.walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT) + ) { + return 'The app does not support smart-contract wallets.' + } else if (!isHex(dapp.id) || dapp.id.length !== HOOK_DAPP_ID_LENGTH) { + return

Hook dapp id must be a hex with length 64.

+ } else if (chainId && conditions.supportedNetworks && !conditions.supportedNetworks.includes(chainId)) { + return

This app/hook doesn't support current network (chainId={chainId}).

+ } else if (conditions.position === 'post' && isPreHook === true) { + return ( +

+ This app/hook can only be used as a post-hook and cannot be added as a pre-hook. +

+ ) + } else if (conditions.position === 'pre' && isPreHook === false) { + return ( +

+ This app/hook can only be used as a pre-hook and cannot be added as a post-hook. +

+ ) + } + } + } else { + return 'Manifest does not contain "cow_hook_dapp" property.' + } + + return null +} diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index 83d2885a28..8f68220e52 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -30,6 +30,7 @@ import { RateInfoParams } from 'common/pure/RateInfo' import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent' import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import { useBaseFlowContextSource } from '../../hooks/useFlowContext' import { useIsEoaEthFlow } from '../../hooks/useIsEoaEthFlow' import { useNavigateToNewOrderCallback } from '../../hooks/useNavigateToNewOrderCallback' import { useShouldPayGas } from '../../hooks/useShouldPayGas' @@ -54,7 +55,6 @@ export interface ConfirmSwapModalSetupProps { doTrade(): void } - export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const { chainId, @@ -77,6 +77,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const shouldPayGas = useShouldPayGas() const isEoaEthFlow = useIsEoaEthFlow() const nativeCurrency = useNativeCurrency() + const baseFlowContextSource = useBaseFlowContextSource() const isInvertedState = useState(false) @@ -89,7 +90,10 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const labelsAndTooltips = useMemo( () => ({ - slippageLabel: isEoaEthFlow || isSmartSlippageApplied ? `Slippage tolerance (${isEoaEthFlow ? 'modified' : 'dynamic'})` : undefined, + slippageLabel: + isEoaEthFlow || isSmartSlippageApplied + ? `Slippage tolerance (${isEoaEthFlow ? 'modified' : 'dynamic'})` + : undefined, slippageTooltip: isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol]) : getNonNativeSlippageTooltip(), @@ -99,7 +103,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { networkCostsSuffix: shouldPayGas ? : null, networkCostsTooltipSuffix: , }), - [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas] + [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas], ) const submittedContent = useSubmittedContent(chainId) @@ -119,6 +123,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { priceImpact={priceImpact} buttonText={buttonText} recipient={recipient} + appData={baseFlowContextSource?.appData || undefined} > <> {receiveAmountInfo && ( @@ -166,7 +171,6 @@ function useSubmittedContent(chainId: SupportedChainId) { navigateToNewOrderCallback={navigateToNewOrderCallback} /> ), - [chainId, transactionHash, orderProgressBarV2Props, navigateToNewOrderCallback] + [chainId, transactionHash, orderProgressBarV2Props, navigateToNewOrderCallback], ) } - diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx index 42050b895c..021a8fd42e 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx @@ -1,10 +1,10 @@ - import { percentToBps } from '@cowprotocol/common-utils' import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' import { AppDataUpdater } from '../../../appData' import { useSwapSlippage } from '../../hooks/useSwapSlippage' +import { BaseFlowContextUpdater } from '../../updaters/BaseFlowContextUpdater' import { SmartSlippageUpdater } from '../../updaters/SmartSlippageUpdater' import { SwapAmountsFromUrlUpdater } from '../../updaters/SwapAmountsFromUrlUpdater' import { SwapDerivedStateUpdater } from '../../updaters/SwapDerivedStateUpdater' @@ -15,10 +15,15 @@ export function SwapUpdaters() { return ( <> - + + ) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts index 0188cffa13..6bed3d4da2 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts @@ -6,15 +6,15 @@ import { useSafeAppsSdk } from '@cowprotocol/wallet' import { useWalletProvider } from '@cowprotocol/wallet-provider' import { TradeType } from '@uniswap/sdk-core' -import { getFlowContext, useBaseFlowContextSetup } from 'modules/swap/hooks/useFlowContext' +import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' import { BaseSafeFlowContext } from 'modules/swap/services/types' import { useGP2SettlementContract } from 'common/hooks/useContract' import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress' export function useBaseSafeBundleFlowContext(): BaseSafeFlowContext | null { - const baseProps = useBaseFlowContextSetup() - const sellToken = baseProps.trade ? getWrappedToken(baseProps.trade.inputAmount.currency) : undefined + const baseProps = useBaseFlowContextSource() + const sellToken = baseProps?.trade ? getWrappedToken(baseProps.trade.inputAmount.currency) : undefined const settlementContract = useGP2SettlementContract() const spender = useTradeSpenderAddress() @@ -22,7 +22,7 @@ export function useBaseSafeBundleFlowContext(): BaseSafeFlowContext | null { const provider = useWalletProvider() return useMemo(() => { - if (!baseProps.trade || !settlementContract || !spender || !safeAppsSdk || !provider) return null + if (!baseProps?.trade || !settlementContract || !spender || !safeAppsSdk || !provider) return null const baseContext = getFlowContext({ baseProps, diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts index 784fcb328c..c3939fb295 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts @@ -6,7 +6,7 @@ import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks' -import { FlowType, getFlowContext, useBaseFlowContextSetup } from 'modules/swap/hooks/useFlowContext' +import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' import { EthFlowContext } from 'modules/swap/services/types' import { addInFlightOrderIdAtom } from 'modules/swap/state/EthFlow/ethFlowInFlightOrderIdsAtom' @@ -14,12 +14,14 @@ import { useEthFlowContract } from 'common/hooks/useContract' import { useCheckEthFlowOrderExists } from './useCheckEthFlowOrderExists' +import { FlowType } from '../types/flowContext' + export function useEthFlowContext(): EthFlowContext | null { const contract = useEthFlowContract() - const baseProps = useBaseFlowContextSetup() + const baseProps = useBaseFlowContextSource() const addTransaction = useTransactionAdder() - const sellToken = baseProps.chainId ? NATIVE_CURRENCIES[baseProps.chainId as SupportedChainId] : undefined + const sellToken = baseProps?.chainId ? NATIVE_CURRENCIES[baseProps.chainId as SupportedChainId] : undefined const addInFlightOrderId = useSetAtom(addInFlightOrderIdAtom) @@ -27,16 +29,17 @@ export function useEthFlowContext(): EthFlowContext | null { const baseContext = useMemo( () => + baseProps && getFlowContext({ baseProps, sellToken, kind: OrderKind.SELL, }), - [baseProps, sellToken] + [baseProps, sellToken], ) return useMemo(() => { - if (!baseContext || !contract || baseProps.flowType !== FlowType.EOA_ETH_FLOW) return null + if (!baseContext || !contract || baseProps?.flowType !== FlowType.EOA_ETH_FLOW) return null return { ...baseContext, @@ -45,5 +48,5 @@ export function useEthFlowContext(): EthFlowContext | null { checkEthFlowOrderExists, addInFlightOrderId, } - }, [baseContext, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, baseProps.flowType]) + }, [baseContext, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, baseProps?.flowType]) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts index 53a2f42cb4..9510a7c33d 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts @@ -1,79 +1,26 @@ -import { Erc20, Weth } from '@cowprotocol/abis' +import { useAtomValue } from 'jotai/index' + import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' -import { getAddress, getIsNativeToken } from '@cowprotocol/common-utils' +import { getIsNativeToken } from '@cowprotocol/common-utils' import { OrderClass, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' -import { useENSAddress } from '@cowprotocol/ens' -import { Command, UiOrderType } from '@cowprotocol/types' -import { GnosisSafeInfo, useGnosisSafeInfo, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' -import { useWalletProvider } from '@cowprotocol/wallet-provider' -import { Web3Provider } from '@ethersproject/providers' -import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' - -import { useDispatch } from 'react-redux' +import { UiOrderType } from '@cowprotocol/types' +import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' -import { AppDispatch } from 'legacy/state' -import { useCloseModals } from 'legacy/state/application/hooks' -import { AddOrderCallback, useAddPendingOrder } from 'legacy/state/orders/hooks' -import { useGetQuoteAndStatus } from 'legacy/state/price/hooks' -import type { QuoteInformationObject } from 'legacy/state/price/reducer' -import TradeGp from 'legacy/state/swap/TradeGp' -import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { computeSlippageAdjustedAmounts } from 'legacy/utils/prices' import { PostOrderParams } from 'legacy/utils/trade' -import { AppDataInfo, TypedAppDataHooks, UploadAppDataParams, useAppDataHooks } from 'modules/appData' -import { useAppData, useUploadAppData } from 'modules/appData' -import { useGetCachedPermit } from 'modules/permit' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { BaseFlowContext } from 'modules/swap/services/types' -import { TradeConfirmActions, useTradeConfirmActions } from 'modules/trade' import { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics' +import { getOrderValidTo } from 'modules/tradeQuote' -import { useTokenContract, useWETHContract } from 'common/hooks/useContract' -import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' import { useSafeMemo } from 'common/hooks/useSafeMemo' -import { useIsSafeEthFlow } from './useIsSafeEthFlow' import { useSwapSlippage } from './useSwapSlippage' -import { useDerivedSwapInfo, useSwapState } from './useSwapState' +import { useDerivedSwapInfo } from './useSwapState' -import { getOrderValidTo } from '../../tradeQuote/utils/quoteDeadline' import { getAmountsForSignature } from '../helpers/getAmountsForSignature' - -export enum FlowType { - REGULAR = 'REGULAR', - EOA_ETH_FLOW = 'EOA_ETH_FLOW', - SAFE_BUNDLE_APPROVAL = 'SAFE_BUNDLE_APPROVAL', - SAFE_BUNDLE_ETH = 'SAFE_BUNDLE_ETH', -} - -interface BaseFlowContextSetup { - chainId: SupportedChainId - account: string | undefined - sellTokenContract: Erc20 | null - provider: Web3Provider | undefined - trade: TradeGp | undefined - appData: AppDataInfo | null - wethContract: Weth | null - inputAmountWithSlippage: CurrencyAmount | undefined - outputAmountWithSlippage: CurrencyAmount | undefined - gnosisSafeInfo: GnosisSafeInfo | undefined - recipient: string | null - recipientAddressOrName: string | null - deadline: number - ensRecipientAddress: string | null - allowsOffchainSigning: boolean - flowType: FlowType - closeModals: Command - uploadAppData: (update: UploadAppDataParams) => void - addOrderCallback: AddOrderCallback - dispatch: AppDispatch - allowedSlippage: Percent - tradeConfirmActions: TradeConfirmActions - getCachedPermit: ReturnType - quote: QuoteInformationObject | undefined - typedHooks: TypedAppDataHooks | undefined -} +import { baseFlowContextSourceAtom } from '../state/baseFlowContextSourceAtom' +import { BaseFlowContextSource } from '../types/flowContext' export function useSwapAmountsWithSlippage(): [ CurrencyAmount | undefined, @@ -87,115 +34,12 @@ export function useSwapAmountsWithSlippage(): [ return useSafeMemo(() => [INPUT, OUTPUT], [INPUT, OUTPUT]) } -export function useBaseFlowContextSetup(): BaseFlowContextSetup { - const provider = useWalletProvider() - const { account, chainId } = useWalletInfo() - const { allowsOffchainSigning } = useWalletDetails() - const gnosisSafeInfo = useGnosisSafeInfo() - const { recipient } = useSwapState() - const slippage = useSwapSlippage() - const { trade, currenciesIds } = useDerivedSwapInfo() - const { quote } = useGetQuoteAndStatus({ - token: currenciesIds.INPUT, - chainId, - }) - - const appData = useAppData() - const typedHooks = useAppDataHooks() - const closeModals = useCloseModals() - const uploadAppData = useUploadAppData() - const addOrderCallback = useAddPendingOrder() - const dispatch = useDispatch() - const tradeConfirmActions = useTradeConfirmActions() - - const { address: ensRecipientAddress } = useENSAddress(recipient) - const recipientAddressOrName = recipient || ensRecipientAddress - const [deadline] = useUserTransactionTTL() - const wethContract = useWETHContract() - const isEoaEthFlow = useIsEoaEthFlow() - const isSafeEthFlow = useIsSafeEthFlow() - const getCachedPermit = useGetCachedPermit() - - const [inputAmountWithSlippage, outputAmountWithSlippage] = useSwapAmountsWithSlippage() - const sellTokenContract = useTokenContract(getAddress(inputAmountWithSlippage?.currency) || undefined, true) - - const isSafeBundle = useIsSafeApprovalBundle(inputAmountWithSlippage) - const flowType = _getFlowType(isSafeBundle, isEoaEthFlow, isSafeEthFlow) - - return useSafeMemo( - () => ({ - chainId, - account, - sellTokenContract, - provider, - trade, - appData, - wethContract, - inputAmountWithSlippage, - outputAmountWithSlippage, - gnosisSafeInfo, - recipient, - recipientAddressOrName, - deadline, - ensRecipientAddress, - allowsOffchainSigning, - uploadAppData, - flowType, - closeModals, - addOrderCallback, - dispatch, - allowedSlippage: slippage, - tradeConfirmActions, - getCachedPermit, - quote, - typedHooks, - }), - [ - chainId, - account, - sellTokenContract, - provider, - trade, - appData, - wethContract, - inputAmountWithSlippage, - outputAmountWithSlippage, - gnosisSafeInfo, - recipient, - recipientAddressOrName, - deadline, - ensRecipientAddress, - allowsOffchainSigning, - uploadAppData, - flowType, - closeModals, - addOrderCallback, - dispatch, - slippage, - tradeConfirmActions, - getCachedPermit, - quote, - typedHooks, - ], - ) -} - -function _getFlowType(isSafeBundle: boolean, isEoaEthFlow: boolean, isSafeEthFlow: boolean): FlowType { - if (isSafeEthFlow) { - // Takes precedence over bundle approval - return FlowType.SAFE_BUNDLE_ETH - } else if (isSafeBundle) { - // Takes precedence over eth flow - return FlowType.SAFE_BUNDLE_APPROVAL - } else if (isEoaEthFlow) { - // Takes precedence over regular flow - return FlowType.EOA_ETH_FLOW - } - return FlowType.REGULAR +export function useBaseFlowContextSource(): BaseFlowContextSource | null { + return useAtomValue(baseFlowContextSourceAtom) } type BaseGetFlowContextProps = { - baseProps: BaseFlowContextSetup + baseProps: BaseFlowContextSource sellToken?: Token kind: OrderKind } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts index f0f3410fed..6f342d3120 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts @@ -2,13 +2,14 @@ import { useMemo } from 'react' import { getWrappedToken } from '@cowprotocol/common-utils' -import { FlowType } from 'modules/swap/hooks/useFlowContext' import { SafeBundleApprovalFlowContext } from 'modules/swap/services/types' import { useTokenContract } from 'common/hooks/useContract' import { useBaseSafeBundleFlowContext } from './useBaseSafeBundleFlowContext' +import { FlowType } from '../types/flowContext' + export function useSafeBundleApprovalFlowContext(): SafeBundleApprovalFlowContext | null { const baseContext = useBaseSafeBundleFlowContext() const trade = baseContext?.context.trade diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts index d7e9781149..cc8bfb1a15 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts @@ -1,6 +1,5 @@ import { useMemo } from 'react' -import { FlowType } from 'modules/swap/hooks/useFlowContext' import { SafeBundleEthFlowContext } from 'modules/swap/services/types' import { useWETHContract } from 'common/hooks/useContract' @@ -8,6 +7,8 @@ import { useNeedsApproval } from 'common/hooks/useNeedsApproval' import { useBaseSafeBundleFlowContext } from './useBaseSafeBundleFlowContext' +import { FlowType } from '../types/flowContext' + export function useSafeBundleEthFlowContext(): SafeBundleEthFlowContext | null { const baseContext = useBaseSafeBundleFlowContext() diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index 4beb0715c4..77bbff00bb 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -2,38 +2,34 @@ import { useMemo } from 'react' import { getWrappedToken } from '@cowprotocol/common-utils' import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' -import { useWalletInfo } from '@cowprotocol/wallet' import { TradeType as UniTradeType } from '@uniswap/sdk-core' import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' -import { - FlowType, - getFlowContext, - useBaseFlowContextSetup, - useSwapAmountsWithSlippage, -} from 'modules/swap/hooks/useFlowContext' +import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' import { SwapFlowContext } from 'modules/swap/services/types' import { useEnoughBalanceAndAllowance } from 'modules/tokens' import { TradeType } from 'modules/trade' import { useGP2SettlementContract } from 'common/hooks/useContract' +import { FlowType } from '../types/flowContext' + export function useSwapFlowContext(): SwapFlowContext | null { const contract = useGP2SettlementContract() - const baseProps = useBaseFlowContextSetup() - const sellCurrency = baseProps.trade?.inputAmount?.currency + const baseProps = useBaseFlowContextSource() + const sellCurrency = baseProps?.trade?.inputAmount?.currency const permitInfo = usePermitInfo(sellCurrency, TradeType.SWAP) const generatePermitHook = useGeneratePermitHook() - const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[baseProps.chainId || SupportedChainId.MAINNET] + const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[baseProps?.chainId || SupportedChainId.MAINNET] const { enoughAllowance } = useEnoughBalanceAndAllowance({ - account: baseProps.account, - amount: baseProps.inputAmountWithSlippage, + account: baseProps?.account, + amount: baseProps?.inputAmountWithSlippage, checkAllowanceAddress, }) return useMemo(() => { - if (!baseProps.trade) { + if (!baseProps?.trade) { return null } @@ -55,17 +51,3 @@ export function useSwapFlowContext(): SwapFlowContext | null { } }, [baseProps, contract, enoughAllowance, permitInfo, generatePermitHook]) } - -export function useSwapEnoughAllowance(): boolean | undefined { - const { chainId, account } = useWalletInfo() - const [inputAmountWithSlippage] = useSwapAmountsWithSlippage() - - const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId] - const { enoughAllowance } = useEnoughBalanceAndAllowance({ - account, - amount: inputAmountWithSlippage, - checkAllowanceAddress, - }) - - return enoughAllowance -} diff --git a/apps/cowswap-frontend/src/modules/swap/services/types.ts b/apps/cowswap-frontend/src/modules/swap/services/types.ts index 8ed51c9e7a..e48dd370f5 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/types.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/types.ts @@ -17,7 +17,8 @@ import { TradeConfirmActions } from 'modules/trade' import { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics' import { EthFlowOrderExistsCallback } from '../hooks/useCheckEthFlowOrderExists' -import { FlowType } from '../hooks/useFlowContext' +import { FlowType } from '../types/flowContext' + export interface BaseFlowContext { context: { chainId: number diff --git a/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts b/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts new file mode 100644 index 0000000000..5295aaffde --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts @@ -0,0 +1,5 @@ +import { atom } from 'jotai' + +import { BaseFlowContextSource } from '../types/flowContext' + +export const baseFlowContextSourceAtom = atom(null) diff --git a/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts b/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts new file mode 100644 index 0000000000..79b9fb126d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts @@ -0,0 +1,51 @@ +import type { Erc20, Weth } from '@cowprotocol/abis' +import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import type { Command } from '@cowprotocol/types' +import type { GnosisSafeInfo } from '@cowprotocol/wallet' +import type { Web3Provider } from '@ethersproject/providers' +import type { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' + +import type { AppDispatch } from 'legacy/state' +import type { AddOrderCallback } from 'legacy/state/orders/hooks' +import type { QuoteInformationObject } from 'legacy/state/price/reducer' +import type TradeGp from 'legacy/state/swap/TradeGp' + +import type { useGetCachedPermit } from 'modules/permit' +import type { TradeConfirmActions } from 'modules/trade' + +import type { AppDataInfo, TypedAppDataHooks, UploadAppDataParams } from '../../appData' + +export enum FlowType { + REGULAR = 'REGULAR', + EOA_ETH_FLOW = 'EOA_ETH_FLOW', + SAFE_BUNDLE_APPROVAL = 'SAFE_BUNDLE_APPROVAL', + SAFE_BUNDLE_ETH = 'SAFE_BUNDLE_ETH', +} + +export interface BaseFlowContextSource { + chainId: SupportedChainId + account: string | undefined + sellTokenContract: Erc20 | null + provider: Web3Provider | undefined + trade: TradeGp | undefined + appData: AppDataInfo | null + wethContract: Weth | null + inputAmountWithSlippage: CurrencyAmount | undefined + outputAmountWithSlippage: CurrencyAmount | undefined + gnosisSafeInfo: GnosisSafeInfo | undefined + recipient: string | null + recipientAddressOrName: string | null + deadline: number + ensRecipientAddress: string | null + allowsOffchainSigning: boolean + flowType: FlowType + closeModals: Command + uploadAppData: (update: UploadAppDataParams) => void + addOrderCallback: AddOrderCallback + dispatch: AppDispatch + allowedSlippage: Percent + tradeConfirmActions: TradeConfirmActions + getCachedPermit: ReturnType + quote: QuoteInformationObject | undefined + typedHooks: TypedAppDataHooks | undefined +} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx b/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx new file mode 100644 index 0000000000..12857e8c11 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx @@ -0,0 +1,147 @@ +import { useSetAtom } from 'jotai' +import { useEffect } from 'react' + +import { getAddress } from '@cowprotocol/common-utils' +import { useENSAddress } from '@cowprotocol/ens' +import { useGnosisSafeInfo, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' +import { useWalletProvider } from '@cowprotocol/wallet-provider' + +import { useDispatch } from 'react-redux' + +import { AppDispatch } from 'legacy/state' +import { useCloseModals } from 'legacy/state/application/hooks' +import { useAddPendingOrder } from 'legacy/state/orders/hooks' +import { useGetQuoteAndStatus } from 'legacy/state/price/hooks' +import { useUserTransactionTTL } from 'legacy/state/user/hooks' + +import { useAppData, useAppDataHooks, useUploadAppData } from 'modules/appData' +import { useGetCachedPermit } from 'modules/permit' +import { useTradeConfirmActions } from 'modules/trade' + +import { useTokenContract, useWETHContract } from 'common/hooks/useContract' +import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' +import { useSafeMemo } from 'common/hooks/useSafeMemo' + +import { useSwapAmountsWithSlippage } from '../hooks/useFlowContext' +import { useIsEoaEthFlow } from '../hooks/useIsEoaEthFlow' +import { useIsSafeEthFlow } from '../hooks/useIsSafeEthFlow' +import { useSwapSlippage } from '../hooks/useSwapSlippage' +import { useDerivedSwapInfo, useSwapState } from '../hooks/useSwapState' +import { baseFlowContextSourceAtom } from '../state/baseFlowContextSourceAtom' +import { FlowType } from '../types/flowContext' + +export function BaseFlowContextUpdater() { + const setBaseFlowContextSource = useSetAtom(baseFlowContextSourceAtom) + const provider = useWalletProvider() + const { account, chainId } = useWalletInfo() + const { allowsOffchainSigning } = useWalletDetails() + const gnosisSafeInfo = useGnosisSafeInfo() + const { recipient } = useSwapState() + const slippage = useSwapSlippage() + const { trade, currenciesIds } = useDerivedSwapInfo() + const { quote } = useGetQuoteAndStatus({ + token: currenciesIds.INPUT, + chainId, + }) + + const appData = useAppData() + const typedHooks = useAppDataHooks() + const closeModals = useCloseModals() + const uploadAppData = useUploadAppData() + const addOrderCallback = useAddPendingOrder() + const dispatch = useDispatch() + const tradeConfirmActions = useTradeConfirmActions() + + const { address: ensRecipientAddress } = useENSAddress(recipient) + const recipientAddressOrName = recipient || ensRecipientAddress + const [deadline] = useUserTransactionTTL() + const wethContract = useWETHContract() + const isEoaEthFlow = useIsEoaEthFlow() + const isSafeEthFlow = useIsSafeEthFlow() + const getCachedPermit = useGetCachedPermit() + + const [inputAmountWithSlippage, outputAmountWithSlippage] = useSwapAmountsWithSlippage() + const sellTokenContract = useTokenContract(getAddress(inputAmountWithSlippage?.currency) || undefined, true) + + const isSafeBundle = useIsSafeApprovalBundle(inputAmountWithSlippage) + const flowType = getFlowType(isSafeBundle, isEoaEthFlow, isSafeEthFlow) + + const source = useSafeMemo( + () => ({ + chainId, + account, + sellTokenContract, + provider, + trade, + appData, + wethContract, + inputAmountWithSlippage, + outputAmountWithSlippage, + gnosisSafeInfo, + recipient, + recipientAddressOrName, + deadline, + ensRecipientAddress, + allowsOffchainSigning, + uploadAppData, + flowType, + closeModals, + addOrderCallback, + dispatch, + allowedSlippage: slippage, + tradeConfirmActions, + getCachedPermit, + quote, + typedHooks, + }), + [ + chainId, + account, + sellTokenContract, + provider, + trade, + appData, + wethContract, + inputAmountWithSlippage, + outputAmountWithSlippage, + gnosisSafeInfo, + recipient, + recipientAddressOrName, + deadline, + ensRecipientAddress, + allowsOffchainSigning, + uploadAppData, + flowType, + closeModals, + addOrderCallback, + dispatch, + slippage, + tradeConfirmActions, + getCachedPermit, + quote, + typedHooks, + ], + ) + + useEffect(() => { + setBaseFlowContextSource(source) + }, [source, setBaseFlowContextSource]) + + return null +} + +function getFlowType(isSafeBundle: boolean, isEoaEthFlow: boolean, isSafeEthFlow: boolean): FlowType { + if (isSafeEthFlow) { + // Takes precedence over bundle approval + return FlowType.SAFE_BUNDLE_ETH + } + if (isSafeBundle) { + // Takes precedence over eth flow + return FlowType.SAFE_BUNDLE_APPROVAL + } + if (isEoaEthFlow) { + // Takes precedence over regular flow + return FlowType.EOA_ETH_FLOW + } + return FlowType.REGULAR +} 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 80018cf1e3..24d858e630 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx @@ -16,6 +16,9 @@ import ms from 'ms.macro' import { upToMedium, useMediaQuery } from 'legacy/hooks/useMediaQuery' import { PriceImpact } from 'legacy/hooks/usePriceImpact' +import type { AppDataInfo } from 'modules/appData' + +import { OrderHooksDetails } from 'common/containers/OrderHooksDetails' import { CurrencyAmountPreview, CurrencyPreviewInfo } from 'common/pure/CurrencyInputPanel' import { QuoteCountdown } from './CountDown' @@ -23,6 +26,7 @@ import { useIsPriceChanged } from './hooks/useIsPriceChanged' import * as styledEl from './styled' import { useTradeConfirmState } from '../../hooks/useTradeConfirmState' +import { ConfirmDetailsItem } from '../ConfirmDetailsItem' import { PriceUpdatedBanner } from '../PriceUpdatedBanner' const ONE_SEC = ms`1s` @@ -34,6 +38,7 @@ export interface TradeConfirmationProps { account: string | undefined ensName: string | undefined + appData?: string | AppDataInfo inputCurrencyInfo: CurrencyPreviewInfo outputCurrencyInfo: CurrencyPreviewInfo isConfirmDisabled: boolean @@ -70,6 +75,7 @@ export function TradeConfirmation(props: TradeConfirmationProps) { children, recipient, isPriceStatic, + appData, } = frozenProps || props /** @@ -143,6 +149,15 @@ export function TradeConfirmation(props: TradeConfirmationProps) { /> {children} + {appData && ( + + {(children) => ( + + {children} + + )} + + )} {/*Banners*/} {showRecipientWarning && } {isPriceChanged && !isPriceStatic && } diff --git a/libs/hook-dapp-lib/src/utils.ts b/libs/hook-dapp-lib/src/utils.ts index 4cbfc1a902..091af2011b 100644 --- a/libs/hook-dapp-lib/src/utils.ts +++ b/libs/hook-dapp-lib/src/utils.ts @@ -2,6 +2,9 @@ import { HOOK_DAPP_ID_LENGTH } from './consts' import * as hookDappsRegistry from './hookDappsRegistry.json' import { CowHook, HookDappBase } from './types' +// permit() function selector +const PERMIT_SELECTOR = '0xd505accf' + export interface HookToDappMatch { dapp: HookDappBase | null hook: CowHook @@ -13,13 +16,27 @@ export function matchHooksToDapps(hooks: CowHook[], dapps: HookDappBase[]): Hook acc[dapp.id] = dapp return acc }, - {} as Record, + {} as Record, ) - return hooks.map((hook) => ({ - hook, - dapp: dappsMap[hook.callData.slice(-HOOK_DAPP_ID_LENGTH)] || null, - })) + return hooks.map((hook) => { + const dapp = dappsMap[hook.callData.slice(-HOOK_DAPP_ID_LENGTH)] + + /** + * Permit token is a special case, as it's not a dapp, but a hook + */ + if (!dapp && hook.callData.startsWith(PERMIT_SELECTOR)) { + return { + hook, + dapp: hookDappsRegistry.PERMIT_TOKEN as HookDappBase, + } + } + + return { + hook, + dapp: dapp || null, + } + }) } export function matchHooksToDappsRegistry(hooks: CowHook[]): HookToDappMatch[] { From 5fb7f344bec8dfd26177f62c765ed1e589c56a56 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 4 Oct 2024 16:00:05 +0500 Subject: [PATCH 006/116] feat: rescue funds from CoW Shed Proxy (#4935) * chore: update cow-sdk * feat(hooks-store): rescue funds from proxy * feat: rescue funds from proxy * feat: rescue funds from proxy * chore: fix code style * chore: hide rescue widget when wallet is not connected * chore: add gas limit * chore: add gas limit * chore: add gas limit * chore: add approve call * chore: reset hooks widget on network changes * chore: hide widget when network unsupported --- .../src/common/hooks/useCowShedHooks.ts | 12 ++ .../containers/HooksStoreWidget/index.tsx | 41 +++-- .../containers/HooksStoreWidget/styled.tsx | 24 +++ .../containers/RescueFundsFromProxy/index.tsx | 130 +++++++++++++++ .../RescueFundsFromProxy/styled.tsx | 22 +++ .../useRescueFundsFromProxy.ts | 102 ++++++++++++ libs/abis/src/abis/CowShedContract.json | 62 ++++++++ .../src/generated/custom/CowShedContract.ts | 148 ++++++++++++++++++ .../factories/CowShedContract__factory.ts | 86 ++++++++++ .../src/generated/custom/factories/index.ts | 22 +-- libs/abis/src/generated/custom/index.ts | 2 + libs/abis/src/index.ts | 2 + package.json | 4 +- yarn.lock | 8 +- 14 files changed, 637 insertions(+), 28 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/hooks/useCowShedHooks.ts create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts create mode 100644 libs/abis/src/abis/CowShedContract.json create mode 100644 libs/abis/src/generated/custom/CowShedContract.ts create mode 100644 libs/abis/src/generated/custom/factories/CowShedContract__factory.ts diff --git a/apps/cowswap-frontend/src/common/hooks/useCowShedHooks.ts b/apps/cowswap-frontend/src/common/hooks/useCowShedHooks.ts new file mode 100644 index 0000000000..056d84c0b0 --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useCowShedHooks.ts @@ -0,0 +1,12 @@ +import { CowShedHooks } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' + +import useSWR from 'swr' + +export function useCowShedHooks() { + const { chainId } = useWalletInfo() + + return useSWR([chainId, 'CowShedHooks'], ([chainId]) => { + return new CowShedHooks(chainId) + }).data +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index 7cb991d41a..0f5b3e0d36 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -1,36 +1,36 @@ -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' - -import styled from 'styled-components/macro' +import { useWalletInfo } from '@cowprotocol/wallet' import { SwapWidget } from 'modules/swap' import { useIsSellNative } from 'modules/trade' +import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' + +import { RescueFundsToggle, TradeWidgetWrapper } from './styled' + import { useSetRecipientOverride } from '../../hooks/useSetRecipientOverride' import { useSetupHooksStoreOrderParams } from '../../hooks/useSetupHooksStoreOrderParams' import { IframeDappsManifestUpdater } from '../../updaters/iframeDappsManifestUpdater' import { HookRegistryList } from '../HookRegistryList' import { PostHookButton } from '../PostHookButton' import { PreHookButton } from '../PreHookButton' +import { RescueFundsFromProxy } from '../RescueFundsFromProxy' type HookPosition = 'pre' | 'post' console.log(ICON_HOOK) -const TradeWidgetWrapper = styled.div<{ visible$: boolean }>` - visibility: ${({ visible$ }) => (visible$ ? 'visible' : 'hidden')}; - height: ${({ visible$ }) => (visible$ ? '' : '0px')}; - width: ${({ visible$ }) => (visible$ ? '100%' : '0px')}; - overflow: hidden; -` - export function HooksStoreWidget() { + const { account, chainId } = useWalletInfo() + const [isRescueWidgetOpen, setRescueWidgetOpen] = useState(false) const [selectedHookPosition, setSelectedHookPosition] = useState(null) const [hookToEdit, setHookToEdit] = useState(undefined) const isNativeSell = useIsSellNative() + const isChainIdUnsupported = useIsProviderNetworkUnsupported() const onDismiss = useCallback(() => { setSelectedHookPosition(null) @@ -47,15 +47,31 @@ export function HooksStoreWidget() { setHookToEdit(uuid) }, []) + useEffect(() => { + if (!account) { + setRescueWidgetOpen(false) + } + }, [account]) + + // Close all screens on network changes (including unsupported chain case) + useEffect(() => { + setRescueWidgetOpen(false) + onDismiss() + }, [chainId, isChainIdUnsupported, onDismiss]) + useSetupHooksStoreOrderParams() useSetRecipientOverride() const isHookSelectionOpen = !!(selectedHookPosition || hookToEdit) + const hideSwapWidget = isHookSelectionOpen || isRescueWidgetOpen - const shouldNotUseHooks = isNativeSell + const shouldNotUseHooks = isNativeSell || isChainIdUnsupported const TopContent = shouldNotUseHooks ? null : ( <> + {!isRescueWidgetOpen && account && ( + setRescueWidgetOpen(true)}>Problems receiving funds? + )} - + {isHookSelectionOpen && ( )} + {isRescueWidgetOpen && setRescueWidgetOpen(false)} />} ) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/styled.tsx new file mode 100644 index 0000000000..9ced435166 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/styled.tsx @@ -0,0 +1,24 @@ +import { UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +export const TradeWidgetWrapper = styled.div<{ visible$: boolean }>` + visibility: ${({ visible$ }) => (visible$ ? 'visible' : 'hidden')}; + height: ${({ visible$ }) => (visible$ ? '' : '0px')}; + width: ${({ visible$ }) => (visible$ ? '100%' : '0px')}; + overflow: hidden; +` + +export const RescueFundsToggle = styled.button` + background: var(${UI.COLOR_PAPER}); + border: 0; + outline: none; + text-align: right; + cursor: pointer; + text-decoration: underline; + padding: 5px; + + &:hover { + text-decoration: none; + } +` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx new file mode 100644 index 0000000000..c08aac51a9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx @@ -0,0 +1,130 @@ +import { atom, useAtom } from 'jotai' +import { useCallback, useState } from 'react' + +import { getCurrencyAddress, getEtherscanLink } from '@cowprotocol/common-utils' +import { Command } from '@cowprotocol/types' +import { BannerOrientation, ButtonPrimary, ExternalLink, InlineBanner, Loader, TokenAmount } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import ms from 'ms.macro' +import useSWR from 'swr' + +import { useErrorModal } from 'legacy/hooks/useErrorMessageAndModal' +import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks' + +import { SelectTokenWidget, useOpenTokenSelectWidget, useUpdateSelectTokenWidgetState } from 'modules/tokensList' + +import { useTokenContract } from 'common/hooks/useContract' +import { CurrencySelectButton } from 'common/pure/CurrencySelectButton' +import { NewModal } from 'common/pure/NewModal' + +import { Content, ProxyInfo, Wrapper } from './styled' +import { useRescueFundsFromProxy } from './useRescueFundsFromProxy' + +const BALANCE_UPDATE_INTERVAL = ms`5s` + +const selectedCurrencyAtom = atom(undefined) + +export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { + const [selectedCurrency, seSelectedCurrency] = useAtom(selectedCurrencyAtom) + const [tokenBalance, setTokenBalance] = useState | null>(null) + + const selectedTokenAddress = selectedCurrency ? getCurrencyAddress(selectedCurrency) : undefined + const hasBalance = !!tokenBalance?.greaterThan(0) + + const { chainId } = useWalletInfo() + const { ErrorModal, handleSetError } = useErrorModal() + const addTransaction = useTransactionAdder() + const erc20Contract = useTokenContract(selectedTokenAddress) + const onSelectToken = useOpenTokenSelectWidget() + const updateSelectTokenWidget = useUpdateSelectTokenWidgetState() + + const onDismissCallback = useCallback(() => { + updateSelectTokenWidget({ open: false }) + onDismiss() + }, [updateSelectTokenWidget, onDismiss]) + + const { + callback: rescueFundsCallback, + isTxSigningInProgress, + proxyAddress, + } = useRescueFundsFromProxy(selectedTokenAddress, tokenBalance) + + const { isLoading: isBalanceLoading } = useSWR( + erc20Contract && proxyAddress && selectedCurrency ? [erc20Contract, proxyAddress, selectedCurrency] : null, + async ([erc20Contract, proxyAddress, selectedCurrency]) => { + const balance = await erc20Contract.balanceOf(proxyAddress) + + setTokenBalance(CurrencyAmount.fromRawAmount(selectedCurrency, balance.toHexString())) + }, + { refreshInterval: BALANCE_UPDATE_INTERVAL, revalidateOnFocus: true }, + ) + + const rescueFunds = useCallback(async () => { + try { + const txHash = await rescueFundsCallback() + + if (txHash) { + addTransaction({ hash: txHash, summary: 'Rescue funds from CoW Shed Proxy' }) + } + } catch (e) { + console.error(e) + handleSetError(e.message || e.toString()) + } + }, [rescueFundsCallback, addTransaction, handleSetError]) + + const onCurrencySelectClick = useCallback(() => { + onSelectToken(selectedTokenAddress, seSelectedCurrency) + }, [onSelectToken, selectedTokenAddress, seSelectedCurrency]) + + return ( + + + + + +

+ In some cases, when orders contain a post-hook using a proxy account, something may go wrong and funds may + remain on the proxy account. Select a currency and get your funds back. +

+
+ + Proxy account:{' '} + {proxyAddress && ( + + {proxyAddress} + + )} + + + + + {selectedTokenAddress ? ( + <> +

+ Balance:{' '} + {tokenBalance ? ( + + ) : isBalanceLoading ? ( + + ) : null} +

+ + {isTxSigningInProgress ? : hasBalance ? 'Rescue funds' : 'No balance'} + + + ) : ( +
+ )} +
+
+
+ ) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx new file mode 100644 index 0000000000..47ab37e6f0 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx @@ -0,0 +1,22 @@ +import styled from 'styled-components/macro' +import { WIDGET_MAX_WIDTH } from 'theme' + +export const Wrapper = styled.div` + width: 100%; + max-width: ${WIDGET_MAX_WIDTH.swap}; + margin: 0 auto; + position: relative; +` + +export const ProxyInfo = styled.div` + margin: 20px 0; + text-align: center; +` + +export const Content = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; +` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts new file mode 100644 index 0000000000..b3e113a014 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts @@ -0,0 +1,102 @@ +import { useCallback, useMemo, useState } from 'react' + +import { CowShedContract, CowShedContractAbi } from '@cowprotocol/abis' +import { SigningScheme } from '@cowprotocol/contracts' +import { COW_SHED_FACTORY, ICoWShedCall } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' +import { useWalletProvider } from '@cowprotocol/wallet-provider' +import { defaultAbiCoder } from '@ethersproject/abi' +import { keccak256 } from '@ethersproject/keccak256' +import { pack } from '@ethersproject/solidity' +import { formatBytes32String, toUtf8Bytes } from '@ethersproject/strings' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import { useContract } from 'common/hooks/useContract' +import { useCowShedHooks } from 'common/hooks/useCowShedHooks' + +const fnSelector = (sig: string) => keccak256(toUtf8Bytes(sig)).slice(0, 10) + +const fnCalldata = (sig: string, encodedData: string) => pack(['bytes4', 'bytes'], [fnSelector(sig), encodedData]) + +export function useRescueFundsFromProxy( + selectedTokenAddress: string | undefined, + tokenBalance: CurrencyAmount | null, +) { + const [isTxSigningInProgress, setTxSigningInProgress] = useState(false) + + const provider = useWalletProvider() + const { account } = useWalletInfo() + const cowShedHooks = useCowShedHooks() + const cowShedContract = useContract(COW_SHED_FACTORY, CowShedContractAbi) + + const proxyAddress = useMemo(() => { + if (!account || !cowShedHooks) return + + return cowShedHooks.proxyOf(account) + }, [account, cowShedHooks]) + + const callback = useCallback(async () => { + if ( + !cowShedHooks || + !provider || + !proxyAddress || + !cowShedContract || + !selectedTokenAddress || + !account || + !tokenBalance + ) + return + + setTxSigningInProgress(true) + + try { + const calls: ICoWShedCall[] = [ + { + target: selectedTokenAddress, + callData: fnCalldata( + 'approve(address,uint256)', + defaultAbiCoder.encode(['address', 'uint256'], [proxyAddress, tokenBalance.quotient.toString()]), + ), + value: 0n, + isDelegateCall: false, + allowFailure: false, + }, + { + target: selectedTokenAddress, + callData: fnCalldata( + 'transferFrom(address,address,uint256)', + defaultAbiCoder.encode( + ['address', 'address', 'uint256'], + [proxyAddress, account, tokenBalance.quotient.toString()], + ), + ), + value: 0n, + isDelegateCall: false, + allowFailure: false, + }, + ] + + const nonce = formatBytes32String(Date.now().toString()) + // This field is supposed to be used with orders, but here we just do a transaction + const validTo = 99999999999 + + const encodedSignature = await cowShedHooks.signCalls( + calls, + nonce, + BigInt(validTo), + provider.getSigner(), + SigningScheme.EIP712, + ) + + const transaction = await cowShedContract.executeHooks(calls, nonce, BigInt(validTo), account, encodedSignature, { + gasLimit: 600_000, + }) + + return transaction.hash + } finally { + setTxSigningInProgress(false) + } + }, [provider, proxyAddress, cowShedContract, selectedTokenAddress, account, tokenBalance]) + + return { callback, isTxSigningInProgress, proxyAddress } +} diff --git a/libs/abis/src/abis/CowShedContract.json b/libs/abis/src/abis/CowShedContract.json new file mode 100644 index 0000000000..8e0afdd1fe --- /dev/null +++ b/libs/abis/src/abis/CowShedContract.json @@ -0,0 +1,62 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "allowFailure", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isDelegateCall", + "type": "bool" + } + ], + "internalType": "struct Call[]", + "name": "calls", + "type": "tuple[]" + }, + { + "internalType": "bytes32", + "name": "nonce", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "executeHooks", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/libs/abis/src/generated/custom/CowShedContract.ts b/libs/abis/src/generated/custom/CowShedContract.ts new file mode 100644 index 0000000000..bc5d54700b --- /dev/null +++ b/libs/abis/src/generated/custom/CowShedContract.ts @@ -0,0 +1,148 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { FunctionFragment, Result } from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, + PromiseOrValue, +} from "./common"; + +export type CallStruct = { + target: PromiseOrValue; + value: PromiseOrValue; + callData: PromiseOrValue; + allowFailure: PromiseOrValue; + isDelegateCall: PromiseOrValue; +}; + +export type CallStructOutput = [string, BigNumber, string, boolean, boolean] & { + target: string; + value: BigNumber; + callData: string; + allowFailure: boolean; + isDelegateCall: boolean; +}; + +export interface CowShedContractInterface extends utils.Interface { + functions: { + "executeHooks((address,uint256,bytes,bool,bool)[],bytes32,uint256,address,bytes)": FunctionFragment; + }; + + getFunction(nameOrSignatureOrTopic: "executeHooks"): FunctionFragment; + + encodeFunctionData( + functionFragment: "executeHooks", + values: [ + CallStruct[], + PromiseOrValue, + PromiseOrValue, + PromiseOrValue, + PromiseOrValue + ] + ): string; + + decodeFunctionResult( + functionFragment: "executeHooks", + data: BytesLike + ): Result; + + events: {}; +} + +export interface CowShedContract extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: CowShedContractInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + executeHooks( + calls: CallStruct[], + nonce: PromiseOrValue, + deadline: PromiseOrValue, + user: PromiseOrValue, + signature: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + }; + + executeHooks( + calls: CallStruct[], + nonce: PromiseOrValue, + deadline: PromiseOrValue, + user: PromiseOrValue, + signature: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + callStatic: { + executeHooks( + calls: CallStruct[], + nonce: PromiseOrValue, + deadline: PromiseOrValue, + user: PromiseOrValue, + signature: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + }; + + filters: {}; + + estimateGas: { + executeHooks( + calls: CallStruct[], + nonce: PromiseOrValue, + deadline: PromiseOrValue, + user: PromiseOrValue, + signature: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + }; + + populateTransaction: { + executeHooks( + calls: CallStruct[], + nonce: PromiseOrValue, + deadline: PromiseOrValue, + user: PromiseOrValue, + signature: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + }; +} diff --git a/libs/abis/src/generated/custom/factories/CowShedContract__factory.ts b/libs/abis/src/generated/custom/factories/CowShedContract__factory.ts new file mode 100644 index 0000000000..789abdcbf9 --- /dev/null +++ b/libs/abis/src/generated/custom/factories/CowShedContract__factory.ts @@ -0,0 +1,86 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from "ethers"; +import type { Provider } from "@ethersproject/providers"; +import type { + CowShedContract, + CowShedContractInterface, +} from "../CowShedContract"; + +const _abi = [ + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "bytes", + name: "callData", + type: "bytes", + }, + { + internalType: "bool", + name: "allowFailure", + type: "bool", + }, + { + internalType: "bool", + name: "isDelegateCall", + type: "bool", + }, + ], + internalType: "struct Call[]", + name: "calls", + type: "tuple[]", + }, + { + internalType: "bytes32", + name: "nonce", + type: "bytes32", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "address", + name: "user", + type: "address", + }, + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + ], + name: "executeHooks", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +export class CowShedContract__factory { + static readonly abi = _abi; + static createInterface(): CowShedContractInterface { + return new utils.Interface(_abi) as CowShedContractInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): CowShedContract { + return new Contract(address, _abi, signerOrProvider) as CowShedContract; + } +} diff --git a/libs/abis/src/generated/custom/factories/index.ts b/libs/abis/src/generated/custom/factories/index.ts index 0ef02962e0..fd48256880 100644 --- a/libs/abis/src/generated/custom/factories/index.ts +++ b/libs/abis/src/generated/custom/factories/index.ts @@ -1,13 +1,15 @@ /* Autogenerated file. Do not edit manually. */ /* tslint:disable */ /* eslint-disable */ -export { CoWSwapEthFlow__factory } from './CoWSwapEthFlow__factory' -export { ComposableCoW__factory } from './ComposableCoW__factory' -export { ExtensibleFallbackHandler__factory } from './ExtensibleFallbackHandler__factory' -export { GPv2Settlement__factory } from './GPv2Settlement__factory' -export { MerkleDrop__factory } from './MerkleDrop__factory' -export { Multicall3__factory } from './Multicall3__factory' -export { SBCDepositContract__factory } from './SBCDepositContract__factory' -export { SignatureVerifierMuxer__factory } from './SignatureVerifierMuxer__factory' -export { TokenDistro__factory } from './TokenDistro__factory' -export { VCow__factory } from './VCow__factory' +export { Airdrop__factory } from "./Airdrop__factory"; +export { CoWSwapEthFlow__factory } from "./CoWSwapEthFlow__factory"; +export { ComposableCoW__factory } from "./ComposableCoW__factory"; +export { CowShedContract__factory } from "./CowShedContract__factory"; +export { ExtensibleFallbackHandler__factory } from "./ExtensibleFallbackHandler__factory"; +export { GPv2Settlement__factory } from "./GPv2Settlement__factory"; +export { MerkleDrop__factory } from "./MerkleDrop__factory"; +export { Multicall3__factory } from "./Multicall3__factory"; +export { SBCDepositContract__factory } from "./SBCDepositContract__factory"; +export { SignatureVerifierMuxer__factory } from "./SignatureVerifierMuxer__factory"; +export { TokenDistro__factory } from "./TokenDistro__factory"; +export { VCow__factory } from "./VCow__factory"; diff --git a/libs/abis/src/generated/custom/index.ts b/libs/abis/src/generated/custom/index.ts index 8276a77bea..5258206c30 100644 --- a/libs/abis/src/generated/custom/index.ts +++ b/libs/abis/src/generated/custom/index.ts @@ -4,6 +4,7 @@ export type { Airdrop } from "./Airdrop"; export type { CoWSwapEthFlow } from "./CoWSwapEthFlow"; export type { ComposableCoW } from "./ComposableCoW"; +export type { CowShedContract } from "./CowShedContract"; export type { ExtensibleFallbackHandler } from "./ExtensibleFallbackHandler"; export type { GPv2Settlement } from "./GPv2Settlement"; export type { MerkleDrop } from "./MerkleDrop"; @@ -15,6 +16,7 @@ export type { VCow } from "./VCow"; export * as factories from "./factories"; export { Airdrop__factory } from "./factories/Airdrop__factory"; export { ComposableCoW__factory } from "./factories/ComposableCoW__factory"; +export { CowShedContract__factory } from "./factories/CowShedContract__factory"; export { CoWSwapEthFlow__factory } from "./factories/CoWSwapEthFlow__factory"; export { ExtensibleFallbackHandler__factory } from "./factories/ExtensibleFallbackHandler__factory"; export { GPv2Settlement__factory } from "./factories/GPv2Settlement__factory"; diff --git a/libs/abis/src/index.ts b/libs/abis/src/index.ts index 73ed2d0431..cc6dc87148 100644 --- a/libs/abis/src/index.ts +++ b/libs/abis/src/index.ts @@ -5,6 +5,7 @@ import { Interface } from '@ethersproject/abi' import _AirdropAbi from './abis/Airdrop.json' import _ComposableCoWAbi from './abis/ComposableCoW.json' +import _CowShedContractAbi from './abis/CowShedContract.json' import _CoWSwapEthFlowAbi from './abis/CoWSwapEthFlow.json' import _GPv2SettlementAbi from './abis/GPv2Settlement.json' import _MerkleDropAbi from './abis/MerkleDrop.json' @@ -36,6 +37,7 @@ export const TokenDistroAbi = _TokenDistroAbi export const CoWSwapEthFlowAbi = _CoWSwapEthFlowAbi export const SBCDepositContractAbi = _SBCDepositContractAbi export const AirdropAbi = _AirdropAbi +export const CowShedContractAbi = _CowShedContractAbi export * from './generated/custom' export type { GPv2Order } from './generated/custom/ComposableCoW' diff --git a/package.json b/package.json index 6066dc70f3..4cf9e29da9 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@cowprotocol/cms": "^0.6.0", "@cowprotocol/contracts": "^1.3.1", "@cowprotocol/cow-runner-game": "^0.2.9", - "@cowprotocol/cow-sdk": "^5.4.1", + "@cowprotocol/cow-sdk": "^5.5.1", "@cowprotocol/ethflowcontract": "cowprotocol/ethflowcontract.git#main-artifacts", "@davatar/react": "1.8.1", "@emotion/react": "^11.11.1", @@ -339,4 +339,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 f8b456bdde..61fac62505 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2429,10 +2429,10 @@ resolved "https://registry.yarnpkg.com/@cowprotocol/cow-runner-game/-/cow-runner-game-0.2.9.tgz#3f94b3f370bd114f77db8b1d238cba3ef4e9d644" integrity sha512-rX7HnoV+HYEEkBaqVUsAkGGo0oBrExi+d6Io+8nQZYwZk+IYLmS9jdcIObsLviM2h4YX8+iin6NuKl35AaiHmg== -"@cowprotocol/cow-sdk@^5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-5.4.1.tgz#35dd62c8eb1c60e2877b5f16f9b0c07e9a6b0432" - integrity sha512-/RFHVVRWr2mwVIZZy1ttG5xGSneth7sahcx5M+x5D7U4FJLXXPlc71njTpr5/dCj3HZ0vlqbZUEbN0KOLoysFA== +"@cowprotocol/cow-sdk@^5.5.1": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-5.5.1.tgz#7bb4627c56958e8c438d08c10f7bdb3a89dc3c2f" + integrity sha512-FFc9pj8xhsfGZ1vjw+4IFGb6UTdPzuOoZOvM/OhZlrfy15ohfSSb2urbHWASvKjbcXeg6KqL4aHS6KEw+BYnmA== dependencies: "@cowprotocol/contracts" "^1.6.0" "@ethersproject/abstract-signer" "^5.7.0" From 83184d23da3c812eff87bfc0ec5a2832af0ff235 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:41:47 +0100 Subject: [PATCH 007/116] feat(hooks-store): style hook details (#4932) --- .../OrderHooksDetails/HookItem/index.tsx | 69 +++++-- .../OrderHooksDetails/HookItem/styled.tsx | 145 ++++++++++++++- .../containers/OrderHooksDetails/index.tsx | 80 +++++---- .../containers/OrderHooksDetails/styled.tsx | 168 ++++++++++++++++-- .../src/modules/analytics/events.ts | 8 + .../src/modules/hooksStore/index.ts | 1 + .../hooksStore/pure/AppliedHookItem/index.tsx | 4 +- .../pure/AppliedHookItem/styled.tsx | 1 - .../hooksStore/pure/AppliedHookList/index.tsx | 2 +- .../hooksStore/validateHookDappManifest.tsx | 2 +- .../LimitOrdersConfirmModal/index.tsx | 34 ++-- .../ConfirmSwapModalSetup/index.tsx | 47 ++--- .../trade/pure/TradeConfirmation/index.tsx | 27 +-- .../hook-dapp-omnibridge/public/manifest.json | 2 +- libs/hook-dapp-lib/src/utils.ts | 7 +- 15 files changed, 472 insertions(+), 125 deletions(-) diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx index ceb64112af..5edb9fce43 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx @@ -1,20 +1,67 @@ +import { useState } from 'react' + import { HookToDappMatch } from '@cowprotocol/hook-dapp-lib' -import { Item, Wrapper } from './styled' +import { ChevronDown, ChevronUp } from 'react-feather' + +import { clickOnHooks } from 'modules/analytics' + +import * as styledEl from './styled' + +export function HookItem({ item, index }: { item: HookToDappMatch; index: number }) { + const [isOpen, setIsOpen] = useState(false) + + const handleLinkClick = () => { + clickOnHooks(item.dapp?.name || 'Unknown hook dapp') + } -export function HookItem({ item }: { item: HookToDappMatch }) { return ( - - {item.dapp ? ( - - {item.dapp.name} + + setIsOpen(!isOpen)}> + + {index + 1} + {item.dapp ? ( + <> + {item.dapp.name} + {item.dapp.name} + + ) : ( + Unknown hook dapp + )} + + + {isOpen ? : } + + + {isOpen && ( + + {item.dapp && ( + <> +

+ Description: {item.dapp.descriptionShort} +

+

+ Version: {item.dapp.version} +

+

+ Website:{' '} + + {item.dapp.website} + +

+ + )} +

+ calldata: {item.hook.callData} +

+

+ target: {item.hook.target} +

- {item.dapp.name} ({item.dapp.version}) + gasLimit: {item.hook.gasLimit}

-
- ) : ( -
Unknown hook dapp
+ )} -
+ ) } diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx index 1a16cca1a6..c04ab34ff6 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx @@ -1,19 +1,148 @@ +import { UI } from '@cowprotocol/ui' + import styled from 'styled-components/macro' -export const Item = styled.li` - list-style: none; - margin: 0; - padding: 0; +export const HookItemWrapper = styled.li` + border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10}); + border-radius: 16px; + background: var(${UI.COLOR_PAPER_DARKER}); + box-shadow: 0 5px 16px 0 transparent; + transition: all 0.2s ease-in-out; + overflow: hidden; + position: relative; + margin-bottom: 10px; + + &:hover { + border: 1px solid var(${UI.COLOR_TEXT_OPACITY_50}); + box-shadow: 0 5px 16px 0 var(${UI.COLOR_PRIMARY_OPACITY_25}); + } ` -export const Wrapper = styled.a` +export const HookItemHeader = styled.div` + cursor: pointer; display: flex; flex-direction: row; - gap: 10px; + width: 100%; + justify-content: space-between; + align-items: center; + padding: 6px 10px 6px 5px; + font-size: 14px; + gap: 8px; + font-weight: 600; + color: var(${UI.COLOR_TEXT_OPACITY_70}); + background: var(${UI.COLOR_PAPER}); + border-top-left-radius: 16px; + border-top-right-radius: 16px; + transition: all 0.2s ease-in-out; +` + +export const HookItemInfo = styled.div` + display: flex; + flex-flow: row nowrap; align-items: center; + gap: 8px; + position: relative; + width: 100%; + user-select: none; > img { - width: 30px; - height: 30px; + --size: 26px; + width: var(--size); + height: var(--size); + object-fit: contain; + border-radius: 9px; + background-color: var(${UI.COLOR_PAPER_DARKER}); + padding: 2px; + } + + > span { + font-weight: 500; + } +` + +export const HookNumber = styled.i` + --minSize: 26px; + min-height: var(--minSize); + min-width: var(--minSize); + border-radius: 9px; + margin: 0; + padding: 3px 6px; + color: var(${UI.COLOR_TEXT}); + font-weight: 500; + background: var(${UI.COLOR_PAPER_DARKER}); + border: 1px dotted transparent; + font-style: normal; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease-in-out; +` + +export const HookItemContent = styled.div` + padding: 10px; + font-size: 13px; + color: var(${UI.COLOR_TEXT_OPACITY_70}); + margin: 10px 0 0; + + > p { + margin: 0 0 1rem; + word-break: break-all; + font-weight: var(${UI.FONT_WEIGHT_NORMAL}); + + &:last-child { + margin: 0; + } + } + + > p > b { + color: var(${UI.COLOR_TEXT}); + display: block; + margin: 0 0 0.25rem; + text-transform: capitalize; + } + + > p > a { + color: var(${UI.COLOR_TEXT_OPACITY_70}); + text-decoration: underline; + + &:hover { + color: var(${UI.COLOR_TEXT}); + } + } +` + +export const ToggleButton = styled.div<{ isOpen: boolean }>` + cursor: pointer; + display: flex; + align-items: center; + justify-content: flex-end; + opacity: ${({ isOpen }) => (isOpen ? 1 : 0.7)}; + transition: opacity ${UI.ANIMATION_DURATION_SLOW} ease-in-out; + outline: none; + + &:hover { + opacity: 1; + } +` + +export const ToggleIcon = styled.div<{ isOpen: boolean }>` + --size: 16px; + transform: ${({ isOpen }) => (isOpen ? 'rotate(180deg)' : 'rotate(0deg)')}; + transition: transform var(${UI.ANIMATION_DURATION_SLOW}) ease-in-out; + display: flex; + align-items: center; + justify-content: center; + width: var(--size); + height: var(--size); + + > svg { + width: var(--size); + height: var(--size); + object-fit: contain; + + path { + fill: var(${UI.COLOR_TEXT}); + } } ` diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index abf8be100a..841a43e58e 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -2,13 +2,16 @@ import { ReactElement, useMemo, useState } from 'react' import { latest } from '@cowprotocol/app-data' import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' +import { InfoTooltip } from '@cowprotocol/ui' import { ChevronDown, ChevronUp } from 'react-feather' import { AppDataInfo, decodeAppData } from 'modules/appData' +import { useCustomHookDapps } from 'modules/hooksStore' import { HookItem } from './HookItem' -import { HooksList, InfoWrapper, ToggleButton, Wrapper } from './styled' +import * as styledEl from './styled' +import { CircleCount } from './styled' interface OrderHooksDetailsProps { appData: string | AppDataInfo @@ -20,37 +23,50 @@ export function OrderHooksDetails({ appData, children }: OrderHooksDetailsProps) const appDataDoc = useMemo(() => { return typeof appData === 'string' ? decodeAppData(appData) : appData.doc }, [appData]) + const preCustomHookDapps = useCustomHookDapps(true) + const postCustomHookDapps = useCustomHookDapps(false) if (!appDataDoc) return null const metadata = appDataDoc.metadata as latest.Metadata - const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || []) - const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || []) + const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || [], preCustomHookDapps) + const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || [], postCustomHookDapps) if (!preHooksToDapp.length && !postHooksToDapp.length) return null return children( - isOpen ? ( - - setOpen(false)}> - - - - - - ) : ( - - - {preHooksToDapp.length ? `Pre: ${preHooksToDapp.length}` : ''} - {preHooksToDapp.length && postHooksToDapp.length ? ' | ' : ''} - {postHooksToDapp.length ? `Post: ${postHooksToDapp.length}` : ''} - - setOpen(true)}> - - - - ), + + + + Hooks + + + setOpen(!isOpen)}> + {preHooksToDapp.length > 0 && ( + 0}> + PRE {preHooksToDapp.length} + + )} + {postHooksToDapp.length > 0 && ( + + POST {postHooksToDapp.length} + + )} + + setOpen(!isOpen)}> + + {isOpen ? : } + + + + {isOpen && ( + + + + + )} + , ) } @@ -63,14 +79,16 @@ function HooksInfo({ data, title }: HooksInfoProps) { return ( <> {data.length ? ( - -

{title}

- - {data.map((item) => { - return - })} - -
+ +

+ {title} {data.length} +

+ + {data.map((item, index) => ( + + ))} + +
) : null} ) diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx index c7a331922b..4f98435f7d 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx @@ -1,33 +1,165 @@ +import { UI } from '@cowprotocol/ui' + import styled from 'styled-components/macro' -export const Wrapper = styled.div` - position: relative; - padding-right: 30px; +export const Wrapper = styled.div<{ isOpen: boolean }>` + display: flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + border: 1px solid var(${UI.COLOR_PAPER_DARKER}); + border-radius: 16px; + padding: 10px; + height: auto; + font-size: 13px; + font-weight: var(${UI.FONT_WEIGHT_MEDIUM}); ` -export const HooksList = styled.ul` - margin: 0; - padding: 0; - padding-left: 10px; +export const Summary = styled.div` + display: grid; + grid-template-columns: auto 1fr auto; + justify-content: space-between; + align-items: center; + width: 100%; + gap: 8px; + font-size: inherit; + font-weight: inherit; ` -export const ToggleButton = styled.button` +export const Label = styled.span` + display: flex; + align-items: center; + gap: 4px; +` + +export const Content = styled.div` + display: flex; + width: max-content; + align-items: center; + background-color: var(${UI.COLOR_PAPER_DARKER}); + border-radius: 12px; + overflow: hidden; + margin: 0 0 0 auto; cursor: pointer; - background: none; - border: 0; - outline: 0; - padding: 0; - margin: 0; - position: absolute; - right: 0; - top: -4px; + transition: background-color var(${UI.ANIMATION_DURATION_SLOW}) ease-in-out; &:hover { - opacity: 0.7; + background-color: var(${UI.COLOR_PRIMARY_OPACITY_10}); } ` + +export const ToggleButton = styled.div<{ isOpen: boolean }>` + cursor: pointer; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 7px; + opacity: ${({ isOpen }) => (isOpen ? 1 : 0.7)}; + transition: opacity ${UI.ANIMATION_DURATION_SLOW} ease-in-out; + outline: none; + font-size: inherit; + font-weight: inherit; + + &:hover { + opacity: 1; + } +` + +export const ToggleIcon = styled.div<{ isOpen: boolean }>` + --size: 16px; + transform: ${({ isOpen }) => (isOpen ? 'rotate(180deg)' : 'rotate(0deg)')}; + transition: transform var(${UI.ANIMATION_DURATION_SLOW}) ease-in-out; + display: flex; + align-items: center; + justify-content: center; + width: var(--size); + height: var(--size); + + > svg { + width: var(--size); + height: var(--size); + object-fit: contain; + + path { + fill: var(${UI.COLOR_TEXT}); + } + } +` + +export const Details = styled.div` + display: flex; + flex-flow: column nowrap; + width: 100%; + margin: 10px 0 0; +` + export const InfoWrapper = styled.div` + margin-bottom: 10px; + h3 { - margin: 0; + font-size: 14px; + margin: 1rem 0 0.5rem; } ` + +export const HooksList = styled.ul` + list-style-type: none; + padding: 0; + margin: 0; +` + +export const HookTag = styled.div<{ isPost?: boolean; addSeparator?: boolean }>` + color: var(${UI.COLOR_TEXT_OPACITY_70}); + padding: 2px 6px; + font-size: 11px; + font-weight: var(${UI.FONT_WEIGHT_MEDIUM}); + display: flex; + align-items: center; + position: relative; + gap: 4px; + letter-spacing: 0.2px; + + > b { + font-weight: var(${UI.FONT_WEIGHT_BOLD}); + color: var(${UI.COLOR_TEXT}); + } + + ${({ isPost }) => + isPost && + ` + padding-left: 6px; + + + `} + + ${({ addSeparator }) => + addSeparator && + ` + padding-right: 10px; + + &::after { + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 4px; + background-color: var(${UI.COLOR_PAPER}); + transform: skew(-10deg); + } + `} +` + +export const CircleCount = styled.span` + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 6px; + background-color: var(${UI.COLOR_PAPER_DARKER}); + color: var(${UI.COLOR_TEXT}); + font-size: 12px; + font-weight: var(${UI.FONT_WEIGHT_BOLD}); + margin: 0; +` diff --git a/apps/cowswap-frontend/src/modules/analytics/events.ts b/apps/cowswap-frontend/src/modules/analytics/events.ts index 55d1b3b05d..58cd62ea61 100644 --- a/apps/cowswap-frontend/src/modules/analytics/events.ts +++ b/apps/cowswap-frontend/src/modules/analytics/events.ts @@ -10,6 +10,7 @@ webVitalsAnalytics.reportWebVitals() export enum Category { TRADE = 'Trade', LIST = 'Lists', + HOOKS = 'Hooks', CURRENCY_SELECT = 'Currency Select', RECIPIENT_ADDRESS = 'Recipient address', ORDER_SLIPAGE_TOLERANCE = 'Order Slippage Tolerance', @@ -331,3 +332,10 @@ export function clickNotifications(event: string, notificationId?: number, title label: title, }) } + +export function clickOnHooks(event: string) { + cowAnalytics.sendEvent({ + category: Category.HOOKS, + action: event, + }) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/index.ts b/apps/cowswap-frontend/src/modules/hooksStore/index.ts index 25ad135a64..5b922a2406 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/index.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/index.ts @@ -1,3 +1,4 @@ export { HooksStoreWidget } from './containers/HooksStoreWidget' export { useHooks } from './hooks/useHooks' export { usePostHooksRecipientOverride } from './hooks/usePostHooksRecipientOverride' +export { useCustomHookDapps } from './hooks/useCustomHookDapps' diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx index 92add9f383..f4d9cd3a39 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -17,7 +17,7 @@ import { CowHookDetailsSerialized, HookDapp } from '../../types/hooks' interface HookItemProp { account: string | undefined hookDetails: CowHookDetailsSerialized - dapp: HookDapp + dapp: HookDapp | undefined isPreHook: boolean removeHook: (uuid: string, isPreHook: boolean) => void editHook: (uuid: string) => void @@ -50,6 +50,8 @@ export function AppliedHookItem({ // TODO: Determine if simulation passed or failed const isSimulationSuccessful = simulationPassed + if (!dapp) return null + return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx index ac71a21f81..54a937e93f 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx @@ -177,7 +177,6 @@ export const SimulateContainer = styled.div<{ isSuccessful: boolean }>` ` export const OldSimulateContainer = styled.div` - border: 1px solid var(${UI.COLOR_TEXT_OPACITY_25}); border-radius: 4px; padding: 10px; display: flex; diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx index fb741ebbf3..21af0d24b9 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx @@ -69,7 +69,7 @@ export function AppliedHookList({ return ( - <> - {tradeContext && ( - - - - )} - - + {(restContent) => ( + <> + {tradeContext && ( + + + + )} + {restContent} + + + )} ) diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index 8f68220e52..9b58d26442 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -125,28 +125,31 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { recipient={recipient} appData={baseFlowContextSource?.appData || undefined} > - <> - {receiveAmountInfo && ( - - - - )} - - {!priceImpact.priceImpact && } - + {(restContent) => ( + <> + {receiveAmountInfo && ( + + + + )} + {restContent} + + {!priceImpact.priceImpact && } + + )} ) 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 24d858e630..31cd765195 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { ReactElement, useEffect, useRef, useState } from 'react' import { BackButton, @@ -26,7 +26,6 @@ import { useIsPriceChanged } from './hooks/useIsPriceChanged' import * as styledEl from './styled' import { useTradeConfirmState } from '../../hooks/useTradeConfirmState' -import { ConfirmDetailsItem } from '../ConfirmDetailsItem' import { PriceUpdatedBanner } from '../PriceUpdatedBanner' const ONE_SEC = ms`1s` @@ -48,7 +47,7 @@ export interface TradeConfirmationProps { isPriceStatic?: boolean recipient?: string | null buttonText?: React.ReactNode - children?: JSX.Element + children?: ReactElement | ((restContent: ReactElement) => ReactElement) } export function TradeConfirmation(props: TradeConfirmationProps) { @@ -126,6 +125,10 @@ export function TradeConfirmation(props: TradeConfirmationProps) { onConfirm() } + const hookDetailsElement = ( + <>{appData && {(hookChildren) => hookChildren}} + ) + return ( e.key === 'Escape' && onDismiss()}> @@ -148,17 +151,15 @@ export function TradeConfirmation(props: TradeConfirmationProps) { priceImpactParams={priceImpact} /> - {children} - {appData && ( - - {(children) => ( - - {children} - - )} - + {typeof children === 'function' ? ( + children(hookDetailsElement) + ) : ( + <> + {children} + {hookDetailsElement} + )} - {/*Banners*/} + {showRecipientWarning && } {isPriceChanged && !isPriceStatic && } diff --git a/apps/hook-dapp-omnibridge/public/manifest.json b/apps/hook-dapp-omnibridge/public/manifest.json index ff04cb43e3..380794482c 100644 --- a/apps/hook-dapp-omnibridge/public/manifest.json +++ b/apps/hook-dapp-omnibridge/public/manifest.json @@ -32,7 +32,7 @@ "image": "http://localhost:4317/hook-dapp-omnibridge/apple-touch-icon.png", "conditions": { "position": "post", - "smartContractWalletSupported": false, + "walletCompatibility": ["EOA"], "supportedNetworks": [100] } } diff --git a/libs/hook-dapp-lib/src/utils.ts b/libs/hook-dapp-lib/src/utils.ts index 091af2011b..a875fc77ae 100644 --- a/libs/hook-dapp-lib/src/utils.ts +++ b/libs/hook-dapp-lib/src/utils.ts @@ -39,6 +39,9 @@ export function matchHooksToDapps(hooks: CowHook[], dapps: HookDappBase[]): Hook }) } -export function matchHooksToDappsRegistry(hooks: CowHook[]): HookToDappMatch[] { - return matchHooksToDapps(hooks, Object.values(hookDappsRegistry) as HookDappBase[]) +export function matchHooksToDappsRegistry( + hooks: CowHook[], + additionalHookDapps: HookDappBase[] = [], +): HookToDappMatch[] { + return matchHooksToDapps(hooks, (Object.values(hookDappsRegistry) as HookDappBase[]).concat(additionalHookDapps)) } From 46699cbe6df02b0f7a3c6c380a04842e9f403a88 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 4 Oct 2024 18:06:30 +0500 Subject: [PATCH 008/116] feat(hooks-store): use dappId from hook model to match with dapp (#4938) --- apps/cowswap-frontend/.env | 14 +++---- .../appData/updater/AppDataHooksUpdater.ts | 4 +- .../src/modules/appData/utils/fullAppData.ts | 2 +- .../containers/HookDappContainer/index.tsx | 2 +- .../hooksStore/dapps/BuildHookApp/index.tsx | 4 ++ .../modules/hooksStore/hooks/useAddHook.ts | 20 +++++----- .../modules/hooksStore/hooks/useEditHook.ts | 17 ++++---- .../modules/hooksStore/hooks/useHookById.ts | 2 +- .../hooks/usePostHooksRecipientOverride.ts | 5 +-- .../modules/hooksStore/hooks/useRemoveHook.ts | 4 +- .../hooksStore/pure/AppliedHookItem/index.tsx | 17 ++------ .../hooksStore/pure/AppliedHookList/index.tsx | 8 ++-- .../hooksStore/state/customHookDappsAtom.ts | 4 +- .../hooksStore/state/hookDetailsAtom.ts | 9 ++--- .../src/modules/hooksStore/types/hooks.ts | 6 --- .../src/modules/hooksStore/utils.ts | 12 ++---- .../useTradeQuotePolling.test.tsx.snap | 8 ++-- .../createTwapOrderTxs.test.ts.snap | 6 +-- libs/hook-dapp-lib/src/types.ts | 10 ++++- libs/hook-dapp-lib/src/utils.ts | 39 ++++++++----------- .../src/lib/generatePermitHook.ts | 7 +++- package.json | 2 +- yarn.lock | 8 ++-- 23 files changed, 97 insertions(+), 113 deletions(-) diff --git a/apps/cowswap-frontend/.env b/apps/cowswap-frontend/.env index dd31b8b60e..9461bc47d5 100644 --- a/apps/cowswap-frontend/.env +++ b/apps/cowswap-frontend/.env @@ -56,13 +56,13 @@ # To set your own `AppData`, change `REACT_APP_FULL_APP_DATA_` # AppData, build yours at https://explorer.cow.fi/appdata -REACT_APP_FULL_APP_DATA_PRODUCTION='{"version":"1.2.0","appCode":"CoW Swap","environment":"production","metadata":{}}' -REACT_APP_FULL_APP_DATA_ENS='{"version":"1.2.0","appCode":"CoW Swap","environment":"ens","metadata":{}}' -REACT_APP_FULL_APP_DATA_BARN='{"version":"1.2.0","appCode":"CoW Swap","environment":"barn","metadata":{}}' -REACT_APP_FULL_APP_DATA_STAGING='{"version":"1.2.0","appCode":"CoW Swap","environment":"staging","metadata":{}}' -REACT_APP_FULL_APP_DATA_PR='{"version":"1.2.0","appCode":"CoW Swap","environment":"pr","metadata":{}}' -REACT_APP_FULL_APP_DATA_DEVELOPMENT='{"version":"1.2.0","appCode":"CoW Swap","environment":"development","metadata":{}}' -REACT_APP_FULL_APP_DATA_LOCAL='{"version":"1.2.0","appCode":"CoW Swap","environment":"local","metadata":{}}' +REACT_APP_FULL_APP_DATA_PRODUCTION='{"version":"1.3.0","appCode":"CoW Swap","environment":"production","metadata":{}}' +REACT_APP_FULL_APP_DATA_ENS='{"version":"1.3.0","appCode":"CoW Swap","environment":"ens","metadata":{}}' +REACT_APP_FULL_APP_DATA_BARN='{"version":"1.3.0","appCode":"CoW Swap","environment":"barn","metadata":{}}' +REACT_APP_FULL_APP_DATA_STAGING='{"version":"1.3.0","appCode":"CoW Swap","environment":"staging","metadata":{}}' +REACT_APP_FULL_APP_DATA_PR='{"version":"1.3.0","appCode":"CoW Swap","environment":"pr","metadata":{}}' +REACT_APP_FULL_APP_DATA_DEVELOPMENT='{"version":"1.3.0","appCode":"CoW Swap","environment":"development","metadata":{}}' +REACT_APP_FULL_APP_DATA_LOCAL='{"version":"1.3.0","appCode":"CoW Swap","environment":"local","metadata":{}}' diff --git a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts index 92a12eba20..b58c1e5130 100644 --- a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts +++ b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts @@ -49,10 +49,10 @@ export function AppDataHooksUpdater(): null { const [permitHook, setPermitHook] = useState(undefined) useEffect(() => { - const preInteractionHooks = (preHooks || []).map(({ hookDetails }) => + const preInteractionHooks = (preHooks || []).map((hookDetails) => cowHookToTypedCowHook(hookDetails.hook, 'hookStore'), ) - const postInteractionHooks = (postHooks || []).map(({ hookDetails }) => + const postInteractionHooks = (postHooks || []).map((hookDetails) => cowHookToTypedCowHook(hookDetails.hook, 'hookStore'), ) diff --git a/apps/cowswap-frontend/src/modules/appData/utils/fullAppData.ts b/apps/cowswap-frontend/src/modules/appData/utils/fullAppData.ts index 6006706663..93b0698ac0 100644 --- a/apps/cowswap-frontend/src/modules/appData/utils/fullAppData.ts +++ b/apps/cowswap-frontend/src/modules/appData/utils/fullAppData.ts @@ -3,7 +3,7 @@ import { EnvironmentName, environmentName } from '@cowprotocol/common-utils' import { AppDataInfo } from '../types' import { toKeccak256 } from '../utils/buildAppData' -const DEFAULT_FULL_APP_DATA = '{"version":"1.2.0","appCode":"CoW Swap","metadata":{}}' +const DEFAULT_FULL_APP_DATA = '{"version":"1.3.0","appCode":"CoW Swap","metadata":{}}' let appData: AppDataInfo = (() => { const fullAppData = getFullAppDataByEnv(environmentName) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index ae3815e1b5..f8eb2cf33e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -44,7 +44,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho chainId, account, orderParams, - hookToEdit: hookToEditDetails?.hookDetails, + hookToEdit: hookToEditDetails, signer, isSmartContract, isPreHook, diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx index 9c95cd5ba4..ffeed661bd 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx @@ -16,12 +16,14 @@ const DEFAULT_HOOK_STATE: CowHook = { target: '', callData: '', gasLimit: '', + dappId: '', } const DEFAULT_ERRORS_STATE: Record = { target: '', callData: '', gasLimit: '', + dappId: '', } const FIELDS = [ @@ -52,6 +54,8 @@ export function BuildHookApp({ context }: HookDappProps) { const newErrors: Record = { ...DEFAULT_ERRORS_STATE } const hasErrors = Object.entries(hook).some(([key, value]) => { + if (key === 'dappId') return false + if (!value.trim()) { newErrors[key as keyof CowHook] = `${capitalizeFirstLetter(key)} is required` return true diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts index 8b57cf575a..b0e012b116 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts @@ -1,11 +1,12 @@ import { useSetAtom } from 'jotai' import { useCallback } from 'react' +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' + import { v4 as uuidv4 } from 'uuid' import { setHooksAtom } from '../state/hookDetailsAtom' -import { AddHook, CowHookDetailsSerialized, HookDapp } from '../types/hooks' -import { appendDappIdToCallData } from '../utils' +import { AddHook, HookDapp } from '../types/hooks' export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { const updateHooks = useSetAtom(setHooksAtom) @@ -15,16 +16,13 @@ export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { console.log('[hooks] Add ' + (isPreHook ? 'pre-hook' : 'post-hook'), hookToAdd, isPreHook) const uuid = uuidv4() - const hookDetails: CowHookDetailsSerialized = { - hookDetails: { - ...hookToAdd, - uuid, - hook: { - ...hookToAdd.hook, - callData: appendDappIdToCallData(hookToAdd.hook.callData, dapp.id), - }, + const hookDetails: CowHookDetails = { + ...hookToAdd, + uuid, + hook: { + ...hookToAdd.hook, + dappId: dapp.id, }, - dappId: dapp.id, } updateHooks((hooks) => { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts index c6f5e3d27f..7b99ce507e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts @@ -1,20 +1,19 @@ import { useSetAtom } from 'jotai' import { useCallback } from 'react' -import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' +import { CowHookToEdit } from '@cowprotocol/hook-dapp-lib' import { setHooksAtom } from '../state/hookDetailsAtom' import { EditHook } from '../types/hooks' -import { appendDappIdToCallData } from '../utils' export function useEditHook(isPreHook: boolean): EditHook { const updateHooks = useSetAtom(setHooksAtom) return useCallback( - (update: CowHookDetails) => { + (update: CowHookToEdit) => { updateHooks((state) => { const type = isPreHook ? 'preHooks' : 'postHooks' - const hookIndex = state[type].findIndex((i) => i.hookDetails.uuid === update.uuid) + const hookIndex = state[type].findIndex((i) => i.uuid === update.uuid) if (hookIndex < 0) return state @@ -23,12 +22,10 @@ export function useEditHook(isPreHook: boolean): EditHook { typeState[hookIndex] = { ...hookDetails, - hookDetails: { - ...update, - hook: { - ...update.hook, - callData: appendDappIdToCallData(update.hook.callData, hookDetails.dappId), - }, + ...update, + hook: { + ...update.hook, + dappId: hookDetails.hook.dappId, }, } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHookById.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHookById.ts index 66a1b41968..75f7b0f2b0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHookById.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHookById.ts @@ -5,5 +5,5 @@ export function useHookById(uuid: string | undefined, isPreHook: boolean) { if (!uuid) return undefined - return (isPreHook ? hooks.preHooks : hooks.postHooks).find((i) => i.hookDetails.uuid === uuid) + return (isPreHook ? hooks.preHooks : hooks.postHooks).find((i) => i.uuid === uuid) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/usePostHooksRecipientOverride.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/usePostHooksRecipientOverride.ts index c20fbf7b5a..57e611c0ee 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/usePostHooksRecipientOverride.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/usePostHooksRecipientOverride.ts @@ -10,8 +10,5 @@ export function usePostHooksRecipientOverride() { * because in the current implementation we always take the value from the last hook * but it might give an unexpected behaviour */ - return useMemo( - () => postHooks.reverse().find((i) => i.hookDetails.recipientOverride)?.hookDetails.recipientOverride, - [postHooks], - ) + return useMemo(() => postHooks.reverse().find((i) => i.recipientOverride)?.recipientOverride, [postHooks]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts index 68546a3ef9..477f8cb68e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts @@ -14,13 +14,13 @@ export function useRemoveHook(isPreHook: boolean): RemoveHook { updateHooks((hooks) => { if (isPreHook) { return { - preHooks: hooks.preHooks.filter((hook) => hook.hookDetails.uuid !== uuid), + preHooks: hooks.preHooks.filter((hook) => hook.uuid !== uuid), postHooks: hooks.postHooks, } } else { return { preHooks: hooks.preHooks, - postHooks: hooks.postHooks.filter((hook) => hook.hookDetails.uuid !== uuid), + postHooks: hooks.postHooks.filter((hook) => hook.uuid !== uuid), } } }) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx index f4d9cd3a39..09b25cd8cf 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -1,9 +1,8 @@ -// src/modules/hooksStore/pure/AppliedHookItem/index.tsx - import ICON_CHECK_ICON from '@cowprotocol/assets/cow-swap/check-singular.svg' import ICON_GRID from '@cowprotocol/assets/cow-swap/grid.svg' import TenderlyLogo from '@cowprotocol/assets/cow-swap/tenderly-logo.svg' import ICON_X from '@cowprotocol/assets/cow-swap/x.svg' +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { InfoTooltip } from '@cowprotocol/ui' import { Edit2, Trash2, ExternalLink as ExternalLinkIcon } from 'react-feather' @@ -12,11 +11,11 @@ import SVG from 'react-inlinesvg' import * as styledEl from './styled' import { TenderlySimulate } from '../../containers/TenderlySimulate' -import { CowHookDetailsSerialized, HookDapp } from '../../types/hooks' +import { HookDapp } from '../../types/hooks' interface HookItemProp { account: string | undefined - hookDetails: CowHookDetailsSerialized + hookDetails: CowHookDetails dapp: HookDapp | undefined isPreHook: boolean removeHook: (uuid: string, isPreHook: boolean) => void @@ -27,15 +26,7 @@ interface HookItemProp { // TODO: remove once a tenderly bundle simulation is ready const isBundleSimulationReady = false -export function AppliedHookItem({ - account, - hookDetails: { hookDetails }, - dapp, - isPreHook, - editHook, - removeHook, - index, -}: HookItemProp) { +export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) { // TODO: Determine the simulation status based on actual simulation results // For demonstration, using a placeholder. Replace with actual logic. const simulationPassed = true // TODO: Replace with actual condition diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx index 21af0d24b9..6803355f39 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx @@ -1,9 +1,11 @@ import { useRef, useEffect } from 'react' +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' + import Sortable from 'sortablejs' import styled from 'styled-components/macro' -import { CowHookDetailsSerialized, HookDapp } from '../../types/hooks' +import { HookDapp } from '../../types/hooks' import { findHookDappById } from '../../utils' import { AppliedHookItem } from '../AppliedHookItem' @@ -19,7 +21,7 @@ const HookList = styled.ul` interface AppliedHookListProps { account: string | undefined dapps: HookDapp[] - hooks: CowHookDetailsSerialized[] + hooks: CowHookDetails[] isPreHook: boolean removeHook: (uuid: string, isPreHook: boolean) => void editHook: (uuid: string) => void @@ -68,7 +70,7 @@ export function AppliedHookList({ {hooks.map((hookDetails, index) => { return ( ({ - preHooks: (hooksState.preHooks || []).filter((hook) => hook.dappId !== hookDappId), - postHooks: (hooksState.postHooks || []).filter((hook) => hook.dappId !== hookDappId), + preHooks: (hooksState.preHooks || []).filter((hookDetails) => hookDetails.hook.dappId !== hookDappId), + postHooks: (hooksState.postHooks || []).filter((hookDetails) => hookDetails.hook.dappId !== hookDappId), })) }) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts index e975b59799..31c52be100 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts @@ -3,13 +3,12 @@ import { atomWithStorage } from 'jotai/utils' import { getJotaiIsolatedStorage } from '@cowprotocol/core' import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { walletInfoAtom } from '@cowprotocol/wallet' -import { CowHookDetailsSerialized } from '../types/hooks' - export type HooksStoreState = { - preHooks: CowHookDetailsSerialized[] - postHooks: CowHookDetailsSerialized[] + preHooks: CowHookDetails[] + postHooks: CowHookDetails[] } type StatePerAccount = Record @@ -21,7 +20,7 @@ const EMPTY_STATE: HooksStoreState = { } const hooksAtomInner = atomWithStorage( - 'hooksStoreAtom:v2', + 'hooksStoreAtom:v3', mapSupportedNetworks({}), getJotaiIsolatedStorage(), ) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts b/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts index 7091944e34..b2f16927d1 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts @@ -6,7 +6,6 @@ import { HookDappOrderParams, CoWHookDappActions, HookDappContext as GenericHookDappContext, - CowHookDetails, HookDappBase, HookDappType, } from '@cowprotocol/hook-dapp-lib' @@ -26,11 +25,6 @@ export interface HookDappIframe extends HookDappBase { export type HookDapp = HookDappInternal | HookDappIframe -export interface CowHookDetailsSerialized { - hookDetails: CowHookDetails - dappId: string -} - export type AddHook = CoWHookDappActions['addHook'] export type EditHook = CoWHookDappActions['editHook'] export type RemoveHook = (uuid: string) => void diff --git a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts index 0d379e5abf..e4a3126f68 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts @@ -1,16 +1,12 @@ -import { HookDappType } from '@cowprotocol/hook-dapp-lib' +import { CowHookDetails, HookDappType } from '@cowprotocol/hook-dapp-lib' -import { CowHookDetailsSerialized, HookDapp, HookDappIframe } from './types/hooks' +import { HookDapp, HookDappIframe } from './types/hooks' // Do a safe guard assertion that receives a HookDapp and asserts is a HookDappIframe export function isHookDappIframe(dapp: HookDapp): dapp is HookDappIframe { return dapp.type === HookDappType.IFRAME } -export function findHookDappById(dapps: HookDapp[], hookDetails: CowHookDetailsSerialized): HookDapp | undefined { - return dapps.find((i) => i.id === hookDetails.dappId) -} - -export function appendDappIdToCallData(callData: string, dappId: string): string { - return callData.endsWith(dappId) ? callData : callData + dappId +export function findHookDappById(dapps: HookDapp[], hookDetails: CowHookDetails): HookDapp | undefined { + return dapps.find((i) => i.id === hookDetails.hook.dappId) } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap index fd0a90f038..6902cfb7ee 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap @@ -2,8 +2,8 @@ exports[`useTradeQuotePolling() When wallet is NOT connected Then the "useAddress" field in the quote request should be 0x000...0000 1`] = ` { - "appData": "{"version":"1.2.0","appCode":"CoW Swap","metadata":{}}", - "appDataHash": "0xf7a2879636a64a86e7e292deea11c301ee241404f195f38c94798665509209ff", + "appData": "{"version":"1.3.0","appCode":"CoW Swap","metadata":{}}", + "appDataHash": "0x2b75dd3fccea5d141e8782354d789fb147bbf51a454c2cd0384ad2d76216116e", "buyToken": "0x0625aFB445C3B6B7B929342a04A22599fd5dBB59", "from": "0x0000000000000000000000000000000000000000", "kind": "sell", @@ -18,8 +18,8 @@ exports[`useTradeQuotePolling() When wallet is NOT connected Then the "useAddres exports[`useTradeQuotePolling() When wallet is connected Then should put account address into "useAddress" field in the quote request 1`] = ` { - "appData": "{"version":"1.2.0","appCode":"CoW Swap","metadata":{}}", - "appDataHash": "0xf7a2879636a64a86e7e292deea11c301ee241404f195f38c94798665509209ff", + "appData": "{"version":"1.3.0","appCode":"CoW Swap","metadata":{}}", + "appDataHash": "0x2b75dd3fccea5d141e8782354d789fb147bbf51a454c2cd0384ad2d76216116e", "buyToken": "0x0625aFB445C3B6B7B929342a04A22599fd5dBB59", "from": "0x333333f332a06ecb5d20d35da44ba07986d6e203", "kind": "sell", diff --git a/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/createTwapOrderTxs.test.ts.snap b/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/createTwapOrderTxs.test.ts.snap index e4b4f94957..2e7a187bdd 100644 --- a/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/createTwapOrderTxs.test.ts.snap +++ b/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/createTwapOrderTxs.test.ts.snap @@ -7,7 +7,7 @@ exports[`Create TWAP order When sell token is NOT approved AND token needs zero { "handler": "0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5", "salt": "0x00000000000000000000000000000000000000000000000000000015c90b9b2a", - "staticInput": "0x0000000000000000000000000625afb445c3b6b7b929342a04a22599fd5dbb59000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000f7a2879636a64a86e7e292deea11c301ee241404f195f38c94798665509209ff", + "staticInput": "0x0000000000000000000000000625afb445c3b6b7b929342a04a22599fd5dbb59000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000025800000000000000000000000000000000000000000000000000000000000000002b75dd3fccea5d141e8782354d789fb147bbf51a454c2cd0384ad2d76216116e", }, "0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc", "0x", @@ -66,7 +66,7 @@ exports[`Create TWAP order When sell token is NOT approved, then should generate { "handler": "0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5", "salt": "0x00000000000000000000000000000000000000000000000000000015c90b9b2a", - "staticInput": "0x0000000000000000000000000625afb445c3b6b7b929342a04a22599fd5dbb59000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000f7a2879636a64a86e7e292deea11c301ee241404f195f38c94798665509209ff", + "staticInput": "0x0000000000000000000000000625afb445c3b6b7b929342a04a22599fd5dbb59000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000025800000000000000000000000000000000000000000000000000000000000000002b75dd3fccea5d141e8782354d789fb147bbf51a454c2cd0384ad2d76216116e", }, "0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc", "0x", @@ -109,7 +109,7 @@ exports[`Create TWAP order When sell token is approved, then should generate onl { "handler": "0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5", "salt": "0x00000000000000000000000000000000000000000000000000000015c90b9b2a", - "staticInput": "0x0000000000000000000000000625afb445c3b6b7b929342a04a22599fd5dbb59000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000f7a2879636a64a86e7e292deea11c301ee241404f195f38c94798665509209ff", + "staticInput": "0x0000000000000000000000000625afb445c3b6b7b929342a04a22599fd5dbb59000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000025800000000000000000000000000000000000000000000000000000000000000002b75dd3fccea5d141e8782354d789fb147bbf51a454c2cd0384ad2d76216116e", }, "0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc", "0x", diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index da26daa099..f44aa21a06 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -6,6 +6,7 @@ export interface CowHook { target: string callData: string gasLimit: string + dappId: string } export interface HookDappConditions { @@ -15,7 +16,7 @@ export interface HookDappConditions { } export interface CowHookCreation { - hook: CowHook + hook: Omit recipientOverride?: string } @@ -24,12 +25,17 @@ export interface TokenData { } export interface CowHookDetails extends CowHookCreation { + hook: CowHook + uuid: string +} + +export interface CowHookToEdit extends CowHookCreation { uuid: string } export interface CoWHookDappActions { addHook(payload: CowHookCreation): void - editHook(payload: CowHookDetails): void + editHook(payload: CowHookToEdit): void setSellToken(token: TokenData): void setBuyToken(token: TokenData): void } diff --git a/libs/hook-dapp-lib/src/utils.ts b/libs/hook-dapp-lib/src/utils.ts index a875fc77ae..56425126a3 100644 --- a/libs/hook-dapp-lib/src/utils.ts +++ b/libs/hook-dapp-lib/src/utils.ts @@ -1,16 +1,15 @@ -import { HOOK_DAPP_ID_LENGTH } from './consts' import * as hookDappsRegistry from './hookDappsRegistry.json' import { CowHook, HookDappBase } from './types' -// permit() function selector -const PERMIT_SELECTOR = '0xd505accf' +// Before the hooks store the dappId wasn't included in the hook object +type StrictCowHook = Omit & { dappId?: string } export interface HookToDappMatch { dapp: HookDappBase | null hook: CowHook } -export function matchHooksToDapps(hooks: CowHook[], dapps: HookDappBase[]): HookToDappMatch[] { +export function matchHooksToDapps(hooks: StrictCowHook[], dapps: HookDappBase[]): HookToDappMatch[] { const dappsMap = dapps.reduce( (acc, dapp) => { acc[dapp.id] = dapp @@ -19,28 +18,24 @@ export function matchHooksToDapps(hooks: CowHook[], dapps: HookDappBase[]): Hook {} as Record, ) - return hooks.map((hook) => { - const dapp = dappsMap[hook.callData.slice(-HOOK_DAPP_ID_LENGTH)] + return ( + hooks + // Skip hooks before the hooks store was introduced + .filter((hook) => !!hook.dappId) + .map((_hook) => { + const hook = _hook as CowHook + const dapp = dappsMap[hook.dappId] - /** - * Permit token is a special case, as it's not a dapp, but a hook - */ - if (!dapp && hook.callData.startsWith(PERMIT_SELECTOR)) { - return { - hook, - dapp: hookDappsRegistry.PERMIT_TOKEN as HookDappBase, - } - } - - return { - hook, - dapp: dapp || null, - } - }) + return { + hook, + dapp: dapp || null, + } + }) + ) } export function matchHooksToDappsRegistry( - hooks: CowHook[], + hooks: StrictCowHook[], additionalHookDapps: HookDappBase[] = [], ): HookToDappMatch[] { return matchHooksToDapps(hooks, (Object.values(hookDappsRegistry) as HookDappBase[]).concat(additionalHookDapps)) diff --git a/libs/permit-utils/src/lib/generatePermitHook.ts b/libs/permit-utils/src/lib/generatePermitHook.ts index d1c3709591..e9f2e32675 100644 --- a/libs/permit-utils/src/lib/generatePermitHook.ts +++ b/libs/permit-utils/src/lib/generatePermitHook.ts @@ -6,6 +6,10 @@ import { buildDaiLikePermitCallData, buildEip2162PermitCallData } from '../utils import { getPermitDeadline } from '../utils/getPermitDeadline' import { isSupportedPermitInfo } from '../utils/isSupportedPermitInfo' +// keccak(PERMIT_TOKEN) +// See hookDappsRegistry.json in @cowprotocol/hook-dapp-lib +const PERMIT_HOOK_DAPP_ID = '1db4bacb661a90fb6b475fd5b585acba9745bc373573c65ecc3e8f5bfd5dee1f' + const REQUESTS_CACHE: { [permitKey: string]: Promise } = {} export async function generatePermitHook(params: PermitHookParams): Promise { @@ -98,6 +102,7 @@ async function generatePermitHookRaw(params: PermitHookParams): Promise { try { // Query the actual gas estimate diff --git a/package.json b/package.json index 4cf9e29da9..aa2dc9f855 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@apollo/client": "^3.1.5", "@babel/runtime": "^7.17.0", "@coinbase/wallet-sdk": "^3.3.0", - "@cowprotocol/app-data": "^2.2.0", + "@cowprotocol/app-data": "^2.3.0", "@cowprotocol/cms": "^0.6.0", "@cowprotocol/contracts": "^1.3.1", "@cowprotocol/cow-runner-game": "^0.2.9", diff --git a/yarn.lock b/yarn.lock index 61fac62505..e4bd823e29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2396,10 +2396,10 @@ resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-4.0.43.tgz#9bd42559ebb41cc5a7fb7cfeea5f231c20977dca" integrity sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ== -"@cowprotocol/app-data@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/app-data/-/app-data-2.2.0.tgz#0e03634f7a1d63226330628de2720687ab26dbc7" - integrity sha512-hHwUeF7UXCgPmYP67a446vl273zRmDqIbCp6Fqf7geNBT7IrXEMQazxWXa+D6HttTJnSVXrJoGc/rJnKpLQkGw== +"@cowprotocol/app-data@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/app-data/-/app-data-2.3.0.tgz#8c9a8ca934193eaba6cc6d67344f2f1f368e8e77" + integrity sha512-wClmhKUAEVEgt9o7+ZLvV5AT/LP2ySMfPwveyy1Nwd762m8djaIddZ5cbex6iBROyOHEmk+9x9rcn8sUHXQ4Ww== dependencies: ajv "^8.11.0" cross-fetch "^3.1.5" From f282b948d011022c563e4c6189af8da86f020754 Mon Sep 17 00:00:00 2001 From: Leandro Date: Mon, 7 Oct 2024 17:14:26 +0100 Subject: [PATCH 009/116] fix(widget-configurator): use different default partner fee address per chain (#4942) * fix(widget-configurator): use different default partner fee address for gchain * chore: temporarily hard code cowswap PR url for testing * feat: add arb1 default partner fee address * fix: accept partner fee per network * fix: switch parnerFeeBps back to number as the input is a number --- .../src/app/configurator/consts.ts | 16 +++++++++++++--- .../hooks/useWidgetParamsAndSettings.ts | 4 ++-- .../src/app/configurator/index.tsx | 10 ++++++++-- .../src/app/configurator/types.ts | 4 ++-- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts index 73fa0cbe07..c2a8caa3c3 100644 --- a/apps/widget-configurator/src/app/configurator/consts.ts +++ b/apps/widget-configurator/src/app/configurator/consts.ts @@ -1,10 +1,20 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' import { CowWidgetEventListeners, CowWidgetEvents, ToastMessageType } from '@cowprotocol/events' import { CowSwapWidgetPaletteParams, TokenInfo, TradeType } from '@cowprotocol/widget-lib' import { TokenListItem } from './types' -// CoW DAO address -export const DEFAULT_PARTNER_FEE_RECIPIENT = '0xcA771eda0c70aA7d053aB1B25004559B918FE662' +// CoW DAO addresses +const MAINNET_DEFAULT_PARTNER_FEE_RECIPIENT = '0xcA771eda0c70aA7d053aB1B25004559B918FE662' +const GNOSIS_DEFAULT_PARTNER_FEE_RECIPIENT = '0x6b3214fd11dc91de14718dee98ef59bcbfcfb432' +const ARB1_DEFAULT_PARTNER_FEE_RECIPIENT = '0x451100Ffc88884bde4ce87adC8bB6c7Df7fACccd' +export const DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK: Record = { + [SupportedChainId.MAINNET]: MAINNET_DEFAULT_PARTNER_FEE_RECIPIENT, + [SupportedChainId.GNOSIS_CHAIN]: GNOSIS_DEFAULT_PARTNER_FEE_RECIPIENT, + [SupportedChainId.ARBITRUM_ONE]: ARB1_DEFAULT_PARTNER_FEE_RECIPIENT, + [SupportedChainId.SEPOLIA]: MAINNET_DEFAULT_PARTNER_FEE_RECIPIENT, +} + export const TRADE_MODES = [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED] // Sourced from https://tokenlists.org/ @@ -35,7 +45,7 @@ export const DEFAULT_TOKEN_LISTS: TokenListItem[] = [ { url: 'https://wrapped.tokensoft.eth.link', enabled: false }, ] // TODO: Move default palette to a new lib that only exposes the palette colors. -// This wayit can be consumed by both the configurator and the widget. +// This way it can be consumed by both the configurator and the widget. export const DEFAULT_LIGHT_PALETTE: CowSwapWidgetPaletteParams = { primary: '#052b65', background: '#FFFFFF', diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index 69143ec592..f3e993ab41 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -11,8 +11,8 @@ const getBaseUrl = (): string => { if (isLocalHost) return 'http://localhost:3000' if (isDev) return 'https://dev.swap.cow.fi' if (isVercel) { - const prKey = window.location.hostname.replace('widget-configurator-git-', '').replace('-cowswap.vercel.app', '') - return `https://swap-dev-git-${prKey}-cowswap.vercel.app` + // TODO: revert before merging!! + return 'https://swap-dev-git-fix-widget-configurator-fee-recipient-cowswap.vercel.app/' } return 'https://swap.cow.fi' diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index 624b6ad80a..1a6b9cf699 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -23,7 +23,13 @@ import ListItemText from '@mui/material/ListItemText' import Typography from '@mui/material/Typography' import { useWeb3ModalAccount, useWeb3ModalTheme } from '@web3modal/ethers5/react' -import { COW_LISTENERS, DEFAULT_PARTNER_FEE_RECIPIENT, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from './consts' +import { + COW_LISTENERS, + DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK, + DEFAULT_TOKEN_LISTS, + IS_IFRAME, + TRADE_MODES, +} from './consts' import { CurrencyInputControl } from './controls/CurrencyInputControl' import { CurrentTradeTypeControl } from './controls/CurrentTradeTypeControl' import { CustomImagesControl } from './controls/CustomImagesControl' @@ -151,7 +157,7 @@ export function Configurator({ title }: { title: string }) { customColors: colorPalette, defaultColors: defaultPalette, partnerFeeBps, - partnerFeeRecipient: DEFAULT_PARTNER_FEE_RECIPIENT, + partnerFeeRecipient: DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK, standaloneMode, disableToastMessages, disableProgressBar, diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/app/configurator/types.ts index a922adccf1..1475143274 100644 --- a/apps/widget-configurator/src/app/configurator/types.ts +++ b/apps/widget-configurator/src/app/configurator/types.ts @@ -1,5 +1,5 @@ import type { SupportedChainId } from '@cowprotocol/cow-sdk' -import { CowSwapWidgetPaletteColors, TradeType } from '@cowprotocol/widget-lib' +import { CowSwapWidgetPaletteColors, PartnerFee, TradeType } from '@cowprotocol/widget-lib' import { PaletteMode } from '@mui/material' @@ -25,7 +25,7 @@ export interface ConfiguratorState { customColors: ColorPalette defaultColors: ColorPalette partnerFeeBps: number - partnerFeeRecipient: string + partnerFeeRecipient: PartnerFee['recipient'] standaloneMode: boolean disableToastMessages: boolean disableProgressBar: boolean From 80472f4d6f8a132056ec2ea796799950b34f45aa Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 7 Oct 2024 21:17:38 +0500 Subject: [PATCH 010/116] chore: release main (#4946) --- .release-please-manifest.json | 2 +- apps/widget-configurator/CHANGELOG.md | 7 +++++++ apps/widget-configurator/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index df37ef9926..1076a1ba14 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -4,7 +4,7 @@ "libs/permit-utils": "0.3.1", "libs/widget-lib": "0.15.0", "libs/widget-react": "0.11.0", - "apps/widget-configurator": "1.7.0", + "apps/widget-configurator": "1.7.1", "libs/analytics": "1.8.0", "libs/assets": "1.8.0", "libs/common-const": "1.7.0", diff --git a/apps/widget-configurator/CHANGELOG.md b/apps/widget-configurator/CHANGELOG.md index bc3807e5a9..3e43fa66a2 100644 --- a/apps/widget-configurator/CHANGELOG.md +++ b/apps/widget-configurator/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.7.1](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.7.0...widget-configurator-v1.7.1) (2024-10-07) + + +### Bug Fixes + +* **widget-configurator:** use different default partner fee address per chain ([#4942](https://github.com/cowprotocol/cowswap/issues/4942)) ([f282b94](https://github.com/cowprotocol/cowswap/commit/f282b948d011022c563e4c6189af8da86f020754)) + ## [1.7.0](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.6.0...widget-configurator-v1.7.0) (2024-09-30) diff --git a/apps/widget-configurator/package.json b/apps/widget-configurator/package.json index 0bdd535c74..aea749c2e7 100644 --- a/apps/widget-configurator/package.json +++ b/apps/widget-configurator/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-configurator", - "version": "1.7.0", + "version": "1.7.1", "description": "CoW Swap widget configurator", "main": "src/main.tsx", "author": "", From 18369d568193bccddd01bcefa18c512c08849bbe Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Mon, 7 Oct 2024 17:22:38 +0100 Subject: [PATCH 011/116] Revert "chore: temporarily hard code cowswap PR url for testing" This reverts commit 7480038630e6ff31f94ada5d6d9dd1b64773009a. --- .../src/app/configurator/hooks/useWidgetParamsAndSettings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index f3e993ab41..69143ec592 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -11,8 +11,8 @@ const getBaseUrl = (): string => { if (isLocalHost) return 'http://localhost:3000' if (isDev) return 'https://dev.swap.cow.fi' if (isVercel) { - // TODO: revert before merging!! - return 'https://swap-dev-git-fix-widget-configurator-fee-recipient-cowswap.vercel.app/' + const prKey = window.location.hostname.replace('widget-configurator-git-', '').replace('-cowswap.vercel.app', '') + return `https://swap-dev-git-${prKey}-cowswap.vercel.app` } return 'https://swap.cow.fi' From 173a0b3c5e40e6e1679eaf052890b2395112b78b Mon Sep 17 00:00:00 2001 From: Leandro Date: Tue, 8 Oct 2024 10:14:07 +0100 Subject: [PATCH 012/116] fix(widget-configurator): update mainnet default partner fee recipient (#4949) * fix(widget-configurator): update mainnet default partner fee recipient address * chore: temp address for testing. REVERT!! * Revert "chore: temp address for testing. REVERT!!" This reverts commit c9eaa02ff587758ba36c6b28857c9fd571a4303c. * Reapply "chore: temp address for testing. REVERT!!" This reverts commit a93b74e6f741442be65d78109226826d4783b100. --- apps/widget-configurator/src/app/configurator/consts.ts | 2 +- .../src/app/configurator/hooks/useWidgetParamsAndSettings.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts index c2a8caa3c3..2d229cce5e 100644 --- a/apps/widget-configurator/src/app/configurator/consts.ts +++ b/apps/widget-configurator/src/app/configurator/consts.ts @@ -5,8 +5,8 @@ import { CowSwapWidgetPaletteParams, TokenInfo, TradeType } from '@cowprotocol/w import { TokenListItem } from './types' // CoW DAO addresses -const MAINNET_DEFAULT_PARTNER_FEE_RECIPIENT = '0xcA771eda0c70aA7d053aB1B25004559B918FE662' const GNOSIS_DEFAULT_PARTNER_FEE_RECIPIENT = '0x6b3214fd11dc91de14718dee98ef59bcbfcfb432' +const MAINNET_DEFAULT_PARTNER_FEE_RECIPIENT = '0xB64963f95215FDe6510657e719bd832BB8bb941B' const ARB1_DEFAULT_PARTNER_FEE_RECIPIENT = '0x451100Ffc88884bde4ce87adC8bB6c7Df7fACccd' export const DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK: Record = { [SupportedChainId.MAINNET]: MAINNET_DEFAULT_PARTNER_FEE_RECIPIENT, diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index f3e993ab41..39bd2dc691 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -12,7 +12,7 @@ const getBaseUrl = (): string => { if (isDev) return 'https://dev.swap.cow.fi' if (isVercel) { // TODO: revert before merging!! - return 'https://swap-dev-git-fix-widget-configurator-fee-recipient-cowswap.vercel.app/' + return 'https://swap-dev-git-fix-widget-conf-parter-fee-cowswap.vercel.app/' } return 'https://swap.cow.fi' From 3bf24c77fd6c1a745b45b7c1ed7183a9fe47e115 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 8 Oct 2024 15:25:20 +0500 Subject: [PATCH 013/116] chore: release main (#4952) --- .release-please-manifest.json | 2 +- apps/widget-configurator/CHANGELOG.md | 7 +++++++ apps/widget-configurator/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1076a1ba14..b6c71c5eff 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -4,7 +4,7 @@ "libs/permit-utils": "0.3.1", "libs/widget-lib": "0.15.0", "libs/widget-react": "0.11.0", - "apps/widget-configurator": "1.7.1", + "apps/widget-configurator": "1.7.2", "libs/analytics": "1.8.0", "libs/assets": "1.8.0", "libs/common-const": "1.7.0", diff --git a/apps/widget-configurator/CHANGELOG.md b/apps/widget-configurator/CHANGELOG.md index 3e43fa66a2..179cf695e8 100644 --- a/apps/widget-configurator/CHANGELOG.md +++ b/apps/widget-configurator/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.7.2](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.7.1...widget-configurator-v1.7.2) (2024-10-08) + + +### Bug Fixes + +* **widget-configurator:** update mainnet default partner fee recipient ([#4949](https://github.com/cowprotocol/cowswap/issues/4949)) ([173a0b3](https://github.com/cowprotocol/cowswap/commit/173a0b3c5e40e6e1679eaf052890b2395112b78b)) + ## [1.7.1](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.7.0...widget-configurator-v1.7.1) (2024-10-07) diff --git a/apps/widget-configurator/package.json b/apps/widget-configurator/package.json index aea749c2e7..b903661cb1 100644 --- a/apps/widget-configurator/package.json +++ b/apps/widget-configurator/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-configurator", - "version": "1.7.1", + "version": "1.7.2", "description": "CoW Swap widget configurator", "main": "src/main.tsx", "author": "", From 6017924aec4df1a53181b5ab4c818afc5d2c5091 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:24:06 +0100 Subject: [PATCH 014/116] feat(cow.fi): modify sitemap lastmod and lazy load twitter js (#4951) --- apps/cow-fi/next-sitemap.config.js | 103 +++++++++++------------------ apps/cow-fi/pages/cow-swap.tsx | 31 +++++++-- 2 files changed, 64 insertions(+), 70 deletions(-) diff --git a/apps/cow-fi/next-sitemap.config.js b/apps/cow-fi/next-sitemap.config.js index 6a6285a466..fed66b70fd 100644 --- a/apps/cow-fi/next-sitemap.config.js +++ b/apps/cow-fi/next-sitemap.config.js @@ -20,33 +20,32 @@ module.exports = { transform: async (config, url) => { // Handle /learn/* pages with lastmod from CMS if (url.startsWith('/learn/')) { - const articles = await getAllArticleSlugsWithDates() - const article = articles.find(({ slug }) => `/learn/${slug}` === url) - if (article) { - return { - loc: url, - changefreq: 'weekly', - priority: 0.6, - lastmod: article.lastModified, + try { + console.log(`Transforming learn page: ${url}`) + const articles = await getAllArticleSlugsWithDates() + const article = articles.find(({ slug }) => `/learn/${slug}` === url) + + if (article) { + console.log(`Found matching article for ${url}`) + return { + loc: url, + changefreq: config.changefreq, + priority: config.priority, + lastmod: article.updatedAt, + } + } else { + console.log(`No matching article found for ${url}`) } + } catch (error) { + console.error(`Error processing ${url}:`, error) } } - // Handle /tokens/* pages - if (url.startsWith('/tokens/')) { - return { - loc: url, - changefreq: 'daily', - priority: 0.6, - lastmod: new Date().toISOString(), // Assume updated daily - } - } - - // Default transformation for all other pages + console.log(`Applying default transformation for: ${url}`) return { loc: url, - changefreq: 'weekly', - priority: 0.5, + changefreq: config.changefreq, + priority: config.priority, lastmod: new Date().toISOString(), } }, @@ -54,66 +53,44 @@ module.exports = { /** * Function to fetch all article slugs with lastModified dates from the CMS API - * Implements caching to avoid redundant network requests + * Implements pagination to fetch all pages of articles */ async function getAllArticleSlugsWithDates() { - // Check if articles are already cached - if (getAllArticleSlugsWithDates.cachedArticles) { - return getAllArticleSlugsWithDates.cachedArticles - } - - const articles = [] const cmsBaseUrl = process.env.NEXT_PUBLIC_CMS_BASE_URL || 'https://cms.cow.fi/api' const cmsApiUrl = `${cmsBaseUrl}/articles` - + let allArticles = [] let page = 1 - const pageSize = 100 - let totalPages = 1 + let hasMorePages = true - while (page <= totalPages) { + while (hasMorePages) { try { - console.log(`Fetching page ${page} of articles from CMS...`) - - const params = new URLSearchParams({ - 'query[fields]': 'slug,updatedAt', // Fetch both slug and updatedAt - 'query[pagination][page]': page, - 'query[pagination][pageSize]': pageSize, - }) - - const response = await fetch(`${cmsApiUrl}?${params.toString()}`, { - headers: { - // Include Authorization header if required - // Authorization: `Bearer ${process.env.CMS_API_KEY}`, - }, - }) + const url = `${cmsApiUrl}?pagination[page]=${page}&pagination[pageSize]=100` + console.log(`Fetching articles from: ${url}`) + const response = await fetch(url) if (!response.ok) { - throw new Error(`Failed to fetch articles: ${response.statusText}`) + throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() + const articles = data.data + allArticles = allArticles.concat(articles) - // Adjust based on your actual CMS API response structure - data.data.forEach((article) => { - articles.push({ - slug: article.attributes.slug, // Ensure 'slug' is the correct field - lastModified: article.attributes.updatedAt, // Ensure 'updatedAt' is the correct field - }) - }) + console.log(`Fetched ${articles.length} articles from page ${page}`) - const pagination = data.meta.pagination - totalPages = pagination.pageCount - page += 1 + // Check if there are more pages + hasMorePages = data.meta.pagination.page < data.meta.pagination.pageCount + page++ } catch (error) { console.error('Error fetching articles for sitemap:', error) - throw error + hasMorePages = false // Stop trying if there's an error } } - console.log(`Total articles fetched: ${articles.length}`) - - // Cache the fetched articles - getAllArticleSlugsWithDates.cachedArticles = articles + console.log(`Total articles fetched: ${allArticles.length}`) - return articles + return allArticles.map((article) => ({ + slug: article.attributes.slug, + updatedAt: article.attributes.updatedAt, + })) } diff --git a/apps/cow-fi/pages/cow-swap.tsx b/apps/cow-fi/pages/cow-swap.tsx index fc67b1afad..b52f4578c3 100644 --- a/apps/cow-fi/pages/cow-swap.tsx +++ b/apps/cow-fi/pages/cow-swap.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { GetStaticProps } from 'next' import { Color, ProductLogo, ProductVariant } from '@cowprotocol/ui' @@ -49,12 +49,29 @@ interface PageProps { } export default function Page({ tweets }: PageProps) { - // Load Twitter script + const tweetSectionRef = useRef(null) + useEffect(() => { - const script = document.createElement('script') - script.src = 'https://platform.twitter.com/widgets.js' - script.async = true - document.head.appendChild(script) + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + const script = document.createElement('script') + script.src = 'https://platform.twitter.com/widgets.js' + script.async = true + document.head.appendChild(script) + observer.disconnect() + } + }, + { rootMargin: '100px' }, + ) + + if (tweetSectionRef.current) { + observer.observe(tweetSectionRef.current) + } + + return () => { + observer.disconnect() + } }, []) return ( @@ -341,7 +358,7 @@ export default function Page({ tweets }: PageProps) { - + Don't take our word for it From aedc8d14c9b8dc3b25f964985b41b25229fd1547 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:47:11 +0100 Subject: [PATCH 015/116] feat(hooks-store): adjust hook details and rescue funds styles (#4948) --- .../containers/OrderHooksDetails/index.tsx | 5 +- .../containers/OrderHooksDetails/styled.tsx | 5 +- .../Transaction/ActivityDetails.tsx | 2 +- .../containers/HooksStoreWidget/index.tsx | 6 +- .../containers/HooksStoreWidget/styled.tsx | 23 +++++- .../containers/RescueFundsFromProxy/index.tsx | 81 +++++++++++-------- .../RescueFundsFromProxy/styled.tsx | 33 ++++++++ .../src/components/AppData/DecodeAppData.tsx | 2 +- .../common/AppDataWrapper/index.tsx | 5 -- .../orders/NumbersBreakdown/index.tsx | 2 +- .../OrderHooksDetails/HookItem/index.tsx | 42 ++++++++-- .../OrderHooksDetails/HookItem/styled.tsx | 63 ++++++++++++--- .../orders/OrderHooksDetails/index.tsx | 48 ++++++----- .../orders/OrderHooksDetails/styled.tsx | 8 +- .../src/explorer/pages/AppData/styled.ts | 37 +++++++-- 15 files changed, 269 insertions(+), 93 deletions(-) diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index 841a43e58e..06d2800412 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -16,9 +16,10 @@ import { CircleCount } from './styled' interface OrderHooksDetailsProps { appData: string | AppDataInfo children: (content: ReactElement) => ReactElement + margin?: string } -export function OrderHooksDetails({ appData, children }: OrderHooksDetailsProps) { +export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetailsProps) { const [isOpen, setOpen] = useState(false) const appDataDoc = useMemo(() => { return typeof appData === 'string' ? decodeAppData(appData) : appData.doc @@ -36,7 +37,7 @@ export function OrderHooksDetails({ appData, children }: OrderHooksDetailsProps) if (!preHooksToDapp.length && !postHooksToDapp.length) return null return children( - + Hooks diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx index 4f98435f7d..55f511bc35 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx @@ -2,17 +2,18 @@ import { UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' -export const Wrapper = styled.div<{ isOpen: boolean }>` +export const Wrapper = styled.div<{ isOpen: boolean; margin?: string }>` display: flex; flex-flow: row wrap; align-items: center; width: 100%; - border: 1px solid var(${UI.COLOR_PAPER_DARKER}); + border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10}); border-radius: 16px; padding: 10px; height: auto; font-size: 13px; font-weight: var(${UI.FONT_WEIGHT_MEDIUM}); + margin: ${({ margin }) => margin || '0'}; ` export const Summary = styled.div` diff --git a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx index 89a101695b..ba5a2e8a76 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx @@ -394,7 +394,7 @@ export function ActivityDetails(props: { )} {appData && ( - + {(children) => ( Hooks diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index 0f5b3e0d36..9cf80a3a42 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -9,7 +9,7 @@ import { useIsSellNative } from 'modules/trade' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' -import { RescueFundsToggle, TradeWidgetWrapper } from './styled' +import { HooksTopActions, RescueFundsToggle, TradeWidgetWrapper } from './styled' import { useSetRecipientOverride } from '../../hooks/useSetRecipientOverride' import { useSetupHooksStoreOrderParams } from '../../hooks/useSetupHooksStoreOrderParams' @@ -70,7 +70,9 @@ export function HooksStoreWidget() { const TopContent = shouldNotUseHooks ? null : ( <> {!isRescueWidgetOpen && account && ( - setRescueWidgetOpen(true)}>Problems receiving funds? + + setRescueWidgetOpen(true)}>Rescue funds + )} ` overflow: hidden; ` +export const HooksTopActions = styled.div` + border: 1px solid var(${UI.COLOR_PAPER_DARKER}); + color: inherit; + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px; + border-radius: 12px; + font-size: 13px; + background: var(${UI.COLOR_PAPER_DARKER}); +` + export const RescueFundsToggle = styled.button` - background: var(${UI.COLOR_PAPER}); + background: transparent; + font-size: inherit; + color: inherit; border: 0; outline: none; - text-align: right; + transition: all 0.2s ease-in-out; + border-radius: 9px; cursor: pointer; - text-decoration: underline; - padding: 5px; + padding: 6px; &:hover { text-decoration: none; + background: var(${UI.COLOR_PAPER}); } ` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx index c08aac51a9..a5034eaa04 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx @@ -13,7 +13,12 @@ import useSWR from 'swr' import { useErrorModal } from 'legacy/hooks/useErrorMessageAndModal' import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks' -import { SelectTokenWidget, useOpenTokenSelectWidget, useUpdateSelectTokenWidgetState } from 'modules/tokensList' +import { + SelectTokenWidget, + useOpenTokenSelectWidget, + useSelectTokenWidgetState, + useUpdateSelectTokenWidgetState, +} from 'modules/tokensList' import { useTokenContract } from 'common/hooks/useContract' import { CurrencySelectButton } from 'common/pure/CurrencySelectButton' @@ -39,6 +44,7 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { const erc20Contract = useTokenContract(selectedTokenAddress) const onSelectToken = useOpenTokenSelectWidget() const updateSelectTokenWidget = useUpdateSelectTokenWidgetState() + const { open: isSelectTokenWidgetOpen } = useSelectTokenWidgetState() const onDismissCallback = useCallback(() => { updateSelectTokenWidget({ open: false }) @@ -89,41 +95,48 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { > - -

- In some cases, when orders contain a post-hook using a proxy account, something may go wrong and funds may - remain on the proxy account. Select a currency and get your funds back. -

-
- - Proxy account:{' '} - {proxyAddress && ( - - {proxyAddress} - - )} - - - - - {selectedTokenAddress ? ( - <> + {!isSelectTokenWidgetOpen && ( + <> +

- Balance:{' '} - {tokenBalance ? ( - - ) : isBalanceLoading ? ( - - ) : null} + In some cases, when orders contain a post-hook using a proxy account, something may go wrong and funds + may remain on the proxy account. Select a currency and get your funds back.

- - {isTxSigningInProgress ? : hasBalance ? 'Rescue funds' : 'No balance'} - - - ) : ( -
- )} -
+ + +

Proxy account:

+ {proxyAddress && ( + + {proxyAddress} ↗ + + )} +
+ + + + {selectedTokenAddress ? ( + <> +

+ Balance to be rescued: +
+ {tokenBalance ? ( + + + + ) : isBalanceLoading ? ( + + ) : null} +

+ + {isTxSigningInProgress ? : hasBalance ? 'Rescue funds' : 'No funds to rescue'} + + + ) : ( +
+ )} +
+ + )}
) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx index 47ab37e6f0..2bada3e019 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx @@ -1,3 +1,5 @@ +import { UI } from '@cowprotocol/ui' + import styled from 'styled-components/macro' import { WIDGET_MAX_WIDTH } from 'theme' @@ -9,8 +11,35 @@ export const Wrapper = styled.div` ` export const ProxyInfo = styled.div` + display: flex; + flex-flow: column wrap; + gap: 10px; margin: 20px 0; text-align: center; + + > h4 { + font-size: 14px; + font-weight: 600; + margin: 10px auto 0; + } + + > a { + color: inherit; + width: 100%; + } + + > a > span { + font-size: 100%; + background: var(${UI.COLOR_PAPER_DARKER}); + border-radius: 16px; + padding: 10px; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + margin: 0 auto; + word-break: break-all; + } ` export const Content = styled.div` @@ -19,4 +48,8 @@ export const Content = styled.div` align-items: center; justify-content: center; gap: 20px; + + p { + text-align: center; + } ` diff --git a/apps/explorer/src/components/AppData/DecodeAppData.tsx b/apps/explorer/src/components/AppData/DecodeAppData.tsx index 8532bc352b..9bbd49c32c 100644 --- a/apps/explorer/src/components/AppData/DecodeAppData.tsx +++ b/apps/explorer/src/components/AppData/DecodeAppData.tsx @@ -79,7 +79,7 @@ const DecodeAppData = (props: Props): React.ReactNode => { } const ShowMoreButton = styled.button` - font-size: 1.2rem; + font-size: 1.4rem; margin-top: 0.5rem; border: none; background: none; diff --git a/apps/explorer/src/components/common/AppDataWrapper/index.tsx b/apps/explorer/src/components/common/AppDataWrapper/index.tsx index 4ad3c24910..cf858750fc 100644 --- a/apps/explorer/src/components/common/AppDataWrapper/index.tsx +++ b/apps/explorer/src/components/common/AppDataWrapper/index.tsx @@ -1,4 +1,3 @@ - import styled from 'styled-components/macro' const AppDataWrapper = styled.div` @@ -24,10 +23,6 @@ const AppDataWrapper = styled.div` background-color: rgba(0, 0, 0, 0.2); } } - - .hidden-content { - margin-top: 10px; - } ` export default AppDataWrapper diff --git a/apps/explorer/src/components/orders/NumbersBreakdown/index.tsx b/apps/explorer/src/components/orders/NumbersBreakdown/index.tsx index ed7d01ab9a..004c36f2a5 100644 --- a/apps/explorer/src/components/orders/NumbersBreakdown/index.tsx +++ b/apps/explorer/src/components/orders/NumbersBreakdown/index.tsx @@ -8,7 +8,7 @@ import useSafeState from 'hooks/useSafeState' import styled from 'styled-components/macro' const ShowMoreButton = styled.button` - font-size: 1.2rem; + font-size: 1.4rem; border: none; background: none; color: ${({ theme }) => theme.textActive1}; diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx index ceb64112af..b80bb6d9b6 100644 --- a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx +++ b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx @@ -1,16 +1,48 @@ +import { useState } from 'react' + import { HookToDappMatch } from '@cowprotocol/hook-dapp-lib' -import { Item, Wrapper } from './styled' +import { Item, Wrapper, ToggleButton, Details } from './styled' + +export function HookItem({ item, number }: { item: HookToDappMatch; number: number }) { + const [showDetails, setShowDetails] = useState(false) + + const toggleDetails = () => setShowDetails(!showDetails) -export function HookItem({ item }: { item: HookToDappMatch }) { return ( {item.dapp ? ( - - {item.dapp.name} +

- {item.dapp.name} ({item.dapp.version}) + #{number} - {item.dapp.name} {item.dapp.name}{' '} + {showDetails ? '[-] Show less' : '[+] Show more'}

+ + {showDetails && ( +
+

+ Version: {item.dapp.version} +

+

+ Description: {item.dapp.descriptionShort} +

+

+ Website: + + {item.dapp.website} + +

+

+ Call Data: {item.hook.callData} +

+

+ Gas Limit: {item.hook.gasLimit} +

+

+ Target: {item.hook.target} +

+
+ )}
) : (
Unknown hook dapp
diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/styled.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/styled.tsx index 1a16cca1a6..f08a5e299f 100644 --- a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/styled.tsx +++ b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/styled.tsx @@ -1,19 +1,64 @@ import styled from 'styled-components/macro' export const Item = styled.li` - list-style: none; margin: 0; - padding: 0; ` -export const Wrapper = styled.a` +export const Wrapper = styled.div` display: flex; - flex-direction: row; - gap: 10px; - align-items: center; + flex-flow: column wrap; + gap: 0.5rem; + align-items: flex-start; - > img { - width: 30px; - height: 30px; + > p { + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; + } + + > p > i { + font-weight: bold; + font-style: normal; + } + + > p > img { + width: 2rem; + height: 2rem; + } +` + +export const ToggleButton = styled.button` + background: none; + border: none; + color: ${({ theme }) => theme.textActive1}; + cursor: pointer; + font-size: 1.4rem; + padding: 0.5rem 0; + text-align: left; + + &:hover { + text-decoration: underline; + } +` + +export const Details = styled.div` + margin: 0 0 1rem; + word-break: break-all; + line-height: 1.5; + overflow: auto; + border: 0.1rem solid rgb(151 151 184 / 10%); + padding: 1.4rem; + background: rgb(151 151 184 / 10%); + border-radius: 0.5rem; + white-space: pre-wrap; + + > p { + margin: 0; + } + + > p > i { + font-weight: 500; + font-style: normal; } ` diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx index d7a1bf543b..da037bbec8 100644 --- a/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx +++ b/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx @@ -1,17 +1,17 @@ -import { ReactElement } from 'react' +import React, { ReactElement } from 'react' import { latest } from '@cowprotocol/app-data' import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' import { HookItem } from './HookItem' -import { HooksList } from './styled' +import { HooksList, Wrapper } from './styled' import { useAppData } from '../../../hooks/useAppData' interface OrderHooksDetailsProps { appData: string fullAppData: string | undefined - children: (content: ReactElement) => ReactElement + children: (content: ReactElement | string) => ReactElement } export function OrderHooksDetails({ appData, fullAppData, children }: OrderHooksDetailsProps) { @@ -24,11 +24,17 @@ export function OrderHooksDetails({ appData, fullAppData, children }: OrderHooks const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || []) const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || []) + const hasHooks = preHooksToDapp.length > 0 || postHooksToDapp.length > 0 + return children( - <> - - - , + hasHooks ? ( + <> + {preHooksToDapp.length > 0 && } + {postHooksToDapp.length > 0 && } + + ) : ( + - + ), ) } @@ -39,17 +45,21 @@ interface HooksInfoProps { function HooksInfo({ data, title }: HooksInfoProps) { return ( - <> - {data.length && ( -
-

{title}

- - {data.map((item) => { - return - })} - -
- )} - + +
+

+ {title} ({data.length}) +

+ + {data.map((item, index) => ( + + ))} + +
+
) } diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/styled.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/styled.tsx index 660fe8c1e3..b1f37c79a2 100644 --- a/apps/explorer/src/components/orders/OrderHooksDetails/styled.tsx +++ b/apps/explorer/src/components/orders/OrderHooksDetails/styled.tsx @@ -1,7 +1,13 @@ import styled from 'styled-components/macro' +export const Wrapper = styled.div` + display: flex; + flex-flow: column wrap; + margin: 0 0 2rem; +` + export const HooksList = styled.ul` margin: 0; padding: 0; - padding-left: 10px; + list-style: none; ` diff --git a/apps/explorer/src/explorer/pages/AppData/styled.ts b/apps/explorer/src/explorer/pages/AppData/styled.ts index 0686edfd07..9b0aa59b76 100644 --- a/apps/explorer/src/explorer/pages/AppData/styled.ts +++ b/apps/explorer/src/explorer/pages/AppData/styled.ts @@ -13,52 +13,64 @@ export const StyledExplorerTabs = styled(ExplorerTabs)` export const Wrapper = styled(WrapperTemplate)` max-width: 118rem; + .disclaimer { font-size: 1.2rem; line-height: 1.3; display: block; margin-bottom: 1rem; + ol { padding-left: 2rem; + li { margin: 0.5rem 0 0.5rem 0; } } } + .info-header { margin-bottom: 2rem; font-size: 1.5rem; + &.box { padding: 3rem 4rem; background: ${({ theme }): string => theme.background}; border-radius: 0.4rem; } + a { margin: 0 0.5rem 0 0.5rem; color: ${({ theme }): string => theme.orange}; } + &.inner-form { + margin-bottom: 3rem; + font-size: 1.2rem; + h2 { margin-bottom: 2rem; } - margin-bottom: 3rem; - font-size: 1.2rem; } + p { line-height: 1.5; margin: 0; } } + ${Content} { display: flex; flex-direction: column; border: 0; padding: 0; + .form-container { ${AppDataWrapper} { align-items: center; } } + ${AppDataWrapper} { flex: 1; padding-left: 2rem; @@ -67,6 +79,7 @@ export const Wrapper = styled(WrapperTemplate)` padding-left: 0; } } + .json-formatter { line-height: 1.25; @@ -82,15 +95,18 @@ export const Wrapper = styled(WrapperTemplate)` background: ${({ theme }): string => transparentize(0.8, theme.error)}; } } + .hidden-content { padding: 0 1rem; border-radius: 0.5rem; font-size: 1.3rem; line-height: 1.6; + &:not(.error) { background: ${({ theme }): string => theme.greyOpacity}; } } + .appData-hash { margin: 0 0 1rem 0; max-width: 55rem; @@ -98,10 +114,12 @@ export const Wrapper = styled(WrapperTemplate)` padding: 0.75rem; background: ${({ theme }): string => theme.tableRowBorder}; border-radius: 0.5rem; + ${Media.upToSmall()} { max-width: none; margin: 1rem 0; } + span, a { font-size: 1.3rem; @@ -131,23 +149,33 @@ export const Wrapper = styled(WrapperTemplate)` i.glyphicon { display: none; } + .btn-add::after { content: 'Add'; } + .array-item-copy::after { content: 'Copy'; } + .array-item-move-up::after { content: 'Move Up'; } + .array-item-move-down::after { content: 'Move Down'; } + .array-item-remove::after { content: 'Remove'; } .hidden-content { + h2 { + margin: 2rem 0 2rem 0; + font-size: 2rem; + } + ${Media.LargeAndUp()} { position: sticky; top: 2.8rem; @@ -172,11 +200,6 @@ export const Wrapper = styled(WrapperTemplate)` top: 4rem; width: 60rem; } - - h2 { - margin: 2rem 0 2rem 0; - font-size: 2rem; - } } } From 09459312b004f5ea96ff0297d01081a3b41c9079 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 9 Oct 2024 19:31:01 +0500 Subject: [PATCH 016/116] refactor: remove excessive console logs (#4973) --- apps/cowswap-frontend/public/emergency.js | 1 - .../src/api/cowProtocol/priceApi.ts | 4 +- .../src/common/updaters/FeesUpdater.ts | 2 +- .../src/legacy/state/index.ts | 2 +- .../src/legacy/state/orders/hooks.ts | 71 +++++++++---------- .../src/legacy/state/price/hooks.ts | 15 +--- .../containers/LimitOrdersWidget/index.tsx | 2 - .../src/googleAnalytics/CowAnalyticsGoogle.ts | 2 - .../updaters/HwAccountIndexUpdater.tsx | 1 - 9 files changed, 41 insertions(+), 59 deletions(-) diff --git a/apps/cowswap-frontend/public/emergency.js b/apps/cowswap-frontend/public/emergency.js index 1473d22a93..82101e40bd 100644 --- a/apps/cowswap-frontend/public/emergency.js +++ b/apps/cowswap-frontend/public/emergency.js @@ -46,7 +46,6 @@ if (window.location.pathname !== '/') { for (let i = 0; i < version; i++) { localStorage.removeItem(`${name}:v${i}`) } - console.log(name, version) }) })() diff --git a/apps/cowswap-frontend/src/api/cowProtocol/priceApi.ts b/apps/cowswap-frontend/src/api/cowProtocol/priceApi.ts index 6176a0946b..1ec7ad53b2 100644 --- a/apps/cowswap-frontend/src/api/cowProtocol/priceApi.ts +++ b/apps/cowswap-frontend/src/api/cowProtocol/priceApi.ts @@ -22,7 +22,7 @@ function _getPriceStrategyApiBaseUrl(chainId: SupportedChainId): string { new Error( `Unsupported Network. The ${API_NAME} strategy API is not deployed in the Network ` + chainId + - '. Defaulting to using Mainnet strategy.' + '. Defaulting to using Mainnet strategy.', ) } return baseUrl @@ -34,8 +34,6 @@ function _fetchPriceStrategy(chainId: SupportedChainId): Promise { } export async function getPriceStrategy(chainId: SupportedChainId): Promise { - console.log(`[api:${API_NAME}] Get GP price strategy for`, chainId) - const response = await _fetchPriceStrategy(chainId) if (!response.ok) { diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts index e5e0fcce0b..a45e3bc669 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts @@ -128,7 +128,7 @@ export function FeesUpdater(): null { // Fee API calculation/call const typedValue = useDebounce(rawTypedValue, TYPED_VALUE_DEBOUNCE_TIME) - const quotesMap = useAllQuotes({ chainId }) + const quotesMap = useAllQuotes(chainId) const quoteInfo = useMemo(() => { return quotesMap && sellCurrencyId ? quotesMap[sellCurrencyId] : undefined diff --git a/apps/cowswap-frontend/src/legacy/state/index.ts b/apps/cowswap-frontend/src/legacy/state/index.ts index eb58f8a759..c4ddfbd5e0 100644 --- a/apps/cowswap-frontend/src/legacy/state/index.ts +++ b/apps/cowswap-frontend/src/legacy/state/index.ts @@ -24,7 +24,7 @@ const reducers = { cowToken, } -const PERSISTED_KEYS: string[] = ['user', 'transactions', 'orders', 'gas', 'affiliate', 'profile', 'swap'] +const PERSISTED_KEYS: string[] = ['user', 'transactions', 'orders', 'gas', 'swap'] export const cowSwapStore = configureStore({ reducer: reducers, diff --git a/apps/cowswap-frontend/src/legacy/state/orders/hooks.ts b/apps/cowswap-frontend/src/legacy/state/orders/hooks.ts index c5e2dea5f6..f6f7ea38c2 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/hooks.ts @@ -1,10 +1,12 @@ import { useCallback, useMemo } from 'react' +import { SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' import { isTruthy } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { UiOrderType } from '@cowprotocol/types' import { useDispatch, useSelector } from 'react-redux' +import useSWR from 'swr' import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' @@ -48,6 +50,9 @@ import { serializeToken } from '../user/hooks' type OrderID = string +const EMPTY_ORDERS_ARRAY = [] as Order[] +const EMPTY_ORDERS_MAP = {} as PartialOrdersMap + export interface AddOrUpdateUnserialisedOrdersParams extends Omit { orders: Order[] } @@ -90,7 +95,7 @@ export type CancelOrdersBatchCallback = (cancelOrdersBatchParams: CancelOrdersBa export type InvalidateOrdersBatchCallback = (params: InvalidateOrdersBatchParams) => void export type PresignOrdersCallback = (fulfillOrderParams: PresignOrdersParams) => void export type UpdatePresignGnosisSafeTxCallback = ( - updatePresignGnosisSafeTxParams: UpdatePresignGnosisSafeTxParams + updatePresignGnosisSafeTxParams: UpdatePresignGnosisSafeTxParams, ) => void export type SetIsOrderUnfillable = (params: SetIsOrderUnfillableParams) => void export type SetIsOrderRefundedBatchCallback = (params: SetIsOrderRefundedBatch) => void @@ -148,13 +153,13 @@ function useOrdersStateNetwork(chainId: SupportedChainId | undefined): OrdersSta export const useOrders = ( chainId: SupportedChainId, account: string | undefined, - uiOrderType: UiOrderType + uiOrderType: UiOrderType, ): Order[] => { const state = useOrdersStateNetwork(chainId) const accountLowerCase = account?.toLowerCase() return useMemo(() => { - if (!state) return [] + if (!state) return EMPTY_ORDERS_ARRAY return _concatOrdersState(state, ORDER_LIST_KEYS).reduce((acc, order) => { if (!order) return acc @@ -180,7 +185,7 @@ const useAllOrdersMap = ({ chainId }: GetOrdersParams): PartialOrdersMap => { const state = useOrdersStateNetwork(chainId) return useMemo(() => { - if (!state) return {} + if (!state) return EMPTY_ORDERS_MAP return flatOrdersStateNetwork(state) }, [state]) @@ -223,29 +228,23 @@ export const useCombinedPendingOrders = ({ creating: PartialOrdersMap } | undefined - >((state) => { - const ordersState = chainId && state.orders?.[chainId] - if (!ordersState) { - return - } + >((state) => state.orders?.[chainId]) - return { - pending: ordersState.pending || {}, - presignaturePending: ordersState.presignaturePending || {}, - creating: ordersState.creating || {}, - } - }) + return useSWR( + [state, account], + ([state, account]) => { + if (!state || !account) return EMPTY_ORDERS_ARRAY - return useMemo(() => { - if (!state || !account) return [] + const { pending, presignaturePending, creating } = state - const { pending, presignaturePending, creating } = state - const allPending = Object.values(pending).concat(Object.values(presignaturePending)).concat(Object.values(creating)) + const allPending = Object.values({ ...pending, ...presignaturePending, ...creating }) - return allPending.map(deserializeOrder).filter((order) => { - return order?.owner.toLowerCase() === account.toLowerCase() - }) as Order[] - }, [state, account]) + return allPending.map(deserializeOrder).filter((order) => { + return order?.owner.toLowerCase() === account.toLowerCase() + }) as Order[] + }, + { ...SWR_NO_REFRESH_OPTIONS, fallbackData: EMPTY_ORDERS_ARRAY }, + ).data } /** @@ -258,11 +257,11 @@ export const useCombinedPendingOrders = ({ */ export const useOnlyPendingOrders = (chainId: SupportedChainId): Order[] => { const state = useSelector( - (state) => chainId && state.orders?.[chainId]?.pending + (state) => chainId && state.orders?.[chainId]?.pending, ) return useMemo(() => { - if (!state) return [] + if (!state) return EMPTY_ORDERS_ARRAY return Object.values(state).map(deserializeOrder).filter(isTruthy) }, [state]) @@ -270,11 +269,11 @@ export const useOnlyPendingOrders = (chainId: SupportedChainId): Order[] => { export const useCancelledOrders = ({ chainId }: GetOrdersParams): Order[] => { const state = useSelector( - (state) => chainId && state.orders?.[chainId]?.cancelled + (state) => chainId && state.orders?.[chainId]?.cancelled, ) return useMemo(() => { - if (!state) return [] + if (!state) return EMPTY_ORDERS_ARRAY return Object.values(state).map(deserializeOrder).filter(isTruthy) }, [state]) @@ -282,11 +281,11 @@ export const useCancelledOrders = ({ chainId }: GetOrdersParams): Order[] => { export const useExpiredOrders = ({ chainId }: GetOrdersParams): Order[] => { const state = useSelector( - (state) => chainId && state.orders?.[chainId]?.expired + (state) => chainId && state.orders?.[chainId]?.expired, ) return useMemo(() => { - if (!state) return [] + if (!state) return EMPTY_ORDERS_ARRAY return Object.values(state).map(deserializeOrder).filter(isTruthy) }, [state]) @@ -303,7 +302,7 @@ export const useAddOrUpdateOrders = (): AddOrUpdateOrdersCallback => { })) dispatch(addOrUpdateOrders({ ...params, orders })) }, - [dispatch] + [dispatch], ) } @@ -313,7 +312,7 @@ export const useAddPendingOrder = (): AddOrderCallback => { (addOrderParams: AddUnserialisedPendingOrderParams) => { addPendingOrderStep(addOrderParams, dispatch) }, - [dispatch] + [dispatch], ) } @@ -327,7 +326,7 @@ export const useFulfillOrdersBatch = (): FulfillOrdersBatchCallback => { const dispatch = useDispatch() return useCallback( (fulfillOrdersBatchParams: FulfillOrdersBatchParams) => dispatch(fulfillOrdersBatch(fulfillOrdersBatchParams)), - [dispatch] + [dispatch], ) } @@ -340,7 +339,7 @@ export const useUpdatePresignGnosisSafeTx = (): UpdatePresignGnosisSafeTxCallbac const dispatch = useDispatch() return useCallback( (params: UpdatePresignGnosisSafeTxParams) => dispatch(updatePresignGnosisSafeTx(params)), - [dispatch] + [dispatch], ) } @@ -348,7 +347,7 @@ export const useExpireOrdersBatch = (): ExpireOrdersBatchCallback => { const dispatch = useDispatch() return useCallback( (expireOrdersBatchParams: ExpireOrdersBatchParams) => dispatch(expireOrdersBatch(expireOrdersBatchParams)), - [dispatch] + [dispatch], ) } @@ -356,7 +355,7 @@ export const useCancelOrdersBatch = (): CancelOrdersBatchCallback => { const dispatch = useDispatch() return useCallback( (cancelOrdersBatchParams: CancelOrdersBatchParams) => dispatch(cancelOrdersBatch(cancelOrdersBatchParams)), - [dispatch] + [dispatch], ) } @@ -375,7 +374,7 @@ export const useRequestOrderCancellation = (): CancelOrderCallback => { const dispatch = useDispatch() return useCallback( (cancelOrderParams: CancelOrderParams) => dispatch(requestOrderCancellation(cancelOrderParams)), - [dispatch] + [dispatch], ) } diff --git a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts index b6f4ee84e6..61df713d5d 100644 --- a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts @@ -1,11 +1,10 @@ import { useCallback } from 'react' -import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { useDispatch, useSelector } from 'react-redux' import { - ClearQuoteParams, getNewQuote, GetQuoteParams, refreshQuote, @@ -26,16 +25,8 @@ type SetQuoteErrorCallback = (params: SetQuoteErrorParams) => void type QuoteParams = { chainId?: ChainId; token?: string | null } -export const useAllQuotes = ({ - chainId, -}: Partial>): Partial | undefined => { - return useSelector | undefined>((state) => { - const quotes = chainId && state.price.quotes[chainId] - - if (!quotes) return {} - - return quotes - }) +export const useAllQuotes = (chainId: SupportedChainId): Partial | undefined => { + return useSelector | undefined>((state) => state.price.quotes[chainId]) } export const useQuote = ({ token, chainId }: QuoteParams): QuoteInformationObject | undefined => { diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index 15fd6f7921..ceb55b05ff 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -164,8 +164,6 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { label: outputCurrencyInfo.label, } - console.debug('RENDER LIMIT ORDERS WIDGET', { inputCurrencyInfo, outputCurrencyInfo }) - const slots = { settingsWidget: , lockScreen: isUnlocked ? undefined : ( diff --git a/libs/analytics/src/googleAnalytics/CowAnalyticsGoogle.ts b/libs/analytics/src/googleAnalytics/CowAnalyticsGoogle.ts index 4193b02e9c..38cbf72b48 100644 --- a/libs/analytics/src/googleAnalytics/CowAnalyticsGoogle.ts +++ b/libs/analytics/src/googleAnalytics/CowAnalyticsGoogle.ts @@ -45,8 +45,6 @@ export class CowAnalyticsGoogle implements CowAnalytics { return acc }, {} as CowDimensionValues) - // Init Google analytics - console.log('[CowAnalyticsGoogle] Init analytics: ', googleAnalyticsId) ReactGA.initialize(googleAnalyticsId, options) } setUserAccount(account: string | undefined): void { diff --git a/libs/wallet/src/web3-react/updaters/HwAccountIndexUpdater.tsx b/libs/wallet/src/web3-react/updaters/HwAccountIndexUpdater.tsx index bd21445f9a..49becd18df 100644 --- a/libs/wallet/src/web3-react/updaters/HwAccountIndexUpdater.tsx +++ b/libs/wallet/src/web3-react/updaters/HwAccountIndexUpdater.tsx @@ -42,7 +42,6 @@ export function HwAccountIndexUpdater() { useEffect(() => { if (account) return - console.debug('[Hardware wallet] reset account index to 0') setHwAccountIndex(0) }, [setHwAccountIndex, account]) From 54c0f6c42937840e48e95d85c139874ca8b76737 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 10 Oct 2024 09:44:11 +0100 Subject: [PATCH 017/116] feat: add balancer token list to arb1 (#4975) --- libs/tokens/src/const/tokensList.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/tokens/src/const/tokensList.json b/libs/tokens/src/const/tokensList.json index 1c68d44c55..9789cd9fb9 100644 --- a/libs/tokens/src/const/tokensList.json +++ b/libs/tokens/src/const/tokensList.json @@ -100,6 +100,10 @@ { "priority": 4, "source": "https://curvefi.github.io/curve-assets/arbitrum.json" + }, + { + "priority": 5, + "source": "https://raw.githubusercontent.com/balancer/tokenlists/refs/heads/main/generated/balancer.tokenlist.json" } ], "11155111": [ From 64e58c6a6995c8b91792aad5e68129cbf30b7c3a Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:54:03 +0100 Subject: [PATCH 018/116] feat: improve mobile rendering of custom recipient (#4954) --- .../src/modules/trade/pure/RecipientRow/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/trade/pure/RecipientRow/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/RecipientRow/index.tsx index 17367f8445..ac03804d2e 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/RecipientRow/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/RecipientRow/index.tsx @@ -7,6 +7,7 @@ import { Nullish } from 'types' const Row = styled.div` display: flex; + width: 100%; flex-direction: row; justify-content: space-between; align-items: center; @@ -49,7 +50,7 @@ export function RecipientRow(props: RecipientRowProps) { href={getExplorerLink(chainId, recipient, ExplorerDataType.ADDRESS)} target="_blank" > - {isAddress(recipient) ? shortenAddress(recipient) : recipient} + {isAddress(recipient) ? shortenAddress(recipient) : recipient} ↗
From a30ec1195d5b49364e24d7728603477aa3b61968 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 10 Oct 2024 16:36:09 +0500 Subject: [PATCH 019/116] chore: release main (#4980) --- .release-please-manifest.json | 16 ++++++++-------- apps/cow-fi/CHANGELOG.md | 7 +++++++ apps/cow-fi/package.json | 2 +- apps/cowswap-frontend/CHANGELOG.md | 18 ++++++++++++++++++ apps/cowswap-frontend/package.json | 2 +- apps/explorer/CHANGELOG.md | 8 ++++++++ apps/explorer/package.json | 2 +- libs/abis/CHANGELOG.md | 7 +++++++ libs/abis/package.json | 2 +- libs/hook-dapp-lib/CHANGELOG.md | 11 +++++++++++ libs/hook-dapp-lib/package.json | 2 +- libs/permit-utils/CHANGELOG.md | 7 +++++++ libs/permit-utils/package.json | 2 +- libs/tokens/CHANGELOG.md | 12 ++++++++++++ libs/tokens/package.json | 2 +- libs/ui/CHANGELOG.md | 7 +++++++ libs/ui/package.json | 2 +- 17 files changed, 93 insertions(+), 16 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b6c71c5eff..b1ec6ed43d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,7 +1,7 @@ { - "apps/cowswap-frontend": "1.84.0", - "apps/explorer": "2.34.1", - "libs/permit-utils": "0.3.1", + "apps/cowswap-frontend": "1.85.0", + "apps/explorer": "2.35.0", + "libs/permit-utils": "0.4.0", "libs/widget-lib": "0.15.0", "libs/widget-react": "0.11.0", "apps/widget-configurator": "1.7.2", @@ -14,15 +14,15 @@ "libs/ens": "1.2.0", "libs/events": "1.5.0", "libs/snackbars": "1.1.0", - "libs/tokens": "1.9.0", + "libs/tokens": "1.10.0", "libs/types": "1.2.0", - "libs/ui": "1.10.0", + "libs/ui": "1.10.1", "libs/wallet": "1.6.0", - "apps/cow-fi": "1.14.0", + "apps/cow-fi": "1.15.0", "libs/wallet-provider": "1.0.0", "libs/ui-utils": "1.1.0", - "libs/abis": "1.1.0", + "libs/abis": "1.2.0", "libs/balances-and-allowances": "1.0.0", "libs/iframe-transport": "1.0.0", - "libs/hook-dapp-lib": "1.0.0" + "libs/hook-dapp-lib": "1.1.0" } diff --git a/apps/cow-fi/CHANGELOG.md b/apps/cow-fi/CHANGELOG.md index 9c53ae64be..26e4c30a12 100644 --- a/apps/cow-fi/CHANGELOG.md +++ b/apps/cow-fi/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.15.0](https://github.com/cowprotocol/cowswap/compare/cow-fi-v1.14.0...cow-fi-v1.15.0) (2024-10-10) + + +### Features + +* **cow.fi:** modify sitemap lastmod and lazy load twitter js ([#4951](https://github.com/cowprotocol/cowswap/issues/4951)) ([6017924](https://github.com/cowprotocol/cowswap/commit/6017924aec4df1a53181b5ab4c818afc5d2c5091)) + ## [1.14.0](https://github.com/cowprotocol/cowswap/compare/cow-fi-v1.13.0...cow-fi-v1.14.0) (2024-09-30) diff --git a/apps/cow-fi/package.json b/apps/cow-fi/package.json index cd45ab4fbe..d3de75f713 100644 --- a/apps/cow-fi/package.json +++ b/apps/cow-fi/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cow-fi", - "version": "1.14.0", + "version": "1.15.0", "description": "CoW DAO website", "main": "index.js", "author": "", diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index 275af45a13..ad2177ef72 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [1.85.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.84.0...cowswap-v1.85.0) (2024-10-10) + + +### Features + +* **hooks-store:** add dapp id to hook callData ([#4920](https://github.com/cowprotocol/cowswap/issues/4920)) ([7111756](https://github.com/cowprotocol/cowswap/commit/7111756e359a8e52daa674068f99217efe27ee5b)) +* **hooks-store:** adjust hook details and rescue funds styles ([#4948](https://github.com/cowprotocol/cowswap/issues/4948)) ([aedc8d1](https://github.com/cowprotocol/cowswap/commit/aedc8d14c9b8dc3b25f964985b41b25229fd1547)) +* **hooks-store:** style hook details ([#4932](https://github.com/cowprotocol/cowswap/issues/4932)) ([83184d2](https://github.com/cowprotocol/cowswap/commit/83184d23da3c812eff87bfc0ec5a2832af0ff235)) +* **hooks-store:** use dappId from hook model to match with dapp ([#4938](https://github.com/cowprotocol/cowswap/issues/4938)) ([46699cb](https://github.com/cowprotocol/cowswap/commit/46699cbe6df02b0f7a3c6c380a04842e9f403a88)) +* improve mobile rendering of custom recipient ([#4954](https://github.com/cowprotocol/cowswap/issues/4954)) ([64e58c6](https://github.com/cowprotocol/cowswap/commit/64e58c6a6995c8b91792aad5e68129cbf30b7c3a)) +* rescue funds from CoW Shed Proxy ([#4935](https://github.com/cowprotocol/cowswap/issues/4935)) ([5fb7f34](https://github.com/cowprotocol/cowswap/commit/5fb7f344bec8dfd26177f62c765ed1e589c56a56)) +* **swap:** display order hooks details ([#4925](https://github.com/cowprotocol/cowswap/issues/4925)) ([1e776fc](https://github.com/cowprotocol/cowswap/commit/1e776fc4f6dfb28eebf881e79bb45dbfd693e472)) + + +### Bug Fixes + +* **tokens-selector:** fix tokens displaying on mobile view ([#4929](https://github.com/cowprotocol/cowswap/issues/4929)) ([f055957](https://github.com/cowprotocol/cowswap/commit/f055957af450967b4bc4d58a15fc7a7b80f0aa77)) + ## [1.84.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.83.0...cowswap-v1.84.0) (2024-09-30) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index 4e2f0df0d2..e3d977b4f7 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.84.0", + "version": "1.85.0", "description": "CoW Swap", "main": "index.js", "author": "", diff --git a/apps/explorer/CHANGELOG.md b/apps/explorer/CHANGELOG.md index a28a3db77f..ef7ef6517a 100644 --- a/apps/explorer/CHANGELOG.md +++ b/apps/explorer/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.35.0](https://github.com/cowprotocol/cowswap/compare/explorer-v2.34.1...explorer-v2.35.0) (2024-10-10) + + +### Features + +* **explorer:** display order hooks details ([#4921](https://github.com/cowprotocol/cowswap/issues/4921)) ([9c364bd](https://github.com/cowprotocol/cowswap/commit/9c364bd81f2e392a8cece06f6470734ee3d7623c)) +* **hooks-store:** adjust hook details and rescue funds styles ([#4948](https://github.com/cowprotocol/cowswap/issues/4948)) ([aedc8d1](https://github.com/cowprotocol/cowswap/commit/aedc8d14c9b8dc3b25f964985b41b25229fd1547)) + ## [2.34.1](https://github.com/cowprotocol/cowswap/compare/explorer-v2.34.0...explorer-v2.34.1) (2024-09-30) diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 6d405cff33..9fb7310293 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/explorer", - "version": "2.34.1", + "version": "2.35.0", "description": "CoW Swap Explorer", "main": "src/main.tsx", "author": "", diff --git a/libs/abis/CHANGELOG.md b/libs/abis/CHANGELOG.md index 2ddfacdac5..76115e0dd3 100644 --- a/libs/abis/CHANGELOG.md +++ b/libs/abis/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.2.0](https://github.com/cowprotocol/cowswap/compare/cowswap-abis-v1.1.0...cowswap-abis-v1.2.0) (2024-10-10) + + +### Features + +* rescue funds from CoW Shed Proxy ([#4935](https://github.com/cowprotocol/cowswap/issues/4935)) ([5fb7f34](https://github.com/cowprotocol/cowswap/commit/5fb7f344bec8dfd26177f62c765ed1e589c56a56)) + ## [1.1.0](https://github.com/cowprotocol/cowswap/compare/cowswap-abis-v1.0.0...cowswap-abis-v1.1.0) (2024-09-30) diff --git a/libs/abis/package.json b/libs/abis/package.json index 73b069d447..55b172e13f 100644 --- a/libs/abis/package.json +++ b/libs/abis/package.json @@ -1,5 +1,5 @@ { "name": "@cowprotocol/cowswap-abis", - "version": "1.1.0", + "version": "1.2.0", "type": "commonjs" } diff --git a/libs/hook-dapp-lib/CHANGELOG.md b/libs/hook-dapp-lib/CHANGELOG.md index 628f2b9be8..92bc14bcf4 100644 --- a/libs/hook-dapp-lib/CHANGELOG.md +++ b/libs/hook-dapp-lib/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.1.0](https://github.com/cowprotocol/cowswap/compare/hook-dapp-lib-v1.0.0...hook-dapp-lib-v1.1.0) (2024-10-10) + + +### Features + +* **explorer:** display order hooks details ([#4921](https://github.com/cowprotocol/cowswap/issues/4921)) ([9c364bd](https://github.com/cowprotocol/cowswap/commit/9c364bd81f2e392a8cece06f6470734ee3d7623c)) +* **hooks-store:** add dapp id to hook callData ([#4920](https://github.com/cowprotocol/cowswap/issues/4920)) ([7111756](https://github.com/cowprotocol/cowswap/commit/7111756e359a8e52daa674068f99217efe27ee5b)) +* **hooks-store:** style hook details ([#4932](https://github.com/cowprotocol/cowswap/issues/4932)) ([83184d2](https://github.com/cowprotocol/cowswap/commit/83184d23da3c812eff87bfc0ec5a2832af0ff235)) +* **hooks-store:** use dappId from hook model to match with dapp ([#4938](https://github.com/cowprotocol/cowswap/issues/4938)) ([46699cb](https://github.com/cowprotocol/cowswap/commit/46699cbe6df02b0f7a3c6c380a04842e9f403a88)) +* **swap:** display order hooks details ([#4925](https://github.com/cowprotocol/cowswap/issues/4925)) ([1e776fc](https://github.com/cowprotocol/cowswap/commit/1e776fc4f6dfb28eebf881e79bb45dbfd693e472)) + ## 1.0.0 (2024-09-30) diff --git a/libs/hook-dapp-lib/package.json b/libs/hook-dapp-lib/package.json index 96aae7a478..f55cdc77df 100644 --- a/libs/hook-dapp-lib/package.json +++ b/libs/hook-dapp-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/hook-dapp-lib", - "version": "1.0.0-RC1", + "version": "1.1.0", "type": "commonjs", "description": "CoW Swap Hook Dapp Library. Allows you to develop pre/post hooks dapps for CoW Protocol.", "main": "index.js", diff --git a/libs/permit-utils/CHANGELOG.md b/libs/permit-utils/CHANGELOG.md index 6a46cf1ff0..474269ba0e 100644 --- a/libs/permit-utils/CHANGELOG.md +++ b/libs/permit-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.4.0](https://github.com/cowprotocol/cowswap/compare/permit-utils-v0.3.1...permit-utils-v0.4.0) (2024-10-10) + + +### Features + +* **hooks-store:** use dappId from hook model to match with dapp ([#4938](https://github.com/cowprotocol/cowswap/issues/4938)) ([46699cb](https://github.com/cowprotocol/cowswap/commit/46699cbe6df02b0f7a3c6c380a04842e9f403a88)) + ## [0.3.1](https://github.com/cowprotocol/cowswap/compare/permit-utils-v0.3.0...permit-utils-v0.3.1) (2024-07-17) diff --git a/libs/permit-utils/package.json b/libs/permit-utils/package.json index a1cccf9949..7bbeb4ac5a 100644 --- a/libs/permit-utils/package.json +++ b/libs/permit-utils/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/permit-utils", - "version": "0.3.1", + "version": "0.4.0", "type": "module", "dependencies": { "ethers": "^5.7.2", diff --git a/libs/tokens/CHANGELOG.md b/libs/tokens/CHANGELOG.md index efb4100137..8d964b2abe 100644 --- a/libs/tokens/CHANGELOG.md +++ b/libs/tokens/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.10.0](https://github.com/cowprotocol/cowswap/compare/tokens-v1.9.0...tokens-v1.10.0) (2024-10-10) + + +### Features + +* add balancer token list to arb1 ([#4975](https://github.com/cowprotocol/cowswap/issues/4975)) ([54c0f6c](https://github.com/cowprotocol/cowswap/commit/54c0f6c42937840e48e95d85c139874ca8b76737)) + + +### Bug Fixes + +* **tokens-selector:** fix tokens displaying on mobile view ([#4929](https://github.com/cowprotocol/cowswap/issues/4929)) ([f055957](https://github.com/cowprotocol/cowswap/commit/f055957af450967b4bc4d58a15fc7a7b80f0aa77)) + ## [1.9.0](https://github.com/cowprotocol/cowswap/compare/tokens-v1.8.1...tokens-v1.9.0) (2024-09-30) diff --git a/libs/tokens/package.json b/libs/tokens/package.json index 2f641f46b1..f78001ec0b 100644 --- a/libs/tokens/package.json +++ b/libs/tokens/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/tokens", - "version": "1.9.0", + "version": "1.10.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/ui/CHANGELOG.md b/libs/ui/CHANGELOG.md index 3486145b87..4d9fbe85eb 100644 --- a/libs/ui/CHANGELOG.md +++ b/libs/ui/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.10.1](https://github.com/cowprotocol/cowswap/compare/ui-v1.10.0...ui-v1.10.1) (2024-10-10) + + +### Bug Fixes + +* **tokens-selector:** fix tokens displaying on mobile view ([#4929](https://github.com/cowprotocol/cowswap/issues/4929)) ([f055957](https://github.com/cowprotocol/cowswap/commit/f055957af450967b4bc4d58a15fc7a7b80f0aa77)) + ## [1.10.0](https://github.com/cowprotocol/cowswap/compare/ui-v1.9.0...ui-v1.10.0) (2024-09-30) diff --git a/libs/ui/package.json b/libs/ui/package.json index 1b2a6fe9d0..7633ab1d29 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/ui", - "version": "1.10.0", + "version": "1.10.1", "main": "./index.js", "types": "./index.d.ts", "exports": { From 7b2a49c41ecfd62107a3128e771003743094d246 Mon Sep 17 00:00:00 2001 From: Leandro Date: Fri, 11 Oct 2024 11:02:18 +0100 Subject: [PATCH 020/116] feat(slippage): small order slippage v2 (#4934) * fix: ignore quote metadata when deciding to to trigger a new quote * fix: memoize useGetQuoteAndStatus response * feat: use smart slippage based on trade size in relation to fee * feat: show loading indicator on suggested slippage when trade is loading * chore: fix cosmos build * chore: fix lint * feat: use smart slippage based on fee amount % * feat: use fee multiplier factor from launch darkly * refactor: exit earlier if LD multiplier is falsy * fix: return undefined and avoid setting smart slippage when both are missing * feat: cap slippage at 50% * refactor: split FeesUpdater * refactor: use useSafeMemoObject instead of useMemo * refactor: avoid repeating the same code * refactor: split SmartSlippageUpdater * refactor: extract calculateBpsFromFeeMultiplier and added unittests * feat: cap the sum of calculated slippages at 50% * refactor: rename variable to calculateBpsFromFeeMultiplier * fix: fix before and after as they don't mean the same thing for buy and sell orders * fix: reset smart slippage when both are disabled * fix: don't update smart slippage if trade review modal is open * feat: set max suggested slippage to 5% (down from 50%) * feat: set min suggested slippage to 0.5% * refactor: adjust import * chore: fix lint * feat: show warning when >2% slippage is suggested * feat: do not show warning while price is loading * feat: do not show warning when not connected --- .../{FeesUpdater.ts => FeesUpdater/index.ts} | 94 +------------------ .../FeesUpdater/isRefetchQuoteRequired.ts | 63 +++++++++++++ .../FeesUpdater/quoteUsingSameParameters.ts | 70 ++++++++++++++ .../legacy/components/SwapWarnings/index.tsx | 29 +++++- .../src/legacy/state/price/hooks.ts | 8 +- .../src/modules/appData/index.ts | 1 + .../swap/containers/Row/RowSlippage/index.tsx | 5 +- .../swap/containers/SwapWidget/index.tsx | 7 +- .../src/modules/swap/hooks/useSwapState.tsx | 1 - .../Row/RowSlippageContent/index.cosmos.tsx | 1 + .../pure/Row/RowSlippageContent/index.tsx | 68 ++++++++++---- .../src/modules/swap/pure/warnings.tsx | 9 +- .../calculateBpsFromFeeMultiplier.test.ts | 51 ++++++++++ .../calculateBpsFromFeeMultiplier.ts | 47 ++++++++++ .../updaters/SmartSlippageUpdater/index.ts | 86 +++++++++++++++++ .../useSmartSlippageFromBff.ts} | 18 +--- .../useSmartSlippageFromFeeMultiplier.ts | 28 ++++++ 17 files changed, 455 insertions(+), 131 deletions(-) rename apps/cowswap-frontend/src/common/updaters/{FeesUpdater.ts => FeesUpdater/index.ts} (63%) create mode 100644 apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts create mode 100644 apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts rename apps/cowswap-frontend/src/modules/swap/updaters/{SmartSlippageUpdater.ts => SmartSlippageUpdater/useSmartSlippageFromBff.ts} (74%) create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts similarity index 63% rename from apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts rename to apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts index a45e3bc669..4eeb7b8030 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts @@ -12,8 +12,6 @@ import ms from 'ms.macro' import { useRefetchQuoteCallback } from 'legacy/hooks/useRefetchPriceCallback' import { useAllQuotes, useIsBestQuoteLoading, useSetQuoteError } from 'legacy/state/price/hooks' -import { QuoteInformationObject } from 'legacy/state/price/reducer' -import { LegacyFeeQuoteParams } from 'legacy/state/price/types' import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { useUserTransactionTTL } from 'legacy/state/user/hooks' @@ -22,95 +20,11 @@ import { useAppData } from 'modules/appData' import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useDerivedSwapInfo, useSwapState } from 'modules/swap/hooks/useSwapState' +import { isRefetchQuoteRequired } from './isRefetchQuoteRequired' +import { quoteUsingSameParameters } from './quoteUsingSameParameters' + export const TYPED_VALUE_DEBOUNCE_TIME = 350 export const SWAP_QUOTE_CHECK_INTERVAL = ms`30s` // Every 30s -const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire -const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s) - -type FeeQuoteParams = Omit - -/** - * Returns if the quote has been recently checked - */ -function wasQuoteCheckedRecently(lastQuoteCheck: number): boolean { - return lastQuoteCheck + WAITING_TIME_BETWEEN_EQUAL_REQUESTS > Date.now() -} - -/** - * Returns true if the fee quote expires soon (in less than RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME milliseconds) - */ -function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): boolean { - const feeExpirationDate = Date.parse(quoteExpirationIsoDate) - return feeExpirationDate <= Date.now() + threshold -} - -/** - * Checks if the parameters for the current quote are correct - * - * Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched - */ -function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean { - const { - amount: currentAmount, - sellToken: currentSellToken, - buyToken: currentBuyToken, - kind: currentKind, - userAddress: currentUserAddress, - receiver: currentReceiver, - appData: currentAppData, - } = currentParams - const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo - const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true - - // cache the base quote params without quoteInfo user address to check - const paramsWithoutAddress = - sellToken === currentSellToken && - buyToken === currentBuyToken && - amount === currentAmount && - kind === currentKind && - appData === currentAppData && - hasSameReceiver - // 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without - // in case user is not connected - return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress -} - -/** - * Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state) - */ -function isRefetchQuoteRequired( - isLoading: boolean, - currentParams: FeeQuoteParams, - quoteInformation?: QuoteInformationObject -): boolean { - // If there's no quote/fee information, we always re-fetch - if (!quoteInformation) { - return true - } - - if (!quoteUsingSameParameters(currentParams, quoteInformation)) { - // If the current parameters don't match the fee, the fee information is invalid and needs to be re-fetched - return true - } - - // The query params are the same, so we only ask for a new quote if: - // - If the quote was not queried recently - // - There's not another price query going on right now - // - The quote will expire soon - if (wasQuoteCheckedRecently(quoteInformation.lastCheck)) { - // Don't Re-fetch if it was queried recently - return false - } else if (isLoading) { - // Don't Re-fetch if there's another quote going on with the same params - // It's better to wait for the timeout or resolution. Also prevents an issue of refreshing too fast with slow APIs - return false - } else if (quoteInformation.fee) { - // Re-fetch if the fee is expiring soon - return isExpiringSoon(quoteInformation.fee.expirationDate, RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME) - } - - return false -} export function FeesUpdater(): null { const { chainId, account } = useWalletInfo() @@ -219,7 +133,7 @@ export function FeesUpdater(): null { // Callback to re-fetch both the fee and the price const refetchQuoteIfRequired = () => { // if no token is unsupported and needs refetching - const hasToRefetch = !unsupportedToken && isRefetchQuoteRequired(isLoading, quoteParams, quoteInfo) + const hasToRefetch = !unsupportedToken && isRefetchQuoteRequired(isLoading, quoteParams, quoteInfo) // if (hasToRefetch) { // Decide if this is a new quote, or just a refresh diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts new file mode 100644 index 0000000000..5b4714631b --- /dev/null +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts @@ -0,0 +1,63 @@ +import ms from 'ms.macro' + +import { QuoteInformationObject } from 'legacy/state/price/reducer' +import { LegacyFeeQuoteParams } from 'legacy/state/price/types' + +import { quoteUsingSameParameters } from './quoteUsingSameParameters' + +const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire +const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s) + +type FeeQuoteParams = Omit + +/** + * Returns if the quote has been recently checked + */ +function wasQuoteCheckedRecently(lastQuoteCheck: number): boolean { + return lastQuoteCheck + WAITING_TIME_BETWEEN_EQUAL_REQUESTS > Date.now() +} + +/** + * Returns true if the fee quote expires soon (in less than RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME milliseconds) + */ +function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): boolean { + const feeExpirationDate = Date.parse(quoteExpirationIsoDate) + return feeExpirationDate <= Date.now() + threshold +} + +/** + * Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state) + */ +export function isRefetchQuoteRequired( + isLoading: boolean, + currentParams: FeeQuoteParams, + quoteInformation?: QuoteInformationObject, +): boolean { + // If there's no quote/fee information, we always re-fetch + if (!quoteInformation) { + return true + } + + if (!quoteUsingSameParameters(currentParams, quoteInformation)) { + // If the current parameters don't match the fee, the fee information is invalid and needs to be re-fetched + return true + } + + // The query params are the same, so we only ask for a new quote if: + // - If the quote was not queried recently + // - There's not another price query going on right now + // - The quote will expire soon + if (wasQuoteCheckedRecently(quoteInformation.lastCheck)) { + // Don't Re-fetch if it was queried recently + return false + } else if (isLoading) { + // Don't Re-fetch if there's another quote going on with the same params + // It's better to wait for the timeout or resolution. Also prevents an issue of refreshing too fast with slow APIs + return false + } else if (quoteInformation.fee) { + // Re-fetch if the fee is expiring soon + return isExpiringSoon(quoteInformation.fee.expirationDate, RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME) + } + + return false +} diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts new file mode 100644 index 0000000000..b41f5e0a66 --- /dev/null +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts @@ -0,0 +1,70 @@ +import { QuoteInformationObject } from 'legacy/state/price/reducer' +import { LegacyFeeQuoteParams } from 'legacy/state/price/types' + +import { decodeAppData } from 'modules/appData' + +type FeeQuoteParams = Omit + +/** + * Checks if the parameters for the current quote are correct + * + * Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched + */ +export function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean { + const { + amount: currentAmount, + sellToken: currentSellToken, + buyToken: currentBuyToken, + kind: currentKind, + userAddress: currentUserAddress, + receiver: currentReceiver, + appData: currentAppData, + } = currentParams + const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo + const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true + const hasSameAppData = compareAppDataWithoutQuoteData(appData, currentAppData) + + // cache the base quote params without quoteInfo user address to check + const paramsWithoutAddress = + sellToken === currentSellToken && + buyToken === currentBuyToken && + amount === currentAmount && + kind === currentKind && + hasSameAppData && + hasSameReceiver + // 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without + // in case user is not connected + return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress +} + +/** + * Compares appData without taking into account the `quote` metadata + */ +function compareAppDataWithoutQuoteData(a: T, b: T): boolean { + if (a === b) { + return true + } + const cleanedA = removeQuoteMetadata(a) + const cleanedB = removeQuoteMetadata(b) + + return cleanedA === cleanedB +} + +/** + * If appData is set and is valid, remove `quote` metadata from it + */ +function removeQuoteMetadata(appData: string | undefined): string | undefined { + if (!appData) { + return + } + + const decoded = decodeAppData(appData) + + if (!decoded) { + return + } + + const { metadata: fullMetadata, ...rest } = decoded + const { quote: _, ...metadata } = fullMetadata + return JSON.stringify({ ...rest, metadata }) +} diff --git a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx index fca6d9cf67..b2b1caa0e9 100644 --- a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx @@ -1,4 +1,3 @@ - import { Command } from '@cowprotocol/types' import { HoverTooltip } from '@cowprotocol/ui' import { Fraction } from '@uniswap/sdk-core' @@ -45,7 +44,7 @@ const WarningCheckboxContainer = styled.span` const WarningContainer = styled(AuxInformationContainer).attrs((props) => ({ ...props, hideInput: true, -})) ` +}))` --warningColor: ${({ theme, level }) => level === HIGH_TIER_FEE ? theme.danger @@ -171,3 +170,29 @@ export const HighFeeWarning = (props: WarningProps) => { ) } + +export type HighSuggestedSlippageWarningProps = { + isSuggestedSlippage: boolean | undefined + slippageBps: number | undefined + className?: string +} & HighFeeContainerProps + +export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarningProps) { + const { isSuggestedSlippage, slippageBps, ...rest } = props + + if (!isSuggestedSlippage || !slippageBps || slippageBps <= 200) { + return null + } + + return ( + +
+ + Beware! High dynamic slippage suggested ({`${slippageBps / 100}`}%) + + + +
+
+ ) +} diff --git a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts index 61df713d5d..2386fb8193 100644 --- a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts @@ -4,6 +4,8 @@ import { SupportedChainId, SupportedChainId as ChainId } from '@cowprotocol/cow- import { useDispatch, useSelector } from 'react-redux' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' + import { getNewQuote, GetQuoteParams, @@ -62,7 +64,11 @@ export const useGetQuoteAndStatus = (params: QuoteParams): UseGetQuoteAndStatus const isGettingNewQuote = Boolean(isLoading && !quote?.price?.amount) const isRefreshingQuote = Boolean(isLoading && quote?.price?.amount) - return { quote, isGettingNewQuote, isRefreshingQuote } + return useSafeMemoObject({ + quote, + isGettingNewQuote, + isRefreshingQuote, + }) } export const useGetNewQuote = (): GetNewQuoteCallback => { diff --git a/apps/cowswap-frontend/src/modules/appData/index.ts b/apps/cowswap-frontend/src/modules/appData/index.ts index da9816331c..6e5d063d5d 100644 --- a/apps/cowswap-frontend/src/modules/appData/index.ts +++ b/apps/cowswap-frontend/src/modules/appData/index.ts @@ -6,5 +6,6 @@ export { decodeAppData } from './utils/decodeAppData' export { replaceHooksOnAppData, buildAppData, removePermitHookFromAppData } from './utils/buildAppData' export { buildAppDataHooks } from './utils/buildAppDataHooks' export * from './utils/getAppDataHooks' +export * from './utils/decodeAppData' export { addPermitHookToHooks, removePermitHookFromHooks } from './utils/typedHooks' export type { AppDataInfo, UploadAppDataParams, TypedAppDataHooks } from './types' diff --git a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx index 681cec67ab..812b97be9e 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx @@ -10,6 +10,7 @@ import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' import { useSetSlippage } from 'modules/swap/hooks/useSetSlippage' import { useSmartSwapSlippage } from 'modules/swap/hooks/useSwapSlippage' +import { useTradePricesUpdate } from 'modules/swap/hooks/useTradePricesUpdate' import { RowSlippageContent } from 'modules/swap/pure/Row/RowSlippageContent' import useNativeCurrency from 'lib/hooks/useNativeCurrency' @@ -37,6 +38,7 @@ export function RowSlippage({ const smartSwapSlippage = useSmartSwapSlippage() const isSmartSlippageApplied = useIsSmartSlippageApplied() const setSlippage = useSetSlippage() + const isTradePriceUpdating = useTradePricesUpdate() const props = useMemo( () => ({ @@ -49,10 +51,11 @@ export function RowSlippage({ slippageTooltip, displaySlippage: `${formatPercent(allowedSlippage)}%`, isSmartSlippageApplied, + isSmartSlippageLoading: isTradePriceUpdating, smartSlippage: smartSwapSlippage && !isEoaEthFlow ? `${formatPercent(new Percent(smartSwapSlippage, 10_000))}%` : undefined, setAutoSlippage: smartSwapSlippage && !isEoaEthFlow ? () => setSlippage(null) : undefined, }), - [chainId, isEoaEthFlow, nativeCurrency.symbol, showSettingOnClick, allowedSlippage, slippageLabel, slippageTooltip, smartSwapSlippage, isSmartSlippageApplied] + [chainId, isEoaEthFlow, nativeCurrency.symbol, showSettingOnClick, allowedSlippage, slippageLabel, slippageTooltip, smartSwapSlippage, isSmartSlippageApplied, isTradePriceUpdating] ) 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 555a036e89..b9db3140fb 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -2,7 +2,7 @@ import { ReactNode, useCallback, useMemo, useState } from 'react' import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const' -import { isFractionFalsy } from '@cowprotocol/common-utils' +import { isFractionFalsy, percentToBps } from '@cowprotocol/common-utils' import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { useIsSafeViaWc, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { TradeType } from '@cowprotocol/widget-lib' @@ -43,6 +43,7 @@ import { SWAP_QUOTE_CHECK_INTERVAL } from 'common/updaters/FeesUpdater' import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { useIsSlippageModified } from '../../hooks/useIsSlippageModified' +import { useIsSmartSlippageApplied } from '../../hooks/useIsSmartSlippageApplied' import { useIsSwapEth } from '../../hooks/useIsSwapEth' import { useSwapSlippage } from '../../hooks/useSwapSlippage' import { @@ -223,6 +224,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const nativeCurrencySymbol = useNativeCurrency().symbol || 'ETH' const wrappedCurrencySymbol = useWrappedToken().symbol || 'WETH' + const isSuggestedSlippage = useIsSmartSlippageApplied() && !isTradePriceUpdating && !!account + const swapWarningsTopProps: SwapWarningsTopProps = { chainId, trade, @@ -242,6 +245,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { buyingFiatAmount, priceImpact: priceImpactParams.priceImpact, tradeUrlParams, + slippageBps: percentToBps(slippage), + isSuggestedSlippage, } const swapWarningsBottomProps: SwapWarningsBottomProps = { diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index c506885a3b..d0a98efd80 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -312,7 +312,6 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { } // compare input balance to max input based on version - // const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)] // mod const balanceIn = currencyBalances[Field.INPUT] const amountIn = slippageAdjustedSellAmount diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx index 40671ff0f1..f180f5effb 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx @@ -19,6 +19,7 @@ const defaultProps: RowSlippageContentProps = { setAutoSlippage: () => { console.log('setAutoSlippage called!') }, + isSmartSlippageLoading: false } export default diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index a3385962ef..c1105bc66c 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -1,7 +1,7 @@ import { INPUT_OUTPUT_EXPLANATION, MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' -import { HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' +import { CenteredDots, HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' @@ -64,7 +64,8 @@ export const getNonNativeSlippageTooltip = () => ( ) -const SUGGESTED_SLIPPAGE_TOOLTIP = "Based on recent volatility for the selected token pair, this is the suggested slippage for ensuring quick execution of your order." +const SUGGESTED_SLIPPAGE_TOOLTIP = + 'Based on recent volatility for the selected token pair, this is the suggested slippage for ensuring quick execution of your order.' export interface RowSlippageContentProps { chainId: SupportedChainId @@ -82,6 +83,7 @@ export interface RowSlippageContentProps { setAutoSlippage?: Command // todo: make them optional smartSlippage?: string isSmartSlippageApplied: boolean + isSmartSlippageLoading: boolean } // TODO: RowDeadlineContent and RowSlippageContent are very similar. Refactor and extract base component? @@ -101,6 +103,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) { setAutoSlippage, smartSlippage, isSmartSlippageApplied, + isSmartSlippageLoading, } = props const tooltipContent = @@ -109,14 +112,33 @@ export function RowSlippageContent(props: RowSlippageContentProps) { // In case the user happened to set the same slippage as the suggestion, do not show the suggestion const suggestedEqualToUserSlippage = smartSlippage && smartSlippage === displaySlippage - const displayDefaultSlippage = isSlippageModified && setAutoSlippage && smartSlippage && !suggestedEqualToUserSlippage && ( - - (Suggested: {smartSlippage}) - - - - - ) + const displayDefaultSlippage = isSlippageModified && + setAutoSlippage && + smartSlippage && + !suggestedEqualToUserSlippage && ( + + {isSmartSlippageLoading ? ( + + ) : ( + <> + (Suggested: {smartSlippage}) + + + + + )} + + ) + + const displaySlippageWithLoader = + isSmartSlippageLoading && isSmartSlippageApplied ? ( + + ) : ( + <> + {displaySlippage} + {displayDefaultSlippage} + + ) return ( @@ -124,10 +146,18 @@ export function RowSlippageContent(props: RowSlippageContentProps) { {showSettingOnClick ? ( - + ) : ( - + )} @@ -136,20 +166,20 @@ export function RowSlippageContent(props: RowSlippageContentProps) { {showSettingOnClick ? ( - - {displaySlippage}{displayDefaultSlippage} - + {displaySlippageWithLoader} ) : ( - - {displaySlippage}{displayDefaultSlippage} - + {displaySlippageWithLoader} )} ) } -type SlippageTextContentsProps = { isEoaEthFlow: boolean; slippageLabel?: React.ReactNode, isDynamicSlippageSet: boolean } +type SlippageTextContentsProps = { + isEoaEthFlow: boolean + slippageLabel?: React.ReactNode + isDynamicSlippageSet: boolean +} function SlippageTextContents({ isEoaEthFlow, slippageLabel, isDynamicSlippageSet }: SlippageTextContentsProps) { return ( diff --git a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx index 8ef2d09252..5f3b3db0df 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx @@ -7,7 +7,7 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import styled from 'styled-components/macro' -import { HighFeeWarning } from 'legacy/components/SwapWarnings' +import { HighFeeWarning, HighSuggestedSlippageWarning } from 'legacy/components/SwapWarnings' import TradeGp from 'legacy/state/swap/TradeGp' import { CompatibilityIssuesWarning } from 'modules/trade/pure/CompatibilityIssuesWarning' @@ -35,7 +35,11 @@ export interface SwapWarningsTopProps { buyingFiatAmount: CurrencyAmount | null priceImpact: Percent | undefined tradeUrlParams: TradeUrlParams + isSuggestedSlippage: boolean | undefined + slippageBps: number | undefined + setFeeWarningAccepted(cb: (state: boolean) => boolean): void + setImpactWarningAccepted(cb: (state: boolean) => boolean): void } @@ -70,6 +74,8 @@ export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps) buyingFiatAmount, priceImpact, tradeUrlParams, + isSuggestedSlippage, + slippageBps, } = props return ( @@ -80,6 +86,7 @@ export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps) acceptedStatus={feeWarningAccepted} acceptWarningCb={account ? () => setFeeWarningAccepted((state) => !state) : undefined} /> + {!hideUnknownImpactWarning && ( { + const sellAmount = CurrencyAmount.fromRawAmount(USDC[1], '1000000') // 1.2 USDC + const feeAmount = CurrencyAmount.fromRawAmount(USDC[1], '200000') // 0.2 USDC + const isSell = true + const multiplierPercentage = 50 + + it('should return undefined for missing parameters', () => { + expect(calculateBpsFromFeeMultiplier(undefined, feeAmount, isSell, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, undefined, isSell, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, feeAmount, undefined, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, 0)).toBeUndefined() + }) + + it('should return undefined for a negative multiplier percentage', () => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, -50) + expect(result).toBeUndefined() + }) + + it('should calculate the correct percentage for selling with different multiplier percentages', () => { + const testCases = [ + [25, 625], // 25%, 6.25% + [50, 1250], // 50%, 12.5% + [75, 1875], // 75%, 18.75% + ] + + testCases.forEach(([multiplier, expectedResult]) => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, multiplier) + expect(result).toBeDefined() + expect(result).toBe(expectedResult) + }) + }) + + it('should calculate the correct percentage for buying with different multiplier percentages', () => { + const testCases = [ + [25, 417], // 25%, 4.17% + [50, 833], // 50%, 8.33% + [75, 1250], // 75%, 12.5% + ] + + testCases.forEach(([multiplier, expectedResult]) => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, !isSell, multiplier) + expect(result).toBeDefined() + expect(result).toBe(expectedResult) + }) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts new file mode 100644 index 0000000000..73d161d60d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts @@ -0,0 +1,47 @@ +import { Currency, CurrencyAmount, Fraction } from '@uniswap/sdk-core' + +const ONE = new Fraction(1) + +export function calculateBpsFromFeeMultiplier( + sellAmount: CurrencyAmount | undefined, + feeAmount: CurrencyAmount | undefined, + isSell: boolean | undefined, + multiplierPercentage: number, +): number | undefined { + if (!sellAmount || !feeAmount || isSell === undefined || multiplierPercentage <= 0) { + return undefined + } + + const feeMultiplierFactor = new Fraction(100 + multiplierPercentage, 100) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction + + if (isSell) { + // sell + // 1 - ((sellAmount - feeAmount * 1.5) / (sellAmount - feeAmount)) + // 1 - (sellAmount - feeAmount * feeMultiplierFactor) / sellAmount - feeAmount + return percentageToBps( + ONE.subtract( + sellAmount + .subtract(feeAmount.multiply(feeMultiplierFactor)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(sellAmount.subtract(feeAmount).asFraction), + ), + ) + } else { + // buy + // (sellAmount + feeAmount * 1.5) / (sellAmount + feeAmount) - 1 + // ((sellAmount + feeAmount * feeMultiplierFactor) / (sellAmount - feeAmount)) - 1 + return percentageToBps( + sellAmount + .add(feeAmount.multiply(feeMultiplierFactor)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(sellAmount.add(feeAmount).asFraction) + .subtract(ONE), + ) + } +} + +function percentageToBps(value: Fraction | undefined): number | undefined { + const bps = value?.multiply(10_000).toFixed(0) + + return bps ? +bps : undefined +} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts new file mode 100644 index 0000000000..5c8b79700e --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -0,0 +1,86 @@ +import { useSetAtom } from 'jotai' +import { useEffect, useMemo } from 'react' + +import { useTradeConfirmState } from 'modules/trade' + +import { useSmartSlippageFromBff } from './useSmartSlippageFromBff' +import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMultiplier' + +import { useDerivedSwapInfo, useHighFeeWarning } from '../../hooks/useSwapState' +import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' + +const MAX_BPS = 500 // 5% +const MIN_BPS = 50 // 0.5% + +export function SmartSlippageUpdater() { + const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) + + const bffSlippageBps = useSmartSlippageFromBff() + // TODO: remove v1 + const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() + const feeMultiplierSlippageBps = useSmartSlippageFromFeeMultiplier() + + const { isOpen: isTradeReviewOpen } = useTradeConfirmState() + + useEffect(() => { + // Don't update it once review is open + if (isTradeReviewOpen) { + return + } + // If both are unset, don't use smart slippage + if (feeMultiplierSlippageBps === undefined && bffSlippageBps === undefined) { + setSmartSwapSlippage(null) + return + } + // Add both slippage values, when present + const slippage = (feeMultiplierSlippageBps || 0) + (bffSlippageBps || 0) + + setSmartSwapSlippage(Math.max(MIN_BPS, Math.min(slippage, MAX_BPS))) + }, [bffSlippageBps, setSmartSwapSlippage, feeMultiplierSlippageBps, isTradeReviewOpen]) + + // TODO: remove before merging + useEffect(() => { + console.log(`SmartSlippageUpdater`, { + granularSlippage: tradeSizeSlippageBpsV1, + fiftyPercentFeeSlippage: feeMultiplierSlippageBps, + bffSlippageBps, + }) + }, [tradeSizeSlippageBpsV1, feeMultiplierSlippageBps]) + + return null +} + +// TODO: remove +/** + * Calculates smart slippage in bps, based on trade size in relation to fee + */ +function useSmartSlippageFromFeePercentage(): number | undefined { + const { trade } = useDerivedSwapInfo() || {} + const { feePercentage } = useHighFeeWarning(trade) + + const percentage = feePercentage && +feePercentage.toFixed(3) + + return useMemo(() => { + if (percentage === undefined) { + // Unset, return undefined + return + } + if (percentage < 1) { + // bigger volume compared to the fee, trust on smart slippage from BFF + return + } else if (percentage < 5) { + // Between 1 and 5, 2% + return 200 + } else if (percentage < 10) { + // Between 5 and 10, 5% + return 500 + } else if (percentage < 20) { + // Between 10 and 20, 10% + return 1000 + } + // TODO: more granularity? + + // > 20%, cap it at 20% slippage + return 2000 + }, [percentage]) +} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts similarity index 74% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts rename to apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts index 7e79d89102..11a70cff07 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts @@ -1,6 +1,3 @@ -import { useSetAtom } from 'jotai' -import { useEffect } from 'react' - import { BFF_BASE_URL } from '@cowprotocol/common-const' import { useFeatureFlags } from '@cowprotocol/common-hooks' import { getCurrencyAddress } from '@cowprotocol/common-utils' @@ -11,8 +8,6 @@ import useSWR from 'swr' import { useDerivedTradeState, useIsWrapOrUnwrap } from 'modules/trade' -import { smartSwapSlippageAtom } from '../state/slippageValueAndTypeAtom' - const SWR_OPTIONS = { dedupingInterval: ms`1m`, } @@ -21,17 +16,16 @@ interface SlippageApiResponse { slippageBps: number } -export function SmartSlippageUpdater() { +export function useSmartSlippageFromBff(): number | undefined { const { isSmartSlippageEnabled } = useFeatureFlags() const { chainId } = useWalletInfo() const { inputCurrency, outputCurrency } = useDerivedTradeState() || {} - const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) const isWrapOrUnwrap = useIsWrapOrUnwrap() const sellTokenAddress = inputCurrency && getCurrencyAddress(inputCurrency).toLowerCase() const buyTokenAddress = outputCurrency && getCurrencyAddress(outputCurrency).toLowerCase() - const slippageBps = useSWR( + return useSWR( !sellTokenAddress || !buyTokenAddress || isWrapOrUnwrap || !isSmartSlippageEnabled ? null : [chainId, sellTokenAddress, buyTokenAddress], @@ -42,12 +36,6 @@ export function SmartSlippageUpdater() { return response.slippageBps }, - SWR_OPTIONS + SWR_OPTIONS, ).data - - useEffect(() => { - setSmartSwapSlippage(typeof slippageBps === 'number' ? slippageBps : null) - }, [slippageBps, setSmartSwapSlippage]) - - return null } diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts new file mode 100644 index 0000000000..f5566085a1 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts @@ -0,0 +1,28 @@ +import { useMemo } from 'react' + +import { useFeatureFlags } from '@cowprotocol/common-hooks' + +import { useReceiveAmountInfo } from 'modules/trade' + +import { calculateBpsFromFeeMultiplier } from './calculateBpsFromFeeMultiplier' + + +/** + * Calculates smart slippage in bps, based on quoted fee + * + * Apply a multiplying factor to the fee (e.g.: 50%), and from there calculate how much slippage would be needed + * for the limit price to take this much more fee. + * More relevant for small orders in relation to fee amount, negligent for larger orders. + */ +export function useSmartSlippageFromFeeMultiplier(): number | undefined { + const { beforeNetworkCosts, afterNetworkCosts, costs, isSell } = useReceiveAmountInfo() || {} + const sellAmount = isSell ? afterNetworkCosts?.sellAmount : beforeNetworkCosts?.sellAmount + const feeAmount = costs?.networkFee?.amountInSellCurrency + + const { smartSlippageFeeMultiplierPercentage = 50 } = useFeatureFlags() + + return useMemo( + () => calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, smartSlippageFeeMultiplierPercentage), + [isSell, sellAmount, feeAmount, smartSlippageFeeMultiplierPercentage], + ) +} From b960bde94570dba2a297ecb59a7a79cbbbfcefb8 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 14 Oct 2024 19:56:39 +0500 Subject: [PATCH 021/116] fix(explorer): do not crash when appData is not parsed (#4983) --- apps/explorer/src/components/orders/OrderHooksDetails/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx index da037bbec8..719eeb2df3 100644 --- a/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx +++ b/apps/explorer/src/components/orders/OrderHooksDetails/index.tsx @@ -17,7 +17,7 @@ interface OrderHooksDetailsProps { export function OrderHooksDetails({ appData, fullAppData, children }: OrderHooksDetailsProps) { const { appDataDoc } = useAppData(appData, fullAppData) - if (!appDataDoc) return null + if (!appDataDoc?.metadata) return null const metadata = appDataDoc.metadata as latest.Metadata From 6d4494ffeccd764768f32e7801398d36905191e4 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 14 Oct 2024 19:59:05 +0500 Subject: [PATCH 022/116] chore: release main (#4984) --- .release-please-manifest.json | 2 +- apps/explorer/CHANGELOG.md | 7 +++++++ apps/explorer/package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b1ec6ed43d..338c004e91 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { "apps/cowswap-frontend": "1.85.0", - "apps/explorer": "2.35.0", + "apps/explorer": "2.35.1", "libs/permit-utils": "0.4.0", "libs/widget-lib": "0.15.0", "libs/widget-react": "0.11.0", diff --git a/apps/explorer/CHANGELOG.md b/apps/explorer/CHANGELOG.md index ef7ef6517a..3c9f1687c5 100644 --- a/apps/explorer/CHANGELOG.md +++ b/apps/explorer/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.35.1](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.0...explorer-v2.35.1) (2024-10-14) + + +### Bug Fixes + +* **explorer:** do not crash when appData is not parsed ([#4983](https://github.com/cowprotocol/cowswap/issues/4983)) ([b960bde](https://github.com/cowprotocol/cowswap/commit/b960bde94570dba2a297ecb59a7a79cbbbfcefb8)) + ## [2.35.0](https://github.com/cowprotocol/cowswap/compare/explorer-v2.34.1...explorer-v2.35.0) (2024-10-10) diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 9fb7310293..34f4e6a3e7 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/explorer", - "version": "2.35.0", + "version": "2.35.1", "description": "CoW Swap Explorer", "main": "src/main.tsx", "author": "", From bd0cd0fc4ae551c4967a17c507a030dfa05acaf7 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 14 Oct 2024 21:53:10 +0500 Subject: [PATCH 023/116] chore: up events lib deps (#4986) --- libs/events/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/events/package.json b/libs/events/package.json index 1200458823..dc1d6d0916 100644 --- a/libs/events/package.json +++ b/libs/events/package.json @@ -20,6 +20,6 @@ "dex" ], "dependencies": { - "@cowprotocol/types": "^1.1.1-RC.0" + "@cowprotocol/types": "^1.2.0" } -} \ No newline at end of file +} From 4b89ecbf661e6c30193586c704e23c78b2bfc22b Mon Sep 17 00:00:00 2001 From: Leandro Date: Mon, 14 Oct 2024 17:57:52 +0100 Subject: [PATCH 024/116] feat(smart-slippage): update smart slippage text (#4982) * feat: update smart slippage related text * refactor: memoize fns * feat: when smart slippage is set, use that as limit for settings * refactor: remove unused consts * chore: remove dead code * fix: replace geat with settings icon * fix: always show the dynamic slippage text --- .../legacy/components/SwapWarnings/index.tsx | 9 +- .../components/TransactionSettings/index.tsx | 139 +++++++++--------- .../pure/Row/RowSlippageContent/index.tsx | 30 ++-- .../updaters/SmartSlippageUpdater/index.ts | 49 +----- libs/common-const/src/misc.ts | 22 +-- 5 files changed, 100 insertions(+), 149 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx index b2b1caa0e9..8fdc4411e9 100644 --- a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx @@ -188,8 +188,13 @@ export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarning
- Beware! High dynamic slippage suggested ({`${slippageBps / 100}`}%) - + Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution +
diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx index ab1060e8df..388726d36c 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx @@ -47,7 +47,7 @@ enum DeadlineError { InvalidInput = 'InvalidInput', } -const Option = styled(FancyButton) <{ active: boolean }>` +const Option = styled(FancyButton)<{ active: boolean }>` margin-right: 8px; :hover { @@ -75,7 +75,7 @@ export const Input = styled.input` text-align: right; ` -export const OptionCustom = styled(FancyButton) <{ active?: boolean; warning?: boolean }>` +export const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>` height: 2rem; position: relative; padding: 0 0.75rem; @@ -84,7 +84,7 @@ export const OptionCustom = styled(FancyButton) <{ active?: boolean; warning?: b :hover { border: ${({ theme, active, warning }) => - active && `1px solid ${warning ? darken(theme.error, 0.1) : darken(theme.bg2, 0.1)}`}; + active && `1px solid ${warning ? darken(theme.error, 0.1) : darken(theme.bg2, 0.1)}`}; } input { @@ -97,6 +97,7 @@ export const OptionCustom = styled(FancyButton) <{ active?: boolean; warning?: b const SlippageEmojiContainer = styled.span` color: #f3841e; + ${Media.upToSmall()} { display: none; } @@ -204,78 +205,84 @@ export function TransactionSettings() { const placeholderSlippage = isSlippageModified ? defaultSwapSlippage : swapSlippage - function parseSlippageInput(value: string) { - // populate what the user typed and clear the error - setSlippageInput(value) - setSlippageError(false) + const parseSlippageInput = useCallback( + (value: string) => { + // populate what the user typed and clear the error + setSlippageInput(value) + setSlippageError(false) - if (value.length === 0) { - slippageToleranceAnalytics('Default', placeholderSlippage.toFixed(2)) - setSwapSlippage(isEoaEthFlow ? percentToBps(minEthFlowSlippage) : null) - } else { - let v = value - - // Prevent inserting more than 2 decimal precision - if (value.split('.')[1]?.length > 2) { - // indexOf + 3 because we are cutting it off at `.XX` - v = value.slice(0, value.indexOf('.') + 3) - // Update the input to remove the extra numbers from UI input - setSlippageInput(v) - } + if (value.length === 0) { + slippageToleranceAnalytics('Default', placeholderSlippage.toFixed(2)) + setSwapSlippage(isEoaEthFlow ? percentToBps(minEthFlowSlippage) : null) + } else { + let v = value + + // Prevent inserting more than 2 decimal precision + if (value.split('.')[1]?.length > 2) { + // indexOf + 3 because we are cutting it off at `.XX` + v = value.slice(0, value.indexOf('.') + 3) + // Update the input to remove the extra numbers from UI input + setSlippageInput(v) + } - const parsed = Math.round(Number.parseFloat(v) * 100) + const parsed = Math.round(Number.parseFloat(v) * 100) - if ( - !Number.isInteger(parsed) || - parsed < (isEoaEthFlow ? minEthFlowSlippageBps : MIN_SLIPPAGE_BPS) || - parsed > MAX_SLIPPAGE_BPS - ) { - if (v !== '.') { - setSlippageError(SlippageError.InvalidInput) + if ( + !Number.isInteger(parsed) || + parsed < (isEoaEthFlow ? minEthFlowSlippageBps : MIN_SLIPPAGE_BPS) || + parsed > MAX_SLIPPAGE_BPS + ) { + if (v !== '.') { + setSlippageError(SlippageError.InvalidInput) + } } - } - slippageToleranceAnalytics('Custom', parsed) - setSwapSlippage(percentToBps(new Percent(parsed, 10_000))) - } - } + slippageToleranceAnalytics('Custom', parsed) + setSwapSlippage(percentToBps(new Percent(parsed, 10_000))) + } + }, + [placeholderSlippage, isEoaEthFlow, minEthFlowSlippage], + ) const tooLow = swapSlippage.lessThan(new Percent(isEoaEthFlow ? minEthFlowSlippageBps : LOW_SLIPPAGE_BPS, 10_000)) const tooHigh = swapSlippage.greaterThan( - new Percent(isEoaEthFlow ? HIGH_ETH_FLOW_SLIPPAGE_BPS : HIGH_SLIPPAGE_BPS, 10_000) + new Percent(isEoaEthFlow ? HIGH_ETH_FLOW_SLIPPAGE_BPS : smartSlippage || HIGH_SLIPPAGE_BPS, 10_000), ) - function parseCustomDeadline(value: string) { - // populate what the user typed and clear the error - setDeadlineInput(value) - setDeadlineError(false) - - if (value.length === 0) { - orderExpirationTimeAnalytics('Default', DEFAULT_DEADLINE_FROM_NOW) - setDeadline(DEFAULT_DEADLINE_FROM_NOW) - } else { - try { - const parsed: number = Math.floor(Number.parseFloat(value) * 60) - if ( - !Number.isInteger(parsed) || // Check deadline is a number - parsed < - (isEoaEthFlow - ? // 10 minute low threshold for eth flow - MINIMUM_ETH_FLOW_DEADLINE_SECONDS - : MINIMUM_ORDER_VALID_TO_TIME_SECONDS) || // Check deadline is not too small - parsed > MAX_DEADLINE_MINUTES * 60 // Check deadline is not too big - ) { + const parseCustomDeadline = useCallback( + (value: string) => { + // populate what the user typed and clear the error + setDeadlineInput(value) + setDeadlineError(false) + + if (value.length === 0) { + orderExpirationTimeAnalytics('Default', DEFAULT_DEADLINE_FROM_NOW) + setDeadline(DEFAULT_DEADLINE_FROM_NOW) + } else { + try { + const parsed: number = Math.floor(Number.parseFloat(value) * 60) + if ( + !Number.isInteger(parsed) || // Check deadline is a number + parsed < + (isEoaEthFlow + ? // 10 minute low threshold for eth flow + MINIMUM_ETH_FLOW_DEADLINE_SECONDS + : MINIMUM_ORDER_VALID_TO_TIME_SECONDS) || // Check deadline is not too small + parsed > MAX_DEADLINE_MINUTES * 60 // Check deadline is not too big + ) { + setDeadlineError(DeadlineError.InvalidInput) + } else { + orderExpirationTimeAnalytics('Custom', parsed) + setDeadline(parsed) + } + } catch (error: any) { + console.error(error) setDeadlineError(DeadlineError.InvalidInput) - } else { - orderExpirationTimeAnalytics('Custom', parsed) - setDeadline(parsed) } - } catch (error: any) { - console.error(error) - setDeadlineError(DeadlineError.InvalidInput) } - } - } + }, + [isEoaEthFlow], + ) const showCustomDeadlineRow = Boolean(chainId) @@ -299,14 +306,14 @@ export function TransactionSettings() { - MEV protected slippage + MEV-protected slippage Your transaction will revert if the price changes unfavorably by more than this percentage. isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol, getWrappedToken(nativeCurrency).symbol]) - : getNonNativeSlippageTooltip() + : getNonNativeSlippageTooltip(true) } /> @@ -366,8 +373,8 @@ export function TransactionSettings() { - Based on recent volatility observed for this token pair, it's recommended to leave the default - to account for price changes. + CoW Swap has dynamically selected this slippage amount to account for current gas prices and + volatility. Changes may result in slower execution. } /> diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index c1105bc66c..07a343a2fd 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -1,4 +1,4 @@ -import { INPUT_OUTPUT_EXPLANATION, MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' +import { MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' import { CenteredDots, HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' @@ -49,23 +49,29 @@ export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (st matching, even in volatile market conditions.

- Orders on CoW Swap are always protected from MEV, so your slippage tolerance cannot be exploited. + {symbols?.[0] || 'Native currency'} orders can, in rare cases, be frontrun due to their on-chain component. For more + robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. ) -export const getNonNativeSlippageTooltip = () => ( +export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( - Your slippage is MEV protected: all orders are submitted with tight spread (0.1%) on-chain. -
-
- The slippage set enables a resubmission of your order in case of unfavourable price movements. -
-
- {INPUT_OUTPUT_EXPLANATION} + CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the + best price.{' '} + {isSettingsModal ? ( + <> + To override this, enter your desired slippage amount. +
+
+ Either way, your slippage is protected from MEV! + + ) : ( + "Trades are protected from MEV, so your slippage can't be exploited!" + )}
) const SUGGESTED_SLIPPAGE_TOOLTIP = - 'Based on recent volatility for the selected token pair, this is the suggested slippage for ensuring quick execution of your order.' + 'This is the recommended slippage tolerance based on current gas prices & volatility. A lower amount may result in slower execution.' export interface RowSlippageContentProps { chainId: SupportedChainId @@ -121,7 +127,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) { ) : ( <> - (Suggested: {smartSlippage}) + (Recommended: {smartSlippage}) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts index 5c8b79700e..73e6173e77 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -1,12 +1,11 @@ import { useSetAtom } from 'jotai' -import { useEffect, useMemo } from 'react' +import { useEffect } from 'react' import { useTradeConfirmState } from 'modules/trade' import { useSmartSlippageFromBff } from './useSmartSlippageFromBff' import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMultiplier' -import { useDerivedSwapInfo, useHighFeeWarning } from '../../hooks/useSwapState' import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' const MAX_BPS = 500 // 5% @@ -16,8 +15,6 @@ export function SmartSlippageUpdater() { const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) const bffSlippageBps = useSmartSlippageFromBff() - // TODO: remove v1 - const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() const feeMultiplierSlippageBps = useSmartSlippageFromFeeMultiplier() const { isOpen: isTradeReviewOpen } = useTradeConfirmState() @@ -38,49 +35,5 @@ export function SmartSlippageUpdater() { setSmartSwapSlippage(Math.max(MIN_BPS, Math.min(slippage, MAX_BPS))) }, [bffSlippageBps, setSmartSwapSlippage, feeMultiplierSlippageBps, isTradeReviewOpen]) - // TODO: remove before merging - useEffect(() => { - console.log(`SmartSlippageUpdater`, { - granularSlippage: tradeSizeSlippageBpsV1, - fiftyPercentFeeSlippage: feeMultiplierSlippageBps, - bffSlippageBps, - }) - }, [tradeSizeSlippageBpsV1, feeMultiplierSlippageBps]) - return null } - -// TODO: remove -/** - * Calculates smart slippage in bps, based on trade size in relation to fee - */ -function useSmartSlippageFromFeePercentage(): number | undefined { - const { trade } = useDerivedSwapInfo() || {} - const { feePercentage } = useHighFeeWarning(trade) - - const percentage = feePercentage && +feePercentage.toFixed(3) - - return useMemo(() => { - if (percentage === undefined) { - // Unset, return undefined - return - } - if (percentage < 1) { - // bigger volume compared to the fee, trust on smart slippage from BFF - return - } else if (percentage < 5) { - // Between 1 and 5, 2% - return 200 - } else if (percentage < 10) { - // Between 5 and 10, 5% - return 500 - } else if (percentage < 20) { - // Between 10 and 20, 10% - return 1000 - } - // TODO: more granularity? - - // > 20%, cap it at 20% slippage - return 2000 - }, [percentage]) -} diff --git a/libs/common-const/src/misc.ts b/libs/common-const/src/misc.ts index dcc1b586df..3fd037eb9f 100644 --- a/libs/common-const/src/misc.ts +++ b/libs/common-const/src/misc.ts @@ -1,29 +1,15 @@ -import { Percent, Fraction } from '@uniswap/sdk-core' +import { Fraction, Percent } from '@uniswap/sdk-core' import JSBI from 'jsbi' export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -export const NetworkContextName = 'NETWORK' - -export const IS_IN_IFRAME = typeof window !== 'undefined' && window.parent !== window - // 30 minutes, denominated in seconds export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30 export const L2_DEADLINE_FROM_NOW = 60 * 5 -// transaction popup dismisal amounts -export const DEFAULT_TXN_DISMISS_MS = 25000 -export const L2_TXN_DISMISS_MS = 5000 - -// used for rewards deadlines -export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7) - -export const BIG_INT_ZERO = JSBI.BigInt(0) - // one basis JSBI.BigInt const BPS_BASE = JSBI.BigInt(10000) -export const ONE_BPS = new Percent(JSBI.BigInt(1), BPS_BASE) // used for warning states export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BPS_BASE) // 1% @@ -34,12 +20,6 @@ export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.Bi // for non expert mode disable swaps above this export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BPS_BASE) // 15% -export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), BPS_BASE) - -export const ZERO_PERCENT = new Percent('0') -export const TWO_PERCENT = new Percent(JSBI.BigInt(200), BPS_BASE) export const ONE_HUNDRED_PERCENT = new Percent('1') -export const IS_SIDE_BANNER_VISIBLE_KEY = 'IS_SIDEBAR_BANNER_VISIBLE' - export const ONE_FRACTION = new Fraction(1, 1) From c3326404eea1da64657d99e9190989c5ad73ed57 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 14 Oct 2024 22:03:04 +0500 Subject: [PATCH 025/116] chore: up widget lib deps (#4987) --- libs/widget-lib/package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/widget-lib/package.json b/libs/widget-lib/package.json index 90d297d6f6..b2dcdaa138 100644 --- a/libs/widget-lib/package.json +++ b/libs/widget-lib/package.json @@ -21,6 +21,7 @@ "widget-lib" ], "dependencies": { - "@cowprotocol/events": "^1.4.1-RC.0" + "@cowprotocol/events": "^1.5.0", + "@cowprotocol/iframe-transport": "^1.1.0" } -} \ No newline at end of file +} From 7a515bb27b411e926fd3eb9d5cf4731c802a2fa4 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 14 Oct 2024 22:10:46 +0500 Subject: [PATCH 026/116] chore: up widget react deps (#4988) --- libs/widget-react/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/widget-react/package.json b/libs/widget-react/package.json index 8045b3c400..ede317c850 100644 --- a/libs/widget-react/package.json +++ b/libs/widget-react/package.json @@ -22,6 +22,6 @@ "react" ], "dependencies": { - "@cowprotocol/widget-lib": "^0.14.1-RC.0" + "@cowprotocol/widget-lib": "^0.15.0" } -} \ No newline at end of file +} From d6c25b84310bd60a8373240c0528a23795f9d358 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 14 Oct 2024 22:19:07 +0500 Subject: [PATCH 027/116] chore: up iframe-transport version --- libs/iframe-transport/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/iframe-transport/package.json b/libs/iframe-transport/package.json index 7e102849ef..6649036570 100644 --- a/libs/iframe-transport/package.json +++ b/libs/iframe-transport/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/iframe-transport", - "version": "1.0.0", + "version": "1.1.0", "type": "commonjs", "description": "CoW Swap iframe-transport", "main": "index.js", From 395f48f57d93de67305791fdb9a668bdd693074e Mon Sep 17 00:00:00 2001 From: Jean Neiverth <79885562+JeanNeiverth@users.noreply.github.com> Date: Wed, 16 Oct 2024 04:03:32 -0300 Subject: [PATCH 028/116] feat(hooks-store): add claim vesting iframe hook (#4924) * feat: add claim vesting IFrame hook * chore: update claim vesting hook name to claim llamapay vesting hook * fix: name of create llamapay hook in hookRegistry --- .../containers/IframeDappContainer/index.tsx | 2 +- .../src/modules/hooksStore/hookRegistry.tsx | 1 + libs/hook-dapp-lib/src/hookDappsRegistry.json | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx index 0a25b9ffb5..c463218c7b 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx @@ -10,7 +10,7 @@ import { HookDappContext as HookDappContextType, HookDappIframe } from '../../ty const Iframe = styled.iframe` border: 0; - min-height: 350px; + min-height: 300px; ` interface IframeDappContainerProps { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index 0fcf02b699..73e31b0da9 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -23,4 +23,5 @@ export const ALL_HOOK_DAPPS = [ ...hookDappsRegistry.CLAIM_COW_AIRDROP, component: (props) => , }, + hookDappsRegistry.CLAIM_LLAMAPAY_VESTING, ] as HookDapp[] diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json index ce98edb976..976c65095f 100644 --- a/libs/hook-dapp-lib/src/hookDappsRegistry.json +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -49,5 +49,19 @@ "conditions": { "supportedNetworks": [11155111] } + }, + "CLAIM_LLAMAPAY_VESTING": { + "id": "5d2c081d11a01ca0b76e2fafbc0d3c62a4c9945ce404706fb1e49e826c0f99eb", + "type": "IFRAME", + "name": "Claim LlamaPay Vesting Hook", + "description": "The Claim LlamaPay Vesting Hook is a powerful and user-friendly feature designed to streamline the process of claiming funds from LlamaPay vesting contracts. This tool empowers users to easily access and manage their vested tokens, ensuring a smooth and efficient experience in handling time-locked assets.", + "descriptionShort": "Claim your LlamaPay vesting contract funds", + "image": "https://cow-hooks-dapps-claim-vesting.vercel.app/llama-pay-icon.png", + "version": "0.1.0", + "website": "https://github.com/bleu/cow-hooks-dapps", + "url": "https://cow-hooks-dapps-claim-vesting.vercel.app", + "conditions": { + "supportedNetworks": [1, 100, 42161] + } } } From 26cbffbbfe8edbc0a4a9ba31fe9c0d42852118d9 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 16 Oct 2024 16:03:27 +0500 Subject: [PATCH 029/116] feat(hooks-store): add sell/buy amounts to hook-dapp context (#4990) --- .../modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts | 2 ++ libs/hook-dapp-lib/src/types.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts index 7bc2cb2dce..7f531ad420 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts @@ -16,6 +16,8 @@ export function useSetupHooksStoreOrderParams() { setOrderParams({ validTo: orderParams.validTo, + sellAmount: orderParams.inputAmount.quotient.toString(), + buyAmount: orderParams.outputAmount.quotient.toString(), sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency), buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency), }) diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index f44aa21a06..9adea48b49 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -44,6 +44,8 @@ export interface HookDappOrderParams { validTo: number sellTokenAddress: string buyTokenAddress: string + sellAmount: string + buyAmount: string } export interface HookDappContext { From eaa29f3ed421d92214b857bf1c57d75b0317cbba Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 16 Oct 2024 17:02:47 +0500 Subject: [PATCH 030/116] fix(explorer): display hook details of unknown hook-dapp (#4995) --- .../OrderHooksDetails/HookItem/index.tsx | 77 ++++++++++--------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx index b80bb6d9b6..c21e92859b 100644 --- a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx +++ b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx @@ -11,42 +11,49 @@ export function HookItem({ item, number }: { item: HookToDappMatch; number: numb return ( - {item.dapp ? ( - -

- #{number} - {item.dapp.name} {item.dapp.name}{' '} - {showDetails ? '[-] Show less' : '[+] Show more'} -

- - {showDetails && ( -
-

- Version: {item.dapp.version} -

-

- Description: {item.dapp.descriptionShort} -

-

- Website: - - {item.dapp.website} - -

-

- Call Data: {item.hook.callData} -

-

- Gas Limit: {item.hook.gasLimit} -

-

- Target: {item.hook.target} -

-
+ +

+ #{number} -{' '} + {item.dapp ? ( + <> + {item.dapp.name} {item.dapp.name}{' '} + + ) : ( + 'Unknown hook dapp' )} - - ) : ( -

Unknown hook dapp
- )} + {showDetails ? '[-] Show less' : '[+] Show more'} +

+ + {showDetails && ( +
+ {item.dapp && ( + <> +

+ Version: {item.dapp.version} +

+

+ Description: {item.dapp.descriptionShort} +

+

+ Website: + + {item.dapp.website} + +

+ + )} +

+ Call Data: {item.hook.callData} +

+

+ Gas Limit: {item.hook.gasLimit} +

+

+ Target: {item.hook.target} +

+
+ )} +
) } From cf004bdd06f63404b950b11817d768fccefb767d Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 16 Oct 2024 17:03:00 +0500 Subject: [PATCH 031/116] chore: update hook docs link (#4996) --- .../hooksStore/containers/HooksStoreWidget/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index 9cf80a3a42..18999dd0ba 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -81,8 +81,12 @@ export function HooksStoreWidget() { bannerId="hooks-store-banner-tradeContainer" >

- With hooks you can add specific actions before and after your swap. {/*TODO: update the link*/} - + With hooks you can add specific actions before and after your swap.{' '} + Learn more.

From 531e63f666ffcafdaf8e2b1c2850991facbe5cf1 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 16 Oct 2024 17:10:30 +0500 Subject: [PATCH 032/116] feat: display new label for cow amm (#4994) * feat: display new label for cow amm * fix: add new label to Hooks tab * chore: add new to hook menu * chore: remove CoWAMM banner --- .../src/common/constants/routes.ts | 9 +++- .../application/containers/App/index.tsx | 8 +--- .../application/containers/App/menuConsts.tsx | 2 + .../TradeWidgetLinks/index.cosmos.tsx | 35 -------------- .../containers/TradeWidgetLinks/index.tsx | 36 ++++----------- .../containers/TradeWidgetLinks/styled.ts | 46 ++----------------- libs/ui/src/index.ts | 1 + libs/ui/src/pure/Badge/index.tsx | 44 ++++++++++++++++++ libs/ui/src/pure/MenuBar/index.tsx | 25 ++++++---- libs/ui/src/pure/MenuBar/styled.ts | 3 ++ 10 files changed, 88 insertions(+), 121 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.cosmos.tsx create mode 100644 libs/ui/src/pure/Badge/index.tsx diff --git a/apps/cowswap-frontend/src/common/constants/routes.ts b/apps/cowswap-frontend/src/common/constants/routes.ts index 1457da5df3..bdc140310a 100644 --- a/apps/cowswap-frontend/src/common/constants/routes.ts +++ b/apps/cowswap-frontend/src/common/constants/routes.ts @@ -37,7 +37,13 @@ export const Routes = { export type RoutesKeys = keyof typeof Routes export type RoutesValues = (typeof Routes)[RoutesKeys] -export const MENU_ITEMS: { route: RoutesValues; label: string; fullLabel?: string; description: string }[] = [ +export const MENU_ITEMS: { + route: RoutesValues + label: string + fullLabel?: string + description: string + badge?: string +}[] = [ { route: Routes.SWAP, label: 'Swap', description: 'Trade tokens' }, { route: Routes.LIMIT_ORDER, label: 'Limit', fullLabel: 'Limit order', description: 'Set your own price' }, { route: Routes.ADVANCED_ORDERS, label: 'TWAP', description: 'Place orders with a time-weighted average price' }, @@ -47,4 +53,5 @@ export const HOOKS_STORE_MENU_ITEM = { route: Routes.HOOKS, label: 'Hooks', description: 'Powerful tool to generate pre/post interaction for CoW Protocol', + badge: 'New', } 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 c08a02680e..e2db6d73bf 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -24,7 +24,6 @@ import { useInitializeUtm } from 'modules/utm' import { InvalidLocalTimeWarning } from 'common/containers/InvalidLocalTimeWarning' import { useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' import { useMenuItems } from 'common/hooks/useMenuItems' -import { CoWAmmBanner } from 'common/pure/CoWAMMBanner' import { LoadingApp } from 'common/pure/LoadingApp' import { CoWDAOFonts } from 'common/styles/CoWDAOFonts' import RedirectAnySwapAffectedUsers from 'pages/error/AnySwapAffectedUsers/RedirectAnySwapAffectedUsers' @@ -62,7 +61,7 @@ export function App() { onClick: toggleDarkMode, }, ], - [darkMode, toggleDarkMode] + [darkMode, toggleDarkMode], ) const tradeContext = useTradeRouteContext() @@ -74,7 +73,7 @@ export function App() { children: menuItems.map((item) => { const href = parameterizeTradeRoute(tradeContext, item.route, true) - return { href, label: item.label, description: item.description } + return { href, label: item.label, description: item.description, badge: item.badge } }), }, ...NAV_ITEMS, @@ -128,9 +127,6 @@ export function App() { /> )} - {/* CoW AMM banner */} - {!isInjectedWidgetMode && } - diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx index 5faf9ff31c..f8bf52d06a 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx @@ -41,6 +41,7 @@ export const NAV_ITEMS: MenuItem[] = [ }, { label: 'More', + badge: 'New', children: [ { href: 'https://cow.fi/cow-protocol', @@ -50,6 +51,7 @@ export const NAV_ITEMS: MenuItem[] = [ { href: 'https://cow.fi/cow-amm', label: 'CoW AMM', + badge: 'New', external: true, }, { diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.cosmos.tsx deleted file mode 100644 index 77fed8e4d1..0000000000 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.cosmos.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { BadgeType } from '@cowprotocol/ui' - -import { Widget } from 'modules/application/pure/Widget' - -import { TradeWidgetLinks } from './index' - -type BadgeInfo = { - text: string - type: BadgeType -} - -const BADGES: BadgeInfo[] = [ - { text: 'BETA', type: 'default' }, - { text: 'NEW!', type: 'success' }, - { text: 'ALPHA', type: 'alert' }, - { text: 'NEW!', type: 'alert2' }, - { text: 'RELEASE', type: 'information' }, -] - -type Fixtures = { - [key: string]: React.FunctionComponent -} - -const BadgeFixtures = BADGES.reduce((fixtures, badge) => { - const Fixture = () => ( - - - - ) - - fixtures[`Badge - ${badge.text} (${badge.type})`] = Fixture - return fixtures -}, {}) - -export default BadgeFixtures diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx index cb7c298f86..08ac319e6d 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react' import { Command } from '@cowprotocol/types' -import { BadgeType } from '@cowprotocol/ui' +import { Badge } from '@cowprotocol/ui' import type { TradeType } from '@cowprotocol/widget-lib' import { Trans } from '@lingui/macro' @@ -23,6 +23,7 @@ import { parameterizeTradeRoute } from '../../utils/parameterizeTradeRoute' interface MenuItemConfig { route: RoutesValues label: string + badge?: string } const TRADE_TYPE_TO_ROUTE: Record = { @@ -32,16 +33,10 @@ const TRADE_TYPE_TO_ROUTE: Record = { } interface TradeWidgetLinksProps { - highlightedBadgeText?: string - highlightedBadgeType?: BadgeType isDropdown?: boolean } -export function TradeWidgetLinks({ - highlightedBadgeText, - highlightedBadgeType, - isDropdown = false, -}: TradeWidgetLinksProps) { +export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) { const tradeContext = useTradeRouteContext() const location = useLocation() const [isDropdownVisible, setDropdownVisible] = useState(false) @@ -72,23 +67,12 @@ export function TradeWidgetLinks({ routePath={routePath} item={item} isActive={isActive} - badgeText={highlightedBadgeText} - badgeType={highlightedBadgeType} onClick={() => handleMenuItemClick(item)} isDropdownVisible={isDropdown && isDropdownVisible} /> ) }) - }, [ - isDropdown, - isDropdownVisible, - enabledItems, - tradeContext, - location.pathname, - highlightedBadgeText, - highlightedBadgeType, - handleMenuItemClick, - ]) + }, [isDropdown, isDropdownVisible, enabledItems, tradeContext, location.pathname, handleMenuItemClick]) const singleMenuItem = menuItemsElements.length === 1 @@ -122,26 +106,22 @@ const MenuItem = ({ routePath, item, isActive, - badgeText, - badgeType, onClick, isDropdownVisible, }: { routePath: string item: MenuItemConfig isActive: boolean - badgeText?: string - badgeType?: BadgeType onClick: Command isDropdownVisible: boolean }) => ( {item.label} - {!isActive && badgeText && ( - - {badgeText} - + {!isActive && item.badge && ( + + {item.badge} + )} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts index 3ed5310d80..770b0e0db9 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts @@ -1,48 +1,8 @@ -import { UI, BadgeType } from '@cowprotocol/ui' +import { Badge, UI } from '@cowprotocol/ui' import { NavLink } from 'react-router-dom' import styled, { css } from 'styled-components/macro' -const badgeBackgrounds: Record = { - information: `var(${UI.COLOR_INFO_BG})`, - alert: `var(${UI.COLOR_BADGE_YELLOW_BG})`, - alert2: `var(${UI.COLOR_BADGE_YELLOW_BG})`, - success: `var(${UI.COLOR_SUCCESS_BG})`, - default: 'transparent', // text only -} - -const badgeColors: Record = { - information: `var(${UI.COLOR_INFO_TEXT})`, - alert: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, - alert2: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, - success: `var(${UI.COLOR_SUCCESS_TEXT})`, - default: `var(${UI.COLOR_DISABLED_TEXT})`, // text only -} - -export const Badge = styled.div<{ type?: BadgeType }>` - background: ${({ type }) => badgeBackgrounds[type || 'default']}; - color: ${({ type }) => badgeColors[type || 'default']}; - border: 0; - cursor: pointer; - border-radius: 16px; - font-size: 9px; - font-weight: inherit; - text-transform: uppercase; - padding: ${({ type }) => (!type || type === 'default' ? '0' : '4px 6px')}; - letter-spacing: 0.2px; - font-weight: 600; - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out; - margin: 0; - - a & { - color: ${({ type }) => badgeColors[type || 'default']}; - } -` - -Badge.defaultProps = { - type: 'default', -} - export const Link = styled(NavLink)` display: flex; align-items: center; @@ -52,7 +12,9 @@ export const Link = styled(NavLink)` gap: 4px; font-weight: inherit; line-height: 1; - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out, fill var(${UI.ANIMATION_DURATION}) ease-in-out; + transition: + color var(${UI.ANIMATION_DURATION}) ease-in-out, + fill var(${UI.ANIMATION_DURATION}) ease-in-out; &:hover { color: inherit; diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index 403e897f4f..3a41db39e3 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -33,6 +33,7 @@ export * from './pure/PercentDisplay' export * from './pure/CmsImage' export * from './pure/DismissableInlineBanner' export * from './pure/Input' +export * from './pure/Badge' export * from './containers/CowSwapSafeAppLink' export * from './containers/InlineBanner' diff --git a/libs/ui/src/pure/Badge/index.tsx b/libs/ui/src/pure/Badge/index.tsx new file mode 100644 index 0000000000..3ce2c3a7cf --- /dev/null +++ b/libs/ui/src/pure/Badge/index.tsx @@ -0,0 +1,44 @@ +import styled from 'styled-components/macro' + +import { UI } from '../../enum' +import { BadgeType } from '../../types' + +const badgeBackgrounds: Record = { + information: `var(${UI.COLOR_INFO_BG})`, + alert: `var(${UI.COLOR_BADGE_YELLOW_BG})`, + alert2: `var(${UI.COLOR_BADGE_YELLOW_BG})`, + success: `var(${UI.COLOR_SUCCESS_BG})`, + default: 'transparent', // text only +} + +const badgeColors: Record = { + information: `var(${UI.COLOR_INFO_TEXT})`, + alert: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, + alert2: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, + success: `var(${UI.COLOR_SUCCESS_TEXT})`, + default: `var(${UI.COLOR_DISABLED_TEXT})`, // text only +} + +export const Badge = styled.div<{ type?: BadgeType }>` + background: ${({ type }) => badgeBackgrounds[type || 'default']}; + color: ${({ type }) => badgeColors[type || 'default']}; + border: 0; + cursor: pointer; + border-radius: 16px; + font-size: 9px; + font-weight: inherit; + text-transform: uppercase; + padding: ${({ type }) => (!type || type === 'default' ? '0' : '4px 6px')}; + letter-spacing: 0.2px; + font-weight: 600; + transition: color var(${UI.ANIMATION_DURATION}) ease-in-out; + margin: 0; + + a & { + color: ${({ type }) => badgeColors[type || 'default']}; + } +` + +Badge.defaultProps = { + type: 'default', +} diff --git a/libs/ui/src/pure/MenuBar/index.tsx b/libs/ui/src/pure/MenuBar/index.tsx index a93ee332f1..346d412053 100644 --- a/libs/ui/src/pure/MenuBar/index.tsx +++ b/libs/ui/src/pure/MenuBar/index.tsx @@ -34,6 +34,7 @@ import { import { Color } from '../../consts' import { Media } from '../../consts' +import { Badge } from '../Badge' import { ProductLogo, ProductVariant } from '../ProductLogo' const DAO_NAV_ITEMS: MenuItem[] = [ @@ -84,6 +85,7 @@ type LinkComponentType = ComponentType> export interface MenuItem { href?: string label?: string + badge?: string children?: DropdownMenuItem[] productVariant?: ProductVariant icon?: string @@ -105,6 +107,7 @@ interface DropdownMenuItem { external?: boolean label?: string icon?: string + badge?: string description?: string isButton?: boolean children?: DropdownMenuItem[] @@ -127,7 +130,7 @@ interface DropdownMenuContent { interface DropdownProps { isOpen: boolean - content: DropdownMenuContent + item: MenuItem onTrigger: () => void closeDropdown: () => void interaction: 'hover' | 'click' @@ -169,7 +172,7 @@ const NavItem = ({ return item.children ? ( = ({ isOpen, - content, + item, onTrigger, interaction, mobileMode, @@ -390,8 +393,8 @@ const GenericDropdown: React.FC = ({ rootDomain, LinkComponent, }) => { - if (!content.title) { - throw new Error('Dropdown content must have a title') + if (!item.label) { + throw new Error('Dropdown content must have a title and children') } const interactionProps = useMemo(() => { @@ -408,12 +411,13 @@ const GenericDropdown: React.FC = ({ return ( - {content.title} - {content.items && } + {item.label} + {item.badge && {item.badge}} + {item.children && } {isOpen && ( = ({ <> {item.icon && } - {item.label} + + {item.label} + {item.badge && {item.badge}} + {item.description && {item.description}} {item.children && } diff --git a/libs/ui/src/pure/MenuBar/styled.ts b/libs/ui/src/pure/MenuBar/styled.ts index e7d1c1395e..ef4f904131 100644 --- a/libs/ui/src/pure/MenuBar/styled.ts +++ b/libs/ui/src/pure/MenuBar/styled.ts @@ -491,6 +491,9 @@ export const DropdownContentItemTitle = styled.span` font-weight: bold; font-size: 18px; line-height: 1.2; + display: flex; + align-items: center; + gap: 8px; ` export const DropdownContentItemDescription = styled.span` From 9842afdb887497d235a01538663488b0b8852bb5 Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 16 Oct 2024 14:34:48 +0100 Subject: [PATCH 033/116] feat(widget): hide bridge info (#4992) * feat: add option to hide brigde info to widget * feat: use hideBridgeInfo on Network alert --- .../components/NetworkAlert/NetworkAlert.tsx | 14 +++++++++++--- .../hooks/useWidgetParamsAndSettings.ts | 2 ++ .../src/app/configurator/index.tsx | 17 ++++++++++++++--- .../src/app/configurator/types.ts | 1 + libs/widget-lib/src/types.ts | 5 +++++ 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/components/NetworkAlert/NetworkAlert.tsx b/apps/cowswap-frontend/src/legacy/components/NetworkAlert/NetworkAlert.tsx index 7fb20f05ed..03bd936cb5 100644 --- a/apps/cowswap-frontend/src/legacy/components/NetworkAlert/NetworkAlert.tsx +++ b/apps/cowswap-frontend/src/legacy/components/NetworkAlert/NetworkAlert.tsx @@ -10,6 +10,8 @@ import styled from 'styled-components/macro' import { useDarkModeManager } from 'legacy/state/user/hooks' +import { useInjectedWidgetParams } from 'modules/injectedWidget' + const HideSmall = styled.span` ${Media.upToSmall()} { display: none; @@ -61,7 +63,9 @@ const StyledArrowUpRight = styled(ArrowUpRight)` const ContentWrapper = styled.div<{ chainId: NetworkAlertChains; darkMode: boolean; logoUrl: string }>` background: var(${UI.COLOR_PAPER_DARKER}); - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out, background var(${UI.ANIMATION_DURATION}) ease-in-out; // MOD + transition: + color var(${UI.ANIMATION_DURATION}) ease-in-out, + background var(${UI.ANIMATION_DURATION}) ease-in-out; // MOD border-radius: 20px; display: flex; flex-direction: row; @@ -88,7 +92,9 @@ const ContentWrapper = styled.div<{ chainId: NetworkAlertChains; darkMode: boole color: inherit; stroke: currentColor; text-decoration: none; - transition: transform var(${UI.ANIMATION_DURATION}) ease-in-out, stroke var(${UI.ANIMATION_DURATION}) ease-in-out, + transition: + transform var(${UI.ANIMATION_DURATION}) ease-in-out, + stroke var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out; } @@ -136,7 +142,9 @@ export function NetworkAlert() { const theme = useTheme() - if (!shouldShowAlert(chainId) || !isActive) { + const { hideBridgeInfo } = useInjectedWidgetParams() + + if (!shouldShowAlert(chainId) || !isActive || hideBridgeInfo) { return null } diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index 69143ec592..b00d73748b 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -39,6 +39,7 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi standaloneMode, disableToastMessages, disableProgressBar, + hideBridgeInfo, } = configuratorState const themeColors = { @@ -84,6 +85,7 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi recipient: partnerFeeRecipient, } : undefined, + hideBridgeInfo, } return params diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index 1a6b9cf699..2bd0b12d5f 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -128,9 +128,10 @@ export function Configurator({ title }: { title: string }) { const firstToast = toasts?.[0] const [disableProgressBar, setDisableProgressBar] = useState(false) - const toggleDisableProgressBar = useCallback(() => { - setDisableProgressBar((curr) => !curr) - }, []) + const toggleDisableProgressBar = useCallback(() => setDisableProgressBar((curr) => !curr), []) + + const [hideBridgeInfo, setHideBridgeInfo] = useState(false) + const toggleHideBridgeInfo = useCallback(() => setHideBridgeInfo((curr) => !curr), []) const LINKS = [ { icon: , label: 'View embed code', onClick: () => handleDialogOpen() }, @@ -161,6 +162,7 @@ export function Configurator({ title }: { title: string }) { standaloneMode, disableToastMessages, disableProgressBar, + hideBridgeInfo, } const computedParams = useWidgetParams(state) @@ -283,6 +285,7 @@ export function Configurator({ title }: { title: string }) { } label="Dapp mode" /> + Progress bar: @@ -291,6 +294,14 @@ export function Configurator({ title }: { title: string }) { + + Hide bridge info: + + } label="Show bridge info" /> + } label="Hide bridge info" /> + + + {isDrawerOpen && ( Date: Thu, 17 Oct 2024 07:03:46 +0100 Subject: [PATCH 034/116] chore: style enhancements for badge (#4999) --- .../trade/containers/TradeWidgetLinks/index.tsx | 11 +++++------ libs/ui/src/pure/Badge/index.tsx | 6 +++--- libs/ui/src/pure/MenuBar/index.tsx | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx index 08ac319e6d..65bf8dda31 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useMemo, useState, useCallback } from 'react' import { Command } from '@cowprotocol/types' import { Badge } from '@cowprotocol/ui' @@ -43,10 +43,9 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) const { enabledTradeTypes } = useInjectedWidgetParams() const menuItems = useMenuItems() - const handleMenuItemClick = (_item?: MenuItemConfig) => { - if (menuItemsElements.length === 1) return + const handleMenuItemClick = useCallback((_item?: MenuItemConfig): void => { setDropdownVisible(false) - } + }, []) const enabledItems = useMemo(() => { return menuItems.filter((item) => { @@ -56,7 +55,7 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) }) }, [menuItems, enabledTradeTypes]) - const menuItemsElements = useMemo(() => { + const menuItemsElements: JSX.Element[] = useMemo(() => { return enabledItems.map((item) => { const routePath = parameterizeTradeRoute(tradeContext, item.route, true) const isActive = !!matchPath(location.pathname, routePath.split('?')[0]) @@ -119,7 +118,7 @@ const MenuItem = ({ {item.label} {!isActive && item.badge && ( - + {item.badge} )} diff --git a/libs/ui/src/pure/Badge/index.tsx b/libs/ui/src/pure/Badge/index.tsx index 3ce2c3a7cf..5346854894 100644 --- a/libs/ui/src/pure/Badge/index.tsx +++ b/libs/ui/src/pure/Badge/index.tsx @@ -5,7 +5,7 @@ import { BadgeType } from '../../types' const badgeBackgrounds: Record = { information: `var(${UI.COLOR_INFO_BG})`, - alert: `var(${UI.COLOR_BADGE_YELLOW_BG})`, + alert: `var(${UI.COLOR_ALERT_BG})`, alert2: `var(${UI.COLOR_BADGE_YELLOW_BG})`, success: `var(${UI.COLOR_SUCCESS_BG})`, default: 'transparent', // text only @@ -13,7 +13,7 @@ const badgeBackgrounds: Record = { const badgeColors: Record = { information: `var(${UI.COLOR_INFO_TEXT})`, - alert: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, + alert: `var(${UI.COLOR_ALERT_TEXT})`, alert2: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, success: `var(${UI.COLOR_SUCCESS_TEXT})`, default: `var(${UI.COLOR_DISABLED_TEXT})`, // text only @@ -25,7 +25,7 @@ export const Badge = styled.div<{ type?: BadgeType }>` border: 0; cursor: pointer; border-radius: 16px; - font-size: 9px; + font-size: 10px; font-weight: inherit; text-transform: uppercase; padding: ${({ type }) => (!type || type === 'default' ? '0' : '4px 6px')}; diff --git a/libs/ui/src/pure/MenuBar/index.tsx b/libs/ui/src/pure/MenuBar/index.tsx index 346d412053..852b07499f 100644 --- a/libs/ui/src/pure/MenuBar/index.tsx +++ b/libs/ui/src/pure/MenuBar/index.tsx @@ -412,7 +412,7 @@ const GenericDropdown: React.FC = ({ {item.label} - {item.badge && {item.badge}} + {item.badge && {item.badge}} {item.children && } {isOpen && ( @@ -483,7 +483,7 @@ const DropdownContentWrapper: React.FC = ({ {item.label} - {item.badge && {item.badge}} + {item.badge && {item.badge}} {item.description && {item.description}} From ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 17 Oct 2024 11:19:05 +0100 Subject: [PATCH 035/116] feat(widget): deadline widget param (#4991) * feat: add deadlines widget parameter * refactor: remove dead code * feat: rename OrderDeadlines to ForcedOrderDeadline and use FlexibleConfig * feat: add useInjectedWidgetDeadline * fix: add spaces to tooltip * feat: use widget deadline on swap form * chore: remove duplicated css property * feat: use widget deadline on limit form * feat: use widget deadline on twap form * chore: remove debug logs * chore: fix build * fix: round timestamp * refactor: rename limitOrdersDeadlines to LIMIT_ORDERS_DEADLINES * fix: use deadlineMilliseconds instead of customDeadline for forcedOrderDeadline * fix: allow deadline input to be cleared * fix: add chainId to hook deps --- .../components/TransactionSettings/index.tsx | 39 ++++++++++--- .../src/legacy/state/user/hooks.tsx | 17 +++--- .../hooks/useInjectedWidgetDeadline.ts | 33 +++++++++++ .../src/modules/injectedWidget/index.ts | 1 + .../containers/DeadlineInput/index.tsx | 47 ++++++++++++--- .../pure/DeadlineSelector/deadlines.ts | 27 ++++++++- .../pure/DeadlineSelector/index.cosmos.tsx | 1 + .../pure/DeadlineSelector/index.tsx | 58 +++++++++++-------- .../updaters/AlternativeLimitOrderUpdater.ts | 6 +- .../swap/pure/Row/RowDeadline/index.tsx | 3 + .../modules/trade/pure/TradeSelect/index.tsx | 3 +- .../twap/containers/TwapFormWidget/index.tsx | 41 ++++++++++++- .../twap/pure/DeadlineSelector/index.tsx | 40 +++++++++---- .../configurator/controls/DeadlineControl.tsx | 23 ++++++++ .../hooks/useWidgetParamsAndSettings.ts | 14 ++++- .../src/app/configurator/index.tsx | 24 ++++++++ .../src/app/configurator/types.ts | 4 ++ libs/common-const/src/misc.ts | 1 - libs/widget-lib/src/types.ts | 12 ++++ 19 files changed, 324 insertions(+), 70 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts create mode 100644 apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx index 388726d36c..7ad9ea8175 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useRef, useState } from 'react' +import { useCallback, useContext, useEffect, useRef, useState } from 'react' import { DEFAULT_DEADLINE_FROM_NOW, @@ -16,6 +16,7 @@ import { useOnClickOutside } from '@cowprotocol/common-hooks' import { getWrappedToken, percentToBps } from '@cowprotocol/common-utils' import { FancyButton, HelpTooltip, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' +import { TradeType } from '@cowprotocol/widget-lib' import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' @@ -27,6 +28,7 @@ import { AutoColumn } from 'legacy/components/Column' import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { orderExpirationTimeAnalytics, slippageToleranceAnalytics } from 'modules/analytics' +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useIsSlippageModified } from 'modules/swap/hooks/useIsSlippageModified' import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' @@ -191,6 +193,7 @@ export function TransactionSettings() { const chosenSlippageMatchesSmartSlippage = smartSlippage && new Percent(smartSlippage, 10_000).equalTo(swapSlippage) const [deadline, setDeadline] = useUserTransactionTTL() + const widgetDeadline = useInjectedWidgetDeadline(TradeType.SWAP) const [slippageInput, setSlippageInput] = useState('') const [slippageError, setSlippageError] = useState(false) @@ -249,6 +252,12 @@ export function TransactionSettings() { new Percent(isEoaEthFlow ? HIGH_ETH_FLOW_SLIPPAGE_BPS : smartSlippage || HIGH_SLIPPAGE_BPS, 10_000), ) + const minDeadline = isEoaEthFlow + ? // 10 minute low threshold for eth flow + MINIMUM_ETH_FLOW_DEADLINE_SECONDS + : MINIMUM_ORDER_VALID_TO_TIME_SECONDS + const maxDeadline = MAX_DEADLINE_MINUTES * 60 + const parseCustomDeadline = useCallback( (value: string) => { // populate what the user typed and clear the error @@ -263,12 +272,8 @@ export function TransactionSettings() { const parsed: number = Math.floor(Number.parseFloat(value) * 60) if ( !Number.isInteger(parsed) || // Check deadline is a number - parsed < - (isEoaEthFlow - ? // 10 minute low threshold for eth flow - MINIMUM_ETH_FLOW_DEADLINE_SECONDS - : MINIMUM_ORDER_VALID_TO_TIME_SECONDS) || // Check deadline is not too small - parsed > MAX_DEADLINE_MINUTES * 60 // Check deadline is not too big + parsed < minDeadline || // Check deadline is not too small + parsed > maxDeadline // Check deadline is not too big ) { setDeadlineError(DeadlineError.InvalidInput) } else { @@ -281,9 +286,26 @@ export function TransactionSettings() { } } }, - [isEoaEthFlow], + [minDeadline, maxDeadline], ) + useEffect(() => { + if (widgetDeadline) { + // Deadline is stored in seconds + const value = Math.floor(widgetDeadline) * 60 + + if (value < minDeadline) { + setDeadline(minDeadline) + } else if (value > maxDeadline) { + setDeadline(maxDeadline) + } else { + setDeadline(value) + } + } + }, [widgetDeadline, minDeadline, maxDeadline]) + + const isDeadlineDisabled = !!widgetDeadline + const showCustomDeadlineRow = Boolean(chainId) const onSlippageInputBlur = useCallback(() => { @@ -417,6 +439,7 @@ export function TransactionSettings() { setDeadlineError(false) }} color={deadlineError ? 'red' : ''} + disabled={isDeadlineDisabled} /> diff --git a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx index 01fa1094ad..f347ddb113 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx +++ b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { L2_DEADLINE_FROM_NOW, NATIVE_CURRENCIES, SupportedLocale, TokenWithLogo } from '@cowprotocol/common-const' +import { NATIVE_CURRENCIES, SupportedLocale, TokenWithLogo } from '@cowprotocol/common-const' import { getIsNativeToken } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' @@ -20,11 +20,12 @@ export function useIsDarkMode(): boolean { userDarkMode, matchesDarkMode, }), - shallowEqual + shallowEqual, ) return userDarkMode === null ? matchesDarkMode : userDarkMode } + export function useDarkModeManager(): [boolean, Command] { const dispatch = useAppDispatch() const darkMode = useIsDarkMode() @@ -48,7 +49,7 @@ export function useUserLocaleManager(): [SupportedLocale | null, (newLocale: Sup (newLocale: SupportedLocale) => { dispatch(updateUserLocale({ userLocale: newLocale })) }, - [dispatch] + [dispatch], ) return [locale, setLocale] @@ -67,7 +68,7 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void (recipient: string | null) => { dispatch(setRecipient({ recipient })) }, - [dispatch] + [dispatch], ) const toggleVisibility = useCallback( @@ -78,7 +79,7 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void onChangeRecipient(null) } }, - [isVisible, dispatch, onChangeRecipient] + [isVisible, dispatch, onChangeRecipient], ) return [isVisible, toggleVisibility] @@ -86,15 +87,13 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void export function useUserTransactionTTL(): [number, (slippage: number) => void] { const dispatch = useAppDispatch() - const userDeadline = useAppSelector((state) => state.user.userDeadline) - const onL2 = false - const deadline = onL2 ? L2_DEADLINE_FROM_NOW : userDeadline + const deadline = useAppSelector((state) => state.user.userDeadline) const setUserDeadline = useCallback( (userDeadline: number) => { dispatch(updateUserDeadline({ userDeadline })) }, - [dispatch] + [dispatch], ) return [deadline, setUserDeadline] diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts b/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts new file mode 100644 index 0000000000..c1ac9c2db9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts @@ -0,0 +1,33 @@ +import { useMemo } from 'react' + +import { isInjectedWidget } from '@cowprotocol/common-utils' +import { useWalletInfo } from '@cowprotocol/wallet' +import { ForcedOrderDeadline, resolveFlexibleConfig, SupportedChainId, TradeType } from '@cowprotocol/widget-lib' + +import { useInjectedWidgetParams } from './useInjectedWidgetParams' + +/** + * Returns the deadline set in the widget for the specific order type in minutes, if any + * + * Additional validation is needed + */ +export function useInjectedWidgetDeadline(tradeType: TradeType): number | undefined { + const { forcedOrderDeadline } = useInjectedWidgetParams() + const { chainId } = useWalletInfo() + + return useMemo(() => { + if (!isInjectedWidget()) { + return + } + + return getDeadline(forcedOrderDeadline, chainId, tradeType) + }, [tradeType, forcedOrderDeadline, chainId]) +} + +function getDeadline(deadline: ForcedOrderDeadline | undefined, chainId: SupportedChainId, tradeType: TradeType) { + if (!deadline) { + return + } + + return resolveFlexibleConfig(deadline, chainId, tradeType) +} diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/index.ts b/apps/cowswap-frontend/src/modules/injectedWidget/index.ts index 7190aa48c3..0b412e48b7 100644 --- a/apps/cowswap-frontend/src/modules/injectedWidget/index.ts +++ b/apps/cowswap-frontend/src/modules/injectedWidget/index.ts @@ -1,6 +1,7 @@ export { InjectedWidgetUpdater } from './updaters/InjectedWidgetUpdater' export { CowEventsUpdater } from './updaters/CowEventsUpdater' export { useInjectedWidgetParams, useWidgetPartnerFee } from './hooks/useInjectedWidgetParams' +export { useInjectedWidgetDeadline } from './hooks/useInjectedWidgetDeadline' export { useInjectedWidgetMetaData } from './hooks/useInjectedWidgetMetaData' export { useInjectedWidgetPalette } from './hooks/useInjectedWidgetPalette' export { injectedWidgetPartnerFeeAtom } from './state/injectedWidgetParamsAtom' diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx index a44991e3ee..5ed0816e20 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx @@ -1,9 +1,15 @@ -import { useSetAtom } from 'jotai' -import { useAtomValue } from 'jotai' -import { useCallback, useMemo, useRef } from 'react' +import { useAtomValue, useSetAtom } from 'jotai' +import { useCallback, useEffect, useMemo, useRef } from 'react' +import { TradeType } from '@cowprotocol/widget-lib' + +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { DeadlineSelector } from 'modules/limitOrders/pure/DeadlineSelector' -import { LimitOrderDeadline, limitOrdersDeadlines } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' +import { + getLimitOrderDeadlines, + LIMIT_ORDERS_DEADLINES, + LimitOrderDeadline, +} from 'modules/limitOrders/pure/DeadlineSelector/deadlines' import { limitOrdersSettingsAtom, updateLimitOrdersSettingsAtom, @@ -13,29 +19,52 @@ export function DeadlineInput() { const { deadlineMilliseconds, customDeadlineTimestamp } = useAtomValue(limitOrdersSettingsAtom) const updateSettingsState = useSetAtom(updateLimitOrdersSettingsAtom) const currentDeadlineNode = useRef() - const existingDeadline = useMemo(() => { - return limitOrdersDeadlines.find((item) => item.value === deadlineMilliseconds) - }, [deadlineMilliseconds]) + const existingDeadline = useMemo( + () => getLimitOrderDeadlines(deadlineMilliseconds).find((item) => item.value === deadlineMilliseconds), + [deadlineMilliseconds], + ) + + const widgetDeadlineMinutes = useInjectedWidgetDeadline(TradeType.LIMIT) + + useEffect(() => { + if (widgetDeadlineMinutes) { + const widgetDeadlineDelta = widgetDeadlineMinutes * 60 * 1000 + const min = LIMIT_ORDERS_DEADLINES[0].value + const max = LIMIT_ORDERS_DEADLINES[LIMIT_ORDERS_DEADLINES.length - 1].value + + let deadlineMilliseconds = widgetDeadlineDelta + if (widgetDeadlineDelta < min) { + deadlineMilliseconds = min + } else if (widgetDeadlineDelta > max) { + deadlineMilliseconds = max + } + + updateSettingsState({ customDeadlineTimestamp: null, deadlineMilliseconds }) + } + }, [widgetDeadlineMinutes, updateSettingsState]) + + const isDeadlineDisabled = !!widgetDeadlineMinutes const selectDeadline = useCallback( (deadline: LimitOrderDeadline) => { updateSettingsState({ deadlineMilliseconds: deadline.value, customDeadlineTimestamp: null }) currentDeadlineNode.current?.click() // Close dropdown }, - [updateSettingsState] + [updateSettingsState], ) const selectCustomDeadline = useCallback( (customDeadline: number | null) => { updateSettingsState({ customDeadlineTimestamp: customDeadline }) }, - [updateSettingsState] + [updateSettingsState], ) return ( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts index 3f183cf2cc..b0a04d3be0 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts @@ -1,4 +1,5 @@ import ms from 'ms.macro' +import { format } from 'timeago.js' import { MAX_ORDER_DEADLINE } from 'common/constants/common' @@ -12,7 +13,7 @@ export const MAX_CUSTOM_DEADLINE = MAX_ORDER_DEADLINE export const defaultLimitOrderDeadline: LimitOrderDeadline = { title: '7 Days', value: ms`7d` } -export const limitOrdersDeadlines: LimitOrderDeadline[] = [ +export const LIMIT_ORDERS_DEADLINES: LimitOrderDeadline[] = [ { title: '5 Minutes', value: ms`5m` }, { title: '30 Minutes', value: ms`30m` }, { title: '1 Hour', value: ms`1 hour` }, @@ -22,3 +23,27 @@ export const limitOrdersDeadlines: LimitOrderDeadline[] = [ { title: '1 Month', value: ms`30d` }, { title: '6 Months (max)', value: MAX_CUSTOM_DEADLINE }, ] + +/** + * Get limit order deadlines and optionally adds + * @param value + */ +export function getLimitOrderDeadlines(value?: number | LimitOrderDeadline): LimitOrderDeadline[] { + if (!value || LIMIT_ORDERS_DEADLINES.find((item) => item === value || item.value === value)) { + return LIMIT_ORDERS_DEADLINES + } + + const itemToAdd = typeof value === 'number' ? buildLimitOrderDeadline(value) : value + + return [...LIMIT_ORDERS_DEADLINES, itemToAdd].sort((a, b) => a.value - b.value) +} + +/** + * Builds a LimitOrderDeadline from milliseconds value. + * Uses timeago to an approximate title + */ +export function buildLimitOrderDeadline(value: number): LimitOrderDeadline { + const title = format(Date.now() + value, undefined).replace(/in /, '') + + return { title, value } +} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx index cb8d3b0b06..f0687b47e3 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx @@ -9,6 +9,7 @@ const Fixtures = { customDeadline={null} selectDeadline={() => void 0} selectCustomDeadline={() => void 0} + isDeadlineDisabled={false} /> ), } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx index 25fb8a440e..247d86c01f 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx @@ -16,7 +16,7 @@ import { import { CowModal as Modal } from 'common/pure/Modal' -import { LimitOrderDeadline, limitOrdersDeadlines } from './deadlines' +import { getLimitOrderDeadlines, LimitOrderDeadline } from './deadlines' import * as styledEl from './styled' const CUSTOM_DATE_OPTIONS: Intl.DateTimeFormatOptions = { @@ -31,12 +31,15 @@ const CUSTOM_DATE_OPTIONS: Intl.DateTimeFormatOptions = { export interface DeadlineSelectorProps { deadline: LimitOrderDeadline | undefined customDeadline: number | null + isDeadlineDisabled: boolean + selectDeadline(deadline: LimitOrderDeadline): void + selectCustomDeadline(deadline: number | null): void } export function DeadlineSelector(props: DeadlineSelectorProps) { - const { deadline, customDeadline, selectDeadline, selectCustomDeadline } = props + const { deadline, customDeadline, isDeadlineDisabled, selectDeadline, selectCustomDeadline } = props const currentDeadlineNode = useRef(null) const [[minDate, maxDate], setMinMax] = useState<[Date, Date]>(calculateMinMax) @@ -66,7 +69,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { } }, [maxDate, minDate, selectCustomDeadline, value]) - const existingDeadline = useMemo(() => limitOrdersDeadlines.find((item) => item === deadline), [deadline]) + const limitOrderDeadlines = useMemo(() => getLimitOrderDeadlines(deadline), [deadline]) const customDeadlineTitle = useMemo(() => { if (!customDeadline) { @@ -81,7 +84,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { selectCustomDeadline(null) // reset custom deadline currentDeadlineNode.current?.click() // Close dropdown }, - [selectCustomDeadline, selectDeadline] + [selectCustomDeadline, selectDeadline], ) // Sets value from input, if it exists @@ -92,7 +95,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { // In that case, use the default min value setValue(value || formatDateToLocalTime(minDate)) }, - [minDate] + [minDate], ) const [isOpen, setIsOpen] = useState(false) @@ -118,29 +121,38 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { onDismiss() }, [onDismiss, selectCustomDeadline, value]) + const deadlineDisplay = customDeadline ? customDeadlineTitle : deadline?.title + return ( Expiry - - - {customDeadline ? customDeadlineTitle : existingDeadline?.title} - - - - {limitOrdersDeadlines.map((item) => ( -
  • - setDeadline(item)}> - {item.title} - -
  • - ))} - - Custom - -
    -
    + + {isDeadlineDisabled ? ( +
    + {deadlineDisplay} +
    + ) : ( + + + {deadlineDisplay} + + + + {limitOrderDeadlines.map((item) => ( +
  • + setDeadline(item)}> + {item.title} + +
  • + ))} + + Custom + +
    +
    + )} {/* Custom deadline modal */} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts b/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts index 6045c3c672..a9cfc54141 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts @@ -15,7 +15,7 @@ import { updateLimitOrdersSettingsAtom, } from 'modules/limitOrders' import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' -import { limitOrdersDeadlines } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' +import { LIMIT_ORDERS_DEADLINES } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' import { partiallyFillableOverrideAtom } from 'modules/limitOrders/state/partiallyFillableOverride' import { useAlternativeOrder, useHideAlternativeOrderModal } from 'modules/trade/state/alternativeOrder' @@ -118,7 +118,7 @@ function useSetAlternativeRate(): null { // Set new active rate // The rate expects a raw fraction which is NOT a Price instace const activeRate = FractionUtils.fromPrice( - new Price({ baseAmount: inputCurrencyAmount, quoteAmount: outputCurrencyAmount }) + new Price({ baseAmount: inputCurrencyAmount, quoteAmount: outputCurrencyAmount }), ) updateRate({ activeRate, isTypedValue: false, isRateFromUrl: false, isAlternativeOrderRate: true }) @@ -170,7 +170,7 @@ function getDuration(order: Order | ParsedOrder): number { */ function getMatchingDeadline(duration: number) { // Match duration with approximate time - return limitOrdersDeadlines.find(({ value }) => { + return LIMIT_ORDERS_DEADLINES.find(({ value }) => { const ratio = value / duration // If the ratio is +/-10% off of 1, consider it a match return ratio > 0.9 && ratio < 1.1 diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx index d74a91ad0d..ac549413d6 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx @@ -20,10 +20,13 @@ export function getNativeOrderDeadlineTooltip(symbols: (string | undefined)[] | ) } + export function getNonNativeOrderDeadlineTooltip() { return ( Your swap expires and will not execute if it is pending for longer than the selected duration. +
    +
    {INPUT_OUTPUT_EXPLANATION}
    ) diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx index 591ce87d25..1d56933f36 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx @@ -1,6 +1,6 @@ import { UI } from '@cowprotocol/ui' -import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button' +import { Menu, MenuButton, MenuItem, MenuList } from '@reach/menu-button' import { ChevronDown } from 'react-feather' import styled from 'styled-components/macro' @@ -47,7 +47,6 @@ const StyledMenuButton = styled(MenuButton)` cursor: pointer; width: 100%; justify-content: space-between; - color: inherit; ` const StyledMenuItem = styled(MenuItem)` diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx index fb804210f3..41050dad54 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx @@ -3,9 +3,11 @@ import { useEffect, useLayoutEffect, useMemo, useState } from 'react' import { renderTooltip } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' +import { TradeType } from '@cowprotocol/widget-lib' import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' import { openAdvancedOrdersTabAnalytics, twapWalletCompatibilityAnalytics } from 'modules/analytics' +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { useReceiveAmountInfo } from 'modules/trade' import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' import { useTradeState } from 'modules/trade/hooks/useTradeState' @@ -21,7 +23,14 @@ import { useRateInfoParams } from 'common/hooks/useRateInfoParams' import * as styledEl from './styled' import { LABELS_TOOLTIPS } from './tooltips' -import { DEFAULT_NUM_OF_PARTS, DEFAULT_TWAP_SLIPPAGE, MAX_TWAP_SLIPPAGE, ORDER_DEADLINES } from '../../const' +import { + DEFAULT_NUM_OF_PARTS, + DEFAULT_TWAP_SLIPPAGE, + MAX_PART_TIME, + MAX_TWAP_SLIPPAGE, + MINIMUM_PART_TIME, + ORDER_DEADLINES, +} from '../../const' import { useFallbackHandlerVerification, useIsFallbackHandlerCompatible, @@ -65,9 +74,34 @@ export function TwapFormWidget() { const limitPriceAfterSlippage = usePrice( receiveAmountInfo?.afterSlippage.sellAmount, - receiveAmountInfo?.afterSlippage.buyAmount + receiveAmountInfo?.afterSlippage.buyAmount, ) + const widgetDeadline = useInjectedWidgetDeadline(TradeType.ADVANCED) + + useEffect(() => { + if (widgetDeadline) { + // Ensure min part duration + const minDuration = Math.floor(MINIMUM_PART_TIME / 60) * 2 // it must have at least 2 parts + + const maxDuration = Math.floor(MAX_PART_TIME / 60) * numberOfPartsValue + + let minutes = widgetDeadline + if (widgetDeadline < minDuration) { + minutes = minDuration + } else if (widgetDeadline > maxDuration) { + minutes = maxDuration + } + + updateSettingsState({ + customDeadline: { hours: 0, minutes }, + isCustomDeadline: true, + }) + } + }, [widgetDeadline, updateSettingsState, numberOfPartsValue]) + + const isDeadlineDisabled = !!widgetDeadline + const deadlineState = { deadline, customDeadline, @@ -166,8 +200,9 @@ export function TwapFormWidget() { updateSettingsState(value)} + setDeadline={updateSettingsState} label={LABELS_TOOLTIPS.totalDuration.label} tooltip={renderTooltip(LABELS_TOOLTIPS.totalDuration.tooltip, { parts: numberOfPartsValue, diff --git a/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx b/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx index 8169b99b0a..6999fd7636 100644 --- a/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react' -import { UI } from '@cowprotocol/ui' -import { renderTooltip } from '@cowprotocol/ui' +import { renderTooltip, UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' @@ -10,14 +9,17 @@ import { Content } from 'modules/trade/pure/TradeWidgetField/styled' import { LabelTooltip } from 'modules/twap' import { customDeadlineToSeconds, deadlinePartsDisplay } from 'modules/twap/utils/deadlinePartsDisplay' +import { TradeWidgetField } from '../../../trade/pure/TradeWidgetField' import { defaultCustomDeadline, TwapOrdersDeadline } from '../../state/twapOrdersSettingsAtom' import { CustomDeadlineSelector } from '../CustomDeadlineSelector' interface DeadlineSelectorProps { items: TradeSelectItem[] deadline: TwapOrdersDeadline + isDeadlineDisabled: boolean label: LabelTooltip['label'] tooltip: LabelTooltip['tooltip'] + setDeadline(value: TwapOrdersDeadline): void } @@ -48,10 +50,22 @@ const StyledTradeSelect = styled(TradeSelect)` } ` +const StyledTradeField = styled(TradeWidgetField)` + ${Content} { + width: 100%; + color: inherit; + } + + ${Content} > div { + width: 100%; + } +` + export function DeadlineSelector(props: DeadlineSelectorProps) { const { items, deadline: { deadline, customDeadline, isCustomDeadline }, + isDeadlineDisabled, label, tooltip, setDeadline, @@ -74,7 +88,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { }) } }, - [setIsCustomModalOpen, setDeadline] + [setIsCustomModalOpen, setDeadline], ) const activeLabel = useMemo(() => { @@ -87,13 +101,19 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { return ( <> - + {isDeadlineDisabled ? ( + +
    {activeLabel}
    +
    + ) : ( + + )} setDeadline({ isCustomDeadline: true, customDeadline: value, deadline: 0 })} customDeadline={customDeadline} diff --git a/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx b/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx new file mode 100644 index 0000000000..57aff95d71 --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx @@ -0,0 +1,23 @@ +import { Dispatch, SetStateAction } from 'react' + +import { FormControl, TextField } from '@mui/material' + +export type DeadlineControlProps = { + label: string + deadlineState: [number | undefined, Dispatch>] +} + +export function DeadlineControl({ label, deadlineState: [state, setState] }: DeadlineControlProps) { + return ( + + setState(value && !isNaN(+value) ? Math.max(1, Number(value)) : undefined)} + size="small" + inputProps={{ min: 1 }} // Set minimum value to 1 + /> + + ) +} diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index b00d73748b..41c736f6d9 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' -import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' +import { CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' import { isDev, isLocalHost, isVercel } from '../../../env' import { ConfiguratorState } from '../types' @@ -31,6 +31,10 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi sellTokenAmount, buyToken, buyTokenAmount, + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, tokenListUrls, customColors, defaultColors, @@ -57,6 +61,14 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi tradeType: currentTradeType, sell: { asset: sellToken, amount: sellTokenAmount ? sellTokenAmount.toString() : undefined }, buy: { asset: buyToken, amount: buyTokenAmount?.toString() }, + forcedOrderDeadline: + swapDeadline || limitDeadline || advancedDeadline + ? { + [TradeType.SWAP]: swapDeadline, + [TradeType.LIMIT]: limitDeadline, + [TradeType.ADVANCED]: advancedDeadline, + } + : deadline, enabledTradeTypes, theme: JSON.stringify(customColors) === JSON.stringify(defaultColors) diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index 2bd0b12d5f..5aed4286e6 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -34,6 +34,7 @@ import { CurrencyInputControl } from './controls/CurrencyInputControl' import { CurrentTradeTypeControl } from './controls/CurrentTradeTypeControl' import { CustomImagesControl } from './controls/CustomImagesControl' import { CustomSoundsControl } from './controls/CustomSoundsControl' +import { DeadlineControl } from './controls/DeadlineControl' import { NetworkControl, NetworkOption, NetworkOptions } from './controls/NetworkControl' import { PaletteControl } from './controls/PaletteControl' import { PartnerFeeControl } from './controls/PartnerFeeControl' @@ -106,6 +107,15 @@ export function Configurator({ title }: { title: string }) { const [buyToken] = buyTokenState const [buyTokenAmount] = buyTokenAmountState + const deadlineState = useState() + const [deadline] = deadlineState + const swapDeadlineState = useState() + const [swapDeadline] = swapDeadlineState + const limitDeadlineState = useState() + const [limitDeadline] = limitDeadlineState + const advancedDeadlineState = useState() + const [advancedDeadline] = advancedDeadlineState + const tokenListUrlsState = useState(DEFAULT_TOKEN_LISTS) const customTokensState = useState([]) const [tokenListUrls] = tokenListUrlsState @@ -146,6 +156,10 @@ export function Configurator({ title }: { title: string }) { // Don't change chainId in the widget URL if the user is connected to a wallet // Because useSyncWidgetNetwork() will send a request to change the network const state: ConfiguratorState = { + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, chainId: IS_IFRAME ? undefined : !isConnected || !walletChainId ? chainId : walletChainId, theme: mode, currentTradeType, @@ -261,6 +275,16 @@ export function Configurator({ title }: { title: string }) { + Forced Order Deadline + + Global deadline settings + + + Individual deadline settings + + + + Integrations diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/app/configurator/types.ts index 2755dbb135..d48f94df67 100644 --- a/apps/widget-configurator/src/app/configurator/types.ts +++ b/apps/widget-configurator/src/app/configurator/types.ts @@ -21,6 +21,10 @@ export interface ConfiguratorState { sellTokenAmount: number | undefined buyToken: string buyTokenAmount: number | undefined + deadline: number | undefined + swapDeadline: number | undefined + limitDeadline: number | undefined + advancedDeadline: number | undefined tokenListUrls: TokenListItem[] customColors: ColorPalette defaultColors: ColorPalette diff --git a/libs/common-const/src/misc.ts b/libs/common-const/src/misc.ts index 3fd037eb9f..2e8bed590c 100644 --- a/libs/common-const/src/misc.ts +++ b/libs/common-const/src/misc.ts @@ -6,7 +6,6 @@ export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' // 30 minutes, denominated in seconds export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30 -export const L2_DEADLINE_FROM_NOW = 60 * 5 // one basis JSBI.BigInt const BPS_BASE = JSBI.BigInt(10000) diff --git a/libs/widget-lib/src/types.ts b/libs/widget-lib/src/types.ts index 368ef01970..4f94816c43 100644 --- a/libs/widget-lib/src/types.ts +++ b/libs/widget-lib/src/types.ts @@ -1,5 +1,6 @@ import type { SupportedChainId } from '@cowprotocol/cow-sdk' import { CowWidgetEventListeners, CowWidgetEventPayloadMap, CowWidgetEvents } from '@cowprotocol/events' + export type { SupportedChainId } from '@cowprotocol/cow-sdk' export type PerTradeTypeConfig = Partial> @@ -82,6 +83,8 @@ interface TradeAsset { amount?: string } +export type ForcedOrderDeadline = FlexibleConfig + export enum TradeType { SWAP = 'swap', LIMIT = 'limit', @@ -230,6 +233,15 @@ export interface CowSwapWidgetParams { */ buy?: TradeAsset + /** + * Forced order deadline in minutes. When set, user's won't be able to edit the deadline. + * + * Either a single value applied to each individual order type accordingly or an optional individual value per order type. + * + * The app will use the appropriated min/max value per order type. + */ + forcedOrderDeadline?: ForcedOrderDeadline + /** * Enables the ability to switch between trade types in the widget. */ From 681fb20dab0b4155d50ad7f32c7a48cb95e084a3 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 17 Oct 2024 11:29:43 +0100 Subject: [PATCH 036/116] feat(widget): hide orders table (#4993) * feat: add widget option to hide orders table * feat: use hideOrdersTable widget param * fix: show orders button on SWAP form * feat: add warning regarding the behaviour --- .../containers/TradeWidget/TradeWidgetForm.tsx | 3 ++- .../src/pages/AdvancedOrders/index.tsx | 16 +++++++++++----- .../src/pages/LimitOrders/RegularLimitOrders.tsx | 15 ++++++++++----- .../hooks/useWidgetParamsAndSettings.ts | 2 ++ .../src/app/configurator/index.tsx | 12 ++++++++++++ .../src/app/configurator/types.ts | 1 + libs/widget-lib/src/types.ts | 9 ++++++++- 7 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index bba2c13940..100517aa7a 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -49,7 +49,7 @@ const scrollToMyOrders = () => { export function TradeWidgetForm(props: TradeWidgetProps) { const isInjectedWidgetMode = isInjectedWidget() - const { standaloneMode } = useInjectedWidgetParams() + const { standaloneMode, hideOrdersTable } = useInjectedWidgetParams() const isAlternativeOrderModalVisible = useIsAlternativeOrderModalVisible() const { pendingActivity } = useCategorizeRecentActivity() @@ -113,6 +113,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { const shouldShowMyOrdersButton = !alternativeOrderModalVisible && (!isInjectedWidgetMode && isConnectedSwapMode ? isUpToLarge : true) && + (isConnectedSwapMode || !hideOrdersTable) && ((isConnectedSwapMode && standaloneMode !== true) || (isLimitOrderMode && isUpToLarge && isLimitOrdersUnlocked) || (isAdvancedMode && isUpToLarge && isAdvancedOrdersUnlocked)) diff --git a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx index 72644944e9..c1b37aa56f 100644 --- a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx @@ -6,6 +6,7 @@ import { FillAdvancedOrdersDerivedStateUpdater, SetupAdvancedOrderAmountsFromUrlUpdater, } from 'modules/advancedOrders' +import { useInjectedWidgetParams } from 'modules/injectedWidget' import { OrdersTableWidget, TabOrderTypes } from 'modules/ordersTable' import * as styledEl from 'modules/trade/pure/TradePageLayout' import { @@ -19,6 +20,7 @@ import { } from 'modules/twap' import { TwapFormState } from 'modules/twap/pure/PrimaryActionButton/getTwapFormState' + export default function AdvancedOrdersPage() { const { isUnlocked } = useAtomValue(advancedOrdersAtom) @@ -32,6 +34,8 @@ export default function AdvancedOrdersPage() { const advancedWidgetParams = { disablePriceImpact } + const { hideOrdersTable } = useInjectedWidgetParams() + return ( <> @@ -50,11 +54,13 @@ export default function AdvancedOrdersPage() { - + {!hideOrdersTable && ( + + )} diff --git a/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx b/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx index e90cde808a..542305fa8f 100644 --- a/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx +++ b/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx @@ -3,14 +3,17 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { useOrders } from 'legacy/state/orders/hooks' +import { useInjectedWidgetParams } from 'modules/injectedWidget' import { LimitOrdersWidget, useIsWidgetUnlocked } from 'modules/limitOrders' import { OrdersTableWidget, TabOrderTypes } from 'modules/ordersTable' import * as styledEl from 'modules/trade/pure/TradePageLayout' + export function RegularLimitOrders() { const isUnlocked = useIsWidgetUnlocked() const { chainId, account } = useWalletInfo() const allLimitOrders = useOrders(chainId, account, UiOrderType.LIMIT) + const { hideOrdersTable } = useInjectedWidgetParams() return ( @@ -19,11 +22,13 @@ export function RegularLimitOrders() { - + {!hideOrdersTable && ( + + )} ) diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index 41c736f6d9..0e9c49a740 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -44,6 +44,7 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi disableToastMessages, disableProgressBar, hideBridgeInfo, + hideOrdersTable, } = configuratorState const themeColors = { @@ -98,6 +99,7 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi } : undefined, hideBridgeInfo, + hideOrdersTable, } return params diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index 5aed4286e6..cb387444d9 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -143,6 +143,9 @@ export function Configurator({ title }: { title: string }) { const [hideBridgeInfo, setHideBridgeInfo] = useState(false) const toggleHideBridgeInfo = useCallback(() => setHideBridgeInfo((curr) => !curr), []) + const [hideOrdersTable, setHideOrdersTable] = useState(false) + const toggleHideOrdersTable = useCallback(() => setHideOrdersTable((curr) => !curr), []) + const LINKS = [ { icon: , label: 'View embed code', onClick: () => handleDialogOpen() }, { icon: , label: 'Widget web', url: `https://cow.fi/widget/?${UTM_PARAMS}` }, @@ -177,6 +180,7 @@ export function Configurator({ title }: { title: string }) { disableToastMessages, disableProgressBar, hideBridgeInfo, + hideOrdersTable, } const computedParams = useWidgetParams(state) @@ -326,6 +330,14 @@ export function Configurator({ title }: { title: string }) { + + Hide orders table: + + } label="Show orders table" /> + } label="Hide orders table" /> + + + {isDrawerOpen && ( Date: Thu, 17 Oct 2024 16:42:54 +0100 Subject: [PATCH 037/116] fix(smart-slippage): fix smart slip tooltip and feature flag (#5004) * fix: remove hard coded smart slippage value * fix: handle case when multiplier percentage in falsy * feat: use different tooltip when feature flag is off * fix: pass dynamic slippage settings to row slippage content * chore: fix lint --- .../components/TransactionSettings/index.tsx | 2 +- .../ConfirmSwapModalSetup/index.tsx | 4 ++- .../pure/Row/RowSlippageContent/index.tsx | 34 +++++++++++++------ .../calculateBpsFromFeeMultiplier.ts | 2 +- .../useSmartSlippageFromFeeMultiplier.ts | 3 +- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx index 7ad9ea8175..371ac7abbb 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx @@ -335,7 +335,7 @@ export function TransactionSettings() { // Your transaction will revert if the price changes unfavorably by more than this percentage. isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol, getWrappedToken(nativeCurrency).symbol]) - : getNonNativeSlippageTooltip(true) + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage, isSettingsModal: true }) } /> diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index 9b58d26442..e904e03fa6 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -35,6 +35,7 @@ import { useIsEoaEthFlow } from '../../hooks/useIsEoaEthFlow' import { useNavigateToNewOrderCallback } from '../../hooks/useNavigateToNewOrderCallback' import { useShouldPayGas } from '../../hooks/useShouldPayGas' import { useSwapConfirmButtonText } from '../../hooks/useSwapConfirmButtonText' +import { useSmartSwapSlippage } from '../../hooks/useSwapSlippage' import { useSwapState } from '../../hooks/useSwapState' import { NetworkCostsTooltipSuffix } from '../../pure/NetworkCostsTooltipSuffix' import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from '../../pure/Row/RowSlippageContent' @@ -87,6 +88,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const buttonText = useSwapConfirmButtonText(slippageAdjustedSellAmount) const isSmartSlippageApplied = useIsSmartSlippageApplied() + const smartSlippage = useSmartSwapSlippage() const labelsAndTooltips = useMemo( () => ({ @@ -96,7 +98,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { : undefined, slippageTooltip: isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol]) - : getNonNativeSlippageTooltip(), + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage }), expectReceiveLabel: isExactIn ? 'Expected to receive' : 'Expected to sell', minReceivedLabel: isExactIn ? 'Minimum receive' : 'Maximum sent', minReceivedTooltip: getMinimumReceivedTooltip(allowedSlippage, isExactIn), diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index 07a343a2fd..4facd945c0 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -53,19 +53,30 @@ export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (st robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. ) -export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( + +export const getNonNativeSlippageTooltip = (params?: { isDynamic?: boolean; isSettingsModal?: boolean }) => ( - CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the - best price.{' '} - {isSettingsModal ? ( + {params?.isDynamic ? ( <> - To override this, enter your desired slippage amount. -
    -
    - Either way, your slippage is protected from MEV! + CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting + the best price.{' '} + {params?.isSettingsModal ? ( + <> + To override this, enter your desired slippage amount. +
    +
    + Either way, your slippage is protected from MEV! + + ) : ( + <> +
    +
    + Trades are protected from MEV, so your slippage can't be exploited! + + )} ) : ( - "Trades are protected from MEV, so your slippage can't be exploited!" + <>CoW Swap trades are protected from MEV, so your slippage can't be exploited! )}
    ) @@ -113,7 +124,10 @@ export function RowSlippageContent(props: RowSlippageContentProps) { } = props const tooltipContent = - slippageTooltip || (isEoaEthFlow ? getNativeSlippageTooltip(chainId, symbols) : getNonNativeSlippageTooltip()) + slippageTooltip || + (isEoaEthFlow + ? getNativeSlippageTooltip(chainId, symbols) + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage })) // In case the user happened to set the same slippage as the suggestion, do not show the suggestion const suggestedEqualToUserSlippage = smartSlippage && smartSlippage === displaySlippage diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts index 73d161d60d..fdd71e5f9f 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts @@ -8,7 +8,7 @@ export function calculateBpsFromFeeMultiplier( isSell: boolean | undefined, multiplierPercentage: number, ): number | undefined { - if (!sellAmount || !feeAmount || isSell === undefined || multiplierPercentage <= 0) { + if (!sellAmount || !feeAmount || isSell === undefined || !multiplierPercentage || multiplierPercentage <= 0) { return undefined } diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts index f5566085a1..d80af85a40 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts @@ -6,7 +6,6 @@ import { useReceiveAmountInfo } from 'modules/trade' import { calculateBpsFromFeeMultiplier } from './calculateBpsFromFeeMultiplier' - /** * Calculates smart slippage in bps, based on quoted fee * @@ -19,7 +18,7 @@ export function useSmartSlippageFromFeeMultiplier(): number | undefined { const sellAmount = isSell ? afterNetworkCosts?.sellAmount : beforeNetworkCosts?.sellAmount const feeAmount = costs?.networkFee?.amountInSellCurrency - const { smartSlippageFeeMultiplierPercentage = 50 } = useFeatureFlags() + const { smartSlippageFeeMultiplierPercentage } = useFeatureFlags() return useMemo( () => calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, smartSlippageFeeMultiplierPercentage), From b47e3635b9b3f0f918c5d4268d2b3e33d5fc009b Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 15:31:26 +0500 Subject: [PATCH 038/116] chore: release main (#5007) --- .release-please-manifest.json | 14 +++++++------- apps/cowswap-frontend/CHANGELOG.md | 19 +++++++++++++++++++ apps/cowswap-frontend/package.json | 2 +- apps/explorer/CHANGELOG.md | 7 +++++++ apps/explorer/package.json | 2 +- apps/widget-configurator/CHANGELOG.md | 9 +++++++++ apps/widget-configurator/package.json | 2 +- libs/common-const/CHANGELOG.md | 8 ++++++++ libs/common-const/package.json | 2 +- libs/hook-dapp-lib/CHANGELOG.md | 8 ++++++++ libs/hook-dapp-lib/package.json | 2 +- libs/ui/CHANGELOG.md | 7 +++++++ libs/ui/package.json | 2 +- libs/widget-lib/CHANGELOG.md | 9 +++++++++ libs/widget-lib/package.json | 2 +- 15 files changed, 81 insertions(+), 14 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 338c004e91..b52eed9497 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,13 +1,13 @@ { - "apps/cowswap-frontend": "1.85.0", - "apps/explorer": "2.35.1", + "apps/cowswap-frontend": "1.86.0", + "apps/explorer": "2.35.2", "libs/permit-utils": "0.4.0", - "libs/widget-lib": "0.15.0", + "libs/widget-lib": "0.16.0", "libs/widget-react": "0.11.0", - "apps/widget-configurator": "1.7.2", + "apps/widget-configurator": "1.8.0", "libs/analytics": "1.8.0", "libs/assets": "1.8.0", - "libs/common-const": "1.7.0", + "libs/common-const": "1.8.0", "libs/common-hooks": "1.4.0", "libs/common-utils": "1.7.2", "libs/core": "1.3.0", @@ -16,7 +16,7 @@ "libs/snackbars": "1.1.0", "libs/tokens": "1.10.0", "libs/types": "1.2.0", - "libs/ui": "1.10.1", + "libs/ui": "1.11.0", "libs/wallet": "1.6.0", "apps/cow-fi": "1.15.0", "libs/wallet-provider": "1.0.0", @@ -24,5 +24,5 @@ "libs/abis": "1.2.0", "libs/balances-and-allowances": "1.0.0", "libs/iframe-transport": "1.0.0", - "libs/hook-dapp-lib": "1.1.0" + "libs/hook-dapp-lib": "1.2.0" } diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index ad2177ef72..560739bd2d 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [1.86.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.85.0...cowswap-v1.86.0) (2024-10-18) + + +### Features + +* display new label for cow amm ([#4994](https://github.com/cowprotocol/cowswap/issues/4994)) ([531e63f](https://github.com/cowprotocol/cowswap/commit/531e63f666ffcafdaf8e2b1c2850991facbe5cf1)) +* **hooks-store:** add claim vesting iframe hook ([#4924](https://github.com/cowprotocol/cowswap/issues/4924)) ([395f48f](https://github.com/cowprotocol/cowswap/commit/395f48f57d93de67305791fdb9a668bdd693074e)) +* **hooks-store:** add sell/buy amounts to hook-dapp context ([#4990](https://github.com/cowprotocol/cowswap/issues/4990)) ([26cbffb](https://github.com/cowprotocol/cowswap/commit/26cbffbbfe8edbc0a4a9ba31fe9c0d42852118d9)) +* **slippage:** small order slippage v2 ([#4934](https://github.com/cowprotocol/cowswap/issues/4934)) ([7b2a49c](https://github.com/cowprotocol/cowswap/commit/7b2a49c41ecfd62107a3128e771003743094d246)) +* **smart-slippage:** update smart slippage text ([#4982](https://github.com/cowprotocol/cowswap/issues/4982)) ([4b89ecb](https://github.com/cowprotocol/cowswap/commit/4b89ecbf661e6c30193586c704e23c78b2bfc22b)) +* **widget:** deadline widget param ([#4991](https://github.com/cowprotocol/cowswap/issues/4991)) ([ce3b5b8](https://github.com/cowprotocol/cowswap/commit/ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2)) +* **widget:** hide bridge info ([#4992](https://github.com/cowprotocol/cowswap/issues/4992)) ([9842afd](https://github.com/cowprotocol/cowswap/commit/9842afdb887497d235a01538663488b0b8852bb5)) +* **widget:** hide orders table ([#4993](https://github.com/cowprotocol/cowswap/issues/4993)) ([681fb20](https://github.com/cowprotocol/cowswap/commit/681fb20dab0b4155d50ad7f32c7a48cb95e084a3)) + + +### Bug Fixes + +* **smart-slippage:** fix smart slip tooltip and feature flag ([#5004](https://github.com/cowprotocol/cowswap/issues/5004)) ([c6ea5af](https://github.com/cowprotocol/cowswap/commit/c6ea5af5d24b9a806540d53d2a0d9e12799d4eff)) + ## [1.85.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.84.0...cowswap-v1.85.0) (2024-10-10) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index e3d977b4f7..a5898d6e9b 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.85.0", + "version": "1.86.0", "description": "CoW Swap", "main": "index.js", "author": "", diff --git a/apps/explorer/CHANGELOG.md b/apps/explorer/CHANGELOG.md index 3c9f1687c5..38a0d41e0f 100644 --- a/apps/explorer/CHANGELOG.md +++ b/apps/explorer/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.35.2](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.1...explorer-v2.35.2) (2024-10-18) + + +### Bug Fixes + +* **explorer:** display hook details of unknown hook-dapp ([#4995](https://github.com/cowprotocol/cowswap/issues/4995)) ([eaa29f3](https://github.com/cowprotocol/cowswap/commit/eaa29f3ed421d92214b857bf1c57d75b0317cbba)) + ## [2.35.1](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.0...explorer-v2.35.1) (2024-10-14) diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 34f4e6a3e7..541ed15881 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/explorer", - "version": "2.35.1", + "version": "2.35.2", "description": "CoW Swap Explorer", "main": "src/main.tsx", "author": "", diff --git a/apps/widget-configurator/CHANGELOG.md b/apps/widget-configurator/CHANGELOG.md index 179cf695e8..06b0a94e05 100644 --- a/apps/widget-configurator/CHANGELOG.md +++ b/apps/widget-configurator/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.8.0](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.7.2...widget-configurator-v1.8.0) (2024-10-18) + + +### Features + +* **widget:** deadline widget param ([#4991](https://github.com/cowprotocol/cowswap/issues/4991)) ([ce3b5b8](https://github.com/cowprotocol/cowswap/commit/ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2)) +* **widget:** hide bridge info ([#4992](https://github.com/cowprotocol/cowswap/issues/4992)) ([9842afd](https://github.com/cowprotocol/cowswap/commit/9842afdb887497d235a01538663488b0b8852bb5)) +* **widget:** hide orders table ([#4993](https://github.com/cowprotocol/cowswap/issues/4993)) ([681fb20](https://github.com/cowprotocol/cowswap/commit/681fb20dab0b4155d50ad7f32c7a48cb95e084a3)) + ## [1.7.2](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.7.1...widget-configurator-v1.7.2) (2024-10-08) diff --git a/apps/widget-configurator/package.json b/apps/widget-configurator/package.json index b903661cb1..8efbe145c2 100644 --- a/apps/widget-configurator/package.json +++ b/apps/widget-configurator/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-configurator", - "version": "1.7.2", + "version": "1.8.0", "description": "CoW Swap widget configurator", "main": "src/main.tsx", "author": "", diff --git a/libs/common-const/CHANGELOG.md b/libs/common-const/CHANGELOG.md index a4af2ddeaf..f47c4b779f 100644 --- a/libs/common-const/CHANGELOG.md +++ b/libs/common-const/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.8.0](https://github.com/cowprotocol/cowswap/compare/common-const-v1.7.0...common-const-v1.8.0) (2024-10-18) + + +### Features + +* **smart-slippage:** update smart slippage text ([#4982](https://github.com/cowprotocol/cowswap/issues/4982)) ([4b89ecb](https://github.com/cowprotocol/cowswap/commit/4b89ecbf661e6c30193586c704e23c78b2bfc22b)) +* **widget:** deadline widget param ([#4991](https://github.com/cowprotocol/cowswap/issues/4991)) ([ce3b5b8](https://github.com/cowprotocol/cowswap/commit/ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2)) + ## [1.7.0](https://github.com/cowprotocol/cowswap/compare/common-const-v1.6.2...common-const-v1.7.0) (2024-07-12) diff --git a/libs/common-const/package.json b/libs/common-const/package.json index 72cb582543..b6222fc68f 100644 --- a/libs/common-const/package.json +++ b/libs/common-const/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/common-const", - "version": "1.7.0", + "version": "1.8.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/hook-dapp-lib/CHANGELOG.md b/libs/hook-dapp-lib/CHANGELOG.md index 92bc14bcf4..391264f286 100644 --- a/libs/hook-dapp-lib/CHANGELOG.md +++ b/libs/hook-dapp-lib/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.2.0](https://github.com/cowprotocol/cowswap/compare/hook-dapp-lib-v1.1.0...hook-dapp-lib-v1.2.0) (2024-10-18) + + +### Features + +* **hooks-store:** add claim vesting iframe hook ([#4924](https://github.com/cowprotocol/cowswap/issues/4924)) ([395f48f](https://github.com/cowprotocol/cowswap/commit/395f48f57d93de67305791fdb9a668bdd693074e)) +* **hooks-store:** add sell/buy amounts to hook-dapp context ([#4990](https://github.com/cowprotocol/cowswap/issues/4990)) ([26cbffb](https://github.com/cowprotocol/cowswap/commit/26cbffbbfe8edbc0a4a9ba31fe9c0d42852118d9)) + ## [1.1.0](https://github.com/cowprotocol/cowswap/compare/hook-dapp-lib-v1.0.0...hook-dapp-lib-v1.1.0) (2024-10-10) diff --git a/libs/hook-dapp-lib/package.json b/libs/hook-dapp-lib/package.json index f55cdc77df..f9dc713435 100644 --- a/libs/hook-dapp-lib/package.json +++ b/libs/hook-dapp-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/hook-dapp-lib", - "version": "1.1.0", + "version": "1.2.0", "type": "commonjs", "description": "CoW Swap Hook Dapp Library. Allows you to develop pre/post hooks dapps for CoW Protocol.", "main": "index.js", diff --git a/libs/ui/CHANGELOG.md b/libs/ui/CHANGELOG.md index 4d9fbe85eb..1e00acf735 100644 --- a/libs/ui/CHANGELOG.md +++ b/libs/ui/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.11.0](https://github.com/cowprotocol/cowswap/compare/ui-v1.10.1...ui-v1.11.0) (2024-10-18) + + +### Features + +* display new label for cow amm ([#4994](https://github.com/cowprotocol/cowswap/issues/4994)) ([531e63f](https://github.com/cowprotocol/cowswap/commit/531e63f666ffcafdaf8e2b1c2850991facbe5cf1)) + ## [1.10.1](https://github.com/cowprotocol/cowswap/compare/ui-v1.10.0...ui-v1.10.1) (2024-10-10) diff --git a/libs/ui/package.json b/libs/ui/package.json index 7633ab1d29..ac6448fab6 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/ui", - "version": "1.10.1", + "version": "1.11.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/widget-lib/CHANGELOG.md b/libs/widget-lib/CHANGELOG.md index 695fc25fb7..4b9aa025fe 100644 --- a/libs/widget-lib/CHANGELOG.md +++ b/libs/widget-lib/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.16.0](https://github.com/cowprotocol/cowswap/compare/widget-lib-v0.15.0...widget-lib-v0.16.0) (2024-10-18) + + +### Features + +* **widget:** deadline widget param ([#4991](https://github.com/cowprotocol/cowswap/issues/4991)) ([ce3b5b8](https://github.com/cowprotocol/cowswap/commit/ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2)) +* **widget:** hide bridge info ([#4992](https://github.com/cowprotocol/cowswap/issues/4992)) ([9842afd](https://github.com/cowprotocol/cowswap/commit/9842afdb887497d235a01538663488b0b8852bb5)) +* **widget:** hide orders table ([#4993](https://github.com/cowprotocol/cowswap/issues/4993)) ([681fb20](https://github.com/cowprotocol/cowswap/commit/681fb20dab0b4155d50ad7f32c7a48cb95e084a3)) + ## [0.15.0](https://github.com/cowprotocol/cowswap/compare/widget-lib-v0.14.1...widget-lib-v0.15.0) (2024-09-30) diff --git a/libs/widget-lib/package.json b/libs/widget-lib/package.json index b2dcdaa138..acfa1891f2 100644 --- a/libs/widget-lib/package.json +++ b/libs/widget-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-lib", - "version": "0.15.0", + "version": "0.16.0", "type": "commonjs", "description": "CoW Swap Widget Library. Allows you to easily embed a CoW Swap widget on your website.", "main": "index.js", From 02855f1fcaee7f74ff927848ed54e1913dc25cb9 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 16:14:30 +0500 Subject: [PATCH 039/116] chore: bump widget-react version (#5010) --- libs/widget-react/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/widget-react/package.json b/libs/widget-react/package.json index ede317c850..a903d60c04 100644 --- a/libs/widget-react/package.json +++ b/libs/widget-react/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-react", - "version": "0.11.0", + "version": "0.12.0", "type": "commonjs", "description": "CoW Swap Widget Library. Allows you to easily embed a CoW Swap widget on your React application.", "main": "index.js", @@ -22,6 +22,6 @@ "react" ], "dependencies": { - "@cowprotocol/widget-lib": "^0.15.0" + "@cowprotocol/widget-lib": "^0.16.0" } } From 3f8446b48a4f493448b262959b943756a24382d9 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 17:03:08 +0500 Subject: [PATCH 040/116] fix(widget): ignore selected eip6963 provider when in widget (#5009) --- apps/cowswap-frontend/src/cow-react/index.tsx | 7 ++++++- .../src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts | 6 ++++-- libs/wallet/src/web3-react/Web3Provider/index.tsx | 7 ++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/cowswap-frontend/src/cow-react/index.tsx b/apps/cowswap-frontend/src/cow-react/index.tsx index c525fe3bc8..bea80941b9 100644 --- a/apps/cowswap-frontend/src/cow-react/index.tsx +++ b/apps/cowswap-frontend/src/cow-react/index.tsx @@ -70,8 +70,13 @@ function Main() { function Web3ProviderInstance({ children }: { children: ReactNode }) { const selectedWallet = useAppSelector((state) => state.user.selectedWallet) + const { standaloneMode } = useInjectedWidgetParams() - return {children} + return ( + + {children} + + ) } function Toasts() { diff --git a/libs/wallet/src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts b/libs/wallet/src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts index a9a86fe0a2..22ac501bef 100644 --- a/libs/wallet/src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts +++ b/libs/wallet/src/web3-react/Web3Provider/hooks/useEagerlyConnect.ts @@ -27,7 +27,7 @@ async function connect(connector: Connector) { } } -export function useEagerlyConnect(selectedWallet: ConnectionType | undefined) { +export function useEagerlyConnect(selectedWallet: ConnectionType | undefined, standaloneMode?: boolean) { const [tryConnectEip6963Provider, setTryConnectEip6963Provider] = useState(false) const eagerlyConnectInitRef = useRef(false) const selectedEip6963ProviderInfo = useSelectedEip6963ProviderInfo() @@ -75,6 +75,8 @@ export function useEagerlyConnect(selectedWallet: ConnectionType | undefined) { * Activate the selected eip6963 provider */ useEffect(() => { + // Ignore remembered eip6963 provider if the app is in widget dapp mode + if (isInjectedWidget() && !standaloneMode) return if (!selectedWallet || !tryConnectEip6963Provider) return const connection = getWeb3ReactConnection(selectedWallet) @@ -90,5 +92,5 @@ export function useEagerlyConnect(selectedWallet: ConnectionType | undefined) { setTryConnectEip6963Provider(false) connect(connector) } - }, [selectedEip6963ProviderInfo, selectedWallet, setEip6963Provider, tryConnectEip6963Provider]) + }, [standaloneMode, selectedEip6963ProviderInfo, selectedWallet, setEip6963Provider, tryConnectEip6963Provider]) } diff --git a/libs/wallet/src/web3-react/Web3Provider/index.tsx b/libs/wallet/src/web3-react/Web3Provider/index.tsx index d25571b454..97cc1dc8df 100644 --- a/libs/wallet/src/web3-react/Web3Provider/index.tsx +++ b/libs/wallet/src/web3-react/Web3Provider/index.tsx @@ -13,10 +13,11 @@ import { Web3ReactConnection } from '../types' interface Web3ProviderProps { children: ReactNode selectedWallet: ConnectionType | undefined + standaloneMode?: boolean } -export function Web3Provider({ children, selectedWallet }: Web3ProviderProps) { - useEagerlyConnect(selectedWallet) +export function Web3Provider({ children, selectedWallet, standaloneMode }: Web3ProviderProps) { + useEagerlyConnect(selectedWallet, standaloneMode) const connections = useOrderedConnections(selectedWallet) const connectors: [Connector, Web3ReactHooks][] = connections @@ -25,7 +26,7 @@ export function Web3Provider({ children, selectedWallet }: Web3ProviderProps) { const key = useMemo( () => connections.map(({ type }: Web3ReactConnection) => getConnectionName(type)).join('-'), - [connections] + [connections], ) return ( From 99c4c42aec60a734a37926935be5dca6cd4cf11c Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 17:32:46 +0500 Subject: [PATCH 041/116] feat: setup vampire attack widget (#4950) * feat(yield): setup yield widget * fix(yield): display correct output amount * feat(trade-quote): support fast quote requests * feat(yield): display trade buttons * feat(yield): display confirm details * feat(yield): scope context to trade * feat(yield): do trade after confirmation * feat(yield): display order progress bar * refactor: move useIsEoaEthFlow to trade module * refactor: move hooks to tradeSlippage module * refactor: rename swapSlippage to tradeSlippage * feat(trade-slippage): split slippage state by trade type * refactor: unlink TransactionSettings from swap module * refactor: use reach modal in Settings component * refactor: move Settings component in trade module * feat(yield): add settings widget * feat(yield): use deadline value from settings * fix(trade-quote): skip fast quote if it slower than optimal * refactor: generalise TradeRateDetails from swap module * refactor: move TradeRateDetails into tradeWidgetAddons module * refactor: move SettingsTab into tradeWidgetAddons module * refactor(swap): generalise useHighFeeWarning() * refactor(swap): move hooks to trade module * refactor: move HighFeeWarning to trade widget addons * refactor: make HighFeeWarning independent * feat(yield): display trade warnings ZeroApprovalWarning and HighFeeWarning * refactor(trade): generalise NoImpactWarning * refactor(trade): generalise ZeroApprovalWarning * refactor(trade): generalise bundle tx banners * refactor: extract TradeWarnings * chore: fix yield form displaying * refactor(swap): generalise trade flow * feat(yield): support safe bundle swaps * chore: hide yield under ff * chore: remove lazy loading * chore: fix imports * fix: generalize smart slippage usage * fix: don't sync url params while navigating with yield * feat: open settings menu on slippage click * chore: update btn text * fix: make slippage settings through * chore: merge develop * chore: fix yield widget * chore: slippage label * fix: fix default trade state in menu * chore: fix lint * chore: add yield in widget conf * chore: merge develop * chore: fix hooks trade state * fix: fix smart slippage displaying * chore: center slippage banner --- .../src/common/constants/routes.ts | 8 + .../src/common/hooks/useGetMarketDimension.ts | 1 + .../src/common/hooks/useMenuItems.ts | 17 +- .../TransactionSubmittedContent/index.tsx | 7 +- .../src/common/updaters/FeesUpdater/index.ts | 2 +- .../updaters/orders/PendingOrdersUpdater.ts | 28 +-- .../common/utils/tradeSettingsTooltips.tsx | 61 ++++++ .../src/legacy/components/Settings/index.tsx | 164 -------------- .../legacy/components/SwapWarnings/index.tsx | 203 ------------------ .../src/legacy/components/swap/styleds.tsx | 100 +-------- .../src/legacy/hooks/usePriceImpact/types.ts | 14 -- .../legacy/hooks/useRefetchPriceCallback.tsx | 8 +- .../src/legacy/state/application/hooks.ts | 4 - .../src/legacy/state/application/reducer.ts | 4 +- .../src/legacy/state/user/hooks.tsx | 10 +- .../containers/AdvancedOrdersWidget/index.tsx | 11 +- .../src/modules/analytics/events.ts | 1 + .../appData/updater/AppDataInfoUpdater.ts | 1 + .../application/containers/App/RoutesApp.tsx | 10 +- .../containers/HookDappContainer/index.tsx | 1 + .../useRescueFundsFromProxy.ts | 2 +- .../containers/TenderlySimulate/index.tsx | 2 +- .../hooksStore/dapps/BuildHookApp/index.tsx | 2 +- .../dapps/ClaimGnoHookApp/index.tsx | 2 +- .../hooksStore/dapps/PermitHookApp/index.tsx | 2 +- .../modules/hooksStore/hooks/useAddHook.ts | 2 +- .../modules/hooksStore/hooks/useRemoveHook.ts | 2 +- .../hooks/useSetRecipientOverride.ts | 2 +- .../hooks/useSetupHooksStoreOrderParams.ts | 30 +-- .../hooksStore/hooks/useTenderlySimulate.ts | 2 +- .../CustomDappLoader/index.tsx | 2 +- .../updaters/iframeDappsManifestUpdater.tsx | 4 +- .../containers/LimitOrdersWarnings/index.tsx | 59 +---- .../containers/LimitOrdersWidget/index.tsx | 47 ++-- .../hooks/useLimitOrdersWarningsAccepted.ts | 17 +- .../limitOrders/services/tradeFlow/index.ts | 2 +- .../state/limitOrdersWarningsAtom.ts | 2 - .../pure/ReceiptModal/OrderTypeField.tsx | 1 + .../src/modules/permit/hooks/usePermitInfo.ts | 1 + .../ConfirmSwapModalSetup/index.tsx | 69 ++---- .../swap/containers/Row/RowDeadline/index.tsx | 37 ---- .../swap/containers/Row/RowSlippage/index.tsx | 62 ------ .../containers/SurplusModalSetup/index.tsx | 3 +- .../swap/containers/SwapUpdaters/index.tsx | 11 +- .../swap/containers/SwapWidget/index.tsx | 152 +++++-------- .../modules/swap/helpers/getEthFlowEnabled.ts | 3 - .../swap/helpers/getSwapButtonState.ts | 3 +- .../hooks/useBaseSafeBundleFlowContext.ts | 43 ---- .../modules/swap/hooks/useEthFlowContext.ts | 64 +++--- .../src/modules/swap/hooks/useFlowContext.ts | 167 -------------- .../modules/swap/hooks/useHandleSwap.test.tsx | 85 -------- .../src/modules/swap/hooks/useHandleSwap.ts | 63 ------ .../swap/hooks/useHandleSwapOrEthFlow.ts | 42 ++++ .../hooks/useSafeBundleApprovalFlowContext.ts | 35 --- .../swap/hooks/useSafeBundleEthFlowContext.ts | 27 --- .../src/modules/swap/hooks/useSetSlippage.ts | 7 - .../swap/hooks/useSwapButtonContext.ts | 27 +-- .../swap/hooks/useSwapConfirmButtonText.tsx | 4 +- .../modules/swap/hooks/useSwapFlowContext.ts | 55 +---- .../src/modules/swap/hooks/useSwapSlippage.ts | 18 -- .../src/modules/swap/hooks/useSwapState.tsx | 104 +-------- .../hooks/useTradeQuoteStateFromLegacy.ts | 3 +- .../src/modules/swap/index.ts | 2 +- .../swap/pure/ReceiveAmountInfo/index.tsx | 2 +- .../swap/pure/Row/RowDeadline/index.tsx | 91 -------- .../src/modules/swap/pure/Row/types.ts | 5 - .../src/modules/swap/pure/Row/typings.ts | 4 - .../swap/pure/SwapButtons/index.cosmos.tsx | 1 - .../modules/swap/pure/SwapButtons/index.tsx | 1 - .../modules/swap/pure/SwapButtons/styled.tsx | 2 +- .../src/modules/swap/pure/styled.tsx | 26 --- .../src/modules/swap/pure/warnings.tsx | 73 +------ .../modules/swap/services/ethFlow/index.ts | 28 ++- .../src/modules/swap/services/types.ts | 70 +----- .../updaters/EthFlowDeadlineUpdater.tsx | 2 +- .../swap/state/baseFlowContextSourceAtom.ts | 5 - .../modules/swap/state/useSwapDerivedState.ts | 4 +- .../src/modules/swap/types/flowContext.ts | 51 ----- .../swap/updaters/BaseFlowContextUpdater.tsx | 147 ------------- .../containers/NoImpactWarning/index.tsx | 73 +++++++ .../TradeBasicConfirmDetails/index.tsx | 10 +- .../TradeConfirmModal/index.cosmos.tsx | 2 +- .../trade/containers/TradeWarnings/index.tsx | 65 ++++++ .../TradeWidget/TradeWidgetForm.tsx | 29 ++- .../TradeWidget/TradeWidgetUpdaters.tsx | 4 + .../trade/containers/TradeWidget/index.tsx | 8 +- .../trade/containers/TradeWidget/types.ts | 4 +- .../containers/TradeWidgetLinks/index.tsx | 44 +++- .../useSetupTradeStateFromUrl.ts | 6 +- .../{swap => trade}/hooks/useIsEoaEthFlow.ts | 0 .../{swap => trade}/hooks/useIsSafeEthFlow.ts | 0 .../{swap => trade}/hooks/useIsSwapEth.ts | 4 +- .../hooks/useNavigateToNewOrderCallback.ts | 8 +- .../trade/hooks/useNotifyWidgetTrade.ts | 1 + .../trade/hooks/useOrderSubmittedContent.tsx | 35 +++ .../{swap => trade}/hooks/useShouldPayGas.ts | 2 +- .../src/modules/trade/hooks/useTradeState.ts | 35 ++- .../trade/hooks/useTradeTypeInfoFromUrl.tsx | 4 +- .../trade/hooks/useUnknownImpactWarning.ts | 29 +++ .../src/modules/trade/index.ts | 14 ++ .../trade/pure/ConfirmDetailsItem/index.tsx | 4 +- .../trade/pure/ConfirmDetailsItem/styled.ts | 6 +- .../trade/pure/NoImpactWarning/index.tsx | 40 ---- .../pure/TradeConfirmation/index.cosmos.tsx | 2 +- .../trade/pure/TradeConfirmation/index.tsx | 11 +- .../ZeroApprovalWarning.cosmos.tsx | 0 .../ZeroApprovalWarning.tsx | 2 +- .../trade}/pure/ZeroApprovalWarning/index.tsx | 0 .../trade/state/derivedTradeStateAtom.ts | 5 + .../{swap => trade}/state/isEoaEthFlowAtom.ts | 0 .../src/modules/trade/types/TradeType.ts | 1 + .../trade/utils/parameterizeTradeRoute.ts | 9 +- .../modules/tradeFlow/hooks/useHandleSwap.ts | 56 +++++ .../hooks/useSafeBundleFlowContext.ts | 47 ++++ .../tradeFlow/hooks/useTradeFlowContext.ts | 192 +++++++++++++++++ .../tradeFlow/hooks/useTradeFlowType.ts | 31 +++ .../src/modules/tradeFlow/index.ts | 4 + .../services/safeBundleFlow/index.ts | 0 .../safeBundleFlow/safeBundleApprovalFlow.ts | 32 ++- .../safeBundleFlow/safeBundleEthFlow.ts | 33 +-- .../services/swapFlow/README.md | 0 .../services/swapFlow/index.ts | 18 +- .../swapFlow/steps/presignOrderStep.ts | 0 .../services/swapFlow/swapFlow.puml | 0 .../tradeFlow/types/TradeFlowContext.ts | 52 +++++ .../services/validateTradeForm.ts | 2 + .../hooks/useSetTradeQuoteParams.ts | 6 +- .../tradeQuote/hooks/useTradeQuotePolling.ts | 47 +++- .../tradeQuote/state/tradeQuoteAtom.ts | 13 +- .../tradeQuote/state/tradeQuoteParamsAtom.ts | 1 + .../HighSuggestedSlippageWarning/index.tsx | 39 ++++ .../hooks/useIsSlippageModified.ts | 0 .../hooks/useIsSmartSlippageApplied.ts | 0 .../tradeSlippage/hooks/useSetSlippage.ts | 7 + .../tradeSlippage/hooks/useTradeSlippage.ts | 22 ++ .../src/modules/tradeSlippage/index.tsx | 6 + .../state/slippageValueAndTypeAtom.ts | 32 +-- .../calculateBpsFromFeeMultiplier.test.ts | 0 .../calculateBpsFromFeeMultiplier.ts | 0 .../updaters/SmartSlippageUpdater/index.ts | 4 +- .../useSmartSlippageFromBff.ts | 0 .../useSmartSlippageFromFeeMultiplier.ts | 0 .../containers/BundleTxWrapBanner/index.tsx | 29 +++ .../containers/HighFeeWarning/consts.ts | 3 + .../HighFeeWarning/hooks/useHighFeeWarning.ts | 86 ++++++++ .../containers/HighFeeWarning/index.tsx | 89 ++++++++ .../containers/HighFeeWarning/styled.tsx | 139 ++++++++++++ .../containers/RowDeadline/index.tsx | 30 +++ .../containers/RowSlippage/index.tsx | 66 ++++++ .../containers/SettingsTab/index.tsx | 116 ++++++++++ .../containers/SettingsTab/styled.tsx | 86 ++++++++ .../containers/TradeRateDetails/index.tsx | 40 ++-- .../containers}/TransactionSettings/index.tsx | 202 ++++------------- .../containers/TransactionSettings/styled.tsx | 131 +++++++++++ .../src/modules/tradeWidgetAddons/index.ts | 7 + .../pure/NetworkCostsTooltipSuffix.tsx | 4 +- .../pure/Row/RowDeadline/index.cosmos.tsx | 1 - .../pure/Row/RowDeadline/index.tsx | 51 +++++ .../Row/RowSlippageContent/index.cosmos.tsx | 5 +- .../pure/Row/RowSlippageContent/index.tsx | 93 ++------ .../pure/Row/styled.ts | 29 ++- .../state/settingsTabState.ts | 3 + .../containers/TwapConfirmModal/index.tsx | 76 +++---- .../containers/TwapFormWarnings/index.tsx | 35 +-- .../twap/containers/TwapFormWidget/index.tsx | 11 +- .../twap/hooks/useAreWarningsAccepted.ts | 11 +- .../twap/hooks/useTwapWarningsContext.ts | 7 +- .../twap/state/twapOrdersSettingsAtom.ts | 4 +- .../modules/volumeFee/state/volumeFeeAtom.ts | 1 + .../yield/containers/TradeButtons/index.tsx | 33 +++ .../yield/containers/Warnings/index.tsx | 9 + .../containers/YieldConfirmModal/index.tsx | 100 +++++++++ .../yield/containers/YieldWidget/index.tsx | 136 ++++++++++++ .../yield/hooks/useUpdateCurrencyAmount.ts | 22 ++ .../yield/hooks/useUpdateYieldRawState.ts | 7 + .../yield/hooks/useYieldDerivedState.ts | 26 +++ .../modules/yield/hooks/useYieldRawState.ts | 7 + .../modules/yield/hooks/useYieldSettings.ts | 30 +++ .../yield/hooks/useYieldWidgetActions.ts | 47 ++++ .../src/modules/yield/index.ts | 5 + .../modules/yield/state/yieldRawStateAtom.ts | 37 ++++ .../modules/yield/state/yieldSettingsAtom.ts | 19 ++ .../updaters/QuoteObserverUpdater/index.tsx | 40 ++++ .../src/modules/yield/updaters/index.tsx | 23 ++ .../src/pages/AdvancedOrders/index.tsx | 8 +- .../src/pages/Yield/index.tsx | 10 + .../src/utils/orderUtils/getUiOrderType.ts | 1 + .../src/app/configurator/consts.ts | 2 +- .../src/app/embedDialog/const.ts | 2 +- libs/types/src/common.ts | 3 + .../src/containers/InlineBanner/banners.tsx | 52 ----- libs/ui/src/containers/InlineBanner/index.tsx | 4 +- libs/ui/src/pure/InfoTooltip/index.tsx | 5 +- libs/widget-lib/src/types.ts | 1 + 194 files changed, 2911 insertions(+), 2610 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx delete mode 100644 apps/cowswap-frontend/src/legacy/components/Settings/index.tsx delete mode 100644 apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/containers/Row/RowDeadline/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/helpers/getEthFlowEnabled.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.test.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useSetSlippage.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useSwapSlippage.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/Row/types.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/Row/typings.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/pure/styled.tsx delete mode 100644 apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/types/flowContext.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx create mode 100644 apps/cowswap-frontend/src/modules/trade/containers/NoImpactWarning/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/trade/containers/TradeWarnings/index.tsx rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useIsEoaEthFlow.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useIsSafeEthFlow.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useIsSwapEth.ts (53%) rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useNavigateToNewOrderCallback.ts (83%) create mode 100644 apps/cowswap-frontend/src/modules/trade/hooks/useOrderSubmittedContent.tsx rename apps/cowswap-frontend/src/modules/{swap => trade}/hooks/useShouldPayGas.ts (82%) create mode 100644 apps/cowswap-frontend/src/modules/trade/hooks/useUnknownImpactWarning.ts delete mode 100644 apps/cowswap-frontend/src/modules/trade/pure/NoImpactWarning/index.tsx rename apps/cowswap-frontend/src/{common => modules/trade}/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx (100%) rename apps/cowswap-frontend/src/{common => modules/trade}/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx (94%) rename apps/cowswap-frontend/src/{common => modules/trade}/pure/ZeroApprovalWarning/index.tsx (100%) rename apps/cowswap-frontend/src/modules/{swap => trade}/state/isEoaEthFlowAtom.ts (100%) create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowType.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/index.ts rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/safeBundleFlow/index.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/safeBundleFlow/safeBundleApprovalFlow.ts (88%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/safeBundleFlow/safeBundleEthFlow.ts (89%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/swapFlow/README.md (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/swapFlow/index.ts (92%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/swapFlow/steps/presignOrderStep.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeFlow}/services/swapFlow/swapFlow.puml (100%) create mode 100644 apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/hooks/useIsSlippageModified.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/hooks/useIsSmartSlippageApplied.ts (100%) create mode 100644 apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useSetSlippage.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useTradeSlippage.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeSlippage/index.tsx rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/state/slippageValueAndTypeAtom.ts (67%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/index.ts (88%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => tradeSlippage}/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts (100%) create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/consts.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowDeadline/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/styled.tsx rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/containers/TradeRateDetails/index.tsx (72%) rename apps/cowswap-frontend/src/{legacy/components => modules/tradeWidgetAddons/containers}/TransactionSettings/index.tsx (71%) create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/index.ts rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/NetworkCostsTooltipSuffix.tsx (93%) rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/Row/RowDeadline/index.cosmos.tsx (92%) create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.tsx rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/Row/RowSlippageContent/index.cosmos.tsx (74%) rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/Row/RowSlippageContent/index.tsx (55%) rename apps/cowswap-frontend/src/modules/{swap => tradeWidgetAddons}/pure/Row/styled.ts (73%) create mode 100644 apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/Warnings/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/YieldConfirmModal/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useUpdateCurrencyAmount.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useUpdateYieldRawState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldDerivedState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldRawState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/index.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/state/yieldRawStateAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/QuoteObserverUpdater/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/index.tsx create mode 100644 apps/cowswap-frontend/src/pages/Yield/index.tsx diff --git a/apps/cowswap-frontend/src/common/constants/routes.ts b/apps/cowswap-frontend/src/common/constants/routes.ts index bdc140310a..37045e433f 100644 --- a/apps/cowswap-frontend/src/common/constants/routes.ts +++ b/apps/cowswap-frontend/src/common/constants/routes.ts @@ -7,6 +7,7 @@ export const Routes = { SWAP: `/:chainId?${TRADE_WIDGET_PREFIX}/swap/:inputCurrencyId?/:outputCurrencyId?`, HOOKS: `/:chainId?${TRADE_WIDGET_PREFIX}/swap/hooks/:inputCurrencyId?/:outputCurrencyId?`, LIMIT_ORDER: `/:chainId?${TRADE_WIDGET_PREFIX}/limit/:inputCurrencyId?/:outputCurrencyId?`, + YIELD: `/:chainId?${TRADE_WIDGET_PREFIX}/yield/:inputCurrencyId?/:outputCurrencyId?`, ADVANCED_ORDERS: `/:chainId?${TRADE_WIDGET_PREFIX}/advanced/:inputCurrencyId?/:outputCurrencyId?`, LONG_LIMIT_ORDER: `/:chainId?${TRADE_WIDGET_PREFIX}/limit-orders/:inputCurrencyId?/:outputCurrencyId?`, LONG_ADVANCED_ORDERS: `/:chainId?${TRADE_WIDGET_PREFIX}/advanced-orders/:inputCurrencyId?/:outputCurrencyId?`, @@ -55,3 +56,10 @@ export const HOOKS_STORE_MENU_ITEM = { description: 'Powerful tool to generate pre/post interaction for CoW Protocol', badge: 'New', } + +export const YIELD_MENU_ITEM = { + route: Routes.YIELD, + label: 'Yield', + fullLabel: 'Yield', + description: 'Provide liquidity', +} diff --git a/apps/cowswap-frontend/src/common/hooks/useGetMarketDimension.ts b/apps/cowswap-frontend/src/common/hooks/useGetMarketDimension.ts index c964124f4f..35291e9a04 100644 --- a/apps/cowswap-frontend/src/common/hooks/useGetMarketDimension.ts +++ b/apps/cowswap-frontend/src/common/hooks/useGetMarketDimension.ts @@ -9,6 +9,7 @@ const widgetTypeMap: Record = { [TradeType.LIMIT_ORDER]: 'LIMIT', // TODO: set different type for other advanced orders [TradeType.ADVANCED_ORDERS]: 'TWAP', + [TradeType.YIELD]: 'YIELD', } /**\ diff --git a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts index 2826cee38d..bf6a72af20 100644 --- a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts +++ b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts @@ -1,14 +1,25 @@ import { useMemo } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' -import { isLocal } from '@cowprotocol/common-utils' +import { isLocal, isPr } from '@cowprotocol/common-utils' -import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS } from '../constants/routes' +import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS, YIELD_MENU_ITEM } from '../constants/routes' export function useMenuItems() { const { isHooksStoreEnabled } = useFeatureFlags() + const { isYieldEnabled } = useFeatureFlags() return useMemo(() => { - return isHooksStoreEnabled || isLocal ? MENU_ITEMS.concat(HOOKS_STORE_MENU_ITEM) : MENU_ITEMS + const items = [...MENU_ITEMS] + + if (isHooksStoreEnabled || isLocal) { + items.push(HOOKS_STORE_MENU_ITEM) + } + + if (isYieldEnabled || isLocal || isPr) { + items.push(YIELD_MENU_ITEM) + } + + return items }, [isHooksStoreEnabled]) } diff --git a/apps/cowswap-frontend/src/common/pure/TransactionSubmittedContent/index.tsx b/apps/cowswap-frontend/src/common/pure/TransactionSubmittedContent/index.tsx index f8ca1c4d75..d1ab7fe366 100644 --- a/apps/cowswap-frontend/src/common/pure/TransactionSubmittedContent/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/TransactionSubmittedContent/index.tsx @@ -1,4 +1,5 @@ -import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { Command } from '@cowprotocol/types' import { BackButton } from '@cowprotocol/ui' import { Currency } from '@uniswap/sdk-core' @@ -6,12 +7,12 @@ import { Nullish } from 'types' import { DisplayLink } from 'legacy/components/TransactionConfirmationModal/DisplayLink' import { ActivityStatus } from 'legacy/hooks/useRecentActivity' +import type { Order } from 'legacy/state/orders/actions' import { ActivityDerivedState } from 'modules/account/containers/Transaction' import { GnosisSafeTxDetails } from 'modules/account/containers/Transaction/ActivityDetails' import { Category, cowAnalytics } from 'modules/analytics' import { EthFlowStepper } from 'modules/swap/containers/EthFlowStepper' -import { NavigateToNewOrderCallback } from 'modules/swap/hooks/useNavigateToNewOrderCallback' import { WatchAssetInWallet } from 'modules/wallet/containers/WatchAssetInWallet' import * as styledEl from './styled' @@ -46,7 +47,7 @@ export interface TransactionSubmittedContentProps { activityDerivedState: ActivityDerivedState | null currencyToAdd?: Nullish orderProgressBarV2Props: OrderProgressBarV2Props - navigateToNewOrderCallback?: NavigateToNewOrderCallback + navigateToNewOrderCallback?: (chainId: SupportedChainId, order?: Order, callback?: Command) => () => void } export function TransactionSubmittedContent({ diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts index 4eeb7b8030..001225192d 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts @@ -17,8 +17,8 @@ import { Field } from 'legacy/state/types' import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { useAppData } from 'modules/appData' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useDerivedSwapInfo, useSwapState } from 'modules/swap/hooks/useSwapState' +import { useIsEoaEthFlow } from 'modules/trade' import { isRefetchQuoteRequired } from './isRefetchQuoteRequired' import { quoteUsingSameParameters } from './quoteUsingSameParameters' diff --git a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts index bb35f5283b..91e589f3a7 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts @@ -57,7 +57,7 @@ async function _updatePresignGnosisSafeTx( getSafeTxInfo: GetSafeTxInfo, updatePresignGnosisSafeTx: UpdatePresignGnosisSafeTxCallback, cancelOrdersBatch: CancelOrdersBatchCallback, - safeInfo: GnosisSafeInfo | undefined + safeInfo: GnosisSafeInfo | undefined, ) { const getSafeTxPromises = allPendingOrders // Update orders that are pending for presingature @@ -100,7 +100,7 @@ async function _updatePresignGnosisSafeTx( if (!error.isCancelledError) { console.error( `[PendingOrdersUpdater] Failed to check Gnosis Safe tx hash: ${presignGnosisSafeTxHash}`, - error + error, ) } }) @@ -113,7 +113,7 @@ async function _updateCreatingOrders( chainId: ChainId, pendingOrders: Order[], isSafeWallet: boolean, - addOrUpdateOrders: AddOrUpdateOrdersCallback + addOrUpdateOrders: AddOrUpdateOrdersCallback, ): Promise { const promises = pendingOrders.reduce[]>((acc, order) => { if (order.status === OrderStatus.CREATING) { @@ -205,7 +205,7 @@ async function _updateOrders({ // Iterate over pending orders fetching API data const unfilteredOrdersData = await Promise.all( - pending.map(async (orderFromStore) => fetchAndClassifyOrder(orderFromStore, chainId)) + pending.map(async (orderFromStore) => fetchAndClassifyOrder(orderFromStore, chainId)), ) // Group resolved promises by status @@ -219,7 +219,7 @@ async function _updateOrders({ } return acc }, - { fulfilled: [], expired: [], cancelled: [], unknown: [], presigned: [], pending: [], presignaturePending: [] } + { fulfilled: [], expired: [], cancelled: [], unknown: [], presigned: [], pending: [], presignaturePending: [] }, ) if (presigned.length > 0) { @@ -310,7 +310,7 @@ async function _updateOrders({ getSafeTxInfo, updatePresignGnosisSafeTx, cancelOrdersBatch, - safeInfo + safeInfo, ) // Update the creating EthFlow orders (if any) await _updateCreatingOrders(chainId, orders, isSafeWallet, addOrUpdateOrders) @@ -318,7 +318,7 @@ async function _updateOrders({ function getReplacedOrCancelledEthFlowOrders( orders: Order[], - allTransactions: UpdateOrdersParams['allTransactions'] + allTransactions: UpdateOrdersParams['allTransactions'], ): Order[] { return orders.filter((order) => { if (!order.orderCreationHash || order.status !== OrderStatus.CREATING) return false @@ -373,6 +373,7 @@ export function PendingOrdersUpdater(): null { const isUpdatingLimit = useRef(false) const isUpdatingTwap = useRef(false) const isUpdatingHooks = useRef(false) + const isUpdatingYield = useRef(false) const updatersRefMap = useMemo( () => ({ @@ -380,8 +381,9 @@ export function PendingOrdersUpdater(): null { [UiOrderType.LIMIT]: isUpdatingLimit, [UiOrderType.TWAP]: isUpdatingTwap, [UiOrderType.HOOKS]: isUpdatingHooks, + [UiOrderType.YIELD]: isUpdatingYield, }), - [] + [], ) // Ref, so we don't rerun useEffect @@ -411,7 +413,7 @@ export function PendingOrdersUpdater(): null { // Remove orders from the cancelling queue (marked by checkbox in the orders table) removeOrdersToCancel(fulfillOrdersBatchParams.orders.map(({ uid }) => uid)) }, - [chainId, _fulfillOrdersBatch, removeOrdersToCancel] + [chainId, _fulfillOrdersBatch, removeOrdersToCancel], ) const updateOrders = useCallback( @@ -460,7 +462,7 @@ export function PendingOrdersUpdater(): null { getSafeTxInfo, safeInfo, allTransactions, - ] + ], ) useEffect(() => { @@ -470,15 +472,15 @@ export function PendingOrdersUpdater(): null { const marketInterval = setInterval( () => updateOrders(chainId, account, isSafeWallet, UiOrderType.SWAP), - MARKET_OPERATOR_API_POLL_INTERVAL + MARKET_OPERATOR_API_POLL_INTERVAL, ) const limitInterval = setInterval( () => updateOrders(chainId, account, isSafeWallet, UiOrderType.LIMIT), - LIMIT_OPERATOR_API_POLL_INTERVAL + LIMIT_OPERATOR_API_POLL_INTERVAL, ) const twapInterval = setInterval( () => updateOrders(chainId, account, isSafeWallet, UiOrderType.TWAP), - LIMIT_OPERATOR_API_POLL_INTERVAL + LIMIT_OPERATOR_API_POLL_INTERVAL, ) updateOrders(chainId, account, isSafeWallet, UiOrderType.SWAP) diff --git a/apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx b/apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx new file mode 100644 index 0000000000..f48fb7aacb --- /dev/null +++ b/apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx @@ -0,0 +1,61 @@ +import { + INPUT_OUTPUT_EXPLANATION, + MINIMUM_ETH_FLOW_DEADLINE_SECONDS, + MINIMUM_ETH_FLOW_SLIPPAGE, + PERCENTAGE_PRECISION, +} from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +import { Trans } from '@lingui/macro' + +export function getNativeOrderDeadlineTooltip(symbols: (string | undefined)[] | undefined) { + return ( + + {symbols?.[0] || 'Native currency (e.g ETH)'} orders require a minimum transaction expiration time threshold of{' '} + {MINIMUM_ETH_FLOW_DEADLINE_SECONDS / 60} minutes to ensure the best swapping experience. +
    +
    + Orders not matched after the threshold time are automatically refunded. +
    + ) +} + +export function getNonNativeOrderDeadlineTooltip() { + return ( + + Your swap expires and will not execute if it is pending for longer than the selected duration. +
    +
    + {INPUT_OUTPUT_EXPLANATION} +
    + ) +} + +export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (string | undefined)[] | undefined) => ( + + When selling {symbols?.[0] || 'a native currency'}, the minimum slippage tolerance is set to{' '} + {MINIMUM_ETH_FLOW_SLIPPAGE[chainId].toSignificant(PERCENTAGE_PRECISION)}% to ensure a high likelihood of order + matching, even in volatile market conditions. +
    +
    + {symbols?.[0] || 'Native currency'} orders can, in rare cases, be frontrun due to their on-chain component. For more + robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. +
    +) + +export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( + + CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the + best price.{' '} + {isSettingsModal ? ( + <> + To override this, enter your desired slippage amount. +
    +
    + Either way, your slippage is protected from MEV! + + ) : ( + "Trades are protected from MEV, so your slippage can't be exploited!" + )} +
    +) diff --git a/apps/cowswap-frontend/src/legacy/components/Settings/index.tsx b/apps/cowswap-frontend/src/legacy/components/Settings/index.tsx deleted file mode 100644 index 9ac795d417..0000000000 --- a/apps/cowswap-frontend/src/legacy/components/Settings/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useCallback, useRef } from 'react' - -import { useOnClickOutside } from '@cowprotocol/common-hooks' -import { HelpTooltip, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' - -import { Trans } from '@lingui/macro' -import { transparentize } from 'color2k' -import { Text } from 'rebass' -import styled from 'styled-components/macro' -import { ThemedText } from 'theme' - -import { AutoColumn } from 'legacy/components/Column' -import { Toggle } from 'legacy/components/Toggle' -import { TransactionSettings } from 'legacy/components/TransactionSettings' -import { useModalIsOpen, useToggleSettingsMenu } from 'legacy/state/application/hooks' -import { ApplicationModal } from 'legacy/state/application/reducer' -import { useRecipientToggleManager } from 'legacy/state/user/hooks' - -import { toggleRecipientAddressAnalytics } from 'modules/analytics' -import { SettingsIcon } from 'modules/trade/pure/Settings' - -export const StyledMenuButton = styled.button` - position: relative; - width: 100%; - border: none; - background-color: transparent; - margin: 0; - padding: 0; - border-radius: 0.5rem; - height: var(${UI.ICON_SIZE_NORMAL}); - opacity: 0.6; - transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; - color: inherit; - display: flex; - align-items: center; - - &:hover, - &:focus { - opacity: 1; - cursor: pointer; - outline: none; - color: currentColor; - } - - svg { - opacity: 1; - margin: auto; - transition: transform 0.3s cubic-bezier(0.65, 0.05, 0.36, 1); - color: inherit; - } -` - -const StyledMenu = styled.div` - margin: 0; - display: flex; - justify-content: center; - align-items: center; - position: relative; - border: none; - text-align: left; - color: inherit; - - ${RowFixed} { - color: inherit; - - > div { - color: inherit; - opacity: 0.85; - } - } -` - -export const MenuFlyout = styled.span` - min-width: 20.125rem; - background: var(${UI.COLOR_PRIMARY}); - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), - 0px 24px 32px rgba(0, 0, 0, 0.01); - border-radius: 12px; - display: flex; - flex-direction: column; - font-size: 1rem; - position: absolute; - z-index: 100; - color: inherit; - box-shadow: ${({ theme }) => theme.boxShadow2}; - border: 1px solid ${({ theme }) => transparentize(theme.white, 0.95)}; - background-color: var(${UI.COLOR_PAPER}); - color: inherit; - padding: 0; - margin: 0; - top: 36px; - right: 0; - width: 280px; - - ${Media.upToMedium()} { - min-width: 18.125rem; - } - - user-select: none; -` - -interface SettingsTabProps { - className?: string -} - -export function SettingsTab({ className }: SettingsTabProps) { - const node = useRef(null) - const open = useModalIsOpen(ApplicationModal.SETTINGS) - const toggle = useToggleSettingsMenu() - - const [recipientToggleVisible, toggleRecipientVisibilityAux] = useRecipientToggleManager() - const toggleRecipientVisibility = useCallback( - (value?: boolean) => { - const isVisible = value ?? !recipientToggleVisible - toggleRecipientAddressAnalytics(isVisible) - toggleRecipientVisibilityAux(isVisible) - }, - [toggleRecipientVisibilityAux, recipientToggleVisible] - ) - - // show confirmation view before turning on - - useOnClickOutside([node], open ? toggle : undefined) - - return ( - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 - - - - - {open && ( - - - - Transaction Settings - - - - Interface Settings - - - - - - Custom Recipient - - Allows you to choose a destination address for the swap other than the connected one. - } - /> - - - - - - )} - - ) -} diff --git a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx deleted file mode 100644 index 8fdc4411e9..0000000000 --- a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { Command } from '@cowprotocol/types' -import { HoverTooltip } from '@cowprotocol/ui' -import { Fraction } from '@uniswap/sdk-core' - -import { AlertTriangle } from 'react-feather' -import styled from 'styled-components/macro' - -import TradeGp from 'legacy/state/swap/TradeGp' -import { useIsDarkMode } from 'legacy/state/user/hooks' - -import { useHighFeeWarning } from 'modules/swap/hooks/useSwapState' -import { StyledInfoIcon } from 'modules/swap/pure/styled' - -import { useSafeMemo } from 'common/hooks/useSafeMemo' - -import { AuxInformationContainer } from '../swap/styleds' - -interface HighFeeContainerProps { - padding?: string - margin?: string - width?: string - level?: number - isDarkMode?: boolean -} - -const WarningCheckboxContainer = styled.span` - display: flex; - width: 100%; - margin: 0 auto; - font-weight: bold; - gap: 2px; - justify-content: center; - align-items: center; - border-radius: 16px; - padding: 0; - margin: 10px auto; - - > input { - cursor: pointer; - margin: 1px 4px 0 0; - } -` - -const WarningContainer = styled(AuxInformationContainer).attrs((props) => ({ - ...props, - hideInput: true, -}))` - --warningColor: ${({ theme, level }) => - level === HIGH_TIER_FEE - ? theme.danger - : level === MEDIUM_TIER_FEE - ? theme.warning - : LOW_TIER_FEE - ? theme.alert - : theme.info}; - color: inherit; - padding: ${({ padding = '16px' }) => padding}; - width: ${({ width = '100%' }) => width}; - border-radius: 16px; - border: 0; - margin: ${({ margin = '0 auto' }) => margin}; - position: relative; - z-index: 1; - - &:hover { - border: 0; - } - - &::before { - content: ''; - display: block; - position: absolute; - top: 0; - left: 0; - border-radius: inherit; - background: var(--warningColor); - opacity: ${({ isDarkMode }) => (isDarkMode ? 0.2 : 0.15)}; - z-index: -1; - width: 100%; - height: 100%; - pointer-events: none; - } - - > div { - display: flex; - justify-content: center; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 500; - text-align: center; - - > svg:first-child { - stroke: var(--warningColor); - } - } -` - -const ErrorStyledInfoIcon = styled(StyledInfoIcon)` - color: ${({ theme }) => (theme.darkMode ? '#FFCA4A' : '#564D00')}; -` -const HIGH_TIER_FEE = 30 -const MEDIUM_TIER_FEE = 20 -const LOW_TIER_FEE = 10 - -// checks fee as percentage (30% not a decimal) -function _getWarningInfo(feePercentage?: Fraction) { - if (!feePercentage || feePercentage.lessThan(LOW_TIER_FEE)) { - return undefined - } else if (feePercentage.lessThan(MEDIUM_TIER_FEE)) { - return LOW_TIER_FEE - } else if (feePercentage.lessThan(HIGH_TIER_FEE)) { - return MEDIUM_TIER_FEE - } else { - return HIGH_TIER_FEE - } -} - -const HighFeeWarningMessage = ({ feePercentage }: { feePercentage?: Fraction }) => ( -
    - - Current network costs make up{' '} - - {feePercentage?.toFixed(2)}% - {' '} - of your swap amount. -
    -
    - Consider waiting for lower network costs. -
    -
    - You may still move forward with this swap but a high percentage of it will be consumed by network costs. -
    -
    -) - -export type WarningProps = { - trade?: TradeGp - acceptedStatus?: boolean - className?: string - acceptWarningCb?: Command - hide?: boolean -} & HighFeeContainerProps - -export const HighFeeWarning = (props: WarningProps) => { - const { acceptedStatus, acceptWarningCb, trade } = props - const darkMode = useIsDarkMode() - - const { isHighFee, feePercentage } = useHighFeeWarning(trade) - const level = useSafeMemo(() => _getWarningInfo(feePercentage), [feePercentage]) - - if (!isHighFee) return null - - return ( - -
    - - Costs exceed {level}% of the swap amount!{' '} - }> - - {' '} -
    - - {acceptWarningCb && ( - - Swap - anyway - - )} -
    - ) -} - -export type HighSuggestedSlippageWarningProps = { - isSuggestedSlippage: boolean | undefined - slippageBps: number | undefined - className?: string -} & HighFeeContainerProps - -export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarningProps) { - const { isSuggestedSlippage, slippageBps, ...rest } = props - - if (!isSuggestedSlippage || !slippageBps || slippageBps <= 200) { - return null - } - - return ( - -
    - - Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution - - - -
    -
    - ) -} diff --git a/apps/cowswap-frontend/src/legacy/components/swap/styleds.tsx b/apps/cowswap-frontend/src/legacy/components/swap/styleds.tsx index 9dbb80ca71..7a1ae3d4be 100644 --- a/apps/cowswap-frontend/src/legacy/components/swap/styleds.tsx +++ b/apps/cowswap-frontend/src/legacy/components/swap/styleds.tsx @@ -1,109 +1,11 @@ -import { Media, UI } from '@cowprotocol/ui' - import styled from 'styled-components/macro' -const FeeInformationTooltipWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - height: 60px; -` - export const Container = styled.div` max-width: 460px; width: 100%; ` + export const Wrapper = styled.div` position: relative; padding: 8px; ` - -// TODO: refactor these styles -export const AuxInformationContainer = styled.div<{ - margin?: string - borderColor?: string - borderWidth?: string - hideInput: boolean - disabled?: boolean - showAux?: boolean -}>` - border: 1px solid ${({ hideInput }) => (hideInput ? ' transparent' : `var(${UI.COLOR_PAPER_DARKER})`)}; - background-color: var(${UI.COLOR_PAPER}); - width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; - - :focus, - :hover { - border: 1px solid ${({ theme, hideInput }) => (hideInput ? ' transparent' : theme.background)}; - } - - ${({ theme, hideInput, disabled }) => - !disabled && - ` - :focus, - :hover { - border: 1px solid ${hideInput ? ' transparent' : theme.background}; - } - `} - - margin: ${({ margin = '0 auto' }) => margin}; - border-radius: 0 0 15px 15px; - border: 2px solid var(${UI.COLOR_PAPER_DARKER}); - - &:hover { - border: 2px solid var(${UI.COLOR_PAPER_DARKER}); - } - - ${Media.upToSmall()} { - height: auto; - flex-flow: column wrap; - justify-content: flex-end; - align-items: flex-end; - } - > ${FeeInformationTooltipWrapper} { - align-items: center; - justify-content: space-between; - margin: 0 16px; - padding: 16px 0; - font-weight: 600; - font-size: 14px; - height: auto; - - ${Media.upToSmall()} { - flex-flow: column wrap; - width: 100%; - align-items: flex-start; - margin: 0; - padding: 16px; - } - - > span { - font-size: 18px; - gap: 2px; - word-break: break-all; - text-align: right; - - ${Media.upToSmall()} { - text-align: left; - align-items: flex-start; - width: 100%; - } - } - - > span:first-child { - font-size: 14px; - display: flex; - align-items: center; - white-space: nowrap; - - ${Media.upToSmall()} { - margin: 0 0 10px; - } - } - - > span > small { - opacity: 0.75; - font-size: 13px; - font-weight: 500; - } - } -` diff --git a/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/types.ts b/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/types.ts index 8c8ae12483..5680fe9f06 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/types.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/types.ts @@ -4,17 +4,3 @@ export type ParsedAmounts = { INPUT: CurrencyAmount | undefined OUTPUT: CurrencyAmount | undefined } - -export interface PriceImpactTrade { - inputAmount: CurrencyAmount | null - outputAmount: CurrencyAmount | null - inputAmountWithoutFee?: CurrencyAmount - outputAmountWithoutFee?: CurrencyAmount -} - -export interface FallbackPriceImpactParams { - abTrade?: PriceImpactTrade - isWrapping: boolean - sellToken: string | null - buyToken: string | null -} diff --git a/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx b/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx index af1d89d539..9c0144dfd7 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx +++ b/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx @@ -19,7 +19,7 @@ import { LegacyFeeQuoteParams, LegacyQuoteParams } from 'legacy/state/price/type import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { getBestQuote, getFastQuote, QuoteResult } from 'legacy/utils/price' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' +import { useIsEoaEthFlow } from 'modules/trade' import { ApiErrorCodes, isValidOperatorError } from 'api/cowProtocol/errors/OperatorError' import QuoteApiError, { @@ -183,8 +183,8 @@ export function useRefetchQuoteCallback() { const previouslyUnsupportedToken = getIsUnsupportedToken(sellToken) ? sellToken : getIsUnsupportedToken(buyToken) - ? buyToken - : null + ? buyToken + : null // can be a previously unsupported token which is now valid // so we check against map and remove it if (previouslyUnsupportedToken) { @@ -263,6 +263,6 @@ export function useRefetchQuoteCallback() { removeGpUnsupportedToken, addUnsupportedToken, setQuoteError, - ] + ], ) } diff --git a/apps/cowswap-frontend/src/legacy/state/application/hooks.ts b/apps/cowswap-frontend/src/legacy/state/application/hooks.ts index 67b8e196a9..ca72c2b086 100644 --- a/apps/cowswap-frontend/src/legacy/state/application/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/application/hooks.ts @@ -31,10 +31,6 @@ export function useToggleWalletModal(): Command { return useToggleModal(ApplicationModal.WALLET) } -export function useToggleSettingsMenu(): Command { - return useToggleModal(ApplicationModal.SETTINGS) -} - // TODO: These two seem to be gone from original. Check whether they have been replaced export function useOpenModal(modal: ApplicationModal): Command { const dispatch = useAppDispatch() diff --git a/apps/cowswap-frontend/src/legacy/state/application/reducer.ts b/apps/cowswap-frontend/src/legacy/state/application/reducer.ts index 6eabdf1cad..18840cb75f 100644 --- a/apps/cowswap-frontend/src/legacy/state/application/reducer.ts +++ b/apps/cowswap-frontend/src/legacy/state/application/reducer.ts @@ -4,7 +4,6 @@ import { initialState } from './initialState' export enum ApplicationModal { NETWORK_SELECTOR, - SETTINGS, WALLET, // ----------------- MOD: CowSwap specific modals -------------------- TRANSACTION_ERROR, @@ -16,7 +15,7 @@ export enum ApplicationModal { } export interface ApplicationState { - readonly openModal: ApplicationModal | null + openModal: ApplicationModal | null } const applicationSlice = createSlice({ @@ -29,5 +28,4 @@ const applicationSlice = createSlice({ }, }) -export const { setOpenModal } = applicationSlice.actions export default applicationSlice.reducer diff --git a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx index f347ddb113..c24357a2fb 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx +++ b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx @@ -55,13 +55,11 @@ export function useUserLocaleManager(): [SupportedLocale | null, (newLocale: Sup return [locale, setLocale] } -// TODO: mod, move to mod file export function useIsRecipientToggleVisible(): boolean { return useAppSelector((state) => state.user.recipientToggleVisible) } -// TODO: mod, move to mod file -export function useRecipientToggleManager(): [boolean, (value?: boolean) => void] { +export function useRecipientToggleManager(): [boolean, (value: boolean) => void] { const dispatch = useAppDispatch() const isVisible = useIsRecipientToggleVisible() const onChangeRecipient = useCallback( @@ -72,14 +70,14 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void ) const toggleVisibility = useCallback( - (value?: boolean) => { - const newIsVisible = value ?? !isVisible + (value: boolean) => { + const newIsVisible = value dispatch(updateRecipientToggleVisible({ recipientToggleVisible: newIsVisible })) if (!newIsVisible) { onChangeRecipient(null) } }, - [isVisible, dispatch, onChangeRecipient], + [dispatch, onChangeRecipient], ) return [isVisible, toggleVisibility] diff --git a/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx index 83b4bb4d89..19f1c6c246 100644 --- a/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx @@ -1,5 +1,5 @@ import { useAtomValue } from 'jotai' -import { PropsWithChildren, ReactNode } from 'react' +import { ReactNode } from 'react' import { isSellOrder } from '@cowprotocol/common-utils' @@ -38,12 +38,13 @@ export type AdvancedOrdersWidgetParams = { disablePriceImpact: boolean } -export type AdvancedOrdersWidgetProps = PropsWithChildren<{ +export type AdvancedOrdersWidgetProps = { updaters?: ReactNode params: AdvancedOrdersWidgetParams mapCurrencyInfo?: (info: CurrencyInfo) => CurrencyInfo confirmContent: JSX.Element -}> + children(warnings: ReactNode): ReactNode +} export function AdvancedOrdersWidget({ children, @@ -98,7 +99,9 @@ export function AdvancedOrdersWidget({ const slots: TradeWidgetSlots = { settingsWidget: , - bottomContent: children, + bottomContent(warnings) { + return children(warnings) + }, updaters, lockScreen: isUnlocked ? undefined : ( = { [UiOrderType.SWAP]: 'Market Order', [UiOrderType.TWAP]: 'TWAP Order', [UiOrderType.HOOKS]: 'Hooks', + [UiOrderType.YIELD]: 'Yield', } function getClassLabel(orderClass: UiOrderType, label?: string) { diff --git a/apps/cowswap-frontend/src/modules/appData/updater/AppDataInfoUpdater.ts b/apps/cowswap-frontend/src/modules/appData/updater/AppDataInfoUpdater.ts index eb91c3cc43..83f01614cc 100644 --- a/apps/cowswap-frontend/src/modules/appData/updater/AppDataInfoUpdater.ts +++ b/apps/cowswap-frontend/src/modules/appData/updater/AppDataInfoUpdater.ts @@ -88,6 +88,7 @@ export function AppDataInfoUpdater({ typedHooks, volumeFee, replacedOrderUid, + isSmartSlippage, ]) } diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx index 327fe74254..7469615b85 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx @@ -18,13 +18,14 @@ import { RedirectPathToSwapOnly, RedirectToPath } from 'legacy/pages/Swap/redire import { Routes as RoutesEnum, RoutesValues } from 'common/constants/routes' import Account, { AccountOverview } from 'pages/Account' +import AdvancedOrdersPage from 'pages/AdvancedOrders' import AnySwapAffectedUsers from 'pages/error/AnySwapAffectedUsers' import { HooksPage } from 'pages/Hooks' +import LimitOrderPage from 'pages/LimitOrders' import { SwapPage } from 'pages/Swap' +import YieldPage from 'pages/Yield' // Async routes -const LimitOrders = lazy(() => import(/* webpackChunkName: "limit_orders" */ 'pages/LimitOrders')) -const AdvancedOrders = lazy(() => import(/* webpackChunkName: "advanced_orders" */ 'pages/AdvancedOrders')) const NotFound = lazy(() => import(/* webpackChunkName: "not_found" */ 'pages/error/NotFound')) const CowRunner = lazy(() => import(/* webpackChunkName: "cow_runner" */ 'pages/games/CowRunner')) const MevSlicer = lazy(() => import(/* webpackChunkName: "mev_slicer" */ 'pages/games/MevSlicer')) @@ -51,9 +52,10 @@ function LazyRoute({ route, element, key }: LazyRouteProps) { } const lazyRoutes: LazyRouteProps[] = [ - { route: RoutesEnum.LIMIT_ORDER, element: }, + { route: RoutesEnum.LIMIT_ORDER, element: }, + { route: RoutesEnum.YIELD, element: }, { route: RoutesEnum.LONG_LIMIT_ORDER, element: }, - { route: RoutesEnum.ADVANCED_ORDERS, element: }, + { route: RoutesEnum.ADVANCED_ORDERS, element: }, { route: RoutesEnum.LONG_ADVANCED_ORDERS, element: }, { route: RoutesEnum.ABOUT, element: }, { route: RoutesEnum.FAQ, element: }, diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index f8eb2cf33e..f678cefe26 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -78,6 +78,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho inputCurrencyId, outputCurrencyId, isDarkMode, + orderParams, ]) const dappProps = useMemo(() => ({ context, dapp, isPreHook }), [context, dapp, isPreHook]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts index b3e113a014..d5bafe2f30 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts @@ -96,7 +96,7 @@ export function useRescueFundsFromProxy( } finally { setTxSigningInProgress(false) } - }, [provider, proxyAddress, cowShedContract, selectedTokenAddress, account, tokenBalance]) + }, [provider, proxyAddress, cowShedContract, selectedTokenAddress, account, tokenBalance, cowShedHooks]) return { callback, isTxSigningInProgress, proxyAddress } } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx index 800da41ee1..ef3a5adb4a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx @@ -52,7 +52,7 @@ export function TenderlySimulate({ hook }: TenderlySimulateProps) { } finally { setIsLoading(false) } - }, [simulate, hook, hookId]) + }, [simulate, hook, hookId, setSimulationError]) if (isLoading) { return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx index ffeed661bd..55a7afffe2 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx @@ -74,7 +74,7 @@ export function BuildHookApp({ context }: HookDappProps) { hook, }) : context.addHook({ hook }) - }, [hook, context, hookToEdit, isPreHook]) + }, [hook, context, hookToEdit]) return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx index 6c9be054d0..e5d24ee68f 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx @@ -38,7 +38,7 @@ export function ClaimGnoHookApp({ context }: HookDappProps) { } return SbcDepositContractInterface.encodeFunctionData('claimWithdrawal', [account]) - }, [context]) + }, [context, account]) useEffect(() => { if (!account || !provider) { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx index a619b317c6..2f29f33527 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx @@ -42,7 +42,7 @@ export function PermitHookApp({ context }: HookDappProps) { } context.addHook({ hook }) - }, [generatePermitHook, context, permitInfo, token, spenderAddress]) + }, [generatePermitHook, context, permitInfo, token, spenderAddress, hookToEdit]) const buttonProps = useMemo(() => { if (!context.account) return { message: 'Connect wallet', disabled: true } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts index b0e012b116..a3bf438e4b 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts @@ -33,6 +33,6 @@ export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { } }) }, - [updateHooks, dapp], + [updateHooks, dapp, isPreHook], ) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts index 477f8cb68e..531411e864 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveHook.ts @@ -25,6 +25,6 @@ export function useRemoveHook(isPreHook: boolean): RemoveHook { } }) }, - [updateHooks], + [updateHooks, isPreHook], ) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts index 63fb6bbe4e..2df029bc11 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts @@ -19,5 +19,5 @@ export function useSetRecipientOverride() { if (!hookRecipientOverride || !isHooksTradeType) return onChangeRecipient(hookRecipientOverride) - }, [hookRecipientOverride, isHooksTradeType, isNativeIn]) + }, [hookRecipientOverride, isHooksTradeType, isNativeIn, onChangeRecipient]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts index 7f531ad420..d77e7e1907 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts @@ -2,24 +2,26 @@ import { useEffect } from 'react' import { getCurrencyAddress } from '@cowprotocol/common-utils' -import { useSwapFlowContext } from 'modules/swap/hooks/useSwapFlowContext' - import { useSetOrderParams } from './useSetOrderParams' +import { useSwapFlowContext } from '../../swap/hooks/useSwapFlowContext' + export function useSetupHooksStoreOrderParams() { - const swapFlowContext = useSwapFlowContext() + const tradeFlowContext = useSwapFlowContext() const setOrderParams = useSetOrderParams() - const orderParams = swapFlowContext?.orderParams + const orderParams = tradeFlowContext?.orderParams useEffect(() => { - if (!orderParams) return - - setOrderParams({ - validTo: orderParams.validTo, - sellAmount: orderParams.inputAmount.quotient.toString(), - buyAmount: orderParams.outputAmount.quotient.toString(), - sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency), - buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency), - }) - }, [orderParams]) + if (!orderParams) { + setOrderParams(null) + } else { + setOrderParams({ + validTo: orderParams.validTo, + sellAmount: orderParams.inputAmount.quotient.toString(), + buyAmount: orderParams.outputAmount.quotient.toString(), + sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency), + buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency), + }) + } + }, [orderParams, setOrderParams]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts index e3d41b223d..64505270cb 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts @@ -20,7 +20,7 @@ export function useTenderlySimulate(): (params: CowHook) => Promise { isRequestRelevant = false } - }, [input, chainId]) + }, [input, isSmartContractWallet, chainId]) return null } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx b/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx index 6a5d950898..82e97b64c4 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx @@ -3,7 +3,6 @@ import { useAtomValue } from 'jotai/index' import { useCallback, useEffect, useMemo } from 'react' import { HookDappBase, HookDappType } from '@cowprotocol/hook-dapp-lib' -import { useWalletInfo } from '@cowprotocol/wallet' import ms from 'ms.macro' @@ -21,7 +20,6 @@ const getLastUpdateTimestamp = () => { export function IframeDappsManifestUpdater() { const hooksState = useAtomValue(customHookDappsAtom) const upsertCustomHookDapp = useSetAtom(upsertCustomHookDappAtom) - const { chainId } = useWalletInfo() const [preHooks, postHooks] = useMemo( () => [Object.values(hooksState.pre), Object.values(hooksState.post)], @@ -55,7 +53,7 @@ export function IframeDappsManifestUpdater() { } }) }, - [chainId, upsertCustomHookDapp], + [upsertCustomHookDapp], ) /** diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx index 8f3c936e00..ec34c608a1 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx @@ -2,14 +2,12 @@ import { useAtomValue, useSetAtom } from 'jotai' import React, { useCallback, useEffect } from 'react' import { isFractionFalsy } from '@cowprotocol/common-utils' -import { BundleTxApprovalBanner, BundleTxSafeWcBanner, SmallVolumeWarningBanner } from '@cowprotocol/ui' -import { useIsSafeViaWc, useWalletInfo } from '@cowprotocol/wallet' +import { SmallVolumeWarningBanner } from '@cowprotocol/ui' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import styled from 'styled-components/macro' import { Nullish } from 'types' -import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' import { useLimitOrdersFormState } from 'modules/limitOrders/hooks/useLimitOrdersFormState' import { useRateImpact } from 'modules/limitOrders/hooks/useRateImpact' @@ -17,16 +15,12 @@ import { limitOrdersWarningsAtom, updateLimitOrdersWarningsAtom, } from 'modules/limitOrders/state/limitOrdersWarningsAtom' -import { useTradePriceImpact } from 'modules/trade' import { SellNativeWarningBanner } from 'modules/trade/containers/SellNativeWarningBanner' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { useGetTradeFormValidation } from 'modules/tradeFormValidation' import { TradeFormValidation } from 'modules/tradeFormValidation/types' import { useTradeQuote } from 'modules/tradeQuote' -import { useShouldZeroApprove } from 'modules/zeroApproval' import { HIGH_FEE_WARNING_PERCENTAGE } from 'common/constants/common' -import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' import { calculatePercentageInRelationToReference } from 'utils/orderUtils/calculatePercentageInRelationToReference' import { RateImpactWarning } from '../../pure/RateImpactWarning' @@ -45,9 +39,6 @@ const Wrapper = styled.div` gap: 10px; ` -const StyledNoImpactWarning = styled(NoImpactWarning)` - margin: 10px auto 0; -` const StyledRateImpactWarning = styled(RateImpactWarning)` margin: 10px auto 0; ` @@ -55,23 +46,18 @@ const StyledRateImpactWarning = styled(RateImpactWarning)` export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { const { feeAmount, isConfirmScreen = false, className } = props - const { isPriceImpactAccepted, isRateImpactAccepted } = useAtomValue(limitOrdersWarningsAtom) + const { isRateImpactAccepted } = useAtomValue(limitOrdersWarningsAtom) const updateLimitOrdersWarnings = useSetAtom(updateLimitOrdersWarningsAtom) const localFormValidation = useLimitOrdersFormState() const primaryFormValidation = useGetTradeFormValidation() const rateImpact = useRateImpact() - const { account } = useWalletInfo() - const { slippageAdjustedSellAmount, inputCurrency, inputCurrencyAmount, outputCurrency, outputCurrencyAmount } = - useLimitOrdersDerivedState() + const { inputCurrency, inputCurrencyAmount, outputCurrencyAmount } = useLimitOrdersDerivedState() const tradeQuote = useTradeQuote() - const priceImpactParams = useTradePriceImpact() - const { banners: widgetBanners } = useInjectedWidgetParams() const isBundling = primaryFormValidation && FORM_STATES_TO_SHOW_BUNDLE_BANNER.includes(primaryFormValidation) const canTrade = localFormValidation === null && (primaryFormValidation === null || isBundling) && !tradeQuote.error - const showPriceImpactWarning = canTrade && !!account && !priceImpactParams.loading && !priceImpactParams.priceImpact const showRateImpactWarning = canTrade && inputCurrency && !isFractionFalsy(inputCurrencyAmount) && !isFractionFalsy(outputCurrencyAmount) @@ -80,34 +66,10 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { const showHighFeeWarning = feePercentage?.greaterThan(HIGH_FEE_WARNING_PERCENTAGE) - const showApprovalBundlingBanner = !isConfirmScreen && isBundling - const shouldZeroApprove = useShouldZeroApprove(slippageAdjustedSellAmount) - const showZeroApprovalWarning = shouldZeroApprove && outputCurrency !== null // Show warning only when output currency is also present. - - const isSafeViaWc = useIsSafeViaWc() - const showSafeWcBundlingBanner = - !isConfirmScreen && - !showApprovalBundlingBanner && - isSafeViaWc && - primaryFormValidation === TradeFormValidation.ApproveRequired && - !widgetBanners?.hideSafeWebAppBanner - // TODO: implement Safe App EthFlow bundling for LIMIT and disable the warning in that case const showNativeSellWarning = primaryFormValidation === TradeFormValidation.SellNativeToken - const isVisible = - showPriceImpactWarning || - rateImpact < 0 || - showHighFeeWarning || - showApprovalBundlingBanner || - showSafeWcBundlingBanner || - shouldZeroApprove || - showNativeSellWarning - - // Reset price impact flag when there is no price impact - useEffect(() => { - updateLimitOrdersWarnings({ isPriceImpactAccepted: !showPriceImpactWarning }) - }, [showPriceImpactWarning, updateLimitOrdersWarnings]) + const isVisible = rateImpact < 0 || showHighFeeWarning || showNativeSellWarning // Reset rate impact before opening confirmation screen useEffect(() => { @@ -115,9 +77,6 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { updateLimitOrdersWarnings({ isRateImpactAccepted: false }) } }, [updateLimitOrdersWarnings, isConfirmScreen]) - const onAcceptPriceImpact = useCallback(() => { - updateLimitOrdersWarnings({ isPriceImpactAccepted: !isPriceImpactAccepted }) - }, [updateLimitOrdersWarnings, isPriceImpactAccepted]) const onAcceptRateImpact = useCallback( (value: boolean) => { @@ -128,14 +87,6 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { return isVisible ? ( - {showZeroApprovalWarning && } - {showPriceImpactWarning && ( - - )} {showRateImpactWarning && ( */} {showHighFeeWarning && } - {showApprovalBundlingBanner && } - {showSafeWcBundlingBanner && } {showNativeSellWarning && } ) : null diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index ceb55b05ff..a4a8ddf4b0 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -8,7 +8,7 @@ import { Field } from 'legacy/state/types' import { LimitOrdersWarnings } from 'modules/limitOrders/containers/LimitOrdersWarnings' import { useLimitOrdersWidgetActions } from 'modules/limitOrders/containers/LimitOrdersWidget/hooks/useLimitOrdersWidgetActions' import { TradeButtons } from 'modules/limitOrders/containers/TradeButtons' -import { TradeWidget, useTradePriceImpact } from 'modules/trade' +import { TradeWidget, TradeWidgetSlots, useTradePriceImpact } from 'modules/trade' import { useTradeConfirmState } from 'modules/trade' import { BulletListItem, UnlockWidgetScreen } from 'modules/trade/pure/UnlockWidgetScreen' import { useSetTradeQuoteParams, useTradeQuote } from 'modules/tradeQuote' @@ -79,7 +79,7 @@ export function LimitOrdersWidget() { const priceImpact = useTradePriceImpact() const quoteAmount = useMemo( () => (isSell ? inputCurrencyAmount : outputCurrencyAmount), - [isSell, inputCurrencyAmount, outputCurrencyAmount] + [isSell, inputCurrencyAmount, outputCurrencyAmount], ) useSetTradeQuoteParams(quoteAmount) @@ -136,15 +136,6 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { feeAmount, } = props - const inputCurrency = inputCurrencyInfo.currency - const outputCurrency = outputCurrencyInfo.currency - - const isTradePriceUpdating = useMemo(() => { - if (!inputCurrency || !outputCurrency) return false - - return isRateLoading - }, [isRateLoading, inputCurrency, outputCurrency]) - const tradeContext = useTradeFlowContext() const updateLimitOrdersState = useUpdateLimitOrdersRawState() const localFormValidation = useLimitOrdersFormState() @@ -164,7 +155,7 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { label: outputCurrencyInfo.label, } - const slots = { + const slots: TradeWidgetSlots = { settingsWidget: , lockScreen: isUnlocked ? undefined : ( { ), - bottomContent: ( - <> - - - - - - - - - - - ), + bottomContent(warnings) { + return ( + <> + + + + + + {warnings} + + + + + + ) + }, outerContent: <>{isUnlocked && }, } @@ -204,10 +198,11 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { compactView: false, recipient, showRecipient, - isTradePriceUpdating, + isTradePriceUpdating: isRateLoading, priceImpact, disablePriceImpact: localFormValidation === LimitOrdersFormState.FeeExceedsFrom, disableQuotePolling: isConfirmOpen, + hideTradeWarnings: !!localFormValidation, } return ( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useLimitOrdersWarningsAccepted.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useLimitOrdersWarningsAccepted.ts index 70b7df391e..9270330b3e 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useLimitOrdersWarningsAccepted.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useLimitOrdersWarningsAccepted.ts @@ -1,16 +1,15 @@ import { useAtomValue } from 'jotai' -import { useMemo } from 'react' import { limitOrdersWarningsAtom } from 'modules/limitOrders/state/limitOrdersWarningsAtom' +import { useIsNoImpactWarningAccepted } from 'modules/trade' export function useLimitOrdersWarningsAccepted(isConfirmScreen: boolean): boolean { - const { isPriceImpactAccepted, isRateImpactAccepted } = useAtomValue(limitOrdersWarningsAtom) + const { isRateImpactAccepted } = useAtomValue(limitOrdersWarningsAtom) + const isPriceImpactAccepted = useIsNoImpactWarningAccepted() - return useMemo(() => { - if (isConfirmScreen) { - return isRateImpactAccepted - } else { - return isPriceImpactAccepted - } - }, [isConfirmScreen, isPriceImpactAccepted, isRateImpactAccepted]) + if (isConfirmScreen) { + return isRateImpactAccepted + } else { + return isPriceImpactAccepted + } } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts index 4ab16a5a0a..5e956da0d8 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts @@ -14,11 +14,11 @@ import { calculateLimitOrdersDeadline } from 'modules/limitOrders/utils/calculat import { emitPostedOrderEvent } from 'modules/orders' import { handlePermit } from 'modules/permit' import { callDataContainsPermitSigner } from 'modules/permit' -import { presignOrderStep } from 'modules/swap/services/swapFlow/steps/presignOrderStep' import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' import { logTradeFlow } from 'modules/trade/utils/logger' import { getSwapErrorMessage } from 'modules/trade/utils/swapErrorHelper' import { TradeFlowAnalyticsContext, tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics' +import { presignOrderStep } from 'modules/tradeFlow/services/swapFlow/steps/presignOrderStep' export async function tradeFlow( params: TradeFlowContext, diff --git a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersWarningsAtom.ts b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersWarningsAtom.ts index 61a4ca8fff..51295d045e 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersWarningsAtom.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersWarningsAtom.ts @@ -2,12 +2,10 @@ import { atom } from 'jotai' interface LimitOrdersWarnings { isRateImpactAccepted: boolean - isPriceImpactAccepted: boolean } export const limitOrdersWarningsAtom = atom({ isRateImpactAccepted: false, - isPriceImpactAccepted: false, }) export const updateLimitOrdersWarningsAtom = atom(null, (get, set, nextState: Partial) => { diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/OrderTypeField.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/OrderTypeField.tsx index 48b9f0abad..d7256f6ffa 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/OrderTypeField.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/OrderTypeField.tsx @@ -14,6 +14,7 @@ const ORDER_UI_TYPE_LABELS: Record = { [UiOrderType.LIMIT]: 'Limit', [UiOrderType.TWAP]: 'TWAP', [UiOrderType.HOOKS]: 'Hooks', + [UiOrderType.YIELD]: 'Yield', } export function OrderTypeField({ order }: Props) { diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts index b837474df6..3cceff4ffd 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts @@ -23,6 +23,7 @@ const ORDER_TYPE_SUPPORTS_PERMIT: Record = { [TradeType.SWAP]: true, [TradeType.LIMIT_ORDER]: true, [TradeType.ADVANCED_ORDERS]: false, + [TradeType.YIELD]: true, } const UNSUPPORTED: PermitInfo = { type: 'unsupported', name: 'native' } diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index 9b58d26442..0403137082 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -1,44 +1,37 @@ -import { useCallback, useMemo, useState } from 'react' +import { useMemo } from 'react' import { getMinimumReceivedTooltip } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { Command } from '@cowprotocol/types' import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { Percent, TradeType } from '@uniswap/sdk-core' -import { HighFeeWarning } from 'legacy/components/SwapWarnings' import { PriceImpact } from 'legacy/hooks/usePriceImpact' -import { useOrder } from 'legacy/state/orders/hooks' import TradeGp from 'legacy/state/swap/TradeGp' +import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { useInjectedWidgetParams } from 'modules/injectedWidget' -import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' +import { useAppData } from 'modules/appData' import { TradeConfirmation, TradeConfirmModal, + useIsEoaEthFlow, + useOrderSubmittedContent, useReceiveAmountInfo, + useShouldPayGas, useTradeConfirmActions, - useTradeConfirmState, } from 'modules/trade' import { TradeBasicConfirmDetails } from 'modules/trade/containers/TradeBasicConfirmDetails' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' +import { useIsSmartSlippageApplied } from 'modules/tradeSlippage' +import { HighFeeWarning } from 'modules/tradeWidgetAddons' +import { NetworkCostsTooltipSuffix, RowDeadline } from 'modules/tradeWidgetAddons' -import { useOrderProgressBarV2Props } from 'common/hooks/orderProgressBarV2' import { CurrencyPreviewInfo } from 'common/pure/CurrencyAmountPreview' import { NetworkCostsSuffix } from 'common/pure/NetworkCostsSuffix' import { RateInfoParams } from 'common/pure/RateInfo' -import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent' +import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' import useNativeCurrency from 'lib/hooks/useNativeCurrency' -import { useBaseFlowContextSource } from '../../hooks/useFlowContext' -import { useIsEoaEthFlow } from '../../hooks/useIsEoaEthFlow' -import { useNavigateToNewOrderCallback } from '../../hooks/useNavigateToNewOrderCallback' -import { useShouldPayGas } from '../../hooks/useShouldPayGas' import { useSwapConfirmButtonText } from '../../hooks/useSwapConfirmButtonText' import { useSwapState } from '../../hooks/useSwapState' -import { NetworkCostsTooltipSuffix } from '../../pure/NetworkCostsTooltipSuffix' -import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from '../../pure/Row/RowSlippageContent' -import { RowDeadline } from '../Row/RowDeadline' const CONFIRM_TITLE = 'Swap' @@ -73,13 +66,11 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const { recipient } = useSwapState() const tradeConfirmActions = useTradeConfirmActions() const receiveAmountInfo = useReceiveAmountInfo() - const widgetParams = useInjectedWidgetParams() const shouldPayGas = useShouldPayGas() const isEoaEthFlow = useIsEoaEthFlow() const nativeCurrency = useNativeCurrency() - const baseFlowContextSource = useBaseFlowContextSource() - - const isInvertedState = useState(false) + const appData = useAppData() + const [userDeadline] = useUserTransactionTTL() const slippageAdjustedSellAmount = trade?.maximumAmountIn(allowedSlippage) const isExactIn = trade?.tradeType === TradeType.EXACT_INPUT @@ -103,10 +94,10 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { networkCostsSuffix: shouldPayGas ? : null, networkCostsTooltipSuffix: , }), - [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas], + [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas, isSmartSlippageApplied], ) - const submittedContent = useSubmittedContent(chainId) + const submittedContent = useOrderSubmittedContent(chainId) return ( @@ -123,17 +114,15 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { priceImpact={priceImpact} buttonText={buttonText} recipient={recipient} - appData={baseFlowContextSource?.appData || undefined} + appData={appData || undefined} > {(restContent) => ( <> {receiveAmountInfo && ( - + )} {restContent} - - {!priceImpact.priceImpact && } + )} ) } - -function useSubmittedContent(chainId: SupportedChainId) { - const { transactionHash } = useTradeConfirmState() - const order = useOrder({ chainId, id: transactionHash || undefined }) - - const orderProgressBarV2Props = useOrderProgressBarV2Props(chainId, order) - - const navigateToNewOrderCallback = useNavigateToNewOrderCallback() - - return useCallback( - (onDismiss: Command) => ( - - ), - [chainId, transactionHash, orderProgressBarV2Props, navigateToNewOrderCallback], - ) -} diff --git a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowDeadline/index.tsx deleted file mode 100644 index dc1025aeb8..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowDeadline/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useMemo } from 'react' - -import { useToggleSettingsMenu } from 'legacy/state/application/hooks' -import { useUserTransactionTTL } from 'legacy/state/user/hooks' - -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' -import { RowDeadlineContent } from 'modules/swap/pure/Row/RowDeadline' -import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' - -import useNativeCurrency from 'lib/hooks/useNativeCurrency' - -export function RowDeadline() { - const [userDeadline] = useUserTransactionTTL() - const toggleSettings = useToggleSettingsMenu() - const isEoaEthFlow = useIsEoaEthFlow() - const nativeCurrency = useNativeCurrency() - const isWrapOrUnwrap = useIsWrapOrUnwrap() - - const props = useMemo(() => { - const displayDeadline = Math.floor(userDeadline / 60) + ' minutes' - return { - userDeadline, - symbols: [nativeCurrency.symbol], - displayDeadline, - isEoaEthFlow, - isWrapOrUnwrap, - toggleSettings, - showSettingOnClick: true, - } - }, [isEoaEthFlow, isWrapOrUnwrap, nativeCurrency.symbol, toggleSettings, userDeadline]) - - if (!isEoaEthFlow || isWrapOrUnwrap) { - return null - } - - return -} diff --git a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx deleted file mode 100644 index 812b97be9e..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useMemo } from 'react' - -import { formatPercent } from '@cowprotocol/common-utils' -import { useWalletInfo } from '@cowprotocol/wallet' -import { Percent } from '@uniswap/sdk-core' - -import { useToggleSettingsMenu } from 'legacy/state/application/hooks' - -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' -import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' -import { useSetSlippage } from 'modules/swap/hooks/useSetSlippage' -import { useSmartSwapSlippage } from 'modules/swap/hooks/useSwapSlippage' -import { useTradePricesUpdate } from 'modules/swap/hooks/useTradePricesUpdate' -import { RowSlippageContent } from 'modules/swap/pure/Row/RowSlippageContent' - -import useNativeCurrency from 'lib/hooks/useNativeCurrency' - -export interface RowSlippageProps { - allowedSlippage: Percent - showSettingOnClick?: boolean - slippageLabel?: React.ReactNode - slippageTooltip?: React.ReactNode - isSlippageModified: boolean -} - -export function RowSlippage({ - allowedSlippage, - showSettingOnClick = true, - slippageTooltip, - slippageLabel, - isSlippageModified, -}: RowSlippageProps) { - const { chainId } = useWalletInfo() - const toggleSettings = useToggleSettingsMenu() - - const isEoaEthFlow = useIsEoaEthFlow() - const nativeCurrency = useNativeCurrency() - const smartSwapSlippage = useSmartSwapSlippage() - const isSmartSlippageApplied = useIsSmartSlippageApplied() - const setSlippage = useSetSlippage() - const isTradePriceUpdating = useTradePricesUpdate() - - const props = useMemo( - () => ({ - chainId, - isEoaEthFlow, - symbols: [nativeCurrency.symbol], - showSettingOnClick, - allowedSlippage, - slippageLabel, - slippageTooltip, - displaySlippage: `${formatPercent(allowedSlippage)}%`, - isSmartSlippageApplied, - isSmartSlippageLoading: isTradePriceUpdating, - smartSlippage: smartSwapSlippage && !isEoaEthFlow ? `${formatPercent(new Percent(smartSwapSlippage, 10_000))}%` : undefined, - setAutoSlippage: smartSwapSlippage && !isEoaEthFlow ? () => setSlippage(null) : undefined, - }), - [chainId, isEoaEthFlow, nativeCurrency.symbol, showSettingOnClick, allowedSlippage, slippageLabel, slippageTooltip, smartSwapSlippage, isSmartSlippageApplied, isTradePriceUpdating] - ) - - return -} diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SurplusModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SurplusModalSetup/index.tsx index ec16c85a42..d53df031ed 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SurplusModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SurplusModalSetup/index.tsx @@ -4,13 +4,12 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { useOrder } from 'legacy/state/orders/hooks' -import { useNavigateToNewOrderCallback } from 'modules/swap/hooks/useNavigateToNewOrderCallback' +import { useTradeConfirmState, useNavigateToNewOrderCallback } from 'modules/trade' import { useOrderProgressBarV2Props } from 'common/hooks/orderProgressBarV2' import { CowModal } from 'common/pure/Modal' import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent' -import { useTradeConfirmState } from '../../../trade' import { useOrderIdForSurplusModal, useRemoveOrderFromSurplusQueue } from '../../state/surplusModal' // TODO: rename? diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx index 021a8fd42e..3e3a8fcd8e 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx @@ -1,16 +1,13 @@ import { percentToBps } from '@cowprotocol/common-utils' -import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' +import { AppDataUpdater } from 'modules/appData' +import { useTradeSlippage, useIsSmartSlippageApplied } from 'modules/tradeSlippage' -import { AppDataUpdater } from '../../../appData' -import { useSwapSlippage } from '../../hooks/useSwapSlippage' -import { BaseFlowContextUpdater } from '../../updaters/BaseFlowContextUpdater' -import { SmartSlippageUpdater } from '../../updaters/SmartSlippageUpdater' import { SwapAmountsFromUrlUpdater } from '../../updaters/SwapAmountsFromUrlUpdater' import { SwapDerivedStateUpdater } from '../../updaters/SwapDerivedStateUpdater' export function SwapUpdaters() { - const slippage = useSwapSlippage() + const slippage = useTradeSlippage() const isSmartSlippageApplied = useIsSmartSlippageApplied() return ( @@ -22,8 +19,6 @@ export function SwapUpdaters() { /> - - ) } 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 b9db3140fb..30c61ac4bc 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -2,22 +2,19 @@ import { ReactNode, useCallback, useMemo, useState } from 'react' import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const' -import { isFractionFalsy, percentToBps } from '@cowprotocol/common-utils' import { useIsTradeUnsupported } from '@cowprotocol/tokens' -import { useIsSafeViaWc, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' +import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { TradeType } from '@cowprotocol/widget-lib' import { NetworkAlert } from 'legacy/components/NetworkAlert/NetworkAlert' -import { SettingsTab } from 'legacy/components/Settings' import { useModalIsOpen } from 'legacy/state/application/hooks' import { ApplicationModal } from 'legacy/state/application/reducer' import { Field } from 'legacy/state/types' +import { useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/user/hooks' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { EthFlowModal, EthFlowProps } from 'modules/swap/containers/EthFlow' import { SwapModals, SwapModalsProps } from 'modules/swap/containers/SwapModals' -import { SwapButtonState } from 'modules/swap/helpers/getSwapButtonState' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useShowRecipientControls } from 'modules/swap/hooks/useShowRecipientControls' import { useSwapButtonContext } from 'modules/swap/hooks/useSwapButtonContext' import { useSwapCurrenciesAmounts } from 'modules/swap/hooks/useSwapCurrenciesAmounts' @@ -29,36 +26,32 @@ import { SwapWarningsTop, SwapWarningsTopProps, } from 'modules/swap/pure/warnings' -import { TradeWidget, TradeWidgetContainer, useReceiveAmountInfo, useTradePriceImpact } from 'modules/trade' -import { useTradeRouteContext } from 'modules/trade/hooks/useTradeRouteContext' -import { useWrappedToken } from 'modules/trade/hooks/useWrappedToken' +import { + TradeWidget, + TradeWidgetContainer, + TradeWidgetSlots, + useReceiveAmountInfo, + useTradePriceImpact, +} from 'modules/trade' +import { + useIsEoaEthFlow, + useTradeRouteContext, + useUnknownImpactWarning, + useIsNoImpactWarningAccepted, +} from 'modules/trade' import { getQuoteTimeOffset } from 'modules/tradeQuote' +import { useTradeSlippage } from 'modules/tradeSlippage' +import { SettingsTab, TradeRateDetails, useHighFeeWarning } from 'modules/tradeWidgetAddons' import { useTradeUsdAmounts } from 'modules/usdAmount' -import { useShouldZeroApprove } from 'modules/zeroApproval' import { useSetLocalTimeOffset } from 'common/containers/InvalidLocalTimeWarning/localTimeOffsetState' import { useRateInfoParams } from 'common/hooks/useRateInfoParams' import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' import { SWAP_QUOTE_CHECK_INTERVAL } from 'common/updaters/FeesUpdater' -import useNativeCurrency from 'lib/hooks/useNativeCurrency' -import { useIsSlippageModified } from '../../hooks/useIsSlippageModified' -import { useIsSmartSlippageApplied } from '../../hooks/useIsSmartSlippageApplied' -import { useIsSwapEth } from '../../hooks/useIsSwapEth' -import { useSwapSlippage } from '../../hooks/useSwapSlippage' -import { - useDerivedSwapInfo, - useHighFeeWarning, - useSwapActionHandlers, - useSwapState, - useUnknownImpactWarning, -} from '../../hooks/useSwapState' +import { useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../hooks/useSwapState' import { useTradeQuoteStateFromLegacy } from '../../hooks/useTradeQuoteStateFromLegacy' import { ConfirmSwapModalSetup } from '../ConfirmSwapModalSetup' -import { TradeRateDetails } from '../TradeRateDetails' - -const BUTTON_STATES_TO_SHOW_BUNDLE_APPROVAL_BANNER = [SwapButtonState.ApproveAndSwap] -const BUTTON_STATES_TO_SHOW_BUNDLE_WRAP_BANNER = [SwapButtonState.WrapAndSwap] export interface SwapWidgetProps { topContent?: ReactNode @@ -66,10 +59,9 @@ export interface SwapWidgetProps { } export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { - const { chainId, account } = useWalletInfo() - const { slippageAdjustedSellAmount, currencies, trade } = useDerivedSwapInfo() - const slippage = useSwapSlippage() - const isSlippageModified = useIsSlippageModified() + const { chainId } = useWalletInfo() + const { currencies, trade } = useDerivedSwapInfo() + const slippage = useTradeSlippage() const parsedAmounts = useSwapCurrenciesAmounts() const { isSupportedWallet } = useWalletDetails() const isSwapUnsupported = useIsTradeUnsupported(currencies.INPUT, currencies.OUTPUT) @@ -78,12 +70,13 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const { independentField, recipient } = swapState const showRecipientControls = useShowRecipientControls(recipient) const isEoaEthFlow = useIsEoaEthFlow() - const shouldZeroApprove = useShouldZeroApprove(slippageAdjustedSellAmount) const widgetParams = useInjectedWidgetParams() - const { enabledTradeTypes, banners: widgetBanners } = widgetParams + const { enabledTradeTypes } = widgetParams const priceImpactParams = useTradePriceImpact() const tradeQuoteStateOverride = useTradeQuoteStateFromLegacy() const receiveAmountInfo = useReceiveAmountInfo() + const recipientToggleState = useRecipientToggleManager() + const deadlineState = useUserTransactionTTL() const isTradePriceUpdating = useTradePricesUpdate() @@ -163,17 +156,10 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const [showNativeWrapModal, setOpenNativeWrapModal] = useState(false) const showCowSubsidyModal = useModalIsOpen(ApplicationModal.COW_SUBSIDY) - // Hide the price impact warning when there is priceImpact value or when it's loading - // The loading values is debounced in useFiatValuePriceImpact() to avoid flickering - const hideUnknownImpactWarning = - isFractionFalsy(parsedAmounts.INPUT) || - isFractionFalsy(parsedAmounts.OUTPUT) || - !!priceImpactParams.priceImpact || - priceImpactParams.loading - - const { feeWarningAccepted, setFeeWarningAccepted } = useHighFeeWarning(trade) - const { impactWarningAccepted: _impactWarningAccepted, setImpactWarningAccepted } = useUnknownImpactWarning() - const impactWarningAccepted = hideUnknownImpactWarning || _impactWarningAccepted + const { feeWarningAccepted } = useHighFeeWarning() + const noImpactWarningAccepted = useIsNoImpactWarningAccepted() + const { impactWarningAccepted: unknownImpactWarning } = useUnknownImpactWarning() + const impactWarningAccepted = noImpactWarningAccepted || unknownImpactWarning const openNativeWrapModal = useCallback(() => setOpenNativeWrapModal(true), []) const dismissNativeWrapModal = useCallback(() => setOpenNativeWrapModal(false), []) @@ -182,7 +168,6 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { feeWarningAccepted, impactWarningAccepted, openNativeWrapModal, - priceImpactParams, }) const tradeUrlParams = useTradeRouteContext() @@ -201,52 +186,15 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { showNativeWrapModal, showCowSubsidyModal, } - - const showApprovalBundlingBanner = BUTTON_STATES_TO_SHOW_BUNDLE_APPROVAL_BANNER.includes( - swapButtonContext.swapButtonState, - ) - const showWrapBundlingBanner = BUTTON_STATES_TO_SHOW_BUNDLE_WRAP_BANNER.includes(swapButtonContext.swapButtonState) - - const isSafeViaWc = useIsSafeViaWc() - const isSwapEth = useIsSwapEth() - - const showSafeWcApprovalBundlingBanner = - !showApprovalBundlingBanner && isSafeViaWc && swapButtonContext.swapButtonState === SwapButtonState.NeedApprove - - const showSafeWcWrapBundlingBanner = !showWrapBundlingBanner && isSafeViaWc && isSwapEth - - // Show the same banner when approval is needed or selling native token - const showSafeWcBundlingBanner = - (showSafeWcApprovalBundlingBanner || showSafeWcWrapBundlingBanner) && !widgetBanners?.hideSafeWebAppBanner - const showTwapSuggestionBanner = !enabledTradeTypes || enabledTradeTypes.includes(TradeType.ADVANCED) - const nativeCurrencySymbol = useNativeCurrency().symbol || 'ETH' - const wrappedCurrencySymbol = useWrappedToken().symbol || 'WETH' - - const isSuggestedSlippage = useIsSmartSlippageApplied() && !isTradePriceUpdating && !!account - const swapWarningsTopProps: SwapWarningsTopProps = { chainId, trade, - account, - feeWarningAccepted, - impactWarningAccepted, - hideUnknownImpactWarning, - showApprovalBundlingBanner, - showWrapBundlingBanner, - showSafeWcBundlingBanner, showTwapSuggestionBanner, - nativeCurrencySymbol, - wrappedCurrencySymbol, - setFeeWarningAccepted, - setImpactWarningAccepted, - shouldZeroApprove, buyingFiatAmount, priceImpact: priceImpactParams.priceImpact, tradeUrlParams, - slippageBps: percentToBps(slippage), - isSuggestedSlippage, } const swapWarningsBottomProps: SwapWarningsBottomProps = { @@ -256,29 +204,43 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { currencyOut: currencies.OUTPUT || undefined, } - const slots = { - settingsWidget: , + const slots: TradeWidgetSlots = { + settingsWidget: , topContent, - bottomContent: ( - <> - {bottomContent} - - - - - + bottomContent: useCallback( + (warnings: ReactNode | null) => { + return ( + <> + {bottomContent} + + + {warnings} + + + + ) + }, + [ + bottomContent, + deadlineState, + isTradePriceUpdating, + rateInfoParams, + swapButtonContext, + swapWarningsTopProps, + swapWarningsBottomProps, + ], ), } const params = { isEoaEthFlow, compactView: true, + enableSmartSlippage: true, recipient, showRecipient: showRecipientControls, isTradePriceUpdating, diff --git a/apps/cowswap-frontend/src/modules/swap/helpers/getEthFlowEnabled.ts b/apps/cowswap-frontend/src/modules/swap/helpers/getEthFlowEnabled.ts deleted file mode 100644 index 76e9a374e4..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/helpers/getEthFlowEnabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getEthFlowEnabled(isSmartContractWallet: boolean): boolean { - return !isSmartContractWallet -} diff --git a/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts b/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts index 34f8a208aa..677617b953 100644 --- a/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts +++ b/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts @@ -4,7 +4,6 @@ import { QuoteError } from 'legacy/state/price/actions' import { QuoteInformationObject } from 'legacy/state/price/reducer' import TradeGp from 'legacy/state/swap/TradeGp' -import { getEthFlowEnabled } from 'modules/swap/helpers/getEthFlowEnabled' import { isQuoteExpired, QuoteDeadlineParams } from 'modules/tradeQuote' import { ApprovalState } from 'common/hooks/useApproveState' @@ -141,7 +140,7 @@ export function getSwapButtonState(input: SwapButtonStateParams): SwapButtonStat } if (input.isNativeIn) { - if (getEthFlowEnabled(input.isSmartContractWallet === true)) { + if (!input.isSmartContractWallet) { return SwapButtonState.RegularEthFlowSwap } else if (input.isBundlingSupported) { return SwapButtonState.WrapAndSwap diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts deleted file mode 100644 index 6bed3d4da2..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useMemo } from 'react' - -import { getWrappedToken } from '@cowprotocol/common-utils' -import { OrderKind } from '@cowprotocol/cow-sdk' -import { useSafeAppsSdk } from '@cowprotocol/wallet' -import { useWalletProvider } from '@cowprotocol/wallet-provider' -import { TradeType } from '@uniswap/sdk-core' - -import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' -import { BaseSafeFlowContext } from 'modules/swap/services/types' - -import { useGP2SettlementContract } from 'common/hooks/useContract' -import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress' - -export function useBaseSafeBundleFlowContext(): BaseSafeFlowContext | null { - const baseProps = useBaseFlowContextSource() - const sellToken = baseProps?.trade ? getWrappedToken(baseProps.trade.inputAmount.currency) : undefined - const settlementContract = useGP2SettlementContract() - const spender = useTradeSpenderAddress() - - const safeAppsSdk = useSafeAppsSdk() - const provider = useWalletProvider() - - return useMemo(() => { - if (!baseProps?.trade || !settlementContract || !spender || !safeAppsSdk || !provider) return null - - const baseContext = getFlowContext({ - baseProps, - sellToken, - kind: baseProps.trade.tradeType === TradeType.EXACT_INPUT ? OrderKind.SELL : OrderKind.BUY, - }) - - if (!baseContext) return null - - return { - ...baseContext, - settlementContract, - spender, - safeAppsSdk, - provider, - } - }, [baseProps, settlementContract, spender, safeAppsSdk, provider, sellToken]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts index c3939fb295..f73262fc99 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts @@ -1,52 +1,54 @@ import { useSetAtom } from 'jotai' -import { useMemo } from 'react' -import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' -import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' + +import useSWR from 'swr' import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks' +import { useGetQuoteAndStatus } from 'legacy/state/price/hooks' -import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' -import { EthFlowContext } from 'modules/swap/services/types' -import { addInFlightOrderIdAtom } from 'modules/swap/state/EthFlow/ethFlowInFlightOrderIdsAtom' +import { useAppData, useUploadAppData } from 'modules/appData' import { useEthFlowContract } from 'common/hooks/useContract' import { useCheckEthFlowOrderExists } from './useCheckEthFlowOrderExists' +import { useDerivedSwapInfo } from './useSwapState' -import { FlowType } from '../types/flowContext' +import { EthFlowContext } from '../services/types' +import { addInFlightOrderIdAtom } from '../state/EthFlow/ethFlowInFlightOrderIdsAtom' export function useEthFlowContext(): EthFlowContext | null { + const { chainId } = useWalletInfo() + const { currenciesIds } = useDerivedSwapInfo() + const { quote } = useGetQuoteAndStatus({ + token: currenciesIds.INPUT, + chainId, + }) const contract = useEthFlowContract() - const baseProps = useBaseFlowContextSource() const addTransaction = useTransactionAdder() - - const sellToken = baseProps?.chainId ? NATIVE_CURRENCIES[baseProps.chainId as SupportedChainId] : undefined + const uploadAppData = useUploadAppData() + const appData = useAppData() const addInFlightOrderId = useSetAtom(addInFlightOrderIdAtom) const checkEthFlowOrderExists = useCheckEthFlowOrderExists() - const baseContext = useMemo( - () => - baseProps && - getFlowContext({ - baseProps, - sellToken, - kind: OrderKind.SELL, - }), - [baseProps, sellToken], + return ( + useSWR( + appData && contract + ? [quote, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, uploadAppData, appData] + : null, + ([quote, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, uploadAppData, appData]) => { + return { + quote, + contract, + addTransaction, + checkEthFlowOrderExists, + addInFlightOrderId, + uploadAppData, + appData, + } + }, + ).data || null ) - - return useMemo(() => { - if (!baseContext || !contract || baseProps?.flowType !== FlowType.EOA_ETH_FLOW) return null - - return { - ...baseContext, - contract, - addTransaction, - checkEthFlowOrderExists, - addInFlightOrderId, - } - }, [baseContext, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, baseProps?.flowType]) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts deleted file mode 100644 index 9510a7c33d..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { useAtomValue } from 'jotai/index' - -import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' -import { getIsNativeToken } from '@cowprotocol/common-utils' -import { OrderClass, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' -import { UiOrderType } from '@cowprotocol/types' -import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' - -import { computeSlippageAdjustedAmounts } from 'legacy/utils/prices' -import { PostOrderParams } from 'legacy/utils/trade' - -import { BaseFlowContext } from 'modules/swap/services/types' -import { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics' -import { getOrderValidTo } from 'modules/tradeQuote' - -import { useSafeMemo } from 'common/hooks/useSafeMemo' - -import { useSwapSlippage } from './useSwapSlippage' -import { useDerivedSwapInfo } from './useSwapState' - -import { getAmountsForSignature } from '../helpers/getAmountsForSignature' -import { baseFlowContextSourceAtom } from '../state/baseFlowContextSourceAtom' -import { BaseFlowContextSource } from '../types/flowContext' - -export function useSwapAmountsWithSlippage(): [ - CurrencyAmount | undefined, - CurrencyAmount | undefined, -] { - const slippage = useSwapSlippage() - const { trade } = useDerivedSwapInfo() - - const { INPUT, OUTPUT } = computeSlippageAdjustedAmounts(trade, slippage) - - return useSafeMemo(() => [INPUT, OUTPUT], [INPUT, OUTPUT]) -} - -export function useBaseFlowContextSource(): BaseFlowContextSource | null { - return useAtomValue(baseFlowContextSourceAtom) -} - -type BaseGetFlowContextProps = { - baseProps: BaseFlowContextSource - sellToken?: Token - kind: OrderKind -} - -export function getFlowContext({ baseProps, sellToken, kind }: BaseGetFlowContextProps): BaseFlowContext | null { - const { - chainId, - account, - provider, - trade, - appData, - wethContract, - inputAmountWithSlippage, - outputAmountWithSlippage, - gnosisSafeInfo, - recipient, - recipientAddressOrName, - deadline, - ensRecipientAddress, - allowsOffchainSigning, - closeModals, - addOrderCallback, - uploadAppData, - dispatch, - flowType, - sellTokenContract, - allowedSlippage, - tradeConfirmActions, - getCachedPermit, - quote, - typedHooks, - } = baseProps - - if ( - !chainId || - !account || - !provider || - !trade || - !appData || - !wethContract || - !inputAmountWithSlippage || - !outputAmountWithSlippage - ) { - return null - } - - const isSafeWallet = !!gnosisSafeInfo - - const buyToken = getIsNativeToken(trade.outputAmount.currency) - ? NATIVE_CURRENCIES[chainId as SupportedChainId] - : trade.outputAmount.currency - const marketLabel = [sellToken?.symbol, buyToken.symbol].join(',') - - if (!sellToken || !buyToken) { - return null - } - - const swapFlowAnalyticsContext: TradeFlowAnalyticsContext = { - account, - recipient, - recipientAddress: recipientAddressOrName, - marketLabel, - orderType: UiOrderType.SWAP, - } - - const validTo = getOrderValidTo(deadline, { - validFor: quote?.validFor, - quoteValidTo: quote?.quoteValidTo, - localQuoteTimestamp: quote?.localQuoteTimestamp, - }) - - const amountsForSignature = getAmountsForSignature({ - trade, - kind, - allowedSlippage, - }) - - const orderParams: PostOrderParams = { - class: OrderClass.MARKET, - kind, - account, - chainId, - ...amountsForSignature, - sellAmountBeforeFee: trade.inputAmountWithoutFee, - feeAmount: trade.fee.feeAsCurrency, - buyToken, - sellToken, - validTo, - recipient: ensRecipientAddress || recipient || account, - recipientAddressOrName, - signer: provider.getSigner(), - allowsOffchainSigning, - partiallyFillable: false, // SWAP orders are always fill or kill - for now - appData, - quoteId: trade.quoteId, - isSafeWallet, - } - - return { - context: { - chainId, - trade, - inputAmountWithSlippage, - outputAmountWithSlippage, - flowType, - }, - flags: { - allowsOffchainSigning, - }, - callbacks: { - closeModals, - addOrderCallback, - uploadAppData, - getCachedPermit, - }, - dispatch, - swapFlowAnalyticsContext, - orderParams, - appDataInfo: appData, - sellTokenContract, - tradeConfirmActions, - quote, - typedHooks, - } -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.test.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.test.tsx deleted file mode 100644 index 4aa2350231..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { PropsWithChildren } from 'react' - -import { renderHook } from '@testing-library/react-hooks' - -import { PriceImpact } from 'legacy/hooks/usePriceImpact' -import { Field } from 'legacy/state/types' - -import { useSafeBundleApprovalFlowContext } from 'modules/swap/hooks/useSafeBundleApprovalFlowContext' -import { ethFlow } from 'modules/swap/services/ethFlow' -import { safeBundleApprovalFlow, safeBundleEthFlow } from 'modules/swap/services/safeBundleFlow' -import { swapFlow } from 'modules/swap/services/swapFlow' - -import { WithModalProvider } from 'utils/withModalProvider' - -import { useEthFlowContext } from './useEthFlowContext' -import { useHandleSwap } from './useHandleSwap' -import { useSafeBundleEthFlowContext } from './useSafeBundleEthFlowContext' -import { useSwapFlowContext } from './useSwapFlowContext' -import { useSwapActionHandlers } from './useSwapState' - -jest.mock('./useSwapState') -jest.mock('./useSwapFlowContext') -jest.mock('./useEthFlowContext') -jest.mock('./useSafeBundleApprovalFlowContext') -jest.mock('./useSafeBundleEthFlowContext') -jest.mock('modules/swap/services/swapFlow') -jest.mock('modules/swap/services/ethFlow') -jest.mock('modules/swap/services/safeBundleFlow') -jest.mock('modules/twap/state/twapOrdersListAtom', () => ({})) -jest.mock('modules/analytics/useAnalyticsReporterCowSwap') - -const mockUseSwapActionHandlers = useSwapActionHandlers as jest.MockedFunction -const mockSwapFlow = swapFlow as jest.MockedFunction -const mockEthFlow = ethFlow as jest.MockedFunction -const mockSafeBundleApprovalFlow = safeBundleApprovalFlow as jest.MockedFunction -const mockSafeBundleEthFlow = safeBundleEthFlow as jest.MockedFunction -const mockUseSwapFlowContext = useSwapFlowContext as jest.MockedFunction -const mockUseEthFlowContext = useEthFlowContext as jest.MockedFunction -const mockUseSafeBundleFlowContext = useSafeBundleApprovalFlowContext as jest.MockedFunction< - typeof useSafeBundleApprovalFlowContext -> -const mockUseSafeBundleEthFlowContext = useSafeBundleEthFlowContext as jest.MockedFunction< - typeof useSafeBundleEthFlowContext -> - -const priceImpactMock: PriceImpact = { - priceImpact: undefined, - loading: false, -} - -const WithProviders = ({ children }: PropsWithChildren) => { - return {children} -} - -describe('useHandleSwapCallback', () => { - let onUserInput: jest.Mock - let onChangeRecipient: jest.Mock - - beforeEach(() => { - onChangeRecipient = jest.fn() - onUserInput = jest.fn() - - mockUseSwapActionHandlers.mockReturnValue({ onChangeRecipient, onUserInput } as any) - mockUseSwapFlowContext.mockReturnValue(1 as any) - mockUseEthFlowContext.mockReturnValue(1 as any) - mockUseSafeBundleFlowContext.mockReturnValue(1 as any) - mockUseSafeBundleEthFlowContext.mockReturnValue(1 as any) - - mockSwapFlow.mockImplementation(() => Promise.resolve()) - mockEthFlow.mockImplementation(() => Promise.resolve()) - mockSafeBundleApprovalFlow.mockImplementation(() => Promise.resolve()) - mockSafeBundleEthFlow.mockImplementation(() => Promise.resolve()) - }) - - it('When a swap happened, then the recipient value should be deleted', async () => { - const { result } = renderHook(() => useHandleSwap(priceImpactMock), { wrapper: WithProviders }) - - await result.current() - - expect(onChangeRecipient).toBeCalledTimes(1) - expect(onChangeRecipient).toHaveBeenCalledWith(null) - expect(onUserInput).toBeCalledTimes(1) - expect(onUserInput).toHaveBeenCalledWith(Field.INPUT, '') - }) -}) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.ts deleted file mode 100644 index b345a36269..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwap.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useCallback } from 'react' - -import { PriceImpact } from 'legacy/hooks/usePriceImpact' -import { Field } from 'legacy/state/types' - -import { useSafeBundleApprovalFlowContext } from 'modules/swap/hooks/useSafeBundleApprovalFlowContext' -import { ethFlow } from 'modules/swap/services/ethFlow' -import { safeBundleApprovalFlow, safeBundleEthFlow } from 'modules/swap/services/safeBundleFlow' -import { swapFlow } from 'modules/swap/services/swapFlow' -import { logTradeFlow } from 'modules/trade/utils/logger' - -import { useConfirmPriceImpactWithoutFee } from 'common/hooks/useConfirmPriceImpactWithoutFee' - -import { useEthFlowContext } from './useEthFlowContext' -import { useSafeBundleEthFlowContext } from './useSafeBundleEthFlowContext' -import { useSwapFlowContext } from './useSwapFlowContext' -import { useSwapActionHandlers } from './useSwapState' - -export function useHandleSwap(priceImpactParams: PriceImpact): () => Promise { - const swapFlowContext = useSwapFlowContext() - const ethFlowContext = useEthFlowContext() - const safeBundleApprovalFlowContext = useSafeBundleApprovalFlowContext() - const safeBundleEthFlowContext = useSafeBundleEthFlowContext() - const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() - const { onChangeRecipient, onUserInput } = useSwapActionHandlers() - - return useCallback(async () => { - if (!swapFlowContext && !ethFlowContext && !safeBundleApprovalFlowContext && !safeBundleEthFlowContext) return - - const tradeResult = await (async () => { - if (safeBundleApprovalFlowContext) { - logTradeFlow('SAFE BUNDLE APPROVAL FLOW', 'Start safe bundle approval flow') - return safeBundleApprovalFlow(safeBundleApprovalFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } else if (safeBundleEthFlowContext) { - logTradeFlow('SAFE BUNDLE ETH FLOW', 'Start safe bundle eth flow') - return safeBundleEthFlow(safeBundleEthFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } else if (swapFlowContext) { - logTradeFlow('SWAP FLOW', 'Start swap flow') - return swapFlow(swapFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } else if (ethFlowContext) { - logTradeFlow('ETH FLOW', 'Start eth flow') - return ethFlow(ethFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } - })() - - const isPriceImpactDeclined = tradeResult === false - - // Clean up form fields after successful swap - if (!isPriceImpactDeclined) { - onChangeRecipient(null) - onUserInput(Field.INPUT, '') - } - }, [ - swapFlowContext, - ethFlowContext, - safeBundleApprovalFlowContext, - safeBundleEthFlowContext, - onChangeRecipient, - onUserInput, - priceImpactParams, - confirmPriceImpactWithoutFee, - ]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts new file mode 100644 index 0000000000..10ecb4104b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts @@ -0,0 +1,42 @@ +import { useCallback } from 'react' + +import { useUserTransactionTTL } from 'legacy/state/user/hooks' + +import { useTradePriceImpact } from 'modules/trade' +import { logTradeFlow } from 'modules/trade/utils/logger' +import { useHandleSwap, useTradeFlowType } from 'modules/tradeFlow' +import { FlowType } from 'modules/tradeFlow' + +import { useConfirmPriceImpactWithoutFee } from 'common/hooks/useConfirmPriceImpactWithoutFee' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' + +import { useEthFlowContext } from './useEthFlowContext' +import { useSwapFlowContext } from './useSwapFlowContext' + +import { ethFlow } from '../services/ethFlow' + +export function useHandleSwapOrEthFlow() { + const priceImpactParams = useTradePriceImpact() + const swapFlowContext = useSwapFlowContext() + const ethFlowContext = useEthFlowContext() + const tradeFlowType = useTradeFlowType() + const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() + + const [deadline] = useUserTransactionTTL() + const { callback: handleSwap, contextIsReady } = useHandleSwap(useSafeMemoObject({ deadline })) + + const callback = useCallback(() => { + if (!swapFlowContext) return + + if (tradeFlowType === FlowType.EOA_ETH_FLOW) { + if (!ethFlowContext) throw new Error('Eth flow context is not ready') + + logTradeFlow('ETH FLOW', 'Start eth flow') + return ethFlow(swapFlowContext, ethFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + } + + return handleSwap() + }, [swapFlowContext, ethFlowContext, handleSwap, tradeFlowType, priceImpactParams, confirmPriceImpactWithoutFee]) + + return { callback, contextIsReady } +} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts deleted file mode 100644 index 6f342d3120..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMemo } from 'react' - -import { getWrappedToken } from '@cowprotocol/common-utils' - -import { SafeBundleApprovalFlowContext } from 'modules/swap/services/types' - -import { useTokenContract } from 'common/hooks/useContract' - -import { useBaseSafeBundleFlowContext } from './useBaseSafeBundleFlowContext' - -import { FlowType } from '../types/flowContext' - -export function useSafeBundleApprovalFlowContext(): SafeBundleApprovalFlowContext | null { - const baseContext = useBaseSafeBundleFlowContext() - const trade = baseContext?.context.trade - - const sellToken = trade ? getWrappedToken(trade.inputAmount.currency) : undefined - const erc20Contract = useTokenContract(sellToken?.address) - - return useMemo(() => { - if ( - !baseContext || - !baseContext.context.trade || - !erc20Contract || - baseContext.context.flowType !== FlowType.SAFE_BUNDLE_APPROVAL - ) { - return null - } - - return { - ...baseContext, - erc20Contract, - } - }, [baseContext, erc20Contract]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts deleted file mode 100644 index cc8bfb1a15..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useMemo } from 'react' - -import { SafeBundleEthFlowContext } from 'modules/swap/services/types' - -import { useWETHContract } from 'common/hooks/useContract' -import { useNeedsApproval } from 'common/hooks/useNeedsApproval' - -import { useBaseSafeBundleFlowContext } from './useBaseSafeBundleFlowContext' - -import { FlowType } from '../types/flowContext' - -export function useSafeBundleEthFlowContext(): SafeBundleEthFlowContext | null { - const baseContext = useBaseSafeBundleFlowContext() - - const wrappedNativeContract = useWETHContract() - const needsApproval = useNeedsApproval(baseContext?.context.inputAmountWithSlippage) - - return useMemo(() => { - if (!wrappedNativeContract || !baseContext || baseContext.context.flowType !== FlowType.SAFE_BUNDLE_ETH) return null - - return { - ...baseContext, - wrappedNativeContract, - needsApproval, - } - }, [baseContext, wrappedNativeContract, needsApproval]) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSetSlippage.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSetSlippage.ts deleted file mode 100644 index 579dbed4d0..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSetSlippage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useSetAtom } from 'jotai' - -import { setSwapSlippageAtom } from '../state/slippageValueAndTypeAtom' - -export function useSetSlippage() { - return useSetAtom(setSwapSlippageAtom) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index f92a26c36a..5efc07fdac 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -12,7 +12,6 @@ import { } from '@cowprotocol/wallet' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { PriceImpact } from 'legacy/hooks/usePriceImpact' import { useToggleWalletModal } from 'legacy/state/application/hooks' import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/hooks' import { Field } from 'legacy/state/types' @@ -20,10 +19,6 @@ import { Field } from 'legacy/state/types' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useTokenSupportsPermit } from 'modules/permit' import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState' -import { useEthFlowContext } from 'modules/swap/hooks/useEthFlowContext' -import { useHandleSwap } from 'modules/swap/hooks/useHandleSwap' -import { useSafeBundleApprovalFlowContext } from 'modules/swap/hooks/useSafeBundleApprovalFlowContext' -import { useSwapFlowContext } from 'modules/swap/hooks/useSwapFlowContext' import { SwapButtonsContext } from 'modules/swap/pure/SwapButtons' import { TradeType, useTradeConfirmActions, useWrapNativeFlow } from 'modules/trade' import { useIsNativeIn } from 'modules/trade/hooks/useIsNativeInOrOut' @@ -34,18 +29,17 @@ import { QuoteDeadlineParams } from 'modules/tradeQuote' import { useApproveState } from 'common/hooks/useApproveState' import { useSafeMemo } from 'common/hooks/useSafeMemo' -import { useSafeBundleEthFlowContext } from './useSafeBundleEthFlowContext' +import { useHandleSwapOrEthFlow } from './useHandleSwapOrEthFlow' import { useDerivedSwapInfo, useSwapActionHandlers } from './useSwapState' export interface SwapButtonInput { feeWarningAccepted: boolean impactWarningAccepted: boolean - priceImpactParams: PriceImpact openNativeWrapModal(): void } export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext { - const { feeWarningAccepted, impactWarningAccepted, openNativeWrapModal, priceImpactParams } = input + const { feeWarningAccepted, impactWarningAccepted, openNativeWrapModal } = input const { account, chainId } = useWalletInfo() const { isSupportedWallet } = useWalletDetails() @@ -58,10 +52,6 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext inputError: swapInputError, } = useDerivedSwapInfo() const toggleWalletModal = useToggleWalletModal() - const swapFlowContext = useSwapFlowContext() - const ethFlowContext = useEthFlowContext() - const safeBundleApprovalFlowContext = useSafeBundleApprovalFlowContext() - const safeBundleEthFlowContext = useSafeBundleEthFlowContext() const { onCurrencySelection } = useSwapActionHandlers() const isBestQuoteLoading = useIsBestQuoteLoading() const tradeConfirmActions = useTradeConfirmActions() @@ -86,12 +76,9 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext const wrapCallback = useWrapNativeFlow() const { state: approvalState } = useApproveState(slippageAdjustedSellAmount || null) - const handleSwap = useHandleSwap(priceImpactParams) + const { callback: handleSwap, contextIsReady } = useHandleSwapOrEthFlow() - const contextExists = ethFlowContext || swapFlowContext || safeBundleApprovalFlowContext || safeBundleEthFlowContext - const recipientAddressOrName = contextExists?.orderParams.recipientAddressOrName || null - - const swapCallbackError = contextExists ? null : 'Missing dependencies' + const swapCallbackError = contextIsReady ? null : 'Missing dependencies' const gnosisSafeInfo = useGnosisSafeInfo() const isReadonlyGnosisSafeUser = gnosisSafeInfo?.isReadOnly || false @@ -106,7 +93,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext quoteValidTo: quote?.quoteValidTo, localQuoteTimestamp: quote?.localQuoteTimestamp, }), - [quote?.validFor, quote?.quoteValidTo, quote?.localQuoteTimestamp] + [quote?.validFor, quote?.quoteValidTo, quote?.localQuoteTimestamp], ) const swapButtonState = getSwapButtonState({ @@ -146,7 +133,6 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext toggleWalletModal, swapInputError, onCurrencySelection, - recipientAddressOrName, widgetStandaloneMode: standaloneMode, quoteDeadlineParams, }), @@ -163,10 +149,9 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext toggleWalletModal, swapInputError, onCurrencySelection, - recipientAddressOrName, standaloneMode, quoteDeadlineParams, - ] + ], ) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmButtonText.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmButtonText.tsx index 685d4c3bc7..6f249918a5 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmButtonText.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmButtonText.tsx @@ -6,9 +6,9 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Nullish } from 'types' -import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' +import { useIsSafeEthFlow } from 'modules/trade' -import { useIsSafeEthFlow } from './useIsSafeEthFlow' +import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' export function useSwapConfirmButtonText(slippageAdjustedSellAmount: Nullish>) { const isSafeApprovalBundle = useIsSafeApprovalBundle(slippageAdjustedSellAmount) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index 77bbff00bb..afed5d89d7 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -1,53 +1,10 @@ -import { useMemo } from 'react' +import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { getWrappedToken } from '@cowprotocol/common-utils' -import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' -import { TradeType as UniTradeType } from '@uniswap/sdk-core' +import { useTradeFlowContext } from 'modules/tradeFlow' -import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' -import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext' -import { SwapFlowContext } from 'modules/swap/services/types' -import { useEnoughBalanceAndAllowance } from 'modules/tokens' -import { TradeType } from 'modules/trade' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' -import { useGP2SettlementContract } from 'common/hooks/useContract' - -import { FlowType } from '../types/flowContext' - -export function useSwapFlowContext(): SwapFlowContext | null { - const contract = useGP2SettlementContract() - const baseProps = useBaseFlowContextSource() - const sellCurrency = baseProps?.trade?.inputAmount?.currency - const permitInfo = usePermitInfo(sellCurrency, TradeType.SWAP) - const generatePermitHook = useGeneratePermitHook() - - const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[baseProps?.chainId || SupportedChainId.MAINNET] - const { enoughAllowance } = useEnoughBalanceAndAllowance({ - account: baseProps?.account, - amount: baseProps?.inputAmountWithSlippage, - checkAllowanceAddress, - }) - - return useMemo(() => { - if (!baseProps?.trade) { - return null - } - - const baseContext = getFlowContext({ - baseProps, - sellToken: getWrappedToken(baseProps.trade.inputAmount.currency), - kind: baseProps.trade.tradeType === UniTradeType.EXACT_INPUT ? OrderKind.SELL : OrderKind.BUY, - }) - - if (!contract || !baseContext || baseProps.flowType !== FlowType.REGULAR) { - return null - } - - return { - ...baseContext, - contract, - permitInfo: !enoughAllowance ? permitInfo : undefined, - generatePermitHook, - } - }, [baseProps, contract, enoughAllowance, permitInfo, generatePermitHook]) +export function useSwapFlowContext() { + const [deadline] = useUserTransactionTTL() + return useTradeFlowContext(useSafeMemoObject({ deadline })) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapSlippage.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapSlippage.ts deleted file mode 100644 index 3f041d35fd..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapSlippage.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useAtomValue } from 'jotai/index' - -import { bpsToPercent } from '@cowprotocol/common-utils' -import { Percent } from '@uniswap/sdk-core' - -import { defaultSlippageAtom, smartSwapSlippageAtom, swapSlippagePercentAtom } from '../state/slippageValueAndTypeAtom' - -export function useSwapSlippage(): Percent { - return useAtomValue(swapSlippagePercentAtom) -} - -export function useDefaultSwapSlippage() { - return bpsToPercent(useAtomValue(defaultSlippageAtom)) -} - -export function useSmartSwapSlippage() { - return useAtomValue(smartSwapSlippageAtom) -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index d0a98efd80..5710c36dd7 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -1,13 +1,12 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' -import { FEE_SIZE_THRESHOLD } from '@cowprotocol/common-const' import { formatSymbol, getIsNativeToken, isAddress, tryParseCurrencyAmount } from '@cowprotocol/common-utils' import { useENS } from '@cowprotocol/ens' import { useAreThereTokensWithSameSymbol, useTokenBySymbolOrAddress } from '@cowprotocol/tokens' import { Command } from '@cowprotocol/types' import { useWalletInfo } from '@cowprotocol/wallet' -import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { t } from '@lingui/macro' @@ -23,13 +22,12 @@ import { Field } from 'legacy/state/types' import { changeSwapAmountAnalytics, switchTokensAnalytics } from 'modules/analytics' import { useNavigateOnCurrencySelection } from 'modules/trade/hooks/useNavigateOnCurrencySelection' import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate' +import { useTradeSlippage } from 'modules/tradeSlippage' import { useVolumeFee } from 'modules/volumeFee' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' import { useSafeMemo } from 'common/hooks/useSafeMemo' -import { useSwapSlippage } from './useSwapSlippage' - export const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = { '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f': true, // v2 factory '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a': true, // v2 router 01 @@ -90,14 +88,14 @@ export function useSwapActionHandlers(): SwapActions { changeSwapAmountAnalytics(field, Number(typedValue)) dispatch(typeInput({ field, typedValue })) }, - [dispatch] + [dispatch], ) const onChangeRecipient = useCallback( (recipient: string | null) => { dispatch(setRecipient({ recipient })) }, - [dispatch] + [dispatch], ) return useMemo( @@ -107,94 +105,14 @@ export function useSwapActionHandlers(): SwapActions { onUserInput, onChangeRecipient, }), - [onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient] - ) -} - -/** - * useHighFeeWarning - * @description checks whether fee vs trade inputAmount = high fee warning - * @description returns params related to high fee and a cb for checking/unchecking fee acceptance - * @param trade TradeGp param - */ -export function useHighFeeWarning(trade?: TradeGp) { - const { INPUT, OUTPUT, independentField } = useSwapState() - - const [feeWarningAccepted, setFeeWarningAccepted] = useState(false) // mod - high fee warning disable state - - // only considers inputAmount vs fee (fee is in input token) - const [isHighFee, feePercentage] = useMemo(() => { - if (!trade) return [false, undefined] - - const { outputAmountWithoutFee, inputAmountAfterFees, fee, volumeFeeAmount } = trade - const isExactInput = trade.tradeType === TradeType.EXACT_INPUT - const feeAsCurrency = isExactInput ? trade.executionPrice.quote(fee.feeAsCurrency) : fee.feeAsCurrency - - const totalFeeAmount = volumeFeeAmount ? feeAsCurrency.add(volumeFeeAmount) : feeAsCurrency - const targetAmount = isExactInput ? outputAmountWithoutFee : inputAmountAfterFees - const feePercentage = totalFeeAmount.divide(targetAmount).multiply(100).asFraction - - return [feePercentage.greaterThan(FEE_SIZE_THRESHOLD), feePercentage] - }, [trade]) - - // reset the state when users change swap params - useEffect(() => { - setFeeWarningAccepted(false) - }, [INPUT.currencyId, OUTPUT.currencyId, independentField]) - - return useSafeMemo( - () => ({ - isHighFee, - feePercentage, - // we only care/check about feeWarning being accepted if the fee is actually high.. - feeWarningAccepted: _computeFeeWarningAcceptedState({ feeWarningAccepted, isHighFee }), - setFeeWarningAccepted, - }), - [isHighFee, feePercentage, feeWarningAccepted, setFeeWarningAccepted] - ) -} - -function _computeFeeWarningAcceptedState({ - feeWarningAccepted, - isHighFee, -}: { - feeWarningAccepted: boolean - isHighFee: boolean -}) { - if (feeWarningAccepted) return true - else { - // is the fee high? that's only when we care - if (isHighFee) { - return feeWarningAccepted - } else { - return true - } - } -} - -export function useUnknownImpactWarning() { - const { INPUT, OUTPUT, independentField } = useSwapState() - - const [impactWarningAccepted, setImpactWarningAccepted] = useState(false) - - // reset the state when users change swap params - useEffect(() => { - setImpactWarningAccepted(false) - }, [INPUT.currencyId, OUTPUT.currencyId, independentField]) - - return useMemo( - () => ({ - impactWarningAccepted, - setImpactWarningAccepted, - }), - [impactWarningAccepted, setImpactWarningAccepted] + [onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient], ) } // from the current swap inputs, compute the best trade and return it. export function useDerivedSwapInfo(): DerivedSwapInfo { const { account, chainId } = useWalletInfo() - const slippage = useSwapSlippage() + const slippage = useTradeSlippage() const { independentField, @@ -220,7 +138,7 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { const isExactIn: boolean = independentField === Field.INPUT const parsedAmount = useMemo( () => tryParseCurrencyAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined), - [inputCurrency, isExactIn, outputCurrency, typedValue] + [inputCurrency, isExactIn, outputCurrency, typedValue], ) const currencies: { [field in Field]?: Currency | null } = useMemo( @@ -228,7 +146,7 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency, }), - [inputCurrency, outputCurrency] + [inputCurrency, outputCurrency], ) // TODO: be careful! For native tokens we use symbol instead of address @@ -243,7 +161,7 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { ? currencies.OUTPUT.symbol : currencies.OUTPUT?.address?.toLowerCase(), }), - [currencies] + [currencies], ) const { quote } = useGetQuoteAndStatus({ @@ -280,7 +198,7 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { [Field.INPUT]: inputCurrencyBalance, [Field.OUTPUT]: outputCurrencyBalance, }), - [inputCurrencyBalance, outputCurrencyBalance] + [inputCurrencyBalance, outputCurrencyBalance], ) // allowed slippage is either auto slippage, or custom user defined slippage if auto slippage disabled diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useTradeQuoteStateFromLegacy.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useTradeQuoteStateFromLegacy.ts index fa840f5c97..b9f88732f3 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useTradeQuoteStateFromLegacy.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useTradeQuoteStateFromLegacy.ts @@ -26,7 +26,8 @@ export function useTradeQuoteStateFromLegacy(): TradeQuoteState | null { quoteParams: quote || null, localQuoteTimestamp: quote?.localQuoteTimestamp || null, hasParamsChanged: isGettingNewQuote, + fetchStartTimestamp: null, }), - [quote, isLoading, isGettingNewQuote] + [quote, isLoading, isGettingNewQuote], ) } diff --git a/apps/cowswap-frontend/src/modules/swap/index.ts b/apps/cowswap-frontend/src/modules/swap/index.ts index d22922dbb8..772c226a02 100644 --- a/apps/cowswap-frontend/src/modules/swap/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/index.ts @@ -2,5 +2,5 @@ export * from './containers/SwapWidget' export * from './containers/SwapUpdaters' export * from './updaters/SwapDerivedStateUpdater' export * from './updaters/SwapAmountsFromUrlUpdater' -export * from './updaters/SmartSlippageUpdater' +export * from '../tradeSlippage/updaters/SmartSlippageUpdater' export * from './state/swapDerivedStateAtom' diff --git a/apps/cowswap-frontend/src/modules/swap/pure/ReceiveAmountInfo/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/ReceiveAmountInfo/index.tsx index 28dae3637f..61a12cd9c2 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/ReceiveAmountInfo/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/ReceiveAmountInfo/index.tsx @@ -6,8 +6,8 @@ import { Trans } from '@lingui/macro' import { BalanceAndSubsidy } from 'legacy/hooks/useCowBalanceAndSubsidy' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { getOrderTypeReceiveAmounts } from 'modules/trade' +import { useIsEoaEthFlow } from 'modules/trade' import { ReceiveAmountInfo } from 'modules/trade/types' import * as styledEl from './styled' diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx deleted file mode 100644 index ac549413d6..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { INPUT_OUTPUT_EXPLANATION, MINIMUM_ETH_FLOW_DEADLINE_SECONDS } from '@cowprotocol/common-const' -import { Command } from '@cowprotocol/types' -import { HoverTooltip, RowFixed } from '@cowprotocol/ui' - -import { Trans } from '@lingui/macro' - -import { ClickableText } from 'modules/swap/pure/Row/RowSlippageContent' -import { StyledRowBetween, TextWrapper } from 'modules/swap/pure/Row/styled' -import { RowStyleProps } from 'modules/swap/pure/Row/typings' -import { StyledInfoIcon, TransactionText } from 'modules/swap/pure/styled' - -export function getNativeOrderDeadlineTooltip(symbols: (string | undefined)[] | undefined) { - return ( - - {symbols?.[0] || 'Native currency (e.g ETH)'} orders require a minimum transaction expiration time threshold of{' '} - {MINIMUM_ETH_FLOW_DEADLINE_SECONDS / 60} minutes to ensure the best swapping experience. -
    -
    - Orders not matched after the threshold time are automatically refunded. -
    - ) -} - -export function getNonNativeOrderDeadlineTooltip() { - return ( - - Your swap expires and will not execute if it is pending for longer than the selected duration. -
    -
    - {INPUT_OUTPUT_EXPLANATION} -
    - ) -} - -export interface RowDeadlineProps { - toggleSettings: Command - isEoaEthFlow: boolean - symbols?: (string | undefined)[] - displayDeadline: string - styleProps?: RowStyleProps - userDeadline: number - showSettingOnClick?: boolean - slippageLabel?: React.ReactNode - slippageTooltip?: React.ReactNode -} - -// TODO: RowDeadlineContent and RowSlippageContent are very similar. Refactor and extract base component? - -export function RowDeadlineContent(props: RowDeadlineProps) { - const { showSettingOnClick, toggleSettings, displayDeadline, isEoaEthFlow, symbols, styleProps } = props - const deadlineTooltipContent = isEoaEthFlow - ? getNativeOrderDeadlineTooltip(symbols) - : getNonNativeOrderDeadlineTooltip() - - return ( - - - - {showSettingOnClick ? ( - - - - ) : ( - - )} - - - - - - - {showSettingOnClick ? ( - {displayDeadline} - ) : ( - {displayDeadline} - )} - - - ) -} - -type DeadlineTextContentsProps = { isEoaEthFlow: boolean } - -function DeadlineTextContents({ isEoaEthFlow }: DeadlineTextContentsProps) { - return ( - - Transaction expiration - {isEoaEthFlow && (modified)} - - ) -} diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/types.ts b/apps/cowswap-frontend/src/modules/swap/pure/Row/types.ts deleted file mode 100644 index f922980cf1..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RowStyleProps { - fontWeight?: number - fontSize?: number - alignContentRight?: boolean -} diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/typings.ts b/apps/cowswap-frontend/src/modules/swap/pure/Row/typings.ts deleted file mode 100644 index 8e5cd4480d..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/typings.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface RowStyleProps { - fontWeight?: number - fontSize?: number -} 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 a6c14a7ca2..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, - recipientAddressOrName: null, 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 c4b93a9e3b..11c34bce1f 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx @@ -38,7 +38,6 @@ export interface SwapButtonsContext { hasEnoughWrappedBalanceForSwap: boolean swapInputError?: ReactNode onCurrencySelection: (field: Field, currency: Currency) => void - recipientAddressOrName: string | null widgetStandaloneMode?: boolean quoteDeadlineParams: QuoteDeadlineParams } diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/styled.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/styled.tsx index 5d1c870e35..71d69029e2 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/styled.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/styled.tsx @@ -23,7 +23,7 @@ export function FeesExceedFromAmountMessage() { - Costs exceed from amount + Sell amount is too small diff --git a/apps/cowswap-frontend/src/modules/swap/pure/styled.tsx b/apps/cowswap-frontend/src/modules/swap/pure/styled.tsx deleted file mode 100644 index dee470f06c..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/pure/styled.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { UI } from '@cowprotocol/ui' - -import { Info } from 'react-feather' -import styled from 'styled-components/macro' - -export const StyledInfoIcon = styled(Info)` - color: inherit; - opacity: 0.6; - line-height: 0; - vertical-align: middle; - transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; - - &:hover { - opacity: 1; - } -` - -export const TransactionText = styled.span` - display: flex; - gap: 3px; - cursor: pointer; - - > i { - font-style: normal; - } -` diff --git a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx index 5f3b3db0df..efaeef37e8 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx @@ -2,45 +2,23 @@ import React from 'react' import { genericPropsChecker } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { BundleTxApprovalBanner, BundleTxSafeWcBanner, BundleTxWrapBanner } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' -import styled from 'styled-components/macro' - -import { HighFeeWarning, HighSuggestedSlippageWarning } from 'legacy/components/SwapWarnings' import TradeGp from 'legacy/state/swap/TradeGp' import { CompatibilityIssuesWarning } from 'modules/trade/pure/CompatibilityIssuesWarning' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { TradeUrlParams } from 'modules/trade/types/TradeRawState' - -import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' +import { BundleTxWrapBanner, HighFeeWarning } from 'modules/tradeWidgetAddons' import { TwapSuggestionBanner } from './banners/TwapSuggestionBanner' export interface SwapWarningsTopProps { chainId: SupportedChainId trade: TradeGp | undefined - account: string | undefined - feeWarningAccepted: boolean - impactWarningAccepted: boolean - hideUnknownImpactWarning: boolean - showApprovalBundlingBanner: boolean - showWrapBundlingBanner: boolean - shouldZeroApprove: boolean - showSafeWcBundlingBanner: boolean showTwapSuggestionBanner: boolean - nativeCurrencySymbol: string - wrappedCurrencySymbol: string buyingFiatAmount: CurrencyAmount | null priceImpact: Percent | undefined tradeUrlParams: TradeUrlParams - isSuggestedSlippage: boolean | undefined - slippageBps: number | undefined - - setFeeWarningAccepted(cb: (state: boolean) => boolean): void - - setImpactWarningAccepted(cb: (state: boolean) => boolean): void } export interface SwapWarningsBottomProps { @@ -50,56 +28,13 @@ export interface SwapWarningsBottomProps { currencyOut: Currency | undefined } -const StyledNoImpactWarning = styled(NoImpactWarning)` - margin-bottom: 15px; -` - export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps) { - const { - chainId, - trade, - account, - feeWarningAccepted, - impactWarningAccepted, - hideUnknownImpactWarning, - showApprovalBundlingBanner, - showWrapBundlingBanner, - showSafeWcBundlingBanner, - showTwapSuggestionBanner, - nativeCurrencySymbol, - wrappedCurrencySymbol, - setFeeWarningAccepted, - setImpactWarningAccepted, - shouldZeroApprove, - buyingFiatAmount, - priceImpact, - tradeUrlParams, - isSuggestedSlippage, - slippageBps, - } = props + const { chainId, trade, showTwapSuggestionBanner, buyingFiatAmount, priceImpact, tradeUrlParams } = props return ( <> - {shouldZeroApprove && } - setFeeWarningAccepted((state) => !state) : undefined} - /> - - {!hideUnknownImpactWarning && ( - setImpactWarningAccepted((state) => !state)} - /> - )} - {showApprovalBundlingBanner && } - {showWrapBundlingBanner && ( - - )} - {showSafeWcBundlingBanner && ( - - )} + + {showTwapSuggestionBanner && ( Promise, @@ -25,20 +27,14 @@ export async function ethFlow( tradeConfirmActions, swapFlowAnalyticsContext, context, - contract, callbacks, - appDataInfo, - dispatch, orderParams: orderParamsOriginal, - checkEthFlowOrderExists, - addInFlightOrderId, - quote, typedHooks, - } = ethFlowContext - const { - chainId, - trade: { inputAmount, outputAmount, fee }, - } = context + } = tradeContext + const { contract, appData, uploadAppData, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, quote } = + ethFlowContext + + const { chainId, inputAmount, outputAmount } = context const tradeAmounts = { inputAmount, outputAmount } const { account, recipientAddressOrName, kind } = orderParamsOriginal @@ -61,7 +57,7 @@ export async function ethFlow( // Do not proceed if fee is expired if ( isQuoteExpired({ - expirationDate: fee.expirationDate, + expirationDate: quote?.fee?.expirationDate, deadlineParams: { validFor: quote?.validFor, quoteValidTo: quote?.quoteValidTo, @@ -69,7 +65,7 @@ export async function ethFlow( }, }) ) { - reportPlaceOrderWithExpiredQuote({ ...orderParamsOriginal, fee }) + reportPlaceOrderWithExpiredQuote({ ...orderParamsOriginal, fee: quote?.fee }) throw new Error('Quote expired. Please refresh.') } @@ -105,13 +101,13 @@ export async function ethFlow( order, isSafeWallet: orderParams.isSafeWallet, }, - dispatch, + callbacks.dispatch, ) // TODO: maybe move this into addPendingOrderStep? - ethFlowContext.addTransaction({ hash: txReceipt.hash, ethFlow: { orderId: order.id, subType: 'creation' } }) + addTransaction({ hash: txReceipt.hash, ethFlow: { orderId: order.id, subType: 'creation' } }) logTradeFlow('ETH FLOW', 'STEP 6: add app data to upload queue') - callbacks.uploadAppData({ chainId: context.chainId, orderId, appData: appDataInfo }) + uploadAppData({ chainId: context.chainId, orderId, appData }) logTradeFlow('ETH FLOW', 'STEP 7: show UI of the successfully sent transaction', orderId) tradeConfirmActions.onSuccess(orderId) diff --git a/apps/cowswap-frontend/src/modules/swap/services/types.ts b/apps/cowswap-frontend/src/modules/swap/services/types.ts index e48dd370f5..cc24db27a4 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/types.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/types.ts @@ -1,78 +1,18 @@ -import { CoWSwapEthFlow, Erc20, GPv2Settlement, Weth } from '@cowprotocol/abis' -import { Command } from '@cowprotocol/types' -import { Web3Provider } from '@ethersproject/providers' -import SafeAppsSDK from '@safe-global/safe-apps-sdk' -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { CoWSwapEthFlow } from '@cowprotocol/abis' -import { AppDispatch } from 'legacy/state' import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks' -import { AddOrderCallback } from 'legacy/state/orders/hooks' import type { QuoteInformationObject } from 'legacy/state/price/reducer' -import TradeGp from 'legacy/state/swap/TradeGp' -import { PostOrderParams } from 'legacy/utils/trade' -import { AppDataInfo, TypedAppDataHooks, UploadAppDataParams } from 'modules/appData' -import { GeneratePermitHook, IsTokenPermittableResult, useGetCachedPermit } from 'modules/permit' -import { TradeConfirmActions } from 'modules/trade' -import { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics' +import { AppDataInfo, UploadAppDataParams } from 'modules/appData' import { EthFlowOrderExistsCallback } from '../hooks/useCheckEthFlowOrderExists' -import { FlowType } from '../types/flowContext' -export interface BaseFlowContext { - context: { - chainId: number - trade: TradeGp - inputAmountWithSlippage: CurrencyAmount - outputAmountWithSlippage: CurrencyAmount - flowType: FlowType - } - flags: { - allowsOffchainSigning: boolean - } - callbacks: { - closeModals: Command - addOrderCallback: AddOrderCallback - uploadAppData: (params: UploadAppDataParams) => void - getCachedPermit: ReturnType - } - sellTokenContract: Erc20 | null - dispatch: AppDispatch - swapFlowAnalyticsContext: TradeFlowAnalyticsContext - orderParams: PostOrderParams - appDataInfo: AppDataInfo - tradeConfirmActions: TradeConfirmActions - quote: QuoteInformationObject | undefined - typedHooks?: TypedAppDataHooks -} - -export type SwapFlowContext = BaseFlowContext & { - contract: GPv2Settlement - permitInfo: IsTokenPermittableResult - generatePermitHook: GeneratePermitHook - typedHooks?: TypedAppDataHooks -} - -export type EthFlowContext = BaseFlowContext & { +export type EthFlowContext = { contract: CoWSwapEthFlow addTransaction: ReturnType checkEthFlowOrderExists: EthFlowOrderExistsCallback addInFlightOrderId: (orderId: string) => void quote: QuoteInformationObject | undefined -} - -export type BaseSafeFlowContext = BaseFlowContext & { - settlementContract: GPv2Settlement - spender: string - safeAppsSdk: SafeAppsSDK - provider: Web3Provider -} - -export type SafeBundleApprovalFlowContext = BaseSafeFlowContext & { - erc20Contract: Erc20 -} - -export type SafeBundleEthFlowContext = BaseSafeFlowContext & { - wrappedNativeContract: Weth - needsApproval: boolean + uploadAppData: (params: UploadAppDataParams) => void + appData: AppDataInfo } diff --git a/apps/cowswap-frontend/src/modules/swap/state/EthFlow/updaters/EthFlowDeadlineUpdater.tsx b/apps/cowswap-frontend/src/modules/swap/state/EthFlow/updaters/EthFlowDeadlineUpdater.tsx index 21ba267bc1..38b095c69d 100644 --- a/apps/cowswap-frontend/src/modules/swap/state/EthFlow/updaters/EthFlowDeadlineUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/swap/state/EthFlow/updaters/EthFlowDeadlineUpdater.tsx @@ -5,7 +5,7 @@ import { loadJsonFromLocalStorage, setJsonToLocalStorage } from '@cowprotocol/co import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' +import { useIsEoaEthFlow } from 'modules/trade' import { DeadlineSettings } from './types' diff --git a/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts b/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts deleted file mode 100644 index 5295aaffde..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { atom } from 'jotai' - -import { BaseFlowContextSource } from '../types/flowContext' - -export const baseFlowContextSourceAtom = atom(null) diff --git a/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts b/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts index c15bbd9a72..45b0f95097 100644 --- a/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts +++ b/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts @@ -6,13 +6,13 @@ import { OrderKind } from '@cowprotocol/cow-sdk' import { Field } from 'legacy/state/types' import { TradeType } from 'modules/trade' +import { useTradeSlippage } from 'modules/tradeSlippage' import { useTradeUsdAmounts } from 'modules/usdAmount' import { useSafeMemoObject } from 'common/hooks/useSafeMemo' import { SwapDerivedState, swapDerivedStateAtom } from './swapDerivedStateAtom' -import { useSwapSlippage } from '../hooks/useSwapSlippage' import { useDerivedSwapInfo, useSwapState } from '../hooks/useSwapState' export function useSwapDerivedState(): SwapDerivedState { @@ -23,7 +23,7 @@ export function useFillSwapDerivedState() { const { independentField, recipient, recipientAddress } = useSwapState() const { trade, currencyBalances, currencies, slippageAdjustedSellAmount, slippageAdjustedBuyAmount, parsedAmount } = useDerivedSwapInfo() - const slippage = useSwapSlippage() + const slippage = useTradeSlippage() const isSellTrade = independentField === Field.INPUT const inputCurrency = currencies.INPUT || null diff --git a/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts b/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts deleted file mode 100644 index 79b9fb126d..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Erc20, Weth } from '@cowprotocol/abis' -import type { SupportedChainId } from '@cowprotocol/cow-sdk' -import type { Command } from '@cowprotocol/types' -import type { GnosisSafeInfo } from '@cowprotocol/wallet' -import type { Web3Provider } from '@ethersproject/providers' -import type { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' - -import type { AppDispatch } from 'legacy/state' -import type { AddOrderCallback } from 'legacy/state/orders/hooks' -import type { QuoteInformationObject } from 'legacy/state/price/reducer' -import type TradeGp from 'legacy/state/swap/TradeGp' - -import type { useGetCachedPermit } from 'modules/permit' -import type { TradeConfirmActions } from 'modules/trade' - -import type { AppDataInfo, TypedAppDataHooks, UploadAppDataParams } from '../../appData' - -export enum FlowType { - REGULAR = 'REGULAR', - EOA_ETH_FLOW = 'EOA_ETH_FLOW', - SAFE_BUNDLE_APPROVAL = 'SAFE_BUNDLE_APPROVAL', - SAFE_BUNDLE_ETH = 'SAFE_BUNDLE_ETH', -} - -export interface BaseFlowContextSource { - chainId: SupportedChainId - account: string | undefined - sellTokenContract: Erc20 | null - provider: Web3Provider | undefined - trade: TradeGp | undefined - appData: AppDataInfo | null - wethContract: Weth | null - inputAmountWithSlippage: CurrencyAmount | undefined - outputAmountWithSlippage: CurrencyAmount | undefined - gnosisSafeInfo: GnosisSafeInfo | undefined - recipient: string | null - recipientAddressOrName: string | null - deadline: number - ensRecipientAddress: string | null - allowsOffchainSigning: boolean - flowType: FlowType - closeModals: Command - uploadAppData: (update: UploadAppDataParams) => void - addOrderCallback: AddOrderCallback - dispatch: AppDispatch - allowedSlippage: Percent - tradeConfirmActions: TradeConfirmActions - getCachedPermit: ReturnType - quote: QuoteInformationObject | undefined - typedHooks: TypedAppDataHooks | undefined -} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx b/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx deleted file mode 100644 index 12857e8c11..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useSetAtom } from 'jotai' -import { useEffect } from 'react' - -import { getAddress } from '@cowprotocol/common-utils' -import { useENSAddress } from '@cowprotocol/ens' -import { useGnosisSafeInfo, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' -import { useWalletProvider } from '@cowprotocol/wallet-provider' - -import { useDispatch } from 'react-redux' - -import { AppDispatch } from 'legacy/state' -import { useCloseModals } from 'legacy/state/application/hooks' -import { useAddPendingOrder } from 'legacy/state/orders/hooks' -import { useGetQuoteAndStatus } from 'legacy/state/price/hooks' -import { useUserTransactionTTL } from 'legacy/state/user/hooks' - -import { useAppData, useAppDataHooks, useUploadAppData } from 'modules/appData' -import { useGetCachedPermit } from 'modules/permit' -import { useTradeConfirmActions } from 'modules/trade' - -import { useTokenContract, useWETHContract } from 'common/hooks/useContract' -import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' -import { useSafeMemo } from 'common/hooks/useSafeMemo' - -import { useSwapAmountsWithSlippage } from '../hooks/useFlowContext' -import { useIsEoaEthFlow } from '../hooks/useIsEoaEthFlow' -import { useIsSafeEthFlow } from '../hooks/useIsSafeEthFlow' -import { useSwapSlippage } from '../hooks/useSwapSlippage' -import { useDerivedSwapInfo, useSwapState } from '../hooks/useSwapState' -import { baseFlowContextSourceAtom } from '../state/baseFlowContextSourceAtom' -import { FlowType } from '../types/flowContext' - -export function BaseFlowContextUpdater() { - const setBaseFlowContextSource = useSetAtom(baseFlowContextSourceAtom) - const provider = useWalletProvider() - const { account, chainId } = useWalletInfo() - const { allowsOffchainSigning } = useWalletDetails() - const gnosisSafeInfo = useGnosisSafeInfo() - const { recipient } = useSwapState() - const slippage = useSwapSlippage() - const { trade, currenciesIds } = useDerivedSwapInfo() - const { quote } = useGetQuoteAndStatus({ - token: currenciesIds.INPUT, - chainId, - }) - - const appData = useAppData() - const typedHooks = useAppDataHooks() - const closeModals = useCloseModals() - const uploadAppData = useUploadAppData() - const addOrderCallback = useAddPendingOrder() - const dispatch = useDispatch() - const tradeConfirmActions = useTradeConfirmActions() - - const { address: ensRecipientAddress } = useENSAddress(recipient) - const recipientAddressOrName = recipient || ensRecipientAddress - const [deadline] = useUserTransactionTTL() - const wethContract = useWETHContract() - const isEoaEthFlow = useIsEoaEthFlow() - const isSafeEthFlow = useIsSafeEthFlow() - const getCachedPermit = useGetCachedPermit() - - const [inputAmountWithSlippage, outputAmountWithSlippage] = useSwapAmountsWithSlippage() - const sellTokenContract = useTokenContract(getAddress(inputAmountWithSlippage?.currency) || undefined, true) - - const isSafeBundle = useIsSafeApprovalBundle(inputAmountWithSlippage) - const flowType = getFlowType(isSafeBundle, isEoaEthFlow, isSafeEthFlow) - - const source = useSafeMemo( - () => ({ - chainId, - account, - sellTokenContract, - provider, - trade, - appData, - wethContract, - inputAmountWithSlippage, - outputAmountWithSlippage, - gnosisSafeInfo, - recipient, - recipientAddressOrName, - deadline, - ensRecipientAddress, - allowsOffchainSigning, - uploadAppData, - flowType, - closeModals, - addOrderCallback, - dispatch, - allowedSlippage: slippage, - tradeConfirmActions, - getCachedPermit, - quote, - typedHooks, - }), - [ - chainId, - account, - sellTokenContract, - provider, - trade, - appData, - wethContract, - inputAmountWithSlippage, - outputAmountWithSlippage, - gnosisSafeInfo, - recipient, - recipientAddressOrName, - deadline, - ensRecipientAddress, - allowsOffchainSigning, - uploadAppData, - flowType, - closeModals, - addOrderCallback, - dispatch, - slippage, - tradeConfirmActions, - getCachedPermit, - quote, - typedHooks, - ], - ) - - useEffect(() => { - setBaseFlowContextSource(source) - }, [source, setBaseFlowContextSource]) - - return null -} - -function getFlowType(isSafeBundle: boolean, isEoaEthFlow: boolean, isSafeEthFlow: boolean): FlowType { - if (isSafeEthFlow) { - // Takes precedence over bundle approval - return FlowType.SAFE_BUNDLE_ETH - } - if (isSafeBundle) { - // Takes precedence over eth flow - return FlowType.SAFE_BUNDLE_APPROVAL - } - if (isEoaEthFlow) { - // Takes precedence over regular flow - return FlowType.EOA_ETH_FLOW - } - return FlowType.REGULAR -} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/NoImpactWarning/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/NoImpactWarning/index.tsx new file mode 100644 index 0000000000..cd69f633ba --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/containers/NoImpactWarning/index.tsx @@ -0,0 +1,73 @@ +import { atom, useAtom, useAtomValue } from 'jotai' +import { useEffect } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useTradePriceImpact } from 'modules/trade' +import { TradeWarning, TradeWarningType } from 'modules/trade/pure/TradeWarning' +import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation' +import { useTradeQuote } from 'modules/tradeQuote' + +const noImpactWarningAcceptedAtom = atom(false) + +const NoImpactWarningMessage = ( +
    + + We are unable to calculate the price impact for this order. +
    +
    + You may still move forward but{' '} + please review carefully that the receive amounts are what you expect. +
    +
    +) + +export function useIsNoImpactWarningAccepted() { + return useAtomValue(noImpactWarningAcceptedAtom) +} + +export interface NoImpactWarningProps { + withoutAccepting?: boolean + className?: string +} + +export function NoImpactWarning(props: NoImpactWarningProps) { + const { withoutAccepting, className } = props + + const [isAccepted, setIsAccepted] = useAtom(noImpactWarningAcceptedAtom) + + const { account } = useWalletInfo() + const priceImpactParams = useTradePriceImpact() + const primaryFormValidation = useGetTradeFormValidation() + const tradeQuote = useTradeQuote() + + const canTrade = + (primaryFormValidation === null || primaryFormValidation === TradeFormValidation.ApproveAndSwap) && + !tradeQuote.error + + const showPriceImpactWarning = canTrade && !!account && !priceImpactParams.loading && !priceImpactParams.priceImpact + + const acceptCallback = () => setIsAccepted((state) => !state) + + useEffect(() => { + setIsAccepted(!showPriceImpactWarning) + }, [showPriceImpactWarning, setIsAccepted]) + + if (!showPriceImpactWarning) return null + + return ( + + Price impact unknown - trade carefully + + } + /> + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/index.tsx index b049e09b4a..0560ff91f0 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeBasicConfirmDetails/index.tsx @@ -1,12 +1,12 @@ -import { Dispatch, ReactNode, SetStateAction, useMemo } from 'react' +import { ReactNode, useMemo, useState } from 'react' import { FractionUtils } from '@cowprotocol/common-utils' import { PercentDisplay } from '@cowprotocol/ui' -import { CowSwapWidgetAppParams } from '@cowprotocol/widget-lib' import { Percent, Price } from '@uniswap/sdk-core' import { Nullish } from 'types' +import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useUsdAmount } from 'modules/usdAmount' import { RateInfoParams } from 'common/pure/RateInfo' @@ -24,9 +24,7 @@ import { TradeFeesAndCosts } from '../TradeFeesAndCosts' type Props = { receiveAmountInfo: ReceiveAmountInfo rateInfoParams: RateInfoParams - isInvertedState: [boolean, Dispatch>] slippage: Percent - widgetParams: Partial labelsAndTooltips?: LabelsAndTooltips children?: ReactNode recipient?: Nullish @@ -53,11 +51,9 @@ type LabelsAndTooltips = { export function TradeBasicConfirmDetails(props: Props) { const { rateInfoParams, - isInvertedState, slippage, labelsAndTooltips, receiveAmountInfo, - widgetParams, hideLimitPrice, hideUsdValues, withTimelineDot = true, @@ -66,6 +62,8 @@ export function TradeBasicConfirmDetails(props: Props) { recipient, account, } = props + const isInvertedState = useState(false) + const widgetParams = useInjectedWidgetParams() const { amountAfterFees, amountAfterSlippage } = getOrderTypeReceiveAmounts(receiveAmountInfo) const { networkCostsSuffix, networkCostsTooltipSuffix } = labelsAndTooltips || {} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.cosmos.tsx index f4e50e9ed7..428a89e636 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.cosmos.tsx @@ -70,7 +70,7 @@ function Custom({ stateValue }: { stateValue: string }) { return ( - Some content + {() => Some content} ) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWarnings/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWarnings/index.tsx new file mode 100644 index 0000000000..a3d98716b4 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWarnings/index.tsx @@ -0,0 +1,65 @@ +import React from 'react' + +import { CowSwapSafeAppLink, InlineBanner } from '@cowprotocol/ui' +import { useIsSafeViaWc } from '@cowprotocol/wallet' + +import { useInjectedWidgetParams } from 'modules/injectedWidget' +import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation' +import { HighSuggestedSlippageWarning } from 'modules/tradeSlippage' +import { useShouldZeroApprove } from 'modules/zeroApproval' + +import { useReceiveAmountInfo } from '../../hooks/useReceiveAmountInfo' +import { ZeroApprovalWarning } from '../../pure/ZeroApprovalWarning' +import { NoImpactWarning } from '../NoImpactWarning' + +interface TradeWarningsProps { + isTradePriceUpdating: boolean + enableSmartSlippage?: boolean +} + +export function TradeWarnings({ isTradePriceUpdating, enableSmartSlippage }: TradeWarningsProps) { + const { banners: widgetBanners } = useInjectedWidgetParams() + const primaryFormValidation = useGetTradeFormValidation() + const receiveAmountInfo = useReceiveAmountInfo() + const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount + const shouldZeroApprove = useShouldZeroApprove(inputAmountWithSlippage) + const isSafeViaWc = useIsSafeViaWc() + + const showBundleTxApprovalBanner = primaryFormValidation === TradeFormValidation.ApproveAndSwap + const showSafeWcBundlingBanner = + isSafeViaWc && primaryFormValidation === TradeFormValidation.ApproveRequired && !widgetBanners?.hideSafeWebAppBanner + + return ( + <> + {shouldZeroApprove && } + + {showBundleTxApprovalBanner && } + {showSafeWcBundlingBanner && } + {enableSmartSlippage && } + + ) +} + +function BundleTxApprovalBanner() { + return ( + + Token approval bundling +

    + For your convenience, token approval and order placement will be bundled into a single transaction, streamlining + your experience! +

    +
    + ) +} + +function BundleTxSafeWcBanner() { + return ( + + Use Safe web app +

    + Use the Safe web app for streamlined trading: token approval and orders bundled in one go! Only available in the{' '} + +

    +
    + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index 100517aa7a..7af3c59f62 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import ICON_ORDERS from '@cowprotocol/assets/svg/orders.svg' import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg' @@ -35,6 +35,7 @@ import { useTradeStateFromUrl } from '../../hooks/setupTradeState/useTradeStateF import { useIsWrapOrUnwrap } from '../../hooks/useIsWrapOrUnwrap' import { useTradeTypeInfo } from '../../hooks/useTradeTypeInfo' import { TradeType } from '../../types' +import { TradeWarnings } from '../TradeWarnings' import { TradeWidgetLinks } from '../TradeWidgetLinks' import { WrapFlowActionButton } from '../WrapFlowActionButton' @@ -59,7 +60,16 @@ export function TradeWidgetForm(props: TradeWidgetProps) { const { settingsWidget, lockScreen, topContent, middleContent, bottomContent, outerContent } = slots const { onCurrencySelection, onUserInput, onSwitchTokens, onChangeRecipient } = actions - const { compactView, showRecipient, isTradePriceUpdating, isEoaEthFlow = false, priceImpact, recipient } = params + const { + compactView, + showRecipient, + isTradePriceUpdating, + isEoaEthFlow = false, + priceImpact, + recipient, + hideTradeWarnings, + enableSmartSlippage, + } = params const inputCurrencyInfo = useMemo( () => (isWrapOrUnwrap ? { ...props.inputCurrencyInfo, receiveAmountInfo: null } : props.inputCurrencyInfo), @@ -200,7 +210,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { isCollapsed={compactView} hasSeparatorLine={!compactView} onSwitchTokens={isChainIdUnsupported ? () => void 0 : throttledOnSwitchTokens} - isLoading={isTradePriceUpdating} + isLoading={Boolean(inputCurrencyInfo.currency && outputCurrencyInfo.currency && isTradePriceUpdating)} disabled={isAlternativeOrderModalVisible} /> @@ -221,7 +231,18 @@ export function TradeWidgetForm(props: TradeWidgetProps) { {withRecipient && } - {isWrapOrUnwrap ? : bottomContent} + {isWrapOrUnwrap ? ( + + ) : ( + bottomContent?.( + hideTradeWarnings ? null : ( + + ), + ) + )} )} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx index f7623ca734..ae2243872b 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx @@ -5,6 +5,7 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { TradeFormValidationUpdater } from 'modules/tradeFormValidation' import { TradeQuoteState, TradeQuoteUpdater, useUpdateTradeQuote } from 'modules/tradeQuote' +import { SmartSlippageUpdater } from 'modules/tradeSlippage' import { usePriorityTokenAddresses } from '../../hooks/usePriorityTokenAddresses' import { useResetRecipient } from '../../hooks/useResetRecipient' @@ -16,6 +17,7 @@ import { RecipientAddressUpdater } from '../../updaters/RecipientAddressUpdater' interface TradeWidgetUpdatersProps { disableQuotePolling: boolean disableNativeSelling: boolean + enableSmartSlippage?: boolean children: ReactNode tradeQuoteStateOverride?: TradeQuoteState | null onChangeRecipient: (recipient: string | null) => void @@ -25,6 +27,7 @@ export function TradeWidgetUpdaters({ disableQuotePolling, disableNativeSelling, tradeQuoteStateOverride, + enableSmartSlippage, onChangeRecipient, children, }: TradeWidgetUpdatersProps) { @@ -49,6 +52,7 @@ export function TradeWidgetUpdaters({ + {enableSmartSlippage && } {disableNativeSelling && } {children} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx index fc9935a4c9..764652582f 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx @@ -8,7 +8,12 @@ export const TradeWidgetContainer = styledEl.Container export function TradeWidget(props: TradeWidgetProps) { const { id, slots, params, confirmModal, genericModal } = props - const { disableQuotePolling = false, disableNativeSelling = false, tradeQuoteStateOverride } = params + const { + disableQuotePolling = false, + disableNativeSelling = false, + tradeQuoteStateOverride, + enableSmartSlippage, + } = params const modals = TradeWidgetModals(confirmModal, genericModal) return ( @@ -18,6 +23,7 @@ export function TradeWidget(props: TradeWidgetProps) { disableQuotePolling={disableQuotePolling} disableNativeSelling={disableNativeSelling} tradeQuoteStateOverride={tradeQuoteStateOverride} + enableSmartSlippage={enableSmartSlippage} onChangeRecipient={props.actions.onChangeRecipient} > {slots.updaters} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts index 396db9923c..d0e0e8e3d8 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts @@ -26,6 +26,8 @@ interface TradeWidgetParams { disableQuotePolling?: boolean disableNativeSelling?: boolean disablePriceImpact?: boolean + hideTradeWarnings?: boolean + enableSmartSlippage?: boolean } export interface TradeWidgetSlots { @@ -33,7 +35,7 @@ export interface TradeWidgetSlots { lockScreen?: ReactNode topContent?: ReactNode middleContent?: ReactNode - bottomContent?: ReactNode + bottomContent?(warnings: ReactNode | null): ReactNode outerContent?: ReactNode updaters?: ReactNode } diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx index 65bf8dda31..c0874b0fc3 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useCallback } from 'react' +import { useCallback, useMemo, useState } from 'react' import { Command } from '@cowprotocol/types' import { Badge } from '@cowprotocol/ui' @@ -7,7 +7,7 @@ import type { TradeType } from '@cowprotocol/widget-lib' import { Trans } from '@lingui/macro' import IMAGE_CARET from 'assets/icon/caret.svg' import SVG from 'react-inlinesvg' -import { matchPath, useLocation } from 'react-router-dom' +import { useLocation } from 'react-router-dom' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { ModalHeader } from 'modules/tokensList/pure/ModalHeader' @@ -18,7 +18,9 @@ import { useMenuItems } from 'common/hooks/useMenuItems' import * as styledEl from './styled' import { useTradeRouteContext } from '../../hooks/useTradeRouteContext' -import { parameterizeTradeRoute } from '../../utils/parameterizeTradeRoute' +import { useGetTradeStateByRoute } from '../../hooks/useTradeState' +import { getDefaultTradeRawState, TradeUrlParams } from '../../types/TradeRawState' +import { addChainIdToRoute, parameterizeTradeRoute } from '../../utils/parameterizeTradeRoute' interface MenuItemConfig { route: RoutesValues @@ -30,6 +32,7 @@ const TRADE_TYPE_TO_ROUTE: Record = { swap: Routes.SWAP, limit: Routes.LIMIT_ORDER, advanced: Routes.ADVANCED_ORDERS, + yield: Routes.YIELD, } interface TradeWidgetLinksProps { @@ -42,6 +45,7 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) const [isDropdownVisible, setDropdownVisible] = useState(false) const { enabledTradeTypes } = useInjectedWidgetParams() const menuItems = useMenuItems() + const getTradeStateByType = useGetTradeStateByRoute() const handleMenuItemClick = useCallback((_item?: MenuItemConfig): void => { setDropdownVisible(false) @@ -57,8 +61,28 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) const menuItemsElements: JSX.Element[] = useMemo(() => { return enabledItems.map((item) => { - const routePath = parameterizeTradeRoute(tradeContext, item.route, true) - const isActive = !!matchPath(location.pathname, routePath.split('?')[0]) + const isItemYield = item.route === Routes.YIELD + const chainId = tradeContext.chainId + + const isCurrentPathYield = location.pathname.startsWith(addChainIdToRoute(Routes.YIELD, chainId)) + const itemTradeState = getTradeStateByType(item.route) + + const routePath = isItemYield + ? addChainIdToRoute(item.route, chainId) + : parameterizeTradeRoute( + isCurrentPathYield + ? ({ + chainId, + inputCurrencyId: + itemTradeState.inputCurrencyId || (chainId && getDefaultTradeRawState(+chainId).inputCurrencyId), + outputCurrencyId: itemTradeState.outputCurrencyId, + } as TradeUrlParams) + : tradeContext, + item.route, + !isCurrentPathYield, + ) + + const isActive = location.pathname.startsWith(routePath.split('?')[0]) return ( ) }) - }, [isDropdown, isDropdownVisible, enabledItems, tradeContext, location.pathname, handleMenuItemClick]) + }, [ + isDropdown, + isDropdownVisible, + enabledItems, + tradeContext, + location.pathname, + handleMenuItemClick, + getTradeStateByType, + ]) const singleMenuItem = menuItemsElements.length === 1 diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeStateFromUrl.ts b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeStateFromUrl.ts index f6a38792dd..5acb220f60 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeStateFromUrl.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeStateFromUrl.ts @@ -1,11 +1,12 @@ import { useSetAtom } from 'jotai' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { useLocation, useParams } from 'react-router-dom' import { tradeStateFromUrlAtom } from 'modules/trade/state/tradeStateFromUrlAtom' import { TradeRawState } from '../../types/TradeRawState' +import { useTradeState } from '../useTradeState' /** * Updater to fetch trade state from URL params and query, and store it on jotai state @@ -18,6 +19,9 @@ export function useSetupTradeStateFromUrl(): null { const location = useLocation() const stringifiedParams = JSON.stringify(params) const setState = useSetAtom(tradeStateFromUrlAtom) + const { state } = useTradeState() + const tradeStateRef = useRef(state) + tradeStateRef.current = state useEffect(() => { const searchParams = new URLSearchParams(location.search) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsEoaEthFlow.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useIsEoaEthFlow.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsEoaEthFlow.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useIsEoaEthFlow.ts diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSafeEthFlow.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useIsSafeEthFlow.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsSafeEthFlow.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useIsSafeEthFlow.ts diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSwapEth.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useIsSwapEth.ts similarity index 53% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsSwapEth.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useIsSwapEth.ts index 30b3a94d08..b73d67afca 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSwapEth.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useIsSwapEth.ts @@ -1,5 +1,5 @@ -import { useIsNativeIn } from 'modules/trade/hooks/useIsNativeInOrOut' -import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' +import { useIsNativeIn } from './useIsNativeInOrOut' +import { useIsWrapOrUnwrap } from './useIsWrapOrUnwrap' export function useIsSwapEth(): boolean { const isNativeIn = useIsNativeIn() diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useNavigateToNewOrderCallback.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateToNewOrderCallback.ts similarity index 83% rename from apps/cowswap-frontend/src/modules/swap/hooks/useNavigateToNewOrderCallback.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useNavigateToNewOrderCallback.ts index 43ee8fe36c..3e35a29f40 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useNavigateToNewOrderCallback.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateToNewOrderCallback.ts @@ -9,10 +9,10 @@ import { Order } from 'legacy/state/orders/actions' import { Routes } from 'common/constants/routes' import { useNavigate } from 'common/hooks/useNavigate' -import { parameterizeTradeRoute } from '../../trade' -import { TradeUrlParams } from '../../trade/types/TradeRawState' +import { TradeUrlParams } from '../types/TradeRawState' +import { parameterizeTradeRoute } from '../utils/parameterizeTradeRoute' -export type NavigateToNewOrderCallback = (chainId: SupportedChainId, order?: Order, callback?: Command) => () => void +type NavigateToNewOrderCallback = (chainId: SupportedChainId, order?: Order, callback?: Command) => () => void export function useNavigateToNewOrderCallback(): NavigateToNewOrderCallback { const navigate = useNavigate() @@ -41,6 +41,6 @@ export function useNavigateToNewOrderCallback(): NavigateToNewOrderCallback { callback?.() } }, - [navigate] + [navigate], ) } diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts index 0d3b539695..38d74eae3b 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts @@ -36,6 +36,7 @@ const TradeTypeToUiOrderType: Record = { [TradeType.SWAP]: UiOrderType.SWAP, [TradeType.LIMIT_ORDER]: UiOrderType.LIMIT, [TradeType.ADVANCED_ORDERS]: UiOrderType.TWAP, + [TradeType.YIELD]: UiOrderType.YIELD, } function getTradeParamsEventPayload(tradeType: TradeType, state: TradeDerivedState): OnTradeParamsPayload { diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useOrderSubmittedContent.tsx b/apps/cowswap-frontend/src/modules/trade/hooks/useOrderSubmittedContent.tsx new file mode 100644 index 0000000000..bf7c5b6d15 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useOrderSubmittedContent.tsx @@ -0,0 +1,35 @@ +import { useCallback } from 'react' + +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Command } from '@cowprotocol/types' + +import { useOrder } from 'legacy/state/orders/hooks' + +import { useOrderProgressBarV2Props } from 'common/hooks/orderProgressBarV2' +import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent' + +import { useNavigateToNewOrderCallback } from './useNavigateToNewOrderCallback' +import { useTradeConfirmState } from './useTradeConfirmState' + +export function useOrderSubmittedContent(chainId: SupportedChainId) { + const { transactionHash } = useTradeConfirmState() + const order = useOrder({ chainId, id: transactionHash || undefined }) + + const orderProgressBarV2Props = useOrderProgressBarV2Props(chainId, order) + + const navigateToNewOrderCallback = useNavigateToNewOrderCallback() + + return useCallback( + (onDismiss: Command) => ( + + ), + [chainId, transactionHash, orderProgressBarV2Props, navigateToNewOrderCallback], + ) +} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useShouldPayGas.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useShouldPayGas.ts similarity index 82% rename from apps/cowswap-frontend/src/modules/swap/hooks/useShouldPayGas.ts rename to apps/cowswap-frontend/src/modules/trade/hooks/useShouldPayGas.ts index bb0f2fc0b6..eb9ca97852 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useShouldPayGas.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useShouldPayGas.ts @@ -1,6 +1,6 @@ import { useWalletDetails } from '@cowprotocol/wallet' -import { useIsEoaEthFlow } from './useIsEoaEthFlow' +import { useIsEoaEthFlow } from 'modules/trade' export function useShouldPayGas() { const { allowsOffchainSigning } = useWalletDetails() diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeState.ts index 213cd9002f..60631701d7 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeState.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { useAdvancedOrdersRawState, @@ -7,6 +7,9 @@ import { import { useLimitOrdersRawState, useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' import { useSwapRawState, useUpdateSwapRawState } from 'modules/swap/hooks/useSwapRawState' import { ExtendedTradeRawState, TradeRawState } from 'modules/trade/types/TradeRawState' +import { useUpdateYieldRawState, useYieldRawState } from 'modules/yield' + +import { Routes, RoutesValues } from 'common/constants/routes' import { useTradeTypeInfoFromUrl } from './useTradeTypeInfoFromUrl' @@ -29,6 +32,9 @@ export function useTradeState(): { const swapTradeState = useSwapRawState() const updateSwapState = useUpdateSwapRawState() + const yieldRawState = useYieldRawState() + const updateYieldRawState = useUpdateYieldRawState() + return useMemo(() => { if (!tradeTypeInfo) return EMPTY_TRADE_STATE @@ -46,6 +52,13 @@ export function useTradeState(): { } } + if (tradeTypeInfo.tradeType === TradeType.YIELD) { + return { + state: yieldRawState, + updateState: updateYieldRawState, + } + } + return { state: limitOrdersState, updateState: updateLimitOrdersState, @@ -60,7 +73,27 @@ export function useTradeState(): { JSON.stringify(advancedOrdersState), // eslint-disable-next-line react-hooks/exhaustive-deps JSON.stringify(swapTradeState), + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(yieldRawState), updateSwapState, updateLimitOrdersState, + updateYieldRawState, ]) } + +export function useGetTradeStateByRoute() { + const limitOrdersState = useLimitOrdersRawState() + const advancedOrdersState = useAdvancedOrdersRawState() + const swapTradeState = useSwapRawState() + const yieldRawState = useYieldRawState() + + return useCallback( + (route: RoutesValues) => { + if (route === Routes.SWAP || route === Routes.HOOKS) return swapTradeState + if (route === Routes.ABOUT) return advancedOrdersState + if (route === Routes.YIELD) return yieldRawState + return limitOrdersState + }, + [swapTradeState, advancedOrdersState, yieldRawState, limitOrdersState], + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeTypeInfoFromUrl.tsx b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeTypeInfoFromUrl.tsx index c5ee725884..0774836eae 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeTypeInfoFromUrl.tsx +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeTypeInfoFromUrl.tsx @@ -12,15 +12,17 @@ export function useTradeTypeInfoFromUrl(): TradeTypeInfo | null { const hooksMatch = !!useMatchTradeRoute('swap/hooks') const limitOrderMatch = !!useMatchTradeRoute('limit') const advancedOrdersMatch = !!useMatchTradeRoute('advanced') + const yieldMatch = !!useMatchTradeRoute('yield') return useMemo(() => { if (hooksMatch) return { tradeType: TradeType.SWAP, route: Routes.HOOKS } if (swapMatch) return { tradeType: TradeType.SWAP, route: Routes.SWAP } if (limitOrderMatch) return { tradeType: TradeType.LIMIT_ORDER, route: Routes.LIMIT_ORDER } if (advancedOrdersMatch) return { tradeType: TradeType.ADVANCED_ORDERS, route: Routes.ADVANCED_ORDERS } + if (yieldMatch) return { tradeType: TradeType.YIELD, route: Routes.YIELD } return null - }, [swapMatch, limitOrderMatch, advancedOrdersMatch]) + }, [swapMatch, hooksMatch, limitOrderMatch, advancedOrdersMatch, yieldMatch]) } function useMatchTradeRoute(route: string): PathMatch<'chainId'> | null { diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useUnknownImpactWarning.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useUnknownImpactWarning.ts new file mode 100644 index 0000000000..e7665060e8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useUnknownImpactWarning.ts @@ -0,0 +1,29 @@ +import { useMemo, useState } from 'react' + +import { useSafeEffect } from 'common/hooks/useSafeMemo' + +import { useReceiveAmountInfo } from './useReceiveAmountInfo' + +export function useUnknownImpactWarning() { + const receiveAmountInfo = useReceiveAmountInfo() + + const state = useState(false) + const [impactWarningAccepted, setImpactWarningAccepted] = state + + // reset the state when users change swap params + useSafeEffect(() => { + setImpactWarningAccepted(false) + }, [ + receiveAmountInfo?.beforeNetworkCosts.sellAmount.currency, + receiveAmountInfo?.beforeNetworkCosts.buyAmount.currency, + receiveAmountInfo?.isSell, + ]) + + return useMemo( + () => ({ + impactWarningAccepted, + setImpactWarningAccepted, + }), + [impactWarningAccepted, setImpactWarningAccepted], + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/index.ts b/apps/cowswap-frontend/src/modules/trade/index.ts index c08f839ed6..10c978158e 100644 --- a/apps/cowswap-frontend/src/modules/trade/index.ts +++ b/apps/cowswap-frontend/src/modules/trade/index.ts @@ -3,6 +3,7 @@ export * from './containers/TradeConfirmModal' export * from './containers/TradeWidgetLinks' export * from './containers/TradeFeesAndCosts' export * from './containers/TradeTotalCostsDetails' +export * from './containers/TradeBasicConfirmDetails' export * from './pure/TradeConfirmation' export * from './hooks/useTradeConfirmActions' export * from './hooks/useTradeTypeInfo' @@ -25,13 +26,26 @@ export * from './hooks/useIsWrapOrUnwrap' export * from './hooks/useIsHooksTradeType' export * from './hooks/useHasTradeEnoughAllowance' export * from './hooks/useIsSellNative' +export * from './hooks/useBuildTradeDerivedState' +export * from './hooks/useOnCurrencySelection' +export * from './hooks/useDerivedTradeState' +export * from './hooks/useNavigateToNewOrderCallback' +export * from './hooks/useOrderSubmittedContent' +export * from './hooks/useIsEoaEthFlow' +export * from './hooks/useShouldPayGas' +export * from './hooks/useWrappedToken' +export * from './hooks/useUnknownImpactWarning' +export * from './hooks/useIsSwapEth' +export * from './hooks/useIsSafeEthFlow' export * from './containers/TradeWidget/types' +export { useIsNoImpactWarningAccepted } from './containers/NoImpactWarning/index' export * from './utils/getReceiveAmountInfo' export * from './utils/parameterizeTradeRoute' export * from './state/receiveAmountInfoAtom' export * from './state/tradeTypeAtom' export * from './state/derivedTradeStateAtom' export * from './state/isWrapOrUnwrapAtom' +export * from './state/isEoaEthFlowAtom' export * from './pure/RecipientRow' export * from './pure/ReceiveAmountTitle' export * from './pure/PartnerFeeRow' diff --git a/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/index.tsx index 75612ae86f..65f4bbc52c 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/index.tsx @@ -5,10 +5,10 @@ import { InfoTooltip } from '@cowprotocol/ui' import { CornerDownRight } from 'react-feather' -import { TimelineDot } from 'modules/trade/pure/Row/styled' - import { Content, Row, Wrapper, Label } from './styled' +import { TimelineDot } from '../Row/styled' + export type ConfirmDetailsItemProps = { children: ReactNode label?: ReactNode diff --git a/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/styled.ts b/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/styled.ts index a442a84135..7416efef16 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/pure/ConfirmDetailsItem/styled.ts @@ -2,7 +2,7 @@ import { Media, UI } from '@cowprotocol/ui' import styled, { css } from 'styled-components/macro' -import { StyledRowBetween } from 'modules/swap/pure/Row/styled' +import { StyledRowBetween } from 'modules/tradeWidgetAddons/pure/Row/styled' export const Wrapper = styled.div<{ alwaysRow: boolean }>` display: flex; @@ -83,7 +83,9 @@ export const Label = styled.span<{ labelOpacity?: boolean }>` gap: 5px; text-align: left; opacity: ${({ labelOpacity }) => (labelOpacity ? 0.7 : 1)}; - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out, opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + transition: + color var(${UI.ANIMATION_DURATION}) ease-in-out, + opacity var(${UI.ANIMATION_DURATION}) ease-in-out; color: inherit; &:hover { diff --git a/apps/cowswap-frontend/src/modules/trade/pure/NoImpactWarning/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/NoImpactWarning/index.tsx deleted file mode 100644 index 4b339bb851..0000000000 --- a/apps/cowswap-frontend/src/modules/trade/pure/NoImpactWarning/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { TradeWarning, TradeWarningType } from 'modules/trade/pure/TradeWarning' - -const NoImpactWarningMessage = ( -
    - - We are unable to calculate the price impact for this order. -
    -
    - You may still move forward but{' '} - please review carefully that the receive amounts are what you expect. -
    -
    -) - -export interface NoImpactWarningProps { - isAccepted: boolean - withoutAccepting?: boolean - className?: string - acceptCallback?(): void -} - -export function NoImpactWarning(props: NoImpactWarningProps) { - const { acceptCallback, isAccepted, withoutAccepting, className } = props - - return ( - - Price impact unknown - trade carefully - - } - /> - ) -} diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.cosmos.tsx index 39ffc51975..18263525af 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.cosmos.tsx @@ -17,7 +17,7 @@ const Fixtures = { refreshInterval={10_000} recipient={null} > - Trade confirmation + {() => Trade confirmation} ), } 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 31cd765195..d8b6e7ab8b 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx @@ -25,6 +25,7 @@ import { QuoteCountdown } from './CountDown' import { useIsPriceChanged } from './hooks/useIsPriceChanged' import * as styledEl from './styled' +import { NoImpactWarning } from '../../containers/NoImpactWarning' import { useTradeConfirmState } from '../../hooks/useTradeConfirmState' import { PriceUpdatedBanner } from '../PriceUpdatedBanner' @@ -47,7 +48,7 @@ export interface TradeConfirmationProps { isPriceStatic?: boolean recipient?: string | null buttonText?: React.ReactNode - children?: ReactElement | ((restContent: ReactElement) => ReactElement) + children?: (restContent: ReactElement) => ReactElement } export function TradeConfirmation(props: TradeConfirmationProps) { @@ -151,13 +152,11 @@ export function TradeConfirmation(props: TradeConfirmationProps) { priceImpactParams={priceImpact} /> - {typeof children === 'function' ? ( - children(hookDetailsElement) - ) : ( + {children?.( <> - {children} {hookDetailsElement} - + + , )} {showRecipientWarning && } diff --git a/apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx b/apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx similarity index 100% rename from apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx rename to apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.cosmos.tsx diff --git a/apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx b/apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx similarity index 94% rename from apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx rename to apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx index 8725c58e99..333ef50ab3 100644 --- a/apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/ZeroApprovalWarning.tsx @@ -4,7 +4,7 @@ import { HashLink } from 'react-router-hash-link' import styled from 'styled-components/macro' import { Nullish } from 'types' -import { WarningCard } from '../WarningCard' +import { WarningCard } from 'common/pure/WarningCard' const Link = styled(HashLink)` text-decoration: underline; diff --git a/apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/index.tsx similarity index 100% rename from apps/cowswap-frontend/src/common/pure/ZeroApprovalWarning/index.tsx rename to apps/cowswap-frontend/src/modules/trade/pure/ZeroApprovalWarning/index.tsx diff --git a/apps/cowswap-frontend/src/modules/trade/state/derivedTradeStateAtom.ts b/apps/cowswap-frontend/src/modules/trade/state/derivedTradeStateAtom.ts index a72ff94d9a..17414929b0 100644 --- a/apps/cowswap-frontend/src/modules/trade/state/derivedTradeStateAtom.ts +++ b/apps/cowswap-frontend/src/modules/trade/state/derivedTradeStateAtom.ts @@ -3,6 +3,7 @@ import { atom } from 'jotai' import { advancedOrdersDerivedStateAtom } from 'modules/advancedOrders' import { limitOrdersDerivedStateAtom } from 'modules/limitOrders' import { swapDerivedStateAtom } from 'modules/swap' +import { yieldDerivedStateAtom } from 'modules/yield' import { tradeTypeAtom } from './tradeTypeAtom' @@ -21,5 +22,9 @@ export const derivedTradeStateAtom = atom((get) => { return get(advancedOrdersDerivedStateAtom) } + if (tradeTypeInfo.tradeType === TradeType.YIELD) { + return get(yieldDerivedStateAtom) + } + return get(limitOrdersDerivedStateAtom) }) diff --git a/apps/cowswap-frontend/src/modules/swap/state/isEoaEthFlowAtom.ts b/apps/cowswap-frontend/src/modules/trade/state/isEoaEthFlowAtom.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/state/isEoaEthFlowAtom.ts rename to apps/cowswap-frontend/src/modules/trade/state/isEoaEthFlowAtom.ts diff --git a/apps/cowswap-frontend/src/modules/trade/types/TradeType.ts b/apps/cowswap-frontend/src/modules/trade/types/TradeType.ts index 0fdcfe3556..283e8ba206 100644 --- a/apps/cowswap-frontend/src/modules/trade/types/TradeType.ts +++ b/apps/cowswap-frontend/src/modules/trade/types/TradeType.ts @@ -4,6 +4,7 @@ export enum TradeType { SWAP = 'SWAP', LIMIT_ORDER = 'LIMIT_ORDER', ADVANCED_ORDERS = 'ADVANCED_ORDERS', + YIELD = 'YIELD', } export interface TradeTypeInfo { diff --git a/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeRoute.ts b/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeRoute.ts index 6a29a001b8..fc893ef326 100644 --- a/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeRoute.ts +++ b/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeRoute.ts @@ -11,7 +11,7 @@ import { RoutesValues } from 'common/constants/routes' export function parameterizeTradeRoute( { chainId, orderKind, inputCurrencyId, outputCurrencyId, inputCurrencyAmount, outputCurrencyAmount }: TradeUrlParams, route: RoutesValues, - withAmounts?: boolean + withAmounts?: boolean, ): string { const path = route .replace('/:chainId?', chainId ? `/${encodeURIComponent(chainId)}` : '') @@ -36,3 +36,10 @@ export function parameterizeTradeRoute( return path } + +export function addChainIdToRoute(route: RoutesValues, chainId: string | undefined): string { + return route + .replace('/:chainId?', chainId ? `/${encodeURIComponent(chainId)}` : '') + .replace('/:inputCurrencyId?', '') + .replace('/:outputCurrencyId?', '') +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts new file mode 100644 index 0000000000..7feb7896b8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts @@ -0,0 +1,56 @@ +import { useCallback } from 'react' + +import { useTradePriceImpact } from 'modules/trade' +import { logTradeFlow } from 'modules/trade/utils/logger' + +import { useConfirmPriceImpactWithoutFee } from 'common/hooks/useConfirmPriceImpactWithoutFee' + +import { useSafeBundleFlowContext } from './useSafeBundleFlowContext' +import { TradeFlowParams, useTradeFlowContext } from './useTradeFlowContext' +import { useTradeFlowType } from './useTradeFlowType' + +import { safeBundleApprovalFlow, safeBundleEthFlow } from '../services/safeBundleFlow' +import { swapFlow } from '../services/swapFlow' +import { FlowType } from '../types/TradeFlowContext' + +export function useHandleSwap(params: TradeFlowParams) { + const tradeFlowType = useTradeFlowType() + const tradeFlowContext = useTradeFlowContext(params) + const safeBundleFlowContext = useSafeBundleFlowContext() + const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() + const priceImpactParams = useTradePriceImpact() + + const contextIsReady = + Boolean( + [FlowType.SAFE_BUNDLE_ETH, FlowType.SAFE_BUNDLE_APPROVAL].includes(tradeFlowType) + ? safeBundleFlowContext + : tradeFlowContext, + ) && !!tradeFlowContext + + const callback = useCallback(async () => { + if (!tradeFlowContext) return + + if (tradeFlowType === FlowType.SAFE_BUNDLE_APPROVAL) { + if (!safeBundleFlowContext) throw new Error('Safe bundle flow context is not ready') + + logTradeFlow('SAFE BUNDLE APPROVAL FLOW', 'Start safe bundle approval flow') + return safeBundleApprovalFlow( + tradeFlowContext, + safeBundleFlowContext, + priceImpactParams, + confirmPriceImpactWithoutFee, + ) + } + if (tradeFlowType === FlowType.SAFE_BUNDLE_ETH) { + if (!safeBundleFlowContext) throw new Error('Safe bundle flow context is not ready') + + logTradeFlow('SAFE BUNDLE ETH FLOW', 'Start safe bundle eth flow') + return safeBundleEthFlow(tradeFlowContext, safeBundleFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + } + + logTradeFlow('SWAP FLOW', 'Start swap flow') + return swapFlow(tradeFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + }, [tradeFlowType, tradeFlowContext, safeBundleFlowContext, priceImpactParams, confirmPriceImpactWithoutFee]) + + return { callback, contextIsReady } +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts new file mode 100644 index 0000000000..5a83744dfa --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useSafeBundleFlowContext.ts @@ -0,0 +1,47 @@ +import { useMemo } from 'react' + +import { getCurrencyAddress } from '@cowprotocol/common-utils' +import { useSafeAppsSdk } from '@cowprotocol/wallet' + +import useSWR from 'swr' + +import { useReceiveAmountInfo } from 'modules/trade' + +import { useGP2SettlementContract, useTokenContract, useWETHContract } from 'common/hooks/useContract' +import { useNeedsApproval } from 'common/hooks/useNeedsApproval' +import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress' + +import { SafeBundleFlowContext } from '../types/TradeFlowContext' + +export function useSafeBundleFlowContext(): SafeBundleFlowContext | null { + const settlementContract = useGP2SettlementContract() + const spender = useTradeSpenderAddress() + + const safeAppsSdk = useSafeAppsSdk() + const wrappedNativeContract = useWETHContract() + const receiveAmountInfo = useReceiveAmountInfo() + const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount + const needsApproval = useNeedsApproval(inputAmountWithSlippage) + const inputCurrencyAddress = useMemo(() => { + return inputAmountWithSlippage ? getCurrencyAddress(inputAmountWithSlippage.currency) : undefined + }, [inputAmountWithSlippage]) + const erc20Contract = useTokenContract(inputCurrencyAddress) + + return ( + useSWR( + settlementContract && spender && safeAppsSdk && wrappedNativeContract && erc20Contract + ? [settlementContract, spender, safeAppsSdk, wrappedNativeContract, needsApproval, erc20Contract] + : null, + ([settlementContract, spender, safeAppsSdk, wrappedNativeContract, needsApproval, erc20Contract]) => { + return { + settlementContract, + spender, + safeAppsSdk, + wrappedNativeContract, + needsApproval, + erc20Contract, + } + }, + ).data || null + ) +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts new file mode 100644 index 0000000000..e080f6d985 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -0,0 +1,192 @@ +import { TokenWithLogo } from '@cowprotocol/common-const' +import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, OrderClass, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' +import { UiOrderType } from '@cowprotocol/types' +import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' +import { useWalletProvider } from '@cowprotocol/wallet-provider' + +import { useDispatch } from 'react-redux' +import useSWR from 'swr' + +import { AppDispatch } from 'legacy/state' +import { useCloseModals } from 'legacy/state/application/hooks' + +import { useAppData, useAppDataHooks } from 'modules/appData' +import { useGeneratePermitHook, useGetCachedPermit, usePermitInfo } from 'modules/permit' +import { useEnoughBalanceAndAllowance } from 'modules/tokens' +import { TradeType, useDerivedTradeState, useReceiveAmountInfo, useTradeConfirmActions } from 'modules/trade' +import { getOrderValidTo, useTradeQuote } from 'modules/tradeQuote' + +import { useGP2SettlementContract } from 'common/hooks/useContract' + +import { TradeFlowContext } from '../types/TradeFlowContext' + +export interface TradeFlowParams { + deadline: number +} + +export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowContext | null { + const { chainId, account } = useWalletInfo() + const provider = useWalletProvider() + const { allowsOffchainSigning } = useWalletDetails() + const isSafeWallet = useIsSafeWallet() + const derivedTradeState = useDerivedTradeState() + const receiveAmountInfo = useReceiveAmountInfo() + + const sellCurrency = derivedTradeState?.inputCurrency + const inputAmount = receiveAmountInfo?.afterNetworkCosts.sellAmount + const outputAmount = receiveAmountInfo?.afterSlippage.buyAmount + const sellAmountBeforeFee = receiveAmountInfo?.afterNetworkCosts.sellAmount + const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount + const networkFee = receiveAmountInfo?.costs.networkFee.amountInSellCurrency + + const permitInfo = usePermitInfo(sellCurrency, TradeType.YIELD) + const generatePermitHook = useGeneratePermitHook() + const getCachedPermit = useGetCachedPermit() + const closeModals = useCloseModals() + const dispatch = useDispatch() + const tradeConfirmActions = useTradeConfirmActions() + const settlementContract = useGP2SettlementContract() + const appData = useAppData() + const typedHooks = useAppDataHooks() + const tradeQuote = useTradeQuote() + + const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId || SupportedChainId.MAINNET] + const { enoughAllowance } = useEnoughBalanceAndAllowance({ + account, + amount: inputAmountWithSlippage, + checkAllowanceAddress, + }) + + const { inputCurrency: sellToken, outputCurrency: buyToken, recipient, recipientAddress } = derivedTradeState || {} + const quoteParams = tradeQuote?.quoteParams + const quoteResponse = tradeQuote?.response + const localQuoteTimestamp = tradeQuote?.localQuoteTimestamp + + return ( + useSWR( + inputAmount && + outputAmount && + inputAmountWithSlippage && + sellAmountBeforeFee && + networkFee && + sellToken && + buyToken && + account && + provider && + appData && + quoteParams && + quoteResponse && + localQuoteTimestamp && + settlementContract + ? [ + account, + allowsOffchainSigning, + appData, + quoteParams, + quoteResponse, + localQuoteTimestamp, + buyToken, + chainId, + closeModals, + dispatch, + enoughAllowance, + generatePermitHook, + inputAmount, + inputAmountWithSlippage, + networkFee, + outputAmount, + permitInfo, + provider, + recipient, + sellAmountBeforeFee, + sellToken, + settlementContract, + tradeConfirmActions, + typedHooks, + deadline, + ] + : null, + ([ + account, + allowsOffchainSigning, + appData, + quoteParams, + quoteResponse, + localQuoteTimestamp, + buyToken, + chainId, + closeModals, + dispatch, + enoughAllowance, + generatePermitHook, + inputAmount, + inputAmountWithSlippage, + networkFee, + outputAmount, + permitInfo, + provider, + recipient, + sellAmountBeforeFee, + sellToken, + settlementContract, + tradeConfirmActions, + typedHooks, + deadline, + ]) => { + return { + context: { + chainId, + inputAmount, + outputAmount, + inputAmountWithSlippage, + }, + flags: { + allowsOffchainSigning, + }, + callbacks: { + closeModals, + getCachedPermit, + dispatch, + }, + tradeConfirmActions, + swapFlowAnalyticsContext: { + account, + recipient, + recipientAddress, + marketLabel: [inputAmount?.currency.symbol, outputAmount?.currency.symbol].join(','), + orderType: UiOrderType.YIELD, + }, + contract: settlementContract, + permitInfo: !enoughAllowance ? permitInfo : undefined, + generatePermitHook, + typedHooks, + orderParams: { + account, + chainId, + signer: provider.getSigner(), + kind: OrderKind.SELL, + inputAmount, + outputAmount, + sellAmountBeforeFee, + feeAmount: networkFee, + sellToken: sellToken as TokenWithLogo, + buyToken: buyToken as TokenWithLogo, + validTo: getOrderValidTo(deadline, { + validFor: quoteParams.validFor, + quoteValidTo: quoteResponse.quote.validTo, + localQuoteTimestamp, + }), + recipient: recipient || account, + recipientAddressOrName: recipient || null, + allowsOffchainSigning, + appData, + class: OrderClass.MARKET, + partiallyFillable: true, + quoteId: quoteResponse.id, + isSafeWallet, + }, + } + }, + ).data || null + ) +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowType.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowType.ts new file mode 100644 index 0000000000..3f8b847dd8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowType.ts @@ -0,0 +1,31 @@ +import { useIsEoaEthFlow, useIsSafeEthFlow, useReceiveAmountInfo } from 'modules/trade' + +import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle' + +import { FlowType } from '../types/TradeFlowContext' + +export function useTradeFlowType(): FlowType { + const isEoaEthFlow = useIsEoaEthFlow() + const isSafeEthFlow = useIsSafeEthFlow() + const receiveAmountInfo = useReceiveAmountInfo() + const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount + + const isSafeBundle = useIsSafeApprovalBundle(inputAmountWithSlippage) + return getFlowType(isSafeBundle, isEoaEthFlow, isSafeEthFlow) +} + +function getFlowType(isSafeBundle: boolean, isEoaEthFlow: boolean, isSafeEthFlow: boolean): FlowType { + if (isSafeEthFlow) { + // Takes precedence over bundle approval + return FlowType.SAFE_BUNDLE_ETH + } + if (isSafeBundle) { + // Takes precedence over eth flow + return FlowType.SAFE_BUNDLE_APPROVAL + } + if (isEoaEthFlow) { + // Takes precedence over regular flow + return FlowType.EOA_ETH_FLOW + } + return FlowType.REGULAR +} diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/index.ts new file mode 100644 index 0000000000..ee07046f0d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/index.ts @@ -0,0 +1,4 @@ +export { useHandleSwap } from './hooks/useHandleSwap' +export { useTradeFlowContext } from './hooks/useTradeFlowContext' +export { useTradeFlowType } from './hooks/useTradeFlowType' +export * from './types/TradeFlowContext' diff --git a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/index.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/index.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/index.ts diff --git a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleApprovalFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts similarity index 88% rename from apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleApprovalFlow.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts index c31a835586..dd64fcdbc0 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleApprovalFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts @@ -11,17 +11,19 @@ import { buildApproveTx } from 'modules/operations/bundle/buildApproveTx' import { buildPresignTx } from 'modules/operations/bundle/buildPresignTx' import { buildZeroApproveTx } from 'modules/operations/bundle/buildZeroApproveTx' import { emitPostedOrderEvent } from 'modules/orders' -import { SafeBundleApprovalFlowContext } from 'modules/swap/services/types' import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' import { logTradeFlow } from 'modules/trade/utils/logger' import { getSwapErrorMessage } from 'modules/trade/utils/swapErrorHelper' import { tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics' import { shouldZeroApprove as shouldZeroApproveFn } from 'modules/zeroApproval' +import { SafeBundleFlowContext, TradeFlowContext } from '../../types/TradeFlowContext' + const LOG_PREFIX = 'SAFE APPROVAL BUNDLE FLOW' export async function safeBundleApprovalFlow( - input: SafeBundleApprovalFlowContext, + tradeContext: TradeFlowContext, + safeBundleContext: SafeBundleFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, ): Promise { @@ -31,19 +33,9 @@ export async function safeBundleApprovalFlow( return false } - const { - erc20Contract, - spender, - context, - callbacks, - dispatch, - orderParams, - settlementContract, - safeAppsSdk, - swapFlowAnalyticsContext, - tradeConfirmActions, - typedHooks, - } = input + const { context, callbacks, orderParams, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext + + const { spender, settlementContract, safeAppsSdk, erc20Contract } = safeBundleContext const { chainId } = context const { account, isSafeWallet, recipientAddressOrName, inputAmount, outputAmount, kind } = orderParams @@ -59,7 +51,7 @@ export async function safeBundleApprovalFlow( const approveTx = await buildApproveTx({ erc20Contract, spender, - amountToApprove: context.trade.inputAmount, + amountToApprove: context.inputAmount, }) orderParams.appData = await removePermitHookFromAppData(orderParams.appData, typedHooks) @@ -79,7 +71,7 @@ export async function safeBundleApprovalFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) logTradeFlow(LOG_PREFIX, 'STEP 4: build presign tx') @@ -94,7 +86,7 @@ export async function safeBundleApprovalFlow( const shouldZeroApprove = await shouldZeroApproveFn({ tokenContract: erc20Contract, spender, - amountToApprove: context.trade.inputAmount, + amountToApprove: context.inputAmount, isBundle: true, }) @@ -102,7 +94,7 @@ export async function safeBundleApprovalFlow( const zeroApproveTx = await buildZeroApproveTx({ erc20Contract, spender, - currency: context.trade.inputAmount.currency, + currency: context.inputAmount.currency, }) safeTransactionData.unshift({ to: zeroApproveTx.to!, @@ -137,7 +129,7 @@ export async function safeBundleApprovalFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) tradeFlowAnalytics.sign(swapFlowAnalyticsContext) diff --git a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleEthFlow.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts similarity index 89% rename from apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleEthFlow.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts index 16377662e1..0b6fe9277b 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/safeBundleFlow/safeBundleEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts @@ -12,16 +12,18 @@ import { buildApproveTx } from 'modules/operations/bundle/buildApproveTx' import { buildPresignTx } from 'modules/operations/bundle/buildPresignTx' import { buildWrapTx } from 'modules/operations/bundle/buildWrapTx' import { emitPostedOrderEvent } from 'modules/orders' -import { SafeBundleEthFlowContext } from 'modules/swap/services/types' import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' import { logTradeFlow } from 'modules/trade/utils/logger' import { getSwapErrorMessage } from 'modules/trade/utils/swapErrorHelper' import { tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics' +import { SafeBundleFlowContext, TradeFlowContext } from '../../types/TradeFlowContext' + const LOG_PREFIX = 'SAFE BUNDLE ETH FLOW' export async function safeBundleEthFlow( - input: SafeBundleEthFlowContext, + tradeContext: TradeFlowContext, + safeBundleContext: SafeBundleFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, ): Promise { @@ -31,27 +33,12 @@ export async function safeBundleEthFlow( return false } - const { - wrappedNativeContract, - needsApproval, - spender, - context, - callbacks, - dispatch, - orderParams, - settlementContract, - safeAppsSdk, - swapFlowAnalyticsContext, - tradeConfirmActions, - typedHooks, - } = input + const { context, callbacks, orderParams, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext + + const { spender, settlementContract, safeAppsSdk, needsApproval, wrappedNativeContract } = safeBundleContext const { account, recipientAddressOrName, kind } = orderParams - const { - inputAmountWithSlippage, - chainId, - trade: { inputAmount, outputAmount }, - } = context + const { inputAmountWithSlippage, chainId, inputAmount, outputAmount } = context tradeFlowAnalytics.wrapApproveAndPresign(swapFlowAnalyticsContext) const nativeAmountInWei = inputAmountWithSlippage.quotient.toString() @@ -107,7 +94,7 @@ export async function safeBundleEthFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) logTradeFlow(LOG_PREFIX, 'STEP 5: build presign tx') @@ -148,7 +135,7 @@ export async function safeBundleEthFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) tradeFlowAnalytics.sign(swapFlowAnalyticsContext) diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/README.md b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/README.md similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/services/swapFlow/README.md rename to apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/README.md diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts similarity index 92% rename from apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts index 405bb7367a..afef3f3941 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts @@ -17,10 +17,10 @@ import { tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics' import { presignOrderStep } from './steps/presignOrderStep' -import { SwapFlowContext } from '../types' +import { TradeFlowContext } from '../../types/TradeFlowContext' export async function swapFlow( - input: SwapFlowContext, + input: TradeFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, ): Promise { @@ -30,9 +30,7 @@ export async function swapFlow( } = input const { - context: { - trade: { inputAmount, outputAmount }, - }, + context: { inputAmount, outputAmount }, typedHooks, } = input const tradeAmounts = { inputAmount, outputAmount } @@ -42,9 +40,9 @@ export async function swapFlow( return false } - const { orderParams, context, permitInfo, generatePermitHook, swapFlowAnalyticsContext, callbacks, dispatch } = input - const { chainId, trade } = context - const inputCurrency = trade.inputAmount.currency + const { orderParams, context, permitInfo, generatePermitHook, swapFlowAnalyticsContext, callbacks } = input + const { chainId } = context + const inputCurrency = inputAmount.currency const cachedPermit = await getCachedPermit(getAddress(inputCurrency)) try { @@ -88,7 +86,7 @@ export async function swapFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) logTradeFlow('SWAP FLOW', 'STEP 5: presign order (optional)') @@ -119,7 +117,7 @@ export async function swapFlow( }, isSafeWallet, }, - dispatch, + callbacks.dispatch, ) } diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/steps/presignOrderStep.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/steps/presignOrderStep.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/services/swapFlow/steps/presignOrderStep.ts rename to apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/steps/presignOrderStep.ts diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/swapFlow.puml b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/swapFlow.puml similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/services/swapFlow/swapFlow.puml rename to apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/swapFlow.puml diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts new file mode 100644 index 0000000000..69ea103b01 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts @@ -0,0 +1,52 @@ +import { Erc20, GPv2Settlement, Weth } from '@cowprotocol/abis' +import type { Command } from '@cowprotocol/types' +import type SafeAppsSDK from '@safe-global/safe-apps-sdk' +import type { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import type { AppDispatch } from 'legacy/state' +import type { PostOrderParams } from 'legacy/utils/trade' + +import type { TypedAppDataHooks } from 'modules/appData' +import type { GeneratePermitHook, IsTokenPermittableResult, useGetCachedPermit } from 'modules/permit' +import type { TradeConfirmActions } from 'modules/trade' +import type { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics' + +export enum FlowType { + REGULAR = 'REGULAR', + EOA_ETH_FLOW = 'EOA_ETH_FLOW', + SAFE_BUNDLE_APPROVAL = 'SAFE_BUNDLE_APPROVAL', + SAFE_BUNDLE_ETH = 'SAFE_BUNDLE_ETH', +} + +export interface TradeFlowContext { + context: { + chainId: number + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + inputAmountWithSlippage: CurrencyAmount + } + flags: { + allowsOffchainSigning: boolean + } + callbacks: { + closeModals: Command + getCachedPermit: ReturnType + dispatch: AppDispatch + } + tradeConfirmActions: TradeConfirmActions + swapFlowAnalyticsContext: TradeFlowAnalyticsContext + orderParams: PostOrderParams + contract: GPv2Settlement + permitInfo: IsTokenPermittableResult + generatePermitHook: GeneratePermitHook + typedHooks?: TypedAppDataHooks +} + +export interface SafeBundleFlowContext { + settlementContract: GPv2Settlement + spender: string + safeAppsSdk: SafeAppsSDK + wrappedNativeContract: Weth + needsApproval: boolean + erc20Contract: Erc20 +} diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts index 2b0656b1ca..c6f1713cb3 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts @@ -1,4 +1,5 @@ import { getIsNativeToken, isAddress, isFractionFalsy } from '@cowprotocol/common-utils' +import { PriceQuality } from '@cowprotocol/cow-sdk' import { TradeType } from 'modules/trade' import { isQuoteExpired } from 'modules/tradeQuote' @@ -79,6 +80,7 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor if ( derivedTradeState.tradeType !== TradeType.LIMIT_ORDER && !tradeQuote.isLoading && + tradeQuote.quoteParams?.priceQuality !== PriceQuality.FAST && isQuoteExpired({ expirationDate: tradeQuote.response?.expiration, deadlineParams: { diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts index ac0677b301..47af867581 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts @@ -11,14 +11,14 @@ import { useUpdateTradeQuote } from './useUpdateTradeQuote' import { tradeQuoteParamsAtom } from '../state/tradeQuoteParamsAtom' -export function useSetTradeQuoteParams(amount: Nullish>) { +export function useSetTradeQuoteParams(amount: Nullish>, fastQuote?: boolean) { const updateTradeQuote = useUpdateTradeQuote() const updateState = useSetAtom(tradeQuoteParamsAtom) - const context = useSafeMemoObject({ amount, updateTradeQuote, updateState }) + const context = useSafeMemoObject({ amount, fastQuote, updateTradeQuote, updateState }) useEffect(() => { context.updateTradeQuote({ response: null, error: null }) - context.updateState({ amount: context.amount || null }) + context.updateState({ amount: context.amount || null, fastQuote: context.fastQuote }) }, [context]) } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 769e40e219..e2f3d301c5 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -3,7 +3,7 @@ import { useLayoutEffect, useMemo } from 'react' import { useDebounce } from '@cowprotocol/common-hooks' import { onlyResolvesLast } from '@cowprotocol/common-utils' -import { OrderQuoteResponse } from '@cowprotocol/cow-sdk' +import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' import { useAreUnsupportedTokens } from '@cowprotocol/tokens' import ms from 'ms.macro' @@ -23,13 +23,14 @@ export const PRICE_UPDATE_INTERVAL = ms`30s` const AMOUNT_CHANGE_DEBOUNCE_TIME = ms`300` // Solves the problem of multiple requests -const getQuoteOnlyResolveLast = onlyResolvesLast(getQuote) +const getFastQuote = onlyResolvesLast(getQuote) +const getOptimalQuote = onlyResolvesLast(getQuote) export function useTradeQuotePolling() { - const { amount } = useAtomValue(tradeQuoteParamsAtom) + const { amount, fastQuote } = useAtomValue(tradeQuoteParamsAtom) const amountStr = useDebounce( useMemo(() => amount?.quotient.toString() || null, [amount]), - AMOUNT_CHANGE_DEBOUNCE_TIME + AMOUNT_CHANGE_DEBOUNCE_TIME, ) const quoteParams = useQuoteParams(amountStr) @@ -51,10 +52,14 @@ export function useTradeQuotePolling() { return } - const fetchQuote = (hasParamsChanged: boolean) => { + const fetchQuote = (hasParamsChanged: boolean, priceQuality: PriceQuality, fetchStartTimestamp: number) => { updateQuoteState({ isLoading: true, hasParamsChanged }) - getQuoteOnlyResolveLast(quoteParams) + const isOptimalQuote = priceQuality === PriceQuality.OPTIMAL + const requestParams = { ...quoteParams, priceQuality } + const request = isOptimalQuote ? getOptimalQuote(requestParams) : getFastQuote(requestParams) + + return request .then((response) => { const { cancelled, data } = response @@ -62,24 +67,44 @@ export function useTradeQuotePolling() { return } - updateQuoteState({ response: data, quoteParams, isLoading: false, error: null, hasParamsChanged: false }) + updateQuoteState({ + response: data, + quoteParams: requestParams, + ...(isOptimalQuote ? { isLoading: false } : null), + error: null, + hasParamsChanged: false, + fetchStartTimestamp, + }) }) .catch((error: QuoteApiError) => { console.log('[useGetQuote]:: fetchQuote error', error) updateQuoteState({ isLoading: false, error, hasParamsChanged: false }) if (error.type === QuoteApiErrorCodes.UnsupportedToken) { - processUnsupportedTokenError(error, quoteParams) + processUnsupportedTokenError(error, requestParams) } }) } - fetchQuote(true) + const fetchStartTimestamp = Date.now() + if (fastQuote) fetchQuote(true, PriceQuality.FAST, fetchStartTimestamp) + fetchQuote(true, PriceQuality.OPTIMAL, fetchStartTimestamp) - const intervalId = setInterval(() => fetchQuote(false), PRICE_UPDATE_INTERVAL) + const intervalId = setInterval(() => { + const fetchStartTimestamp = Date.now() + if (fastQuote) fetchQuote(false, PriceQuality.FAST, fetchStartTimestamp) + fetchQuote(false, PriceQuality.OPTIMAL, fetchStartTimestamp) + }, PRICE_UPDATE_INTERVAL) return () => clearInterval(intervalId) - }, [quoteParams, updateQuoteState, updateCurrencyAmount, processUnsupportedTokenError, getIsUnsupportedTokens]) + }, [ + fastQuote, + quoteParams, + updateQuoteState, + updateCurrencyAmount, + processUnsupportedTokenError, + getIsUnsupportedTokens, + ]) return null } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts index b4e84e580e..0929541273 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts @@ -1,6 +1,6 @@ import { atom } from 'jotai' -import { OrderQuoteResponse } from '@cowprotocol/cow-sdk' +import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' import type { LegacyFeeQuoteParams } from 'legacy/state/price/types' @@ -12,6 +12,7 @@ export interface TradeQuoteState { isLoading: boolean hasParamsChanged: boolean quoteParams: LegacyFeeQuoteParams | null + fetchStartTimestamp: number | null localQuoteTimestamp: number | null } @@ -21,6 +22,7 @@ export const DEFAULT_TRADE_QUOTE_STATE: TradeQuoteState = { isLoading: false, hasParamsChanged: false, quoteParams: null, + fetchStartTimestamp: null, localQuoteTimestamp: null, } @@ -30,6 +32,15 @@ export const updateTradeQuoteAtom = atom(null, (get, set, nextState: Partial { const prevState = get(tradeQuoteAtom) + // Don't update state if Fast quote finished after Optimal quote + if ( + prevState.fetchStartTimestamp === nextState.fetchStartTimestamp && + nextState.response && + nextState.quoteParams?.priceQuality === PriceQuality.FAST + ) { + return { ...prevState } + } + return { ...prevState, ...nextState, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteParamsAtom.ts b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteParamsAtom.ts index dd5e54de55..ae1193ec84 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteParamsAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteParamsAtom.ts @@ -4,6 +4,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' export interface TradeQuoteParamsState { amount: CurrencyAmount | null + fastQuote?: boolean } export const tradeQuoteParamsAtom = atom({ amount: null }) diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx b/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx new file mode 100644 index 0000000000..225e0b99f9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx @@ -0,0 +1,39 @@ +import { percentToBps } from '@cowprotocol/common-utils' +import { BannerOrientation, InfoTooltip, InlineBanner } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' + +import styled from 'styled-components/macro' + +import { useIsSmartSlippageApplied, useTradeSlippage } from 'modules/tradeSlippage' + +const StyledInlineBanner = styled(InlineBanner)` + text-align: center; +` + +export type HighSuggestedSlippageWarningProps = { + isTradePriceUpdating: boolean +} + +export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarningProps) { + const { isTradePriceUpdating } = props + const { account } = useWalletInfo() + const slippage = useTradeSlippage() + + const isSmartSlippageApplied = useIsSmartSlippageApplied() + const isSuggestedSlippage = isSmartSlippageApplied && !isTradePriceUpdating && !!account + const slippageBps = percentToBps(slippage) + + if (!isSuggestedSlippage || !slippageBps || slippageBps <= 200) { + return null + } + + return ( + + Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSlippageModified.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSlippageModified.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsSlippageModified.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSlippageModified.ts diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useIsSmartSlippageApplied.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSmartSlippageApplied.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useIsSmartSlippageApplied.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useIsSmartSlippageApplied.ts diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useSetSlippage.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useSetSlippage.ts new file mode 100644 index 0000000000..3040c569c6 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useSetSlippage.ts @@ -0,0 +1,7 @@ +import { useSetAtom } from 'jotai' + +import { setTradeSlippageAtom } from '../state/slippageValueAndTypeAtom' + +export function useSetSlippage() { + return useSetAtom(setTradeSlippageAtom) +} diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useTradeSlippage.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useTradeSlippage.ts new file mode 100644 index 0000000000..e3666cbc36 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/hooks/useTradeSlippage.ts @@ -0,0 +1,22 @@ +import { useAtomValue } from 'jotai/index' + +import { bpsToPercent } from '@cowprotocol/common-utils' +import { Percent } from '@uniswap/sdk-core' + +import { + defaultSlippageAtom, + smartTradeSlippageAtom, + tradeSlippagePercentAtom, +} from '../state/slippageValueAndTypeAtom' + +export function useTradeSlippage(): Percent { + return useAtomValue(tradeSlippagePercentAtom) +} + +export function useDefaultTradeSlippage() { + return bpsToPercent(useAtomValue(defaultSlippageAtom)) +} + +export function useSmartTradeSlippage() { + return useAtomValue(smartTradeSlippageAtom) +} diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/index.tsx b/apps/cowswap-frontend/src/modules/tradeSlippage/index.tsx new file mode 100644 index 0000000000..35f3926515 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/index.tsx @@ -0,0 +1,6 @@ +export { SmartSlippageUpdater } from './updaters/SmartSlippageUpdater' +export { HighSuggestedSlippageWarning } from './containers/HighSuggestedSlippageWarning' +export * from './hooks/useSetSlippage' +export * from './hooks/useTradeSlippage' +export * from './hooks/useIsSmartSlippageApplied' +export * from './hooks/useIsSlippageModified' diff --git a/apps/cowswap-frontend/src/modules/swap/state/slippageValueAndTypeAtom.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts similarity index 67% rename from apps/cowswap-frontend/src/modules/swap/state/slippageValueAndTypeAtom.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts index c7351bcafd..f1d081e351 100644 --- a/apps/cowswap-frontend/src/modules/swap/state/slippageValueAndTypeAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts @@ -6,16 +6,21 @@ import { bpsToPercent } from '@cowprotocol/common-utils' import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { walletInfoAtom } from '@cowprotocol/wallet' -import { isEoaEthFlowAtom } from './isEoaEthFlowAtom' +import { isEoaEthFlowAtom } from 'modules/trade' type SlippageBpsPerNetwork = Record type SlippageType = 'smart' | 'default' | 'user' -const normalSwapSlippageAtom = atomWithStorage('swapSlippageAtom:v0', mapSupportedNetworks(null)) +const normalTradeSlippageAtom = atomWithStorage( + 'swapSlippageAtom:v0', + mapSupportedNetworks(null), +) const ethFlowSlippageAtom = atomWithStorage('ethFlowSlippageAtom:v0', mapSupportedNetworks(null)) +export const smartTradeSlippageAtom = atom(null) + export const defaultSlippageAtom = atom((get) => { const { chainId } = get(walletInfoAtom) const isEoaEthFlow = get(isEoaEthFlowAtom) @@ -26,41 +31,42 @@ export const defaultSlippageAtom = atom((get) => { const currentSlippageAtom = atom((get) => { const { chainId } = get(walletInfoAtom) const isEoaEthFlow = get(isEoaEthFlowAtom) - const normalSwapSlippage = get(normalSwapSlippageAtom) + const normalSlippage = get(normalTradeSlippageAtom) const ethFlowSlippage = get(ethFlowSlippageAtom) - return (isEoaEthFlow ? ethFlowSlippage : normalSwapSlippage)?.[chainId] ?? null + return (isEoaEthFlow ? ethFlowSlippage : normalSlippage)?.[chainId] ?? null }) -export const smartSwapSlippageAtom = atom(null) - export const slippageValueAndTypeAtom = atom<{ type: SlippageType; value: number }>((get) => { const currentSlippage = get(currentSlippageAtom) const defaultSlippage = get(defaultSlippageAtom) - const smartSwapSlippage = get(smartSwapSlippageAtom) + const smartSlippage = get(smartTradeSlippageAtom) const isEoaEthFlow = get(isEoaEthFlowAtom) if (typeof currentSlippage === 'number') { return { type: 'user', value: currentSlippage } } - if (!isEoaEthFlow && smartSwapSlippage && smartSwapSlippage !== defaultSlippage) { - return { type: 'smart', value: smartSwapSlippage } + if (!isEoaEthFlow && smartSlippage && smartSlippage !== defaultSlippage) { + return { type: 'smart', value: smartSlippage } } return { type: 'default', value: defaultSlippage } }) -export const swapSlippagePercentAtom = atom((get) => { +export const tradeSlippagePercentAtom = atom((get) => { return bpsToPercent(get(slippageValueAndTypeAtom).value) }) -export const setSwapSlippageAtom = atom(null, (get, set, slippageBps: number | null) => { +export const setTradeSlippageAtom = atom(null, (get, set, slippageBps: number | null) => { const { chainId } = get(walletInfoAtom) const isEoaEthFlow = get(isEoaEthFlowAtom) - const currentStateAtom = isEoaEthFlow ? ethFlowSlippageAtom : normalSwapSlippageAtom + const currentStateAtom = isEoaEthFlow ? ethFlowSlippageAtom : normalTradeSlippageAtom const currentState = get(currentStateAtom) - set(currentStateAtom, { ...currentState, [chainId]: slippageBps }) + set(currentStateAtom, { + ...currentState, + [chainId]: slippageBps, + }) }) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/index.ts similarity index 88% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/index.ts index 73e6173e77..88efcdae6a 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/index.ts @@ -6,13 +6,13 @@ import { useTradeConfirmState } from 'modules/trade' import { useSmartSlippageFromBff } from './useSmartSlippageFromBff' import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMultiplier' -import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' +import { smartTradeSlippageAtom } from '../../state/slippageValueAndTypeAtom' const MAX_BPS = 500 // 5% const MIN_BPS = 50 // 0.5% export function SmartSlippageUpdater() { - const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) + const setSmartSwapSlippage = useSetAtom(smartTradeSlippageAtom) const bffSlippageBps = useSmartSlippageFromBff() const feeMultiplierSlippageBps = useSmartSlippageFromFeeMultiplier() diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts rename to apps/cowswap-frontend/src/modules/tradeSlippage/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx new file mode 100644 index 0000000000..dfc6ddcdd5 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/BundleTxWrapBanner/index.tsx @@ -0,0 +1,29 @@ +import { InlineBanner } from '@cowprotocol/ui' +import { useIsBundlingSupported, useIsSmartContractWallet } from '@cowprotocol/wallet' + +import { useIsNativeIn, useWrappedToken } from 'modules/trade' + +import useNativeCurrency from 'lib/hooks/useNativeCurrency' + +export function BundleTxWrapBanner() { + const nativeCurrencySymbol = useNativeCurrency().symbol || 'ETH' + const wrappedCurrencySymbol = useWrappedToken().symbol || 'WETH' + + const isBundlingSupported = useIsBundlingSupported() + const isNativeIn = useIsNativeIn() + const isSmartContractWallet = useIsSmartContractWallet() + const showWrapBundlingBanner = Boolean(isNativeIn && isSmartContractWallet && isBundlingSupported) + + if (!showWrapBundlingBanner) return null + + return ( + + Token wrapping bundling +

    + For your convenience, CoW Swap will bundle all the necessary actions for this trade into a single transaction. + This includes the {nativeCurrencySymbol} wrapping and, if needed, {wrappedCurrencySymbol} +  approval. Even if the trade fails, your wrapping and approval will be done! +

    +
    + ) +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/consts.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/consts.ts new file mode 100644 index 0000000000..6dc0eed2a7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/consts.ts @@ -0,0 +1,3 @@ +export const HIGH_TIER_FEE = 30 +export const MEDIUM_TIER_FEE = 20 +export const LOW_TIER_FEE = 10 diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts new file mode 100644 index 0000000000..158912718c --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts @@ -0,0 +1,86 @@ +import { atom, useAtom } from 'jotai' +import { useMemo } from 'react' + +import { FEE_SIZE_THRESHOLD } from '@cowprotocol/common-const' + +import { useReceiveAmountInfo } from 'modules/trade' + +import { useSafeEffect, useSafeMemo } from 'common/hooks/useSafeMemo' + +const feeWarningAcceptedAtom = atom(false) + +/** + * useHighFeeWarning + * @description checks whether fee vs trade inputAmount = high fee warning + * @description returns params related to high fee and a cb for checking/unchecking fee acceptance + */ +export function useHighFeeWarning() { + const receiveAmountInfo = useReceiveAmountInfo() + + const [feeWarningAccepted, setFeeWarningAccepted] = useAtom(feeWarningAcceptedAtom) + + // only considers inputAmount vs fee (fee is in input token) + const [isHighFee, feePercentage] = useMemo(() => { + if (!receiveAmountInfo) return [false, undefined] + + const { + isSell, + beforeNetworkCosts, + afterNetworkCosts, + costs: { networkFee, partnerFee }, + quotePrice, + } = receiveAmountInfo + + const outputAmountWithoutFee = isSell ? beforeNetworkCosts.buyAmount : afterNetworkCosts.buyAmount + + const inputAmountAfterFees = isSell ? beforeNetworkCosts.sellAmount : afterNetworkCosts.sellAmount + + const feeAsCurrency = isSell ? quotePrice.quote(networkFee.amountInSellCurrency) : networkFee.amountInSellCurrency + + const volumeFeeAmount = partnerFee.amount + + const totalFeeAmount = volumeFeeAmount ? feeAsCurrency.add(volumeFeeAmount) : feeAsCurrency + const targetAmount = isSell ? outputAmountWithoutFee : inputAmountAfterFees + const feePercentage = totalFeeAmount.divide(targetAmount).multiply(100).asFraction + + return [feePercentage.greaterThan(FEE_SIZE_THRESHOLD), feePercentage] + }, [receiveAmountInfo]) + + // reset the state when users change swap params + useSafeEffect(() => { + setFeeWarningAccepted(false) + }, [ + receiveAmountInfo?.beforeNetworkCosts.sellAmount.currency, + receiveAmountInfo?.beforeNetworkCosts.buyAmount.currency, + receiveAmountInfo?.isSell, + ]) + + return useSafeMemo( + () => ({ + isHighFee, + feePercentage, + // we only care/check about feeWarning being accepted if the fee is actually high.. + feeWarningAccepted: _computeFeeWarningAcceptedState({ feeWarningAccepted, isHighFee }), + setFeeWarningAccepted, + }), + [isHighFee, feePercentage, feeWarningAccepted, setFeeWarningAccepted], + ) +} + +function _computeFeeWarningAcceptedState({ + feeWarningAccepted, + isHighFee, +}: { + feeWarningAccepted: boolean + isHighFee: boolean +}) { + if (feeWarningAccepted) return true + else { + // is the fee high? that's only when we care + if (isHighFee) { + return feeWarningAccepted + } else { + return true + } + } +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/index.tsx new file mode 100644 index 0000000000..a68da1afc3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/index.tsx @@ -0,0 +1,89 @@ +import { useCallback } from 'react' + +import { HoverTooltip } from '@cowprotocol/ui' +import { useWalletInfo } from '@cowprotocol/wallet' +import { Fraction } from '@uniswap/sdk-core' + +import { AlertTriangle } from 'react-feather' + +import { useIsDarkMode } from 'legacy/state/user/hooks' + +import { useSafeMemo } from 'common/hooks/useSafeMemo' + +import { HIGH_TIER_FEE, LOW_TIER_FEE, MEDIUM_TIER_FEE } from './consts' +import { useHighFeeWarning } from './hooks/useHighFeeWarning' +import { ErrorStyledInfoIcon, WarningCheckboxContainer, WarningContainer } from './styled' + +interface HighFeeWarningProps { + readonlyMode?: boolean +} + +export function HighFeeWarning({ readonlyMode }: HighFeeWarningProps) { + const { account } = useWalletInfo() + const { feeWarningAccepted, setFeeWarningAccepted } = useHighFeeWarning() + const darkMode = useIsDarkMode() + + const toggleFeeWarningAccepted = useCallback(() => { + setFeeWarningAccepted((state) => !state) + }, [setFeeWarningAccepted]) + + const { isHighFee, feePercentage } = useHighFeeWarning() + const level = useSafeMemo(() => _getWarningInfo(feePercentage), [feePercentage]) + + if (!isHighFee) return null + + return ( + +
    + + Costs exceed {level}% of the swap amount!{' '} + }> + + {' '} +
    + + {account && !readonlyMode && ( + + {' '} + Swap anyway + + )} +
    + ) +} + +// checks fee as percentage (30% not a decimal) +function _getWarningInfo(feePercentage?: Fraction) { + if (!feePercentage || feePercentage.lessThan(LOW_TIER_FEE)) { + return undefined + } else if (feePercentage.lessThan(MEDIUM_TIER_FEE)) { + return LOW_TIER_FEE + } else if (feePercentage.lessThan(HIGH_TIER_FEE)) { + return MEDIUM_TIER_FEE + } else { + return HIGH_TIER_FEE + } +} + +const HighFeeWarningMessage = ({ feePercentage }: { feePercentage?: Fraction }) => ( +
    + + Current network costs make up{' '} + + {feePercentage?.toFixed(2)}% + {' '} + of your swap amount. +
    +
    + Consider waiting for lower network costs. +
    +
    + You may still move forward with this swap but a high percentage of it will be consumed by network costs. +
    +
    +) diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/styled.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/styled.tsx new file mode 100644 index 0000000000..8088eb926a --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/styled.tsx @@ -0,0 +1,139 @@ +import { Media, UI } from '@cowprotocol/ui' + +import { Info } from 'react-feather' +import styled from 'styled-components/macro' + +import { HIGH_TIER_FEE, LOW_TIER_FEE, MEDIUM_TIER_FEE } from './consts' + +interface HighFeeContainerProps { + level?: number + isDarkMode?: boolean +} + +// TODO: refactor these styles +export const AuxInformationContainer = styled.div<{ + margin?: string + borderColor?: string + borderWidth?: string + hideInput: boolean + disabled?: boolean + showAux?: boolean +}>` + border: 1px solid ${({ hideInput }) => (hideInput ? ' transparent' : `var(${UI.COLOR_PAPER_DARKER})`)}; + background-color: var(${UI.COLOR_PAPER}); + width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; + + :focus, + :hover { + border: 1px solid ${({ theme, hideInput }) => (hideInput ? ' transparent' : theme.background)}; + } + + ${({ theme, hideInput, disabled }) => + !disabled && + ` + :focus, + :hover { + border: 1px solid ${hideInput ? ' transparent' : theme.background}; + } + `} + + margin: ${({ margin = '0 auto' }) => margin}; + border-radius: 0 0 15px 15px; + border: 2px solid var(${UI.COLOR_PAPER_DARKER}); + + &:hover { + border: 2px solid var(${UI.COLOR_PAPER_DARKER}); + } + + ${Media.upToSmall()} { + height: auto; + flex-flow: column wrap; + justify-content: flex-end; + align-items: flex-end; + } +` + +export const WarningCheckboxContainer = styled.label` + display: flex; + width: 100%; + font-weight: bold; + gap: 2px; + justify-content: center; + align-items: center; + border-radius: 16px; + padding: 0; + margin: 10px auto; + cursor: pointer; + + > input { + cursor: pointer; + margin: 1px 4px 0 0; + } +` + +export const WarningContainer = styled(AuxInformationContainer).attrs((props) => ({ + ...props, + hideInput: true, +}))` + --warningColor: ${({ theme, level }) => + level === HIGH_TIER_FEE + ? theme.danger + : level === MEDIUM_TIER_FEE + ? theme.warning + : LOW_TIER_FEE + ? theme.alert + : theme.info}; + color: inherit; + padding: 16px; + width: 100%; + border-radius: 16px; + border: 0; + margin: ${({ margin = '0 auto' }) => margin}; + position: relative; + z-index: 1; + + &:hover { + border: 0; + } + + &::before { + content: ''; + display: block; + position: absolute; + top: 0; + left: 0; + border-radius: inherit; + background: var(--warningColor); + opacity: ${({ isDarkMode }) => (isDarkMode ? 0.2 : 0.15)}; + z-index: -1; + width: 100%; + height: 100%; + pointer-events: none; + } + + > div { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + text-align: center; + + > svg:first-child { + stroke: var(--warningColor); + } + } +` + +export const ErrorStyledInfoIcon = styled(Info)` + opacity: 0.6; + line-height: 0; + vertical-align: middle; + transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + + &:hover { + opacity: 1; + } + color: ${({ theme }) => (theme.darkMode ? '#FFCA4A' : '#564D00')}; +` diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowDeadline/index.tsx new file mode 100644 index 0000000000..2bc83f665d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowDeadline/index.tsx @@ -0,0 +1,30 @@ +import { useMemo } from 'react' + +import { useIsEoaEthFlow, useIsWrapOrUnwrap } from 'modules/trade' + +import useNativeCurrency from 'lib/hooks/useNativeCurrency' + +import { RowDeadlineContent } from '../../pure/Row/RowDeadline' + +export function RowDeadline({ deadline }: { deadline: number }) { + const isEoaEthFlow = useIsEoaEthFlow() + const nativeCurrency = useNativeCurrency() + const isWrapOrUnwrap = useIsWrapOrUnwrap() + + const props = useMemo(() => { + const displayDeadline = Math.floor(deadline / 60) + ' minutes' + return { + userDeadline: deadline, + symbols: [nativeCurrency.symbol], + displayDeadline, + isEoaEthFlow, + isWrapOrUnwrap, + } + }, [isEoaEthFlow, isWrapOrUnwrap, nativeCurrency.symbol, deadline]) + + if (!isEoaEthFlow || isWrapOrUnwrap) { + return null + } + + return +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx new file mode 100644 index 0000000000..99f75e014f --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx @@ -0,0 +1,66 @@ +import { useMemo } from 'react' + +import { formatPercent } from '@cowprotocol/common-utils' +import { useWalletInfo } from '@cowprotocol/wallet' +import { Percent } from '@uniswap/sdk-core' + +import { useIsEoaEthFlow } from 'modules/trade' +import { useIsSmartSlippageApplied, useSetSlippage, useSmartTradeSlippage } from 'modules/tradeSlippage' + +import useNativeCurrency from 'lib/hooks/useNativeCurrency' + +import { RowSlippageContent } from '../../pure/Row/RowSlippageContent' + +export interface RowSlippageProps { + allowedSlippage: Percent + slippageLabel?: React.ReactNode + slippageTooltip?: React.ReactNode + isSlippageModified: boolean + isTradePriceUpdating: boolean +} + +export function RowSlippage({ + allowedSlippage, + slippageTooltip, + slippageLabel, + isTradePriceUpdating, + isSlippageModified, +}: RowSlippageProps) { + const { chainId } = useWalletInfo() + + const isEoaEthFlow = useIsEoaEthFlow() + const nativeCurrency = useNativeCurrency() + const smartSlippage = useSmartTradeSlippage() + const isSmartSlippageApplied = useIsSmartSlippageApplied() + const setSlippage = useSetSlippage() + + const props = useMemo( + () => ({ + chainId, + isEoaEthFlow, + symbols: [nativeCurrency.symbol], + allowedSlippage, + slippageLabel, + slippageTooltip, + displaySlippage: `${formatPercent(allowedSlippage)}%`, + isSmartSlippageApplied, + isSmartSlippageLoading: isTradePriceUpdating, + smartSlippage: + smartSlippage && !isEoaEthFlow ? `${formatPercent(new Percent(smartSlippage, 10_000))}%` : undefined, + setAutoSlippage: smartSlippage && !isEoaEthFlow ? () => setSlippage(null) : undefined, + }), + [ + chainId, + isEoaEthFlow, + nativeCurrency.symbol, + allowedSlippage, + slippageLabel, + slippageTooltip, + smartSlippage, + isSmartSlippageApplied, + isTradePriceUpdating, + ], + ) + + return +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx new file mode 100644 index 0000000000..e43aba6dcb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -0,0 +1,116 @@ +import { useAtom } from 'jotai' +import { ReactElement, RefObject, useCallback, useEffect, useRef } from 'react' + +import { StatefulValue } from '@cowprotocol/types' +import { HelpTooltip, RowBetween, RowFixed } from '@cowprotocol/ui' + +import { Trans } from '@lingui/macro' +import { Menu, useMenuButtonContext } from '@reach/menu-button' +import { Text } from 'rebass' +import { ThemedText } from 'theme' + +import { AutoColumn } from 'legacy/components/Column' +import { Toggle } from 'legacy/components/Toggle' + +import { toggleRecipientAddressAnalytics } from 'modules/analytics' +import { SettingsIcon } from 'modules/trade/pure/Settings' + +import * as styledEl from './styled' + +import { settingsTabStateAtom } from '../../state/settingsTabState' +import { TransactionSettings } from '../TransactionSettings' + +interface SettingsTabProps { + className?: string + recipientToggleState: StatefulValue + deadlineState: StatefulValue +} + +export function SettingsTab({ className, recipientToggleState, deadlineState }: SettingsTabProps) { + const menuButtonRef = useRef(null) + + const [recipientToggleVisible, toggleRecipientVisibilityAux] = recipientToggleState + const toggleRecipientVisibility = useCallback( + (value?: boolean) => { + const isVisible = value ?? !recipientToggleVisible + toggleRecipientAddressAnalytics(isVisible) + toggleRecipientVisibilityAux(isVisible) + }, + [toggleRecipientVisibilityAux, recipientToggleVisible], + ) + + return ( + + + + + + + + + + Transaction Settings + + + + Interface Settings + + + + + + Custom Recipient + + + Allows you to choose a destination address for the swap other than the connected one. + + } + /> + + + + + + + + + ) +} + +interface SettingsTabControllerProps { + buttonRef: RefObject + children: ReactElement +} + +/** + * https://stackoverflow.com/questions/70596487/how-to-programmatically-expand-react-reach-ui-reach-menu-button-menu + */ +function SettingsTabController({ buttonRef, children }: SettingsTabControllerProps) { + const [settingsTabState, setSettingsTabState] = useAtom(settingsTabStateAtom) + const { isExpanded } = useMenuButtonContext() + + const toggleMenu = () => { + buttonRef.current?.dispatchEvent(new Event('mousedown', { bubbles: true })) + } + + useEffect(() => { + if (settingsTabState.open) { + toggleMenu() + } + }, [settingsTabState.open]) + + useEffect(() => { + if (settingsTabState.open && !isExpanded) { + toggleMenu() + setSettingsTabState({ open: false }) + } + }, [settingsTabState.open, isExpanded]) + + return children +} diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/styled.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/styled.tsx new file mode 100644 index 0000000000..0b63cc2c26 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/styled.tsx @@ -0,0 +1,86 @@ +import { Media, RowFixed, UI } from '@cowprotocol/ui' + +import { MenuButton, MenuList } from '@reach/menu-button' +import { transparentize } from 'color2k' +import styled from 'styled-components/macro' + +export const StyledMenuButton = styled(MenuButton)` + position: relative; + width: 100%; + border: none; + background-color: transparent; + margin: 0; + padding: 0; + border-radius: 0.5rem; + height: var(${UI.ICON_SIZE_NORMAL}); + opacity: 0.6; + transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + color: inherit; + display: flex; + align-items: center; + + &:hover, + &:focus { + opacity: 1; + cursor: pointer; + outline: none; + color: currentColor; + } + + svg { + opacity: 1; + margin: auto; + transition: transform 0.3s cubic-bezier(0.65, 0.05, 0.36, 1); + color: inherit; + } +` + +export const StyledMenu = styled.div` + margin: 0; + display: flex; + justify-content: center; + align-items: center; + position: relative; + border: none; + text-align: left; + color: inherit; + + ${RowFixed} { + color: inherit; + + > div { + color: inherit; + opacity: 0.85; + } + } +` + +export const MenuFlyout = styled(MenuList)` + min-width: 20.125rem; + background: var(${UI.COLOR_PRIMARY}); + box-shadow: + 0px 0px 1px rgba(0, 0, 0, 0.01), + 0px 4px 8px rgba(0, 0, 0, 0.04), + 0px 16px 24px rgba(0, 0, 0, 0.04), + 0px 24px 32px rgba(0, 0, 0, 0.01); + border-radius: 12px; + display: flex; + flex-direction: column; + font-size: 1rem; + position: absolute; + z-index: 100; + color: inherit; + box-shadow: ${({ theme }) => theme.boxShadow2}; + border: 1px solid ${({ theme }) => transparentize(theme.white, 0.95)}; + background-color: var(${UI.COLOR_PAPER}); + color: inherit; + padding: 0; + margin: 0; + top: 36px; + right: 0; + width: 280px; + + ${Media.upToMedium()} { + min-width: 18.125rem; + } +` diff --git a/apps/cowswap-frontend/src/modules/swap/containers/TradeRateDetails/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx similarity index 72% rename from apps/cowswap-frontend/src/modules/swap/containers/TradeRateDetails/index.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx index 7c5aa2e111..6b18c7a060 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/TradeRateDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx @@ -1,41 +1,41 @@ -import React, { useMemo, useState, useCallback } from 'react' +import React, { useMemo, useState, useCallback, ReactElement } from 'react' -import { CurrencyAmount, Percent } from '@uniswap/sdk-core' +import { CurrencyAmount } from '@uniswap/sdk-core' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { getTotalCosts, - ReceiveAmountInfo, TradeFeesAndCosts, TradeTotalCostsDetails, useDerivedTradeState, NetworkCostsRow, + useReceiveAmountInfo, + useShouldPayGas, } from 'modules/trade' import { useTradeQuote } from 'modules/tradeQuote' +import { useIsSlippageModified, useTradeSlippage } from 'modules/tradeSlippage' import { useUsdAmount } from 'modules/usdAmount' import { NetworkCostsSuffix } from 'common/pure/NetworkCostsSuffix' import { RateInfoParams } from 'common/pure/RateInfo' -import { useShouldPayGas } from '../../hooks/useShouldPayGas' import { NetworkCostsTooltipSuffix } from '../../pure/NetworkCostsTooltipSuffix' -import { RowDeadline } from '../Row/RowDeadline' -import { RowSlippage } from '../Row/RowSlippage' +import { RowDeadline } from '../RowDeadline' +import { RowSlippage } from '../RowSlippage' interface TradeRateDetailsProps { - receiveAmountInfo: ReceiveAmountInfo | null + deadline: number rateInfoParams: RateInfoParams - allowedSlippage: Percent | null - isSlippageModified: boolean + children?: ReactElement + isTradePriceUpdating: boolean } -export function TradeRateDetails({ - allowedSlippage, - receiveAmountInfo, - rateInfoParams, - isSlippageModified, -}: TradeRateDetailsProps) { +export function TradeRateDetails({ rateInfoParams, deadline, isTradePriceUpdating }: TradeRateDetailsProps) { const [isFeeDetailsOpen, setFeeDetailsOpen] = useState(false) + + const slippage = useTradeSlippage() + const isSlippageModified = useIsSlippageModified() + const receiveAmountInfo = useReceiveAmountInfo() const derivedTradeState = useDerivedTradeState() const tradeQuote = useTradeQuote() const shouldPayGas = useShouldPayGas() @@ -90,8 +90,14 @@ export function TradeRateDetails({ networkCostsTooltipSuffix={} alwaysRow /> - {allowedSlippage && } - + {slippage && ( + + )} + ) } diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx similarity index 71% rename from apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx index 7ad9ea8175..274c563b77 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx @@ -14,31 +14,40 @@ import { } from '@cowprotocol/common-const' import { useOnClickOutside } from '@cowprotocol/common-hooks' import { getWrappedToken, percentToBps } from '@cowprotocol/common-utils' -import { FancyButton, HelpTooltip, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' +import { StatefulValue } from '@cowprotocol/types' +import { HelpTooltip, RowBetween, RowFixed, UI } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' import { TradeType } from '@cowprotocol/widget-lib' import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' -import { darken } from 'color2k' -import styled, { ThemeContext } from 'styled-components/macro' +import { ThemeContext } from 'styled-components/macro' import { ThemedText } from 'theme' import { AutoColumn } from 'legacy/components/Column' -import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { orderExpirationTimeAnalytics, slippageToleranceAnalytics } from 'modules/analytics' import { useInjectedWidgetDeadline } from 'modules/injectedWidget' -import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' -import { useIsSlippageModified } from 'modules/swap/hooks/useIsSlippageModified' -import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' -import { useSetSlippage } from 'modules/swap/hooks/useSetSlippage' -import { useDefaultSwapSlippage, useSmartSwapSlippage, useSwapSlippage } from 'modules/swap/hooks/useSwapSlippage' -import { getNativeOrderDeadlineTooltip, getNonNativeOrderDeadlineTooltip } from 'modules/swap/pure/Row/RowDeadline' -import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'modules/swap/pure/Row/RowSlippageContent' +import { useIsEoaEthFlow } from 'modules/trade' +import { + useDefaultTradeSlippage, + useIsSlippageModified, + useIsSmartSlippageApplied, + useSetSlippage, + useSmartTradeSlippage, + useTradeSlippage, +} from 'modules/tradeSlippage' +import { + getNativeOrderDeadlineTooltip, + getNativeSlippageTooltip, + getNonNativeOrderDeadlineTooltip, + getNonNativeSlippageTooltip, +} from 'common/utils/tradeSettingsTooltips' import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import * as styledEl from './styled' + const MAX_DEADLINE_MINUTES = 180 // 3h enum SlippageError { @@ -49,150 +58,26 @@ enum DeadlineError { InvalidInput = 'InvalidInput', } -const Option = styled(FancyButton)<{ active: boolean }>` - margin-right: 8px; - - :hover { - cursor: pointer; - } - - &:disabled { - border: none; - pointer-events: none; - } -` - -export const Input = styled.input` - background: var(${UI.COLOR_PAPER}); - font-size: 16px; - width: auto; - outline: none; - - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - } - - color: ${({ theme, color }) => (color === 'red' ? theme.error : `var(${UI.COLOR_TEXT})`)}; - text-align: right; -` - -export const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>` - height: 2rem; - position: relative; - padding: 0 0.75rem; - flex: 1; - border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.error : theme.bg2}`}; - - :hover { - border: ${({ theme, active, warning }) => - active && `1px solid ${warning ? darken(theme.error, 0.1) : darken(theme.bg2, 0.1)}`}; - } - - input { - width: 100%; - height: 100%; - border: 0; - border-radius: 2rem; - } -` - -const SlippageEmojiContainer = styled.span` - color: #f3841e; - - ${Media.upToSmall()} { - display: none; - } -` - -const SmartSlippageInfo = styled.div` - color: var(${UI.COLOR_GREEN}); - font-size: 13px; - text-align: right; - width: 100%; - padding-right: 0.2rem; - display: flex; - justify-content: flex-end; - padding-bottom: 0.35rem; - - > span { - margin-left: 4px; - } -` - -const Wrapper = styled.div` - ${RowBetween} > button, ${OptionCustom} { - &:disabled { - color: var(${UI.COLOR_TEXT_OPACITY_50}); - background-color: var(${UI.COLOR_PAPER}); - border: none; - pointer-events: none; - } - } - - ${OptionCustom} { - background-color: var(${UI.COLOR_PAPER_DARKER}); - border: 0; - color: inherit; - - > div > input { - background: transparent; - color: inherit; - - &:disabled { - color: inherit; - background-color: inherit; - } - } - - > div > input::placeholder { - opacity: 0.5; - color: inherit; - } - } - - ${RowFixed} { - color: inherit; - - > div { - color: inherit; - opacity: 0.85; - } - - > button { - background-color: var(${UI.COLOR_PAPER_DARKER}); - border: 0; - } - - > button > input { - background: transparent; - color: inherit; - } - - > button > input::placeholder { - background: transparent; - opacity: 0.5; - color: inherit; - } - } -` +interface TransactionSettingsProps { + deadlineState: StatefulValue +} -export function TransactionSettings() { +export function TransactionSettings({ deadlineState }: TransactionSettingsProps) { const { chainId } = useWalletInfo() const theme = useContext(ThemeContext) const isEoaEthFlow = useIsEoaEthFlow() const nativeCurrency = useNativeCurrency() - const swapSlippage = useSwapSlippage() - const defaultSwapSlippage = useDefaultSwapSlippage() + const swapSlippage = useTradeSlippage() + const defaultSwapSlippage = useDefaultTradeSlippage() const setSwapSlippage = useSetSlippage() const isSmartSlippageApplied = useIsSmartSlippageApplied() - const smartSlippage = useSmartSwapSlippage() + const smartSlippage = useSmartTradeSlippage() const chosenSlippageMatchesSmartSlippage = smartSlippage && new Percent(smartSlippage, 10_000).equalTo(swapSlippage) - const [deadline, setDeadline] = useUserTransactionTTL() + const [deadline, setDeadline] = deadlineState const widgetDeadline = useInjectedWidgetDeadline(TradeType.SWAP) const [slippageInput, setSlippageInput] = useState('') @@ -323,7 +208,7 @@ export function TransactionSettings() { useOnClickOutside([wrapperRef], onSlippageInputBlur) return ( - + @@ -340,24 +225,24 @@ export function TransactionSettings() { /> - - + + {!isSmartSlippageApplied && !chosenSlippageMatchesSmartSlippage && (tooLow || tooHigh) ? ( - + ⚠️ - + ) : null} - 0 ? slippageInput : !isSlippageModified ? '' : swapSlippage.toFixed(2)} onChange={(e) => parseSlippageInput(e.target.value)} @@ -366,15 +251,14 @@ export function TransactionSettings() { /> % - + {!isSmartSlippageApplied && !chosenSlippageMatchesSmartSlippage && (slippageError || tooLow || tooHigh) ? ( {slippageError ? ( @@ -391,7 +275,7 @@ export function TransactionSettings() { ) : null} {isSmartSlippageApplied && ( - + @@ -401,7 +285,7 @@ export function TransactionSettings() { } /> Dynamic - + )} @@ -423,8 +307,8 @@ export function TransactionSettings() { /> - - + 0 @@ -441,7 +325,7 @@ export function TransactionSettings() { color={deadlineError ? 'red' : ''} disabled={isDeadlineDisabled} /> - + minutes @@ -449,6 +333,6 @@ export function TransactionSettings() { )}
    - + ) } diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/styled.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/styled.tsx new file mode 100644 index 0000000000..d3ef4a7381 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/styled.tsx @@ -0,0 +1,131 @@ +import { FancyButton, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' + +import { darken } from 'color2k' +import styled from 'styled-components/macro' + +export const Option = styled(FancyButton)<{ active: boolean }>` + margin-right: 8px; + + :hover { + cursor: pointer; + } + + &:disabled { + border: none; + pointer-events: none; + } +` + +export const Input = styled.input` + background: var(${UI.COLOR_PAPER}); + font-size: 16px; + width: auto; + outline: none; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + } + + color: ${({ theme, color }) => (color === 'red' ? theme.error : `var(${UI.COLOR_TEXT})`)}; + text-align: right; +` + +export const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>` + height: 2rem; + position: relative; + padding: 0 0.75rem; + flex: 1; + border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.error : theme.bg2}`}; + + :hover { + border: ${({ theme, active, warning }) => + active && `1px solid ${warning ? darken(theme.error, 0.1) : darken(theme.bg2, 0.1)}`}; + } + + input { + width: 100%; + height: 100%; + border: 0; + border-radius: 2rem; + } +` + +export const SlippageEmojiContainer = styled.span` + color: #f3841e; + ${Media.upToSmall()} { + display: none; + } +` + +export const SmartSlippageInfo = styled.div` + color: var(${UI.COLOR_GREEN}); + font-size: 13px; + text-align: right; + width: 100%; + padding-right: 0.2rem; + display: flex; + justify-content: flex-end; + padding-bottom: 0.35rem; + + > span { + margin-left: 4px; + } +` + +export const Wrapper = styled.div` + ${RowBetween} > button, ${OptionCustom} { + &:disabled { + color: var(${UI.COLOR_TEXT_OPACITY_50}); + background-color: var(${UI.COLOR_PAPER}); + border: none; + pointer-events: none; + } + } + + ${OptionCustom} { + background-color: var(${UI.COLOR_PAPER_DARKER}); + border: 0; + color: inherit; + + > div > input { + background: transparent; + color: inherit; + + &:disabled { + color: inherit; + background-color: inherit; + } + } + + > div > input::placeholder { + opacity: 0.5; + color: inherit; + } + } + + ${RowFixed} { + color: inherit; + + > div { + color: inherit; + opacity: 0.85; + } + + > button { + background-color: var(${UI.COLOR_PAPER_DARKER}); + border: 0; + } + + > button > input { + background: transparent; + color: inherit; + } + + > button > input::placeholder { + background: transparent; + opacity: 0.5; + color: inherit; + } + } +` diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/index.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/index.ts new file mode 100644 index 0000000000..5506835feb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/index.ts @@ -0,0 +1,7 @@ +export { RowDeadline } from './containers/RowDeadline' +export { TradeRateDetails } from './containers/TradeRateDetails' +export { SettingsTab } from './containers/SettingsTab' +export { HighFeeWarning } from './containers/HighFeeWarning' +export { BundleTxWrapBanner } from './containers/BundleTxWrapBanner' +export { useHighFeeWarning } from './containers/HighFeeWarning/hooks/useHighFeeWarning' +export { NetworkCostsTooltipSuffix } from './pure/NetworkCostsTooltipSuffix' diff --git a/apps/cowswap-frontend/src/modules/swap/pure/NetworkCostsTooltipSuffix.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/NetworkCostsTooltipSuffix.tsx similarity index 93% rename from apps/cowswap-frontend/src/modules/swap/pure/NetworkCostsTooltipSuffix.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/NetworkCostsTooltipSuffix.tsx index 60c9cc8290..204feb6e6a 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/NetworkCostsTooltipSuffix.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/NetworkCostsTooltipSuffix.tsx @@ -1,9 +1,9 @@ import { isTruthy } from '@cowprotocol/common-utils' import { useWalletDetails } from '@cowprotocol/wallet' -import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import { useIsEoaEthFlow } from 'modules/trade' -import { useIsEoaEthFlow } from '../hooks/useIsEoaEthFlow' +import useNativeCurrency from 'lib/hooks/useNativeCurrency' export function NetworkCostsTooltipSuffix() { const { allowsOffchainSigning } = useWalletDetails() diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.cosmos.tsx similarity index 92% rename from apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.cosmos.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.cosmos.tsx index 06633ec398..7b353c6f7f 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.cosmos.tsx @@ -3,7 +3,6 @@ import { MINIMUM_ETH_FLOW_DEADLINE_SECONDS } from '@cowprotocol/common-const' import { RowDeadlineContent, RowDeadlineProps } from '.' const defaultProps: RowDeadlineProps = { - toggleSettings: console.log, isEoaEthFlow: true, displayDeadline: Math.floor(MINIMUM_ETH_FLOW_DEADLINE_SECONDS / 60) + ' minutes', symbols: ['ETH', 'WETH'], diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.tsx new file mode 100644 index 0000000000..2cc56caa79 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowDeadline/index.tsx @@ -0,0 +1,51 @@ +import { HoverTooltip, RowFixed } from '@cowprotocol/ui' + +import { Trans } from '@lingui/macro' + +import { getNativeOrderDeadlineTooltip, getNonNativeOrderDeadlineTooltip } from 'common/utils/tradeSettingsTooltips' + +import { StyledRowBetween, TextWrapper, StyledInfoIcon, TransactionText, RowStyleProps } from '../styled' + +export interface RowDeadlineProps { + isEoaEthFlow: boolean + symbols?: (string | undefined)[] + displayDeadline: string + styleProps?: RowStyleProps + userDeadline: number + slippageLabel?: React.ReactNode + slippageTooltip?: React.ReactNode +} + +export function RowDeadlineContent(props: RowDeadlineProps) { + const { displayDeadline, isEoaEthFlow, symbols, styleProps } = props + const deadlineTooltipContent = isEoaEthFlow + ? getNativeOrderDeadlineTooltip(symbols) + : getNonNativeOrderDeadlineTooltip() + + return ( + + + + + + + + + + + {displayDeadline} + + + ) +} + +type DeadlineTextContentsProps = { isEoaEthFlow: boolean } + +function DeadlineTextContents({ isEoaEthFlow }: DeadlineTextContentsProps) { + return ( + + Transaction expiration + {isEoaEthFlow && (modified)} + + ) +} diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.cosmos.tsx similarity index 74% rename from apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.cosmos.tsx index f180f5effb..84cd07a457 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.cosmos.tsx @@ -1,6 +1,6 @@ import { Percent } from '@uniswap/sdk-core' -import { RowSlippageContent, RowSlippageContentProps } from 'modules/swap/pure/Row/RowSlippageContent' +import { RowSlippageContent, RowSlippageContentProps } from './index' const defaultProps: RowSlippageContentProps = { chainId: 1, @@ -10,9 +10,6 @@ const defaultProps: RowSlippageContentProps = { get displaySlippage() { return this.isEoaEthFlow ? '2%' : '0.2%' }, - toggleSettings() { - console.log('RowSlippageContent settings toggled!') - }, isSlippageModified: false, isSmartSlippageApplied: false, smartSlippage: '0.2%', diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx similarity index 55% rename from apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx index 07a343a2fd..75e4388c4b 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx @@ -1,4 +1,5 @@ -import { MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' +import { useSetAtom } from 'jotai' + import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' import { CenteredDots, HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' @@ -7,24 +8,10 @@ import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' import styled from 'styled-components/macro' -import { StyledRowBetween, TextWrapper } from 'modules/swap/pure/Row/styled' -import { RowStyleProps } from 'modules/swap/pure/Row/types' -import { StyledInfoIcon, TransactionText } from 'modules/swap/pure/styled' - -export const ClickableText = styled.button` - background: none; - border: none; - outline: none; - padding: 0; - margin: 0; - font-size: inherit; - font-weight: inherit; - color: inherit; - - > div { - display: inline-block; - } -` +import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' + +import { settingsTabStateAtom } from '../../../state/settingsTabState' +import { StyledRowBetween, TextWrapper, StyledInfoIcon, TransactionText, RowStyleProps } from '../styled' const DefaultSlippage = styled.span` display: inline-flex; @@ -42,47 +29,17 @@ const DefaultSlippage = styled.span` } ` -export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (string | undefined)[] | undefined) => ( - - When selling {symbols?.[0] || 'a native currency'}, the minimum slippage tolerance is set to{' '} - {MINIMUM_ETH_FLOW_SLIPPAGE[chainId].toSignificant(PERCENTAGE_PRECISION)}% to ensure a high likelihood of order - matching, even in volatile market conditions. -
    -
    - {symbols?.[0] || 'Native currency'} orders can, in rare cases, be frontrun due to their on-chain component. For more - robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. -
    -) -export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( - - CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the - best price.{' '} - {isSettingsModal ? ( - <> - To override this, enter your desired slippage amount. -
    -
    - Either way, your slippage is protected from MEV! - - ) : ( - "Trades are protected from MEV, so your slippage can't be exploited!" - )} -
    -) - const SUGGESTED_SLIPPAGE_TOOLTIP = 'This is the recommended slippage tolerance based on current gas prices & volatility. A lower amount may result in slower execution.' export interface RowSlippageContentProps { chainId: SupportedChainId - toggleSettings: Command displaySlippage: string isEoaEthFlow: boolean symbols?: (string | undefined)[] wrappedSymbol?: string styleProps?: RowStyleProps allowedSlippage: Percent - showSettingOnClick?: boolean slippageLabel?: React.ReactNode slippageTooltip?: React.ReactNode isSlippageModified: boolean @@ -92,13 +49,9 @@ export interface RowSlippageContentProps { isSmartSlippageLoading: boolean } -// TODO: RowDeadlineContent and RowSlippageContent are very similar. Refactor and extract base component? - export function RowSlippageContent(props: RowSlippageContentProps) { const { chainId, - showSettingOnClick, - toggleSettings, displaySlippage, isEoaEthFlow, symbols, @@ -112,6 +65,10 @@ export function RowSlippageContent(props: RowSlippageContentProps) { isSmartSlippageLoading, } = props + const setSettingTabState = useSetAtom(settingsTabStateAtom) + + const openSettings = () => setSettingTabState({ open: true }) + const tooltipContent = slippageTooltip || (isEoaEthFlow ? getNativeSlippageTooltip(chainId, symbols) : getNonNativeSlippageTooltip()) @@ -149,33 +106,19 @@ export function RowSlippageContent(props: RowSlippageContentProps) { return ( - - {showSettingOnClick ? ( - - - - ) : ( - - )} + + - - {showSettingOnClick ? ( - {displaySlippageWithLoader} - ) : ( - {displaySlippageWithLoader} - )} + + {displaySlippageWithLoader} ) diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/styled.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/styled.ts similarity index 73% rename from apps/cowswap-frontend/src/modules/swap/pure/Row/styled.ts rename to apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/styled.ts index 0ace5489f6..121d164342 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/styled.ts +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/styled.ts @@ -2,10 +2,15 @@ import { Media, UI } from '@cowprotocol/ui' import { RowBetween, RowFixed } from '@cowprotocol/ui' import { HoverTooltip } from '@cowprotocol/ui' +import { Info } from 'react-feather' import { Text } from 'rebass' import styled from 'styled-components/macro' -import { RowStyleProps } from './types' +export interface RowStyleProps { + fontWeight?: number + fontSize?: number + alignContentRight?: boolean +} const StyledHoverTooltip = styled(HoverTooltip)`` export const TextWrapper = styled(Text)<{ success?: boolean }>` @@ -58,3 +63,25 @@ export const StyledRowBetween = styled(RowBetween)` color: inherit; } ` + +export const StyledInfoIcon = styled(Info)` + color: inherit; + opacity: 0.6; + line-height: 0; + vertical-align: middle; + transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + + &:hover { + opacity: 1; + } +` + +export const TransactionText = styled.span` + display: flex; + gap: 3px; + cursor: pointer; + + > i { + font-style: normal; + } +` diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts new file mode 100644 index 0000000000..659fb6321f --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/state/settingsTabState.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai' + +export const settingsTabStateAtom = atom({ open: false }) diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx index 9ea59d9265..ad08864297 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx @@ -1,13 +1,11 @@ import { useAtomValue } from 'jotai' -import React, { useState } from 'react' +import React from 'react' import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' -import { useInjectedWidgetParams } from 'modules/injectedWidget' import { TradeConfirmation, TradeConfirmModal, useTradeConfirmActions, useTradePriceImpact } from 'modules/trade' import { TradeBasicConfirmDetails } from 'modules/trade/containers/TradeBasicConfirmDetails' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { DividerHorizontal } from 'modules/trade/pure/Row/styled' import { PRICE_UPDATE_INTERVAL } from 'modules/tradeQuote/hooks/useTradeQuotePolling' @@ -20,7 +18,6 @@ import { useCreateTwapOrder } from '../../hooks/useCreateTwapOrder' import { useIsFallbackHandlerRequired } from '../../hooks/useFallbackHandlerVerification' import { useTwapFormState } from '../../hooks/useTwapFormState' import { useTwapSlippage } from '../../hooks/useTwapSlippage' -import { useTwapWarningsContext } from '../../hooks/useTwapWarningsContext' import { scaledReceiveAmountInfoAtom } from '../../state/scaledReceiveAmountInfoAtom' import { twapOrderAtom } from '../../state/twapOrderAtom' import { TwapFormWarnings } from '../TwapFormWarnings' @@ -70,15 +67,10 @@ export function TwapConfirmModal() { const twapOrder = useAtomValue(twapOrderAtom) const receiveAmountInfo = useAtomValue(scaledReceiveAmountInfoAtom) const slippage = useTwapSlippage() - const { showPriceImpactWarning } = useTwapWarningsContext() const localFormValidation = useTwapFormState() const tradeConfirmActions = useTradeConfirmActions() const createTwapOrder = useCreateTwapOrder() - const widgetParams = useInjectedWidgetParams() - - const isInvertedState = useState(false) - const isConfirmDisabled = !!localFormValidation const priceImpact = useTradePriceImpact() @@ -121,40 +113,40 @@ export function TwapConfirmModal() { refreshInterval={PRICE_UPDATE_INTERVAL} recipient={recipient} > - <> - {receiveAmountInfo && numOfParts && ( - : null, - networkCostsTooltipSuffix: !allowsOffchainSigning ? ( - <> -
    -
    - Because you are using a smart contract wallet, you will pay a separate gas cost for signing the - order placement on-chain. - - ) : null, - }} + {(warnings) => ( + <> + {receiveAmountInfo && numOfParts && ( + : null, + networkCostsTooltipSuffix: !allowsOffchainSigning ? ( + <> +
    +
    + Because you are using a smart contract wallet, you will pay a separate gas cost for signing the + order placement on-chain. + + ) : null, + }} + /> + )} + + - )} - - - {showPriceImpactWarning && } - - + {warnings} + + + )} ) diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx index 07fd7f1b14..ec1f665172 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx @@ -1,20 +1,14 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' -import { BundleTxApprovalBanner } from '@cowprotocol/ui' import { useIsSafeViaWc, useWalletInfo } from '@cowprotocol/wallet' -import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' import { modifySafeHandlerAnalytics } from 'modules/analytics' import { SellNativeWarningBanner } from 'modules/trade/containers/SellNativeWarningBanner' import { useTradeRouteContext } from 'modules/trade/hooks/useTradeRouteContext' -import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { useGetTradeFormValidation } from 'modules/tradeFormValidation' import { TradeFormValidation } from 'modules/tradeFormValidation/types' import { useTradeQuoteFeeFiatAmount } from 'modules/tradeQuote' -import { useShouldZeroApprove } from 'modules/zeroApproval' - -import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' import { FallbackHandlerWarning, @@ -31,32 +25,28 @@ import { useTwapSlippage } from '../../hooks/useTwapSlippage' import { useTwapWarningsContext } from '../../hooks/useTwapWarningsContext' import { TwapFormState } from '../../pure/PrimaryActionButton/getTwapFormState' import { swapAmountDifferenceAtom } from '../../state/swapAmountDifferenceAtom' -import { twapDeadlineAtom, twapOrderAtom } from '../../state/twapOrderAtom' +import { twapDeadlineAtom } from '../../state/twapOrderAtom' import { twapOrdersSettingsAtom, updateTwapOrdersSettingsAtom } from '../../state/twapOrdersSettingsAtom' import { isPriceProtectionNotEnough } from '../../utils/isPriceProtectionNotEnough' -const BUNDLE_APPROVAL_STATES = [TradeFormValidation.ApproveAndSwap] - interface TwapFormWarningsProps { localFormValidation: TwapFormState | null isConfirmationModal?: boolean } export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: TwapFormWarningsProps) { - const { isFallbackHandlerSetupAccepted, isPriceImpactAccepted } = useAtomValue(twapOrdersSettingsAtom) + const { isFallbackHandlerSetupAccepted } = useAtomValue(twapOrdersSettingsAtom) const updateTwapOrdersSettings = useSetAtom(updateTwapOrdersSettingsAtom) - const twapOrder = useAtomValue(twapOrderAtom) const slippage = useTwapSlippage() const deadline = useAtomValue(twapDeadlineAtom) const swapAmountDifference = useAtomValue(swapAmountDifferenceAtom) - const { outputCurrencyAmount } = useAdvancedOrdersDerivedState() const primaryFormValidation = useGetTradeFormValidation() const { chainId } = useWalletInfo() const isFallbackHandlerRequired = useIsFallbackHandlerRequired() const isSafeViaWc = useIsSafeViaWc() const tradeQuoteFeeFiatAmount = useTradeQuoteFeeFiatAmount() - const { canTrade, showPriceImpactWarning, walletIsNotConnected } = useTwapWarningsContext() + const { canTrade, walletIsNotConnected } = useTwapWarningsContext() const tradeUrlParams = useTradeRouteContext() const toggleFallbackHandlerSetupFlag = useCallback( @@ -67,17 +57,9 @@ export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: T [updateTwapOrdersSettings], ) - const shouldZeroApprove = useShouldZeroApprove(twapOrder?.sellAmount) - const showZeroApprovalWarning = !isConfirmationModal && shouldZeroApprove && outputCurrencyAmount !== null - const showApprovalBundlingBanner = - !isConfirmationModal && primaryFormValidation && BUNDLE_APPROVAL_STATES.includes(primaryFormValidation) const showTradeFormWarnings = !isConfirmationModal && canTrade const showFallbackHandlerWarning = showTradeFormWarnings && isFallbackHandlerRequired - const setIsPriceImpactAccepted = useCallback(() => { - updateTwapOrdersSettings({ isPriceImpactAccepted: !isPriceImpactAccepted }) - }, [updateTwapOrdersSettings, isPriceImpactAccepted]) - // Don't display any warnings while a wallet is not connected if (walletIsNotConnected) return null @@ -91,17 +73,6 @@ export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: T return ( <> - {showZeroApprovalWarning && } - {showApprovalBundlingBanner && } - - {!isConfirmationModal && showPriceImpactWarning && ( - setIsPriceImpactAccepted()} - /> - )} - {(() => { if (localFormValidation === TwapFormState.NOT_SAFE) { return diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx index 41050dad54..3050f4d664 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx @@ -1,5 +1,5 @@ import { useAtomValue, useSetAtom } from 'jotai' -import { useEffect, useLayoutEffect, useMemo, useState } from 'react' +import { ReactNode, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { renderTooltip } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' @@ -48,7 +48,11 @@ import { TwapFormWarnings } from '../TwapFormWarnings' export type { LabelTooltip, LabelTooltipItems } from './tooltips' -export function TwapFormWidget() { +interface TwapFormWidget { + tradeWarnings: ReactNode +} + +export function TwapFormWidget({ tradeWarnings }: TwapFormWidget) { const { account } = useWalletInfo() const { numberOfPartsValue, deadline, customDeadline, isCustomDeadline } = useAtomValue(twapOrdersSettingsAtom) @@ -110,7 +114,7 @@ export function TwapFormWidget() { // Reset warnings flags once on start useEffect(() => { - updateSettingsState({ isFallbackHandlerSetupAccepted: false, isPriceImpactAccepted: false }) + updateSettingsState({ isFallbackHandlerSetupAccepted: false }) openAdvancedOrdersTabAnalytics() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -217,6 +221,7 @@ export function TwapFormWidget() { + {tradeWarnings} { const canTrade = !primaryFormValidation || NOT_BLOCKING_VALIDATIONS.includes(primaryFormValidation) - const showPriceImpactWarning = canTrade && !priceImpactParams.loading && !priceImpactParams.priceImpact const walletIsNotConnected = !account return { canTrade, - showPriceImpactWarning, walletIsNotConnected, } - }, [primaryFormValidation, account, priceImpactParams]) + }, [primaryFormValidation, account]) } diff --git a/apps/cowswap-frontend/src/modules/twap/state/twapOrdersSettingsAtom.ts b/apps/cowswap-frontend/src/modules/twap/state/twapOrdersSettingsAtom.ts index faaecd4b8d..fa99ca5455 100644 --- a/apps/cowswap-frontend/src/modules/twap/state/twapOrdersSettingsAtom.ts +++ b/apps/cowswap-frontend/src/modules/twap/state/twapOrdersSettingsAtom.ts @@ -21,7 +21,6 @@ export interface TwapOrdersSettingsState extends TwapOrdersDeadline { readonly numberOfPartsValue: number readonly slippageValue: number | null readonly isFallbackHandlerSetupAccepted: boolean - readonly isPriceImpactAccepted: boolean } export const defaultCustomDeadline: TwapOrdersDeadline['customDeadline'] = { @@ -38,13 +37,12 @@ export const defaultTwapOrdersSettings: TwapOrdersSettingsState = { // null = auto slippageValue: null, isFallbackHandlerSetupAccepted: false, - isPriceImpactAccepted: false, } export const twapOrdersSettingsAtom = atomWithStorage( 'twap-orders-settings-atom:v1', defaultTwapOrdersSettings, - getJotaiIsolatedStorage() + getJotaiIsolatedStorage(), ) export const updateTwapOrdersSettingsAtom = atom(null, (get, set, nextState: Partial) => { diff --git a/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts b/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts index ebe42b5f42..60b30aa4e0 100644 --- a/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts +++ b/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts @@ -49,4 +49,5 @@ const TradeTypeMap: Record = { [TradeType.SWAP]: WidgetTradeType.SWAP, [TradeType.LIMIT_ORDER]: WidgetTradeType.LIMIT, [TradeType.ADVANCED_ORDERS]: WidgetTradeType.ADVANCED, + [TradeType.YIELD]: WidgetTradeType.YIELD, } diff --git a/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx new file mode 100644 index 0000000000..5dc96fefef --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx @@ -0,0 +1,33 @@ +import { useIsNoImpactWarningAccepted, useTradeConfirmActions } from 'modules/trade' +import { TradeFormButtons, useGetTradeFormValidation, useTradeFormButtonContext } from 'modules/tradeFormValidation' +import { useHighFeeWarning } from 'modules/tradeWidgetAddons' + +const CONFIRM_TEXT = 'Swap' + +interface TradeButtonsProps { + isTradeContextReady: boolean +} + +export function TradeButtons({ isTradeContextReady }: TradeButtonsProps) { + const primaryFormValidation = useGetTradeFormValidation() + const tradeConfirmActions = useTradeConfirmActions() + const { feeWarningAccepted } = useHighFeeWarning() + const isNoImpactWarningAccepted = useIsNoImpactWarningAccepted() + + const confirmTrade = tradeConfirmActions.onOpen + + const tradeFormButtonContext = useTradeFormButtonContext(CONFIRM_TEXT, confirmTrade) + + const isDisabled = !isTradeContextReady || !feeWarningAccepted || !isNoImpactWarningAccepted + + if (!tradeFormButtonContext) return null + + return ( + + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/containers/Warnings/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/Warnings/index.tsx new file mode 100644 index 0000000000..c81c3644e0 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/Warnings/index.tsx @@ -0,0 +1,9 @@ +import { HighFeeWarning } from 'modules/tradeWidgetAddons' + +export function Warnings() { + return ( + <> + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldConfirmModal/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldConfirmModal/index.tsx new file mode 100644 index 0000000000..08b205bac0 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldConfirmModal/index.tsx @@ -0,0 +1,100 @@ +import React, { useMemo } from 'react' + +import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' + +import type { PriceImpact } from 'legacy/hooks/usePriceImpact' + +import { useAppData } from 'modules/appData' +import { + TradeBasicConfirmDetails, + TradeConfirmation, + TradeConfirmModal, + useOrderSubmittedContent, + useReceiveAmountInfo, + useTradeConfirmActions, +} from 'modules/trade' +import { HighFeeWarning } from 'modules/tradeWidgetAddons' + +import { useRateInfoParams } from 'common/hooks/useRateInfoParams' +import { CurrencyPreviewInfo } from 'common/pure/CurrencyAmountPreview' +import { getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' + +import { useYieldDerivedState } from '../../hooks/useYieldDerivedState' + +const CONFIRM_TITLE = 'Confirm order' + +const labelsAndTooltips = { + slippageTooltip: getNonNativeSlippageTooltip(), +} + +export interface YieldConfirmModalProps { + doTrade(): Promise + + inputCurrencyInfo: CurrencyPreviewInfo + outputCurrencyInfo: CurrencyPreviewInfo + priceImpact: PriceImpact + recipient?: string | null +} + +export function YieldConfirmModal(props: YieldConfirmModalProps) { + const { inputCurrencyInfo, outputCurrencyInfo, priceImpact, recipient, doTrade: _doTrade } = props + + /** + * This is a very important part of the code. + * After the confirmation modal opens, the trade context should not be recreated. + * In order to prevent this, we use useMemo to keep the trade context the same when the modal was opened. + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + const doTrade = useMemo(() => _doTrade, []) + + const { account, chainId } = useWalletInfo() + const { ensName } = useWalletDetails() + const appData = useAppData() + const receiveAmountInfo = useReceiveAmountInfo() + const tradeConfirmActions = useTradeConfirmActions() + const { slippage } = useYieldDerivedState() + + const rateInfoParams = useRateInfoParams(inputCurrencyInfo.amount, outputCurrencyInfo.amount) + const submittedContent = useOrderSubmittedContent(chainId) + + return ( + + + {(restContent) => ( + <> + {receiveAmountInfo && slippage && ( + + )} + {restContent} + + + )} + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx new file mode 100644 index 0000000000..b034093df9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -0,0 +1,136 @@ +import { ReactNode, useCallback } from 'react' + +import { Field } from 'legacy/state/types' + +import { + TradeWidget, + TradeWidgetSlots, + useReceiveAmountInfo, + useTradeConfirmState, + useTradePriceImpact, +} from 'modules/trade' +import { useHandleSwap } from 'modules/tradeFlow' +import { useTradeQuote } from 'modules/tradeQuote' +import { SettingsTab, TradeRateDetails } from 'modules/tradeWidgetAddons' + +import { useRateInfoParams } from 'common/hooks/useRateInfoParams' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' +import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' + +import { useYieldDerivedState } from '../../hooks/useYieldDerivedState' +import { useYieldDeadlineState, useYieldRecipientToggleState, useYieldSettings } from '../../hooks/useYieldSettings' +import { useYieldWidgetActions } from '../../hooks/useYieldWidgetActions' +import { TradeButtons } from '../TradeButtons' +import { Warnings } from '../Warnings' +import { YieldConfirmModal } from '../YieldConfirmModal' + +export function YieldWidget() { + const { showRecipient } = useYieldSettings() + const deadlineState = useYieldDeadlineState() + const recipientToggleState = useYieldRecipientToggleState() + const { isLoading: isRateLoading } = useTradeQuote() + const priceImpact = useTradePriceImpact() + const { isOpen: isConfirmOpen } = useTradeConfirmState() + const widgetActions = useYieldWidgetActions() + const receiveAmountInfo = useReceiveAmountInfo() + + const { + inputCurrency, + outputCurrency, + inputCurrencyAmount, + outputCurrencyAmount, + inputCurrencyBalance, + outputCurrencyBalance, + inputCurrencyFiatAmount, + outputCurrencyFiatAmount, + recipient, + } = useYieldDerivedState() + const doTrade = useHandleSwap(useSafeMemoObject({ deadline: deadlineState[0] })) + + const inputCurrencyInfo: CurrencyInfo = { + field: Field.INPUT, + currency: inputCurrency, + amount: inputCurrencyAmount, + isIndependent: true, + balance: inputCurrencyBalance, + fiatAmount: inputCurrencyFiatAmount, + receiveAmountInfo: null, + } + const outputCurrencyInfo: CurrencyInfo = { + field: Field.OUTPUT, + currency: outputCurrency, + amount: outputCurrencyAmount, + isIndependent: false, + balance: outputCurrencyBalance, + fiatAmount: outputCurrencyFiatAmount, + receiveAmountInfo, + } + const inputCurrencyPreviewInfo = { + amount: inputCurrencyInfo.amount, + fiatAmount: inputCurrencyInfo.fiatAmount, + balance: inputCurrencyInfo.balance, + label: 'Sell amount', + } + + const outputCurrencyPreviewInfo = { + amount: outputCurrencyInfo.amount, + fiatAmount: outputCurrencyInfo.fiatAmount, + balance: outputCurrencyInfo.balance, + label: 'Receive (before fees)', + } + + const rateInfoParams = useRateInfoParams(inputCurrencyInfo.amount, outputCurrencyInfo.amount) + + const slots: TradeWidgetSlots = { + settingsWidget: , + bottomContent: useCallback( + (tradeWarnings: ReactNode | null) => { + return ( + <> + + + {tradeWarnings} + + + ) + }, + [doTrade.contextIsReady, isRateLoading, rateInfoParams, deadlineState], + ), + } + + const params = { + compactView: true, + enableSmartSlippage: true, + recipient, + showRecipient, + isTradePriceUpdating: isRateLoading, + priceImpact, + disableQuotePolling: isConfirmOpen, + } + + return ( + + ) : null + } + /> + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateCurrencyAmount.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateCurrencyAmount.ts new file mode 100644 index 0000000000..701a2d52ae --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateCurrencyAmount.ts @@ -0,0 +1,22 @@ +import { useCallback } from 'react' + +import { FractionUtils } from '@cowprotocol/common-utils' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import { Field } from 'legacy/state/types' + +import { useUpdateYieldRawState } from './useUpdateYieldRawState' + +export function useUpdateCurrencyAmount() { + const updateYieldState = useUpdateYieldRawState() + + return useCallback( + (field: Field, value: CurrencyAmount) => { + updateYieldState({ + [field === Field.INPUT ? 'inputCurrencyAmount' : 'outputCurrencyAmount']: + FractionUtils.serializeFractionToJSON(value), + }) + }, + [updateYieldState], + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateYieldRawState.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateYieldRawState.ts new file mode 100644 index 0000000000..11b110957d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useUpdateYieldRawState.ts @@ -0,0 +1,7 @@ +import { useSetAtom } from 'jotai' + +import { updateYieldRawStateAtom } from '../state/yieldRawStateAtom' + +export function useUpdateYieldRawState() { + return useSetAtom(updateYieldRawStateAtom) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldDerivedState.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldDerivedState.ts new file mode 100644 index 0000000000..d2554d390a --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldDerivedState.ts @@ -0,0 +1,26 @@ +import { useAtomValue } from 'jotai' +import { useSetAtom } from 'jotai/index' +import { useEffect } from 'react' + +import { INITIAL_ALLOWED_SLIPPAGE_PERCENT } from '@cowprotocol/common-const' + +import { TradeType, useBuildTradeDerivedState } from 'modules/trade' + +import { yieldDerivedStateAtom, yieldRawStateAtom } from '../state/yieldRawStateAtom' + +export function useYieldDerivedState() { + return useAtomValue(yieldDerivedStateAtom) +} + +export function useFillYieldDerivedState() { + const updateDerivedState = useSetAtom(yieldDerivedStateAtom) + const derivedState = useBuildTradeDerivedState(yieldRawStateAtom) + + useEffect(() => { + updateDerivedState({ + ...derivedState, + slippage: INITIAL_ALLOWED_SLIPPAGE_PERCENT, + tradeType: TradeType.YIELD, + }) + }, [derivedState, updateDerivedState]) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldRawState.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldRawState.ts new file mode 100644 index 0000000000..bc09f9631d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldRawState.ts @@ -0,0 +1,7 @@ +import { useAtomValue } from 'jotai' + +import { yieldRawStateAtom } from '../state/yieldRawStateAtom' + +export function useYieldRawState() { + return useAtomValue(yieldRawStateAtom) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts new file mode 100644 index 0000000000..6ba8f042a7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts @@ -0,0 +1,30 @@ +import { useSetAtom } from 'jotai' +import { useAtomValue } from 'jotai/index' +import { useMemo } from 'react' + +import { StatefulValue } from '@cowprotocol/types' + +import { updateYieldSettingsAtom, yieldSettingsAtom } from '../state/yieldSettingsAtom' + +export function useYieldSettings() { + return useAtomValue(yieldSettingsAtom) +} + +export function useYieldDeadlineState(): StatefulValue { + const updateState = useSetAtom(updateYieldSettingsAtom) + const settings = useYieldSettings() + + return useMemo( + () => [settings.deadline, (deadline: number) => updateState({ deadline })], + [settings.deadline, updateState], + ) +} +export function useYieldRecipientToggleState(): StatefulValue { + const updateState = useSetAtom(updateYieldSettingsAtom) + const settings = useYieldSettings() + + return useMemo( + () => [settings.showRecipient, (showRecipient: boolean) => updateState({ showRecipient })], + [settings.showRecipient, updateState], + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts new file mode 100644 index 0000000000..88b044dc26 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts @@ -0,0 +1,47 @@ +import { useCallback, useMemo } from 'react' + +import { isSellOrder, tryParseCurrencyAmount } from '@cowprotocol/common-utils' +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { Field } from 'legacy/state/types' + +import { TradeWidgetActions, useIsWrapOrUnwrap, useOnCurrencySelection, useSwitchTokensPlaces } from 'modules/trade' + +import { useUpdateCurrencyAmount } from './useUpdateCurrencyAmount' +import { useUpdateYieldRawState } from './useUpdateYieldRawState' +import { useYieldDerivedState } from './useYieldDerivedState' + +export function useYieldWidgetActions(): TradeWidgetActions { + const { inputCurrency, outputCurrency, orderKind } = useYieldDerivedState() + const isWrapOrUnwrap = useIsWrapOrUnwrap() + const updateYieldState = useUpdateYieldRawState() + const onCurrencySelection = useOnCurrencySelection() + const updateCurrencyAmount = useUpdateCurrencyAmount() + + const onUserInput = useCallback( + (field: Field, typedValue: string) => { + const currency = field === Field.INPUT ? inputCurrency : outputCurrency + + if (!currency) return + + const value = tryParseCurrencyAmount(typedValue, currency) || null + + updateCurrencyAmount(field, value) + }, + [updateCurrencyAmount, isWrapOrUnwrap, inputCurrency, outputCurrency], + ) + + const onSwitchTokens = useSwitchTokensPlaces({ + orderKind: isSellOrder(orderKind) ? OrderKind.BUY : OrderKind.SELL, + }) + + const onChangeRecipient = useCallback( + (recipient: string | null) => updateYieldState({ recipient }), + [updateYieldState], + ) + + return useMemo( + () => ({ onUserInput, onSwitchTokens, onChangeRecipient, onCurrencySelection }), + [onUserInput, onSwitchTokens, onChangeRecipient, onCurrencySelection], + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/index.ts b/apps/cowswap-frontend/src/modules/yield/index.ts new file mode 100644 index 0000000000..54d34b9a12 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/index.ts @@ -0,0 +1,5 @@ +export { YieldWidget } from './containers/YieldWidget' +export { useYieldRawState } from './hooks/useYieldRawState' +export { useUpdateYieldRawState } from './hooks/useUpdateYieldRawState' +export { YieldUpdaters } from './updaters' +export { yieldDerivedStateAtom } from './state/yieldRawStateAtom' diff --git a/apps/cowswap-frontend/src/modules/yield/state/yieldRawStateAtom.ts b/apps/cowswap-frontend/src/modules/yield/state/yieldRawStateAtom.ts new file mode 100644 index 0000000000..b378ead59d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/state/yieldRawStateAtom.ts @@ -0,0 +1,37 @@ +import { atom } from 'jotai/index' +import { atomWithStorage } from 'jotai/utils' + +import { atomWithPartialUpdate } from '@cowprotocol/common-utils' +import { getJotaiIsolatedStorage } from '@cowprotocol/core' +import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' + +import { DEFAULT_TRADE_DERIVED_STATE, TradeDerivedState } from 'modules/trade/types/TradeDerivedState' +import { ExtendedTradeRawState, getDefaultTradeRawState } from 'modules/trade/types/TradeRawState' + +export interface YieldDerivedState extends TradeDerivedState {} + +export interface YieldRawState extends ExtendedTradeRawState {} + +export function getDefaultYieldState(chainId: SupportedChainId | null): YieldRawState { + return { + ...getDefaultTradeRawState(chainId), + inputCurrencyAmount: null, + outputCurrencyAmount: null, + orderKind: OrderKind.SELL, + } +} + +const rawState = atomWithPartialUpdate( + atomWithStorage('yieldStateAtom:v1', getDefaultYieldState(null), getJotaiIsolatedStorage()), +) + +export const yieldRawStateAtom = atom((get) => ({ + ...get(rawState.atom), + orderKind: OrderKind.SELL, +})) + +export const updateYieldRawStateAtom = rawState.updateAtom + +export const yieldDerivedStateAtom = atom({ + ...DEFAULT_TRADE_DERIVED_STATE, +}) diff --git a/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts b/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts new file mode 100644 index 0000000000..116c884354 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts @@ -0,0 +1,19 @@ +import { atomWithStorage } from 'jotai/utils' + +import { DEFAULT_DEADLINE_FROM_NOW } from '@cowprotocol/common-const' +import { atomWithPartialUpdate } from '@cowprotocol/common-utils' +import { getJotaiIsolatedStorage } from '@cowprotocol/core' + +export interface YieldSettingsState { + readonly showRecipient: boolean + readonly deadline: number +} + +export const defaultYieldSettings: YieldSettingsState = { + showRecipient: false, + deadline: DEFAULT_DEADLINE_FROM_NOW, +} + +export const { atom: yieldSettingsAtom, updateAtom: updateYieldSettingsAtom } = atomWithPartialUpdate( + atomWithStorage('yieldSettingsAtom:v0', defaultYieldSettings, getJotaiIsolatedStorage()), +) diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/QuoteObserverUpdater/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/QuoteObserverUpdater/index.tsx new file mode 100644 index 0000000000..2390d91029 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/updaters/QuoteObserverUpdater/index.tsx @@ -0,0 +1,40 @@ +import { useEffect, useLayoutEffect } from 'react' + +import { CurrencyAmount } from '@uniswap/sdk-core' + +import { Field } from 'legacy/state/types' + +import { useReceiveAmountInfo, useDerivedTradeState } from 'modules/trade' + +import { useUpdateCurrencyAmount } from '../../hooks/useUpdateCurrencyAmount' + +export function QuoteObserverUpdater() { + const state = useDerivedTradeState() + const receiveAmountInfo = useReceiveAmountInfo() + const { beforeNetworkCosts } = receiveAmountInfo || {} + + const updateCurrencyAmount = useUpdateCurrencyAmount() + + const inputCurrency = state?.inputCurrency + const outputCurrency = state?.outputCurrency + + // Set the output amount from quote response (receiveAmountInfo is a derived state from tradeQuote state) + useLayoutEffect(() => { + if (!outputCurrency || !inputCurrency || !beforeNetworkCosts?.buyAmount) { + return + } + + updateCurrencyAmount(Field.OUTPUT, beforeNetworkCosts.buyAmount) + }, [beforeNetworkCosts, inputCurrency, outputCurrency, updateCurrencyAmount]) + + // Reset the output amount when the input amount changes + useEffect(() => { + if (!outputCurrency) { + return + } + + updateCurrencyAmount(Field.OUTPUT, CurrencyAmount.fromRawAmount(outputCurrency, 0)) + }, [state?.inputCurrencyAmount, state?.inputCurrency, updateCurrencyAmount, outputCurrency]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx new file mode 100644 index 0000000000..e46923e292 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx @@ -0,0 +1,23 @@ +import { INITIAL_ALLOWED_SLIPPAGE_PERCENT } from '@cowprotocol/common-const' +import { percentToBps } from '@cowprotocol/common-utils' + +import { AppDataUpdater } from 'modules/appData' +import { useSetTradeQuoteParams } from 'modules/tradeQuote' + +import { QuoteObserverUpdater } from './QuoteObserverUpdater' + +import { useFillYieldDerivedState, useYieldDerivedState } from '../hooks/useYieldDerivedState' + +export function YieldUpdaters() { + const { inputCurrencyAmount } = useYieldDerivedState() + + useFillYieldDerivedState() + useSetTradeQuoteParams(inputCurrencyAmount, true) + + return ( + <> + + + + ) +} diff --git a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx index c1b37aa56f..db9d944cb9 100644 --- a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx @@ -48,8 +48,12 @@ export default function AdvancedOrdersPage() { params={advancedWidgetParams} mapCurrencyInfo={mapTwapCurrencyInfo} > - {/*TODO: conditionally display a widget for current advanced order type*/} - + {(tradeWarnings) => ( + <> + {/*TODO: conditionally display a widget for current advanced order type*/} + + + )} diff --git a/apps/cowswap-frontend/src/pages/Yield/index.tsx b/apps/cowswap-frontend/src/pages/Yield/index.tsx new file mode 100644 index 0000000000..0c4d78cf01 --- /dev/null +++ b/apps/cowswap-frontend/src/pages/Yield/index.tsx @@ -0,0 +1,10 @@ +import { YieldWidget, YieldUpdaters } from 'modules/yield' + +export default function YieldPage() { + return ( + <> + + + + ) +} diff --git a/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts b/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts index 3094b3c877..fcffd64457 100644 --- a/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts +++ b/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts @@ -25,6 +25,7 @@ export const ORDER_UI_TYPE_TITLES: Record = { [UiOrderType.LIMIT]: 'Limit order', [UiOrderType.TWAP]: 'TWAP order', [UiOrderType.HOOKS]: 'Hooks', + [UiOrderType.YIELD]: 'Yield', } export type UiOrderTypeParams = Pick diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts index 2d229cce5e..30cf0b2f20 100644 --- a/apps/widget-configurator/src/app/configurator/consts.ts +++ b/apps/widget-configurator/src/app/configurator/consts.ts @@ -15,7 +15,7 @@ export const DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK: Record = { tradeType: 'swap, limit or advanced', sell: 'Sell token. Optionally add amount for sell orders', buy: 'Buy token. Optionally add amount for buy orders', - enabledTradeTypes: 'swap, limit and/or advanced', + enabledTradeTypes: 'swap, limit, advanced, yield', partnerFee: 'Partner fee, in Basis Points (BPS) and a receiver address', } diff --git a/libs/types/src/common.ts b/libs/types/src/common.ts index 8b40e93722..50ff3e3638 100644 --- a/libs/types/src/common.ts +++ b/libs/types/src/common.ts @@ -1,5 +1,7 @@ export type Command = () => void +export type StatefulValue = [T, (value: T) => void] + /** * UI order type that is different from existing types or classes * @@ -11,6 +13,7 @@ export enum UiOrderType { LIMIT = 'LIMIT', TWAP = 'TWAP', HOOKS = 'HOOKS', + YIELD = 'YIELD', } export type TokenInfo = { diff --git a/libs/ui/src/containers/InlineBanner/banners.tsx b/libs/ui/src/containers/InlineBanner/banners.tsx index 993d6de17a..699261d24b 100644 --- a/libs/ui/src/containers/InlineBanner/banners.tsx +++ b/libs/ui/src/containers/InlineBanner/banners.tsx @@ -3,7 +3,6 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import styled from 'styled-components/macro' -import { CowSwapSafeAppLink } from '../../containers/CowSwapSafeAppLink' import { ButtonSecondaryAlt } from '../../pure/ButtonSecondaryAlt' import { LinkStyledButton } from '../../pure/LinkStyledButton' import { TokenAmount } from '../../pure/TokenAmount' @@ -16,57 +15,6 @@ export enum BannerOrientation { Vertical = 'vertical', } -export function BundleTxApprovalBanner() { - return ( - - Token approval bundling -

    - For your convenience, token approval and order placement will be bundled into a single transaction, streamlining - your experience! -

    -
    - ) -} - -export type BundleTxWrapBannerProps = { - nativeCurrencySymbol: string - wrappedCurrencySymbol: string -} - -export function BundleTxWrapBanner({ nativeCurrencySymbol, wrappedCurrencySymbol }: BundleTxWrapBannerProps) { - return ( - - Token wrapping bundling -

    - For your convenience, CoW Swap will bundle all the necessary actions for this trade into a single transaction. - This includes the {nativeCurrencySymbol} wrapping and, if needed, {wrappedCurrencySymbol} -  approval. Even if the trade fails, your wrapping and approval will be done! -

    -
    - ) -} - -// If supportsWrapping is true, nativeCurrencySymbol is required -type WrappingSupportedProps = { supportsWrapping: true; nativeCurrencySymbol: string } -// If supportsWrapping is not set or false, nativeCurrencySymbol is not required -type WrappingUnsupportedProps = { supportsWrapping?: false; nativeCurrencySymbol?: undefined } - -export type BundleTxSafeWcBannerProps = WrappingSupportedProps | WrappingUnsupportedProps - -export function BundleTxSafeWcBanner({ nativeCurrencySymbol, supportsWrapping }: BundleTxSafeWcBannerProps) { - const supportsWrappingText = supportsWrapping ? `${nativeCurrencySymbol} wrapping, ` : '' - - return ( - - Use Safe web app -

    - Use the Safe web app for streamlined trading: {supportsWrappingText}token approval and orders bundled in one go! - Only available in the -

    -
    - ) -} - export type SmallVolumeWarningBannerProps = { feePercentage: Nullish feeAmount: Nullish> diff --git a/libs/ui/src/containers/InlineBanner/index.tsx b/libs/ui/src/containers/InlineBanner/index.tsx index 0d994b1490..1e9ae9195b 100644 --- a/libs/ui/src/containers/InlineBanner/index.tsx +++ b/libs/ui/src/containers/InlineBanner/index.tsx @@ -168,6 +168,7 @@ export interface InlineBannerProps { padding?: string margin?: string width?: string + noWrapContent?: boolean onClose?: () => void } @@ -185,6 +186,7 @@ export function InlineBanner({ margin, width, onClose, + noWrapContent, }: InlineBannerProps) { const colorEnums = getColorEnums(bannerType) @@ -213,7 +215,7 @@ export function InlineBanner({ ) : !hideIcon && colorEnums.iconText ? ( {colorEnums.iconText} ) : null} - {children} + {noWrapContent ? children : {children}} {onClose && } diff --git a/libs/ui/src/pure/InfoTooltip/index.tsx b/libs/ui/src/pure/InfoTooltip/index.tsx index 4d26d37df3..03ad62fb31 100644 --- a/libs/ui/src/pure/InfoTooltip/index.tsx +++ b/libs/ui/src/pure/InfoTooltip/index.tsx @@ -32,16 +32,17 @@ const StyledTooltipContainer = styled(TooltipContainer)` export interface InfoTooltipProps { content: ReactNode + size?: number className?: string } -export function InfoTooltip({ content, className }: InfoTooltipProps) { +export function InfoTooltip({ content, className, size = 16 }: InfoTooltipProps) { const tooltipContent = {content} return ( - + ) diff --git a/libs/widget-lib/src/types.ts b/libs/widget-lib/src/types.ts index 553c3791cc..0cebd8bb1b 100644 --- a/libs/widget-lib/src/types.ts +++ b/libs/widget-lib/src/types.ts @@ -93,6 +93,7 @@ export enum TradeType { * But in the future it can be extended to support other order types. */ ADVANCED = 'advanced', + YIELD = 'yield', } /** From 8952e9f7b29ff848fa3da3f811e3e6232eb92361 Mon Sep 17 00:00:00 2001 From: Leandro Date: Fri, 18 Oct 2024 13:41:36 +0100 Subject: [PATCH 042/116] feat(explorer): update explorer graph images (#5008) * feat: update explorer graph images * fix: reduce image sizes --- apps/explorer/src/assets/img/CoW-protocol.svg | 7 ++++++- apps/explorer/src/assets/img/Trader-variant.svg | 7 ++++++- apps/explorer/src/assets/img/Trader.svg | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/explorer/src/assets/img/CoW-protocol.svg b/apps/explorer/src/assets/img/CoW-protocol.svg index 811bb20718..ef3b6cc984 100644 --- a/apps/explorer/src/assets/img/CoW-protocol.svg +++ b/apps/explorer/src/assets/img/CoW-protocol.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + diff --git a/apps/explorer/src/assets/img/Trader-variant.svg b/apps/explorer/src/assets/img/Trader-variant.svg index 4db00ab991..1a7bb1b79b 100644 --- a/apps/explorer/src/assets/img/Trader-variant.svg +++ b/apps/explorer/src/assets/img/Trader-variant.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + diff --git a/apps/explorer/src/assets/img/Trader.svg b/apps/explorer/src/assets/img/Trader.svg index 1c9a32c52f..ef1afda187 100644 --- a/apps/explorer/src/assets/img/Trader.svg +++ b/apps/explorer/src/assets/img/Trader.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + From cb589b6d99c79710350bd8a8829f2655b36b0171 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 18 Oct 2024 17:51:46 +0500 Subject: [PATCH 043/116] chore: release main (#5011) --- .release-please-manifest.json | 6 +++--- apps/cowswap-frontend/CHANGELOG.md | 7 +++++++ apps/cowswap-frontend/package.json | 2 +- apps/explorer/CHANGELOG.md | 7 +++++++ apps/explorer/package.json | 2 +- libs/wallet/CHANGELOG.md | 7 +++++++ libs/wallet/package.json | 2 +- 7 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b52eed9497..4fd2f7cbd8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - "apps/cowswap-frontend": "1.86.0", - "apps/explorer": "2.35.2", + "apps/cowswap-frontend": "1.86.1", + "apps/explorer": "2.36.0", "libs/permit-utils": "0.4.0", "libs/widget-lib": "0.16.0", "libs/widget-react": "0.11.0", @@ -17,7 +17,7 @@ "libs/tokens": "1.10.0", "libs/types": "1.2.0", "libs/ui": "1.11.0", - "libs/wallet": "1.6.0", + "libs/wallet": "1.6.1", "apps/cow-fi": "1.15.0", "libs/wallet-provider": "1.0.0", "libs/ui-utils": "1.1.0", diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index 560739bd2d..0f09d39910 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.86.1](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.86.0...cowswap-v1.86.1) (2024-10-18) + + +### Bug Fixes + +* **widget:** ignore selected eip6963 provider when in widget ([#5009](https://github.com/cowprotocol/cowswap/issues/5009)) ([3f8446b](https://github.com/cowprotocol/cowswap/commit/3f8446b48a4f493448b262959b943756a24382d9)) + ## [1.86.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.85.0...cowswap-v1.86.0) (2024-10-18) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index a5898d6e9b..0b9af36fdc 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.86.0", + "version": "1.86.1", "description": "CoW Swap", "main": "index.js", "author": "", diff --git a/apps/explorer/CHANGELOG.md b/apps/explorer/CHANGELOG.md index 38a0d41e0f..b35339d854 100644 --- a/apps/explorer/CHANGELOG.md +++ b/apps/explorer/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.36.0](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.2...explorer-v2.36.0) (2024-10-18) + + +### Features + +* **explorer:** update explorer graph images ([#5008](https://github.com/cowprotocol/cowswap/issues/5008)) ([8952e9f](https://github.com/cowprotocol/cowswap/commit/8952e9f7b29ff848fa3da3f811e3e6232eb92361)) + ## [2.35.2](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.1...explorer-v2.35.2) (2024-10-18) diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 541ed15881..23375d4751 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/explorer", - "version": "2.35.2", + "version": "2.36.0", "description": "CoW Swap Explorer", "main": "src/main.tsx", "author": "", diff --git a/libs/wallet/CHANGELOG.md b/libs/wallet/CHANGELOG.md index d902625ba5..19aa9378f6 100644 --- a/libs/wallet/CHANGELOG.md +++ b/libs/wallet/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.6.1](https://github.com/cowprotocol/cowswap/compare/wallet-v1.6.0...wallet-v1.6.1) (2024-10-18) + + +### Bug Fixes + +* **widget:** ignore selected eip6963 provider when in widget ([#5009](https://github.com/cowprotocol/cowswap/issues/5009)) ([3f8446b](https://github.com/cowprotocol/cowswap/commit/3f8446b48a4f493448b262959b943756a24382d9)) + ## [1.6.0](https://github.com/cowprotocol/cowswap/compare/wallet-v1.5.2...wallet-v1.6.0) (2024-09-30) diff --git a/libs/wallet/package.json b/libs/wallet/package.json index c06ecfef10..fc6205c015 100644 --- a/libs/wallet/package.json +++ b/libs/wallet/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/wallet", - "version": "1.6.0", + "version": "1.6.1", "main": "./index.js", "types": "./index.d.ts", "exports": { From 1abd82527dc1f96d6897533d750dcc6f2a51e7a0 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Fri, 18 Oct 2024 17:46:25 +0100 Subject: [PATCH 044/116] fix: fix bad merge --- .../swap/containers/ConfirmSwapModalSetup/index.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index c7bedcf835..f0b391f752 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -20,9 +20,8 @@ import { useTradeConfirmActions, } from 'modules/trade' import { TradeBasicConfirmDetails } from 'modules/trade/containers/TradeBasicConfirmDetails' -import { useIsSmartSlippageApplied } from 'modules/tradeSlippage' -import { HighFeeWarning } from 'modules/tradeWidgetAddons' -import { NetworkCostsTooltipSuffix, RowDeadline } from 'modules/tradeWidgetAddons' +import { useIsSmartSlippageApplied, useSmartTradeSlippage } from 'modules/tradeSlippage' +import { HighFeeWarning, NetworkCostsTooltipSuffix, RowDeadline } from 'modules/tradeWidgetAddons' import { CurrencyPreviewInfo } from 'common/pure/CurrencyAmountPreview' import { NetworkCostsSuffix } from 'common/pure/NetworkCostsSuffix' @@ -31,7 +30,6 @@ import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/ut import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { useSwapConfirmButtonText } from '../../hooks/useSwapConfirmButtonText' -import { useSmartSwapSlippage } from '../../hooks/useSwapSlippage' import { useSwapState } from '../../hooks/useSwapState' const CONFIRM_TITLE = 'Swap' @@ -79,7 +77,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const buttonText = useSwapConfirmButtonText(slippageAdjustedSellAmount) const isSmartSlippageApplied = useIsSmartSlippageApplied() - const smartSlippage = useSmartSwapSlippage() + const smartSlippage = useSmartTradeSlippage() const labelsAndTooltips = useMemo( () => ({ From 9308fc1e35ce5ecfdc69c76974136182352eeca0 Mon Sep 17 00:00:00 2001 From: Leandro Date: Mon, 21 Oct 2024 16:15:51 +0100 Subject: [PATCH 045/116] fix(smart-slippage): replace volatity with trade size on tooltips (#5012) --- .../containers/HighSuggestedSlippageWarning/index.tsx | 2 +- .../containers/TransactionSettings/index.tsx | 4 ++-- .../tradeWidgetAddons/pure/Row/RowSlippageContent/index.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx b/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx index 225e0b99f9..ba9afaaa33 100644 --- a/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/containers/HighSuggestedSlippageWarning/index.tsx @@ -32,7 +32,7 @@ export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarning Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution ) diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx index 4ec57aa276..eb3d25cfb9 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx @@ -279,8 +279,8 @@ export function TransactionSettings({ deadlineState }: TransactionSettingsProps) - CoW Swap has dynamically selected this slippage amount to account for current gas prices and - volatility. Changes may result in slower execution. + CoW Swap has dynamically selected this slippage amount to account for current gas prices and trade + size. Changes may result in slower execution. } /> 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 76ccec401d..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 @@ -11,7 +11,7 @@ import styled from 'styled-components/macro' import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from 'common/utils/tradeSettingsTooltips' import { settingsTabStateAtom } from '../../../state/settingsTabState' -import { StyledRowBetween, TextWrapper, StyledInfoIcon, TransactionText, RowStyleProps } from '../styled' +import { RowStyleProps, StyledInfoIcon, StyledRowBetween, TextWrapper, TransactionText } from '../styled' const DefaultSlippage = styled.span` display: inline-flex; @@ -30,7 +30,7 @@ const DefaultSlippage = styled.span` ` const SUGGESTED_SLIPPAGE_TOOLTIP = - 'This is the recommended slippage tolerance based on current gas prices & volatility. A lower amount may result in slower execution.' + 'This is the recommended slippage tolerance based on current gas prices & trade size. A lower amount may result in slower execution.' export interface RowSlippageContentProps { chainId: SupportedChainId From cbbeb8bc3d89796da989fd4b17a6eb6e3a4629a4 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 22 Oct 2024 18:43:37 +0500 Subject: [PATCH 046/116] fix(swap): disable partial fills (#5016) * fix(swap): disable partial fills * fix: use order kind correspondingly * fix: use corresponding trade type in context --- .../src/modules/trade/const/common.ts | 10 ++++++ .../trade/hooks/useNotifyWidgetTrade.ts | 10 ++---- .../tradeFlow/hooks/useTradeFlowContext.ts | 33 ++++++++++++++----- 3 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/trade/const/common.ts diff --git a/apps/cowswap-frontend/src/modules/trade/const/common.ts b/apps/cowswap-frontend/src/modules/trade/const/common.ts new file mode 100644 index 0000000000..503f40f539 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/const/common.ts @@ -0,0 +1,10 @@ +import { UiOrderType } from '@cowprotocol/types' + +import { TradeType } from '../types' + +export const TradeTypeToUiOrderType: Record = { + [TradeType.SWAP]: UiOrderType.SWAP, + [TradeType.LIMIT_ORDER]: UiOrderType.LIMIT, + [TradeType.ADVANCED_ORDERS]: UiOrderType.TWAP, + [TradeType.YIELD]: UiOrderType.YIELD, +} diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts index 38d74eae3b..5e3037ad8b 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts @@ -2,13 +2,14 @@ import { useEffect, useRef } from 'react' import { getCurrencyAddress } from '@cowprotocol/common-utils' import { AtomsAndUnits, CowWidgetEvents, OnTradeParamsPayload } from '@cowprotocol/events' -import { TokenInfo, UiOrderType } from '@cowprotocol/types' +import { TokenInfo } from '@cowprotocol/types' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { WIDGET_EVENT_EMITTER } from 'widgetEventEmitter' import { useDerivedTradeState } from './useDerivedTradeState' +import { TradeTypeToUiOrderType } from '../const/common' import { TradeType } from '../types' import { TradeDerivedState } from '../types/TradeDerivedState' @@ -32,13 +33,6 @@ export function useNotifyWidgetTrade() { }, [state]) } -const TradeTypeToUiOrderType: Record = { - [TradeType.SWAP]: UiOrderType.SWAP, - [TradeType.LIMIT_ORDER]: UiOrderType.LIMIT, - [TradeType.ADVANCED_ORDERS]: UiOrderType.TWAP, - [TradeType.YIELD]: UiOrderType.YIELD, -} - function getTradeParamsEventPayload(tradeType: TradeType, state: TradeDerivedState): OnTradeParamsPayload { return { orderType: TradeTypeToUiOrderType[tradeType], diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts index e080f6d985..f3580a1a9c 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -1,6 +1,5 @@ import { TokenWithLogo } from '@cowprotocol/common-const' -import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, OrderClass, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' -import { UiOrderType } from '@cowprotocol/types' +import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, OrderClass, SupportedChainId } from '@cowprotocol/cow-sdk' import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { useWalletProvider } from '@cowprotocol/wallet-provider' @@ -13,11 +12,12 @@ import { useCloseModals } from 'legacy/state/application/hooks' import { useAppData, useAppDataHooks } from 'modules/appData' import { useGeneratePermitHook, useGetCachedPermit, usePermitInfo } from 'modules/permit' import { useEnoughBalanceAndAllowance } from 'modules/tokens' -import { TradeType, useDerivedTradeState, useReceiveAmountInfo, useTradeConfirmActions } from 'modules/trade' +import { useDerivedTradeState, useReceiveAmountInfo, useTradeConfirmActions, useTradeTypeInfo } from 'modules/trade' import { getOrderValidTo, useTradeQuote } from 'modules/tradeQuote' import { useGP2SettlementContract } from 'common/hooks/useContract' +import { TradeTypeToUiOrderType } from '../../trade/const/common' import { TradeFlowContext } from '../types/TradeFlowContext' export interface TradeFlowParams { @@ -31,6 +31,9 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon const isSafeWallet = useIsSafeWallet() const derivedTradeState = useDerivedTradeState() const receiveAmountInfo = useReceiveAmountInfo() + const tradeTypeInfo = useTradeTypeInfo() + const tradeType = tradeTypeInfo?.tradeType + const uiOrderType = tradeType ? TradeTypeToUiOrderType[tradeType] : null const sellCurrency = derivedTradeState?.inputCurrency const inputAmount = receiveAmountInfo?.afterNetworkCosts.sellAmount @@ -39,7 +42,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount const networkFee = receiveAmountInfo?.costs.networkFee.amountInSellCurrency - const permitInfo = usePermitInfo(sellCurrency, TradeType.YIELD) + const permitInfo = usePermitInfo(sellCurrency, tradeType) const generatePermitHook = useGeneratePermitHook() const getCachedPermit = useGetCachedPermit() const closeModals = useCloseModals() @@ -57,7 +60,13 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon checkAllowanceAddress, }) - const { inputCurrency: sellToken, outputCurrency: buyToken, recipient, recipientAddress } = derivedTradeState || {} + const { + inputCurrency: sellToken, + outputCurrency: buyToken, + recipient, + recipientAddress, + orderKind, + } = derivedTradeState || {} const quoteParams = tradeQuote?.quoteParams const quoteResponse = tradeQuote?.response const localQuoteTimestamp = tradeQuote?.localQuoteTimestamp @@ -77,7 +86,9 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon quoteParams && quoteResponse && localQuoteTimestamp && - settlementContract + orderKind && + settlementContract && + uiOrderType ? [ account, allowsOffchainSigning, @@ -104,6 +115,8 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon tradeConfirmActions, typedHooks, deadline, + orderKind, + uiOrderType, ] : null, ([ @@ -132,6 +145,8 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon tradeConfirmActions, typedHooks, deadline, + orderKind, + uiOrderType, ]) => { return { context: { @@ -154,7 +169,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon recipient, recipientAddress, marketLabel: [inputAmount?.currency.symbol, outputAmount?.currency.symbol].join(','), - orderType: UiOrderType.YIELD, + orderType: uiOrderType, }, contract: settlementContract, permitInfo: !enoughAllowance ? permitInfo : undefined, @@ -164,7 +179,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon account, chainId, signer: provider.getSigner(), - kind: OrderKind.SELL, + kind: orderKind, inputAmount, outputAmount, sellAmountBeforeFee, @@ -181,7 +196,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon allowsOffchainSigning, appData, class: OrderClass.MARKET, - partiallyFillable: true, + partiallyFillable: false, // SWAP orders are always fill or kill - for now quoteId: quoteResponse.id, isSafeWallet, }, From 52460461d6cc80635a25aefe5b119dbd7de1fb69 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Wed, 23 Oct 2024 08:27:37 +0100 Subject: [PATCH 047/116] feat: add vampire attack banner (#4981) * feat: refactor cowamm banner * feat: refactor cowamm banner * feat: add token selector cow amm banner * feat: add shared atom state * feat: add animation and refactoring * feat: consolidate banners into single component * feat: add darkmode styling for cow amm banners * feat: add poolinfo element * feat: text fit optimisation * feat: do not show banners without connect wallet * feat: add usdc scenario * feat: modify CTA button text for SC wallets * feat: add dark mode styles for token selector banner * feat: only show banners on supported networks * chore: temporarily hide cow amm banner --------- Co-authored-by: Alexandr Kazachenko --- .../pure/CoWAMMBanner/CoWAmmBannerContent.tsx | 396 ++++++++++++++ .../pure/CoWAMMBanner/arrowBackground.tsx | 88 ++++ .../pure/CoWAMMBanner/cowAmmBannerState.ts | 5 + .../src/common/pure/CoWAMMBanner/dummyData.ts | 115 +++++ .../src/common/pure/CoWAMMBanner/index.tsx | 287 +++-------- .../src/common/pure/CoWAMMBanner/styled.ts | 484 ++++++++++++++++++ .../application/containers/App/index.tsx | 8 + .../pure/SelectTokenModal/index.tsx | 2 + .../pure/TokensVirtualList/index.tsx | 5 + libs/assets/src/cow-swap/icon-curve.svg | 1 + libs/assets/src/cow-swap/icon-pancakeswap.svg | 1 + libs/assets/src/cow-swap/icon-sushi.svg | 1 + libs/assets/src/cow-swap/icon-uni.svg | 1 + libs/ui/src/enum.ts | 12 + libs/ui/src/theme/ThemeColorVars.tsx | 16 +- 15 files changed, 1200 insertions(+), 222 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts create mode 100644 libs/assets/src/cow-swap/icon-curve.svg create mode 100644 libs/assets/src/cow-swap/icon-pancakeswap.svg create mode 100644 libs/assets/src/cow-swap/icon-sushi.svg create mode 100644 libs/assets/src/cow-swap/icon-uni.svg diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx new file mode 100644 index 0000000000..2a2fd6d5dd --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx @@ -0,0 +1,396 @@ +import React from 'react' +import { useCallback, useMemo, useRef } from 'react' + +import ICON_ARROW from '@cowprotocol/assets/cow-swap/arrow.svg' +import ICON_CURVE from '@cowprotocol/assets/cow-swap/icon-curve.svg' +import ICON_PANCAKESWAP from '@cowprotocol/assets/cow-swap/icon-pancakeswap.svg' +import ICON_SUSHISWAP from '@cowprotocol/assets/cow-swap/icon-sushi.svg' +import ICON_UNISWAP from '@cowprotocol/assets/cow-swap/icon-uni.svg' +import ICON_STAR from '@cowprotocol/assets/cow-swap/star-shine.svg' +import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' + +import SVG from 'react-inlinesvg' +import { Textfit } from 'react-textfit' + +import { upToSmall, useMediaQuery } from 'legacy/hooks/useMediaQuery' +import { useIsDarkMode } from 'legacy/state/user/hooks' + +import { ArrowBackground } from './arrowBackground' +import { LpToken, StateKey, dummyData, lpTokenConfig } from './dummyData' +import * as styledEl from './styled' + +import { BannerLocation, DEMO_DROPDOWN_OPTIONS } from './index' +import { TokenLogo } from '../../../../../../libs/tokens/src/pure/TokenLogo' +import { USDC, WBTC } from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { DummyDataType, TwoLpScenario, InferiorYieldScenario } from './dummyData' + +const lpTokenIcons: Record = { + [LpToken.UniswapV2]: ICON_UNISWAP, + [LpToken.Sushiswap]: ICON_SUSHISWAP, + [LpToken.PancakeSwap]: ICON_PANCAKESWAP, + [LpToken.Curve]: ICON_CURVE, +} + +interface CoWAmmBannerContentProps { + id: string + title: string + ctaText: string + location: BannerLocation + isDemo: boolean + selectedState: StateKey + setSelectedState: (state: StateKey) => void + dummyData: typeof dummyData + lpTokenConfig: typeof lpTokenConfig + onCtaClick: () => void + onClose: () => void +} + +function isTwoLpScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is TwoLpScenario { + return 'uniV2Apr' in scenario && 'sushiApr' in scenario +} + +function isInferiorYieldScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is InferiorYieldScenario { + return 'poolsCount' in scenario +} + +const renderTextfit = ( + content: React.ReactNode, + mode: 'single' | 'multi', + minFontSize: number, + maxFontSize: number, + key: string, +) => ( + + {content} + +) + +export function CoWAmmBannerContent({ + id, + title, + ctaText, + location, + isDemo, + selectedState, + setSelectedState, + dummyData, + lpTokenConfig, + onCtaClick, + onClose, +}: CoWAmmBannerContentProps) { + const isMobile = useMediaQuery(upToSmall) + const isDarkMode = useIsDarkMode() + const arrowBackgroundRef = useRef(null) + + const handleCTAMouseEnter = useCallback(() => { + if (arrowBackgroundRef.current) { + arrowBackgroundRef.current.style.visibility = 'visible' + arrowBackgroundRef.current.style.opacity = '1' + } + }, []) + + const handleCTAMouseLeave = useCallback(() => { + if (arrowBackgroundRef.current) { + arrowBackgroundRef.current.style.visibility = 'hidden' + arrowBackgroundRef.current.style.opacity = '0' + } + }, []) + + const { apr } = dummyData[selectedState] + + const aprMessage = useMemo(() => { + if (selectedState === 'uniV2InferiorWithLowAverageYield') { + const currentData = dummyData[selectedState] + if (isInferiorYieldScenario(currentData)) { + return `${currentData.poolsCount}+` + } + } + return `+${apr.toFixed(1)}%` + }, [selectedState, apr, dummyData]) + + const comparisonMessage = useMemo(() => { + const currentData = dummyData[selectedState] + + if (!currentData) { + return 'Invalid state selected' + } + + const renderPoolInfo = (poolName: string) => ( + + higher APR available for your {poolName} pool: + +
    + +
    + WBTC-USDC +
    +
    + ) + + if (isTwoLpScenario(currentData)) { + if (selectedState === 'twoLpsMixed') { + return renderPoolInfo('UNI-V2') + } else if (selectedState === 'twoLpsBothSuperior') { + const { uniV2Apr, sushiApr } = currentData + const higherAprPool = uniV2Apr > sushiApr ? 'UNI-V2' : 'SushiSwap' + return renderPoolInfo(higherAprPool) + } + } + + if (selectedState === 'uniV2Superior') { + return renderPoolInfo('UNI-V2') + } + + if (selectedState === 'uniV2InferiorWithLowAverageYield' && isInferiorYieldScenario(currentData)) { + return 'pools available to get yield on your assets!' + } + + if (currentData.hasCoWAmmPool) { + return `yield over average ${currentData.comparison} pool` + } else { + const tokens = lpTokenConfig[selectedState] + if (tokens.length > 1) { + const tokenNames = tokens + .map((token) => { + switch (token) { + case LpToken.UniswapV2: + return 'UNI-V2' + case LpToken.Sushiswap: + return 'Sushi' + case LpToken.PancakeSwap: + return 'PancakeSwap' + case LpToken.Curve: + return 'Curve' + default: + return '' + } + }) + .filter(Boolean) + + return `yield over average ${tokenNames.join(', ')} pool${tokenNames.length > 1 ? 's' : ''}` + } else { + return `yield over average UNI-V2 pool` + } + } + }, [selectedState, location, lpTokenConfig, isDarkMode]) + + const lpEmblems = useMemo(() => { + const tokens = lpTokenConfig[selectedState] + const totalItems = tokens.length + + const renderEmblemContent = () => ( + <> + + + + + + + + ) + + if (totalItems === 0 || selectedState === 'uniV2InferiorWithLowAverageYield') { + return ( + + + {selectedState === 'uniV2InferiorWithLowAverageYield' ? ( + + ) : ( + + )} + + {renderEmblemContent()} + + ) + } + + return ( + + + {tokens.map((token, index) => ( + + + + ))} + + {renderEmblemContent()} + + ) + }, [selectedState, lpTokenConfig, lpTokenIcons]) + + const renderProductLogo = useCallback( + (color: string) => ( + + ), + [], + ) + + const renderStarIcon = useCallback( + (props: any) => ( + + + + ), + [], + ) + + const renderTokenSelectorContent = () => ( + + + + + {renderProductLogo(isDarkMode ? UI.COLOR_COWAMM_LIGHT_GREEN : UI.COLOR_COWAMM_DARK_GREEN)} + {title} + + + {renderStarIcon({ size: 26, top: -16, right: 80, color: `var(${UI.COLOR_COWAMM_LIGHTER_GREEN})` })} +

    {renderTextfit(aprMessage, 'single', 35, 65, `apr-${selectedState}`)}

    + + {renderTextfit(comparisonMessage, 'multi', 15, isMobile ? 15 : 21, `comparison-${selectedState}`)} + + {renderStarIcon({ size: 16, bottom: 3, right: 20, color: `var(${UI.COLOR_COWAMM_LIGHTER_GREEN})` })} +
    + + {ctaText} + +
    +
    + ) + + const renderGlobalContent = () => { + return ( + + + + {renderProductLogo(UI.COLOR_COWAMM_LIGHT_GREEN)} + {title} + + + {renderStarIcon({ size: 36, top: -17, right: 80 })} +

    {renderTextfit(aprMessage, 'single', isMobile ? 40 : 80, isMobile ? 50 : 80, `apr-${selectedState}`)}

    + + {renderTextfit(comparisonMessage, 'multi', 10, isMobile ? 21 : 28, `comparison-${selectedState}`)} + + {renderStarIcon({ size: 26, bottom: -10, right: 20 })} +
    + + {!isMobile && ( + + + {renderTextfit( + <> + One-click convert, boost yield + , + 'multi', + 10, + 30, + `boost-yield-${selectedState}`, + )} + + {lpEmblems} + + )} + + + {ctaText} + + + Pool analytics ↗ + + +
    + ) + } + + const renderDemoDropdown = () => ( + setSelectedState(e.target.value as StateKey)}> + {DEMO_DROPDOWN_OPTIONS.map((option) => ( + + ))} + + ) + + const content = (() => { + switch (location) { + case BannerLocation.TokenSelector: + return renderTokenSelectorContent() + case BannerLocation.Global: + return renderGlobalContent() + default: + return null + } + })() + + if (!content) { + return null + } + + return ( +
    + {content} + {isDemo && renderDemoDropdown()} +
    + ) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx new file mode 100644 index 0000000000..a4677c4674 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx @@ -0,0 +1,88 @@ +import { forwardRef, useMemo } from 'react' +import { memo } from 'react' + +import { UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +const ArrowBackgroundWrapper = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + visibility: hidden; + opacity: 0; + transition: opacity 0.3s ease-in-out; +` + +const MIN_FONT_SIZE = 18 +const MAX_FONT_SIZE = 42 + +const Arrow = styled.div<{ delay: number; color: string; fontSize: number }>` + position: absolute; + font-size: ${({ fontSize }) => fontSize}px; + color: ${({ color }) => color}; + animation: float 2s infinite linear; + animation-delay: ${({ delay }) => delay}s; + + @keyframes float { + 0% { + transform: translateY(100%); + opacity: 0; + } + 10% { + opacity: 0.3; + } + 50% { + opacity: 0.3; + } + 90% { + opacity: 0.2; + } + 100% { + transform: translateY(-100%); + opacity: 0; + } + } +` + +export interface ArrowBackgroundProps { + count?: number + color?: string +} + +export const ArrowBackground = memo( + forwardRef( + ({ count = 36, color = `var(${UI.COLOR_COWAMM_LIGHT_GREEN})` }, ref) => { + const arrows = useMemo(() => { + return Array.from({ length: count }, (_, index) => ({ + delay: (index / count) * 4, + left: `${Math.random() * 100}%`, + top: `${Math.random() * 100}%`, + fontSize: Math.floor(Math.random() * (MAX_FONT_SIZE - MIN_FONT_SIZE + 1)) + MIN_FONT_SIZE, + })) + }, [count]) + + return ( + + {arrows.map((arrow, index) => ( + + ↑ + + ))} + + ) + }, + ), +) diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts new file mode 100644 index 0000000000..bc0c9b3054 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts @@ -0,0 +1,5 @@ +import { atom } from 'jotai' + +import { StateKey } from './dummyData' + +export const cowAmmBannerStateAtom = atom('noLp') diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts new file mode 100644 index 0000000000..26492684f6 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts @@ -0,0 +1,115 @@ +export enum LpToken { + UniswapV2 = 'UniswapV2', + Sushiswap = 'Sushiswap', + PancakeSwap = 'PancakeSwap', + Curve = 'Curve', +} + +type BaseScenario = { + readonly apr: number + readonly comparison: string + readonly hasCoWAmmPool: boolean +} + +export type TwoLpScenario = BaseScenario & { + readonly uniV2Apr: number + readonly sushiApr: number +} + +export type InferiorYieldScenario = BaseScenario & { + readonly poolsCount: number +} + +export type DummyDataType = { + noLp: BaseScenario + uniV2Superior: BaseScenario + uniV2Inferior: BaseScenario + sushi: BaseScenario + curve: BaseScenario + pancake: BaseScenario & { readonly isYieldSuperior: boolean } + twoLpsMixed: TwoLpScenario + twoLpsBothSuperior: TwoLpScenario + threeLps: BaseScenario + fourLps: BaseScenario + uniV2InferiorWithLowAverageYield: InferiorYieldScenario +} + +export const dummyData: DummyDataType = { + noLp: { + apr: 1.5, + comparison: 'average UNI-V2 pool', + hasCoWAmmPool: false, + }, + uniV2Superior: { + apr: 2.1, + comparison: 'UNI-V2', + hasCoWAmmPool: true, + }, + uniV2Inferior: { + apr: 1.2, + comparison: 'UNI-V2', + hasCoWAmmPool: true, + }, + sushi: { + apr: 1.8, + comparison: 'SushiSwap', + hasCoWAmmPool: true, + }, + curve: { + apr: 1.3, + comparison: 'Curve', + hasCoWAmmPool: true, + }, + pancake: { + apr: 2.5, + comparison: 'PancakeSwap', + hasCoWAmmPool: true, + isYieldSuperior: true, + }, + twoLpsMixed: { + apr: 2.5, + comparison: 'UNI-V2 and SushiSwap', + hasCoWAmmPool: true, + uniV2Apr: 3.0, + sushiApr: 1.8, + } as TwoLpScenario, + twoLpsBothSuperior: { + apr: 3.2, + comparison: 'UNI-V2 and SushiSwap', + hasCoWAmmPool: true, + uniV2Apr: 3.5, + sushiApr: 2.9, + } as TwoLpScenario, + threeLps: { + apr: 2.2, + comparison: 'UNI-V2, SushiSwap, and Curve', + hasCoWAmmPool: false, + }, + fourLps: { + apr: 2.4, + comparison: 'UNI-V2, SushiSwap, Curve, and PancakeSwap', + hasCoWAmmPool: false, + }, + uniV2InferiorWithLowAverageYield: { + apr: 1.2, + comparison: 'UNI-V2', + hasCoWAmmPool: true, + poolsCount: 195, + }, +} + +export type StateKey = keyof typeof dummyData + +export const lpTokenConfig: Record = { + noLp: [], + uniV2Superior: [LpToken.UniswapV2], + uniV2Inferior: [LpToken.UniswapV2], + sushi: [LpToken.Sushiswap], + curve: [LpToken.Curve], + pancake: [LpToken.PancakeSwap], + twoLpsMixed: [LpToken.UniswapV2, LpToken.Sushiswap], + twoLpsBothSuperior: [LpToken.UniswapV2, LpToken.Sushiswap], + threeLps: [LpToken.UniswapV2, LpToken.Sushiswap, LpToken.Curve], + fourLps: [LpToken.UniswapV2, LpToken.Sushiswap, LpToken.Curve, LpToken.PancakeSwap], + uniV2InferiorWithLowAverageYield: [LpToken.UniswapV2], +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx index 265c32cc46..5f831b0ece 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx @@ -1,237 +1,84 @@ -import { Media } from '@cowprotocol/ui' -import { ClosableBanner } from '@cowprotocol/ui' +import { useAtom } from 'jotai' +import { useCallback } from 'react' -import { X } from 'react-feather' -import styled from 'styled-components/macro' +import { ClosableBanner } from '@cowprotocol/ui' +import { useIsSmartContractWallet } from '@cowprotocol/wallet' import { cowAnalytics } from 'modules/analytics' -const BannerWrapper = styled.div` - --darkGreen: #194d05; - --lightGreen: #bcec79; - - position: fixed; - top: 76px; - right: 10px; - z-index: 3; - width: 400px; - height: 345px; - border-radius: 24px; - background-color: var(--darkGreen); - color: var(--lightGreen); - padding: 24px; - display: flex; - flex-flow: column wrap; - align-items: center; - justify-content: center; - gap: 24px; - overflow: hidden; - - ${Media.upToSmall()} { - width: 100%; - height: auto; - left: 0; - right: 0; - margin: 0 auto; - bottom: 57px; - top: initial; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - box-shadow: 0 0 0 100vh rgb(0 0 0 / 40%); - z-index: 10; - } - - > i { - position: absolute; - top: -30px; - left: -30px; - width: 166px; - height: 42px; - border: 1px solid var(--lightGreen); - border-radius: 16px; - border-left: 0; - animation: bounceLeftRight 7s infinite; - animation-delay: 2s; - } - - &::before { - content: ''; - position: absolute; - top: 100px; - left: -23px; - width: 56px; - height: 190px; - border: 1px solid var(--lightGreen); - border-radius: 16px; - border-left: 0; - animation: bounceUpDown 7s infinite; - } - - &::after { - content: ''; - position: absolute; - bottom: -21px; - right: 32px; - width: 76px; - height: 36px; - border: 1px solid var(--lightGreen); - border-radius: 16px; - border-bottom: 0; - animation: bounceLeftRight 7s infinite; - animation-delay: 1s; - } - - > div { - display: flex; - flex-flow: column wrap; - gap: 24px; - width: 100%; - max-width: 75%; - margin: 0 auto; - } - - @keyframes bounceUpDown { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-7px); - } - } - - @keyframes bounceLeftRight { - 0%, - 100% { - transform: translateX(0); - } - 50% { - transform: translateX(7px); - } - } -` - -const Title = styled.h2` - font-size: 34px; - font-weight: bold; - margin: 0; - - ${Media.upToSmall()} { - font-size: 26px; - } -` - -const Description = styled.p` - font-size: 17px; - line-height: 1.5; - margin: 0; - - ${Media.upToSmall()} { - font-size: 15px; - } -` - -const CTAButton = styled.button` - background-color: var(--lightGreen); - color: var(--darkGreen); - border: none; - border-radius: 56px; - padding: 12px 24px; - font-size: 18px; - font-weight: bold; - cursor: pointer; - width: 100%; - max-width: 75%; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - transition: border-radius 0.2s ease-in-out; - - &:hover { - border-radius: 16px; - - > i { - transform: rotate(45deg); - } - } - - > i { - font-size: 22px; - font-weight: bold; - font-style: normal; - line-height: 1; - margin: 3px 0 0; - transition: transform 0.2s ease-in-out; - animation: spin 6s infinite; - } - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - 20% { - transform: rotate(360deg); - } - 100% { - transform: rotate(360deg); - } - } -` +import { CoWAmmBannerContent } from './CoWAmmBannerContent' +import { cowAmmBannerStateAtom } from './cowAmmBannerState' +import { dummyData, lpTokenConfig } from './dummyData' + +const IS_DEMO_MODE = true +const ANALYTICS_URL = 'https://cow.fi/pools?utm_source=swap.cow.fi&utm_medium=web&utm_content=cow_amm_banner' + +export const DEMO_DROPDOWN_OPTIONS = [ + { value: 'noLp', label: '🚫 No LP Tokens' }, + { value: 'uniV2Superior', label: '⬆️ 🐴 UNI-V2 LP (Superior Yield)' }, + { value: 'uniV2Inferior', label: '⬇️ 🐴 UNI-V2 LP (Inferior Yield)' }, + { value: 'uniV2InferiorWithLowAverageYield', label: '⬇️ 🐴 UNI-V2 LP (Inferior Yield, Lower Average)' }, + { value: 'sushi', label: '⬇️ 🍣 SushiSwap LP (Inferior Yield)' }, + { value: 'curve', label: '⬇️ 🌈 Curve LP (Inferior Yield)' }, + { value: 'pancake', label: '⬇️ 🥞 PancakeSwap LP (Inferior Yield)' }, + { value: 'twoLpsMixed', label: '⬆️ 🐴 UNI-V2 (Superior) & ⬇️ 🍣 SushiSwap (Inferior) LPs' }, + { value: 'twoLpsBothSuperior', label: '⬆️ 🐴 UNI-V2 & ⬆️ 🍣 SushiSwap LPs (Both Superior, but UNI-V2 is higher)' }, + { value: 'threeLps', label: '⬇️ 🐴 UNI-V2, 🍣 SushiSwap & 🌈 Curve LPs (Inferior Yield)' }, + { value: 'fourLps', label: '⬇️ 🐴 UNI-V2, 🍣 SushiSwap, 🌈 Curve & 🥞 PancakeSwap LPs (Inferior Yield)' }, +] + +export enum BannerLocation { + Global = 'global', + TokenSelector = 'tokenSelector', +} -const CloseButton = styled(X)` - position: absolute; - top: 16px; - right: 16px; - cursor: pointer; - color: var(--lightGreen); - opacity: 0.6; - transition: opacity 0.2s ease-in-out; +interface BannerProps { + location: BannerLocation +} - &:hover { - opacity: 1; - } -` +export function CoWAmmBanner({ location }: BannerProps) { + const [selectedState, setSelectedState] = useAtom(cowAmmBannerStateAtom) -export function CoWAmmBanner() { - const handleCTAClick = () => { + const handleCTAClick = useCallback(() => { cowAnalytics.sendEvent({ category: 'CoW Swap', - action: 'CoW AMM Banner CTA Clicked', + action: `CoW AMM Banner [${location}] CTA Clicked`, }) - window.open( - 'https://balancer.fi/pools/cow?utm_source=swap.cow.fi&utm_medium=web&utm_content=cow_amm_banner', - '_blank' - ) - } + window.open(ANALYTICS_URL, '_blank') + }, [location]) - const handleClose = () => { + const handleClose = useCallback(() => { cowAnalytics.sendEvent({ category: 'CoW Swap', - action: 'CoW AMM Banner Closed', + action: `CoW AMM Banner [${location}] Closed`, }) - } - - return ClosableBanner('cow_amm_banner', (close) => ( - - - { - handleClose() - close() - }} - /> -
    - Now live: the first MEV-capturing AMM - - CoW AMM shields you from LVR, so you can provide liquidity with less risk and more rewards. - -
    - - LP on CoW AMM - -
    + }, [location]) + + const handleBannerClose = useCallback(() => { + handleClose() + }, [handleClose]) + + const bannerId = `cow_amm_banner_2024_va_${location}` + + const isSmartContractWallet = useIsSmartContractWallet() + + return ClosableBanner(bannerId, (close) => ( + { + handleBannerClose() + close() + }} + /> )) } diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts new file mode 100644 index 0000000000..ba9093db41 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts @@ -0,0 +1,484 @@ +import { UI, Media } from '@cowprotocol/ui' + +import { X } from 'react-feather' +import styled, { keyframes } from 'styled-components/macro' +import { TokenLogoWrapper } from '../../../../../../libs/tokens/src/pure/TokenLogo' + +const arrowUpAnimation = keyframes` + 0% { + transform: translateY(100%); + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + transform: translateY(-100%); + opacity: 0; + } +` + +export const BannerWrapper = styled.div` + position: fixed; + top: 76px; + right: 10px; + z-index: 3; + width: 485px; + height: auto; + border-radius: 24px; + background-color: var(${UI.COLOR_COWAMM_DARK_GREEN}); + color: var(${UI.COLOR_COWAMM_DARK_GREEN}); + padding: 20px; + display: flex; + flex-flow: column wrap; + align-items: center; + justify-content: center; + gap: 20px; + overflow: hidden; + overflow: hidden; + transition: transform 0.2s ease; + + ${Media.upToSmall()} { + width: 100%; + height: auto; + left: 0; + right: 0; + margin: 0 auto; + bottom: 57px; + top: initial; + border-radius: 24px 24px 0 0; + box-shadow: 0 0 0 100vh rgb(0 0 0 / 40%); + z-index: 10; + } +` + +export const CloseButton = styled(X)<{ color?: string; top?: number }>` + position: absolute; + top: ${({ top = 16 }) => top}px; + right: 16px; + cursor: pointer; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_LIGHT_GREEN})`}; + opacity: 0.6; + transition: opacity 0.2s ease-in-out; + + &:hover { + opacity: 1; + } +` + +export const Title = styled.h2<{ color?: string }>` + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + font-weight: bold; + margin: 0 auto 0 0; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_LIGHT_GREEN})`}; + + ${Media.upToSmall()} { + font-size: 26px; + } +` + +export const Card = styled.div<{ + bgColor?: string + color?: string + height?: number | 'max-content' + borderColor?: string + borderWidth?: number + padding?: string + gap?: string +}>` + --default-height: 150px; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + gap: ${({ gap }) => gap || '24px'}; + font-size: 30px; + line-height: 1.2; + font-weight: 500; + margin: 0; + width: 100%; + max-width: 100%; + height: ${({ height }) => + height === 'max-content' ? 'max-content' : height ? `${height}px` : 'var(--default-height)'}; + max-height: ${({ height }) => + height === 'max-content' ? 'max-content' : height ? `${height}px` : 'var(--default-height)'}; + border-radius: 16px; + padding: ${({ padding }) => padding || '24px'}; + background: ${({ bgColor }) => bgColor || 'transparent'}; + color: ${({ color }) => color || 'inherit'}; + border: ${({ borderWidth, borderColor }) => borderWidth && borderColor && `${borderWidth}px solid ${borderColor}`}; + position: relative; + + ${Media.upToSmall()} { + flex-flow: column wrap; + height: auto; + max-height: initial; + gap: 8px; + } + + > h3, + > span { + display: flex; + align-items: center; + justify-content: center; + margin: 0; + width: max-content; + height: 100%; + max-height: 100%; + color: inherit; + + ${Media.upToSmall()} { + width: 100%; + text-align: center; + } + + > div { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } + + > h3 { + font-weight: bold; + letter-spacing: -2px; + } + + > span { + font-weight: inherit; + } + + > span b { + font-weight: 900; + color: var(${UI.COLOR_COWAMM_LIGHTER_GREEN}); + } +` + +export const PoolInfo = styled.div<{ + flow?: 'column' | 'row' + align?: 'flex-start' | 'center' + color?: string + bgColor?: string + tokenBorderColor?: string +}>` + display: flex; + align-items: ${({ align = 'flex-start' }) => align}; + flex-flow: ${({ flow = 'column' }) => flow}; + font-size: inherit; + gap: 10px; + + ${Media.upToSmall()} { + flex-flow: column wrap; + } + + > i { + font-style: normal; + background: ${({ bgColor }) => bgColor || `var(${UI.COLOR_COWAMM_LIGHT_BLUE})`}; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_DARK_BLUE})`}; + display: flex; + flex-flow: row; + gap: 6px; + padding: 6px 12px 6px 6px; + height: min-content; + border-radius: 62px; + width: min-content; + box-shadow: var(${UI.BOX_SHADOW_2}); + + ${Media.upToSmall()} { + margin: 0 auto; + } + } + + > i > div { + display: flex; + } + + ${TokenLogoWrapper} { + border: 2px solid ${({ tokenBorderColor }) => tokenBorderColor || `var(${UI.COLOR_COWAMM_LIGHT_BLUE})`}; + + :last-child { + margin-left: -18px; + } + } +` + +export const CTAButton = styled.button<{ + bgColor?: string + bgHoverColor?: string + color?: string + size?: number + fontSize?: number + fontSizeMobile?: number +}>` + --size: ${({ size = 58 }) => size}px; + --font-size: ${({ fontSize = 24 }) => fontSize}px; + background: ${({ bgColor }) => bgColor || `var(${UI.COLOR_COWAMM_LIGHT_GREEN})`}; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_DARK_GREEN})`}; + border: none; + border-radius: var(--size); + min-height: var(--size); + padding: 12px 24px; + font-size: var(--font-size); + font-weight: bold; + cursor: pointer; + width: 100%; + max-width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + position: relative; + overflow: hidden; + z-index: 1; + + ${Media.upToSmall()} { + --font-size: ${({ fontSizeMobile = 21 }) => fontSizeMobile}px; + min-height: initial; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: ${({ bgHoverColor }) => bgHoverColor || `var(${UI.COLOR_COWAMM_LIGHTER_GREEN})`}; + z-index: -1; + transform: scaleX(0); + transform-origin: left; + transition: transform 2s ease-out; + } + + &:hover::before { + transform: scaleX(1); + } + + > * { + z-index: 2; + } +` + +export const SecondaryLink = styled.a` + color: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + font-size: 14px; + font-weight: 500; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +` + +export const DEMO_DROPDOWN = styled.select` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 999999999; + padding: 5px; + font-size: 14px; + + ${Media.upToSmall()} { + bottom: initial; + top: 0; + width: 100%; + right: 0; + left: 0; + } +` + +export const StarIcon = styled.div<{ + color?: string + size?: number + top?: number | 'initial' + left?: number | 'initial' + right?: number | 'initial' + bottom?: number | 'initial' +}>` + width: ${({ size = 16 }) => size}px; + height: ${({ size = 16 }) => size}px; + position: absolute; + top: ${({ top }) => (top === 'initial' ? 'initial' : top != null ? `${top}px` : 'initial')}; + left: ${({ left }) => (left === 'initial' ? 'initial' : left != null ? `${left}px` : 'initial')}; + right: ${({ right }) => (right === 'initial' ? 'initial' : right != null ? `${right}px` : 'initial')}; + bottom: ${({ bottom }) => (bottom === 'initial' ? 'initial' : bottom != null ? `${bottom}px` : 'initial')}; + color: ${({ color }) => color ?? `var(${UI.COLOR_COWAMM_LIGHT_BLUE})`}; + + > svg > path { + fill: ${({ color }) => color ?? 'currentColor'}; + } +` + +export const LpEmblems = styled.div` + display: flex; + gap: 8px; + width: 100%; + justify-content: center; + align-items: center; +` + +export const LpEmblemItemsWrapper = styled.div<{ totalItems: number }>` + display: ${({ totalItems }) => (totalItems > 2 ? 'grid' : 'flex')}; + gap: ${({ totalItems }) => (totalItems > 2 ? '0' : '8px')}; + width: 100%; + justify-content: center; + align-items: center; + + ${({ totalItems }) => + totalItems === 3 && + ` + grid-template: 1fr 1fr / 1fr 1fr; + justify-items: center; + + > :first-child { + grid-column: 1 / -1; + } + `} + + ${({ totalItems }) => + totalItems === 4 && + ` + grid-template: 1fr 1fr / 1fr 1fr; + `} +` + +export const LpEmblemItem = styled.div<{ + totalItems: number + index: number + isUSDC?: boolean +}>` + --size: ${({ totalItems }) => + totalItems === 4 ? '50px' : totalItems === 3 ? '65px' : totalItems === 2 ? '80px' : '104px'}; + width: var(--size); + height: var(--size); + padding: ${({ totalItems, isUSDC }) => + isUSDC ? '9px' : totalItems === 4 ? '10px' : totalItems >= 2 ? '15px' : '20px'}; + border-radius: 50%; + background: var(${UI.COLOR_COWAMM_DARK_GREEN}); + color: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + border: ${({ totalItems }) => + totalItems > 2 ? `2px solid var(${UI.COLOR_COWAMM_GREEN})` : `4px solid var(${UI.COLOR_COWAMM_GREEN})`}; + display: flex; + align-items: center; + justify-content: center; + position: relative; + + > svg { + width: 100%; + height: 100%; + } + + ${({ totalItems, index }) => { + const styleMap: Record> = { + 2: { + 0: 'margin-right: -42px;', + }, + 3: { + 0: 'margin-bottom: -20px; z-index: 10;', + 1: 'margin-top: -20px;', + 2: 'margin-top: -20px;', + }, + 4: { + 0: 'margin: -5px -10px -5px 0; z-index: 10;', + 1: 'margin-bottom: -5px; z-index: 10;', + 2: 'margin: -5px -10px 0 0;', + 3: 'margin-top: -5px;', + }, + } + + return styleMap[totalItems]?.[index] || '' + }} + + ${TokenLogoWrapper} { + width: 100%; + height: 100%; + } +` + +export const CoWAMMEmblemItem = styled.div` + --size: 104px; + width: var(--size); + height: var(--size); + border-radius: var(--size); + padding: 30px 30px 23px; + background: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + color: var(${UI.COLOR_COWAMM_DARK_GREEN}); + border: 4px solid var(${UI.COLOR_COWAMM_GREEN}); + display: flex; + align-items: center; + justify-content: center; +` + +export const EmblemArrow = styled.div` + --size: 32px; + width: var(--size); + height: var(--size); + min-width: var(--size); + border-radius: var(--size); + background: var(${UI.COLOR_COWAMM_DARK_GREEN}); + border: 3px solid var(${UI.COLOR_COWAMM_GREEN}); + margin: 0 -24px; + padding: 6px; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + color: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + + > svg > path { + fill: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + } +` + +export const ArrowBackground = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + height: 100%; + width: 100%; + visibility: hidden; + opacity: 0; + transition: opacity 0.2s ease-in-out; +` + +export const Arrow = styled.span<{ delay: number }>` + position: absolute; + font-size: 28px; + color: rgba(255, 255, 255, 0.3); + animation: ${arrowUpAnimation} 1s linear infinite; + animation-delay: ${({ delay }) => delay}s; + left: ${() => Math.random() * 100}%; + font-weight: 500; +` + +export const TokenSelectorWrapper = styled.div` + z-index: 3; + width: 100%; + padding: 20px; + position: relative; +` + +export const TokenSelectorWrapperInner = styled.div<{ bgColor?: string; color?: string }>` + position: relative; + width: 100%; + height: auto; + border-radius: 24px; + background: ${({ bgColor }) => bgColor || `var(${UI.COLOR_COWAMM_LIGHT_GREEN})`}; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_DARK_GREEN})`}; + padding: 14px; + margin: 0 auto; + display: flex; + flex-flow: column wrap; + align-items: center; + justify-content: center; + gap: 14px; + overflow: hidden; +` 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 e2db6d73bf..b8f9b4db77 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -93,6 +93,9 @@ export function App() { ) + // const { account } = useWalletInfo() + // const isChainIdUnsupported = useIsProviderNetworkUnsupported() + return ( }> @@ -127,6 +130,11 @@ export function App() { /> )} + {/* CoW AMM banner */} + {/*{!isInjectedWidgetMode && account && !isChainIdUnsupported && (*/} + {/* */} + {/*)}*/} + diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index 1dba5f772b..36b6ba37b2 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -88,7 +88,9 @@ export function SelectTokenModal(props: SelectTokenModalProps) { hideTooltip={hideFavoriteTokensTooltip} /> + + {inputValue.trim() ? ( ) : ( diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index 16e91c70a8..3637167df1 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -34,6 +34,8 @@ export function TokensVirtualList(props: TokensVirtualListProps) { const { values: balances, isLoading: balancesLoading } = balancesState const isWalletConnected = !!account + // const isInjectedWidgetMode = isInjectedWidget() + // const isChainIdUnsupported = useIsProviderNetworkUnsupported() const scrollTimeoutRef = useRef() const parentRef = useRef(null) @@ -65,6 +67,9 @@ export function TokensVirtualList(props: TokensVirtualListProps) { return ( + {/*{!isInjectedWidgetMode && account && !isChainIdUnsupported && (*/} + {/* */} + {/*)}*/} {items.map((virtualRow) => { diff --git a/libs/assets/src/cow-swap/icon-curve.svg b/libs/assets/src/cow-swap/icon-curve.svg new file mode 100644 index 0000000000..07688bbd5c --- /dev/null +++ b/libs/assets/src/cow-swap/icon-curve.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/cow-swap/icon-pancakeswap.svg b/libs/assets/src/cow-swap/icon-pancakeswap.svg new file mode 100644 index 0000000000..3367035850 --- /dev/null +++ b/libs/assets/src/cow-swap/icon-pancakeswap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/cow-swap/icon-sushi.svg b/libs/assets/src/cow-swap/icon-sushi.svg new file mode 100644 index 0000000000..5e214be336 --- /dev/null +++ b/libs/assets/src/cow-swap/icon-sushi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/cow-swap/icon-uni.svg b/libs/assets/src/cow-swap/icon-uni.svg new file mode 100644 index 0000000000..a88a09ae2e --- /dev/null +++ b/libs/assets/src/cow-swap/icon-uni.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/ui/src/enum.ts b/libs/ui/src/enum.ts index 8a2986e83b..7b083a0990 100644 --- a/libs/ui/src/enum.ts +++ b/libs/ui/src/enum.ts @@ -60,6 +60,18 @@ export enum UI { COLOR_DANGER_BG = '--cow-color-danger-bg', COLOR_DANGER_TEXT = '--cow-color-danger-text', + // CoW AMM Colors + COLOR_COWAMM_DARK_GREEN = '--cow-color-cowamm-dark-green', + COLOR_COWAMM_DARK_GREEN_OPACITY_30 = '--cow-color-cowamm-dark-green-opacity-30', + COLOR_COWAMM_DARK_GREEN_OPACITY_15 = '--cow-color-cowamm-dark-green-opacity-15', + COLOR_COWAMM_GREEN = '--cow-color-cowamm-green', + COLOR_COWAMM_LIGHT_GREEN = '--cow-color-cowamm-light-green', + COLOR_COWAMM_LIGHT_GREEN_OPACITY_30 = '--cow-color-cowamm-light-green-opacity-30', + COLOR_COWAMM_LIGHTER_GREEN = '--cow-color-cowamm-lighter-green', + COLOR_COWAMM_BLUE = '--cow-color-cowamm-blue', + COLOR_COWAMM_DARK_BLUE = '--cow-color-cowamm-dark-blue', + COLOR_COWAMM_LIGHT_BLUE = '--cow-color-cowamm-light-blue', + // ================================================================================ // Badge diff --git a/libs/ui/src/theme/ThemeColorVars.tsx b/libs/ui/src/theme/ThemeColorVars.tsx index 6f2860c49a..03acc39dcf 100644 --- a/libs/ui/src/theme/ThemeColorVars.tsx +++ b/libs/ui/src/theme/ThemeColorVars.tsx @@ -97,8 +97,20 @@ export const ThemeColorVars = css` ${UI.COLOR_GREEN}: ${({ theme }) => theme.success}; ${UI.COLOR_RED}: ${({ theme }) => theme.danger}; + // CoW AMM Colors + ${UI.COLOR_COWAMM_DARK_GREEN}: #194d05; + ${UI.COLOR_COWAMM_DARK_GREEN_OPACITY_30}: ${() => transparentize('#194d05', 0.7)}; + ${UI.COLOR_COWAMM_DARK_GREEN_OPACITY_15}: ${() => transparentize('#194d05', 0.85)}; + ${UI.COLOR_COWAMM_GREEN}: #2b6f0b; + ${UI.COLOR_COWAMM_LIGHT_GREEN}: #bcec79; + ${UI.COLOR_COWAMM_LIGHT_GREEN_OPACITY_30}: ${() => transparentize('#bcec79', 0.7)}; + ${UI.COLOR_COWAMM_LIGHTER_GREEN}: #dcf8a7; + ${UI.COLOR_COWAMM_BLUE}: #3fc4ff; + ${UI.COLOR_COWAMM_DARK_BLUE}: #012F7A; + ${UI.COLOR_COWAMM_LIGHT_BLUE}: #ccf8ff; + // Base - ${UI.COLOR_CONTAINER_BG_02}: ${UI.COLOR_PAPER}; + ${UI.COLOR_CONTAINER_BG_02}: var(${UI.COLOR_PAPER}); ${UI.MODAL_BACKDROP}: var(${UI.COLOR_TEXT}); ${UI.BORDER_RADIUS_NORMAL}: 24px; ${UI.PADDING_NORMAL}: 24px; @@ -115,7 +127,7 @@ export const ThemeColorVars = css` ${UI.COLOR_TEXT_OPACITY_25}: ${({ theme }) => transparentize(theme.text, 0.75)}; ${UI.COLOR_TEXT_OPACITY_10}: ${({ theme }) => transparentize(theme.text, 0.9)}; ${UI.COLOR_TEXT2}: ${({ theme }) => transparentize(theme.text, 0.3)}; - ${UI.COLOR_LINK}: ${`var(${UI.COLOR_PRIMARY})`}; + ${UI.COLOR_LINK}: var(${UI.COLOR_PRIMARY}); ${UI.COLOR_LINK_OPACITY_10}: ${({ theme }) => transparentize(theme.info, 0.9)}; // Font Weights & Sizes From f6f6f8cb9c8df72857d55f42d1e521a6784f9126 Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 23 Oct 2024 11:20:51 +0100 Subject: [PATCH 048/116] feat(swap-deadline): higher swap deadline (#5002) * feat: increase max deadline for swaps to 12h * chore: linting * chore: fix typo * feat: use only validFor on quote requests, remove validTo * fix: always send validFor as is * chore: formatting * fix: do not got over max order validity for quotes * fix: remove validTo, no longer in use * feat: use different max swap deadlines for SC and EOAs --- .../src/api/cowProtocol/api.ts | 30 +++++++------------ .../InvalidLocalTimeWarning/index.tsx | 3 +- .../FeesUpdater/isRefetchQuoteRequired.ts | 4 +-- .../FeesUpdater/quoteUsingSameParameters.ts | 7 +++-- .../orders/UnfillableOrdersUpdater.ts | 12 ++------ .../legacy/hooks/useRefetchPriceCallback.tsx | 10 +++++-- .../src/legacy/state/price/types.ts | 1 - .../src/legacy/utils/priceLegacy.ts | 26 +++++----------- .../src/legacy/utils/trade.ts | 6 ++-- .../limitOrders/services/tradeFlow/index.ts | 5 ++-- .../tradeFlow/services/swapFlow/index.ts | 3 +- .../containers/TransactionSettings/index.tsx | 8 +++-- .../src/utils/orderUtils/getQuoteValidFor.ts | 3 ++ 13 files changed, 49 insertions(+), 69 deletions(-) create mode 100644 apps/cowswap-frontend/src/utils/orderUtils/getQuoteValidFor.ts diff --git a/apps/cowswap-frontend/src/api/cowProtocol/api.ts b/apps/cowswap-frontend/src/api/cowProtocol/api.ts index 1eacdf5ea7..90ff5493cf 100644 --- a/apps/cowswap-frontend/src/api/cowProtocol/api.ts +++ b/apps/cowswap-frontend/src/api/cowProtocol/api.ts @@ -10,7 +10,6 @@ import { } from '@cowprotocol/common-utils' import { Address, - SupportedChainId as ChainId, CompetitionOrderStatus, CowEnv, EnrichedOrder, @@ -21,6 +20,7 @@ import { OrderQuoteSideKindSell, PartialApiContext, SigningScheme, + SupportedChainId as ChainId, TotalSurplus, Trade, } from '@cowprotocol/cow-sdk' @@ -31,10 +31,13 @@ import { LegacyFeeQuoteParams as FeeQuoteParams } from 'legacy/state/price/types import { getAppData } from 'modules/appData' +import { getQuoteValidFor } from 'utils/orderUtils/getQuoteValidFor' + import { ApiErrorCodes } from './errors/OperatorError' import QuoteApiError, { mapOperatorErrorToQuoteError, QuoteApiErrorDetails } from './errors/QuoteError' import { getIsOrderBookTypedError } from './getIsOrderBookTypedError' + function getProfileUrl(): Partial> { if (isLocal || isDev || isPr || isBarn) { return { @@ -70,7 +73,7 @@ function _fetchProfile( chainId: ChainId, url: string, method: 'GET' | 'POST' | 'DELETE', - data?: any + data?: any, ): Promise { const baseUrl = _getProfileApiBaseUrl(chainId) @@ -96,19 +99,8 @@ const ETH_FLOW_AUX_QUOTE_PARAMS = { } function _mapNewToLegacyParams(params: FeeQuoteParams): OrderQuoteRequest { - const { - amount, - kind, - userAddress, - receiver, - validTo, - validFor, - sellToken, - buyToken, - chainId, - priceQuality, - isEthFlow, - } = params + const { amount, kind, userAddress, receiver, validFor, sellToken, buyToken, chainId, priceQuality, isEthFlow } = + params const fallbackAddress = userAddress || ZERO_ADDRESS const { appData, appDataHash } = _getAppDataQuoteParams(params) @@ -122,7 +114,7 @@ function _mapNewToLegacyParams(params: FeeQuoteParams): OrderQuoteRequest { appDataHash, partiallyFillable: false, priceQuality, - ...(validFor ? { validFor } : { validTo }), + ...getQuoteValidFor(validFor), } if (isEthFlow) { @@ -172,7 +164,7 @@ export async function getQuote(params: FeeQuoteParams): Promise { return orderBookApi.getOrders(params, context) } @@ -227,7 +219,7 @@ export async function getSurplusData(chainId: ChainId, address: string): Promise export async function getOrderCompetitionStatus( chainId: ChainId, - orderId: string + orderId: string, ): Promise { try { return await orderBookApi.getOrderCompetitionStatus(orderId, { chainId }) diff --git a/apps/cowswap-frontend/src/common/containers/InvalidLocalTimeWarning/index.tsx b/apps/cowswap-frontend/src/common/containers/InvalidLocalTimeWarning/index.tsx index 0a9b8e5401..ae41704018 100644 --- a/apps/cowswap-frontend/src/common/containers/InvalidLocalTimeWarning/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/InvalidLocalTimeWarning/index.tsx @@ -16,8 +16,7 @@ export function InvalidLocalTimeWarning() { return ( - Local device time does is not accurate, CoW Swap most likely will not work correctly. Please adjust your device's - time. + Local device time is not accurate, CoW Swap most likely will not work correctly. Please adjust your device's time. ) } diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts index 5b4714631b..12aa904b67 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts @@ -8,8 +8,6 @@ import { quoteUsingSameParameters } from './quoteUsingSameParameters' const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s) -type FeeQuoteParams = Omit - /** * Returns if the quote has been recently checked */ @@ -30,7 +28,7 @@ function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): bool */ export function isRefetchQuoteRequired( isLoading: boolean, - currentParams: FeeQuoteParams, + currentParams: LegacyFeeQuoteParams, quoteInformation?: QuoteInformationObject, ): boolean { // If there's no quote/fee information, we always re-fetch diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts index b41f5e0a66..4d43f12e5f 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts @@ -3,14 +3,15 @@ import { LegacyFeeQuoteParams } from 'legacy/state/price/types' import { decodeAppData } from 'modules/appData' -type FeeQuoteParams = Omit - /** * Checks if the parameters for the current quote are correct * * Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched */ -export function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean { +export function quoteUsingSameParameters( + currentParams: LegacyFeeQuoteParams, + quoteInfo: QuoteInformationObject, +): boolean { const { amount: currentAmount, sellToken: currentSellToken, diff --git a/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts index 033f4e14e5..7667007a5e 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts @@ -5,7 +5,6 @@ import { useTokensBalances } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCY_ADDRESS, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/common-const' import { useIsWindowVisible } from '@cowprotocol/common-hooks' import { getPromiseFulfilledValue, isSellOrder } from '@cowprotocol/common-utils' -import { timestamp } from '@cowprotocol/contracts' import { PriceQuality, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { UiOrderType } from '@cowprotocol/types' import { useWalletInfo } from '@cowprotocol/wallet' @@ -22,7 +21,7 @@ import { getEstimatedExecutionPrice, getOrderMarketPrice, getRemainderAmount, - isOrderUnfillable, + isOrderUnfillable } from 'legacy/state/orders/utils' import type { LegacyFeeQuoteParams } from 'legacy/state/price/types' import { getBestQuote } from 'legacy/utils/price' @@ -222,13 +221,8 @@ async function _getOrderPrice(chainId: ChainId, order: Order, strategy: PriceStr } const legacyFeeQuoteParams = quoteParams as LegacyFeeQuoteParams - // Limit order may have arbitrary validTo, but API doesn't allow values greater than 1 hour - // To avoid ExcessiveValidTo error we use PRICE_QUOTE_VALID_TO_TIME - if (order.class === 'limit') { - legacyFeeQuoteParams.validFor = Math.round(PRICE_QUOTE_VALID_TO_TIME / 1000) - } else { - legacyFeeQuoteParams.validTo = timestamp(order.validTo) - } + + legacyFeeQuoteParams.validFor = Math.round(PRICE_QUOTE_VALID_TO_TIME / 1000) try { return getBestQuote({ strategy, quoteParams, fetchFee: false, isPriceRefresh: false }) diff --git a/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx b/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx index 9c0144dfd7..253f5dc137 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx +++ b/apps/cowswap-frontend/src/legacy/hooks/useRefetchPriceCallback.tsx @@ -16,18 +16,20 @@ import { QuoteError } from 'legacy/state/price/actions' import { useQuoteDispatchers } from 'legacy/state/price/hooks' import { QuoteInformationObject } from 'legacy/state/price/reducer' import { LegacyFeeQuoteParams, LegacyQuoteParams } from 'legacy/state/price/types' -import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { getBestQuote, getFastQuote, QuoteResult } from 'legacy/utils/price' import { useIsEoaEthFlow } from 'modules/trade' import { ApiErrorCodes, isValidOperatorError } from 'api/cowProtocol/errors/OperatorError' import QuoteApiError, { + isValidQuoteError, QuoteApiErrorCodes, QuoteApiErrorDetails, - isValidQuoteError, } from 'api/cowProtocol/errors/QuoteError' +import { PRICE_QUOTE_VALID_TO_TIME } from '../../common/constants/quote' +import { useUserTransactionTTL } from '../state/user/hooks' + interface HandleQuoteErrorParams { quoteData: QuoteInformationObject | LegacyFeeQuoteParams error: unknown @@ -114,6 +116,8 @@ function handleQuoteError({ quoteData, error, addUnsupportedToken }: HandleQuote const getBestQuoteResolveOnlyLastCall = onlyResolvesLast(getBestQuote) const getFastQuoteResolveOnlyLastCall = onlyResolvesLast(getFastQuote) +const MAX_VALID_FOR = Math.round(PRICE_QUOTE_VALID_TO_TIME / 1000) + /** * @returns callback that fetches a new quote and update the state */ @@ -131,7 +135,7 @@ export function useRefetchQuoteCallback() { async (params: QuoteParamsForFetching) => { const { quoteParams, isPriceRefresh } = params // set the validTo time here - quoteParams.validFor = deadline + quoteParams.validFor = Math.min(deadline, MAX_VALID_FOR) // do not go over MAX_VALID_FOR quoteParams.isEthFlow = isEoaEthFlow let quoteData: LegacyFeeQuoteParams | QuoteInformationObject = quoteParams diff --git a/apps/cowswap-frontend/src/legacy/state/price/types.ts b/apps/cowswap-frontend/src/legacy/state/price/types.ts index cc1f97fae0..8081221c24 100644 --- a/apps/cowswap-frontend/src/legacy/state/price/types.ts +++ b/apps/cowswap-frontend/src/legacy/state/price/types.ts @@ -20,7 +20,6 @@ interface FeeQuoteParams extends Pick + oneInchPriceResult: PromiseSettledResult, ): [Array, Array] { // Prepare an array with all successful estimations const priceQuotes: Array = [] @@ -131,7 +132,7 @@ function _checkFeeErrorForData(error: QuoteApiError) { */ export async function getBestPrice( params: LegacyPriceQuoteParams, - options?: GetBestPriceOptions + options?: GetBestPriceOptions, ): Promise { // Get all prices const { oneInchPriceResult } = await getAllPrices(params) @@ -181,19 +182,8 @@ export async function getBestQuoteLegacy({ fetchFee, previousResponse, }: Omit): Promise { - const { - sellToken, - buyToken, - fromDecimals, - toDecimals, - amount, - kind, - chainId, - userAddress, - validTo, - validFor, - priceQuality, - } = quoteParams + const { sellToken, buyToken, fromDecimals, toDecimals, amount, kind, chainId, userAddress, validFor, priceQuality } = + quoteParams const { baseToken, quoteToken } = getCanonicalMarket({ sellToken, buyToken, kind }) // Get a new fee quote (if required) const feePromise = fetchFee || !previousResponse ? getQuote(quoteParams) : Promise.resolve(previousResponse) @@ -238,7 +228,7 @@ export async function getBestQuoteLegacy({ kind, userAddress, priceQuality, - ...(validFor ? { validFor } : { validTo }), + ...getQuoteValidFor(validFor), }) : // fee exceeds our price, is invalid Promise.reject(FEE_EXCEEDS_FROM_ERROR) diff --git a/apps/cowswap-frontend/src/legacy/utils/trade.ts b/apps/cowswap-frontend/src/legacy/utils/trade.ts index f88cfde3fd..6184cf9e98 100644 --- a/apps/cowswap-frontend/src/legacy/utils/trade.ts +++ b/apps/cowswap-frontend/src/legacy/utils/trade.ts @@ -65,7 +65,7 @@ export function getOrderSubmitSummary( params: Pick< PostOrderParams, 'kind' | 'account' | 'inputAmount' | 'outputAmount' | 'recipient' | 'recipientAddressOrName' | 'feeAmount' - > + >, ): string { const { kind, account, inputAmount, outputAmount, recipient, recipientAddressOrName, feeAmount } = params @@ -249,7 +249,7 @@ export async function signAndPostOrder(params: PostOrderParams): Promise { diff --git a/apps/cowswap-frontend/src/utils/orderUtils/getQuoteValidFor.ts b/apps/cowswap-frontend/src/utils/orderUtils/getQuoteValidFor.ts new file mode 100644 index 0000000000..9f8b140d2d --- /dev/null +++ b/apps/cowswap-frontend/src/utils/orderUtils/getQuoteValidFor.ts @@ -0,0 +1,3 @@ +export function getQuoteValidFor(validFor: number | undefined) { + return validFor ? { validFor } : undefined +} From 435bfdfa3e68cea1652bc00dcf5908bbc991d7b1 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari <55461956+yvesfracari@users.noreply.github.com> Date: Wed, 23 Oct 2024 08:03:39 -0300 Subject: [PATCH 049/116] feat(hook-store): create bundle hooks tenderly simulation (#4943) * chore: init tenderly module * feat: enable bundle simulations * refactor: change bundle simulation to use SWR * chore: consider custom recipient * chore: remove goldrush sdk * fix: error on post hooks get simulation * refactor: use bff on bundle simulation feature * chore: remove console.logs * chore: fix leandro comments * chore: remove unused tenderly consts * refactor: rename tenderly simulation hook * chore: refactor top token holder swr to jotai with cache * chore: rename hook to match file name * refactor: use seconds for cache time in toptokenholder state --- apps/cowswap-frontend/.env | 2 +- apps/cowswap-frontend/package.json | 2 +- .../hooks/useSetupHooksStoreOrderParams.ts | 1 + .../hooksStore/pure/AppliedHookItem/index.tsx | 45 +++---- .../pure/AppliedHookItem/styled.tsx | 16 +++ .../tenderly/hooks/useGetTopTokenHolders.ts | 20 +++ .../hooks/useTenderlyBundleSimulation.ts | 106 ++++++++++++++++ .../modules/tenderly/state/topTokenHolders.ts | 55 ++++++++ .../src/modules/tenderly/types.ts | 26 ++++ .../tenderly/utils/bundleSimulation.ts | 117 ++++++++++++++++++ .../tenderly/utils/generateSimulationData.ts | 36 ++++++ .../tenderly/utils/getTokenTransferInfo.ts | 45 +++++++ libs/hook-dapp-lib/src/types.ts | 1 + 13 files changed, 448 insertions(+), 24 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/types.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts diff --git a/apps/cowswap-frontend/.env b/apps/cowswap-frontend/.env index 9461bc47d5..2ea99eeddf 100644 --- a/apps/cowswap-frontend/.env +++ b/apps/cowswap-frontend/.env @@ -135,4 +135,4 @@ REACT_APP_MOCK=true # REACT_APP_DOMAIN_REGEX_ENS="(:?^cowswap\.eth|ipfs)" # Path regex (to detect environment) -# REACT_APP_PATH_REGEX_ENS="/ipfs" +# REACT_APP_PATH_REGEX_ENS="/ipfs" \ No newline at end of file diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index 0b9af36fdc..457d76151a 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -30,4 +30,4 @@ "dependencies": {}, "devDependencies": {}, "nx": {} -} \ No newline at end of file +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts index d77e7e1907..10c9422649 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts @@ -21,6 +21,7 @@ export function useSetupHooksStoreOrderParams() { buyAmount: orderParams.outputAmount.quotient.toString(), sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency), buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency), + receiver: orderParams.recipient, }) } }, [orderParams, setOrderParams]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx index 09b25cd8cf..d30afa2a05 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -1,3 +1,5 @@ +import { useMemo } from 'react' + import ICON_CHECK_ICON from '@cowprotocol/assets/cow-swap/check-singular.svg' import ICON_GRID from '@cowprotocol/assets/cow-swap/grid.svg' import TenderlyLogo from '@cowprotocol/assets/cow-swap/tenderly-logo.svg' @@ -8,6 +10,8 @@ import { InfoTooltip } from '@cowprotocol/ui' import { Edit2, Trash2, ExternalLink as ExternalLinkIcon } from 'react-feather' import SVG from 'react-inlinesvg' +import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' + import * as styledEl from './styled' import { TenderlySimulate } from '../../containers/TenderlySimulate' @@ -23,25 +27,21 @@ interface HookItemProp { index: number } -// TODO: remove once a tenderly bundle simulation is ready -const isBundleSimulationReady = false +// TODO: refactor tu use single simulation as fallback +const isBundleSimulationReady = true export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) { - // TODO: Determine the simulation status based on actual simulation results - // For demonstration, using a placeholder. Replace with actual logic. - const simulationPassed = true // TODO: Replace with actual condition - const simulationStatus = simulationPassed ? 'Simulation successful' : 'Simulation failed' - const simulationTooltip = simulationPassed - ? 'The Tenderly simulation was successful. Your transaction is expected to succeed.' - : 'The Tenderly simulation failed. Please review your transaction.' + const { isValidating, data } = useTenderlyBundleSimulation() - // TODO: Placeholder for Tenderly simulation URL; replace with actual logic when available - const tenderlySimulationUrl = '' // e.g., 'https://tenderly.co/simulation/12345' + const simulationData = useMemo(() => { + if (!data) return + return data[hookDetails.uuid] + }, [data, hookDetails.uuid]) - // TODO: Determine if simulation passed or failed - const isSimulationSuccessful = simulationPassed - - if (!dapp) return null + const simulationStatus = simulationData?.status ? 'Simulation successful' : 'Simulation failed' + const simulationTooltip = simulationData?.status + ? 'The Tenderly simulation was successful. Your transaction is expected to succeed.' + : 'The Tenderly simulation failed. Please review your transaction.' return ( @@ -51,8 +51,9 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo {index + 1} - {dapp.name} - {dapp.name} + {dapp?.name} + {dapp?.name} + {isValidating && } editHook(hookDetails.uuid)}> @@ -64,15 +65,15 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo - {account && isBundleSimulationReady && ( - - {isSimulationSuccessful ? ( + {account && isBundleSimulationReady && simulationData && ( + + {simulationData.status ? ( ) : ( )} - {tenderlySimulationUrl ? ( - + {simulationData.link ? ( + {simulationStatus} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx index 54a937e93f..10bf26cc22 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx @@ -211,3 +211,19 @@ export const SimulateFooter = styled.div` padding: 2px; } ` + +export const Spinner = styled.div` + border: 5px solid transparent; + border-top-color: ${`var(${UI.COLOR_PRIMARY_LIGHTER})`}; + border-radius: 50%; + animation: spin 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +` diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts new file mode 100644 index 0000000000..194a086a11 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts @@ -0,0 +1,20 @@ +import { useAtom } from 'jotai' +import { useCallback } from 'react' + +import { topTokenHoldersAtom } from '../state/topTokenHolders' +import { GetTopTokenHoldersParams } from '../types' + +export function useGetTopTokenHolders() { + const [cachedData, fetchTopTokenHolders] = useAtom(topTokenHoldersAtom) + + return useCallback( + async (params: GetTopTokenHoldersParams) => { + const key = `${params.chainId}-${params.tokenAddress}` + if (cachedData[key]?.value) { + return cachedData[key].value + } + return fetchTopTokenHolders(params) + }, + [cachedData, fetchTopTokenHolders], + ) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts new file mode 100644 index 0000000000..7ab1478887 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -0,0 +1,106 @@ +import { useCallback } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import useSWR from 'swr' + +import { useHooks } from 'modules/hooksStore' +import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams' + +import { useTokenContract } from 'common/hooks/useContract' + +import { useGetTopTokenHolders } from './useGetTopTokenHolders' + +import { completeBundleSimulation, preHooksBundleSimulation } from '../utils/bundleSimulation' +import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData' +import { getTokenTransferInfo } from '../utils/getTokenTransferInfo' + +export function useTenderlyBundleSimulation() { + const { account, chainId } = useWalletInfo() + const { preHooks, postHooks } = useHooks() + const orderParams = useOrderParams() + const tokenSell = useTokenContract(orderParams?.sellTokenAddress) + const tokenBuy = useTokenContract(orderParams?.buyTokenAddress) + const buyAmount = orderParams?.buyAmount + const sellAmount = orderParams?.sellAmount + const orderReceiver = orderParams?.receiver || account + + const getTopTokenHolder = useGetTopTokenHolders() + + const simulateBundle = useCallback(async () => { + if (postHooks.length === 0 && preHooks.length === 0) return + + if (!postHooks.length) + return preHooksBundleSimulation({ + chainId, + preHooks, + }) + + if (!account || !tokenBuy || !tokenSell || !buyAmount || !sellAmount || !orderReceiver) { + return + } + + const buyTokenTopHolders = await getTopTokenHolder({ + tokenAddress: tokenBuy.address, + chainId, + }) + + if (!buyTokenTopHolders) return + + const tokenBuyTransferInfo = getTokenTransferInfo({ + tokenHolders: buyTokenTopHolders, + amountToTransfer: buyAmount, + }) + + const paramsComplete = { + postHooks, + preHooks, + tokenBuy, + tokenBuyTransferInfo, + sellAmount, + orderReceiver, + tokenSell, + account, + chainId, + } + + return completeBundleSimulation(paramsComplete) + }, [ + account, + chainId, + getTopTokenHolder, + tokenBuy, + postHooks, + preHooks, + buyAmount, + sellAmount, + orderReceiver, + tokenSell, + ]) + + const getNewSimulationData = useCallback(async () => { + try { + const simulationData = await simulateBundle() + + if (!simulationData) { + return {} + } + + return generateNewSimulationData(simulationData, { preHooks, postHooks }) + } catch { + return generateSimulationDataToError({ preHooks, postHooks }) + } + }, [preHooks, postHooks, simulateBundle]) + + const { data, isValidating: isBundleSimulationLoading } = useSWR( + ['tenderly-bundle-simulation', postHooks, preHooks, orderParams?.sellTokenAddress, orderParams?.buyTokenAddress], + getNewSimulationData, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }, + ) + + return { data, isValidating: isBundleSimulationLoading } +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts new file mode 100644 index 0000000000..64910555ca --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts @@ -0,0 +1,55 @@ +import { atom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +import { BFF_BASE_URL } from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +export interface GetTopTokenHoldersParams { + tokenAddress?: string + chainId: SupportedChainId +} + +export interface TokenHolder { + address: string + balance: string +} + +export async function getTopTokenHolder({ tokenAddress, chainId }: GetTopTokenHoldersParams) { + if (!tokenAddress) return + + return (await fetch(`${BFF_BASE_URL}/${chainId}/tokens/${tokenAddress}/topHolders`, { + method: 'GET', + }).then((res) => res.json())) as TokenHolder[] +} + +interface CachedValue { + value: T + timestamp: number +} + +const baseTopTokenHolderAtom = atomWithStorage>>( + 'topTokenHolders:v1', + {}, +) + +export const topTokenHoldersAtom = atom( + (get) => get(baseTopTokenHolderAtom), + async (get, set, params: GetTopTokenHoldersParams) => { + const key = `${params.chainId}:${params.tokenAddress?.toLowerCase()}` + const cachedData = get(baseTopTokenHolderAtom) + const currentTime = Date.now() / 1000 + + // 1 hour in seconds + if (cachedData[key] && currentTime - cachedData[key].timestamp <= 3600) { + return cachedData[key].value + } + + const newValue = await getTopTokenHolder(params) + set(baseTopTokenHolderAtom, { + ...cachedData, + [key]: { value: newValue, timestamp: currentTime }, + }) + + return newValue + }, +) diff --git a/apps/cowswap-frontend/src/modules/tenderly/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts new file mode 100644 index 0000000000..9ef8eb6b56 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -0,0 +1,26 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +export interface SimulationInput { + input: string + from: string + to: string + value?: string + gas?: number + gas_price?: string +} + +export interface SimulationData { + link: string + status: boolean + id: string +} + +export interface GetTopTokenHoldersParams { + tokenAddress?: string + chainId: SupportedChainId +} + +export interface TokenHolder { + address: string + balance: string +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts new file mode 100644 index 0000000000..63d7c8dd0a --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -0,0 +1,117 @@ +import { Erc20 } from '@cowprotocol/abis' +import { BFF_BASE_URL } from '@cowprotocol/common-const' +import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' + +import { CowHook } from 'modules/hooksStore/types/hooks' + +import { SimulationData, SimulationInput } from '../types' + +export interface GetTransferTenderlySimulationInput { + currencyAmount: string + from: string + receiver: string + token: Erc20 +} + +export type TokenBuyTransferInfo = { + sender: string + amount: string +}[] +export interface PostBundleSimulationParams { + account: string + chainId: SupportedChainId + tokenSell: Erc20 + tokenBuy: Erc20 + preHooks: CowHookDetails[] + postHooks: CowHookDetails[] + sellAmount: string + orderReceiver: string + tokenBuyTransferInfo: TokenBuyTransferInfo +} + +export const completeBundleSimulation = async (params: PostBundleSimulationParams): Promise => { + const input = getBundleTenderlySimulationInput(params) + return simulateBundle(input, params.chainId) +} + +export const preHooksBundleSimulation = async ( + params: Pick, +): Promise => { + const input = params.preHooks.map((hook) => + getCoWHookTenderlySimulationInput(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[params.chainId], hook.hook), + ) + return simulateBundle(input, params.chainId) +} + +const simulateBundle = async (input: SimulationInput[], chainId: SupportedChainId): Promise => { + const response = await fetch(`${BFF_BASE_URL}/${chainId}/simulation/simulateBundle`, { + method: 'POST', + body: JSON.stringify(input), + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => res.json()) + + return response as SimulationData[] +} + +export function getCoWHookTenderlySimulationInput(from: string, params: CowHook): SimulationInput { + return { + input: params.callData, + to: params.target, + from, + } +} + +export function getTransferTenderlySimulationInput({ + currencyAmount, + from, + receiver, + token, +}: GetTransferTenderlySimulationInput): SimulationInput { + const callData = token.interface.encodeFunctionData('transfer', [receiver, currencyAmount]) + + return { + input: callData, + to: token.address, + from, + } +} + +export function getBundleTenderlySimulationInput({ + account, + chainId, + tokenSell, + tokenBuy, + preHooks, + postHooks, + sellAmount, + orderReceiver, + tokenBuyTransferInfo, +}: PostBundleSimulationParams): SimulationInput[] { + const settlementAddress = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId] + const preHooksSimulations = preHooks.map((hook) => getCoWHookTenderlySimulationInput(settlementAddress, hook.hook)) + const postHooksSimulations = postHooks.map((hook) => getCoWHookTenderlySimulationInput(settlementAddress, hook.hook)) + + // If there are no post hooks, we don't need to simulate the transfer + if (postHooks.length === 0) return preHooksSimulations + + const sellTokenTransfer = getTransferTenderlySimulationInput({ + currencyAmount: sellAmount, + from: account, + receiver: COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], + token: tokenSell, + }) + + const buyTokenTransfers = tokenBuyTransferInfo.map((transferInfo) => + getTransferTenderlySimulationInput({ + currencyAmount: transferInfo.amount, + from: transferInfo.sender, + receiver: postHooks[0].recipientOverride || orderReceiver, + token: tokenBuy, + }), + ) + + return [...preHooksSimulations, sellTokenTransfer, ...buyTokenTransfers, ...postHooksSimulations] +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts new file mode 100644 index 0000000000..5d3416b736 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -0,0 +1,36 @@ +import { PostBundleSimulationParams } from './bundleSimulation' + +import { SimulationData } from '../types' + +export function generateSimulationDataToError( + postParams: Pick, +): Record { + const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid) + const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid) + const hooksKeys = [...preHooksKeys, ...postHooksKeys] + + return hooksKeys.reduce( + (acc, key) => ({ + ...acc, + [key]: { link: '', status: false, id: key }, + }), + {}, + ) +} + +export function generateNewSimulationData( + simulationData: SimulationData[], + postParams: Pick, +): Record { + const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid) + const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid) + + const preHooksData = simulationData.slice(0, preHooksKeys.length) + + const postHooksData = simulationData.slice(-postHooksKeys.length) + + return { + ...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}), + ...postHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: postHooksData[index] }), {}), + } +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts new file mode 100644 index 0000000000..1b3a232e4e --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts @@ -0,0 +1,45 @@ +import { BigNumber } from 'ethers' + +import { TokenBuyTransferInfo } from './bundleSimulation' + +import { TokenHolder } from '../types' + +export function getTokenTransferInfo({ + tokenHolders, + amountToTransfer, +}: { + tokenHolders: TokenHolder[] + amountToTransfer: string +}): TokenBuyTransferInfo { + const amountToTransferBigNumber = BigNumber.from(amountToTransfer) + let sum = BigNumber.from('0') + const result: TokenBuyTransferInfo = [] + + if (!tokenHolders) { + return result + } + + for (const tokenHolder of tokenHolders) { + // skip token holders with no address or balance + if (!tokenHolder.address || !tokenHolder.balance) continue + + const tokenHolderAmount = BigNumber.from(tokenHolder.balance) + const sumWithTokenHolder = sum.add(tokenHolderAmount) + + if (sumWithTokenHolder.gte(amountToTransferBigNumber)) { + const remainingAmount = amountToTransferBigNumber.sub(sum) + result.push({ + sender: tokenHolder.address, + amount: remainingAmount.toString(), + }) + break + } + sum = sum.add(tokenHolderAmount) + result.push({ + sender: tokenHolder.address, + amount: tokenHolderAmount.toString(), + }) + } + + return result +} diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index 9adea48b49..57c2c2d65d 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -44,6 +44,7 @@ export interface HookDappOrderParams { validTo: number sellTokenAddress: string buyTokenAddress: string + receiver: string sellAmount: string buyAmount: string } From 7d81f2772f7ba5de3a7dcfb17b7d7d51137a72d8 Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 23 Oct 2024 14:31:29 +0100 Subject: [PATCH 050/116] chore: fix lint issues (#5025) --- .../src/common/hooks/useMenuItems.ts | 2 +- .../pure/CoWAMMBanner/CoWAmmBannerContent.tsx | 24 ++++++----- .../src/common/pure/CoWAMMBanner/styled.ts | 5 +-- .../common/pure/OrderProgressBarV2/index.tsx | 33 ++------------- .../containers/TenderlySimulate/index.tsx | 2 +- .../AirdropHookApp/hooks/useClaimData.ts | 4 +- .../dapps/ClaimGnoHookApp/index.tsx | 7 ++-- .../hooksStore/dapps/PermitHookApp/index.tsx | 2 +- .../hooksStore/hooks/useTenderlySimulate.ts | 4 +- .../CustomDappLoader/index.tsx | 2 +- .../ConfirmSwapModalSetup/index.tsx | 11 ++++- .../swap/containers/SwapWidget/index.tsx | 40 ++++++++++--------- .../modules/trade/hooks/useResetRecipient.ts | 2 +- .../containers/RowSlippage/index.tsx | 1 + .../containers/SettingsTab/index.tsx | 8 ++-- .../containers/TransactionSettings/index.tsx | 6 +-- .../yield/hooks/useYieldWidgetActions.ts | 5 +-- 17 files changed, 75 insertions(+), 83 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts index bf6a72af20..bdc1ee255d 100644 --- a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts +++ b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts @@ -21,5 +21,5 @@ export function useMenuItems() { } return items - }, [isHooksStoreEnabled]) + }, [isHooksStoreEnabled, isYieldEnabled]) } diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx index 2a2fd6d5dd..9a6557cd21 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx @@ -1,5 +1,4 @@ -import React from 'react' -import { useCallback, useMemo, useRef } from 'react' +import React, { useCallback, useMemo, useRef } from 'react' import ICON_ARROW from '@cowprotocol/assets/cow-swap/arrow.svg' import ICON_CURVE from '@cowprotocol/assets/cow-swap/icon-curve.svg' @@ -7,6 +6,9 @@ import ICON_PANCAKESWAP from '@cowprotocol/assets/cow-swap/icon-pancakeswap.svg' import ICON_SUSHISWAP from '@cowprotocol/assets/cow-swap/icon-sushi.svg' import ICON_UNISWAP from '@cowprotocol/assets/cow-swap/icon-uni.svg' import ICON_STAR from '@cowprotocol/assets/cow-swap/star-shine.svg' +import { USDC, WBTC } from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { TokenLogo } from '@cowprotocol/tokens' import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' import SVG from 'react-inlinesvg' @@ -16,14 +18,18 @@ import { upToSmall, useMediaQuery } from 'legacy/hooks/useMediaQuery' import { useIsDarkMode } from 'legacy/state/user/hooks' import { ArrowBackground } from './arrowBackground' -import { LpToken, StateKey, dummyData, lpTokenConfig } from './dummyData' +import { + dummyData, + DummyDataType, + InferiorYieldScenario, + LpToken, + lpTokenConfig, + StateKey, + TwoLpScenario, +} from './dummyData' import * as styledEl from './styled' import { BannerLocation, DEMO_DROPDOWN_OPTIONS } from './index' -import { TokenLogo } from '../../../../../../libs/tokens/src/pure/TokenLogo' -import { USDC, WBTC } from '@cowprotocol/common-const' -import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { DummyDataType, TwoLpScenario, InferiorYieldScenario } from './dummyData' const lpTokenIcons: Record = { [LpToken.UniswapV2]: ICON_UNISWAP, @@ -197,7 +203,7 @@ export function CoWAmmBannerContent({ return `yield over average UNI-V2 pool` } } - }, [selectedState, location, lpTokenConfig, isDarkMode]) + }, [selectedState, location, lpTokenConfig, isDarkMode, dummyData]) const lpEmblems = useMemo(() => { const tokens = lpTokenConfig[selectedState] @@ -251,7 +257,7 @@ export function CoWAmmBannerContent({ {renderEmblemContent()} ) - }, [selectedState, lpTokenConfig, lpTokenIcons]) + }, [selectedState, lpTokenConfig]) const renderProductLogo = useCallback( (color: string) => ( diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts index ba9093db41..9f506bdfb8 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts @@ -1,8 +1,8 @@ -import { UI, Media } from '@cowprotocol/ui' +import { TokenLogoWrapper } from '@cowprotocol/tokens' +import { Media, UI } from '@cowprotocol/ui' import { X } from 'react-feather' import styled, { keyframes } from 'styled-components/macro' -import { TokenLogoWrapper } from '../../../../../../libs/tokens/src/pure/TokenLogo' const arrowUpAnimation = keyframes` 0% { @@ -393,7 +393,6 @@ export const LpEmblemItem = styled.div<{ return styleMap[totalItems]?.[index] || '' }} - ${TokenLogoWrapper} { width: 100%; height: 100%; diff --git a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx index 5b23384a84..985c97a570 100644 --- a/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/OrderProgressBarV2/index.tsx @@ -22,8 +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 { ExternalLink, InfoTooltip, ProductLogo, ProductVariant, TokenAmount, UI } from '@cowprotocol/ui' -import { Confetti } 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' @@ -47,6 +46,7 @@ import { SurplusData } from 'common/hooks/useGetSurplusFiatValue' import { getIsCustomRecipient } from 'utils/orderUtils/getIsCustomRecipient' import * as styledEl from './styled' + const IS_DEBUG_MODE = false const DEBUG_FORCE_SHOW_SURPLUS = false @@ -328,7 +328,7 @@ export function OrderProgressBarV2(props: OrderProgressBarV2Props) { }, [currentStep, getDuration]) // Ensure StepComponent will be a valid React component or null - let StepComponent: React.ComponentType | null = null + let StepComponent: React.ComponentType | null if (currentStep === 'cancellationFailed' || currentStep === 'finished') { StepComponent = FinishedStep @@ -409,21 +409,6 @@ function RenderProgressTopSection({ const shouldShowSurplus = DEBUG_FORCE_SHOW_SURPLUS || showSurplus const surplusPercentValue = surplusPercent ? parseFloat(surplusPercent).toFixed(2) : 'N/A' - const shareOnTwitter = useCallback(() => { - const twitterUrl = shouldShowSurplus - ? getTwitterShareUrl(surplusData, order) - : getTwitterShareUrlForBenefit(randomBenefit) - window.open(twitterUrl, '_blank', 'noopener,noreferrer') - }, [shouldShowSurplus, surplusData, order, randomBenefit]) - - const trackShareClick = useCallback(() => { - cowAnalytics.sendEvent({ - category: Category.PROGRESS_BAR, - action: 'Click Share Button', - label: shouldShowSurplus ? 'Surplus' : 'Benefit', - }) - }, [shouldShowSurplus]) - const content = useMemo(() => { switch (stepName) { case 'initial': @@ -577,17 +562,7 @@ function RenderProgressTopSection({ default: return null } - }, [ - stepName, - order, - countdown, - randomImage, - randomBenefit, - shouldShowSurplus, - surplusPercentValue, - shareOnTwitter, - trackShareClick, - ]) + }, [stepName, order, countdown, randomImage, randomBenefit, shouldShowSurplus, surplusPercentValue]) return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx index ef3a5adb4a..d7a7821fe0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx @@ -52,7 +52,7 @@ export function TenderlySimulate({ hook }: TenderlySimulateProps) { } finally { setIsLoading(false) } - }, [simulate, hook, hookId, setSimulationError]) + }, [simulate, hook, hookId, setSimulationError, setSimulationLink]) if (isLoading) { return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hooks/useClaimData.ts b/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hooks/useClaimData.ts index 8d23c9ffbe..507b10b62d 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hooks/useClaimData.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hooks/useClaimData.ts @@ -9,7 +9,7 @@ import useSWR from 'swr' import { useContract } from 'common/hooks/useContract' -import { AirdropDataInfo, IClaimData, IAirdrop } from '../types' +import { AirdropDataInfo, IAirdrop, IClaimData } from '../types' type IntervalsType = { [key: string]: string } @@ -136,7 +136,7 @@ export const useClaimData = (selectedAirdrop?: IAirdrop) => { formattedAmount, } }, - [account, airdropContract], + [account, airdropContract, selectedAirdrop], ) return useSWR( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx index e5d24ee68f..8112c3d620 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx @@ -1,8 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { ButtonPrimary } from '@cowprotocol/ui' -import { UI } from '@cowprotocol/ui' +import { ButtonPrimary, UI } from '@cowprotocol/ui' import { useWalletProvider } from '@cowprotocol/wallet-provider' import { BigNumber } from '@ethersproject/bignumber' @@ -11,7 +10,7 @@ import { formatUnits } from 'ethers/lib/utils' import { SBC_DEPOSIT_CONTRACT_ADDRESS, SBCDepositContract } from './const' import { HookDappProps } from '../../types/hooks' -import { ContentWrapper, Text, LoadingLabel, Wrapper } from '../styled' +import { ContentWrapper, LoadingLabel, Text, Wrapper } from '../styled' const SbcDepositContractInterface = SBCDepositContract.interface @@ -38,7 +37,7 @@ export function ClaimGnoHookApp({ context }: HookDappProps) { } return SbcDepositContractInterface.encodeFunctionData('claimWithdrawal', [account]) - }, [context, account]) + }, [account]) useEffect(() => { if (!account || !provider) { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx index 2f29f33527..c2135b7ebe 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/index.tsx @@ -52,7 +52,7 @@ export function PermitHookApp({ context }: HookDappProps) { if (!token || !isAddress(spenderAddress)) return { message: 'Invalid parameters', disabled: true } if (!isSupportedPermitInfo(permitInfo)) return { message: 'Token not permittable', disabled: true } return { message: confirmMessage, disabled: false } - }, [hookToEdit, token, permitInfo, context.account, tokenAddress, spenderAddress]) + }, [hookToEdit, token, permitInfo, context.account, tokenAddress, spenderAddress, isPermitEnabled, isPreHook]) return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts index 64505270cb..3d41d399a4 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts @@ -8,7 +8,7 @@ import { CowHook } from '../types/hooks' import { SimulationError, TenderlySimulatePayload, TenderlySimulation } from '../types/TenderlySimulation' export function useTenderlySimulate(): (params: CowHook) => Promise { - const { account, chainId } = useWalletInfo() + const { chainId } = useWalletInfo() const settlementContract = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId] return useCallback( @@ -20,7 +20,7 @@ export function useTenderlySimulate(): (params: CowHook) => Promise { isRequestRelevant = false } - }, [input, isSmartContractWallet, chainId]) + }, [input, isSmartContractWallet, chainId, isPreHook, setDappInfo, setLoading, setManifestError]) return null } diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index f0b391f752..f177d742cd 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -94,7 +94,16 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { networkCostsSuffix: shouldPayGas ? : null, networkCostsTooltipSuffix: , }), - [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas, isSmartSlippageApplied], + [ + chainId, + allowedSlippage, + nativeCurrency.symbol, + isEoaEthFlow, + isExactIn, + shouldPayGas, + isSmartSlippageApplied, + smartSlippage, + ], ) const submittedContent = useOrderSubmittedContent(chainId) 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 30c61ac4bc..36737367e8 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -30,14 +30,12 @@ import { TradeWidget, TradeWidgetContainer, TradeWidgetSlots, + useIsEoaEthFlow, + useIsNoImpactWarningAccepted, useReceiveAmountInfo, useTradePriceImpact, -} from 'modules/trade' -import { - useIsEoaEthFlow, useTradeRouteContext, useUnknownImpactWarning, - useIsNoImpactWarningAccepted, } from 'modules/trade' import { getQuoteTimeOffset } from 'modules/tradeQuote' import { useTradeSlippage } from 'modules/tradeSlippage' @@ -188,21 +186,27 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { } const showTwapSuggestionBanner = !enabledTradeTypes || enabledTradeTypes.includes(TradeType.ADVANCED) - const swapWarningsTopProps: SwapWarningsTopProps = { - chainId, - trade, - showTwapSuggestionBanner, - buyingFiatAmount, - priceImpact: priceImpactParams.priceImpact, - tradeUrlParams, - } + const swapWarningsTopProps: SwapWarningsTopProps = useMemo( + () => ({ + chainId, + trade, + showTwapSuggestionBanner, + buyingFiatAmount, + priceImpact: priceImpactParams.priceImpact, + tradeUrlParams, + }), + [chainId, trade, showTwapSuggestionBanner, buyingFiatAmount, priceImpactParams.priceImpact, tradeUrlParams], + ) - const swapWarningsBottomProps: SwapWarningsBottomProps = { - isSupportedWallet, - swapIsUnsupported: isSwapUnsupported, - currencyIn: currencies.INPUT || undefined, - currencyOut: currencies.OUTPUT || undefined, - } + const swapWarningsBottomProps: SwapWarningsBottomProps = useMemo( + () => ({ + isSupportedWallet, + swapIsUnsupported: isSwapUnsupported, + currencyIn: currencies.INPUT || undefined, + currencyOut: currencies.OUTPUT || undefined, + }), + [isSupportedWallet, isSwapUnsupported, currencies.INPUT, currencies.OUTPUT], + ) const slots: TradeWidgetSlots = { settingsWidget: , diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useResetRecipient.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useResetRecipient.ts index b4640608d4..a762d076e2 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useResetRecipient.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useResetRecipient.ts @@ -43,7 +43,7 @@ export function useResetRecipient(onChangeRecipient: (recipient: string | null) if (!postHooksRecipientOverride) { onChangeRecipient(null) } - }, [chainId, onChangeRecipient]) + }, [chainId, onChangeRecipient, postHooksRecipientOverride]) /** * Remove recipient override when its source hook was deleted diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx index 99f75e014f..ad5fe5033b 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/RowSlippage/index.tsx @@ -59,6 +59,7 @@ export function RowSlippage({ smartSlippage, isSmartSlippageApplied, isTradePriceUpdating, + setSlippage, ], ) 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 e43aba6dcb..e4493cb2c1 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -95,22 +95,22 @@ function SettingsTabController({ buttonRef, children }: SettingsTabControllerPro const [settingsTabState, setSettingsTabState] = useAtom(settingsTabStateAtom) const { isExpanded } = useMenuButtonContext() - const toggleMenu = () => { + const toggleMenu = useCallback(() => { buttonRef.current?.dispatchEvent(new Event('mousedown', { bubbles: true })) - } + }, [buttonRef.current]) useEffect(() => { if (settingsTabState.open) { toggleMenu() } - }, [settingsTabState.open]) + }, [settingsTabState.open, toggleMenu]) useEffect(() => { if (settingsTabState.open && !isExpanded) { toggleMenu() setSettingsTabState({ open: false }) } - }, [settingsTabState.open, isExpanded]) + }, [settingsTabState.open, isExpanded, toggleMenu, setSettingsTabState]) return children } diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx index e3fbe7b464..b6605bfe36 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TransactionSettings/index.tsx @@ -131,7 +131,7 @@ export function TransactionSettings({ deadlineState }: TransactionSettingsProps) setSwapSlippage(percentToBps(new Percent(parsed, 10_000))) } }, - [placeholderSlippage, isEoaEthFlow, minEthFlowSlippage], + [placeholderSlippage, isEoaEthFlow, minEthFlowSlippage, minEthFlowSlippageBps, setSwapSlippage], ) const tooLow = swapSlippage.lessThan(new Percent(isEoaEthFlow ? minEthFlowSlippageBps : LOW_SLIPPAGE_BPS, 10_000)) @@ -173,7 +173,7 @@ export function TransactionSettings({ deadlineState }: TransactionSettingsProps) } } }, - [minDeadline, maxDeadline], + [minDeadline, maxDeadline, setDeadline], ) useEffect(() => { @@ -189,7 +189,7 @@ export function TransactionSettings({ deadlineState }: TransactionSettingsProps) setDeadline(value) } } - }, [widgetDeadline, minDeadline, maxDeadline]) + }, [widgetDeadline, minDeadline, maxDeadline, setDeadline]) const isDeadlineDisabled = !!widgetDeadline diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts index 88b044dc26..0573c0504e 100644 --- a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldWidgetActions.ts @@ -5,7 +5,7 @@ import { OrderKind } from '@cowprotocol/cow-sdk' import { Field } from 'legacy/state/types' -import { TradeWidgetActions, useIsWrapOrUnwrap, useOnCurrencySelection, useSwitchTokensPlaces } from 'modules/trade' +import { TradeWidgetActions, useOnCurrencySelection, useSwitchTokensPlaces } from 'modules/trade' import { useUpdateCurrencyAmount } from './useUpdateCurrencyAmount' import { useUpdateYieldRawState } from './useUpdateYieldRawState' @@ -13,7 +13,6 @@ import { useYieldDerivedState } from './useYieldDerivedState' export function useYieldWidgetActions(): TradeWidgetActions { const { inputCurrency, outputCurrency, orderKind } = useYieldDerivedState() - const isWrapOrUnwrap = useIsWrapOrUnwrap() const updateYieldState = useUpdateYieldRawState() const onCurrencySelection = useOnCurrencySelection() const updateCurrencyAmount = useUpdateCurrencyAmount() @@ -28,7 +27,7 @@ export function useYieldWidgetActions(): TradeWidgetActions { updateCurrencyAmount(field, value) }, - [updateCurrencyAmount, isWrapOrUnwrap, inputCurrency, outputCurrency], + [updateCurrencyAmount, inputCurrency, outputCurrency], ) const onSwitchTokens = useSwitchTokensPlaces({ From 92ad33c5d6a280bd1a9e40e6cf49486f6f71130a Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:38:15 +0100 Subject: [PATCH 051/116] feat: fix path (#5022) --- apps/cow-fi/components/ArticlesList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cow-fi/components/ArticlesList.tsx b/apps/cow-fi/components/ArticlesList.tsx index 542c9ede40..7d0520b3c4 100644 --- a/apps/cow-fi/components/ArticlesList.tsx +++ b/apps/cow-fi/components/ArticlesList.tsx @@ -7,7 +7,7 @@ interface ArticlesListProps { articles: Article[] } -const ARTICLES_PATH = '/learn/articles/' +const ARTICLES_PATH = '/learn/' export const ArticlesList: React.FC = ({ articles }) => ( From cfcc57fea728bfab7a76fcf3a796d3b5f303715c Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 24 Oct 2024 17:23:57 +0500 Subject: [PATCH 052/116] chore: do not enable yet in PRs by default (#5030) --- apps/cowswap-frontend/src/common/hooks/useMenuItems.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts index bdc1ee255d..c416b3c0b1 100644 --- a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts +++ b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' -import { isLocal, isPr } from '@cowprotocol/common-utils' +import { isLocal } from '@cowprotocol/common-utils' import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS, YIELD_MENU_ITEM } from '../constants/routes' @@ -16,7 +16,7 @@ export function useMenuItems() { items.push(HOOKS_STORE_MENU_ITEM) } - if (isYieldEnabled || isLocal || isPr) { + if (isYieldEnabled || isLocal) { items.push(YIELD_MENU_ITEM) } From e2d7f97046c9444f0daaaf4e5584f7eab726bd1f Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 24 Oct 2024 18:14:19 +0500 Subject: [PATCH 053/116] fix: fix trade type selector mobile view (#5023) * fix: fix trade type selector mobile view * refactor: get rid of dependency inversion issue * chore: fix trade menu height * chore: fix condition * chore: fix condition --- .../containers/LimitOrdersWidget/index.tsx | 35 ++++++++++-- .../limitOrdersPropsChecker.ts | 2 + .../swap/containers/SwapWidget/index.tsx | 1 + .../TradeWidget/TradeWidgetForm.tsx | 53 ++++--------------- .../trade/containers/TradeWidget/types.ts | 1 + .../containers/TradeWidgetLinks/styled.ts | 9 ++-- .../yield/containers/YieldWidget/index.tsx | 1 + 7 files changed, 50 insertions(+), 52 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index a4a8ddf4b0..a2420948e3 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -1,14 +1,16 @@ import { useAtomValue } from 'jotai' import React, { useMemo } from 'react' +import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg' import { isSellOrder } from '@cowprotocol/common-utils' +import { BannerOrientation, ClosableBanner, InlineBanner } from '@cowprotocol/ui' import { Field } from 'legacy/state/types' import { LimitOrdersWarnings } from 'modules/limitOrders/containers/LimitOrdersWarnings' import { useLimitOrdersWidgetActions } from 'modules/limitOrders/containers/LimitOrdersWidget/hooks/useLimitOrdersWidgetActions' import { TradeButtons } from 'modules/limitOrders/containers/TradeButtons' -import { TradeWidget, TradeWidgetSlots, useTradePriceImpact } from 'modules/trade' +import { TradeWidget, TradeWidgetSlots, useIsWrapOrUnwrap, useTradePriceImpact } from 'modules/trade' import { useTradeConfirmState } from 'modules/trade' import { BulletListItem, UnlockWidgetScreen } from 'modules/trade/pure/UnlockWidgetScreen' import { useSetTradeQuoteParams, useTradeQuote } from 'modules/tradeQuote' @@ -52,6 +54,8 @@ const UNLOCK_SCREEN = { 'https://medium.com/@cow-protocol/cow-swap-improves-the-limit-order-experience-with-partially-fillable-limit-orders-45f19143e87d', } +const ZERO_BANNER_STORAGE_KEY = 'limitOrdersZeroBalanceBanner:v0' + export function LimitOrdersWidget() { const { inputCurrency, @@ -72,6 +76,7 @@ export function LimitOrdersWidget() { const { isLoading: isRateLoading } = useTradeQuote() const rateInfoParams = useRateInfoParams(inputCurrencyAmount, outputCurrencyAmount) const widgetActions = useLimitOrdersWidgetActions() + const isWrapOrUnwrap = useIsWrapOrUnwrap() const { showRecipient: showRecipientSetting } = settingsState const showRecipient = showRecipientSetting || !!recipient @@ -117,6 +122,7 @@ export function LimitOrdersWidget() { settingsState, feeAmount, widgetActions, + isWrapOrUnwrap, } return @@ -134,6 +140,7 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { rateInfoParams, priceImpact, feeAmount, + isWrapOrUnwrap, } = props const tradeContext = useTradeFlowContext() @@ -170,10 +177,28 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { /> ), middleContent: ( - - - - + <> + {!isWrapOrUnwrap && + ClosableBanner(ZERO_BANNER_STORAGE_KEY, (onClose) => ( + +

    + NEW: You can now place limit orders for amounts larger than your wallet balance. Partial fill + orders will execute until you run out of sell tokens. Fill-or-kill orders will become active once you + top up your balance. +

    +
    + ))} + + + + + ), bottomContent(warnings) { return ( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/limitOrdersPropsChecker.ts b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/limitOrdersPropsChecker.ts index 12402335af..42350f5520 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/limitOrdersPropsChecker.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/limitOrdersPropsChecker.ts @@ -23,6 +23,7 @@ export interface LimitOrdersProps { settingsState: LimitOrdersSettingsState feeAmount: CurrencyAmount | null widgetActions: TradeWidgetActions + isWrapOrUnwrap: boolean } export function limitOrdersPropsChecker(a: LimitOrdersProps, b: LimitOrdersProps): boolean { @@ -34,6 +35,7 @@ export function limitOrdersPropsChecker(a: LimitOrdersProps, b: LimitOrdersProps a.showRecipient === b.showRecipient && a.recipient === b.recipient && a.widgetActions === b.widgetActions && + a.isWrapOrUnwrap === b.isWrapOrUnwrap && checkRateInfoParams(a.rateInfoParams, b.rateInfoParams) && checkPriceImpact(a.priceImpact, b.priceImpact) && genericPropsChecker(a.settingsState, b.settingsState) && 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 36737367e8..dc2c80dc66 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -245,6 +245,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { isEoaEthFlow, compactView: true, enableSmartSlippage: true, + isMarketOrderWidget: true, recipient, showRecipient: showRecipientControls, isTradePriceUpdating, diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index 7af3c59f62..4731a75dc7 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -1,9 +1,8 @@ import React, { useCallback, useMemo } from 'react' import ICON_ORDERS from '@cowprotocol/assets/svg/orders.svg' -import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg' import { isInjectedWidget, maxAmountSpend } from '@cowprotocol/common-utils' -import { BannerOrientation, ButtonOutlined, ClosableBanner, InlineBanner, MY_ORDERS_ID } from '@cowprotocol/ui' +import { ButtonOutlined, MY_ORDERS_ID } from '@cowprotocol/ui' import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { t } from '@lingui/macro' @@ -13,9 +12,7 @@ import { AccountElement } from 'legacy/components/Header/AccountElement' import { upToLarge, useMediaQuery } from 'legacy/hooks/useMediaQuery' import { useToggleAccountModal } from 'modules/account' -import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders/hooks/useAdvancedOrdersDerivedState' import { useInjectedWidgetParams } from 'modules/injectedWidget' -import { useIsWidgetUnlocked } from 'modules/limitOrders' import { SetRecipient } from 'modules/swap/containers/SetRecipient' import { useOpenTokenSelectWidget } from 'modules/tokensList' import { useIsAlternativeOrderModalVisible } from 'modules/trade/state/alternativeOrder' @@ -33,14 +30,10 @@ import { TradeWidgetProps } from './types' import { useTradeStateFromUrl } from '../../hooks/setupTradeState/useTradeStateFromUrl' import { useIsWrapOrUnwrap } from '../../hooks/useIsWrapOrUnwrap' -import { useTradeTypeInfo } from '../../hooks/useTradeTypeInfo' -import { TradeType } from '../../types' import { TradeWarnings } from '../TradeWarnings' import { TradeWidgetLinks } from '../TradeWidgetLinks' import { WrapFlowActionButton } from '../WrapFlowActionButton' -const ZERO_BANNER_STORAGE_KEY = 'limitOrdersZeroBalanceBanner:v0' - const scrollToMyOrders = () => { const element = document.getElementById(MY_ORDERS_ID) if (element) { @@ -69,6 +62,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { recipient, hideTradeWarnings, enableSmartSlippage, + isMarketOrderWidget = false, } = params const inputCurrencyInfo = useMemo( @@ -110,23 +104,15 @@ export function TradeWidgetForm(props: TradeWidgetProps) { // Disable too frequent tokens switching const throttledOnSwitchTokens = useThrottleFn(onSwitchTokens, 500) - const tradeTypeInfo = useTradeTypeInfo() - const { isUnlocked: isAdvancedOrdersUnlocked } = useAdvancedOrdersDerivedState() - const isLimitOrdersUnlocked = useIsWidgetUnlocked() const isUpToLarge = useMediaQuery(upToLarge) - const isSwapMode = tradeTypeInfo?.tradeType === TradeType.SWAP - const isLimitOrderMode = tradeTypeInfo?.tradeType === TradeType.LIMIT_ORDER - const isAdvancedMode = tradeTypeInfo?.tradeType === TradeType.ADVANCED_ORDERS - const isConnectedSwapMode = !!account && isSwapMode + const isConnectedMarketOrderWidget = !!account && isMarketOrderWidget const shouldShowMyOrdersButton = !alternativeOrderModalVisible && - (!isInjectedWidgetMode && isConnectedSwapMode ? isUpToLarge : true) && - (isConnectedSwapMode || !hideOrdersTable) && - ((isConnectedSwapMode && standaloneMode !== true) || - (isLimitOrderMode && isUpToLarge && isLimitOrdersUnlocked) || - (isAdvancedMode && isUpToLarge && isAdvancedOrdersUnlocked)) + (!isInjectedWidgetMode && isConnectedMarketOrderWidget ? isUpToLarge : true) && + (isConnectedMarketOrderWidget || !hideOrdersTable) && + ((isConnectedMarketOrderWidget && standaloneMode !== true) || (!isMarketOrderWidget && isUpToLarge && !lockScreen)) const showDropdown = shouldShowMyOrdersButton || isInjectedWidgetMode @@ -144,13 +130,13 @@ export function TradeWidgetForm(props: TradeWidgetProps) { const toggleAccountModal = useToggleAccountModal() - const handleClick = useCallback(() => { - if (isSwapMode) { + const handleMyOrdersClick = useCallback(() => { + if (isMarketOrderWidget) { toggleAccountModal() } else { scrollToMyOrders() } - }, [isSwapMode, toggleAccountModal]) + }, [isMarketOrderWidget, toggleAccountModal]) return ( <> @@ -162,7 +148,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { )} {shouldShowMyOrdersButton && ( - + My orders )} @@ -184,25 +170,6 @@ export function TradeWidgetForm(props: TradeWidgetProps) { topLabel={isWrapOrUnwrap ? undefined : inputCurrencyInfo.label} {...currencyInputCommonProps} /> - - {isLimitOrderMode && - !isWrapOrUnwrap && - ClosableBanner(ZERO_BANNER_STORAGE_KEY, (onClose) => ( - -

    - NEW: You can now place limit orders for amounts larger than your wallet balance. Partial - fill orders will execute until you run out of sell tokens. Fill-or-kill orders will become active - once you top up your balance. -

    -
    - ))} {!isWrapOrUnwrap && middleContent} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts index d0e0e8e3d8..0176cdf9f6 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts @@ -28,6 +28,7 @@ interface TradeWidgetParams { disablePriceImpact?: boolean hideTradeWarnings?: boolean enableSmartSlippage?: boolean + isMarketOrderWidget?: boolean } export interface TradeWidgetSlots { diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts index 770b0e0db9..41747bbcb5 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts @@ -90,17 +90,18 @@ export const MenuItem = styled.div<{ isActive?: boolean; isDropdownVisible: bool ` export const SelectMenu = styled.div` - display: flex; - flex-flow: column wrap; width: 100%; - height: 100%; + min-height: 100%; position: absolute; z-index: 100; left: 0; top: 0; - gap: ${({ theme }) => (theme.isInjectedWidgetMode ? '16px' : '24px')}; background: var(${UI.COLOR_PAPER}); border-radius: var(${UI.BORDER_RADIUS_NORMAL}); + + > div:first-child { + margin-bottom: ${({ theme }) => (theme.isInjectedWidgetMode ? '16px' : '24px')}; + } ` export const TradeWidgetContent = styled.div` diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx index b034093df9..2406ec013d 100644 --- a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -105,6 +105,7 @@ export function YieldWidget() { const params = { compactView: true, enableSmartSlippage: true, + isMarketOrderWidget: true, recipient, showRecipient, isTradePriceUpdating: isRateLoading, From 661cf2fcffa1b0e329a6df905c5949ee71ee24c7 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 24 Oct 2024 18:14:19 +0500 Subject: [PATCH 054/116] fix: fix trade type selector mobile view (#5023) * fix: fix trade type selector mobile view * refactor: get rid of dependency inversion issue * chore: fix trade menu height * chore: fix condition * chore: fix condition --- .../containers/LimitOrdersWidget/index.tsx | 35 ++++++++++-- .../limitOrdersPropsChecker.ts | 2 + .../swap/containers/SwapWidget/index.tsx | 1 + .../TradeWidget/TradeWidgetForm.tsx | 53 ++++--------------- .../trade/containers/TradeWidget/types.ts | 1 + .../containers/TradeWidgetLinks/styled.ts | 9 ++-- .../yield/containers/YieldWidget/index.tsx | 1 + 7 files changed, 50 insertions(+), 52 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index a4a8ddf4b0..a2420948e3 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -1,14 +1,16 @@ import { useAtomValue } from 'jotai' import React, { useMemo } from 'react' +import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg' import { isSellOrder } from '@cowprotocol/common-utils' +import { BannerOrientation, ClosableBanner, InlineBanner } from '@cowprotocol/ui' import { Field } from 'legacy/state/types' import { LimitOrdersWarnings } from 'modules/limitOrders/containers/LimitOrdersWarnings' import { useLimitOrdersWidgetActions } from 'modules/limitOrders/containers/LimitOrdersWidget/hooks/useLimitOrdersWidgetActions' import { TradeButtons } from 'modules/limitOrders/containers/TradeButtons' -import { TradeWidget, TradeWidgetSlots, useTradePriceImpact } from 'modules/trade' +import { TradeWidget, TradeWidgetSlots, useIsWrapOrUnwrap, useTradePriceImpact } from 'modules/trade' import { useTradeConfirmState } from 'modules/trade' import { BulletListItem, UnlockWidgetScreen } from 'modules/trade/pure/UnlockWidgetScreen' import { useSetTradeQuoteParams, useTradeQuote } from 'modules/tradeQuote' @@ -52,6 +54,8 @@ const UNLOCK_SCREEN = { 'https://medium.com/@cow-protocol/cow-swap-improves-the-limit-order-experience-with-partially-fillable-limit-orders-45f19143e87d', } +const ZERO_BANNER_STORAGE_KEY = 'limitOrdersZeroBalanceBanner:v0' + export function LimitOrdersWidget() { const { inputCurrency, @@ -72,6 +76,7 @@ export function LimitOrdersWidget() { const { isLoading: isRateLoading } = useTradeQuote() const rateInfoParams = useRateInfoParams(inputCurrencyAmount, outputCurrencyAmount) const widgetActions = useLimitOrdersWidgetActions() + const isWrapOrUnwrap = useIsWrapOrUnwrap() const { showRecipient: showRecipientSetting } = settingsState const showRecipient = showRecipientSetting || !!recipient @@ -117,6 +122,7 @@ export function LimitOrdersWidget() { settingsState, feeAmount, widgetActions, + isWrapOrUnwrap, } return @@ -134,6 +140,7 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { rateInfoParams, priceImpact, feeAmount, + isWrapOrUnwrap, } = props const tradeContext = useTradeFlowContext() @@ -170,10 +177,28 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { /> ), middleContent: ( - - - - + <> + {!isWrapOrUnwrap && + ClosableBanner(ZERO_BANNER_STORAGE_KEY, (onClose) => ( + +

    + NEW: You can now place limit orders for amounts larger than your wallet balance. Partial fill + orders will execute until you run out of sell tokens. Fill-or-kill orders will become active once you + top up your balance. +

    +
    + ))} + + + + + ), bottomContent(warnings) { return ( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/limitOrdersPropsChecker.ts b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/limitOrdersPropsChecker.ts index 12402335af..42350f5520 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/limitOrdersPropsChecker.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/limitOrdersPropsChecker.ts @@ -23,6 +23,7 @@ export interface LimitOrdersProps { settingsState: LimitOrdersSettingsState feeAmount: CurrencyAmount | null widgetActions: TradeWidgetActions + isWrapOrUnwrap: boolean } export function limitOrdersPropsChecker(a: LimitOrdersProps, b: LimitOrdersProps): boolean { @@ -34,6 +35,7 @@ export function limitOrdersPropsChecker(a: LimitOrdersProps, b: LimitOrdersProps a.showRecipient === b.showRecipient && a.recipient === b.recipient && a.widgetActions === b.widgetActions && + a.isWrapOrUnwrap === b.isWrapOrUnwrap && checkRateInfoParams(a.rateInfoParams, b.rateInfoParams) && checkPriceImpact(a.priceImpact, b.priceImpact) && genericPropsChecker(a.settingsState, b.settingsState) && 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 36737367e8..dc2c80dc66 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -245,6 +245,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { isEoaEthFlow, compactView: true, enableSmartSlippage: true, + isMarketOrderWidget: true, recipient, showRecipient: showRecipientControls, isTradePriceUpdating, diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index 7af3c59f62..4731a75dc7 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -1,9 +1,8 @@ import React, { useCallback, useMemo } from 'react' import ICON_ORDERS from '@cowprotocol/assets/svg/orders.svg' -import ICON_TOKENS from '@cowprotocol/assets/svg/tokens.svg' import { isInjectedWidget, maxAmountSpend } from '@cowprotocol/common-utils' -import { BannerOrientation, ButtonOutlined, ClosableBanner, InlineBanner, MY_ORDERS_ID } from '@cowprotocol/ui' +import { ButtonOutlined, MY_ORDERS_ID } from '@cowprotocol/ui' import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { t } from '@lingui/macro' @@ -13,9 +12,7 @@ import { AccountElement } from 'legacy/components/Header/AccountElement' import { upToLarge, useMediaQuery } from 'legacy/hooks/useMediaQuery' import { useToggleAccountModal } from 'modules/account' -import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders/hooks/useAdvancedOrdersDerivedState' import { useInjectedWidgetParams } from 'modules/injectedWidget' -import { useIsWidgetUnlocked } from 'modules/limitOrders' import { SetRecipient } from 'modules/swap/containers/SetRecipient' import { useOpenTokenSelectWidget } from 'modules/tokensList' import { useIsAlternativeOrderModalVisible } from 'modules/trade/state/alternativeOrder' @@ -33,14 +30,10 @@ import { TradeWidgetProps } from './types' import { useTradeStateFromUrl } from '../../hooks/setupTradeState/useTradeStateFromUrl' import { useIsWrapOrUnwrap } from '../../hooks/useIsWrapOrUnwrap' -import { useTradeTypeInfo } from '../../hooks/useTradeTypeInfo' -import { TradeType } from '../../types' import { TradeWarnings } from '../TradeWarnings' import { TradeWidgetLinks } from '../TradeWidgetLinks' import { WrapFlowActionButton } from '../WrapFlowActionButton' -const ZERO_BANNER_STORAGE_KEY = 'limitOrdersZeroBalanceBanner:v0' - const scrollToMyOrders = () => { const element = document.getElementById(MY_ORDERS_ID) if (element) { @@ -69,6 +62,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { recipient, hideTradeWarnings, enableSmartSlippage, + isMarketOrderWidget = false, } = params const inputCurrencyInfo = useMemo( @@ -110,23 +104,15 @@ export function TradeWidgetForm(props: TradeWidgetProps) { // Disable too frequent tokens switching const throttledOnSwitchTokens = useThrottleFn(onSwitchTokens, 500) - const tradeTypeInfo = useTradeTypeInfo() - const { isUnlocked: isAdvancedOrdersUnlocked } = useAdvancedOrdersDerivedState() - const isLimitOrdersUnlocked = useIsWidgetUnlocked() const isUpToLarge = useMediaQuery(upToLarge) - const isSwapMode = tradeTypeInfo?.tradeType === TradeType.SWAP - const isLimitOrderMode = tradeTypeInfo?.tradeType === TradeType.LIMIT_ORDER - const isAdvancedMode = tradeTypeInfo?.tradeType === TradeType.ADVANCED_ORDERS - const isConnectedSwapMode = !!account && isSwapMode + const isConnectedMarketOrderWidget = !!account && isMarketOrderWidget const shouldShowMyOrdersButton = !alternativeOrderModalVisible && - (!isInjectedWidgetMode && isConnectedSwapMode ? isUpToLarge : true) && - (isConnectedSwapMode || !hideOrdersTable) && - ((isConnectedSwapMode && standaloneMode !== true) || - (isLimitOrderMode && isUpToLarge && isLimitOrdersUnlocked) || - (isAdvancedMode && isUpToLarge && isAdvancedOrdersUnlocked)) + (!isInjectedWidgetMode && isConnectedMarketOrderWidget ? isUpToLarge : true) && + (isConnectedMarketOrderWidget || !hideOrdersTable) && + ((isConnectedMarketOrderWidget && standaloneMode !== true) || (!isMarketOrderWidget && isUpToLarge && !lockScreen)) const showDropdown = shouldShowMyOrdersButton || isInjectedWidgetMode @@ -144,13 +130,13 @@ export function TradeWidgetForm(props: TradeWidgetProps) { const toggleAccountModal = useToggleAccountModal() - const handleClick = useCallback(() => { - if (isSwapMode) { + const handleMyOrdersClick = useCallback(() => { + if (isMarketOrderWidget) { toggleAccountModal() } else { scrollToMyOrders() } - }, [isSwapMode, toggleAccountModal]) + }, [isMarketOrderWidget, toggleAccountModal]) return ( <> @@ -162,7 +148,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { )} {shouldShowMyOrdersButton && ( - + My orders )} @@ -184,25 +170,6 @@ export function TradeWidgetForm(props: TradeWidgetProps) { topLabel={isWrapOrUnwrap ? undefined : inputCurrencyInfo.label} {...currencyInputCommonProps} /> - - {isLimitOrderMode && - !isWrapOrUnwrap && - ClosableBanner(ZERO_BANNER_STORAGE_KEY, (onClose) => ( - -

    - NEW: You can now place limit orders for amounts larger than your wallet balance. Partial - fill orders will execute until you run out of sell tokens. Fill-or-kill orders will become active - once you top up your balance. -

    -
    - ))} {!isWrapOrUnwrap && middleContent} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts index d0e0e8e3d8..0176cdf9f6 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts @@ -28,6 +28,7 @@ interface TradeWidgetParams { disablePriceImpact?: boolean hideTradeWarnings?: boolean enableSmartSlippage?: boolean + isMarketOrderWidget?: boolean } export interface TradeWidgetSlots { diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts index 770b0e0db9..41747bbcb5 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts @@ -90,17 +90,18 @@ export const MenuItem = styled.div<{ isActive?: boolean; isDropdownVisible: bool ` export const SelectMenu = styled.div` - display: flex; - flex-flow: column wrap; width: 100%; - height: 100%; + min-height: 100%; position: absolute; z-index: 100; left: 0; top: 0; - gap: ${({ theme }) => (theme.isInjectedWidgetMode ? '16px' : '24px')}; background: var(${UI.COLOR_PAPER}); border-radius: var(${UI.BORDER_RADIUS_NORMAL}); + + > div:first-child { + margin-bottom: ${({ theme }) => (theme.isInjectedWidgetMode ? '16px' : '24px')}; + } ` export const TradeWidgetContent = styled.div` diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx index b034093df9..2406ec013d 100644 --- a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -105,6 +105,7 @@ export function YieldWidget() { const params = { compactView: true, enableSmartSlippage: true, + isMarketOrderWidget: true, recipient, showRecipient, isTradePriceUpdating: isRateLoading, From 09f512407f8a37d49ccd422e951da20e6733afc4 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 24 Oct 2024 20:26:18 +0500 Subject: [PATCH 055/116] fix(swap): fix safe eth-flow (#5041) --- .../services/safeBundleFlow/safeBundleEthFlow.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 0b6fe9277b..12d0bde498 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts @@ -1,11 +1,13 @@ import { Erc20 } from '@cowprotocol/abis' +import { WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' import { UiOrderType } from '@cowprotocol/types' import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { Percent } from '@uniswap/sdk-core' import { PriceImpact } from 'legacy/hooks/usePriceImpact' import { partialOrderUpdate } from 'legacy/state/orders/utils' -import { signAndPostOrder } from 'legacy/utils/trade' +import { type PostOrderParams, signAndPostOrder } from 'legacy/utils/trade' import { removePermitHookFromAppData } from 'modules/appData' import { buildApproveTx } from 'modules/operations/bundle/buildApproveTx' @@ -33,13 +35,19 @@ export async function safeBundleEthFlow( return false } - const { context, callbacks, orderParams, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext + const { context, callbacks, swapFlowAnalyticsContext, tradeConfirmActions, typedHooks } = tradeContext const { spender, settlementContract, safeAppsSdk, needsApproval, wrappedNativeContract } = safeBundleContext - const { account, recipientAddressOrName, kind } = orderParams const { inputAmountWithSlippage, chainId, inputAmount, outputAmount } = context + const orderParams: PostOrderParams = { + ...tradeContext.orderParams, + sellToken: WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId], + } + + const { account, recipientAddressOrName, kind } = orderParams + tradeFlowAnalytics.wrapApproveAndPresign(swapFlowAnalyticsContext) const nativeAmountInWei = inputAmountWithSlippage.quotient.toString() const tradeAmounts = { inputAmount, outputAmount } From 229f243bd834da7d962c64bf151b5cf5db644259 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 25 Oct 2024 17:32:17 +0500 Subject: [PATCH 056/116] fix(trade): use recipient address in order data (#5040) * fix(trade): use recipient address in order data * chore: fix recipient --- .../src/modules/swap/state/useSwapDerivedState.ts | 12 +++++------- .../modules/tradeFlow/hooks/useTradeFlowContext.ts | 4 +++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts b/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts index 45b0f95097..bc9ecc5104 100644 --- a/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts +++ b/apps/cowswap-frontend/src/modules/swap/state/useSwapDerivedState.ts @@ -1,7 +1,8 @@ -import { useAtomValue, useSetAtom } from 'jotai' +import { useSetAtom } from 'jotai' import { useEffect } from 'react' import { OrderKind } from '@cowprotocol/cow-sdk' +import { useENSAddress } from '@cowprotocol/ens' import { Field } from 'legacy/state/types' @@ -11,16 +12,13 @@ import { useTradeUsdAmounts } from 'modules/usdAmount' import { useSafeMemoObject } from 'common/hooks/useSafeMemo' -import { SwapDerivedState, swapDerivedStateAtom } from './swapDerivedStateAtom' +import { swapDerivedStateAtom } from './swapDerivedStateAtom' import { useDerivedSwapInfo, useSwapState } from '../hooks/useSwapState' -export function useSwapDerivedState(): SwapDerivedState { - return useAtomValue(swapDerivedStateAtom) -} - export function useFillSwapDerivedState() { - const { independentField, recipient, recipientAddress } = useSwapState() + const { independentField, recipient } = useSwapState() + const { address: recipientAddress } = useENSAddress(recipient) const { trade, currencyBalances, currencies, slippageAdjustedSellAmount, slippageAdjustedBuyAmount, parsedAmount } = useDerivedSwapInfo() const slippage = useTradeSlippage() diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts index f3580a1a9c..cc0b74bfef 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -109,6 +109,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon permitInfo, provider, recipient, + recipientAddress, sellAmountBeforeFee, sellToken, settlementContract, @@ -139,6 +140,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon permitInfo, provider, recipient, + recipientAddress, sellAmountBeforeFee, sellToken, settlementContract, @@ -191,7 +193,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon quoteValidTo: quoteResponse.quote.validTo, localQuoteTimestamp, }), - recipient: recipient || account, + recipient: recipientAddress || recipient || account, recipientAddressOrName: recipient || null, allowsOffchainSigning, appData, From c9e9987f4bf7f876e5c8431703ed7fa7486905be Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Mon, 28 Oct 2024 16:11:16 +0500 Subject: [PATCH 057/116] chore: add order kind to HookDappOrderParams --- .../modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts | 1 + libs/hook-dapp-lib/src/types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts index 10c9422649..da859d2380 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts @@ -16,6 +16,7 @@ export function useSetupHooksStoreOrderParams() { setOrderParams(null) } else { setOrderParams({ + kind: orderParams.kind, validTo: orderParams.validTo, sellAmount: orderParams.inputAmount.quotient.toString(), buyAmount: orderParams.outputAmount.quotient.toString(), diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index 57c2c2d65d..a1f7abf594 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -41,6 +41,7 @@ export interface CoWHookDappActions { } export interface HookDappOrderParams { + kind: 'buy' | 'sell' validTo: number sellTokenAddress: string buyTokenAddress: string From a062ff5309c89fa3d1cddb56bc85a0a0badf0ca5 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 28 Oct 2024 18:58:46 +0500 Subject: [PATCH 058/116] fix(swap): reset widget start after successful swap (#5047) --- .../swap/containers/SwapWidget/index.tsx | 13 ++-- .../containers/SwapWidget/propsChecker.ts | 44 ------------- .../swap/containers/SwapWidget/types.ts | 35 ---------- .../swap/hooks/useHandleSwapOrEthFlow.ts | 31 +++++++-- .../swap/hooks/useSwapButtonContext.ts | 6 +- .../src/modules/swap/hooks/useSwapState.tsx | 11 +--- .../modules/swap/services/ethFlow/index.ts | 4 +- .../modules/tradeFlow/hooks/useHandleSwap.ts | 64 +++++++++++++------ .../safeBundleFlow/safeBundleApprovalFlow.ts | 4 +- .../safeBundleFlow/safeBundleEthFlow.ts | 4 +- .../tradeFlow/services/swapFlow/index.ts | 4 +- .../yield/containers/YieldWidget/index.tsx | 2 +- 12 files changed, 95 insertions(+), 127 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/propsChecker.ts delete mode 100644 apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/types.ts 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 dc2c80dc66..d5c0f61f5c 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -162,11 +162,14 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const openNativeWrapModal = useCallback(() => setOpenNativeWrapModal(true), []) const dismissNativeWrapModal = useCallback(() => setOpenNativeWrapModal(false), []) - const swapButtonContext = useSwapButtonContext({ - feeWarningAccepted, - impactWarningAccepted, - openNativeWrapModal, - }) + const swapButtonContext = useSwapButtonContext( + { + feeWarningAccepted, + impactWarningAccepted, + openNativeWrapModal, + }, + swapActions, + ) const tradeUrlParams = useTradeRouteContext() diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/propsChecker.ts b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/propsChecker.ts deleted file mode 100644 index 53b68a4466..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/propsChecker.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { genericPropsChecker } from '@cowprotocol/common-utils' -import { areFractionsEqual } from '@cowprotocol/common-utils' - -import { PriceImpact } from 'legacy/hooks/usePriceImpact' - -import { SwapFormProps } from 'modules/swap/containers/SwapWidget/types' - -import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' - -function isCurrencyInfoEqual(prev: CurrencyInfo, next: CurrencyInfo): boolean { - const isCurrencyEqual = - prev.currency && next.currency ? prev.currency.equals(next.currency) : prev.currency === next.currency - const isBalanceEqual = areFractionsEqual(prev.balance, next.balance) - const isFiatAmountEqual = areFractionsEqual(prev.fiatAmount, next.fiatAmount) - const isAmountEqual = areFractionsEqual(prev.amount, next.amount) - const isIsIndependentEqual = prev.isIndependent === next.isIndependent - - return ( - isCurrencyEqual && - isBalanceEqual && - isFiatAmountEqual && - isAmountEqual && - isIsIndependentEqual && - prev.receiveAmountInfo === next.receiveAmountInfo - ) -} - -function isPriceImpactEqual(prev: PriceImpact, next: PriceImpact): boolean { - return prev.loading === next.loading && areFractionsEqual(prev.priceImpact, next.priceImpact) -} - -export function swapPagePropsChecker(prev: SwapFormProps, next: SwapFormProps): boolean { - return ( - prev.allowedSlippage.equalTo(next.allowedSlippage) && - prev.showRecipientControls === next.showRecipientControls && - prev.recipient === next.recipient && - prev.isTradePriceUpdating === next.isTradePriceUpdating && - prev.allowsOffchainSigning === next.allowsOffchainSigning && - genericPropsChecker(prev.subsidyAndBalance, next.subsidyAndBalance) && - isCurrencyInfoEqual(prev.inputCurrencyInfo, next.inputCurrencyInfo) && - isCurrencyInfoEqual(prev.outputCurrencyInfo, next.outputCurrencyInfo) && - isPriceImpactEqual(prev.priceImpactParams, next.priceImpactParams) - ) -} diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/types.ts b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/types.ts deleted file mode 100644 index b610f65bd1..0000000000 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' - -import { BalanceAndSubsidy } from 'legacy/hooks/useCowBalanceAndSubsidy' -import { PriceImpact } from 'legacy/hooks/usePriceImpact' - -import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' - -import { SwapActions } from '../../hooks/useSwapState' - -export interface SwapFormProps { - chainId: number | undefined - recipient: string | null - inputCurrencyInfo: CurrencyInfo - outputCurrencyInfo: CurrencyInfo - allowedSlippage: Percent - isTradePriceUpdating: boolean - allowsOffchainSigning: boolean - showRecipientControls: boolean - subsidyAndBalance: BalanceAndSubsidy - priceImpactParams: PriceImpact - swapActions: SwapActions -} - -export interface TradeStateFromUrl { - inputCurrency: string | null - outputCurrency: string | null - amount: string | null - independentField: string | null - recipient: string | null -} - -export interface CurrenciesBalances { - INPUT: CurrencyAmount | null - OUTPUT: CurrencyAmount | null -} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts index 10ecb4104b..f550702e03 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useHandleSwapOrEthFlow.ts @@ -1,8 +1,9 @@ import { useCallback } from 'react' +import { Field } from 'legacy/state/types' import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { useTradePriceImpact } from 'modules/trade' +import { TradeWidgetActions, useTradePriceImpact } from 'modules/trade' import { logTradeFlow } from 'modules/trade/utils/logger' import { useHandleSwap, useTradeFlowType } from 'modules/tradeFlow' import { FlowType } from 'modules/tradeFlow' @@ -15,28 +16,46 @@ import { useSwapFlowContext } from './useSwapFlowContext' import { ethFlow } from '../services/ethFlow' -export function useHandleSwapOrEthFlow() { +export function useHandleSwapOrEthFlow(actions: TradeWidgetActions) { const priceImpactParams = useTradePriceImpact() const swapFlowContext = useSwapFlowContext() const ethFlowContext = useEthFlowContext() const tradeFlowType = useTradeFlowType() const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() + const { onUserInput, onChangeRecipient } = actions const [deadline] = useUserTransactionTTL() - const { callback: handleSwap, contextIsReady } = useHandleSwap(useSafeMemoObject({ deadline })) + const { callback: handleSwap, contextIsReady } = useHandleSwap(useSafeMemoObject({ deadline }), actions) - const callback = useCallback(() => { + const callback = useCallback(async () => { if (!swapFlowContext) return if (tradeFlowType === FlowType.EOA_ETH_FLOW) { if (!ethFlowContext) throw new Error('Eth flow context is not ready') logTradeFlow('ETH FLOW', 'Start eth flow') - return ethFlow(swapFlowContext, ethFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + const result = await ethFlow(swapFlowContext, ethFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + + // Clean up form fields after successful swap + if (result === true) { + onChangeRecipient(null) + onUserInput(Field.INPUT, '') + } + + return result } return handleSwap() - }, [swapFlowContext, ethFlowContext, handleSwap, tradeFlowType, priceImpactParams, confirmPriceImpactWithoutFee]) + }, [ + swapFlowContext, + ethFlowContext, + handleSwap, + tradeFlowType, + priceImpactParams, + confirmPriceImpactWithoutFee, + onUserInput, + onChangeRecipient, + ]) return { callback, contextIsReady } } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index 5efc07fdac..933a0b6c14 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -20,7 +20,7 @@ import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useTokenSupportsPermit } from 'modules/permit' import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState' import { SwapButtonsContext } from 'modules/swap/pure/SwapButtons' -import { TradeType, useTradeConfirmActions, useWrapNativeFlow } from 'modules/trade' +import { TradeType, TradeWidgetActions, useTradeConfirmActions, useWrapNativeFlow } from 'modules/trade' import { useIsNativeIn } from 'modules/trade/hooks/useIsNativeInOrOut' import { useIsWrappedOut } from 'modules/trade/hooks/useIsWrappedInOrOut' import { useWrappedToken } from 'modules/trade/hooks/useWrappedToken' @@ -38,7 +38,7 @@ export interface SwapButtonInput { openNativeWrapModal(): void } -export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext { +export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidgetActions): SwapButtonsContext { const { feeWarningAccepted, impactWarningAccepted, openNativeWrapModal } = input const { account, chainId } = useWalletInfo() @@ -76,7 +76,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext const wrapCallback = useWrapNativeFlow() const { state: approvalState } = useApproveState(slippageAdjustedSellAmount || null) - const { callback: handleSwap, contextIsReady } = useHandleSwapOrEthFlow() + const { callback: handleSwap, contextIsReady } = useHandleSwapOrEthFlow(actions) const swapCallbackError = contextIsReady ? null : 'Missing dependencies' diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index 5710c36dd7..4953671631 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -4,7 +4,6 @@ import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { formatSymbol, getIsNativeToken, isAddress, tryParseCurrencyAmount } from '@cowprotocol/common-utils' import { useENS } from '@cowprotocol/ens' import { useAreThereTokensWithSameSymbol, useTokenBySymbolOrAddress } from '@cowprotocol/tokens' -import { Command } from '@cowprotocol/types' import { useWalletInfo } from '@cowprotocol/wallet' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' @@ -20,6 +19,7 @@ import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { changeSwapAmountAnalytics, switchTokensAnalytics } from 'modules/analytics' +import type { TradeWidgetActions } from 'modules/trade' import { useNavigateOnCurrencySelection } from 'modules/trade/hooks/useNavigateOnCurrencySelection' import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate' import { useTradeSlippage } from 'modules/tradeSlippage' @@ -48,13 +48,6 @@ export function useSwapState(): AppState['swap'] { export type Currencies = { [field in Field]?: Currency | null } -export interface SwapActions { - onCurrencySelection: (field: Field, currency: Currency) => void - onSwitchTokens: Command - onUserInput: (field: Field, typedValue: string) => void - onChangeRecipient: (recipient: string | null) => void -} - interface DerivedSwapInfo { currencies: Currencies currenciesIds: { [field in Field]?: string | null } @@ -67,7 +60,7 @@ interface DerivedSwapInfo { trade: TradeGp | undefined } -export function useSwapActionHandlers(): SwapActions { +export function useSwapActionHandlers(): TradeWidgetActions { const { chainId } = useWalletInfo() const dispatch = useAppDispatch() const onCurrencySelection = useNavigateOnCurrencySelection() diff --git a/apps/cowswap-frontend/src/modules/swap/services/ethFlow/index.ts b/apps/cowswap-frontend/src/modules/swap/services/ethFlow/index.ts index af590d611f..9898f1353d 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/ethFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/ethFlow/index.ts @@ -22,7 +22,7 @@ export async function ethFlow( ethFlowContext: EthFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, -): Promise { +): Promise { const { tradeConfirmActions, swapFlowAnalyticsContext, @@ -112,6 +112,8 @@ export async function ethFlow( logTradeFlow('ETH FLOW', 'STEP 7: show UI of the successfully sent transaction', orderId) tradeConfirmActions.onSuccess(orderId) tradeFlowAnalytics.sign(swapFlowAnalyticsContext) + + return true } catch (error: any) { logTradeFlow('ETH FLOW', 'STEP 8: ERROR: ', error) const swapErrorMessage = getSwapErrorMessage(error) diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts index 7feb7896b8..169108d890 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useHandleSwap.ts @@ -1,6 +1,8 @@ import { useCallback } from 'react' -import { useTradePriceImpact } from 'modules/trade' +import { Field } from 'legacy/state/types' + +import { TradeWidgetActions, useTradePriceImpact } from 'modules/trade' import { logTradeFlow } from 'modules/trade/utils/logger' import { useConfirmPriceImpactWithoutFee } from 'common/hooks/useConfirmPriceImpactWithoutFee' @@ -13,12 +15,13 @@ import { safeBundleApprovalFlow, safeBundleEthFlow } from '../services/safeBundl import { swapFlow } from '../services/swapFlow' import { FlowType } from '../types/TradeFlowContext' -export function useHandleSwap(params: TradeFlowParams) { +export function useHandleSwap(params: TradeFlowParams, actions: TradeWidgetActions) { const tradeFlowType = useTradeFlowType() const tradeFlowContext = useTradeFlowContext(params) const safeBundleFlowContext = useSafeBundleFlowContext() const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() const priceImpactParams = useTradePriceImpact() + const { onUserInput, onChangeRecipient } = actions const contextIsReady = Boolean( @@ -30,27 +33,48 @@ export function useHandleSwap(params: TradeFlowParams) { const callback = useCallback(async () => { if (!tradeFlowContext) return - if (tradeFlowType === FlowType.SAFE_BUNDLE_APPROVAL) { - if (!safeBundleFlowContext) throw new Error('Safe bundle flow context is not ready') + const result = await (() => { + if (tradeFlowType === FlowType.SAFE_BUNDLE_APPROVAL) { + if (!safeBundleFlowContext) throw new Error('Safe bundle flow context is not ready') - logTradeFlow('SAFE BUNDLE APPROVAL FLOW', 'Start safe bundle approval flow') - return safeBundleApprovalFlow( - tradeFlowContext, - safeBundleFlowContext, - priceImpactParams, - confirmPriceImpactWithoutFee, - ) - } - if (tradeFlowType === FlowType.SAFE_BUNDLE_ETH) { - if (!safeBundleFlowContext) throw new Error('Safe bundle flow context is not ready') + logTradeFlow('SAFE BUNDLE APPROVAL FLOW', 'Start safe bundle approval flow') + return safeBundleApprovalFlow( + tradeFlowContext, + safeBundleFlowContext, + priceImpactParams, + confirmPriceImpactWithoutFee, + ) + } + if (tradeFlowType === FlowType.SAFE_BUNDLE_ETH) { + if (!safeBundleFlowContext) throw new Error('Safe bundle flow context is not ready') - logTradeFlow('SAFE BUNDLE ETH FLOW', 'Start safe bundle eth flow') - return safeBundleEthFlow(tradeFlowContext, safeBundleFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - } + logTradeFlow('SAFE BUNDLE ETH FLOW', 'Start safe bundle eth flow') + return safeBundleEthFlow( + tradeFlowContext, + safeBundleFlowContext, + priceImpactParams, + confirmPriceImpactWithoutFee, + ) + } - logTradeFlow('SWAP FLOW', 'Start swap flow') - return swapFlow(tradeFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) - }, [tradeFlowType, tradeFlowContext, safeBundleFlowContext, priceImpactParams, confirmPriceImpactWithoutFee]) + logTradeFlow('SWAP FLOW', 'Start swap flow') + return swapFlow(tradeFlowContext, priceImpactParams, confirmPriceImpactWithoutFee) + })() + + // Clean up form fields after successful swap + if (result === true) { + onChangeRecipient(null) + onUserInput(Field.INPUT, '') + } + }, [ + tradeFlowType, + tradeFlowContext, + safeBundleFlowContext, + priceImpactParams, + confirmPriceImpactWithoutFee, + onChangeRecipient, + onUserInput, + ]) return { callback, contextIsReady } } 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 dd64fcdbc0..cae474c566 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleApprovalFlow.ts @@ -26,7 +26,7 @@ export async function safeBundleApprovalFlow( safeBundleContext: SafeBundleFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, -): Promise { +): Promise { logTradeFlow(LOG_PREFIX, 'STEP 1: confirm price impact') if (priceImpactParams?.priceImpact && !(await confirmPriceImpactWithoutFee(priceImpactParams.priceImpact))) { @@ -135,6 +135,8 @@ export async function safeBundleApprovalFlow( logTradeFlow(LOG_PREFIX, 'STEP 7: show UI of the successfully sent transaction') tradeConfirmActions.onSuccess(orderId) + + return true } catch (error) { logTradeFlow(LOG_PREFIX, 'STEP 8: error', error) const swapErrorMessage = getSwapErrorMessage(error) 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 12d0bde498..a718959824 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts @@ -28,7 +28,7 @@ export async function safeBundleEthFlow( safeBundleContext: SafeBundleFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, -): Promise { +): Promise { logTradeFlow(LOG_PREFIX, 'STEP 1: confirm price impact') if (priceImpactParams?.priceImpact && !(await confirmPriceImpactWithoutFee(priceImpactParams.priceImpact))) { @@ -149,6 +149,8 @@ export async function safeBundleEthFlow( logTradeFlow(LOG_PREFIX, 'STEP 8: show UI of the successfully sent transaction') tradeConfirmActions.onSuccess(orderId) + + return true } catch (error) { logTradeFlow(LOG_PREFIX, 'STEP 9: error', error) const swapErrorMessage = getSwapErrorMessage(error) 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 145e84f826..d26e22b9d4 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts @@ -22,7 +22,7 @@ export async function swapFlow( input: TradeFlowContext, priceImpactParams: PriceImpact, confirmPriceImpactWithoutFee: (priceImpact: Percent) => Promise, -): Promise { +): Promise { const { tradeConfirmActions, callbacks: { getCachedPermit }, @@ -123,6 +123,8 @@ export async function swapFlow( logTradeFlow('SWAP FLOW', 'STEP 7: show UI of the successfully sent transaction', orderUid) tradeConfirmActions.onSuccess(orderUid) tradeFlowAnalytics.sign(swapFlowAnalyticsContext) + + return true } catch (error: any) { logTradeFlow('SWAP FLOW', 'STEP 8: ERROR: ', error) const swapErrorMessage = getSwapErrorMessage(error) diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx index 2406ec013d..61dbde911c 100644 --- a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -45,7 +45,7 @@ export function YieldWidget() { outputCurrencyFiatAmount, recipient, } = useYieldDerivedState() - const doTrade = useHandleSwap(useSafeMemoObject({ deadline: deadlineState[0] })) + const doTrade = useHandleSwap(useSafeMemoObject({ deadline: deadlineState[0] }), widgetActions) const inputCurrencyInfo: CurrencyInfo = { field: Field.INPUT, From 562868401aac8b0676d88f9a367ae02b13f44386 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari <55461956+yvesfracari@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:21:47 -0300 Subject: [PATCH 059/116] feat(hooks-store): add cow amm withdraw iframe dapp (#4947) --- .../src/modules/hooksStore/hookRegistry.tsx | 1 + libs/hook-dapp-lib/src/hookDappsRegistry.json | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index 73e31b0da9..423e66c8da 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -23,5 +23,6 @@ export const ALL_HOOK_DAPPS = [ ...hookDappsRegistry.CLAIM_COW_AIRDROP, component: (props) => , }, + hookDappsRegistry.COW_AMM_WITHDRAW, hookDappsRegistry.CLAIM_LLAMAPAY_VESTING, ] as HookDapp[] diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json index 976c65095f..c3e72565ce 100644 --- a/libs/hook-dapp-lib/src/hookDappsRegistry.json +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -50,6 +50,22 @@ "supportedNetworks": [11155111] } }, + "COW_AMM_WITHDRAW": { + "id": "5dc71fa5829d976c462bdf37b38b6fd9bbc289252a5a18e61525f8c8a3c775df", + "name": "CoW AMM Withdraw Liquidity", + "type": "IFRAME", + "descriptionShort": "Remove liquidity from a CoW AMM pool before the swap", + "description": "Reduce or withdraw liquidity from a pool before a token swap integrating the process directly into the transaction flow. By adjusting your liquidity ahead of time, you gain more control over your assets without any extra steps. Optimize your position in a pool, all in one seamless action — no need for multiple transactions or added complexity.", + "version": "0.0.1", + "website": "https://balancer.fi/pools/cow", + "image": "https://cow-hooks-dapps-withdraw-pool.vercel.app/icon.png", + "url": "https://cow-hooks-dapps-withdraw-pool.vercel.app/", + "conditions": { + "position": "pre", + "walletCompatibility": ["EOA"], + "smartContractWalletSupported": false + } + }, "CLAIM_LLAMAPAY_VESTING": { "id": "5d2c081d11a01ca0b76e2fafbc0d3c62a4c9945ce404706fb1e49e826c0f99eb", "type": "IFRAME", From 8cfbd238da2c7e9d461ef5922b7304568bb64ecc Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:23:15 +0000 Subject: [PATCH 060/116] feat(hooks-store): prevent adding unsupported hooks for your wallet (#5020) * feat: prevent adding unsupported hooks for your wallet * feat: make walletType optional * feat: address comments * chore: fix code style --------- Co-authored-by: Alexandr Kazachenko --- .../containers/HookRegistryList/index.tsx | 232 ++++++++---------- .../containers/HooksStoreWidget/index.tsx | 14 +- .../CustomDappLoader/index.tsx | 10 +- .../pure/AddCustomHookForm/index.tsx | 11 +- .../hooksStore/pure/HookDappDetails/index.tsx | 23 +- .../pure/HookDetailHeader/index.tsx | 16 +- .../pure/HookDetailHeader/styled.ts | 10 +- .../hooksStore/pure/HookListItem/index.tsx | 21 +- .../hooksStore/pure/HookListItem/styled.tsx | 26 +- .../src/modules/hooksStore/utils.ts | 9 +- 10 files changed, 205 insertions(+), 167 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx index 8953a4b5ce..38378027bc 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' +import { HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { Command } from '@cowprotocol/types' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' -import { useIsSmartContractWallet } from '@cowprotocol/wallet' import { NewModal } from 'common/pure/NewModal' @@ -20,7 +20,7 @@ import { HookDetailHeader } from '../../pure/HookDetailHeader' import { HookListItem } from '../../pure/HookListItem' import { HookListsTabs } from '../../pure/HookListsTabs' import { HookDapp, HookDappIframe } from '../../types/hooks' -import { findHookDappById, isHookDappIframe } from '../../utils' +import { findHookDappById, isHookCompatible, isHookDappIframe } from '../../utils' import { HookDappContainer } from '../HookDappContainer' import { HookSearchInput } from '../HookSearchInput' @@ -28,67 +28,51 @@ interface HookStoreModal { onDismiss: Command isPreHook: boolean hookToEdit?: string + walletType: HookDappWalletCompatibility } -export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStoreModal) { +export function HookRegistryList({ onDismiss, isPreHook, hookToEdit, walletType }: HookStoreModal) { const [selectedDapp, setSelectedDapp] = useState(null) const [dappDetails, setDappDetails] = useState(null) - const [isAllHooksTab, setIsAllHooksTab] = useState(true) - - const isSmartContractWallet = useIsSmartContractWallet() + const [searchQuery, setSearchQuery] = useState('') const addCustomHookDapp = useAddCustomHookDapp(isPreHook) const removeCustomHookDapp = useRemoveCustomHookDapp() const customHookDapps = useCustomHookDapps(isPreHook) const hookToEditDetails = useHookById(hookToEdit, isPreHook) - - // State for Search Input - const [searchQuery, setSearchQuery] = useState('') - - // Clear search input handler - const handleClearSearch = useCallback(() => { - setSearchQuery('') - }, []) - const internalHookDapps = useInternalHookDapps(isPreHook) - const currentDapps = useMemo(() => { - return isAllHooksTab ? internalHookDapps.concat(customHookDapps) : customHookDapps - }, [isAllHooksTab, internalHookDapps, customHookDapps]) + const currentDapps = useMemo( + () => (isAllHooksTab ? [...internalHookDapps, ...customHookDapps] : customHookDapps), + [isAllHooksTab, internalHookDapps, customHookDapps], + ) - // Compute filteredDapps based on searchQuery const filteredDapps = useMemo(() => { if (!searchQuery) return currentDapps - const lowerQuery = searchQuery.toLowerCase() + return currentDapps.filter(({ name = '', descriptionShort = '' }) => + [name, descriptionShort].some((text) => text.toLowerCase().includes(lowerQuery)), + ) + }, [currentDapps, searchQuery]) - return currentDapps.filter((dapp) => { - const name = dapp.name?.toLowerCase() || '' - const description = dapp.descriptionShort?.toLowerCase() || '' - - return name.includes(lowerQuery) || description.includes(lowerQuery) + const sortedFilteredDapps = useMemo(() => { + return filteredDapps.sort((a, b) => { + const isCompatibleA = isHookCompatible(a, walletType) + const isCompatibleB = isHookCompatible(b, walletType) + return isCompatibleA === isCompatibleB ? 0 : isCompatibleA ? -1 : 1 }) - }, [currentDapps, searchQuery]) + }, [filteredDapps, walletType]) const customHooksCount = customHookDapps.length const allHooksCount = internalHookDapps.length + customHooksCount - // Compute title based on selected dapp or details - const title = useMemo(() => { - if (selectedDapp) return selectedDapp.name - if (dappDetails) return 'Hook description' - return 'Hook Store' - }, [selectedDapp, dappDetails]) + const title = selectedDapp?.name || (dappDetails ? 'Hook description' : 'Hook Store') - // Handle modal dismiss const onDismissModal = useCallback(() => { if (hookToEdit) { setSelectedDapp(null) onDismiss() - return - } - - if (dappDetails) { + } else if (dappDetails) { setDappDetails(null) } else if (selectedDapp) { setSelectedDapp(null) @@ -97,78 +81,80 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore } }, [onDismiss, selectedDapp, dappDetails, hookToEdit]) - // Handle hookToEditDetails useEffect(() => { - if (!hookToEditDetails) { - setSelectedDapp(null) + if (hookToEditDetails) { + const foundDapp = findHookDappById(currentDapps, hookToEditDetails) + setSelectedDapp(foundDapp || null) } else { - setSelectedDapp(findHookDappById(currentDapps, hookToEditDetails) || null) + setSelectedDapp(null) } }, [hookToEditDetails, currentDapps]) - // Reset dappDetails when tab changes useEffect(() => { setDappDetails(null) }, [isAllHooksTab]) - // Handle add custom hook button - const handleAddCustomHook = useCallback(() => { - setIsAllHooksTab(false) - }, [setIsAllHooksTab]) + const handleAddCustomHook = () => setIsAllHooksTab(false) + const handleClearSearch = () => setSearchQuery('') + + const emptyListMessage = useMemo( + () => + isAllHooksTab + ? searchQuery + ? 'No hooks match your search.' + : 'No hooks available.' + : "You haven't added any custom hooks yet. Add a custom hook to get started.", + [isAllHooksTab, searchQuery], + ) - // Determine the message for EmptyList based on the active tab and search query - const emptyListMessage = useMemo(() => { - if (isAllHooksTab) { - return searchQuery ? 'No hooks match your search.' : 'No hooks available.' - } else { - return "You haven't added any custom hooks yet. Add a custom hook to get started." - } - }, [isAllHooksTab, searchQuery]) - - const DappsListContent = ( - <> - {isAllHooksTab && ( - -

    - Can't find a hook that you like?{' '} - - Add a custom hook - -

    -
    - )} - - setSearchQuery(e.target.value?.trim())} - placeholder="Search hooks by title or description" - ariaLabel="Search hooks" - onClear={handleClearSearch} - /> - - {filteredDapps.length > 0 ? ( - - {filteredDapps.map((dapp) => ( - removeCustomHookDapp(dapp as HookDappIframe)} - onSelect={() => setSelectedDapp(dapp)} - onOpenDetails={() => setDappDetails(dapp)} - /> - ))} - - ) : ( - {emptyListMessage} - )} - + const DappsListContent = useMemo( + () => ( + <> + {isAllHooksTab && ( + +

    + Can't find a hook that you like?{' '} + + Add a custom hook + +

    +
    + )} + + setSearchQuery(e.target.value?.trim())} + placeholder="Search hooks by title or description" + ariaLabel="Search hooks" + onClear={handleClearSearch} + /> + + {sortedFilteredDapps.length > 0 ? ( + + {sortedFilteredDapps.map((dapp) => ( + removeCustomHookDapp(dapp as HookDappIframe) : undefined} + onSelect={() => setSelectedDapp(dapp)} + onOpenDetails={() => setDappDetails(dapp)} + /> + ))} + + ) : ( + {emptyListMessage} + )} + + ), + [isAllHooksTab, searchQuery, sortedFilteredDapps, handleAddCustomHook, handleClearSearch], ) return ( @@ -189,37 +175,25 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore onAddCustomHook={handleAddCustomHook} /> )} - {(() => { - if (selectedDapp) { - return ( - <> - - - - ) - } - - if (dappDetails) { - return setSelectedDapp(dappDetails)} /> - } - - return isAllHooksTab ? ( - DappsListContent - ) : ( - + + - {DappsListContent} - - ) - })()} + onDismiss={onDismiss} + dapp={selectedDapp} + hookToEdit={hookToEdit} + /> + + ) : dappDetails ? ( + setSelectedDapp(dappDetails)} walletType={walletType} /> + ) : isAllHooksTab ? ( + DappsListContent + ) : ( + + {DappsListContent} + + )}
    ) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index 18999dd0ba..ef0ad58b5d 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -1,8 +1,9 @@ import { useCallback, useEffect, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' +import { HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' -import { useWalletInfo } from '@cowprotocol/wallet' +import { useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet' import { SwapWidget } from 'modules/swap' import { useIsSellNative } from 'modules/trade' @@ -32,6 +33,10 @@ export function HooksStoreWidget() { const isNativeSell = useIsSellNative() const isChainIdUnsupported = useIsProviderNetworkUnsupported() + const walletType = useIsSmartContractWallet() + ? HookDappWalletCompatibility.SMART_CONTRACT + : HookDappWalletCompatibility.EOA + const onDismiss = useCallback(() => { setSelectedHookPosition(null) setHookToEdit(undefined) @@ -106,7 +111,12 @@ export function HooksStoreWidget() { {isHookSelectionOpen && ( - + )} {isRescueWidgetOpen && setRescueWidgetOpen(false)} />} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx index c16a3847be..09cecb924b 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx @@ -1,6 +1,6 @@ import { Dispatch, SetStateAction, useEffect } from 'react' -import { HookDappBase, HookDappType } from '@cowprotocol/hook-dapp-lib' +import { HookDappBase, HookDappType, HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { useWalletInfo } from '@cowprotocol/wallet' import { HookDappIframe } from '../../../types/hooks' @@ -9,7 +9,7 @@ import { validateHookDappManifest } from '../../../validateHookDappManifest' interface ExternalDappLoaderProps { input: string isPreHook: boolean - isSmartContractWallet: boolean | undefined + walletType: HookDappWalletCompatibility setDappInfo: Dispatch> setLoading: Dispatch> setManifestError: Dispatch> @@ -20,7 +20,7 @@ export function ExternalDappLoader({ setLoading, setManifestError, setDappInfo, - isSmartContractWallet, + walletType, isPreHook, }: ExternalDappLoaderProps) { const { chainId } = useWalletInfo() @@ -41,7 +41,7 @@ export function ExternalDappLoader({ data.cow_hook_dapp as HookDappBase, chainId, isPreHook, - isSmartContractWallet, + walletType === HookDappWalletCompatibility.SMART_CONTRACT, ) if (validationError) { @@ -70,7 +70,7 @@ export function ExternalDappLoader({ return () => { isRequestRelevant = false } - }, [input, isSmartContractWallet, chainId, isPreHook, setDappInfo, setLoading, setManifestError]) + }, [input, walletType, chainId, isPreHook, setDappInfo, setLoading, setManifestError]) return null } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx index 5f43f8b616..31cb491b0e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx @@ -1,6 +1,7 @@ import { ReactElement, useCallback, useEffect, useState } from 'react' import { uriToHttp } from '@cowprotocol/common-utils' +import { HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { ButtonOutlined, ButtonPrimary, InlineBanner, Loader, SearchInput } from '@cowprotocol/ui' import { ExternalSourceAlert } from 'common/pure/ExternalSourceAlert' @@ -13,12 +14,12 @@ import { HookDappDetails } from '../HookDappDetails' interface AddCustomHookFormProps { isPreHook: boolean - isSmartContractWallet: boolean | undefined + walletType: HookDappWalletCompatibility addHookDapp(dapp: HookDappIframe): void children: ReactElement | null } -export function AddCustomHookForm({ addHookDapp, children, isPreHook, isSmartContractWallet }: AddCustomHookFormProps) { +export function AddCustomHookForm({ addHookDapp, children, isPreHook, walletType }: AddCustomHookFormProps) { const [input, setInput] = useState(undefined) const [isSearchOpen, setSearchOpen] = useState(false) const [isWarningAccepted, setWarningAccepted] = useState(false) @@ -101,7 +102,7 @@ export function AddCustomHookForm({ addHookDapp, children, isPreHook, isSmartCon setFinalStep(true)} />} + {dappInfo && !isFinalStep && ( + setFinalStep(true)} /> + )} {/* Final Step: Warning and Confirmation */} {isFinalStep && ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx index c292d66ed9..3461eb6edd 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx @@ -12,23 +12,28 @@ import { HookDetailHeader } from '../HookDetailHeader' interface HookDappDetailsProps { dapp: HookDapp onSelect: Command + walletType: HookDappWalletCompatibility } -export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) { +export function HookDappDetails({ dapp, onSelect, walletType }: HookDappDetailsProps) { const tags = useMemo(() => { const { version, website, type, conditions } = dapp const walletCompatibility = conditions?.walletCompatibility || [] const getWalletCompatibilityTooltip = () => { - const isSmartContract = walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT) - const isEOA = walletCompatibility.includes(HookDappWalletCompatibility.EOA) + const supportedWallets = { + [HookDappWalletCompatibility.SMART_CONTRACT]: 'smart contracts (e.g. Safe)', + [HookDappWalletCompatibility.EOA]: 'EOA wallets', + } + + if (walletCompatibility.length === 0) { + return 'No wallet compatibility information available.' + } + + const supportedTypes = walletCompatibility.map((type) => supportedWallets[type]).filter(Boolean) return `This hook is compatible with ${ - isSmartContract && isEOA - ? 'both smart contracts (e.g. Safe) and EOA wallets' - : isSmartContract - ? 'smart contracts (e.g. Safe)' - : 'EOA wallets' + supportedTypes.length > 1 ? `both ${supportedTypes.join(' and ')}` : supportedTypes[0] }.` } @@ -60,7 +65,7 @@ export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) { return ( - +

    {dapp.description}

    diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/index.tsx index e5b56af885..1f84756a79 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/index.tsx @@ -1,17 +1,22 @@ +import { HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' + import * as styled from './styled' import { HookDapp } from '../../types/hooks' +import { isHookCompatible } from '../../utils' interface HookDetailHeaderProps { dapp: HookDapp + walletType: HookDappWalletCompatibility onSelect?: () => void iconSize?: number gap?: number padding?: string } -export function HookDetailHeader({ dapp, onSelect, iconSize, gap, padding }: HookDetailHeaderProps) { +export function HookDetailHeader({ dapp, walletType, onSelect, iconSize, gap, padding }: HookDetailHeaderProps) { const { name, image, descriptionShort } = dapp + const isCompatible = isHookCompatible(dapp, walletType) return ( @@ -19,7 +24,14 @@ export function HookDetailHeader({ dapp, onSelect, iconSize, gap, padding }: Hoo

    {name}

    {descriptionShort} - {onSelect && Add} + {onSelect && + (isCompatible ? ( + Add + ) : ( + + n/a + + ))}
    ) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/styled.ts b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/styled.ts index be49d95476..a87608ecf3 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/styled.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/styled.ts @@ -57,20 +57,20 @@ export const Description = styled.span` } ` -export const AddButton = styled.button` - background: var(${UI.COLOR_PRIMARY}); - color: var(${UI.COLOR_PAPER}); +export const AddButton = styled.button<{ disabled?: boolean }>` + background: ${({ disabled }) => `var(${disabled ? UI.COLOR_PRIMARY_OPACITY_10 : UI.COLOR_PRIMARY})`}; + color: ${({ disabled }) => `var(${disabled ? UI.COLOR_TEXT_OPACITY_50 : UI.COLOR_PAPER})`}; border: none; outline: none; font-weight: 600; font-size: 16px; padding: 11px; border-radius: 21px; - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; transition: background 0.2s ease-in-out; margin: 16px 0 0; &:hover { - background: var(${UI.COLOR_PRIMARY_DARKEST}); + background: ${({ disabled }) => `var(${disabled ? UI.COLOR_PRIMARY_OPACITY_10 : UI.COLOR_PRIMARY_DARKEST})`}; } ` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx index fb9293bdff..87661dc041 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx @@ -1,4 +1,5 @@ import ICON_INFO from '@cowprotocol/assets/cow-swap/info.svg' +import { HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { Command } from '@cowprotocol/types' import SVG from 'react-inlinesvg' @@ -6,17 +7,21 @@ import SVG from 'react-inlinesvg' import * as styled from './styled' import { HookDapp } from '../../types/hooks' +import { isHookCompatible } from '../../utils' interface HookListItemProps { dapp: HookDapp + walletType: HookDappWalletCompatibility onSelect: Command onOpenDetails: Command onRemove?: Command } -export function HookListItem({ dapp, onSelect, onOpenDetails, onRemove }: HookListItemProps) { +export function HookListItem({ dapp, walletType, onSelect, onOpenDetails, onRemove }: HookListItemProps) { const { name, descriptionShort, image, version } = dapp + const isCompatible = isHookCompatible(dapp, walletType) + const handleItemClick = (event: React.MouseEvent) => { const target = event.target as HTMLElement // Check if the click target is not a button or the info icon @@ -26,7 +31,7 @@ export function HookListItem({ dapp, onSelect, onOpenDetails, onRemove }: HookLi } return ( - + {name} @@ -37,9 +42,15 @@ export function HookListItem({ dapp, onSelect, onOpenDetails, onRemove }: HookLi

    - - Add - + {isCompatible ? ( + + Add + + ) : ( + + n/a + + )} {onRemove ? ( Remove diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx index a009113cac..ae02584cdb 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx @@ -17,13 +17,14 @@ const BaseButton = css` transition: all 0.2s ease-in-out; ` -export const LinkButton = styled.button` +export const LinkButton = styled.button<{ disabled?: boolean }>` ${BaseButton} - background: var(${UI.COLOR_PRIMARY}); - color: var(${UI.COLOR_PAPER}); + background: ${({ disabled }) => `var(${disabled ? UI.COLOR_PRIMARY_OPACITY_10 : UI.COLOR_PRIMARY})`}; + color: ${({ disabled }) => `var(${disabled ? UI.COLOR_TEXT_OPACITY_50 : UI.COLOR_PAPER})`}; border: none; font-weight: 600; font-size: 16px; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; ${Media.upToSmall()} { width: 100%; @@ -31,7 +32,7 @@ export const LinkButton = styled.button` } &:hover { - background: var(${UI.COLOR_PRIMARY_DARKEST}); + background: ${({ disabled }) => `var(${disabled ? UI.COLOR_PRIMARY_OPACITY_10 : UI.COLOR_PRIMARY_DARKEST})`}; } ` @@ -48,7 +49,7 @@ export const RemoveButton = styled.button` } ` -export const HookDappListItem = styled.li<{ isDescriptionView?: boolean }>` +export const HookDappListItem = styled.li<{ isDescriptionView?: boolean; isCompatible?: boolean }>` width: 100%; background: transparent; display: flex; @@ -63,6 +64,21 @@ export const HookDappListItem = styled.li<{ isDescriptionView?: boolean }>` transition: all 0.2s ease-in-out; margin: 0; cursor: pointer; + background: ${({ isCompatible }) => (isCompatible ? `var(${UI.COLOR_PAPER})` : `var(${UI.COLOR_PAPER_DARKER})`)}; + + &::after { + content: ${({ isCompatible }) => (isCompatible ? 'none' : '"This hook is not compatible with your wallet"')}; + color: var(${UI.COLOR_ALERT_TEXT}); + font-size: 12px; + background-color: var(${UI.COLOR_ALERT_BG}); + padding: 4px 8px; + border-radius: 12px; + display: ${({ isCompatible }) => (isCompatible ? 'none' : 'block')}; + width: 100%; + text-align: center; + margin: 0 0 -8px; + } + &:hover { background: ${({ isDescriptionView }) => isDescriptionView ? 'transparent' : `var(${UI.COLOR_PRIMARY_OPACITY_10})`}; diff --git a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts index e4a3126f68..4ad330b05c 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts @@ -1,4 +1,4 @@ -import { CowHookDetails, HookDappType } from '@cowprotocol/hook-dapp-lib' +import { CowHookDetails, HookDappType, HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { HookDapp, HookDappIframe } from './types/hooks' @@ -10,3 +10,10 @@ export function isHookDappIframe(dapp: HookDapp): dapp is HookDappIframe { export function findHookDappById(dapps: HookDapp[], hookDetails: CowHookDetails): HookDapp | undefined { return dapps.find((i) => i.id === hookDetails.hook.dappId) } + +// If walletCompatibility is not defined, the hook is compatible with any wallet type +export const isHookCompatible = (dapp: HookDapp, walletType: HookDappWalletCompatibility) => + !dapp.conditions?.walletCompatibility || + dapp.conditions.walletCompatibility.includes( + walletType === 'EOA' ? HookDappWalletCompatibility.EOA : HookDappWalletCompatibility.SMART_CONTRACT, + ) From 7c8d9bd8dc58185bc856c9dfb15587aa593b38c3 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:50:22 +0000 Subject: [PATCH 061/116] feat(halloween): add Halloween mode (#5036) * feat: add support for custom halloween dark mode * feat: add halloween backgrounds * feat: add sound theme logic * feat: add custom theme featureFlag checks * fix: lint error * feat: disable theme sounds for widget mode * feat: simplify feature flag --- .../public/audio/halloween.mp3 | Bin 0 -> 76694 bytes .../application/containers/App/index.tsx | 18 +++-- .../application/containers/App/styled.ts | 30 ++++++-- .../src/modules/sounds/utils/sound.ts | 65 ++++++++++++++++-- ...ckground-cowswap-halloween-dark-medium.svg | 1 + ...ackground-cowswap-halloween-dark-small.svg | 1 + .../background-cowswap-halloween-dark.svg | 1 + .../src/images/logo-cowswap-halloween.svg | 1 + libs/common-const/src/theme.ts | 7 +- libs/ui/src/consts.ts | 4 +- libs/ui/src/pure/MenuBar/index.tsx | 6 +- libs/ui/src/pure/ProductLogo/index.tsx | 17 ++++- libs/ui/src/types.ts | 2 +- 13 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 apps/cowswap-frontend/public/audio/halloween.mp3 create mode 100644 libs/assets/src/images/background-cowswap-halloween-dark-medium.svg create mode 100644 libs/assets/src/images/background-cowswap-halloween-dark-small.svg create mode 100644 libs/assets/src/images/background-cowswap-halloween-dark.svg create mode 100644 libs/assets/src/images/logo-cowswap-halloween.svg diff --git a/apps/cowswap-frontend/public/audio/halloween.mp3 b/apps/cowswap-frontend/public/audio/halloween.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7fde26b03377019f6953095510dec8202f7529e1 GIT binary patch literal 76694 zcmaglcQo7I|2Xg$f`lOU-dl~BMQQE5H#K7KU0PZdqxRmb6?^ZhRm9$VD@DoR82#H~2 zlr;2=%xoOod;)^PVh^Qc9?L1JsB3BK85)~CwY0HwbawOb@_qI^_~q-ch^Uyj#FX^R z?7V`);h0AKJTm`UZza$ERi&7FX6cx4!IsJv{mG^WxWkH+TQ$q-7guFD@x4 zEc$Qoe-{TR?|(1v=OM}R8UO#@{=a9;O^O2mU3e_f3)$rt5k}GgfIUFOkPYRBj{xI< z?H~{X05RqOb@*2VkN&q^KHhGlFsf!CtlpFVX z-&Zjf;c=zyo3R19tv2#5!gx^6Rr+iFS-QtEP{-*0q+#yM{{ik`>d z;IiP?Hpz(^KgDHq`xQ28Kdi!U^`+yiKx+2Xg@nhpXl$EE1VaBJSh+lw)78N4QOIAv z1PHWL)ysOWCM&u`PG9yc zlW?(Arp`oO<)`I6$MbBQVP40jylt6O~LQB z1XDqgEPrYloy;V*-Ks=)a_wI|GWsIPY@=n%pJQ5vAio^mG*nsL7-1=4M@s(k8Hn=n zouq@1BFXldcM~_$KZYxSrvwDY5kIjJ{3a}9#IO3dPRv>A6Im>h?q&i#q?{?!d!GBZ z$li2LCAfPCG84Kbb0}EJc7!D^f*fCKkN=UZ#tn(*nwk0PPMVg&@*a$1nfBe!Lpjr3 zx|s|qH^6{@TV?7~H1@P!biq0F#Jy@sC-1spZsFR?7*Nne6idv;m`P|=ND*9$Y}Y($ z)S;Qh_Q$4E+Wk7Q)ZOs8iY6M_hugosk-;Oz_(r2NCR?qj&{S<$ZuN)ke6e+!=j+ek zYKm(ux7%G9;I`8UOCH}^B0{)~w?_5tNHE8u=8biAZ_YCM>G#h=#4UB@uq@uxUWxAK zdl_4HCYKC9CTuF)9tfrAb>_&9NBq#%|0kr59MI)fvG0_d71;*+-Uk=4(gZn)izB)W zu)FsHG>BWrEDv@a+E4%(K%ha?3ADH_bKSv{58u3!bW9M7Knb0pCLTJsZc-kyDLL<> z9p)tQj3s9(*<1uq=R||LMl&K%hP3W_G3VP7{VM6cZ>9<&dBR!V(6qcNaJrE8`)@(- z1c&Kt^fgGXLj6;x{*TrVZN_n!OD_u;n6I0!?b1-8we#yrzj!xsq3=rMXz}smHmj#D zn+_2Ie>$%pD)z*^9j(R!_l2=2(8v?SHnVKxipN4C%yen;aocMluDRh!4R9B_G<}Fz z5-+cFx(_>R$s2)o1(aZEZ=g1RAZcTuU~}nOl=H)z&UXJ6$HKurITdcMf<&BMI~KPc zJu|zZadECnxoUbvy)`K+ZDUL~|1oCq#h-o=q?F@&wCa@D(d=7%S4XAaR*31Zr+h>C zS@P^8)~%0@zV}?Vwe(&S9|o3^>Z-{(YJt%2hiat~hRkS&C82MkMu+&>-2XsiDTd0^utb1T(+67~^BgLCgmh)!mxW*l+R#Yj7r z`(cZGjBcEE9y2aToON(Uarvubg<9CvlI6xn4asC|@t1!mZG6wKo-Ai`Yrr}c2FHh< zmJf#h>0a^NI#QA(1E}Mwu*Z5F1p+)Qt~OC62S*`KHhX^5KX6(b$5?i*qG$$LsRs>Y4-G|MnBjvkKq$RevdbuLx?yFLV(+P#H5V1g!B*Q zri(0TfLxhz%ev&19I~-$s_>%<@K&K_=e+OT9 zcFS@BjojapWOL<)7bTnvIs`n;zbp0>kvH0s-zJbK?I^zM(q4a~@}n@ewYLy+XKa*` zTitZRL6gYC=|@5?D9k;;@iNWG9P94Ka$)Z=Mo$aLf~Cf?i~D)eR3#xQ>o}GB5!FFe zL0tf0`#y7peWjX4wpZxVW_7gXgH|2tNy`+h6arvX?k7#gV@R|dKth}w3#MoIYmb}_ zXu?TDM$;=21?fJ*+V1NvCM!>_4G8Bn~CVxO=YIHhh~V=6+uz$*G{yb=^nz0d@{B09Z=; zgcBWJZOh1I05WEU&KAil(jBxIuq+d{S8~Hz_!=0}J}^`s@+IcM>OPn4q@#C3O%&*yb zY+itGkH0x#5^AQ8%fDr6?m(jW_a!B!`73qTaq*1uQpM(TUH58?={Nv_AaM~kci0&r zX}(wL3HUM3$Rx7rGxMdz*Wv7wzPAQ|2Ot{j#7bC)1yIyl8?Z)H;zL@hr zv2P}2il_H-g#YZvL}f5D>`xAt65H)D;jfvt`d5~9TQ%I(-?UhzKQEf5j@xB)lUQFO zd~@PS@Lixz^>a}uy;GBEf*FB4WTJJKgg5Bet7DLZt<;MtnibAZl#c6jDWN2h&X8Iz z>;x@=q-08dL&53cCXoc9d`S4uA~=v)-8RP|3F>A;VJmF2`{NL5_|oz$!OrxMPgf7H zmnF_LxlC-efs;JWE6aaElK=%Z$FS%pbmId#a14me3#yeYP`!iR|BGMB)LS12AtKtR`# z*k*6hlh=FbCPmHdvGLTSB|9*=^rGSAo1wQ?mT!aWAMowc-(&=Y5kA)whSV*#eqc&x ztB_LS^^E$S0D!RoUJh+T8SD~G-w1MW1Rc;g$n%4-oFiY;RfYZyo{i1S5W5q?y=XmP z)$YX6_2^xSkvP63h(GQLXMmpUq*3SqZJ?>Ujp~%Q@PNVWVvJLh?Z*%+FG3mg4028MVB z%@Dk7hXbrbWF|!=WyLIPAh3V87pgZC z4fF$U?=14-C6#>t2_1kywY-B7pMrdZkzk+uZH@SM1$+IzBRT`HL){ORH#tRZYqLmV z{`S1XIc-8@NVfLoXXo9m=`WL*sO@_!M0DgXhpt!m+QeAi?RhTR$Xt#*wK?r_R6QK` zK3U#$ZCyD9>)_Ke@aP+NtteS${iKSkqhNL`{ZVz`#!l8o{CI>niSDJL4Rd8Rzr1ZS z2eo^=>&t6_g@zrqauqZPc$(7phFqpF`SwgA=UMQm$!m!}Ox{kJQtb1Bj>&Qf#!Ft` z3)I^jjtpr{zU=qYsp zWAykYQ?BVOMnCM=N3^nmx*80|q8C1Ct(TY0kBqREU3`r(7`sg}Szhm+$qXSbb$d4= z5ttC*aqME)reLm9eX#jwZSCqv$NFT;0zd))bpj5+GB(OVM5p^==s;CVJxs6fJ%_>$ zbrTDP+B1F_kPCa*?SKY<{s`3@pbgG;q4+Nd2;-2-i>ZxcdVA6&Xb>p9$?!(lS5W;w z&D*a(zy2)$&!hgI&=q*;wfa!Yy5di10E_^!S(40_vFq5rLBB=*FPG!Rb-tj!W2(7w z(5P=tFZ=1aq`VsS){IWdYyU-D|<7LCq(0J#x`7o%dF|J66Jod0dPEWo7^7J=UB&HxCLpQ~>q_v3Iq6+3uAl)i^G93vUA~ItVzT$f#a(dPAwYD*yWAS5; zRqsxUpI}COnVNYVf3WlW8hwIrjpm3KWfgiw+LTKnT9p@Um8VPoeB6I~TV^NSaO7u?qU8gdPh>D=cqvNkhB~pZN;DT8| zR0IvP43uObs((T!*tlhS$`fmfKShw>sQXNTOjwcS`^NyF3Tan*A46Hu4&X=%$NIXc zZtQj*2R_a{U#gP^jAh;H?w47wr36zo4@;>94Zdym@V;x_p1;-Y7A$FU4xQE%w3@E_ z@1~C+f-;}&SG!A&nwago@U;Cwt@>#-RHune*Oq6MCs&5A5~*6G|DKny4^B7xTWXV| z(B;HJJLBl9mz8U`Cz!*-CCyT;n`$@+hnx+iFZP_cg%L|%*s`lbZ^*ZX13xI3s}t%8fgd`C~(vP2u&FeSH7o<6xk${AR);cGQ9LG2o$ypaB#Sme%} zp~=x!X%2w>=C4iSv|j7m_dDAADO#ib25;(A+U^$ughlXVnz*SjxuNrDYu6;5 z?sNB25(lIx+h)krwdMHn@Za_*QK(816NKsd9%{=0<@lmWmjv}#GhN4)8daGpv%ELFc zRr9Z?^2&ezgqES8j(ef?ARj&eTzhW<)2ie0+TTa#0c@!IT<&-L#VhZDFpiMjyTX(s z2yJ+svBg%$=Nz}5zvP0?H&90#Bvnd04}k;6gR8Kmq3srakJ&0#_ZyrPkj{P*eLV_dmW5InNjIL?3(kAV#N zNbwbG0y;B|zgI$Ml;$Vmh>x84v2umwrDw*tW<0f^3ddiw-_s2d)af|Ts z!S@dJxsGbGjcTF+DCEDwV7p9FYV5c%C{9?c$r-r znV)K>K}r;bQtBoa3|p?a%t<5e2ei?f0K9{>6?umq0on3KOA~R^9?%bn& zfG}RV<^K5&Hl;v$iT_ZRQi^F%cEhyn^J}Tgz^#VEpQ;;+#w&Cu#$(H==B9xh8OED! z4;SVaA5!@K-N4MhJ9atsWPw#e^F(mu2Wv_4XbB;aGyrW97HbsTkcKF{Bg~9F$0#f) z!i+wL3p)tOf{PR0npKpy{xU&I-ZOV}j=CfHEe$;@YidPR9!5Q%;gM4s7{Ld>yJvCbDWyP-B;^+8W={8!?@4IiYvZ{jq3n&Fcuw!J^kZo$bX2Nv#e$FNF~_{86vHnqYBV^ z+F_u^jL()>x9J++QCu98JeGqjCA* z8#8J3PiP-{(Z{Vp-)?@!BS(Y0C!wQNBo^`{K~Ew7C$y{}?o(R~#FfQNyRz;u()06U z576f6u!>7Zq$qBfl`v)xb7o2THy=Sy93R^GKUy&5a@n^k9or0;C|IfWa@xqU(e4U- zRCnFku`+ngm&$4!ZSgmzKxCU%B1K=aMmiMJ>y)Dp%47iqzZQX&q19Q0=VRlP>s#+5 z`z||}yFM-VDB9S3|AD#NN=L~7AbPKUWKdf1oPh;)SU6l}U^XU$KEwjH3ISt=w(8qc z+XvE#RP)?@UgWtTuoR3|3UeY!1rP}u?D+Sk2-qVZw|>tVsg6VbcP6DIt;!bmY&C>b<#|mJ=NpdNvZsExN7I=_n*?C-jE!2h zc%!cGGLfurwFISk@@~#+GYv|-vNNBv@O*4}S(<7f$mEK>`5>h%c0EJjsJavWmq?5; z`|<2=X}G;;0wDap23}wSALW4mCW7Y}kZ(&teiB$H*U)cNC@6+g`$wiazM&o+*cKUV z|68j}9h{fvC`MHotNby_d7H=T!MhwISoMaKt8 z){ic*kK3;rrx>i8G~PN2Vla25@^v|Zf$QGI4oUCaA=uPpT!{w^UC&m-c>FVtTgq?}PL0AzG)DyTZq3954 z*|fUc_p*LMKuN%7mRGE4EFK1$#)-r!c2$aKi4S159x@>df~P{I?M4R#Cuym({|Ox? z4R#3+r*{PVh$8-XKLnG^XVTf>MYl$Nd6Q5ITYtwC1hqzs28I@7xqwmc1Pje=@W6juclFhYL$_s@fM?O-JLy$e@rqDy@`4xY);D5kQDJZQ_bY=#P z8?+Bb=vSC19(}kF@_#b(w4=9)ELlEf7*9VfyZj}EHicl0VaaUE5UyySB!#*@6AK(( zk`m7NaCs6Qp;@q=f5O@I>$11!clV#Frozj+EeysHM++>cHgY1AQ05FDs)RtB6jKIR zkXrakFf^PrK>!Ojyp1#If*lb9BAYh49Ms3{H8zIg;qWI%@<Ck=tb zRmj&Yd_)kd^Y-2V>EAgDAzNPbK-8BvZ)0G=2&ZR9t?246Qczeri9LfFqzf+=lxt74hyxCpqw*Pd!pJV=ArG1t-T6Tcis*0`F zY%FNz748(aO3b#0|0AL#P~Bsw(eV!G0WJs|6fd5c(E3Kco{A2_fd}2BGmjCpjU;3Z zG>;#hS|dNk`@a}v zvHYe&F*WTlL=J`RKh{$q(zMfy-$2=haZ(zA#wOWX@Yn>Hj43ldH5+TlA&%J|;$%8t z36+L@`!V!jF=_Bb#@E8je0a}#h%?scQB>LDd2X4;F<VGH#ed zw=EPIGw&KMN%4CCM)F(%xk<)0_5pk*_F=OtfKN5$H=S5J3y9cL;!Cw%F@64O*{9pSBv^#|sS-VT)&Yu)qrvLndQ!%ge7dA!=02uZdAe4p* zLpo9?a>c$GQ*`{fD4Xoo)6`yD+TtaY=0R?5Ju9M!B?N{o6*+_$qImH7|{T%k>I<#xLg zGG-kmrGQ2*o-v`qxG<1lrLY>*lS2F196Mix3qwcFwo>CB+hxt^EgvQ5iE35$kMb)( zx&?bDN>qiUL?n(@c!iUkZ95Qt#nFO=)`NTD7B?osb!ZLMJ_O3??OY9qsTqswlben-YVc<_zSLsRk?jQ6Ll=bT9MA@jWEt{wK5^gL5r5y3i?q zCMZ9;=6T3P5>v&gv+s<4AI-j2PLq-Vg7d?JJv~FnHg#B0y5^xR^Mx&U`7OGuS6!KMEfzk#ck_wn4MRG?jV?nv{Mzy&diRC%Ed; zWP`)kV|cJ4`7akew>4Knm~XbYJVH;$XB+NU8|F_0pw1ye>9hQzB%CJZT2+gXPG1sH zB%FXXsFje$EUiL}N5FXF%R1rPMl?|QV_wyH^G{a!KHckFi;Q%M%7pTSpPZlAIieo4 zy)FBara;E_>f&Ne$P=CvREXI?MCx*-9=S6(JXBCRaQd46u1R=&F8%hPW&K?R7=~l7 zn0ocmo`BOQHFG5}tDR5(tAyq8mpZ#=q0ia36B)Zhe_{S4A(XB|7IUzm!g?W-iNH%J zltT|Hd{$2+k~LHKDA7O--2&j>)CEUz7@{+Ur&2?4s?IN`uoG(?}ry=e=~%0=B6X`SYTyD#&<$! z>C~DMWRjO4Fk&X&p9Eg6Ah?+w?3^_YTkit{pkSxBYxY}Wnl$2g< zhe;SaS=2A_)yU&5`jDNvmV`xXyhIgOzSl2pN1hvuJi1)qnqlL}eXk}m;hOtk)V+B| zUb>3?mRL=Q|-l*S@eM7d*^s9CM59B080uWaqE7I zhi5BeBr8hsyO6A4K?w+hUsFZkqv67|IEKJB0Eg2m62ZZ6Fct~{tu_&yo|S7dpbTjJ z$wCZ401(;)J-8T@aX{ZVZ7jFtNDMh>Ox*alcH(2Xv!3Xek3xaW&x<6p0!2zu`H6?G z))yLP!rzXP&L;Nh%_~qd8H21nOdbSI^GjjWaJ4a!!krAf^Zm{Tlv5jKIbzBq^Qjd> zwBqqZ%(t>z*MIoI0aV0}!W;@}q?D?BhS^j-Qv~KgyA%ADXX99NM zFYXXBN`@;65*KfDRn+cS8ERA#@g=8l?KjfG|N3so>s$Z;o@RU)CS_HFDz-q+RD=Zx z7O>}t;1pOXa8<0Z0wwYx;I_Pa+2+7;Bcpdd)~yYFdSgijR7&Px75QJB&eJ_FI!BUe zE(mzt2^Jq&e4g~{_t#g4?3HTxY2V1cDov^tn^e?zklcEDjjAK;Ioj!(cq2yOVJzVc zr9??L6H?Q$OI$L)B4Cxs32~tK#+tJJuBP?#g?9h~O#x^bL}*ZX~kVXL*U`u&#KXJY8o7xk&EU0q%AsXnO$4^l0As1`nM{0D@7 zB{5%AxE_GLup<~*#qv3a=!eqRFLxAT^6x{H>J{PrBMyexBZTMTxqELK28Fx4d1_z# z$wvkQ36DK9?~3#E+(oqAI8ATpGF#iQ);gkkq41fz1~?MrClT;Gi*(eQC?ehm))JXW z1du`ECH3S4%kVz8S`QIb5G!VuWl^0QfyhK|^S5jxlp`zu3AGVtOgvJ{pJ8m|SCS%k z>6ald*k#h$_D0vkcGpT{&P0JsMQ~j5BX{dw>wL)$7U}w3jZ}FF%#Vt@K!Tvc6dWvo zA|Lk+*CspdpMn>&uIOk$DGi?#tPxEk!MyYF=WxTs4=5i^gYnquZ#Idx68iCM0_o(F z|6pQwD?{QV4kBN=3an+|q96B=Ios-d?x7`sVv7q*|ed#a)#4!Zf}qkRJAJcTZ$m5Q&jjI;qG@Vyzm`vU^v9+z zyTcqMBe=8mf3|Kh#Xy=OURx@B7~Eq?`xl8Mesw)ndh<`H0bFt*pwX49eg@}I`|TPo zNdByYPRO4P-ID15DUJDAoacg%m8HhjSVz%AMq5}}7)5L|(T z?=#8WUZ``B@R{W3wkQ=ahV4{C3J-eux=dg*u)HNzten3uW0DMJZ$GSrTUA*LoEEct z0eV=Z8moWHf4!>_{TOARy6zaaEUr49Jj7VMQOrxwG@+E0hgD>G|HPdBxHTGH zQGVb4J14gs+g@4Gw8%S&ua9r*^CN{y3bumMyCx2WOZjoW|V# z8d|(IrZ9`V*@;k-?9R}Z82zz74gi|$w0~zok_CztF6jebK$D^OC`1Lfo4Mui$kQvc z_$aFfwQ0;sWA^6#YBdhasS5kGDXV?S65daBt`!Xea9?dl$o}j1VJuN*xv?3>nEk>B z^9xUkckC|w`NGQ%i3uZ>VVn5bj9Ok5HH3h>ZbdZA;R^q7NVMv`AhCLyUQu@zzk5LrXs7 zr8LL3tlU4T&f0JWq5Fpe!VhtL&jUUVgzS2}?8`Gp6S7R(~j3ws})JztM-qF(T68#3;1U$m_fjQDx^u zLuLD-)FuMP^LbF7hlkdwIf$V=Q=HqzH%7P^C6z6i@(Z@m&6L6UX`_(fZ5Ge4VyaR5 zNz)tO&)A(U>FL=iUCkK7A~iOgX)L*C+Us=qDf4=ha}wFw>TD4yn}gn6nBEHiTjpgo zF%;~-$kifoH&UVoerNiXq?8HMn>cRVMIkx0w=eQF?g}Dp$uYfmMpUei8oCa!hQrN9 zwHYSe$zs;q2`7>AAoRA_q$rx=e!foMP*Txu+VFoNG>|kX{&1AMf&5IkR0{h(gh-51 zS#%Dg(dZQWq4%+FK+HIn(tT#?VleSE-kFYVXf1BcVNZ?jXvAsqDwdi_f*(3Tgcn77 zkh3LN;@@aNRKj+$rbM!Y!?vSmU+6J|vc26%BBOW+e(J95dQ*ofO#FTw`o)03h3@wV zhGP1z?IM2n+p$TQdd2odm;-pSmeb6W9&;TUOe+VW0f@(07`O&U(v-EZ{7Ne)#Rgiw z_Q0tJ|cDQqcotcIdCiVzcZE{k4Rd?KJaBk9lR(GD~snZ=9kS9f3Icl&OmmK zt=(@-(bX#2*1aTaYRr1QP$bxqNw`Ok0uQ-b))ZY zDBNk~51z?lS|jt3#2bd|Aw#?%zl?l->{7E($# z{K$oxRh_4xw&u5bI~5&^BC2VOYD?^LWgx+O|Ma`)*PdYao8JL>?G3}BGnf_W3;iIr z{T!u@bFN++jQuTha3B6}JRj|GXa|nMF|LRi0bb|Dr;9P#p*r^ggkb&Cusoyx5)#fI z(WjT_lj{DW=o@5B#Eam6Myzc+8Z*ovoDkAyRLz2%m14k-z&odaYv2uF!|};Qo<-x+ zXfzPMXL&G`mA$ks5@Q(dD)i-hSVl*bNbytwng!)tNM|J|f0_fy04>z5fM!2dXsB@hiheOnDFUzQ0EQTK;dTzR7=&BD%mLmL!adUn-v3fRN1U*%}7Zfnh)h)|ygd8JVx@m{Trd@5h6(KCH~F(Rm04Ch@% zRYKk#7h@G&L2h<+3x41Ywe92{9KQ;aIj=4MEbaQg#F4F=^gq9c#2XF-A`XLz>zXp- z2pX7o}QvKMJ2RTK6+ALd+EEdXGt$I7)`xkaaWh_G@ zO^dTH!egJ{a)kAIWakfPV~P0oKOYXg4S4a?32f<@bpytO`{rkPxU-Q*MbqF(;8gR; zmNE>4{|E7YS6j?(3((9Rj+$D$6f)0;^D(EA79jM%i$pGE*u2%&p{TW_`y2G5+m~d3 z`Y14{Cfpfg-h7JBEkk@%Vb_Q+DI2-kSl3RsvW3*W~{KZduS3J_m1{xvH zX!KAl$5llRD(;n2o(#e3UoVO$0`7V=8kR6i^#F4VnY#Teh7z|$oTC>PbQ<=;*rM2q z^Gzw}Y^KM4e`rX<7K-_Xh~@Ma!Uz>dOYyBF!?&~R^1UZgDZ`DnJL)n6>TS)xE9`w# zf6L~_1_RZAgh1dh zE(L9&5w>-~mjf|s5wIm44iiBg1%zhxAv9GZz|b%}Fs^l=GT(s@^2+qr`)dXIl(z7_ zoL}LY&+mSby^^6L-)j4FmWR+-xtzfSof$^F8D|7BzQo#JzJ~f(K5@3QUIykc80Rb0 ztS%y=#jQ?+JQX>j=f>5-CC5;VK1{B8T#aGM`Z#L5l5P0e68);vOh~!VSSj>RH5L$F z|0xW@)tVAs&#hUT!{g=%>w77$fJ=eE<0Sbf)CwNhzfTl8)z5_OT)ueYQ4!@d@PC(? zkn4xoD|d6~$&?gjGTah{5QR0-#(Wb|^9@;V{5g{G>CsQuBO>Aj9jkP$g{{aksqP9f zA*^$m`J9WN?8?}{;{mSej;W^evFE%WLS(I{GM{nW#H>%NwWDdk;mFt$T6)a^dHf1} zCl!lk_J}_Q7yhpvQZSntKK=V6TO$0q!m0Gz_clqF^NZpTfGQneX%ZIHN~cSa5&JF1 zE}1pG$c2kVz!BNk?lB%WKY@#^ts9a5&_1($dG&G@pDaK{fq#~~T469nl{Y>=OcIi! zOn($`L`ez2&Kw5mMRfq*uVKUa~O`KCVju6M&26-BmUSyR!S75 zV-jH_jYT0Aq+wh{N_Uv$5MLPw>*uIyq7-6*ATR5J?_3^XdCP9`h-Z%A3v#BUGy zxH&^IFr4yRJ;%T9tJ)Z?%o^^NTA?rFF1|oz$r;WBBK6q|E(i zYQY9ZYIImB7EDF710`fsIv>2TMKVtByQrvUWXPM;9Fnh`V3}yo(X|Kz5+Z(5Stl=H z>LgDOcOmfQH78Sj34k|+lQZ)CcG~zC6G?H zaHlO!m4-1V2JPaLgF;X|B@h^lu}4;lpCdf<+jZO+$#BWd}AHPQRLt=IpoOrJ>=f}0CvR~V-bupih4=ZU&kw&d?Kr|za=OtjQ zA)Wm;=#$wSj=k*W?wnt?8M)w~7T|5wDxa83L9IMbzT?+K{>xb9H}Q1$X@kgc9HF;V zo*9Wk?u#3S^@2#b;66P|$R%cOk(#|6A6(AA>!eQVlBRfPDZ zp6(UQ5K-;6fA!z!!2D=o?z8ID`T7%+(g?~0=}&d#j{arh;%^_s=$W{Eh<97K05!!$ z;ip0fpF@?ZOg4P*Qo~V7tKr`65xg6}N}Rv{@pk@P(b%sjPl;e)d6EoBygTjmeM1`c zuJOP5vz+Ep%v!ah5v5>8DO;=V>m>OFJ(~k<_yo4097I~L|TD-tsQbM}@|4qp$WEBSEC<7fX*=!9VLqxcB<#)~s)yBB>PlV-$5sT|+;qtPE@ z?ZYNZE$(ZkiwW9Z4T+7vl-a(E)0+=prLnpZ=Z5*LeC)6fd>R~2ax9z%!b-f!x|vfA zl^hhhU!_&nuQ2I@`Ds<{H4iYpN^?3m64O=?f6+o3)_Zi=IVn$_ zXtzaN!Z6d-G4&Cm)u4!$=Z(c$BTC2+HA-Yzc*3v4-@%p=T}=sUQNI}&X@%v59PWzX zrwSa9>APvOwxiG==Ss#pswz=hywZA=uZ&*T>^PB)G`mnUKBzZ7KdmpxH@d)Nued#a zpnp&|QPC@*W{2PDDM4p!2QhQUIAqRQG5DiFlnn6_pR(j>Cp6xE*rL~Qk5M<*uKs|E z{QRlKLX?}kG*zVV&z_TaO#<9LR)7y6hy*|%X2F&S%SHq|&ozM9snX33iKgkO_bcR; zYlOBIDRa(>QR^y3J2{B+mWfe&P5Jy2Iwz)FRUU{~7d?9fiuvY>{Q`Dy%%F3BOyOgi z^W_EP@jMA#RP4uf=iAc)21<$v0mZOP<6bSbIQ5F>nA6+RJmsroACL~|iyGiEe*^qO z+{0k~59aoDD;OsOz*Gb!Bd#K8F#(5%X0vWMkd;nTI`Bzr&`{1VXSdLbt^bB}qF)8~ zDprVdt4-n$$_*=T5*fs-PF7bX+zulKCh;b@0QexlrGX1Oj_1pS@JcXvYXEca^>k%&0rE zLvfQ6JoKmDQ8{H)e?Ow+WKeDq~CY<`Q49H?v zjce~(w+A1p%9FpT=-!5tu*^`|6iw0$H)ep;3XL#S1kHX#hsdyEG|^UJ^Jh7-nPEF$h{H4~$KE6${H5&WF!H(KODwU;E5l-SoTp{gH`V6u2tr(j$F*LsRGz2O z`?;wbM;lD189(NGKfKIrW=6XCX@UN=@YtpQuDfTF!hes?Ul>QJ@c_?unl4j_LM-Q@K}GtXsc^aE48YpYNK&h_5zfp$ns zdiwWpo!||q(4S&1REhR1fw48in(Lp?Di{P6Qd{T6Ch5YQkfToFIzTQ6!2K8(AE-F@=8A#ax~?veVfdn6$Wd>~GsN5_@jl$Qi5X$rCO6 zHQ3$`j+@}iI=u5TFyf7-#7;_8n+A{EPCddHr8i#w$&JTR<)ZMWvv&&xj3UEnaP;sv zw5&k~#-MfM>4Y*P5>gVWmwVmYaw@T1J~1xP*HfF%f)~U3vrKPYVOUm~KmmG6N-Gc) z*(dO1(lFc-y ziLM8e`xUdY|JkwfL@i3QrcKeYzS@`oFYyK%C1I;*o0C3S3BGSQsg6-Zz2oJ6KfOq(u9-^w!3wAH8#_q@*)@%)5r23gjRi`c)oRd`vPSNM!}90#x}u8@g{s&#L%)t#k3^{q?__q^jgv5GsZw{klfF8 zhsEGQKTw?eyVJ+!rBuo{%YE2*l~HM9unSWP^+fYp*d#6+&-iKhimR* z)5XHS`SrzE0HA}w1DuG!M^4tvPT(*z=Xq==Py`)98h;m6&w!1k_elr~83U_*B$3rl z{wmjC^|3XjI{M4&Iz-q%p)CT?s<_I7Rq`|V7jrAucn(<1H*TGe-st{l&erVG6eb0U zVi2DUk=>=Qm|CG0@6#nR-uVY=Kgo0#6*J!&(u!tbzu2y|jjd}Ti!$B~(`Fn?v^V?J zG@mUT5QHf#$Thz8=4EJ{IXUdGAZHH1{IT+0(}vc`QOKb=ib0S#?$t?I&?qBgvZepm z0j=kGwC(w7o(P9;AG|&HrcC!qJ5jVHSgD$#qw4GCyCkfJacFij+K5C2MFPlAX(yUL z$HLlUMOcv$V^VF?^W+UW&(*9B9)IaQ|1v(?SScX*CG{n-3%_~1io~j(QOZ)T{K#9) z)<-f6LlxuQK;a^-I6jGP20wl_KHb7alg|c2(yI(&+R~+QR!vUp&osxE)wkVIpvSDz z4k3y0!nR~hTwe?bu_A?Em&YWjFZfnPfQE|0PN_JuyfHQ0w{H@PA~7of2^fZ*ws-&k zNZn<8__xYk6b^(~G2ad_n|H#Aum|JWoI&+mnF!8%6`0dFD0p4R3}y9iC-yS#&&$hv zcuv9h-J!vtqm@P{j_NDtKcUT}_xZ}IEi?3I!o{vs4r2x+{BOCu4)>5fq#Q-}?V%;F zUoIiWFfqDUcrDMn;=nWxc(2I7kP{^?7D>mOKQ09Gl|!<{nqGrqj* z`0%v-*@EuSVBtP@fQI2{-z6a=2lURrQ;v&td|L&PMHmJIBlvKuL;S1yxm%vm>O11Q4*Wlo&cmPT_x%&j0e>wkPXTR}0Pe5!1*b$H9eK;SU^(G8_qb9`2m&b(2C~f0dXbzvl;#3xK-9_fFh3Y?HhN=8nqnj z%fK1phfK>pjERaG+kG;uFO|aG7s4jz3pc3CaBd6}gq;K|{hG zMsN&fIwq~`?@*s1c!0%lw`cn5R=Gj{C!gqtFH`a?IWG~Tmp)r0P=O4j``vRrG zc_C0{fW6xVbEEQ8f{yJI@v28$qdZ>hN7uYII*hgZ?20a4b-`uj6JA8uXfAI8Ibn^H zn+Zm_L+UCTn$y@vsGZAth$}MELr{2Jy`bXv8(uNOk4%lOH1Qb{P+VTF&z>G!k}}dy z)U0E??ykQP@buh@jQ*DUVpKyH`I1C~$4GF2bT1;n<<5g)vufKHYL#jdiG!^3Ep=Zh z06+j-g*aau=t|(yI4X5b-4PeDNb?Hsp(0N#v0W`yeGLryvagZ!^+L=62Y@AsJgNea z1sZ5JrY?hsWKph+B`YNEEWR3G%b>fQ;CnJ_KWEl{bzP*nJKetO8NPO7xOIm;e@4D= zFMHuK3;#___Xn$+X7}aIAe*lGuQ~Hv>JVbus|P`7KC{4ogr-swV6uZHFWe^NwN({7 zjOY;k+~>@yTe6?St2YX<6F2{?@0{BjbarNbXg*Ma=?io%AC z_qH+uM0lgXf9AG%P!%Pw1(5q`IZO&QD1WRQDV`B6_A)hf&){izhs(LD!QP~1MOC4) zpT3OBeRJ2s4UqGfUSvsX-`xd(tD=tajOI4O`c(|99_u;^EF*ciZfv_duHLQuOC-`c z%8%IHlVyUgjA>4X?9J`&W~x1_INaH>cns8B%yH?ik5Rt()_=(|o2?QzXgX?Gl3Ih3 zx*%_@j4Yh1C`F;e>wgFf^Jilm|$%G63 zr;z2Aupoy5)mJX=k#Y{ZU$qVfZ>%BL%L#K#-2o3RvIB6>#$J}}xYlet-VK=68|nU3N{&}oBSj6J*Cf8d9b7DQn|XW%0%F* zDa&+$P|%hbWj(cOs1xrX;AcBT`FK;oqBQ1ZOyj*)2VY>dxxR*5;fk{O*{toRYd7PxE z+>SFJcJ7>w^?F2~tG_&8Bm>99^LD)4x{@LZOJeet^syIA;`HOZB8#Jg#nyWIGExDO ziYemV_d*W4QJi?7jE5PLEibp*D zs&UDKmu!s=?zkN+nVF>COSe1o&gvX5eYYX?_OL`k!fa-y2j$&K%sZtNM>nnJnplU7w#U(229uybsaD^OfYKU<4;y z)N_nQ=oiv3-O5H2-r*?oFp$W7zV&0=y8RQ+vZ(3_mVmK_Cy&?lH}-8$GtjK7X%_P* zdliW6MTeR6YZpE#pV!JNxqF~V`*<2f0x$qlu#!}g^D~8zp>%i=zTuu;deiaNt9b0ex)zRK*^T_Xc$&KP>qre5$#%Q z?ujLX<$~3~II-I^DCQCT8Oflw*(^HNH2!|qs!b6dv!IE}3UtiE{gW;-B?FX!1zWx$ zLq<6mNj)hUr)g``AHpCVlIJ!ucH=D?s9sh7VDrShZ2WwJE}KNKjbR_fD0=Of1AAnf z=r|}gsL-GJoGRsF*Nsny_(##}oc#tX*e_IqNe=j&+>ap~P{~JrwQ$F?y z3!nX8%<9#mTl0TnjL+!_+XPz!8hIe393CwTG1iK~*U%|k3PIxAU=T3n5YG$Pz!nh@ z;CV`Rc>=g7gHeyjkxEUQa*-^5`rnkRRonlj4|`dOCCa*xS*dM;uOpkTJIx1B=36&zI6MCx-T!+yKsy@T!Nm=Q6`BV!WHH_QeTzmrqyU_N90Amn z1D3SMx9KM(u)F-Qv2`Fs)X3;Qn-94BTD(}f$?DTat>j$8cQ2zHCP-FQYd~ zWsUxcJ`3|&Nt&UpTNDb)knun>j^YltmmH4X308=kmMa;V>qNT&s9a|1CDHVjXMyF9 zBH2 z<5@_vm%7j@TQSr>wF6au-md~c2JkmyPyTqEnwaKYITX(Gi{G(to9-)n%$$O$+|Kc# zn2?Q}m!*fn{Exbyg}Ziu1O%Y)EDJMQxQtxTKMl>ZBs89OPTyajz*jn|`3V^!2R~qgTJ0$m z22ZmljDyAG+p{(PQ5@dspz10Eq$SxG%ItP^T57^^RkV%MYl<(z-1lb%zuO&{ zC0ei$kBgCM`>R$O;;QiQ!p0@%6O4bhr$B?v7xOP0h70G8nJ!Vl06SbRLFo>7ihe0h z{;F^ugw!a9GwlH&oLQ!7a5*Rvq0Gn)NnwgZ7E+4Fs#k^7G)#pbdF$R2mmG3+mXh!- z1an&TBABIw$B$|sw3x3bc3!M8l;!}F(rktafukCDq8j_K)KHlZOukQcD z%ch%&Ehz9tsOt@yxNQ+Vf(|I}{n#{?Ot9>;&1RmahWDv+j=J|ckVP_72l0zbjxQ>lE3ymvJbKY+O{RWdW9+F; z&%Lk!F)14DSO@~xf_1+!EcP$hRSf09epi?zZcLQ>^*!+RwR|MKv7L|i;o#t%_`ue} ze{xcI63Pg`0RSA@%c#AKtT+wk>+q%793?GUt$Ph75kM)Ff%0DMax^PEO;OY|Jz~*N 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�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 { + if (ACTIVE_CUSTOM_THEME === CustomTheme.HALLOWEEN && darkMode && featureFlags.isHalloweenEnabled) { + return 'darkHalloween' as CowSwapTheme + } + return undefined + }, [darkMode, featureFlags.isHalloweenEnabled]) const persistentAdditionalContent = ( @@ -93,9 +103,6 @@ export function App() { ) - // const { account } = useWalletInfo() - // const isChainIdUnsupported = useIsProviderNetworkUnsupported() - return ( }> @@ -114,6 +121,7 @@ export function App() { */} {/*)}*/} - + diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts b/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts index a6c6c56ee5..d5f51ffbe0 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts +++ b/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts @@ -1,6 +1,9 @@ import IMAGE_BACKGROUND_DARK from '@cowprotocol/assets/images/background-cowswap-darkmode.svg' +import IMAGE_BACKGROUND_DARK_HALLOWEEN_MEDIUM from '@cowprotocol/assets/images/background-cowswap-halloween-dark-medium.svg' +import IMAGE_BACKGROUND_DARK_HALLOWEEN_SMALL from '@cowprotocol/assets/images/background-cowswap-halloween-dark-small.svg' +import IMAGE_BACKGROUND_DARK_HALLOWEEN from '@cowprotocol/assets/images/background-cowswap-halloween-dark.svg' import IMAGE_BACKGROUND_LIGHT from '@cowprotocol/assets/images/background-cowswap-lightmode.svg' -import { Media } from '@cowprotocol/ui' +import { CowSwapTheme, Media } from '@cowprotocol/ui' import * as CSS from 'csstype' import styled from 'styled-components/macro' @@ -17,7 +20,7 @@ export const Marginer = styled.div` margin-top: 5rem; ` -export const BodyWrapper = styled.div` +export const BodyWrapper = styled.div<{ customTheme?: CowSwapTheme }>` --marginBottomOffset: 65px; display: flex; flex-direction: row; @@ -32,12 +35,19 @@ export const BodyWrapper = styled.div` border-bottom-left-radius: ${({ theme }) => (theme.isInjectedWidgetMode ? '0' : 'var(--marginBottomOffset)')}; border-bottom-right-radius: ${({ theme }) => (theme.isInjectedWidgetMode ? '0' : 'var(--marginBottomOffset)')}; min-height: ${({ theme }) => (theme.isInjectedWidgetMode ? 'initial' : 'calc(100vh - 200px)')}; - background: ${({ theme }) => { + background: ${({ theme, customTheme }) => { if (theme.isInjectedWidgetMode) { return 'transparent' } else { const backgroundColor = theme.darkMode ? '#0E0F2D' : '#65D9FF' - const backgroundImage = theme.darkMode ? `url(${IMAGE_BACKGROUND_DARK})` : `url(${IMAGE_BACKGROUND_LIGHT})` + let backgroundImage + + if (customTheme === ('darkHalloween' as CowSwapTheme)) { + backgroundImage = `url(${IMAGE_BACKGROUND_DARK_HALLOWEEN})` + } else { + backgroundImage = theme.darkMode ? `url(${IMAGE_BACKGROUND_DARK})` : `url(${IMAGE_BACKGROUND_LIGHT})` + } + return `${backgroundColor} ${backgroundImage} no-repeat bottom -1px center / contain` } }}; @@ -47,10 +57,22 @@ export const BodyWrapper = styled.div` flex: none; min-height: ${({ theme }) => (theme.isInjectedWidgetMode ? 'initial' : 'calc(100vh - 200px)')}; background-size: auto; + + ${({ customTheme }) => + customTheme === ('darkHalloween' as CowSwapTheme) && + ` + background-image: url(${IMAGE_BACKGROUND_DARK_HALLOWEEN_MEDIUM}); + `} } ${Media.upToSmall()} { padding: ${({ theme }) => (theme.isInjectedWidgetMode ? '0 0 16px' : '90px 16px 76px')}; min-height: ${({ theme }) => (theme.isInjectedWidgetMode ? 'initial' : 'calc(100vh - 100px)')}; + + ${({ customTheme }) => + customTheme === ('darkHalloween' as CowSwapTheme) && + ` + background-image: url(${IMAGE_BACKGROUND_DARK_HALLOWEEN_SMALL}); + `} } ` diff --git a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts index cb886bc82b..5fd13b7908 100644 --- a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts +++ b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts @@ -1,25 +1,81 @@ -import { CHRISTMAS_THEME_ENABLED } from '@cowprotocol/common-const' +import { ACTIVE_CUSTOM_THEME, CustomTheme } from '@cowprotocol/common-const' +import { isInjectedWidget } from '@cowprotocol/common-utils' import { jotaiStore } from '@cowprotocol/core' import { CowSwapWidgetAppParams } from '@cowprotocol/widget-lib' +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 +type ThemedSoundOptions = { + winterSound?: string + halloweenSound?: string +} -const COW_SOUNDS: Sounds = { - SEND: CHRISTMAS_THEME_ENABLED ? '/audio/send-winterTheme.mp3' : '/audio/send.mp3', +const DEFAULT_COW_SOUNDS: Sounds = { + SEND: '/audio/send.mp3', SUCCESS: '/audio/success.mp3', ERROR: '/audio/error.mp3', } +const THEMED_SOUNDS: Partial> = { + SEND: { + winterSound: '/audio/send-winterTheme.mp3', + halloweenSound: '/audio/halloween.mp3', + }, + SUCCESS: { + halloweenSound: '/audio/halloween.mp3', + }, +} + const COW_SOUND_TO_WIDGET_KEY: Record = { SEND: 'postOrder', SUCCESS: 'orderExecuted', ERROR: 'orderError', } +function isDarkMode(): boolean { + const state = cowSwapStore.getState() + const { userDarkMode, matchesDarkMode } = state.user + return userDarkMode === null ? matchesDarkMode : userDarkMode +} + +function getThemeBasedSound(type: SoundType): string { + const featureFlags = jotaiStore.get(featureFlagsAtom) as Record + const defaultSound = DEFAULT_COW_SOUNDS[type] + const themedOptions = THEMED_SOUNDS[type] + const isInjectedWidgetMode = isInjectedWidget() + + // When in widget mode, always return default sounds + if (isInjectedWidgetMode) { + return DEFAULT_COW_SOUNDS[type] + } + + if (!themedOptions) { + return defaultSound + } + + if (ACTIVE_CUSTOM_THEME === CustomTheme.CHRISTMAS && featureFlags.isChristmasEnabled && themedOptions.winterSound) { + return themedOptions.winterSound + } + + if ( + ACTIVE_CUSTOM_THEME === CustomTheme.HALLOWEEN && + featureFlags.isHalloweenEnabled && + themedOptions.halloweenSound && + isDarkMode() + ) { + return themedOptions.halloweenSound + } + + return defaultSound +} + const EMPTY_SOUND = new Audio('') const SOUND_CACHE: Record = {} @@ -37,7 +93,8 @@ function getAudio(type: SoundType): HTMLAudioElement { return EMPTY_SOUND } - const soundPath = widgetSound || COW_SOUNDS[type] + // Widget sounds take precedence over themed sounds + const soundPath = widgetSound || getThemeBasedSound(type) let sound = SOUND_CACHE[soundPath] if (!sound) { diff --git a/libs/assets/src/images/background-cowswap-halloween-dark-medium.svg b/libs/assets/src/images/background-cowswap-halloween-dark-medium.svg new file mode 100644 index 0000000000..9a5f9a9e0f --- /dev/null +++ b/libs/assets/src/images/background-cowswap-halloween-dark-medium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/images/background-cowswap-halloween-dark-small.svg b/libs/assets/src/images/background-cowswap-halloween-dark-small.svg new file mode 100644 index 0000000000..746adb3ee4 --- /dev/null +++ b/libs/assets/src/images/background-cowswap-halloween-dark-small.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/images/background-cowswap-halloween-dark.svg b/libs/assets/src/images/background-cowswap-halloween-dark.svg new file mode 100644 index 0000000000..1a9f7f9ad7 --- /dev/null +++ b/libs/assets/src/images/background-cowswap-halloween-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/images/logo-cowswap-halloween.svg b/libs/assets/src/images/logo-cowswap-halloween.svg new file mode 100644 index 0000000000..0ba5ccfa38 --- /dev/null +++ b/libs/assets/src/images/logo-cowswap-halloween.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/common-const/src/theme.ts b/libs/common-const/src/theme.ts index 96b82dbe89..254ebaa53d 100644 --- a/libs/common-const/src/theme.ts +++ b/libs/common-const/src/theme.ts @@ -1 +1,6 @@ -export const CHRISTMAS_THEME_ENABLED = false +export enum CustomTheme { + CHRISTMAS = 'CHRISTMAS', + HALLOWEEN = 'HALLOWEEN', +} + +export const ACTIVE_CUSTOM_THEME: CustomTheme = CustomTheme.HALLOWEEN diff --git a/libs/ui/src/consts.ts b/libs/ui/src/consts.ts index 306c1e3ac6..057f17581f 100644 --- a/libs/ui/src/consts.ts +++ b/libs/ui/src/consts.ts @@ -25,13 +25,13 @@ export const Media = { isMediumOnly: (useMediaPrefix = true) => getMediaQuery( `(min-width: ${MEDIA_WIDTHS.upToSmall + 1}px) and (max-width: ${MEDIA_WIDTHS.upToMedium}px)`, - useMediaPrefix + useMediaPrefix, ), upToMedium: (useMediaPrefix = true) => getMediaQuery(`(max-width: ${MEDIA_WIDTHS.upToMedium}px)`, useMediaPrefix), isLargeOnly: (useMediaPrefix = true) => getMediaQuery( `(min-width: ${MEDIA_WIDTHS.upToMedium + 1}px) and (max-width: ${MEDIA_WIDTHS.upToLarge}px)`, - useMediaPrefix + useMediaPrefix, ), upToLarge: (useMediaPrefix = true) => getMediaQuery(`(max-width: ${MEDIA_WIDTHS.upToLarge}px)`, useMediaPrefix), upToLargeAlt: (useMediaPrefix = true) => getMediaQuery(`(max-width: ${MEDIA_WIDTHS.upToLargeAlt}px)`, useMediaPrefix), diff --git a/libs/ui/src/pure/MenuBar/index.tsx b/libs/ui/src/pure/MenuBar/index.tsx index 852b07499f..5e27a41623 100644 --- a/libs/ui/src/pure/MenuBar/index.tsx +++ b/libs/ui/src/pure/MenuBar/index.tsx @@ -37,6 +37,8 @@ import { Media } from '../../consts' import { Badge } from '../Badge' import { ProductLogo, ProductVariant } from '../ProductLogo' +import type { CowSwapTheme } from '../../types' + const DAO_NAV_ITEMS: MenuItem[] = [ { href: 'https://cow.fi/', @@ -674,6 +676,7 @@ interface MenuBarProps { hoverBackgroundDark?: string padding?: string maxWidth?: number + customTheme?: CowSwapTheme } export const MenuBar = (props: MenuBarProps) => { @@ -701,6 +704,7 @@ export const MenuBar = (props: MenuBarProps) => { hoverBackgroundDark, padding, maxWidth, + customTheme, LinkComponent, } = props @@ -784,7 +788,7 @@ export const MenuBar = (props: MenuBarProps) => { rootDomain={rootDomain} LinkComponent={LinkComponent} /> - + {!isMobile && ( diff --git a/libs/ui/src/pure/ProductLogo/index.tsx b/libs/ui/src/pure/ProductLogo/index.tsx index e3de673090..e6020684e1 100644 --- a/libs/ui/src/pure/ProductLogo/index.tsx +++ b/libs/ui/src/pure/ProductLogo/index.tsx @@ -2,6 +2,7 @@ import LOGO_COWAMM from '@cowprotocol/assets/images/logo-cowamm.svg' import LOGO_COWDAO from '@cowprotocol/assets/images/logo-cowdao.svg' import LOGO_COWEXPLORER from '@cowprotocol/assets/images/logo-cowexplorer.svg' import LOGO_COWPROTOCOL from '@cowprotocol/assets/images/logo-cowprotocol.svg' +import LOGO_COWSWAP_HALLOWEEN from '@cowprotocol/assets/images/logo-cowswap-halloween.svg' import LOGO_COWSWAP from '@cowprotocol/assets/images/logo-cowswap.svg' import LOGO_ICON_COW from '@cowprotocol/assets/images/logo-icon-cow.svg' import LOGO_ICON_MEVBLOCKER from '@cowprotocol/assets/images/logo-icon-mevblocker.svg' @@ -30,7 +31,11 @@ interface LogoInfo { color?: string // Optional color attribute for SVG } -export type ThemedLogo = Record +export type ThemedLogo = Partial> & { + light: { default: LogoInfo; logoIconOnly?: LogoInfo } + dark: { default: LogoInfo; logoIconOnly?: LogoInfo } + darkHalloween?: { default: LogoInfo; logoIconOnly?: LogoInfo } +} const LOGOS: Record = { // CoW Swap @@ -59,6 +64,13 @@ const LOGOS: Record = { color: '#65D9FF', }, }, + darkHalloween: { + default: { + src: LOGO_COWSWAP_HALLOWEEN, + alt: 'CoW Swap', + color: '#65D9FF', + }, + }, }, // CoW Explorer @@ -270,7 +282,8 @@ export const ProductLogo = ({ external = false, }: LogoProps) => { const themeMode = useTheme() - const logoForTheme = LOGOS[variant][customThemeMode || (themeMode.darkMode ? 'dark' : 'light')] + const selectedTheme = customThemeMode || (themeMode.darkMode ? 'dark' : 'light') + const logoForTheme = LOGOS[variant][selectedTheme] || LOGOS[variant]['light'] // Fallback to light theme if selected theme is not available const logoInfo = logoIconOnly && logoForTheme.logoIconOnly ? logoForTheme.logoIconOnly : logoForTheme.default const initialColor = overrideColor || logoInfo.color diff --git a/libs/ui/src/types.ts b/libs/ui/src/types.ts index 17972a5606..02a65e50bc 100644 --- a/libs/ui/src/types.ts +++ b/libs/ui/src/types.ts @@ -13,4 +13,4 @@ export type ComposableCowInfo = { export type BadgeType = 'information' | 'success' | 'alert' | 'alert2' | 'default' -export type CowSwapTheme = 'dark' | 'light' +export type CowSwapTheme = 'dark' | 'light' | 'darkHalloween' From 791796d139828f3dd0657222cbf98a5ce93ff321 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:50:22 +0000 Subject: [PATCH 062/116] feat(halloween): add Halloween mode (#5036) * feat: add support for custom halloween dark mode * feat: add halloween backgrounds * feat: add sound theme logic * feat: add custom theme featureFlag checks * fix: lint error * feat: disable theme sounds for widget mode * feat: simplify feature flag --- .../public/audio/halloween.mp3 | Bin 0 -> 76694 bytes .../application/containers/App/index.tsx | 18 +++-- .../application/containers/App/styled.ts | 30 ++++++-- .../src/modules/sounds/utils/sound.ts | 65 ++++++++++++++++-- ...ckground-cowswap-halloween-dark-medium.svg | 1 + ...ackground-cowswap-halloween-dark-small.svg | 1 + .../background-cowswap-halloween-dark.svg | 1 + .../src/images/logo-cowswap-halloween.svg | 1 + libs/common-const/src/theme.ts | 7 +- libs/ui/src/consts.ts | 4 +- libs/ui/src/pure/MenuBar/index.tsx | 6 +- libs/ui/src/pure/ProductLogo/index.tsx | 17 ++++- libs/ui/src/types.ts | 2 +- 13 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 apps/cowswap-frontend/public/audio/halloween.mp3 create mode 100644 libs/assets/src/images/background-cowswap-halloween-dark-medium.svg create mode 100644 libs/assets/src/images/background-cowswap-halloween-dark-small.svg create mode 100644 libs/assets/src/images/background-cowswap-halloween-dark.svg create mode 100644 libs/assets/src/images/logo-cowswap-halloween.svg diff --git a/apps/cowswap-frontend/public/audio/halloween.mp3 b/apps/cowswap-frontend/public/audio/halloween.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7fde26b03377019f6953095510dec8202f7529e1 GIT binary patch literal 76694 zcmaglcQo7I|2Xg$f`lOU-dl~BMQQE5H#K7KU0PZdqxRmb6?^ZhRm9$VD@DoR82#H~2 zlr;2=%xoOod;)^PVh^Qc9?L1JsB3BK85)~CwY0HwbawOb@_qI^_~q-ch^Uyj#FX^R z?7V`);h0AKJTm`UZza$ERi&7FX6cx4!IsJv{mG^WxWkH+TQ$q-7guFD@x4 zEc$Qoe-{TR?|(1v=OM}R8UO#@{=a9;O^O2mU3e_f3)$rt5k}GgfIUFOkPYRBj{xI< z?H~{X05RqOb@*2VkN&q^KHhGlFsf!CtlpFVX z-&Zjf;c=zyo3R19tv2#5!gx^6Rr+iFS-QtEP{-*0q+#yM{{ik`>d z;IiP?Hpz(^KgDHq`xQ28Kdi!U^`+yiKx+2Xg@nhpXl$EE1VaBJSh+lw)78N4QOIAv z1PHWL)ysOWCM&u`PG9yc zlW?(Arp`oO<)`I6$MbBQVP40jylt6O~LQB z1XDqgEPrYloy;V*-Ks=)a_wI|GWsIPY@=n%pJQ5vAio^mG*nsL7-1=4M@s(k8Hn=n zouq@1BFXldcM~_$KZYxSrvwDY5kIjJ{3a}9#IO3dPRv>A6Im>h?q&i#q?{?!d!GBZ z$li2LCAfPCG84Kbb0}EJc7!D^f*fCKkN=UZ#tn(*nwk0PPMVg&@*a$1nfBe!Lpjr3 zx|s|qH^6{@TV?7~H1@P!biq0F#Jy@sC-1spZsFR?7*Nne6idv;m`P|=ND*9$Y}Y($ z)S;Qh_Q$4E+Wk7Q)ZOs8iY6M_hugosk-;Oz_(r2NCR?qj&{S<$ZuN)ke6e+!=j+ek zYKm(ux7%G9;I`8UOCH}^B0{)~w?_5tNHE8u=8biAZ_YCM>G#h=#4UB@uq@uxUWxAK zdl_4HCYKC9CTuF)9tfrAb>_&9NBq#%|0kr59MI)fvG0_d71;*+-Uk=4(gZn)izB)W zu)FsHG>BWrEDv@a+E4%(K%ha?3ADH_bKSv{58u3!bW9M7Knb0pCLTJsZc-kyDLL<> z9p)tQj3s9(*<1uq=R||LMl&K%hP3W_G3VP7{VM6cZ>9<&dBR!V(6qcNaJrE8`)@(- z1c&Kt^fgGXLj6;x{*TrVZN_n!OD_u;n6I0!?b1-8we#yrzj!xsq3=rMXz}smHmj#D zn+_2Ie>$%pD)z*^9j(R!_l2=2(8v?SHnVKxipN4C%yen;aocMluDRh!4R9B_G<}Fz z5-+cFx(_>R$s2)o1(aZEZ=g1RAZcTuU~}nOl=H)z&UXJ6$HKurITdcMf<&BMI~KPc zJu|zZadECnxoUbvy)`K+ZDUL~|1oCq#h-o=q?F@&wCa@D(d=7%S4XAaR*31Zr+h>C zS@P^8)~%0@zV}?Vwe(&S9|o3^>Z-{(YJt%2hiat~hRkS&C82MkMu+&>-2XsiDTd0^utb1T(+67~^BgLCgmh)!mxW*l+R#Yj7r z`(cZGjBcEE9y2aToON(Uarvubg<9CvlI6xn4asC|@t1!mZG6wKo-Ai`Yrr}c2FHh< zmJf#h>0a^NI#QA(1E}Mwu*Z5F1p+)Qt~OC62S*`KHhX^5KX6(b$5?i*qG$$LsRs>Y4-G|MnBjvkKq$RevdbuLx?yFLV(+P#H5V1g!B*Q zri(0TfLxhz%ev&19I~-$s_>%<@K&K_=e+OT9 zcFS@BjojapWOL<)7bTnvIs`n;zbp0>kvH0s-zJbK?I^zM(q4a~@}n@ewYLy+XKa*` zTitZRL6gYC=|@5?D9k;;@iNWG9P94Ka$)Z=Mo$aLf~Cf?i~D)eR3#xQ>o}GB5!FFe zL0tf0`#y7peWjX4wpZxVW_7gXgH|2tNy`+h6arvX?k7#gV@R|dKth}w3#MoIYmb}_ zXu?TDM$;=21?fJ*+V1NvCM!>_4G8Bn~CVxO=YIHhh~V=6+uz$*G{yb=^nz0d@{B09Z=; zgcBWJZOh1I05WEU&KAil(jBxIuq+d{S8~Hz_!=0}J}^`s@+IcM>OPn4q@#C3O%&*yb zY+itGkH0x#5^AQ8%fDr6?m(jW_a!B!`73qTaq*1uQpM(TUH58?={Nv_AaM~kci0&r zX}(wL3HUM3$Rx7rGxMdz*Wv7wzPAQ|2Ot{j#7bC)1yIyl8?Z)H;zL@hr zv2P}2il_H-g#YZvL}f5D>`xAt65H)D;jfvt`d5~9TQ%I(-?UhzKQEf5j@xB)lUQFO zd~@PS@Lixz^>a}uy;GBEf*FB4WTJJKgg5Bet7DLZt<;MtnibAZl#c6jDWN2h&X8Iz z>;x@=q-08dL&53cCXoc9d`S4uA~=v)-8RP|3F>A;VJmF2`{NL5_|oz$!OrxMPgf7H zmnF_LxlC-efs;JWE6aaElK=%Z$FS%pbmId#a14me3#yeYP`!iR|BGMB)LS12AtKtR`# z*k*6hlh=FbCPmHdvGLTSB|9*=^rGSAo1wQ?mT!aWAMowc-(&=Y5kA)whSV*#eqc&x ztB_LS^^E$S0D!RoUJh+T8SD~G-w1MW1Rc;g$n%4-oFiY;RfYZyo{i1S5W5q?y=XmP z)$YX6_2^xSkvP63h(GQLXMmpUq*3SqZJ?>Ujp~%Q@PNVWVvJLh?Z*%+FG3mg4028MVB z%@Dk7hXbrbWF|!=WyLIPAh3V87pgZC z4fF$U?=14-C6#>t2_1kywY-B7pMrdZkzk+uZH@SM1$+IzBRT`HL){ORH#tRZYqLmV z{`S1XIc-8@NVfLoXXo9m=`WL*sO@_!M0DgXhpt!m+QeAi?RhTR$Xt#*wK?r_R6QK` zK3U#$ZCyD9>)_Ke@aP+NtteS${iKSkqhNL`{ZVz`#!l8o{CI>niSDJL4Rd8Rzr1ZS z2eo^=>&t6_g@zrqauqZPc$(7phFqpF`SwgA=UMQm$!m!}Ox{kJQtb1Bj>&Qf#!Ft` z3)I^jjtpr{zU=qYsp zWAykYQ?BVOMnCM=N3^nmx*80|q8C1Ct(TY0kBqREU3`r(7`sg}Szhm+$qXSbb$d4= z5ttC*aqME)reLm9eX#jwZSCqv$NFT;0zd))bpj5+GB(OVM5p^==s;CVJxs6fJ%_>$ zbrTDP+B1F_kPCa*?SKY<{s`3@pbgG;q4+Nd2;-2-i>ZxcdVA6&Xb>p9$?!(lS5W;w z&D*a(zy2)$&!hgI&=q*;wfa!Yy5di10E_^!S(40_vFq5rLBB=*FPG!Rb-tj!W2(7w z(5P=tFZ=1aq`VsS){IWdYyU-D|<7LCq(0J#x`7o%dF|J66Jod0dPEWo7^7J=UB&HxCLpQ~>q_v3Iq6+3uAl)i^G93vUA~ItVzT$f#a(dPAwYD*yWAS5; zRqsxUpI}COnVNYVf3WlW8hwIrjpm3KWfgiw+LTKnT9p@Um8VPoeB6I~TV^NSaO7u?qU8gdPh>D=cqvNkhB~pZN;DT8| zR0IvP43uObs((T!*tlhS$`fmfKShw>sQXNTOjwcS`^NyF3Tan*A46Hu4&X=%$NIXc zZtQj*2R_a{U#gP^jAh;H?w47wr36zo4@;>94Zdym@V;x_p1;-Y7A$FU4xQE%w3@E_ z@1~C+f-;}&SG!A&nwago@U;Cwt@>#-RHune*Oq6MCs&5A5~*6G|DKny4^B7xTWXV| z(B;HJJLBl9mz8U`Cz!*-CCyT;n`$@+hnx+iFZP_cg%L|%*s`lbZ^*ZX13xI3s}t%8fgd`C~(vP2u&FeSH7o<6xk${AR);cGQ9LG2o$ypaB#Sme%} zp~=x!X%2w>=C4iSv|j7m_dDAADO#ib25;(A+U^$ughlXVnz*SjxuNrDYu6;5 z?sNB25(lIx+h)krwdMHn@Za_*QK(816NKsd9%{=0<@lmWmjv}#GhN4)8daGpv%ELFc zRr9Z?^2&ezgqES8j(ef?ARj&eTzhW<)2ie0+TTa#0c@!IT<&-L#VhZDFpiMjyTX(s z2yJ+svBg%$=Nz}5zvP0?H&90#Bvnd04}k;6gR8Kmq3srakJ&0#_ZyrPkj{P*eLV_dmW5InNjIL?3(kAV#N zNbwbG0y;B|zgI$Ml;$Vmh>x84v2umwrDw*tW<0f^3ddiw-_s2d)af|Ts z!S@dJxsGbGjcTF+DCEDwV7p9FYV5c%C{9?c$r-r znV)K>K}r;bQtBoa3|p?a%t<5e2ei?f0K9{>6?umq0on3KOA~R^9?%bn& zfG}RV<^K5&Hl;v$iT_ZRQi^F%cEhyn^J}Tgz^#VEpQ;;+#w&Cu#$(H==B9xh8OED! z4;SVaA5!@K-N4MhJ9atsWPw#e^F(mu2Wv_4XbB;aGyrW97HbsTkcKF{Bg~9F$0#f) z!i+wL3p)tOf{PR0npKpy{xU&I-ZOV}j=CfHEe$;@YidPR9!5Q%;gM4s7{Ld>yJvCbDWyP-B;^+8W={8!?@4IiYvZ{jq3n&Fcuw!J^kZo$bX2Nv#e$FNF~_{86vHnqYBV^ z+F_u^jL()>x9J++QCu98JeGqjCA* z8#8J3PiP-{(Z{Vp-)?@!BS(Y0C!wQNBo^`{K~Ew7C$y{}?o(R~#FfQNyRz;u()06U z576f6u!>7Zq$qBfl`v)xb7o2THy=Sy93R^GKUy&5a@n^k9or0;C|IfWa@xqU(e4U- zRCnFku`+ngm&$4!ZSgmzKxCU%B1K=aMmiMJ>y)Dp%47iqzZQX&q19Q0=VRlP>s#+5 z`z||}yFM-VDB9S3|AD#NN=L~7AbPKUWKdf1oPh;)SU6l}U^XU$KEwjH3ISt=w(8qc z+XvE#RP)?@UgWtTuoR3|3UeY!1rP}u?D+Sk2-qVZw|>tVsg6VbcP6DIt;!bmY&C>b<#|mJ=NpdNvZsExN7I=_n*?C-jE!2h zc%!cGGLfurwFISk@@~#+GYv|-vNNBv@O*4}S(<7f$mEK>`5>h%c0EJjsJavWmq?5; z`|<2=X}G;;0wDap23}wSALW4mCW7Y}kZ(&teiB$H*U)cNC@6+g`$wiazM&o+*cKUV z|68j}9h{fvC`MHotNby_d7H=T!MhwISoMaKt8 z){ic*kK3;rrx>i8G~PN2Vla25@^v|Zf$QGI4oUCaA=uPpT!{w^UC&m-c>FVtTgq?}PL0AzG)DyTZq3954 z*|fUc_p*LMKuN%7mRGE4EFK1$#)-r!c2$aKi4S159x@>df~P{I?M4R#Cuym({|Ox? z4R#3+r*{PVh$8-XKLnG^XVTf>MYl$Nd6Q5ITYtwC1hqzs28I@7xqwmc1Pje=@W6juclFhYL$_s@fM?O-JLy$e@rqDy@`4xY);D5kQDJZQ_bY=#P z8?+Bb=vSC19(}kF@_#b(w4=9)ELlEf7*9VfyZj}EHicl0VaaUE5UyySB!#*@6AK(( zk`m7NaCs6Qp;@q=f5O@I>$11!clV#Frozj+EeysHM++>cHgY1AQ05FDs)RtB6jKIR zkXrakFf^PrK>!Ojyp1#If*lb9BAYh49Ms3{H8zIg;qWI%@<Ck=tb zRmj&Yd_)kd^Y-2V>EAgDAzNPbK-8BvZ)0G=2&ZR9t?246Qczeri9LfFqzf+=lxt74hyxCpqw*Pd!pJV=ArG1t-T6Tcis*0`F zY%FNz748(aO3b#0|0AL#P~Bsw(eV!G0WJs|6fd5c(E3Kco{A2_fd}2BGmjCpjU;3Z zG>;#hS|dNk`@a}v zvHYe&F*WTlL=J`RKh{$q(zMfy-$2=haZ(zA#wOWX@Yn>Hj43ldH5+TlA&%J|;$%8t z36+L@`!V!jF=_Bb#@E8je0a}#h%?scQB>LDd2X4;F<VGH#ed zw=EPIGw&KMN%4CCM)F(%xk<)0_5pk*_F=OtfKN5$H=S5J3y9cL;!Cw%F@64O*{9pSBv^#|sS-VT)&Yu)qrvLndQ!%ge7dA!=02uZdAe4p* zLpo9?a>c$GQ*`{fD4Xoo)6`yD+TtaY=0R?5Ju9M!B?N{o6*+_$qImH7|{T%k>I<#xLg zGG-kmrGQ2*o-v`qxG<1lrLY>*lS2F196Mix3qwcFwo>CB+hxt^EgvQ5iE35$kMb)( zx&?bDN>qiUL?n(@c!iUkZ95Qt#nFO=)`NTD7B?osb!ZLMJ_O3??OY9qsTqswlben-YVc<_zSLsRk?jQ6Ll=bT9MA@jWEt{wK5^gL5r5y3i?q zCMZ9;=6T3P5>v&gv+s<4AI-j2PLq-Vg7d?JJv~FnHg#B0y5^xR^Mx&U`7OGuS6!KMEfzk#ck_wn4MRG?jV?nv{Mzy&diRC%Ed; zWP`)kV|cJ4`7akew>4Knm~XbYJVH;$XB+NU8|F_0pw1ye>9hQzB%CJZT2+gXPG1sH zB%FXXsFje$EUiL}N5FXF%R1rPMl?|QV_wyH^G{a!KHckFi;Q%M%7pTSpPZlAIieo4 zy)FBara;E_>f&Ne$P=CvREXI?MCx*-9=S6(JXBCRaQd46u1R=&F8%hPW&K?R7=~l7 zn0ocmo`BOQHFG5}tDR5(tAyq8mpZ#=q0ia36B)Zhe_{S4A(XB|7IUzm!g?W-iNH%J zltT|Hd{$2+k~LHKDA7O--2&j>)CEUz7@{+Ur&2?4s?IN`uoG(?}ry=e=~%0=B6X`SYTyD#&<$! z>C~DMWRjO4Fk&X&p9Eg6Ah?+w?3^_YTkit{pkSxBYxY}Wnl$2g< zhe;SaS=2A_)yU&5`jDNvmV`xXyhIgOzSl2pN1hvuJi1)qnqlL}eXk}m;hOtk)V+B| zUb>3?mRL=Q|-l*S@eM7d*^s9CM59B080uWaqE7I zhi5BeBr8hsyO6A4K?w+hUsFZkqv67|IEKJB0Eg2m62ZZ6Fct~{tu_&yo|S7dpbTjJ z$wCZ401(;)J-8T@aX{ZVZ7jFtNDMh>Ox*alcH(2Xv!3Xek3xaW&x<6p0!2zu`H6?G z))yLP!rzXP&L;Nh%_~qd8H21nOdbSI^GjjWaJ4a!!krAf^Zm{Tlv5jKIbzBq^Qjd> zwBqqZ%(t>z*MIoI0aV0}!W;@}q?D?BhS^j-Qv~KgyA%ADXX99NM zFYXXBN`@;65*KfDRn+cS8ERA#@g=8l?KjfG|N3so>s$Z;o@RU)CS_HFDz-q+RD=Zx z7O>}t;1pOXa8<0Z0wwYx;I_Pa+2+7;Bcpdd)~yYFdSgijR7&Px75QJB&eJ_FI!BUe zE(mzt2^Jq&e4g~{_t#g4?3HTxY2V1cDov^tn^e?zklcEDjjAK;Ioj!(cq2yOVJzVc zr9??L6H?Q$OI$L)B4Cxs32~tK#+tJJuBP?#g?9h~O#x^bL}*ZX~kVXL*U`u&#KXJY8o7xk&EU0q%AsXnO$4^l0As1`nM{0D@7 zB{5%AxE_GLup<~*#qv3a=!eqRFLxAT^6x{H>J{PrBMyexBZTMTxqELK28Fx4d1_z# z$wvkQ36DK9?~3#E+(oqAI8ATpGF#iQ);gkkq41fz1~?MrClT;Gi*(eQC?ehm))JXW z1du`ECH3S4%kVz8S`QIb5G!VuWl^0QfyhK|^S5jxlp`zu3AGVtOgvJ{pJ8m|SCS%k z>6ald*k#h$_D0vkcGpT{&P0JsMQ~j5BX{dw>wL)$7U}w3jZ}FF%#Vt@K!Tvc6dWvo zA|Lk+*CspdpMn>&uIOk$DGi?#tPxEk!MyYF=WxTs4=5i^gYnquZ#Idx68iCM0_o(F z|6pQwD?{QV4kBN=3an+|q96B=Ios-d?x7`sVv7q*|ed#a)#4!Zf}qkRJAJcTZ$m5Q&jjI;qG@Vyzm`vU^v9+z zyTcqMBe=8mf3|Kh#Xy=OURx@B7~Eq?`xl8Mesw)ndh<`H0bFt*pwX49eg@}I`|TPo zNdByYPRO4P-ID15DUJDAoacg%m8HhjSVz%AMq5}}7)5L|(T z?=#8WUZ``B@R{W3wkQ=ahV4{C3J-eux=dg*u)HNzten3uW0DMJZ$GSrTUA*LoEEct z0eV=Z8moWHf4!>_{TOARy6zaaEUr49Jj7VMQOrxwG@+E0hgD>G|HPdBxHTGH zQGVb4J14gs+g@4Gw8%S&ua9r*^CN{y3bumMyCx2WOZjoW|V# z8d|(IrZ9`V*@;k-?9R}Z82zz74gi|$w0~zok_CztF6jebK$D^OC`1Lfo4Mui$kQvc z_$aFfwQ0;sWA^6#YBdhasS5kGDXV?S65daBt`!Xea9?dl$o}j1VJuN*xv?3>nEk>B z^9xUkckC|w`NGQ%i3uZ>VVn5bj9Ok5HH3h>ZbdZA;R^q7NVMv`AhCLyUQu@zzk5LrXs7 zr8LL3tlU4T&f0JWq5Fpe!VhtL&jUUVgzS2}?8`Gp6S7R(~j3ws})JztM-qF(T68#3;1U$m_fjQDx^u zLuLD-)FuMP^LbF7hlkdwIf$V=Q=HqzH%7P^C6z6i@(Z@m&6L6UX`_(fZ5Ge4VyaR5 zNz)tO&)A(U>FL=iUCkK7A~iOgX)L*C+Us=qDf4=ha}wFw>TD4yn}gn6nBEHiTjpgo zF%;~-$kifoH&UVoerNiXq?8HMn>cRVMIkx0w=eQF?g}Dp$uYfmMpUei8oCa!hQrN9 zwHYSe$zs;q2`7>AAoRA_q$rx=e!foMP*Txu+VFoNG>|kX{&1AMf&5IkR0{h(gh-51 zS#%Dg(dZQWq4%+FK+HIn(tT#?VleSE-kFYVXf1BcVNZ?jXvAsqDwdi_f*(3Tgcn77 zkh3LN;@@aNRKj+$rbM!Y!?vSmU+6J|vc26%BBOW+e(J95dQ*ofO#FTw`o)03h3@wV zhGP1z?IM2n+p$TQdd2odm;-pSmeb6W9&;TUOe+VW0f@(07`O&U(v-EZ{7Ne)#Rgiw z_Q0tJ|cDQqcotcIdCiVzcZE{k4Rd?KJaBk9lR(GD~snZ=9kS9f3Icl&OmmK zt=(@-(bX#2*1aTaYRr1QP$bxqNw`Ok0uQ-b))ZY zDBNk~51z?lS|jt3#2bd|Aw#?%zl?l->{7E($# z{K$oxRh_4xw&u5bI~5&^BC2VOYD?^LWgx+O|Ma`)*PdYao8JL>?G3}BGnf_W3;iIr z{T!u@bFN++jQuTha3B6}JRj|GXa|nMF|LRi0bb|Dr;9P#p*r^ggkb&Cusoyx5)#fI z(WjT_lj{DW=o@5B#Eam6Myzc+8Z*ovoDkAyRLz2%m14k-z&odaYv2uF!|};Qo<-x+ zXfzPMXL&G`mA$ks5@Q(dD)i-hSVl*bNbytwng!)tNM|J|f0_fy04>z5fM!2dXsB@hiheOnDFUzQ0EQTK;dTzR7=&BD%mLmL!adUn-v3fRN1U*%}7Zfnh)h)|ygd8JVx@m{Trd@5h6(KCH~F(Rm04Ch@% zRYKk#7h@G&L2h<+3x41Ywe92{9KQ;aIj=4MEbaQg#F4F=^gq9c#2XF-A`XLz>zXp- z2pX7o}QvKMJ2RTK6+ALd+EEdXGt$I7)`xkaaWh_G@ zO^dTH!egJ{a)kAIWakfPV~P0oKOYXg4S4a?32f<@bpytO`{rkPxU-Q*MbqF(;8gR; zmNE>4{|E7YS6j?(3((9Rj+$D$6f)0;^D(EA79jM%i$pGE*u2%&p{TW_`y2G5+m~d3 z`Y14{Cfpfg-h7JBEkk@%Vb_Q+DI2-kSl3RsvW3*W~{KZduS3J_m1{xvH zX!KAl$5llRD(;n2o(#e3UoVO$0`7V=8kR6i^#F4VnY#Teh7z|$oTC>PbQ<=;*rM2q z^Gzw}Y^KM4e`rX<7K-_Xh~@Ma!Uz>dOYyBF!?&~R^1UZgDZ`DnJL)n6>TS)xE9`w# zf6L~_1_RZAgh1dh zE(L9&5w>-~mjf|s5wIm44iiBg1%zhxAv9GZz|b%}Fs^l=GT(s@^2+qr`)dXIl(z7_ zoL}LY&+mSby^^6L-)j4FmWR+-xtzfSof$^F8D|7BzQo#JzJ~f(K5@3QUIykc80Rb0 ztS%y=#jQ?+JQX>j=f>5-CC5;VK1{B8T#aGM`Z#L5l5P0e68);vOh~!VSSj>RH5L$F z|0xW@)tVAs&#hUT!{g=%>w77$fJ=eE<0Sbf)CwNhzfTl8)z5_OT)ueYQ4!@d@PC(? zkn4xoD|d6~$&?gjGTah{5QR0-#(Wb|^9@;V{5g{G>CsQuBO>Aj9jkP$g{{aksqP9f zA*^$m`J9WN?8?}{;{mSej;W^evFE%WLS(I{GM{nW#H>%NwWDdk;mFt$T6)a^dHf1} zCl!lk_J}_Q7yhpvQZSntKK=V6TO$0q!m0Gz_clqF^NZpTfGQneX%ZIHN~cSa5&JF1 zE}1pG$c2kVz!BNk?lB%WKY@#^ts9a5&_1($dG&G@pDaK{fq#~~T469nl{Y>=OcIi! zOn($`L`ez2&Kw5mMRfq*uVKUa~O`KCVju6M&26-BmUSyR!S75 zV-jH_jYT0Aq+wh{N_Uv$5MLPw>*uIyq7-6*ATR5J?_3^XdCP9`h-Z%A3v#BUGy zxH&^IFr4yRJ;%T9tJ)Z?%o^^NTA?rFF1|oz$r;WBBK6q|E(i zYQY9ZYIImB7EDF710`fsIv>2TMKVtByQrvUWXPM;9Fnh`V3}yo(X|Kz5+Z(5Stl=H z>LgDOcOmfQH78Sj34k|+lQZ)CcG~zC6G?H zaHlO!m4-1V2JPaLgF;X|B@h^lu}4;lpCdf<+jZO+$#BWd}AHPQRLt=IpoOrJ>=f}0CvR~V-bupih4=ZU&kw&d?Kr|za=OtjQ zA)Wm;=#$wSj=k*W?wnt?8M)w~7T|5wDxa83L9IMbzT?+K{>xb9H}Q1$X@kgc9HF;V zo*9Wk?u#3S^@2#b;66P|$R%cOk(#|6A6(AA>!eQVlBRfPDZ zp6(UQ5K-;6fA!z!!2D=o?z8ID`T7%+(g?~0=}&d#j{arh;%^_s=$W{Eh<97K05!!$ z;ip0fpF@?ZOg4P*Qo~V7tKr`65xg6}N}Rv{@pk@P(b%sjPl;e)d6EoBygTjmeM1`c zuJOP5vz+Ep%v!ah5v5>8DO;=V>m>OFJ(~k<_yo4097I~L|TD-tsQbM}@|4qp$WEBSEC<7fX*=!9VLqxcB<#)~s)yBB>PlV-$5sT|+;qtPE@ z?ZYNZE$(ZkiwW9Z4T+7vl-a(E)0+=prLnpZ=Z5*LeC)6fd>R~2ax9z%!b-f!x|vfA zl^hhhU!_&nuQ2I@`Ds<{H4iYpN^?3m64O=?f6+o3)_Zi=IVn$_ zXtzaN!Z6d-G4&Cm)u4!$=Z(c$BTC2+HA-Yzc*3v4-@%p=T}=sUQNI}&X@%v59PWzX zrwSa9>APvOwxiG==Ss#pswz=hywZA=uZ&*T>^PB)G`mnUKBzZ7KdmpxH@d)Nued#a zpnp&|QPC@*W{2PDDM4p!2QhQUIAqRQG5DiFlnn6_pR(j>Cp6xE*rL~Qk5M<*uKs|E z{QRlKLX?}kG*zVV&z_TaO#<9LR)7y6hy*|%X2F&S%SHq|&ozM9snX33iKgkO_bcR; zYlOBIDRa(>QR^y3J2{B+mWfe&P5Jy2Iwz)FRUU{~7d?9fiuvY>{Q`Dy%%F3BOyOgi z^W_EP@jMA#RP4uf=iAc)21<$v0mZOP<6bSbIQ5F>nA6+RJmsroACL~|iyGiEe*^qO z+{0k~59aoDD;OsOz*Gb!Bd#K8F#(5%X0vWMkd;nTI`Bzr&`{1VXSdLbt^bB}qF)8~ zDprVdt4-n$$_*=T5*fs-PF7bX+zulKCh;b@0QexlrGX1Oj_1pS@JcXvYXEca^>k%&0rE zLvfQ6JoKmDQ8{H)e?Ow+WKeDq~CY<`Q49H?v zjce~(w+A1p%9FpT=-!5tu*^`|6iw0$H)ep;3XL#S1kHX#hsdyEG|^UJ^Jh7-nPEF$h{H4~$KE6${H5&WF!H(KODwU;E5l-SoTp{gH`V6u2tr(j$F*LsRGz2O z`?;wbM;lD189(NGKfKIrW=6XCX@UN=@YtpQuDfTF!hes?Ul>QJ@c_?unl4j_LM-Q@K}GtXsc^aE48YpYNK&h_5zfp$ns zdiwWpo!||q(4S&1REhR1fw48in(Lp?Di{P6Qd{T6Ch5YQkfToFIzTQ6!2K8(AE-F@=8A#ax~?veVfdn6$Wd>~GsN5_@jl$Qi5X$rCO6 zHQ3$`j+@}iI=u5TFyf7-#7;_8n+A{EPCddHr8i#w$&JTR<)ZMWvv&&xj3UEnaP;sv zw5&k~#-MfM>4Y*P5>gVWmwVmYaw@T1J~1xP*HfF%f)~U3vrKPYVOUm~KmmG6N-Gc) z*(dO1(lFc-y ziLM8e`xUdY|JkwfL@i3QrcKeYzS@`oFYyK%C1I;*o0C3S3BGSQsg6-Zz2oJ6KfOq(u9-^w!3wAH8#_q@*)@%)5r23gjRi`c)oRd`vPSNM!}90#x}u8@g{s&#L%)t#k3^{q?__q^jgv5GsZw{klfF8 zhsEGQKTw?eyVJ+!rBuo{%YE2*l~HM9unSWP^+fYp*d#6+&-iKhimR* z)5XHS`SrzE0HA}w1DuG!M^4tvPT(*z=Xq==Py`)98h;m6&w!1k_elr~83U_*B$3rl z{wmjC^|3XjI{M4&Iz-q%p)CT?s<_I7Rq`|V7jrAucn(<1H*TGe-st{l&erVG6eb0U zVi2DUk=>=Qm|CG0@6#nR-uVY=Kgo0#6*J!&(u!tbzu2y|jjd}Ti!$B~(`Fn?v^V?J zG@mUT5QHf#$Thz8=4EJ{IXUdGAZHH1{IT+0(}vc`QOKb=ib0S#?$t?I&?qBgvZepm z0j=kGwC(w7o(P9;AG|&HrcC!qJ5jVHSgD$#qw4GCyCkfJacFij+K5C2MFPlAX(yUL z$HLlUMOcv$V^VF?^W+UW&(*9B9)IaQ|1v(?SScX*CG{n-3%_~1io~j(QOZ)T{K#9) z)<-f6LlxuQK;a^-I6jGP20wl_KHb7alg|c2(yI(&+R~+QR!vUp&osxE)wkVIpvSDz z4k3y0!nR~hTwe?bu_A?Em&YWjFZfnPfQE|0PN_JuyfHQ0w{H@PA~7of2^fZ*ws-&k zNZn<8__xYk6b^(~G2ad_n|H#Aum|JWoI&+mnF!8%6`0dFD0p4R3}y9iC-yS#&&$hv zcuv9h-J!vtqm@P{j_NDtKcUT}_xZ}IEi?3I!o{vs4r2x+{BOCu4)>5fq#Q-}?V%;F zUoIiWFfqDUcrDMn;=nWxc(2I7kP{^?7D>mOKQ09Gl|!<{nqGrqj* z`0%v-*@EuSVBtP@fQI2{-z6a=2lURrQ;v&td|L&PMHmJIBlvKuL;S1yxm%vm>O11Q4*Wlo&cmPT_x%&j0e>wkPXTR}0Pe5!1*b$H9eK;SU^(G8_qb9`2m&b(2C~f0dXbzvl;#3xK-9_fFh3Y?HhN=8nqnj z%fK1phfK>pjERaG+kG;uFO|aG7s4jz3pc3CaBd6}gq;K|{hG zMsN&fIwq~`?@*s1c!0%lw`cn5R=Gj{C!gqtFH`a?IWG~Tmp)r0P=O4j``vRrG zc_C0{fW6xVbEEQ8f{yJI@v28$qdZ>hN7uYII*hgZ?20a4b-`uj6JA8uXfAI8Ibn^H zn+Zm_L+UCTn$y@vsGZAth$}MELr{2Jy`bXv8(uNOk4%lOH1Qb{P+VTF&z>G!k}}dy z)U0E??ykQP@buh@jQ*DUVpKyH`I1C~$4GF2bT1;n<<5g)vufKHYL#jdiG!^3Ep=Zh z06+j-g*aau=t|(yI4X5b-4PeDNb?Hsp(0N#v0W`yeGLryvagZ!^+L=62Y@AsJgNea z1sZ5JrY?hsWKph+B`YNEEWR3G%b>fQ;CnJ_KWEl{bzP*nJKetO8NPO7xOIm;e@4D= zFMHuK3;#___Xn$+X7}aIAe*lGuQ~Hv>JVbus|P`7KC{4ogr-swV6uZHFWe^NwN({7 zjOY;k+~>@yTe6?St2YX<6F2{?@0{BjbarNbXg*Ma=?io%AC z_qH+uM0lgXf9AG%P!%Pw1(5q`IZO&QD1WRQDV`B6_A)hf&){izhs(LD!QP~1MOC4) zpT3OBeRJ2s4UqGfUSvsX-`xd(tD=tajOI4O`c(|99_u;^EF*ciZfv_duHLQuOC-`c z%8%IHlVyUgjA>4X?9J`&W~x1_INaH>cns8B%yH?ik5Rt()_=(|o2?QzXgX?Gl3Ih3 zx*%_@j4Yh1C`F;e>wgFf^Jilm|$%G63 zr;z2Aupoy5)mJX=k#Y{ZU$qVfZ>%BL%L#K#-2o3RvIB6>#$J}}xYlet-VK=68|nU3N{&}oBSj6J*Cf8d9b7DQn|XW%0%F* zDa&+$P|%hbWj(cOs1xrX;AcBT`FK;oqBQ1ZOyj*)2VY>dxxR*5;fk{O*{toRYd7PxE z+>SFJcJ7>w^?F2~tG_&8Bm>99^LD)4x{@LZOJeet^syIA;`HOZB8#Jg#nyWIGExDO ziYemV_d*W4QJi?7jE5PLEibp*D zs&UDKmu!s=?zkN+nVF>COSe1o&gvX5eYYX?_OL`k!fa-y2j$&K%sZtNM>nnJnplU7w#U(229uybsaD^OfYKU<4;y z)N_nQ=oiv3-O5H2-r*?oFp$W7zV&0=y8RQ+vZ(3_mVmK_Cy&?lH}-8$GtjK7X%_P* zdliW6MTeR6YZpE#pV!JNxqF~V`*<2f0x$qlu#!}g^D~8zp>%i=zTuu;deiaNt9b0ex)zRK*^T_Xc$&KP>qre5$#%Q z?ujLX<$~3~II-I^DCQCT8Oflw*(^HNH2!|qs!b6dv!IE}3UtiE{gW;-B?FX!1zWx$ zLq<6mNj)hUr)g``AHpCVlIJ!ucH=D?s9sh7VDrShZ2WwJE}KNKjbR_fD0=Of1AAnf z=r|}gsL-GJoGRsF*Nsny_(##}oc#tX*e_IqNe=j&+>ap~P{~JrwQ$F?y z3!nX8%<9#mTl0TnjL+!_+XPz!8hIe393CwTG1iK~*U%|k3PIxAU=T3n5YG$Pz!nh@ z;CV`Rc>=g7gHeyjkxEUQa*-^5`rnkRRonlj4|`dOCCa*xS*dM;uOpkTJIx1B=36&zI6MCx-T!+yKsy@T!Nm=Q6`BV!WHH_QeTzmrqyU_N90Amn z1D3SMx9KM(u)F-Qv2`Fs)X3;Qn-94BTD(}f$?DTat>j$8cQ2zHCP-FQYd~ zWsUxcJ`3|&Nt&UpTNDb)knun>j^YltmmH4X308=kmMa;V>qNT&s9a|1CDHVjXMyF9 zBH2 z<5@_vm%7j@TQSr>wF6au-md~c2JkmyPyTqEnwaKYITX(Gi{G(to9-)n%$$O$+|Kc# zn2?Q}m!*fn{Exbyg}Ziu1O%Y)EDJMQxQtxTKMl>ZBs89OPTyajz*jn|`3V^!2R~qgTJ0$m z22ZmljDyAG+p{(PQ5@dspz10Eq$SxG%ItP^T57^^RkV%MYl<(z-1lb%zuO&{ zC0ei$kBgCM`>R$O;;QiQ!p0@%6O4bhr$B?v7xOP0h70G8nJ!Vl06SbRLFo>7ihe0h z{;F^ugw!a9GwlH&oLQ!7a5*Rvq0Gn)NnwgZ7E+4Fs#k^7G)#pbdF$R2mmG3+mXh!- z1an&TBABIw$B$|sw3x3bc3!M8l;!}F(rktafukCDq8j_K)KHlZOukQcD z%ch%&Ehz9tsOt@yxNQ+Vf(|I}{n#{?Ot9>;&1RmahWDv+j=J|ckVP_72l0zbjxQ>lE3ymvJbKY+O{RWdW9+F; z&%Lk!F)14DSO@~xf_1+!EcP$hRSf09epi?zZcLQ>^*!+RwR|MKv7L|i;o#t%_`ue} ze{xcI63Pg`0RSA@%c#AKtT+wk>+q%793?GUt$Ph75kM)Ff%0DMax^PEO;OY|Jz~*N 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�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 { + if (ACTIVE_CUSTOM_THEME === CustomTheme.HALLOWEEN && darkMode && featureFlags.isHalloweenEnabled) { + return 'darkHalloween' as CowSwapTheme + } + return undefined + }, [darkMode, featureFlags.isHalloweenEnabled]) const persistentAdditionalContent = ( @@ -93,9 +103,6 @@ export function App() { ) - // const { account } = useWalletInfo() - // const isChainIdUnsupported = useIsProviderNetworkUnsupported() - return ( }> @@ -114,6 +121,7 @@ export function App() { */} {/*)}*/} - + diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts b/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts index a6c6c56ee5..d5f51ffbe0 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts +++ b/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts @@ -1,6 +1,9 @@ import IMAGE_BACKGROUND_DARK from '@cowprotocol/assets/images/background-cowswap-darkmode.svg' +import IMAGE_BACKGROUND_DARK_HALLOWEEN_MEDIUM from '@cowprotocol/assets/images/background-cowswap-halloween-dark-medium.svg' +import IMAGE_BACKGROUND_DARK_HALLOWEEN_SMALL from '@cowprotocol/assets/images/background-cowswap-halloween-dark-small.svg' +import IMAGE_BACKGROUND_DARK_HALLOWEEN from '@cowprotocol/assets/images/background-cowswap-halloween-dark.svg' import IMAGE_BACKGROUND_LIGHT from '@cowprotocol/assets/images/background-cowswap-lightmode.svg' -import { Media } from '@cowprotocol/ui' +import { CowSwapTheme, Media } from '@cowprotocol/ui' import * as CSS from 'csstype' import styled from 'styled-components/macro' @@ -17,7 +20,7 @@ export const Marginer = styled.div` margin-top: 5rem; ` -export const BodyWrapper = styled.div` +export const BodyWrapper = styled.div<{ customTheme?: CowSwapTheme }>` --marginBottomOffset: 65px; display: flex; flex-direction: row; @@ -32,12 +35,19 @@ export const BodyWrapper = styled.div` border-bottom-left-radius: ${({ theme }) => (theme.isInjectedWidgetMode ? '0' : 'var(--marginBottomOffset)')}; border-bottom-right-radius: ${({ theme }) => (theme.isInjectedWidgetMode ? '0' : 'var(--marginBottomOffset)')}; min-height: ${({ theme }) => (theme.isInjectedWidgetMode ? 'initial' : 'calc(100vh - 200px)')}; - background: ${({ theme }) => { + background: ${({ theme, customTheme }) => { if (theme.isInjectedWidgetMode) { return 'transparent' } else { const backgroundColor = theme.darkMode ? '#0E0F2D' : '#65D9FF' - const backgroundImage = theme.darkMode ? `url(${IMAGE_BACKGROUND_DARK})` : `url(${IMAGE_BACKGROUND_LIGHT})` + let backgroundImage + + if (customTheme === ('darkHalloween' as CowSwapTheme)) { + backgroundImage = `url(${IMAGE_BACKGROUND_DARK_HALLOWEEN})` + } else { + backgroundImage = theme.darkMode ? `url(${IMAGE_BACKGROUND_DARK})` : `url(${IMAGE_BACKGROUND_LIGHT})` + } + return `${backgroundColor} ${backgroundImage} no-repeat bottom -1px center / contain` } }}; @@ -47,10 +57,22 @@ export const BodyWrapper = styled.div` flex: none; min-height: ${({ theme }) => (theme.isInjectedWidgetMode ? 'initial' : 'calc(100vh - 200px)')}; background-size: auto; + + ${({ customTheme }) => + customTheme === ('darkHalloween' as CowSwapTheme) && + ` + background-image: url(${IMAGE_BACKGROUND_DARK_HALLOWEEN_MEDIUM}); + `} } ${Media.upToSmall()} { padding: ${({ theme }) => (theme.isInjectedWidgetMode ? '0 0 16px' : '90px 16px 76px')}; min-height: ${({ theme }) => (theme.isInjectedWidgetMode ? 'initial' : 'calc(100vh - 100px)')}; + + ${({ customTheme }) => + customTheme === ('darkHalloween' as CowSwapTheme) && + ` + background-image: url(${IMAGE_BACKGROUND_DARK_HALLOWEEN_SMALL}); + `} } ` diff --git a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts index cb886bc82b..5fd13b7908 100644 --- a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts +++ b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts @@ -1,25 +1,81 @@ -import { CHRISTMAS_THEME_ENABLED } from '@cowprotocol/common-const' +import { ACTIVE_CUSTOM_THEME, CustomTheme } from '@cowprotocol/common-const' +import { isInjectedWidget } from '@cowprotocol/common-utils' import { jotaiStore } from '@cowprotocol/core' import { CowSwapWidgetAppParams } from '@cowprotocol/widget-lib' +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 +type ThemedSoundOptions = { + winterSound?: string + halloweenSound?: string +} -const COW_SOUNDS: Sounds = { - SEND: CHRISTMAS_THEME_ENABLED ? '/audio/send-winterTheme.mp3' : '/audio/send.mp3', +const DEFAULT_COW_SOUNDS: Sounds = { + SEND: '/audio/send.mp3', SUCCESS: '/audio/success.mp3', ERROR: '/audio/error.mp3', } +const THEMED_SOUNDS: Partial> = { + SEND: { + winterSound: '/audio/send-winterTheme.mp3', + halloweenSound: '/audio/halloween.mp3', + }, + SUCCESS: { + halloweenSound: '/audio/halloween.mp3', + }, +} + const COW_SOUND_TO_WIDGET_KEY: Record = { SEND: 'postOrder', SUCCESS: 'orderExecuted', ERROR: 'orderError', } +function isDarkMode(): boolean { + const state = cowSwapStore.getState() + const { userDarkMode, matchesDarkMode } = state.user + return userDarkMode === null ? matchesDarkMode : userDarkMode +} + +function getThemeBasedSound(type: SoundType): string { + const featureFlags = jotaiStore.get(featureFlagsAtom) as Record + const defaultSound = DEFAULT_COW_SOUNDS[type] + const themedOptions = THEMED_SOUNDS[type] + const isInjectedWidgetMode = isInjectedWidget() + + // When in widget mode, always return default sounds + if (isInjectedWidgetMode) { + return DEFAULT_COW_SOUNDS[type] + } + + if (!themedOptions) { + return defaultSound + } + + if (ACTIVE_CUSTOM_THEME === CustomTheme.CHRISTMAS && featureFlags.isChristmasEnabled && themedOptions.winterSound) { + return themedOptions.winterSound + } + + if ( + ACTIVE_CUSTOM_THEME === CustomTheme.HALLOWEEN && + featureFlags.isHalloweenEnabled && + themedOptions.halloweenSound && + isDarkMode() + ) { + return themedOptions.halloweenSound + } + + return defaultSound +} + const EMPTY_SOUND = new Audio('') const SOUND_CACHE: Record = {} @@ -37,7 +93,8 @@ function getAudio(type: SoundType): HTMLAudioElement { return EMPTY_SOUND } - const soundPath = widgetSound || COW_SOUNDS[type] + // Widget sounds take precedence over themed sounds + const soundPath = widgetSound || getThemeBasedSound(type) let sound = SOUND_CACHE[soundPath] if (!sound) { diff --git a/libs/assets/src/images/background-cowswap-halloween-dark-medium.svg b/libs/assets/src/images/background-cowswap-halloween-dark-medium.svg new file mode 100644 index 0000000000..9a5f9a9e0f --- /dev/null +++ b/libs/assets/src/images/background-cowswap-halloween-dark-medium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/images/background-cowswap-halloween-dark-small.svg b/libs/assets/src/images/background-cowswap-halloween-dark-small.svg new file mode 100644 index 0000000000..746adb3ee4 --- /dev/null +++ b/libs/assets/src/images/background-cowswap-halloween-dark-small.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/images/background-cowswap-halloween-dark.svg b/libs/assets/src/images/background-cowswap-halloween-dark.svg new file mode 100644 index 0000000000..1a9f7f9ad7 --- /dev/null +++ b/libs/assets/src/images/background-cowswap-halloween-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/images/logo-cowswap-halloween.svg b/libs/assets/src/images/logo-cowswap-halloween.svg new file mode 100644 index 0000000000..0ba5ccfa38 --- /dev/null +++ b/libs/assets/src/images/logo-cowswap-halloween.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/common-const/src/theme.ts b/libs/common-const/src/theme.ts index 96b82dbe89..254ebaa53d 100644 --- a/libs/common-const/src/theme.ts +++ b/libs/common-const/src/theme.ts @@ -1 +1,6 @@ -export const CHRISTMAS_THEME_ENABLED = false +export enum CustomTheme { + CHRISTMAS = 'CHRISTMAS', + HALLOWEEN = 'HALLOWEEN', +} + +export const ACTIVE_CUSTOM_THEME: CustomTheme = CustomTheme.HALLOWEEN diff --git a/libs/ui/src/consts.ts b/libs/ui/src/consts.ts index 306c1e3ac6..057f17581f 100644 --- a/libs/ui/src/consts.ts +++ b/libs/ui/src/consts.ts @@ -25,13 +25,13 @@ export const Media = { isMediumOnly: (useMediaPrefix = true) => getMediaQuery( `(min-width: ${MEDIA_WIDTHS.upToSmall + 1}px) and (max-width: ${MEDIA_WIDTHS.upToMedium}px)`, - useMediaPrefix + useMediaPrefix, ), upToMedium: (useMediaPrefix = true) => getMediaQuery(`(max-width: ${MEDIA_WIDTHS.upToMedium}px)`, useMediaPrefix), isLargeOnly: (useMediaPrefix = true) => getMediaQuery( `(min-width: ${MEDIA_WIDTHS.upToMedium + 1}px) and (max-width: ${MEDIA_WIDTHS.upToLarge}px)`, - useMediaPrefix + useMediaPrefix, ), upToLarge: (useMediaPrefix = true) => getMediaQuery(`(max-width: ${MEDIA_WIDTHS.upToLarge}px)`, useMediaPrefix), upToLargeAlt: (useMediaPrefix = true) => getMediaQuery(`(max-width: ${MEDIA_WIDTHS.upToLargeAlt}px)`, useMediaPrefix), diff --git a/libs/ui/src/pure/MenuBar/index.tsx b/libs/ui/src/pure/MenuBar/index.tsx index 852b07499f..5e27a41623 100644 --- a/libs/ui/src/pure/MenuBar/index.tsx +++ b/libs/ui/src/pure/MenuBar/index.tsx @@ -37,6 +37,8 @@ import { Media } from '../../consts' import { Badge } from '../Badge' import { ProductLogo, ProductVariant } from '../ProductLogo' +import type { CowSwapTheme } from '../../types' + const DAO_NAV_ITEMS: MenuItem[] = [ { href: 'https://cow.fi/', @@ -674,6 +676,7 @@ interface MenuBarProps { hoverBackgroundDark?: string padding?: string maxWidth?: number + customTheme?: CowSwapTheme } export const MenuBar = (props: MenuBarProps) => { @@ -701,6 +704,7 @@ export const MenuBar = (props: MenuBarProps) => { hoverBackgroundDark, padding, maxWidth, + customTheme, LinkComponent, } = props @@ -784,7 +788,7 @@ export const MenuBar = (props: MenuBarProps) => { rootDomain={rootDomain} LinkComponent={LinkComponent} /> - + {!isMobile && ( diff --git a/libs/ui/src/pure/ProductLogo/index.tsx b/libs/ui/src/pure/ProductLogo/index.tsx index e3de673090..e6020684e1 100644 --- a/libs/ui/src/pure/ProductLogo/index.tsx +++ b/libs/ui/src/pure/ProductLogo/index.tsx @@ -2,6 +2,7 @@ import LOGO_COWAMM from '@cowprotocol/assets/images/logo-cowamm.svg' import LOGO_COWDAO from '@cowprotocol/assets/images/logo-cowdao.svg' import LOGO_COWEXPLORER from '@cowprotocol/assets/images/logo-cowexplorer.svg' import LOGO_COWPROTOCOL from '@cowprotocol/assets/images/logo-cowprotocol.svg' +import LOGO_COWSWAP_HALLOWEEN from '@cowprotocol/assets/images/logo-cowswap-halloween.svg' import LOGO_COWSWAP from '@cowprotocol/assets/images/logo-cowswap.svg' import LOGO_ICON_COW from '@cowprotocol/assets/images/logo-icon-cow.svg' import LOGO_ICON_MEVBLOCKER from '@cowprotocol/assets/images/logo-icon-mevblocker.svg' @@ -30,7 +31,11 @@ interface LogoInfo { color?: string // Optional color attribute for SVG } -export type ThemedLogo = Record +export type ThemedLogo = Partial> & { + light: { default: LogoInfo; logoIconOnly?: LogoInfo } + dark: { default: LogoInfo; logoIconOnly?: LogoInfo } + darkHalloween?: { default: LogoInfo; logoIconOnly?: LogoInfo } +} const LOGOS: Record = { // CoW Swap @@ -59,6 +64,13 @@ const LOGOS: Record = { color: '#65D9FF', }, }, + darkHalloween: { + default: { + src: LOGO_COWSWAP_HALLOWEEN, + alt: 'CoW Swap', + color: '#65D9FF', + }, + }, }, // CoW Explorer @@ -270,7 +282,8 @@ export const ProductLogo = ({ external = false, }: LogoProps) => { const themeMode = useTheme() - const logoForTheme = LOGOS[variant][customThemeMode || (themeMode.darkMode ? 'dark' : 'light')] + const selectedTheme = customThemeMode || (themeMode.darkMode ? 'dark' : 'light') + const logoForTheme = LOGOS[variant][selectedTheme] || LOGOS[variant]['light'] // Fallback to light theme if selected theme is not available const logoInfo = logoIconOnly && logoForTheme.logoIconOnly ? logoForTheme.logoIconOnly : logoForTheme.default const initialColor = overrideColor || logoInfo.color diff --git a/libs/ui/src/types.ts b/libs/ui/src/types.ts index 17972a5606..02a65e50bc 100644 --- a/libs/ui/src/types.ts +++ b/libs/ui/src/types.ts @@ -13,4 +13,4 @@ export type ComposableCowInfo = { export type BadgeType = 'information' | 'success' | 'alert' | 'alert2' | 'default' -export type CowSwapTheme = 'dark' | 'light' +export type CowSwapTheme = 'dark' | 'light' | 'darkHalloween' From 2877df52be2fd519a20157a1cd91a2e18e954dae Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 29 Oct 2024 13:30:39 +0500 Subject: [PATCH 063/116] feat(yield): fetch balances for LP-tokens (#5005) * feat(yield): setup yield widget * fix(yield): display correct output amount * feat(trade-quote): support fast quote requests * feat(yield): display trade buttons * feat(yield): display confirm details * feat(yield): scope context to trade * feat(yield): do trade after confirmation * feat(yield): display order progress bar * refactor: move useIsEoaEthFlow to trade module * refactor: move hooks to tradeSlippage module * refactor: rename swapSlippage to tradeSlippage * feat(trade-slippage): split slippage state by trade type * refactor: unlink TransactionSettings from swap module * refactor: use reach modal in Settings component * refactor: move Settings component in trade module * feat(yield): add settings widget * feat(yield): use deadline value from settings * fix(trade-quote): skip fast quote if it slower than optimal * refactor: generalise TradeRateDetails from swap module * refactor: move TradeRateDetails into tradeWidgetAddons module * refactor: move SettingsTab into tradeWidgetAddons module * refactor(swap): generalise useHighFeeWarning() * refactor(swap): move hooks to trade module * refactor: move HighFeeWarning to trade widget addons * refactor: make HighFeeWarning independent * feat(yield): display trade warnings ZeroApprovalWarning and HighFeeWarning * refactor(trade): generalise NoImpactWarning * refactor(trade): generalise ZeroApprovalWarning * refactor(trade): generalise bundle tx banners * refactor: extract TradeWarnings * chore: fix yield form displaying * refactor(swap): generalise trade flow * feat(yield): support safe bundle swaps * chore: hide yield under ff * chore: remove lazy loading * chore: fix imports * fix: generalize smart slippage usage * fix: don't sync url params while navigating with yield * feat: open settings menu on slippage click * chore: update btn text * fix: make slippage settings through * chore: merge develop * chore: fix yield widget * chore: remove old migration * feat: add default lp-token lists * feat(tokens): display custom token selector for Yield widget * feat(tokens): display lp-token lists * feat: display lp-token logo * chore: slippage label * fix: fix default trade state in menu * chore: fix lint * feat: fetch lp-token balances Also added optimization to the balances updater, because node started firing 429 error * feat: reuse virtual list for lp-tokens list * chore: adjust balances updater * chore: add yield in widget conf * chore: reset balances only when account changed * feat: cache balances into localStorage * feat: display cached token balances * feat: link to create pool * chore: merge develop * chore: fix hooks trade state * fix: fix smart slippage displaying * chore: center slippage banner * chore: condition to displayCreatePoolBanner * fix: poll lp-token balances only in yield widget * feat: modify pool token list layout (#5014) * feat: modify pool token list layout * feat: apply mobile widgetlinks menu * feat: add padding to pool token balance --------- Co-authored-by: Alexandr Kazachenko * chore: link to create pool * feat(yield): add unlock screen (#5043) * feat: add unlock screen for yield tab * feat: add unlock screen for yield tab * chore: revert --------- Co-authored-by: Alexandr Kazachenko --------- Co-authored-by: fairlight <31534717+fairlighteth@users.noreply.github.com> --- .../src/common/pure/VirtualList/index.tsx | 70 +++++++ .../pure/VirtualList}/styled.ts | 19 +- .../LpBalancesAndAllowancesUpdater.tsx | 51 +++++ .../application/containers/App/Updaters.tsx | 5 + .../containers/LpTokenListsWidget/index.tsx | 70 +++++++ .../containers/LpTokenListsWidget/styled.tsx | 37 ++++ .../containers/SelectTokenWidget/index.tsx | 7 +- .../tokensList/pure/LpTokenLists/index.tsx | 160 +++++++++++++++ .../tokensList/pure/LpTokenLists/styled.ts | 194 ++++++++++++++++++ .../pure/SelectTokenModal/index.tsx | 53 ++--- .../pure/SelectTokenModal/styled.ts | 2 +- .../tokensList/pure/TokenListItem/index.tsx | 29 +-- .../tokensList/pure/TokenTags/index.tsx | 2 +- .../pure/TokensVirtualList/index.tsx | 100 +++------ .../TradeWidget/TradeWidgetForm.tsx | 5 +- .../TradeWidget/TradeWidgetModals.tsx | 22 +- .../trade/containers/TradeWidget/index.tsx | 2 +- .../trade/containers/TradeWidget/types.ts | 1 + .../containers/TradeWidgetLinks/styled.ts | 7 +- .../yield/containers/YieldWidget/index.tsx | 41 +++- .../modules/yield/hooks/useYieldSettings.ts | 11 + .../modules/yield/state/yieldSettingsAtom.ts | 2 + .../hooks/usePersistBalancesAndAllowances.ts | 29 ++- libs/balances-and-allowances/src/index.ts | 1 + .../src/state/balancesAtom.ts | 13 +- ...er.ts => BalancesAndAllowancesUpdater.tsx} | 6 +- .../src/updaters/BalancesCacheUpdater.tsx | 89 ++++++++ libs/common-const/src/types.ts | 29 ++- .../hooks/useMultipleContractSingleData.ts | 8 +- libs/multicall/src/multicall.ts | 27 ++- libs/tokens/src/const/lpTokensList.json | 32 +++ libs/tokens/src/const/tokensLists.ts | 10 +- .../tokens/src/hooks/tokens/useAllLpTokens.ts | 43 ++++ libs/tokens/src/index.ts | 8 +- .../userAddedTokenListsAtomv2Migration.ts | 34 --- libs/tokens/src/services/fetchTokenList.ts | 21 +- .../state/tokenLists/tokenListsStateAtom.ts | 4 +- libs/tokens/src/state/tokens/allTokensAtom.ts | 26 +-- libs/tokens/src/types.ts | 12 +- .../src/updaters/TokensListsUpdater/index.ts | 9 +- libs/tokens/src/utils/parseTokenInfo.ts | 26 +++ .../src/utils/tokenMapToListWithLogo.ts | 4 +- libs/tokens/src/utils/validateTokenList.ts | 29 ++- libs/types/src/common.ts | 1 + libs/ui/src/containers/ExternalLink/index.tsx | 41 ---- libs/ui/src/enum.ts | 1 + libs/ui/src/index.ts | 2 +- libs/ui/src/pure/InfoTooltip/index.tsx | 34 +-- libs/ui/src/pure/Loader/styled.tsx | 5 + libs/ui/src/theme/ThemeColorVars.tsx | 1 + 50 files changed, 1126 insertions(+), 309 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx rename apps/cowswap-frontend/src/{modules/tokensList/pure/TokensVirtualList => common/pure/VirtualList}/styled.ts (66%) create mode 100644 apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx create mode 100644 apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts rename libs/balances-and-allowances/src/updaters/{BalancesAndAllowancesUpdater.ts => BalancesAndAllowancesUpdater.tsx} (92%) create mode 100644 libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx create mode 100644 libs/tokens/src/const/lpTokensList.json create mode 100644 libs/tokens/src/hooks/tokens/useAllLpTokens.ts delete mode 100644 libs/tokens/src/migrations/userAddedTokenListsAtomv2Migration.ts create mode 100644 libs/tokens/src/utils/parseTokenInfo.ts diff --git a/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx b/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx new file mode 100644 index 0000000000..348f86a792 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx @@ -0,0 +1,70 @@ +import { ReactNode, useCallback, useRef } from 'react' + +import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual' +import ms from 'ms.macro' + +import { ListInner, ListScroller, ListWrapper, LoadingRows } from './styled' + +const scrollDelay = ms`400ms` + +const threeDivs = () => ( + <> +
    +
    +
    + +) + +interface VirtualListProps { + id?: string + items: T[] + getItemView(items: T[], item: VirtualItem): ReactNode + loading?: boolean + estimateSize?: () => number +} + +export function VirtualList({ id, items, loading, getItemView, estimateSize = () => 56 }: VirtualListProps) { + const parentRef = useRef(null) + const wrapperRef = useRef(null) + const scrollTimeoutRef = useRef() + + const onScroll = useCallback(() => { + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current) + if (wrapperRef.current) wrapperRef.current.style.pointerEvents = 'none' + } + + scrollTimeoutRef.current = setTimeout(() => { + if (wrapperRef.current) wrapperRef.current.style.pointerEvents = '' + }, scrollDelay) + }, []) + + const virtualizer = useVirtualizer({ + getScrollElement: () => parentRef.current, + count: items.length, + estimateSize, + overscan: 5, + }) + + const virtualItems = virtualizer.getVirtualItems() + + return ( + + + + {virtualItems.map((item) => { + if (loading) { + return {threeDivs()} + } + + return ( +
    + {getItemView(items, item)} +
    + ) + })} +
    +
    +
    + ) +} diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/styled.ts b/apps/cowswap-frontend/src/common/pure/VirtualList/styled.ts similarity index 66% rename from apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/styled.ts rename to apps/cowswap-frontend/src/common/pure/VirtualList/styled.ts index 1585ac2abd..8169ec3773 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/styled.ts +++ b/apps/cowswap-frontend/src/common/pure/VirtualList/styled.ts @@ -2,12 +2,27 @@ import { LoadingRows as BaseLoadingRows } from '@cowprotocol/ui' import styled from 'styled-components/macro' -export const TokensInner = styled.div` +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + overflow: auto; + margin-bottom: 15px; +` +export const ListWrapper = styled.div` + display: block; + width: 100%; + height: 100%; + overflow: auto; + + ${({ theme }) => theme.colorScrollbar}; +` + +export const ListInner = styled.div` width: 100%; position: relative; ` -export const TokensScroller = styled.div` +export const ListScroller = styled.div` position: absolute; top: 0; left: 0; diff --git a/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx b/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx new file mode 100644 index 0000000000..7661ea860b --- /dev/null +++ b/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx @@ -0,0 +1,51 @@ +import { useEffect, useMemo, useState } from 'react' + +import { usePersistBalancesAndAllowances } from '@cowprotocol/balances-and-allowances' +import { SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' +import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import { TokenListCategory, useAllLpTokens } from '@cowprotocol/tokens' + +import ms from 'ms.macro' + +// A small gap between balances and allowances refresh intervals is needed to avoid high load to the node at the same time +const LP_BALANCES_SWR_CONFIG = { refreshInterval: ms`32s` } +const LP_ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`34s` } +const LP_MULTICALL_OPTIONS = { consequentExecution: true } + +// To avoid high load to the node at the same time +// We start the updater with a delay +const LP_UPDATER_START_DELAY = ms`3s` + +const LP_CATEGORIES = [TokenListCategory.LP, TokenListCategory.COW_AMM_LP] + +export interface BalancesAndAllowancesUpdaterProps { + account: string | undefined + chainId: SupportedChainId + enablePolling: boolean +} +export function LpBalancesAndAllowancesUpdater({ account, chainId, enablePolling }: BalancesAndAllowancesUpdaterProps) { + const allLpTokens = useAllLpTokens(LP_CATEGORIES) + const [isUpdaterPaused, setIsUpdaterPaused] = useState(true) + + const lpTokenAddresses = useMemo(() => allLpTokens.map((token) => token.address), [allLpTokens]) + + usePersistBalancesAndAllowances({ + account: isUpdaterPaused ? undefined : account, + chainId, + tokenAddresses: lpTokenAddresses, + setLoadingState: false, + balancesSwrConfig: enablePolling ? LP_BALANCES_SWR_CONFIG : SWR_NO_REFRESH_OPTIONS, + allowancesSwrConfig: enablePolling ? LP_ALLOWANCES_SWR_CONFIG : SWR_NO_REFRESH_OPTIONS, + multicallOptions: LP_MULTICALL_OPTIONS, + }) + + useEffect(() => { + const timeout = setTimeout(() => { + setIsUpdaterPaused(false) + }, LP_UPDATER_START_DELAY) + + return () => clearTimeout(timeout) + }, []) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index 55a2dc74a6..4b42b461c9 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -13,6 +13,7 @@ import { FinalizeTxUpdater } from 'modules/onchainTransactions' import { OrdersNotificationsUpdater } from 'modules/orders' import { EthFlowDeadlineUpdater } from 'modules/swap/state/EthFlow/updaters' import { useOnTokenListAddingError } from 'modules/tokensList' +import { TradeType, useTradeTypeInfo } from 'modules/trade' import { UsdPricesUpdater } from 'modules/usdAmount' import { ProgressBarV2ExecutingOrdersUpdater } from 'common/hooks/orderProgressBarV2' @@ -20,6 +21,7 @@ import { TotalSurplusUpdater } from 'common/state/totalSurplusState' import { FeatureFlagsUpdater } from 'common/updaters/FeatureFlagsUpdater' import { FeesUpdater } from 'common/updaters/FeesUpdater' import { GasUpdater } from 'common/updaters/GasUpdater' +import { LpBalancesAndAllowancesUpdater } from 'common/updaters/LpBalancesAndAllowancesUpdater' import { CancelledOrdersUpdater, ExpiredOrdersUpdater, @@ -36,6 +38,8 @@ export function Updaters() { const { tokenLists, appCode, customTokens, standaloneMode } = useInjectedWidgetParams() const onTokenListAddingError = useOnTokenListAddingError() const { isGeoBlockEnabled } = useFeatureFlags() + const tradeTypeInfo = useTradeTypeInfo() + const isYieldWidget = tradeTypeInfo?.tradeType === TradeType.YIELD return ( <> @@ -76,6 +80,7 @@ export function Updaters() { /> + ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx new file mode 100644 index 0000000000..830fa46f17 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx @@ -0,0 +1,70 @@ +import { ReactNode, useState } from 'react' + +import { useTokensBalances } from '@cowprotocol/balances-and-allowances' +import { TokenListCategory, useAllLpTokens, useTokensByAddressMap } from '@cowprotocol/tokens' +import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' + +import { TabButton, TabsContainer } from './styled' + +import { LpTokenLists } from '../../pure/LpTokenLists' + +interface LpTokenListsProps { + children: ReactNode +} + +const tabs = [ + { id: 'all', title: 'All', value: null }, + { id: 'pool', title: 'Pool tokens', value: [TokenListCategory.LP, TokenListCategory.COW_AMM_LP] }, + { + id: 'cow-amm', + title: ( + <> + {' '} + CoW AMM only + + ), + value: [TokenListCategory.COW_AMM_LP], + }, +] + +export function LpTokenListsWidget({ children }: LpTokenListsProps) { + const [listsCategories, setListsCategories] = useState(null) + const lpTokens = useAllLpTokens(listsCategories) + const tokensByAddress = useTokensByAddressMap() + const balancesState = useTokensBalances() + + return ( + <> + + {tabs.map((tab) => ( + setListsCategories(tab.value)}> + {tab.title} + + ))} + + {listsCategories === null ? ( + children + ) : lpTokens.length === 0 ? ( + + ) : ( + + )} + + ) +} diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/styled.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/styled.tsx new file mode 100644 index 0000000000..53b2d313c9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/styled.tsx @@ -0,0 +1,37 @@ +import { Media, UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +export const TabsContainer = styled.div` + display: flex; + flex-direction: row; + gap: 10px; + margin: 0 20px 20px; + + ${Media.upToSmall()} { + gap: 4px; + } +` +export const TabButton = styled.button<{ active$: boolean; isCowAmm?: boolean }>` + cursor: pointer; + border: 1px solid var(${UI.COLOR_BACKGROUND}); + outline: none; + padding: 8px 16px; + border-radius: 32px; + font-size: 14px; + font-weight: ${({ active$ }) => (active$ ? '600' : '500')}; + color: ${({ active$ }) => (active$ ? `var(${UI.COLOR_TEXT})` : `var(${UI.COLOR_TEXT_OPACITY_70})`)}; + background: ${({ active$ }) => (active$ ? `var(${UI.COLOR_BACKGROUND})` : 'transparent')}; + display: flex; + align-items: center; + gap: 4px; + + ${Media.upToSmall()} { + padding: 8px 12px; + font-size: 13px; + } + + &:hover { + background: var(${UI.COLOR_BACKGROUND}); + } +` diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index e4296a5f64..9a30f04cb0 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -37,7 +37,11 @@ const Wrapper = styled.div` } ` -export function SelectTokenWidget() { +interface SelectTokenWidgetProps { + displayLpTokenLists?: boolean +} + +export function SelectTokenWidget({displayLpTokenLists}: SelectTokenWidgetProps) { const { open, onSelectToken, tokenToImport, listToImport, selectedToken, onInputPressEnter } = useSelectTokenWidgetState() const [isManageWidgetOpen, setIsManageWidgetOpen] = useState(false) @@ -137,6 +141,7 @@ export function SelectTokenWidget() { return ( + + +) + +const MobileCardRowItem: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( + + {label}: + {value} + +) + +const TokenInfo: React.FC<{ token: LpToken }> = ({ token }) => ( + <> + + + +

    + +

    + +) + +const LpTokenLogos: React.FC<{ token0: string; token1: string; tokensByAddress: TokensByAddress; size: number }> = ({ + token0, + token1, + tokensByAddress, + size, +}) => ( + + + + +) + +interface LpTokenListsProps { + lpTokens: LpToken[] + tokensByAddress: TokensByAddress + balancesState: BalancesState + displayCreatePoolBanner: boolean +} + +export function LpTokenLists({ lpTokens, tokensByAddress, balancesState, displayCreatePoolBanner }: LpTokenListsProps) { + const { values: balances } = balancesState + const isMobile = useMediaQuery(Media.upToSmall(false)) + + const getItemView = useCallback( + (lpTokens: LpToken[], item: VirtualItem) => { + const token = lpTokens[item.index] + const token0 = token.tokens?.[0]?.toLowerCase() + const token1 = token.tokens?.[1]?.toLowerCase() + const balance = balances ? balances[token.address.toLowerCase()] : undefined + const balanceAmount = balance ? CurrencyAmount.fromRawAmount(token, balance.toHexString()) : undefined + + const commonContent = ( + <> + + + + + + ) + + if (isMobile) { + return ( + + {commonContent} + : LoadingElement} + /> + + + + TODO + + + } + /> + + ) + } + + return ( + + {commonContent} + {balanceAmount ? : LoadingElement} + 40% + + TODO + + + ) + }, + [balances, tokensByAddress, isMobile], + ) + + return ( + + {lpTokens.length > 0 ? ( + <> + {!isMobile && ( + + Pool + Balance + APR + + + )} + + + ) : ( + No pool tokens available + )} + {displayCreatePoolBanner && ( + +
    Can’t find the pool you’re looking for?
    + Create a pool ↗ +
    + )} +
    + ) +} diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts new file mode 100644 index 0000000000..578baf1b60 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts @@ -0,0 +1,194 @@ +import { TokenLogoWrapper } from '@cowprotocol/tokens' +import { ExternalLink, Media, UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +export const Wrapper = styled.div` + --grid-columns: 1fr 100px 50px 20px; + display: flex; + flex-direction: column; + overflow: auto; + margin: 0 0 20px; + height: 100%; +` + +export const LpTokenWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; +` + +export const ListHeader = styled.div` + display: grid; + grid-template-columns: var(--grid-columns); + font-size: 12px; + font-weight: 500; + color: var(${UI.COLOR_TEXT_OPACITY_70}); + margin: 0 25px 15px 20px; +` + +export const ListItem = styled.div` + display: grid; + grid-template-columns: var(--grid-columns); + padding: 10px 20px; + cursor: pointer; + + &:hover { + background: var(${UI.COLOR_PAPER_DARKER}); + } +` + +export const LpTokenLogo = styled.div` + --size: 36px; + position: relative; + width: var(--size); + height: var(--size); + object-fit: contain; + + ${Media.upToSmall()} { + --size: 32px; + } + + ${TokenLogoWrapper} { + position: relative; + z-index: 1; + border-radius: var(--size); + width: var(--size); + height: var(--size); + min-width: var(--size); + min-height: var(--size); + font-size: var(--size); + } + + ${TokenLogoWrapper}:nth-child(2) { + position: absolute; + left: 0; + top: 0; + z-index: 2; + clip-path: inset(0 0 0 50%); + } + + &::after { + content: ''; + position: absolute; + top: 0; + left: 50%; + width: 2px; + height: 100%; + background-color: var(${UI.COLOR_PAPER}); + transform: translateX(-50%); + z-index: 3; + } +` + +export const LpTokenInfo = styled.div` + display: flex; + flex-direction: column; + gap: 2px; + + > strong { + font-weight: 600; + } + + > p { + margin: 0; + font-size: 12px; + color: var(${UI.COLOR_TEXT_OPACITY_60}); + letter-spacing: -0.02rem; + } +` + +export const LpTokenYieldPercentage = styled.span` + display: flex; + align-items: center; + font-size: 16px; + font-weight: 600; +` + +export const LpTokenBalance = styled.span` + display: flex; + align-items: center; + font-size: 14px; + letter-spacing: -0.02rem; + color: var(${UI.COLOR_TEXT_OPACITY_70}); + padding: 0 8px 0 0; +` + +export const LpTokenTooltip = styled.div` + display: flex; + align-items: center; + margin: auto; +` + +export const NoPoolWrapper = styled.div` + border-top: 1px solid var(${UI.COLOR_BORDER}); + color: var(${UI.COLOR_TEXT_OPACITY_70}); + padding: 20px 20px 0; + display: flex; + flex-flow: row wrap; + width: 100%; + gap: 10px; + align-items: center; + justify-content: space-between; + font-size: 13px; + margin: auto 0 0; + + > div { + flex: 1; + } +` + +export const CreatePoolLink = styled(ExternalLink)` + display: inline-block; + background: #bcec79; + color: #194d05; + font-size: 16px; + font-weight: bold; + border-radius: 24px; + padding: 10px 24px; + text-decoration: none; + + &:hover { + opacity: 0.8; + } +` + +export const EmptyList = styled.div` + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; + font-size: 16px; + color: var(${UI.COLOR_TEXT_OPACITY_70}); +` + +export const MobileCard = styled.div` + display: flex; + flex-direction: column; + background-color: var(${UI.COLOR_PAPER}); + padding: 20px; + border-bottom: 1px solid var(${UI.COLOR_PAPER_DARKER}); +` + +export const MobileCardRow = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 8px; + gap: 8px; + + &:last-child { + margin-bottom: 0; + } +` + +export const MobileCardLabel = styled.span` + font-size: 14px; + color: var(${UI.COLOR_TEXT_OPACITY_70}); +` + +export const MobileCardValue = styled.span` + font-size: 16px; + font-weight: 600; +` diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index 36b6ba37b2..e5248e3262 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -11,6 +11,7 @@ import { PermitCompatibleTokens } from 'modules/permit' import * as styledEl from './styled' +import { LpTokenListsWidget } from '../../containers/LpTokenListsWidget' import { TokenSearchResults } from '../../containers/TokenSearchResults' import { SelectTokenContext } from '../../types' import { FavoriteTokensList } from '../FavoriteTokensList' @@ -24,6 +25,7 @@ export interface SelectTokenModalProps { selectedToken?: string permitCompatibleTokens: PermitCompatibleTokens hideFavoriteTokensTooltip?: boolean + displayLpTokenLists?: boolean account: string | undefined onSelectToken(token: TokenWithLogo): void @@ -52,6 +54,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) { onOpenManageWidget, onInputPressEnter, account, + displayLpTokenLists } = props const [inputValue, setInputValue] = useState(defaultInputValue) @@ -61,9 +64,32 @@ export function SelectTokenModal(props: SelectTokenModalProps) { selectedToken, onSelectToken, unsupportedTokens, - permitCompatibleTokens, + permitCompatibleTokens } + const allListsContent = <> + + + + + {inputValue.trim() ? ( + + ) : ( + + )} + +
    + + Manage Token Lists + +
    + + return ( @@ -80,28 +106,9 @@ export function SelectTokenModal(props: SelectTokenModalProps) { placeholder="Search name or paste address" /> - - - - - - - {inputValue.trim() ? ( - - ) : ( - - )} - -
    - - Manage Token Lists - -
    + {displayLpTokenLists + ? {allListsContent} + : allListsContent}
    ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/styled.ts index 92f88d3443..fd9c69560f 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/styled.ts @@ -13,7 +13,7 @@ export const Wrapper = styled.div` ` export const Row = styled.div` - margin: 0 20px 15px 20px; + margin: 0 20px 20px; ` export const Separator = styled.div` diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx index 7823fb7dc6..e9493aaf32 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx @@ -1,5 +1,5 @@ import { TokenWithLogo } from '@cowprotocol/common-const' -import { TokenAmount } from '@cowprotocol/ui' +import { LoadingRows, LoadingRowSmall, TokenAmount } from '@cowprotocol/ui' import { BigNumber } from '@ethersproject/bignumber' import { CurrencyAmount } from '@uniswap/sdk-core' @@ -8,32 +8,24 @@ import * as styledEl from './styled' import { TokenInfo } from '../TokenInfo' import { TokenTags } from '../TokenTags' -import type { VirtualItem } from '@tanstack/react-virtual' +const LoadingElement = ( + + + +) export interface TokenListItemProps { token: TokenWithLogo selectedToken?: string balance: BigNumber | undefined onSelectToken(token: TokenWithLogo): void - measureElement?: (node: Element | null) => void - virtualRow?: VirtualItem isUnsupported: boolean isPermitCompatible: boolean isWalletConnected: boolean } export function TokenListItem(props: TokenListItemProps) { - const { - token, - selectedToken, - balance, - onSelectToken, - virtualRow, - isUnsupported, - isPermitCompatible, - measureElement, - isWalletConnected, - } = props + const { token, selectedToken, balance, onSelectToken, isUnsupported, isPermitCompatible, isWalletConnected } = props const isTokenSelected = token.address.toLowerCase() === selectedToken?.toLowerCase() @@ -41,9 +33,6 @@ export function TokenListItem(props: TokenListItemProps) { return ( onSelectToken(token)} @@ -51,7 +40,9 @@ export function TokenListItem(props: TokenListItemProps) { {isWalletConnected && ( <> - {balanceAmount && } + + {balanceAmount ? : LoadingElement} + )} diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenTags/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenTags/index.tsx index 86cc250495..89df32f8a3 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenTags/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenTags/index.tsx @@ -50,7 +50,7 @@ export function TokenTags({ } if (tagsToShow.length === 0) { - return + return null } return ( diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index 3637167df1..8223f60ea3 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -1,28 +1,15 @@ -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, useMemo } from 'react' import { TokenWithLogo } from '@cowprotocol/common-const' -import { useVirtualizer } from '@tanstack/react-virtual' -import ms from 'ms.macro' +import { VirtualItem } from '@tanstack/react-virtual' -import * as styledEl from './styled' +import { VirtualList } from 'common/pure/VirtualList' import { SelectTokenContext } from '../../types' import { tokensListSorter } from '../../utils/tokensListSorter' -import { CommonListContainer } from '../commonElements' import { TokenListItem } from '../TokenListItem' -const estimateSize = () => 56 -const threeDivs = () => ( - <> -
    -
    -
    - -) - -const scrollDelay = ms`400ms` - export interface TokensVirtualListProps extends SelectTokenContext { allTokens: TokenWithLogo[] account: string | undefined @@ -31,73 +18,36 @@ export interface TokensVirtualListProps extends SelectTokenContext { export function TokensVirtualList(props: TokensVirtualListProps) { const { allTokens, selectedToken, balancesState, onSelectToken, unsupportedTokens, permitCompatibleTokens, account } = props - const { values: balances, isLoading: balancesLoading } = balancesState + const { values: balances } = balancesState const isWalletConnected = !!account // const isInjectedWidgetMode = isInjectedWidget() // const isChainIdUnsupported = useIsProviderNetworkUnsupported() - const scrollTimeoutRef = useRef() - const parentRef = useRef(null) - const wrapperRef = useRef(null) - - const onScroll = useCallback(() => { - if (scrollTimeoutRef.current) { - clearTimeout(scrollTimeoutRef.current) - if (wrapperRef.current) wrapperRef.current.style.pointerEvents = 'none' - } - - scrollTimeoutRef.current = setTimeout(() => { - if (wrapperRef.current) wrapperRef.current.style.pointerEvents = '' - }, scrollDelay) - }, []) - - const virtualizer = useVirtualizer({ - getScrollElement: () => parentRef.current, - count: allTokens.length, - estimateSize, - overscan: 5, - }) - const sortedTokens = useMemo(() => { return balances ? allTokens.sort(tokensListSorter(balances)) : allTokens }, [allTokens, balances]) - const items = virtualizer.getVirtualItems() - - return ( - - {/*{!isInjectedWidgetMode && account && !isChainIdUnsupported && (*/} - {/* */} - {/*)}*/} - - - {items.map((virtualRow) => { - const token = sortedTokens[virtualRow.index] - const addressLowerCase = token.address.toLowerCase() - const balance = balances ? balances[token.address.toLowerCase()] : undefined - - if (balancesLoading) { - return {threeDivs()} - } - - return ( - - ) - })} - - - + const getItemView = useCallback( + (sortedTokens: TokenWithLogo[], virtualRow: VirtualItem) => { + const token = sortedTokens[virtualRow.index] + const addressLowerCase = token.address.toLowerCase() + const balance = balances ? balances[token.address.toLowerCase()] : undefined + + return ( + + ) + }, + [balances, unsupportedTokens, permitCompatibleTokens, selectedToken, onSelectToken, isWalletConnected], ) + + return } diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index 4731a75dc7..52fb94db66 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo } from 'react' import ICON_ORDERS from '@cowprotocol/assets/svg/orders.svg' import { isInjectedWidget, maxAmountSpend } from '@cowprotocol/common-utils' -import { ButtonOutlined, MY_ORDERS_ID } from '@cowprotocol/ui' +import { ButtonOutlined, Media, MY_ORDERS_ID } from '@cowprotocol/ui' import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { t } from '@lingui/macro' @@ -44,6 +44,7 @@ const scrollToMyOrders = () => { export function TradeWidgetForm(props: TradeWidgetProps) { const isInjectedWidgetMode = isInjectedWidget() const { standaloneMode, hideOrdersTable } = useInjectedWidgetParams() + const isMobile = useMediaQuery(Media.upToSmall(false)) const isAlternativeOrderModalVisible = useIsAlternativeOrderModalVisible() const { pendingActivity } = useCategorizeRecentActivity() @@ -114,7 +115,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { (isConnectedMarketOrderWidget || !hideOrdersTable) && ((isConnectedMarketOrderWidget && standaloneMode !== true) || (!isMarketOrderWidget && isUpToLarge && !lockScreen)) - const showDropdown = shouldShowMyOrdersButton || isInjectedWidgetMode + const showDropdown = shouldShowMyOrdersButton || isInjectedWidgetMode || isMobile const currencyInputCommonProps = { isChainIdUnsupported, diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetModals.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetModals.tsx index c6f916a43b..be84faee34 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetModals.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetModals.tsx @@ -8,9 +8,9 @@ import { SelectTokenWidget, useSelectTokenWidgetState, useTokenListAddingError, - useUpdateSelectTokenWidgetState, + useUpdateSelectTokenWidgetState } from 'modules/tokensList' -import { ZeroApprovalModal, useZeroApproveModalState } from 'modules/zeroApproval' +import { useZeroApproveModalState, ZeroApprovalModal } from 'modules/zeroApproval' import { TradeApproveModal } from 'common/containers/TradeApprove' import { useTradeApproveState } from 'common/hooks/useTradeApproveState' @@ -24,7 +24,17 @@ import { useTradeState } from '../../hooks/useTradeState' import { useWrapNativeScreenState } from '../../hooks/useWrapNativeScreenState' import { WrapNativeModal } from '../WrapNativeModal' -export function TradeWidgetModals(confirmModal: ReactNode | undefined, genericModal: ReactNode | undefined) { +interface TradeWidgetModalsProps { + confirmModal: ReactNode | undefined, + genericModal: ReactNode | undefined + selectTokenWidget: ReactNode | undefined +} + +export function TradeWidgetModals({ + confirmModal, + genericModal, + selectTokenWidget = +}: TradeWidgetModalsProps) { const { chainId, account } = useWalletInfo() const { state: rawState } = useTradeState() const importTokenCallback = useAddUserToken() @@ -37,7 +47,7 @@ export function TradeWidgetModals(confirmModal: ReactNode | undefined, genericMo const { isModalOpen: isZeroApprovalModalOpen, closeModal: closeZeroApprovalModal } = useZeroApproveModalState() const { tokensToImport, - modalState: { isModalOpen: isAutoImportModalOpen, closeModal: closeAutoImportModal }, + modalState: { isModalOpen: isAutoImportModalOpen, closeModal: closeAutoImportModal } } = useAutoImportTokensState(rawState?.inputCurrencyId, rawState?.outputCurrencyId) const { onDismiss: closeTradeConfirm } = useTradeConfirmActions() @@ -59,7 +69,7 @@ export function TradeWidgetModals(confirmModal: ReactNode | undefined, genericMo updateSelectTokenWidgetState, setWrapNativeScreenState, updateTradeApproveState, - setTokenListAddingError, + setTokenListAddingError ]) const error = tokenListAddingError || approveError || confirmError @@ -84,7 +94,7 @@ export function TradeWidgetModals(confirmModal: ReactNode | undefined, genericMo } if (isTokenSelectOpen) { - return + return selectTokenWidget } if (isWrapNativeOpen) { diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx index 764652582f..743505a670 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx @@ -14,7 +14,7 @@ export function TradeWidget(props: TradeWidgetProps) { tradeQuoteStateOverride, enableSmartSlippage, } = params - const modals = TradeWidgetModals(confirmModal, genericModal) + const modals = TradeWidgetModals({confirmModal, genericModal, selectTokenWidget: slots.selectTokenWidget}) return ( <> diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts index 0176cdf9f6..fbfdaceadd 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts @@ -39,6 +39,7 @@ export interface TradeWidgetSlots { bottomContent?(warnings: ReactNode | null): ReactNode outerContent?: ReactNode updaters?: ReactNode + selectTokenWidget?: ReactNode } export interface TradeWidgetProps { diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts index 41747bbcb5..fe44cdacf6 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts @@ -84,12 +84,15 @@ export const MenuItem = styled.div<{ isActive?: boolean; isDropdownVisible: bool css` padding: 16px; width: 100%; - margin-bottom: 20px; + margin: 0 0 10px; `} + + } } ` export const SelectMenu = styled.div` + display: block; width: 100%; min-height: 100%; position: absolute; @@ -105,5 +108,5 @@ export const SelectMenu = styled.div` ` export const TradeWidgetContent = styled.div` - padding: 0 16px 16px 16px; + padding: 16px; ` diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx index 61dbde911c..9d395d27c7 100644 --- a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -1,7 +1,9 @@ -import { ReactNode, useCallback } from 'react' +import React, { useCallback } from 'react' +import { ReactNode } from 'react' import { Field } from 'legacy/state/types' +import { SelectTokenWidget } from 'modules/tokensList' import { TradeWidget, TradeWidgetSlots, @@ -9,6 +11,7 @@ import { useTradeConfirmState, useTradePriceImpact, } from 'modules/trade' +import { UnlockWidgetScreen, BulletListItem } from 'modules/trade/pure/UnlockWidgetScreen' import { useHandleSwap } from 'modules/tradeFlow' import { useTradeQuote } from 'modules/tradeQuote' import { SettingsTab, TradeRateDetails } from 'modules/tradeWidgetAddons' @@ -18,16 +21,37 @@ import { useSafeMemoObject } from 'common/hooks/useSafeMemo' import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' import { useYieldDerivedState } from '../../hooks/useYieldDerivedState' -import { useYieldDeadlineState, useYieldRecipientToggleState, useYieldSettings } from '../../hooks/useYieldSettings' +import { + useYieldDeadlineState, + useYieldRecipientToggleState, + useYieldSettings, + useYieldUnlockState, +} from '../../hooks/useYieldSettings' import { useYieldWidgetActions } from '../../hooks/useYieldWidgetActions' import { TradeButtons } from '../TradeButtons' import { Warnings } from '../Warnings' import { YieldConfirmModal } from '../YieldConfirmModal' +const YIELD_BULLET_LIST_CONTENT: BulletListItem[] = [ + { content: 'Maximize your yield on existing LP positions' }, + { content: 'Seamlessly swap your tokens into CoW AMM pools' }, + { content: 'Earn higher returns with reduced impermanent loss' }, + { content: 'Leverage advanced strategies for optimal growth' }, +] + +const YIELD_UNLOCK_SCREEN = { + id: 'yield-widget', + title: 'Unlock Enhanced Yield Features', + subtitle: 'Boooost your current LP positions with CoW AMM’s pools.', + orderType: 'yield', + buttonText: 'Start boooosting your yield!', +} + export function YieldWidget() { const { showRecipient } = useYieldSettings() const deadlineState = useYieldDeadlineState() const recipientToggleState = useYieldRecipientToggleState() + const [isUnlocked, setIsUnlocked] = useYieldUnlockState() const { isLoading: isRateLoading } = useTradeQuote() const priceImpact = useTradePriceImpact() const { isOpen: isConfirmOpen } = useTradeConfirmState() @@ -82,6 +106,7 @@ export function YieldWidget() { const rateInfoParams = useRateInfoParams(inputCurrencyInfo.amount, outputCurrencyInfo.amount) const slots: TradeWidgetSlots = { + selectTokenWidget: , settingsWidget: , bottomContent: useCallback( (tradeWarnings: ReactNode | null) => { @@ -100,6 +125,18 @@ export function YieldWidget() { }, [doTrade.contextIsReady, isRateLoading, rateInfoParams, deadlineState], ), + + lockScreen: !isUnlocked ? ( + setIsUnlocked(true)} + title={YIELD_UNLOCK_SCREEN.title} + subtitle={YIELD_UNLOCK_SCREEN.subtitle} + orderType={YIELD_UNLOCK_SCREEN.orderType} + buttonText={YIELD_UNLOCK_SCREEN.buttonText} + /> + ) : undefined, } const params = { diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts index 6ba8f042a7..ff1ba7ba92 100644 --- a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldSettings.ts @@ -19,6 +19,7 @@ export function useYieldDeadlineState(): StatefulValue { [settings.deadline, updateState], ) } + export function useYieldRecipientToggleState(): StatefulValue { const updateState = useSetAtom(updateYieldSettingsAtom) const settings = useYieldSettings() @@ -28,3 +29,13 @@ export function useYieldRecipientToggleState(): StatefulValue { [settings.showRecipient, updateState], ) } + +export function useYieldUnlockState(): StatefulValue { + const updateState = useSetAtom(updateYieldSettingsAtom) + const settings = useYieldSettings() + + return useMemo( + () => [settings.isUnlocked, (isUnlocked: boolean) => updateState({ isUnlocked })], + [settings.isUnlocked, updateState], + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts b/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts index 116c884354..48f5ca6b65 100644 --- a/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts +++ b/apps/cowswap-frontend/src/modules/yield/state/yieldSettingsAtom.ts @@ -7,11 +7,13 @@ import { getJotaiIsolatedStorage } from '@cowprotocol/core' export interface YieldSettingsState { readonly showRecipient: boolean readonly deadline: number + readonly isUnlocked: boolean } export const defaultYieldSettings: YieldSettingsState = { showRecipient: false, deadline: DEFAULT_DEADLINE_FROM_NOW, + isUnlocked: false, } export const { atom: yieldSettingsAtom, updateAtom: updateYieldSettingsAtom } = atomWithPartialUpdate( diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts index 787b588085..267810206f 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts @@ -1,11 +1,12 @@ -import { useSetAtom } from 'jotai/index' +import { useSetAtom } from 'jotai' import { useResetAtom } from 'jotai/utils' import { useEffect, useMemo } from 'react' import { ERC_20_INTERFACE } from '@cowprotocol/abis' +import { usePrevious } from '@cowprotocol/common-hooks' import { getIsNativeToken } from '@cowprotocol/common-utils' import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' -import { useMultipleContractSingleData } from '@cowprotocol/multicall' +import { MultiCallOptions, useMultipleContractSingleData } from '@cowprotocol/multicall' import { BigNumber } from '@ethersproject/bignumber' import { SWRConfiguration } from 'swr' @@ -22,11 +23,21 @@ export interface PersistBalancesAndAllowancesParams { balancesSwrConfig: SWRConfiguration allowancesSwrConfig: SWRConfiguration setLoadingState?: boolean + multicallOptions?: MultiCallOptions } export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowancesParams) { - const { account, chainId, tokenAddresses, setLoadingState, balancesSwrConfig, allowancesSwrConfig } = params + const { + account, + chainId, + tokenAddresses, + setLoadingState, + balancesSwrConfig, + allowancesSwrConfig, + multicallOptions = MULTICALL_OPTIONS, + } = params + const prevAccount = usePrevious(account) const setBalances = useSetAtom(balancesAtom) const setAllowances = useSetAtom(allowancesFullState) @@ -43,8 +54,8 @@ export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowa ERC_20_INTERFACE, 'balanceOf', balanceOfParams, - MULTICALL_OPTIONS, - balancesSwrConfig + multicallOptions, + balancesSwrConfig, ) const { isLoading: isAllowancesLoading, data: allowances } = useMultipleContractSingleData<[BigNumber]>( @@ -52,8 +63,8 @@ export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowa ERC_20_INTERFACE, 'allowance', allowanceParams, - MULTICALL_OPTIONS, - allowancesSwrConfig + multicallOptions, + allowancesSwrConfig, ) // Set balances loading state @@ -110,9 +121,9 @@ export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowa // Reset states when wallet is not connected useEffect(() => { - if (!account) { + if (prevAccount && prevAccount !== account) { resetBalances() resetAllowances() } - }, [account, resetAllowances, resetBalances]) + }, [account, prevAccount, resetAllowances, resetBalances]) } diff --git a/libs/balances-and-allowances/src/index.ts b/libs/balances-and-allowances/src/index.ts index 2423102726..2f6a868643 100644 --- a/libs/balances-and-allowances/src/index.ts +++ b/libs/balances-and-allowances/src/index.ts @@ -11,6 +11,7 @@ export { useNativeCurrencyAmount } from './hooks/useNativeCurrencyAmount' export { useCurrencyAmountBalance } from './hooks/useCurrencyAmountBalance' export { useTokenBalanceForAccount } from './hooks/useTokenBalanceForAccount' export { useAddPriorityAllowance } from './hooks/useAddPriorityAllowance' +export { usePersistBalancesAndAllowances } from './hooks/usePersistBalancesAndAllowances' // Types export type { BalancesState } from './state/balancesAtom' diff --git a/libs/balances-and-allowances/src/state/balancesAtom.ts b/libs/balances-and-allowances/src/state/balancesAtom.ts index 8e1a8ec6d5..a9f3f7c3fc 100644 --- a/libs/balances-and-allowances/src/state/balancesAtom.ts +++ b/libs/balances-and-allowances/src/state/balancesAtom.ts @@ -1,7 +1,18 @@ -import { atomWithReset } from 'jotai/utils' +import { atomWithReset, atomWithStorage } from 'jotai/utils' + +import { getJotaiMergerStorage } from '@cowprotocol/core' +import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { Erc20MulticallState } from '../types' +type BalancesCache = Record> + export interface BalancesState extends Erc20MulticallState {} +export const balancesCacheAtom = atomWithStorage( + 'balancesCacheAtom:v0', + mapSupportedNetworks({}), + getJotaiMergerStorage(), +) + export const balancesAtom = atomWithReset({ isLoading: false, values: {} }) diff --git a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx similarity index 92% rename from libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts rename to libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx index b2221819f1..e5cf06bff9 100644 --- a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts +++ b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx @@ -1,4 +1,4 @@ -import { useSetAtom } from 'jotai/index' +import { useSetAtom } from 'jotai' import { useEffect, useMemo } from 'react' import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' @@ -7,6 +7,8 @@ import { useAllTokens } from '@cowprotocol/tokens' import ms from 'ms.macro' +import { BalancesCacheUpdater } from './BalancesCacheUpdater' + import { useNativeTokenBalance } from '../hooks/useNativeTokenBalance' import { usePersistBalancesAndAllowances } from '../hooks/usePersistBalancesAndAllowances' import { balancesAtom } from '../state/balancesAtom' @@ -44,5 +46,5 @@ export function BalancesAndAllowancesUpdater({ account, chainId }: BalancesAndAl setBalances((state) => ({ ...state, values: { ...state.values, ...nativeBalanceState } })) }, [nativeTokenBalance, chainId, setBalances]) - return null + return } diff --git a/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx new file mode 100644 index 0000000000..0cd33ae770 --- /dev/null +++ b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx @@ -0,0 +1,89 @@ +import { useAtom } from 'jotai/index' +import { useEffect, useRef } from 'react' + +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { BigNumber } from '@ethersproject/bignumber' + +import { balancesAtom, balancesCacheAtom } from '../state/balancesAtom' + +export function BalancesCacheUpdater({ chainId }: { chainId: SupportedChainId }) { + const [balances, setBalances] = useAtom(balancesAtom) + const [balancesCache, setBalancesCache] = useAtom(balancesCacheAtom) + const areBalancesRestoredFromCacheRef = useRef(false) + + // Persist into localStorage only non-zero balances + useEffect(() => { + setBalancesCache((state) => { + const balancesValues = balances.values + + const balancesToCache = Object.keys(balancesValues).reduce( + (acc, tokenAddress) => { + const balance = balancesValues[tokenAddress] + + if (balance && !balance.isZero()) { + acc[tokenAddress] = balance.toString() + } + + return acc + }, + {} as Record, + ) + + const currentCache = state[chainId] + // Remove zero balances from the current cache + const updatedCache = Object.keys(currentCache).reduce( + (acc, tokenAddress) => { + if (!balancesValues[tokenAddress]?.isZero()) { + acc[tokenAddress] = currentCache[tokenAddress] + } + + return acc + }, + {} as Record, + ) + + return { + ...state, + [chainId]: { + ...updatedCache, + ...balancesToCache, + }, + } + }) + }, [chainId, balances.values]) + + // Restore balances from cache once + useEffect(() => { + const cache = balancesCache[chainId] + + if (areBalancesRestoredFromCacheRef.current) return + if (!cache) return + + const cacheKeys = Object.keys(cache) + + if (cacheKeys.length === 0) return + + areBalancesRestoredFromCacheRef.current = true + + setBalances((state) => { + return { + isLoading: state.isLoading, + values: { + ...state.values, + ...cacheKeys.reduce( + (acc, tokenAddress) => { + acc[tokenAddress] = BigNumber.from(cache[tokenAddress]) + + return acc + }, + {} as Record, + ), + }, + } + }) + + return + }, [balancesCache, chainId]) + + return null +} diff --git a/libs/common-const/src/types.ts b/libs/common-const/src/types.ts index 33c595ebc7..ed3d668c4a 100644 --- a/libs/common-const/src/types.ts +++ b/libs/common-const/src/types.ts @@ -1,6 +1,8 @@ import { TokenInfo } from '@cowprotocol/types' import { Token } from '@uniswap/sdk-core' +const emptyTokens = [] as string[] + export class TokenWithLogo extends Token { static fromToken(token: Token | TokenInfo, logoURI?: string): TokenWithLogo { return new TokenWithLogo(logoURI, token.chainId, token.address, token.decimals, token.symbol, token.name) @@ -13,8 +15,33 @@ export class TokenWithLogo extends Token { decimals: number, symbol?: string, name?: string, - bypassChecksum?: boolean + bypassChecksum?: boolean, ) { super(chainId, address, decimals, symbol, name, bypassChecksum) } } + +export class LpToken extends TokenWithLogo { + static fromToken(token: Token | TokenInfo): LpToken { + return new LpToken( + token instanceof Token ? emptyTokens : token.tokens || emptyTokens, + token.chainId, + token.address, + token.decimals, + token.symbol, + token.name, + ) + } + + constructor( + public tokens: string[], + chainId: number, + address: string, + decimals: number, + symbol?: string, + name?: string, + bypassChecksum?: boolean, + ) { + super(undefined, chainId, address, decimals, symbol, name, bypassChecksum) + } +} diff --git a/libs/multicall/src/hooks/useMultipleContractSingleData.ts b/libs/multicall/src/hooks/useMultipleContractSingleData.ts index 8610778a29..2d4a03cfb6 100644 --- a/libs/multicall/src/hooks/useMultipleContractSingleData.ts +++ b/libs/multicall/src/hooks/useMultipleContractSingleData.ts @@ -1,7 +1,9 @@ import { useMemo } from 'react' +import type { Multicall3 } from '@cowprotocol/abis' import { useWalletProvider } from '@cowprotocol/wallet-provider' import { Interface, Result } from '@ethersproject/abi' +import type { Web3Provider } from '@ethersproject/providers' import useSWR, { SWRConfiguration, SWRResponse } from 'swr' @@ -35,10 +37,8 @@ export function useMultipleContractSingleData( }, [addresses, callData]) return useSWR<(T | undefined)[] | null>( - ['useMultipleContractSingleData', provider, calls, multicallOptions], - () => { - if (!calls || calls.length === 0 || !provider) return null - + !calls?.length || !provider ? null : [provider, calls, multicallOptions, 'useMultipleContractSingleData'], + async ([provider, calls, multicallOptions]: [Web3Provider, Multicall3.CallStruct[], MultiCallOptions]) => { return multiCall(provider, calls, multicallOptions).then((results) => { return results.map((result) => { try { diff --git a/libs/multicall/src/multicall.ts b/libs/multicall/src/multicall.ts index 8aed7aff7b..abb3616e84 100644 --- a/libs/multicall/src/multicall.ts +++ b/libs/multicall/src/multicall.ts @@ -6,6 +6,7 @@ import { getMulticallContract } from './utils/getMulticallContract' export interface MultiCallOptions { batchSize?: number + consequentExecution?: boolean } /** @@ -16,19 +17,31 @@ export interface MultiCallOptions { export async function multiCall( provider: Web3Provider, calls: Multicall3.CallStruct[], - options: MultiCallOptions = {} + options: MultiCallOptions = {}, ): Promise { - const { batchSize = DEFAULT_BATCH_SIZE } = options + const { batchSize = DEFAULT_BATCH_SIZE, consequentExecution } = options const multicall = getMulticallContract(provider) const batches = splitIntoBatches(calls, batchSize) - const requests = batches.map((batch) => { - return multicall.callStatic.tryAggregate(false, batch) - }) - - return (await Promise.all(requests)).flat() + return consequentExecution + ? batches + .reduce>((acc, batch) => { + return acc.then((results) => { + return multicall.callStatic.tryAggregate(false, batch).then((batchResults) => { + results.push(batchResults) + + return results + }) + }) + }, Promise.resolve([])) + .then((results) => results.flat()) + : Promise.all( + batches.map((batch) => { + return multicall.callStatic.tryAggregate(false, batch) + }), + ).then((res) => res.flat()) } function splitIntoBatches(calls: Multicall3.CallStruct[], batchSize: number): Multicall3.CallStruct[][] { diff --git a/libs/tokens/src/const/lpTokensList.json b/libs/tokens/src/const/lpTokensList.json new file mode 100644 index 0000000000..bffe99e65c --- /dev/null +++ b/libs/tokens/src/const/lpTokensList.json @@ -0,0 +1,32 @@ +[ + { + "priority": 100, + "category": "COW_AMM_LP", + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/cow-amm.json" + }, + { + "priority": 101, + "category": "LP", + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/uniswapv2.json" + }, + { + "priority": 102, + "category": "LP", + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/curve.json" + }, + { + "priority": 103, + "category": "LP", + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/balancerv2.json" + }, + { + "priority": 104, + "category": "LP", + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/sushiswap.json" + }, + { + "priority": 105, + "category": "LP", + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/055c8ccebe59e91874baf8600403a487ad33e93d/src/public/lp-tokens/pancakeswap.json" + } +] diff --git a/libs/tokens/src/const/tokensLists.ts b/libs/tokens/src/const/tokensLists.ts index d30a403e82..f692865510 100644 --- a/libs/tokens/src/const/tokensLists.ts +++ b/libs/tokens/src/const/tokensLists.ts @@ -1,8 +1,14 @@ +import { mapSupportedNetworks } from '@cowprotocol/cow-sdk' + +import lpTokensList from './lpTokensList.json' import tokensList from './tokensList.json' -import { ListsSourcesByNetwork } from '../types' +import { ListSourceConfig, ListsSourcesByNetwork } from '../types' -export const DEFAULT_TOKENS_LISTS: ListsSourcesByNetwork = tokensList +export const DEFAULT_TOKENS_LISTS: ListsSourcesByNetwork = mapSupportedNetworks((chainId) => [ + ...tokensList[chainId], + ...(lpTokensList as Array), +]) export const UNISWAP_TOKENS_LIST = 'https://ipfs.io/ipns/tokens.uniswap.org' diff --git a/libs/tokens/src/hooks/tokens/useAllLpTokens.ts b/libs/tokens/src/hooks/tokens/useAllLpTokens.ts new file mode 100644 index 0000000000..68d94a767e --- /dev/null +++ b/libs/tokens/src/hooks/tokens/useAllLpTokens.ts @@ -0,0 +1,43 @@ +import { useAtomValue } from 'jotai' + +import { LpToken, SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' + +import useSWR from 'swr' + +import { environmentAtom } from '../../state/environmentAtom' +import { listsStatesListAtom } from '../../state/tokenLists/tokenListsStateAtom' +import { TokenListCategory, TokensMap } from '../../types' +import { parseTokenInfo } from '../../utils/parseTokenInfo' +import { tokenMapToListWithLogo } from '../../utils/tokenMapToListWithLogo' + +const fallbackData: LpToken[] = [] + +export function useAllLpTokens(categories: TokenListCategory[] | null): LpToken[] { + const { chainId } = useAtomValue(environmentAtom) + const state = useAtomValue(listsStatesListAtom) + + return useSWR( + categories ? [state, chainId, categories] : null, + ([state, chainId, categories]) => { + const tokensMap = state.reduce((acc, list) => { + if (!list.category || !categories.includes(list.category)) { + return acc + } + + list.list.tokens.forEach((token) => { + const tokenInfo = parseTokenInfo(chainId, token) + const tokenAddressKey = tokenInfo?.address.toLowerCase() + + if (!tokenInfo || !tokenAddressKey) return + + acc[tokenAddressKey] = tokenInfo + }) + + return acc + }, {}) + + return tokenMapToListWithLogo(tokensMap, chainId) as LpToken[] + }, + { ...SWR_NO_REFRESH_OPTIONS, fallbackData }, + ).data +} diff --git a/libs/tokens/src/index.ts b/libs/tokens/src/index.ts index b4315f4d41..6c61741e59 100644 --- a/libs/tokens/src/index.ts +++ b/libs/tokens/src/index.ts @@ -1,10 +1,3 @@ -// Containers -import { userAddedTokenListsAtomv2Migration } from './migrations/userAddedTokenListsAtomv2Migration' - -// Run migrations first of all -// TODO: remove it after 01.04.2024 -userAddedTokenListsAtomv2Migration() - // Updaters export { TokensListsUpdater } from './updaters/TokensListsUpdater' export { UnsupportedTokensUpdater } from './updaters/UnsupportedTokensUpdater' @@ -47,6 +40,7 @@ export { useAreThereTokensWithSameSymbol } from './hooks/tokens/useAreThereToken export { useSearchList } from './hooks/lists/useSearchList' export { useSearchToken } from './hooks/tokens/useSearchToken' export { useSearchNonExistentToken } from './hooks/tokens/useSearchNonExistentToken' +export { useAllLpTokens } from './hooks/tokens/useAllLpTokens' // Utils export { getTokenListViewLink } from './utils/getTokenListViewLink' diff --git a/libs/tokens/src/migrations/userAddedTokenListsAtomv2Migration.ts b/libs/tokens/src/migrations/userAddedTokenListsAtomv2Migration.ts deleted file mode 100644 index dfb65121a1..0000000000 --- a/libs/tokens/src/migrations/userAddedTokenListsAtomv2Migration.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk' - -import { ListsSourcesByNetwork } from '../types' - -/** - * Context: https://github.com/cowprotocol/cowswap/pull/3881#issuecomment-1953522918 - * In v2 atom we added excessive data to the atom, which is not needed and causes localStorage to be bloated. - * To not loose user-added token lists, we need to migrate the data to a new atom version. - */ -export function userAddedTokenListsAtomv2Migration() { - try { - const v2StateRaw = localStorage.getItem('userAddedTokenListsAtom:v2') - if (!v2StateRaw) return - - const v2State = JSON.parse(v2StateRaw) as ListsSourcesByNetwork - - // Remove excessive data - Object.keys(v2State).forEach((chainId) => { - const state = v2State[chainId as unknown as SupportedChainId] - - state.forEach((list) => { - delete (list as never)['list'] - }) - }) - - // Save the new state - localStorage.setItem('userAddedTokenListsAtom:v3', JSON.stringify(v2State)) - } catch (e) { - console.error('userAddedTokenListsAtomv2Migration failed', e) - } - - // Remove the old state - localStorage.removeItem('userAddedTokenListsAtom:v2') -} diff --git a/libs/tokens/src/services/fetchTokenList.ts b/libs/tokens/src/services/fetchTokenList.ts index 4f9b159424..80fb2db075 100644 --- a/libs/tokens/src/services/fetchTokenList.ts +++ b/libs/tokens/src/services/fetchTokenList.ts @@ -21,11 +21,7 @@ export function fetchTokenList(list: ListSourceConfig): Promise { async function fetchTokenListByUrl(list: ListSourceConfig): Promise { return _fetchTokenList(list.source, [list.source]).then((result) => { - return { - ...result, - priority: list.priority, - source: list.source, - } + return listStateFromSourceConfig(result, list) }) } @@ -35,11 +31,7 @@ async function fetchTokenListByEnsName(list: ListSourceConfig): Promise { - return { - ...result, - priority: list.priority, - source: list.source, - } + return listStateFromSourceConfig(result, list) }) } @@ -90,6 +82,15 @@ async function _fetchTokenList(source: string, urls: string[]): Promise { // Remove tokens from the list that don't have valid addresses const tokens = list.tokens.filter(({ address }) => isAddress(address)) diff --git a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts index d983ae7120..3728e1baa7 100644 --- a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts +++ b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts @@ -39,7 +39,7 @@ const curatedListSourceAtom = atom((get) => { export const userAddedListsSourcesAtom = atomWithStorage( 'userAddedTokenListsAtom:v3', mapSupportedNetworks([]), - getJotaiMergerStorage() + getJotaiMergerStorage(), ) export const allListsSourcesAtom = atom((get) => { @@ -57,7 +57,7 @@ export const allListsSourcesAtom = atom((get) => { export const listsStatesByChainAtom = atomWithStorage( 'allTokenListsInfoAtom:v3', mapSupportedNetworks({}), - getJotaiMergerStorage() + getJotaiMergerStorage(), ) export const tokenListsUpdatingAtom = atom(false) diff --git a/libs/tokens/src/state/tokens/allTokensAtom.ts b/libs/tokens/src/state/tokens/allTokensAtom.ts index 855c70dd5a..fde9b92bea 100644 --- a/libs/tokens/src/state/tokens/allTokensAtom.ts +++ b/libs/tokens/src/state/tokens/allTokensAtom.ts @@ -8,11 +8,11 @@ import { userAddedTokensAtom } from './userAddedTokensAtom' import { TokensMap } from '../../types' import { lowerCaseTokensMap } from '../../utils/lowerCaseTokensMap' +import { parseTokenInfo } from '../../utils/parseTokenInfo' import { tokenMapToListWithLogo } from '../../utils/tokenMapToListWithLogo' import { environmentAtom } from '../environmentAtom' import { listsEnabledStateAtom, listsStatesListAtom } from '../tokenLists/tokenListsStateAtom' - export interface TokensByAddress { [address: string]: TokenWithLogo | undefined } @@ -26,12 +26,6 @@ export interface TokensState { inactiveTokens: TokensMap } -interface BridgeInfo { - [chainId: number]: { - tokenAddress: string - } -} - export const tokensStateAtom = atom((get) => { const { chainId } = get(environmentAtom) const listsStatesList = get(listsStatesListAtom) @@ -42,18 +36,10 @@ export const tokensStateAtom = atom((get) => { const isListEnabled = listsEnabledState[list.source] list.list.tokens.forEach((token) => { - const bridgeInfo = token.extensions?.['bridgeInfo'] as never as BridgeInfo | undefined - const currentChainInfo = bridgeInfo?.[chainId] - const bridgeAddress = currentChainInfo?.tokenAddress + const tokenInfo = parseTokenInfo(chainId, token) + const tokenAddressKey = tokenInfo?.address.toLowerCase() - if (token.chainId !== chainId && !bridgeAddress) return - - const tokenAddress = bridgeAddress || token.address - const tokenAddressKey = tokenAddress.toLowerCase() - const tokenInfo: TokenInfo = { - ...token, - address: tokenAddress, - } + if (!tokenInfo || !tokenAddressKey) return if (isListEnabled) { if (!acc.activeTokens[tokenAddressKey]) { @@ -68,7 +54,7 @@ export const tokensStateAtom = atom((get) => { return acc }, - { activeTokens: {}, inactiveTokens: {} } + { activeTokens: {}, inactiveTokens: {} }, ) }) @@ -92,7 +78,7 @@ export const activeTokensAtom = atom((get) => { ...lowerCaseTokensMap(userAddedTokens[chainId]), ...lowerCaseTokensMap(favoriteTokensState[chainId]), }, - chainId + chainId, ) }) diff --git a/libs/tokens/src/types.ts b/libs/tokens/src/types.ts index bce11b429c..d1e6d4a206 100644 --- a/libs/tokens/src/types.ts +++ b/libs/tokens/src/types.ts @@ -2,10 +2,17 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { TokenInfo } from '@cowprotocol/types' import type { TokenList as UniTokenList } from '@uniswap/token-lists' +export enum TokenListCategory { + ERC20 = 'ERC20', + LP = 'LP', + COW_AMM_LP = 'COW_AMM_LP', +} + export type ListSourceConfig = { widgetAppCode?: string priority?: number enabledByDefault?: boolean + category?: TokenListCategory source: string } @@ -17,11 +24,8 @@ export type UnsupportedTokensState = { [tokenAddress: string]: { dateAdded: numb export type ListsEnabledState = { [listId: string]: boolean | undefined } -export interface ListState { - source: string +export interface ListState extends Pick{ list: UniTokenList - widgetAppCode?: string - priority?: number isEnabled?: boolean } diff --git a/libs/tokens/src/updaters/TokensListsUpdater/index.ts b/libs/tokens/src/updaters/TokensListsUpdater/index.ts index acc4285dab..7a882406e3 100644 --- a/libs/tokens/src/updaters/TokensListsUpdater/index.ts +++ b/libs/tokens/src/updaters/TokensListsUpdater/index.ts @@ -2,7 +2,6 @@ import { useAtomValue, useSetAtom } from 'jotai' import { atomWithStorage } from 'jotai/utils' import { useEffect } from 'react' - import { atomWithPartialUpdate, isInjectedWidget } from '@cowprotocol/common-utils' import { getJotaiMergerStorage } from '@cowprotocol/core' import { SupportedChainId, mapSupportedNetworks } from '@cowprotocol/cow-sdk' @@ -20,10 +19,10 @@ import { ListState } from '../../types' const { atom: lastUpdateTimeAtom, updateAtom: updateLastUpdateTimeAtom } = atomWithPartialUpdate( atomWithStorage>( - 'tokens:lastUpdateTimeAtom:v1', + 'tokens:lastUpdateTimeAtom:v2', mapSupportedNetworks(0), - getJotaiMergerStorage() - ) + getJotaiMergerStorage(), + ), ) const swrOptions: SWRConfiguration = { @@ -68,7 +67,7 @@ export function TokensListsUpdater({ chainId: currentChainId, isGeoBlockEnabled return Promise.allSettled(allTokensLists.map(fetchTokenList)).then(getFulfilledResults) }, - swrOptions + swrOptions, ) // Fulfill tokens lists with tokens from fetched lists diff --git a/libs/tokens/src/utils/parseTokenInfo.ts b/libs/tokens/src/utils/parseTokenInfo.ts new file mode 100644 index 0000000000..2c9f2a67a1 --- /dev/null +++ b/libs/tokens/src/utils/parseTokenInfo.ts @@ -0,0 +1,26 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { TokenInfo } from '@cowprotocol/types' +import type { TokenInfo as Erc20TokenInfo } from '@uniswap/token-lists' + +interface BridgeInfo { + [chainId: number]: { + tokenAddress: string + } +} + +export function parseTokenInfo(chainId: SupportedChainId, token: Erc20TokenInfo): TokenInfo | null { + const bridgeInfo = token.extensions?.['bridgeInfo'] as never as BridgeInfo | undefined + const currentChainInfo = bridgeInfo?.[chainId] + const bridgeAddress = currentChainInfo?.tokenAddress + + if (token.chainId !== chainId && !bridgeAddress) return null + + const tokenAddress = bridgeAddress || token.address + const lpTokens = token.extensions?.['tokens'] as string | undefined + + return { + ...token, + address: tokenAddress, + ...(lpTokens ? { tokens: lpTokens.split(',') } : undefined), + } +} diff --git a/libs/tokens/src/utils/tokenMapToListWithLogo.ts b/libs/tokens/src/utils/tokenMapToListWithLogo.ts index ab0345687b..d66c8a1d73 100644 --- a/libs/tokens/src/utils/tokenMapToListWithLogo.ts +++ b/libs/tokens/src/utils/tokenMapToListWithLogo.ts @@ -1,4 +1,4 @@ -import { TokenWithLogo } from '@cowprotocol/common-const' +import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' import { TokensMap } from '../types' @@ -9,5 +9,5 @@ export function tokenMapToListWithLogo(tokenMap: TokensMap, chainId: number): To return Object.values(tokenMap) .filter((token) => token.chainId === chainId) .sort((a, b) => a.symbol.localeCompare(b.symbol)) - .map((token) => TokenWithLogo.fromToken(token, token.logoURI)) + .map((token) => (token.tokens ? LpToken.fromToken(token) : TokenWithLogo.fromToken(token, token.logoURI))) } diff --git a/libs/tokens/src/utils/validateTokenList.ts b/libs/tokens/src/utils/validateTokenList.ts index 98fc447dfe..eac5b01d12 100644 --- a/libs/tokens/src/utils/validateTokenList.ts +++ b/libs/tokens/src/utils/validateTokenList.ts @@ -5,11 +5,11 @@ import type { Ajv, ValidateFunction } from 'ajv' const SYMBOL_AND_NAME_VALIDATION = [ { - const: '', + const: '' }, { - pattern: '^[^<>]+$', - }, + pattern: '^[^<>]+$' + } ] const patchValidationSchema = (schema: any) => ({ @@ -23,17 +23,26 @@ const patchValidationSchema = (schema: any) => ({ symbol: { ...schema.definitions.TokenInfo.properties.symbol, maxLength: 80, - anyOf: SYMBOL_AND_NAME_VALIDATION, + anyOf: SYMBOL_AND_NAME_VALIDATION }, name: { ...schema.definitions.TokenInfo.properties.name, maxLength: 100, - anyOf: SYMBOL_AND_NAME_VALIDATION, - }, - }, + anyOf: SYMBOL_AND_NAME_VALIDATION + } + } }, - }, + ExtensionPrimitiveValue: { + 'anyOf': [{ + 'type': 'string', + 'minLength': 1, + 'maxLength': 420, + 'examples': ['#00000'] + }, { 'type': 'boolean', 'examples': [true] }, { 'type': 'number', 'examples': [15] }, { 'type': 'null' }] + } + } }) + enum ValidationSchema { LIST = 'list', TOKENS = 'tokens', @@ -48,9 +57,9 @@ const validator = new Promise((resolve) => { { ...patchValidationSchema(schema), $id: schema.$id + '#tokens', - required: ['tokens'], + required: ['tokens'] }, - ValidationSchema.TOKENS, + ValidationSchema.TOKENS ) resolve(validator) }) diff --git a/libs/types/src/common.ts b/libs/types/src/common.ts index 50ff3e3638..0228b65166 100644 --- a/libs/types/src/common.ts +++ b/libs/types/src/common.ts @@ -23,4 +23,5 @@ export type TokenInfo = { decimals: number symbol: string logoURI?: string + tokens?: string[] } diff --git a/libs/ui/src/containers/ExternalLink/index.tsx b/libs/ui/src/containers/ExternalLink/index.tsx index 03bec48e6c..3dff19f6a2 100644 --- a/libs/ui/src/containers/ExternalLink/index.tsx +++ b/libs/ui/src/containers/ExternalLink/index.tsx @@ -28,27 +28,7 @@ export const StyledLink = styled.a` text-decoration: none; } ` -const LinkIconWrapper = styled.a` - text-decoration: none; - cursor: pointer; - align-items: center; - justify-content: center; - display: flex; - - :hover { - text-decoration: none; - opacity: 0.7; - } - :focus { - outline: none; - text-decoration: none; - } - - :active { - text-decoration: none; - } -` export const LinkIcon = styled(LinkIconFeather as any)` height: 16px; width: 18px; @@ -114,24 +94,3 @@ function handleClickExternalLink(cowAnalytics: CowAnalytics, event: React.MouseE }, }) } - -export function ExternalLinkIcon({ - target = '_blank', - href, - rel = 'noopener noreferrer', - ...rest -}: Omit, 'as' | 'ref' | 'onClick'> & { href: string }) { - const cowAnalytics = useCowAnalytics() - - return ( - handleClickExternalLink(cowAnalytics, e)} - {...rest} - > - - - ) -} diff --git a/libs/ui/src/enum.ts b/libs/ui/src/enum.ts index 7b083a0990..614921d50e 100644 --- a/libs/ui/src/enum.ts +++ b/libs/ui/src/enum.ts @@ -27,6 +27,7 @@ export enum UI { COLOR_TEXT_PAPER = '--cow-color-text-paper', COLOR_TEXT_OPACITY_70 = '--cow-color-text-opacity-70', + COLOR_TEXT_OPACITY_60 = '--cow-color-text-opacity-60', COLOR_TEXT_OPACITY_50 = '--cow-color-text-opacity-50', COLOR_TEXT_OPACITY_25 = '--cow-color-text-opacity-25', COLOR_TEXT_OPACITY_10 = '--cow-color-text-opacity-10', diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index 3a41db39e3..26f4b1e44b 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -17,7 +17,7 @@ export * from './pure/ProductLogo' export * from './pure/MenuBar' export * from './pure/InfoTooltip' export * from './pure/HelpTooltip' -export { loadingOpacityMixin, LoadingRows } from './pure/Loader/styled' +export { loadingOpacityMixin, LoadingRows, LoadingRowSmall } from './pure/Loader/styled' export * from './pure/Row' export * from './pure/FiatAmount' export * from './pure/TokenSymbol' diff --git a/libs/ui/src/pure/InfoTooltip/index.tsx b/libs/ui/src/pure/InfoTooltip/index.tsx index 03ad62fb31..74481128d1 100644 --- a/libs/ui/src/pure/InfoTooltip/index.tsx +++ b/libs/ui/src/pure/InfoTooltip/index.tsx @@ -6,20 +6,26 @@ import styled from 'styled-components/macro' import { UI } from '../../enum' import { HoverTooltip, TooltipContainer } from '../Tooltip' -const StyledIcon = styled.div` - display: inline-block; +const StyledIcon = styled.div<{ size: number }>` + display: inline-flex; + align-items: center; color: inherit; + opacity: 0.6; + transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; + height: ${({ size }) => size}px; + + &:hover { + opacity: 1; + } + + > span { + margin-right: 4px; + } > svg { - opacity: 0.6; stroke: currentColor; line-height: 0; vertical-align: middle; - transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; - - :hover { - opacity: 1; - } } ` @@ -31,17 +37,21 @@ const StyledTooltipContainer = styled(TooltipContainer)` ` export interface InfoTooltipProps { - content: ReactNode + // @deprecated use children instead + content?: ReactNode + children?: ReactNode size?: number className?: string + preText?: string } -export function InfoTooltip({ content, className, size = 16 }: InfoTooltipProps) { - const tooltipContent = {content} +export function InfoTooltip({ content, children, className, size = 16, preText }: InfoTooltipProps) { + const tooltipContent = {children || content} return ( - + + {preText && {preText}} diff --git a/libs/ui/src/pure/Loader/styled.tsx b/libs/ui/src/pure/Loader/styled.tsx index e942ea4054..798d0735e6 100644 --- a/libs/ui/src/pure/Loader/styled.tsx +++ b/libs/ui/src/pure/Loader/styled.tsx @@ -30,6 +30,11 @@ export const LoadingRows = styled.div` } ` +export const LoadingRowSmall = styled.div` + width: 24px; + height: 10px !important; +` + export const loadingOpacityMixin = css<{ $loading: boolean }>` filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')}; opacity: ${({ $loading }) => ($loading ? '0.4' : '1')}; diff --git a/libs/ui/src/theme/ThemeColorVars.tsx b/libs/ui/src/theme/ThemeColorVars.tsx index 03acc39dcf..6a4c8d6a0f 100644 --- a/libs/ui/src/theme/ThemeColorVars.tsx +++ b/libs/ui/src/theme/ThemeColorVars.tsx @@ -41,6 +41,7 @@ export const ThemeColorVars = css` ${UI.COLOR_TEXT_PAPER}: ${({ theme }) => getContrastText(theme.paper, theme.text)}; ${UI.COLOR_TEXT_OPACITY_70}: ${({ theme }) => transparentize(theme.text, 0.3)}; + ${UI.COLOR_TEXT_OPACITY_60}: ${({ theme }) => transparentize(theme.text, 0.4)}; ${UI.COLOR_TEXT_OPACITY_50}: ${({ theme }) => transparentize(theme.text, 0.5)}; ${UI.COLOR_TEXT_OPACITY_25}: ${({ theme }) => transparentize(theme.text, 0.75)}; ${UI.COLOR_TEXT_OPACITY_10}: ${({ theme }) => transparentize(theme.text, 0.9)}; From 37d04c168fff949ec453052feabf3e8d8795cfd1 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Tue, 29 Oct 2024 08:40:46 +0000 Subject: [PATCH 064/116] fix: sound widget logic (#5051) --- .../src/modules/sounds/utils/sound.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts index 5fd13b7908..f2f16959a0 100644 --- a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts +++ b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts @@ -88,13 +88,26 @@ function getWidgetSoundUrl(type: SoundType): string | null | undefined { function getAudio(type: SoundType): HTMLAudioElement { const widgetSound = getWidgetSoundUrl(type) - - if (widgetSound === null) { - return EMPTY_SOUND + const isWidgetMode = isInjectedWidget() + + if (isWidgetMode) { + if (widgetSound === null) { + return EMPTY_SOUND + } + // If in widget mode, use widget sound if provided, otherwise use default sound + const soundPath = widgetSound || DEFAULT_COW_SOUNDS[type] + let sound = SOUND_CACHE[soundPath] + + if (!sound) { + sound = new Audio(soundPath) + SOUND_CACHE[soundPath] = sound + } + + return sound } - // Widget sounds take precedence over themed sounds - const soundPath = widgetSound || getThemeBasedSound(type) + // If not in widget mode, use theme-based sound + const soundPath = getThemeBasedSound(type) let sound = SOUND_CACHE[soundPath] if (!sound) { From 739b7dba93ad55b47e4c052c47867bc3259b50f2 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 29 Oct 2024 14:00:00 +0500 Subject: [PATCH 065/116] chore: release main (#5052) --- .release-please-manifest.json | 18 +++++++++--------- apps/cow-fi/CHANGELOG.md | 7 +++++++ apps/cow-fi/package.json | 2 +- apps/cowswap-frontend/CHANGELOG.md | 22 ++++++++++++++++++++++ apps/cowswap-frontend/package.json | 2 +- apps/widget-configurator/CHANGELOG.md | 7 +++++++ apps/widget-configurator/package.json | 2 +- libs/assets/CHANGELOG.md | 8 ++++++++ libs/assets/package.json | 2 +- libs/common-const/CHANGELOG.md | 7 +++++++ libs/common-const/package.json | 2 +- libs/hook-dapp-lib/CHANGELOG.md | 7 +++++++ libs/hook-dapp-lib/package.json | 2 +- libs/types/CHANGELOG.md | 7 +++++++ libs/types/package.json | 2 +- libs/ui/CHANGELOG.md | 9 +++++++++ libs/ui/package.json | 2 +- libs/widget-lib/CHANGELOG.md | 7 +++++++ libs/widget-lib/package.json | 2 +- 19 files changed, 99 insertions(+), 18 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4fd2f7cbd8..276d8577e5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,13 +1,13 @@ { - "apps/cowswap-frontend": "1.86.1", + "apps/cowswap-frontend": "1.87.0", "apps/explorer": "2.36.0", "libs/permit-utils": "0.4.0", - "libs/widget-lib": "0.16.0", + "libs/widget-lib": "0.17.0", "libs/widget-react": "0.11.0", - "apps/widget-configurator": "1.8.0", + "apps/widget-configurator": "1.9.0", "libs/analytics": "1.8.0", - "libs/assets": "1.8.0", - "libs/common-const": "1.8.0", + "libs/assets": "1.9.0", + "libs/common-const": "1.9.0", "libs/common-hooks": "1.4.0", "libs/common-utils": "1.7.2", "libs/core": "1.3.0", @@ -15,14 +15,14 @@ "libs/events": "1.5.0", "libs/snackbars": "1.1.0", "libs/tokens": "1.10.0", - "libs/types": "1.2.0", - "libs/ui": "1.11.0", + "libs/types": "1.3.0", + "libs/ui": "1.12.0", "libs/wallet": "1.6.1", - "apps/cow-fi": "1.15.0", + "apps/cow-fi": "1.16.0", "libs/wallet-provider": "1.0.0", "libs/ui-utils": "1.1.0", "libs/abis": "1.2.0", "libs/balances-and-allowances": "1.0.0", "libs/iframe-transport": "1.0.0", - "libs/hook-dapp-lib": "1.2.0" + "libs/hook-dapp-lib": "1.3.0" } diff --git a/apps/cow-fi/CHANGELOG.md b/apps/cow-fi/CHANGELOG.md index 26e4c30a12..93a1947ddc 100644 --- a/apps/cow-fi/CHANGELOG.md +++ b/apps/cow-fi/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.16.0](https://github.com/cowprotocol/cowswap/compare/cow-fi-v1.15.0...cow-fi-v1.16.0) (2024-10-29) + + +### Features + +* fix path ([#5022](https://github.com/cowprotocol/cowswap/issues/5022)) ([92ad33c](https://github.com/cowprotocol/cowswap/commit/92ad33c5d6a280bd1a9e40e6cf49486f6f71130a)) + ## [1.15.0](https://github.com/cowprotocol/cowswap/compare/cow-fi-v1.14.0...cow-fi-v1.15.0) (2024-10-10) diff --git a/apps/cow-fi/package.json b/apps/cow-fi/package.json index d3de75f713..c6be037a90 100644 --- a/apps/cow-fi/package.json +++ b/apps/cow-fi/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cow-fi", - "version": "1.15.0", + "version": "1.16.0", "description": "CoW DAO website", "main": "index.js", "author": "", diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index 0f09d39910..dfe323fc57 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [1.87.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.86.1...cowswap-v1.87.0) (2024-10-29) + + +### Features + +* add vampire attack banner ([#4981](https://github.com/cowprotocol/cowswap/issues/4981)) ([5246046](https://github.com/cowprotocol/cowswap/commit/52460461d6cc80635a25aefe5b119dbd7de1fb69)) +* **halloween:** add Halloween mode ([#5036](https://github.com/cowprotocol/cowswap/issues/5036)) ([791796d](https://github.com/cowprotocol/cowswap/commit/791796d139828f3dd0657222cbf98a5ce93ff321)) +* **hook-store:** create bundle hooks tenderly simulation ([#4943](https://github.com/cowprotocol/cowswap/issues/4943)) ([435bfdf](https://github.com/cowprotocol/cowswap/commit/435bfdfa3e68cea1652bc00dcf5908bbc991d7b1)) +* **swap-deadline:** higher swap deadline ([#5002](https://github.com/cowprotocol/cowswap/issues/5002)) ([f6f6f8c](https://github.com/cowprotocol/cowswap/commit/f6f6f8cb9c8df72857d55f42d1e521a6784f9126)) + + +### Bug Fixes + +* fix bad merge ([1abd825](https://github.com/cowprotocol/cowswap/commit/1abd82527dc1f96d6897533d750dcc6f2a51e7a0)) +* fix trade type selector mobile view ([#5023](https://github.com/cowprotocol/cowswap/issues/5023)) ([661cf2f](https://github.com/cowprotocol/cowswap/commit/661cf2fcffa1b0e329a6df905c5949ee71ee24c7)) +* **smart-slippage:** replace volatity with trade size on tooltips ([#5012](https://github.com/cowprotocol/cowswap/issues/5012)) ([9308fc1](https://github.com/cowprotocol/cowswap/commit/9308fc1e35ce5ecfdc69c76974136182352eeca0)) +* sound widget logic ([#5051](https://github.com/cowprotocol/cowswap/issues/5051)) ([37d04c1](https://github.com/cowprotocol/cowswap/commit/37d04c168fff949ec453052feabf3e8d8795cfd1)) +* **swap:** disable partial fills ([#5016](https://github.com/cowprotocol/cowswap/issues/5016)) ([cbbeb8b](https://github.com/cowprotocol/cowswap/commit/cbbeb8bc3d89796da989fd4b17a6eb6e3a4629a4)) +* **swap:** fix safe eth-flow ([#5041](https://github.com/cowprotocol/cowswap/issues/5041)) ([09f5124](https://github.com/cowprotocol/cowswap/commit/09f512407f8a37d49ccd422e951da20e6733afc4)) +* **swap:** reset widget start after successful swap ([#5047](https://github.com/cowprotocol/cowswap/issues/5047)) ([a062ff5](https://github.com/cowprotocol/cowswap/commit/a062ff5309c89fa3d1cddb56bc85a0a0badf0ca5)) +* **trade:** use recipient address in order data ([#5040](https://github.com/cowprotocol/cowswap/issues/5040)) ([229f243](https://github.com/cowprotocol/cowswap/commit/229f243bd834da7d962c64bf151b5cf5db644259)) + ## [1.86.1](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.86.0...cowswap-v1.86.1) (2024-10-18) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index 457d76151a..e0510774fe 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.86.1", + "version": "1.87.0", "description": "CoW Swap", "main": "index.js", "author": "", diff --git a/apps/widget-configurator/CHANGELOG.md b/apps/widget-configurator/CHANGELOG.md index 06b0a94e05..724c8ba36a 100644 --- a/apps/widget-configurator/CHANGELOG.md +++ b/apps/widget-configurator/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.9.0](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.8.0...widget-configurator-v1.9.0) (2024-10-29) + + +### Features + +* setup vampire attack widget ([#4950](https://github.com/cowprotocol/cowswap/issues/4950)) ([99c4c42](https://github.com/cowprotocol/cowswap/commit/99c4c42aec60a734a37926935be5dca6cd4cf11c)) + ## [1.8.0](https://github.com/cowprotocol/cowswap/compare/widget-configurator-v1.7.2...widget-configurator-v1.8.0) (2024-10-18) diff --git a/apps/widget-configurator/package.json b/apps/widget-configurator/package.json index 8efbe145c2..fece0abc67 100644 --- a/apps/widget-configurator/package.json +++ b/apps/widget-configurator/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-configurator", - "version": "1.8.0", + "version": "1.9.0", "description": "CoW Swap widget configurator", "main": "src/main.tsx", "author": "", diff --git a/libs/assets/CHANGELOG.md b/libs/assets/CHANGELOG.md index 7474b703c8..88025e4193 100644 --- a/libs/assets/CHANGELOG.md +++ b/libs/assets/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.9.0](https://github.com/cowprotocol/cowswap/compare/assets-v1.8.0...assets-v1.9.0) (2024-10-29) + + +### Features + +* add vampire attack banner ([#4981](https://github.com/cowprotocol/cowswap/issues/4981)) ([5246046](https://github.com/cowprotocol/cowswap/commit/52460461d6cc80635a25aefe5b119dbd7de1fb69)) +* **halloween:** add Halloween mode ([#5036](https://github.com/cowprotocol/cowswap/issues/5036)) ([791796d](https://github.com/cowprotocol/cowswap/commit/791796d139828f3dd0657222cbf98a5ce93ff321)) + ## [1.8.0](https://github.com/cowprotocol/cowswap/compare/assets-v1.7.0...assets-v1.8.0) (2024-09-17) diff --git a/libs/assets/package.json b/libs/assets/package.json index ba11fd694e..a5cade5ca5 100644 --- a/libs/assets/package.json +++ b/libs/assets/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/assets", - "version": "1.8.0", + "version": "1.9.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/common-const/CHANGELOG.md b/libs/common-const/CHANGELOG.md index f47c4b779f..b47cf7ba50 100644 --- a/libs/common-const/CHANGELOG.md +++ b/libs/common-const/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.9.0](https://github.com/cowprotocol/cowswap/compare/common-const-v1.8.0...common-const-v1.9.0) (2024-10-29) + + +### Features + +* **halloween:** add Halloween mode ([#5036](https://github.com/cowprotocol/cowswap/issues/5036)) ([791796d](https://github.com/cowprotocol/cowswap/commit/791796d139828f3dd0657222cbf98a5ce93ff321)) + ## [1.8.0](https://github.com/cowprotocol/cowswap/compare/common-const-v1.7.0...common-const-v1.8.0) (2024-10-18) diff --git a/libs/common-const/package.json b/libs/common-const/package.json index b6222fc68f..5e80a4a585 100644 --- a/libs/common-const/package.json +++ b/libs/common-const/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/common-const", - "version": "1.8.0", + "version": "1.9.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/hook-dapp-lib/CHANGELOG.md b/libs/hook-dapp-lib/CHANGELOG.md index 391264f286..d2935ad208 100644 --- a/libs/hook-dapp-lib/CHANGELOG.md +++ b/libs/hook-dapp-lib/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.3.0](https://github.com/cowprotocol/cowswap/compare/hook-dapp-lib-v1.2.0...hook-dapp-lib-v1.3.0) (2024-10-29) + + +### Features + +* **hook-store:** create bundle hooks tenderly simulation ([#4943](https://github.com/cowprotocol/cowswap/issues/4943)) ([435bfdf](https://github.com/cowprotocol/cowswap/commit/435bfdfa3e68cea1652bc00dcf5908bbc991d7b1)) + ## [1.2.0](https://github.com/cowprotocol/cowswap/compare/hook-dapp-lib-v1.1.0...hook-dapp-lib-v1.2.0) (2024-10-18) diff --git a/libs/hook-dapp-lib/package.json b/libs/hook-dapp-lib/package.json index f9dc713435..68806134a5 100644 --- a/libs/hook-dapp-lib/package.json +++ b/libs/hook-dapp-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/hook-dapp-lib", - "version": "1.2.0", + "version": "1.3.0", "type": "commonjs", "description": "CoW Swap Hook Dapp Library. Allows you to develop pre/post hooks dapps for CoW Protocol.", "main": "index.js", diff --git a/libs/types/CHANGELOG.md b/libs/types/CHANGELOG.md index 8a3f90f0a4..eda2d52621 100644 --- a/libs/types/CHANGELOG.md +++ b/libs/types/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.3.0](https://github.com/cowprotocol/cowswap/compare/types-v1.2.0...types-v1.3.0) (2024-10-29) + + +### Features + +* setup vampire attack widget ([#4950](https://github.com/cowprotocol/cowswap/issues/4950)) ([99c4c42](https://github.com/cowprotocol/cowswap/commit/99c4c42aec60a734a37926935be5dca6cd4cf11c)) + ## [1.2.0](https://github.com/cowprotocol/cowswap/compare/types-v1.1.0...types-v1.2.0) (2024-09-17) diff --git a/libs/types/package.json b/libs/types/package.json index 2d68cfd71b..f56e8b501a 100644 --- a/libs/types/package.json +++ b/libs/types/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/types", - "version": "1.2.0", + "version": "1.3.0", "type": "commonjs", "description": "CoW Swap events", "main": "index.js", diff --git a/libs/ui/CHANGELOG.md b/libs/ui/CHANGELOG.md index 1e00acf735..5969b9e077 100644 --- a/libs/ui/CHANGELOG.md +++ b/libs/ui/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.12.0](https://github.com/cowprotocol/cowswap/compare/ui-v1.11.0...ui-v1.12.0) (2024-10-29) + + +### Features + +* add vampire attack banner ([#4981](https://github.com/cowprotocol/cowswap/issues/4981)) ([5246046](https://github.com/cowprotocol/cowswap/commit/52460461d6cc80635a25aefe5b119dbd7de1fb69)) +* **halloween:** add Halloween mode ([#5036](https://github.com/cowprotocol/cowswap/issues/5036)) ([791796d](https://github.com/cowprotocol/cowswap/commit/791796d139828f3dd0657222cbf98a5ce93ff321)) +* setup vampire attack widget ([#4950](https://github.com/cowprotocol/cowswap/issues/4950)) ([99c4c42](https://github.com/cowprotocol/cowswap/commit/99c4c42aec60a734a37926935be5dca6cd4cf11c)) + ## [1.11.0](https://github.com/cowprotocol/cowswap/compare/ui-v1.10.1...ui-v1.11.0) (2024-10-18) diff --git a/libs/ui/package.json b/libs/ui/package.json index ac6448fab6..1458c7fdd0 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/ui", - "version": "1.11.0", + "version": "1.12.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/widget-lib/CHANGELOG.md b/libs/widget-lib/CHANGELOG.md index 4b9aa025fe..0cf8d3358e 100644 --- a/libs/widget-lib/CHANGELOG.md +++ b/libs/widget-lib/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.17.0](https://github.com/cowprotocol/cowswap/compare/widget-lib-v0.16.0...widget-lib-v0.17.0) (2024-10-29) + + +### Features + +* setup vampire attack widget ([#4950](https://github.com/cowprotocol/cowswap/issues/4950)) ([99c4c42](https://github.com/cowprotocol/cowswap/commit/99c4c42aec60a734a37926935be5dca6cd4cf11c)) + ## [0.16.0](https://github.com/cowprotocol/cowswap/compare/widget-lib-v0.15.0...widget-lib-v0.16.0) (2024-10-18) diff --git a/libs/widget-lib/package.json b/libs/widget-lib/package.json index acfa1891f2..0792435472 100644 --- a/libs/widget-lib/package.json +++ b/libs/widget-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/widget-lib", - "version": "0.16.0", + "version": "0.17.0", "type": "commonjs", "description": "CoW Swap Widget Library. Allows you to easily embed a CoW Swap widget on your website.", "main": "index.js", From b66d2068a9f3bcaddc8da7df5499c17fc05f693f Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 29 Oct 2024 14:00:56 +0500 Subject: [PATCH 066/116] feat(yield): use lp-token in widget (#5013) * feat(yield): setup yield widget * fix(yield): display correct output amount * feat(trade-quote): support fast quote requests * feat(yield): display trade buttons * feat(yield): display confirm details * feat(yield): scope context to trade * feat(yield): do trade after confirmation * feat(yield): display order progress bar * refactor: move useIsEoaEthFlow to trade module * refactor: move hooks to tradeSlippage module * refactor: rename swapSlippage to tradeSlippage * feat(trade-slippage): split slippage state by trade type * refactor: unlink TransactionSettings from swap module * refactor: use reach modal in Settings component * refactor: move Settings component in trade module * feat(yield): add settings widget * feat(yield): use deadline value from settings * fix(trade-quote): skip fast quote if it slower than optimal * refactor: generalise TradeRateDetails from swap module * refactor: move TradeRateDetails into tradeWidgetAddons module * refactor: move SettingsTab into tradeWidgetAddons module * refactor(swap): generalise useHighFeeWarning() * refactor(swap): move hooks to trade module * refactor: move HighFeeWarning to trade widget addons * refactor: make HighFeeWarning independent * feat(yield): display trade warnings ZeroApprovalWarning and HighFeeWarning * refactor(trade): generalise NoImpactWarning * refactor(trade): generalise ZeroApprovalWarning * refactor(trade): generalise bundle tx banners * refactor: extract TradeWarnings * chore: fix yield form displaying * refactor(swap): generalise trade flow * feat(yield): support safe bundle swaps * chore: hide yield under ff * chore: remove lazy loading * chore: fix imports * fix: generalize smart slippage usage * fix: don't sync url params while navigating with yield * feat: open settings menu on slippage click * chore: update btn text * fix: make slippage settings through * chore: merge develop * chore: fix yield widget * chore: remove old migration * feat: add default lp-token lists * feat(tokens): display custom token selector for Yield widget * feat(tokens): display lp-token lists * feat: display lp-token logo * chore: slippage label * fix: fix default trade state in menu * chore: fix lint * feat: fetch lp-token balances Also added optimization to the balances updater, because node started firing 429 error * feat: reuse virtual list for lp-tokens list * chore: adjust balances updater * chore: add yield in widget conf * chore: reset balances only when account changed * feat: cache balances into localStorage * feat: display cached token balances * feat: link to create pool * chore: merge develop * chore: fix hooks trade state * fix: fix smart slippage displaying * chore: center slippage banner * chore: condition to displayCreatePoolBanner * fix: poll lp-token balances only in yield widget * feat(yield): persist pools info * feat(yield): allow using lp-tokens in widget * feat(yield): display lp-token logo in widget * feat(yield): display pool apy * chore: fix lp token logo * feat(yield): pool info page * chore: fix build * chore(yield): fix balance loading state * chore(yield): fix pools info fetching * chore: fix yield menu link * chore: format pool displayed values * chore: always use address for lp-tokens in url * fix: fix lp-tokens usage * chore: fix lint * chore: fix apr default value * feat: modify pool token list layout (#5014) * feat: modify pool token list layout * feat: apply mobile widgetlinks menu * feat: add padding to pool token balance --------- Co-authored-by: Alexandr Kazachenko * chore: link to create pool * chore: merge changes * chore: fix balances cache --------- Co-authored-by: fairlight <31534717+fairlighteth@users.noreply.github.com> --- .../LpBalancesAndAllowancesUpdater.tsx | 6 +- .../application/containers/App/Updaters.tsx | 8 +- .../containers/LpTokenListsWidget/index.tsx | 76 +++++++----- .../containers/LpTokenPage/index.tsx | 111 ++++++++++++++++++ .../containers/LpTokenPage/styled.tsx | 77 ++++++++++++ .../containers/SelectTokenWidget/index.tsx | 40 ++++++- .../tokensList/pure/LpTokenLists/index.tsx | 100 ++++++++-------- .../tokensList/pure/LpTokenLists/styled.ts | 55 ++------- .../tokensList/pure/ModalHeader/index.tsx | 4 +- .../pure/SelectTokenModal/index.cosmos.tsx | 4 + .../pure/SelectTokenModal/index.tsx | 75 +++++++----- .../tokensList/state/selectTokenWidgetAtom.ts | 3 +- .../containers/TradeWidgetLinks/index.tsx | 29 ++--- .../trade/hooks/useAutoImportTokensState.ts | 6 +- .../hooks/useNavigateOnCurrencySelection.ts | 13 +- .../yield/hooks/useLpTokensWithBalances.ts | 35 ++++++ .../src/modules/yield/hooks/usePoolsInfo.ts | 7 ++ .../src/modules/yield/shared.ts | 3 + .../src/modules/yield/state/poolsInfoAtom.ts | 65 ++++++++++ .../yield/updaters/PoolsInfoUpdater/index.tsx | 54 +++++++++ .../updaters/PoolsInfoUpdater/mockPoolInfo.ts | 18 +++ .../updaters/BalancesAndAllowancesUpdater.tsx | 9 +- libs/tokens/src/const/tokensLists.ts | 4 +- .../hooks/tokens/useTokenBySymbolOrAddress.ts | 6 +- libs/tokens/src/index.ts | 1 + libs/tokens/src/pure/TokenLogo/index.tsx | 101 ++++++++++++---- libs/tokens/src/state/environmentAtom.ts | 3 +- .../state/tokenLists/tokenListsStateAtom.ts | 10 +- libs/tokens/src/state/tokens/allTokensAtom.ts | 28 ++++- libs/tokens/src/types.ts | 4 +- .../src/updaters/TokensListsUpdater/index.ts | 11 +- 31 files changed, 743 insertions(+), 223 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/usePoolsInfo.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/shared.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/state/poolsInfoAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts diff --git a/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx b/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx index 7661ea860b..f5b31128ac 100644 --- a/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx +++ b/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react' import { usePersistBalancesAndAllowances } from '@cowprotocol/balances-and-allowances' import { SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' import type { SupportedChainId } from '@cowprotocol/cow-sdk' -import { TokenListCategory, useAllLpTokens } from '@cowprotocol/tokens' +import { LP_TOKEN_LIST_CATEGORIES, useAllLpTokens } from '@cowprotocol/tokens' import ms from 'ms.macro' @@ -16,15 +16,13 @@ const LP_MULTICALL_OPTIONS = { consequentExecution: true } // We start the updater with a delay const LP_UPDATER_START_DELAY = ms`3s` -const LP_CATEGORIES = [TokenListCategory.LP, TokenListCategory.COW_AMM_LP] - export interface BalancesAndAllowancesUpdaterProps { account: string | undefined chainId: SupportedChainId enablePolling: boolean } export function LpBalancesAndAllowancesUpdater({ account, chainId, enablePolling }: BalancesAndAllowancesUpdaterProps) { - const allLpTokens = useAllLpTokens(LP_CATEGORIES) + const allLpTokens = useAllLpTokens(LP_TOKEN_LIST_CATEGORIES) const [isUpdaterPaused, setIsUpdaterPaused] = useState(true) const lpTokenAddresses = useMemo(() => allLpTokens.map((token) => token.address), [allLpTokens]) diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index 4b42b461c9..e6753d580d 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -15,6 +15,7 @@ import { EthFlowDeadlineUpdater } from 'modules/swap/state/EthFlow/updaters' import { useOnTokenListAddingError } from 'modules/tokensList' import { TradeType, useTradeTypeInfo } from 'modules/trade' import { UsdPricesUpdater } from 'modules/usdAmount' +import { PoolsInfoUpdater } from 'modules/yield/shared' import { ProgressBarV2ExecutingOrdersUpdater } from 'common/hooks/orderProgressBarV2' import { TotalSurplusUpdater } from 'common/state/totalSurplusState' @@ -69,7 +70,11 @@ export function Updaters() { - + + ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx index 830fa46f17..57defdf16b 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx @@ -1,23 +1,30 @@ -import { ReactNode, useState } from 'react' +import { ReactNode, useMemo } from 'react' import { useTokensBalances } from '@cowprotocol/balances-and-allowances' -import { TokenListCategory, useAllLpTokens, useTokensByAddressMap } from '@cowprotocol/tokens' +import { TokenWithLogo } from '@cowprotocol/common-const' +import { getTokenSearchFilter, LP_TOKEN_LIST_CATEGORIES, TokenListCategory, useAllLpTokens } from '@cowprotocol/tokens' import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' +import { usePoolsInfo } from 'modules/yield/shared' + import { TabButton, TabsContainer } from './styled' import { LpTokenLists } from '../../pure/LpTokenLists' +import { tokensListSorter } from '../../utils/tokensListSorter' -interface LpTokenListsProps { +interface LpTokenListsProps { + account: string | undefined children: ReactNode + search: string + onSelectToken(token: TokenWithLogo): void + openPoolPage(poolAddress: string): void + tokenListCategoryState: [T, (category: T) => void] } const tabs = [ { id: 'all', title: 'All', value: null }, - { id: 'pool', title: 'Pool tokens', value: [TokenListCategory.LP, TokenListCategory.COW_AMM_LP] }, - { - id: 'cow-amm', - title: ( + { id: 'pool', title: 'Pool tokens', value: LP_TOKEN_LIST_CATEGORIES }, + { id: 'cow-amm', title: ( <> {' '} CoW AMM only - ), - value: [TokenListCategory.COW_AMM_LP], - }, + ), value: [TokenListCategory.COW_AMM_LP] }, ] -export function LpTokenListsWidget({ children }: LpTokenListsProps) { - const [listsCategories, setListsCategories] = useState(null) +export function LpTokenListsWidget({ + account, + search, + children, + onSelectToken, + openPoolPage, + tokenListCategoryState, +}: LpTokenListsProps) { + const [listsCategories, setListsCategories] = tokenListCategoryState const lpTokens = useAllLpTokens(listsCategories) - const tokensByAddress = useTokensByAddressMap() const balancesState = useTokensBalances() + const poolsInfo = usePoolsInfo() + + const balances = balancesState.values + + const sortedTokens = useMemo(() => { + const filter = getTokenSearchFilter(search) + + return balances ? lpTokens.filter(filter).sort(tokensListSorter(balances)) : lpTokens + }, [lpTokens, balances, search]) return ( <> - {tabs.map((tab) => ( - setListsCategories(tab.value)}> - {tab.title} - - ))} + {tabs.map((tab) => { + return ( + setListsCategories(tab.value)} + > + {tab.title} + + ) + })} {listsCategories === null ? ( children - ) : lpTokens.length === 0 ? ( - ) : ( )} diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/index.tsx new file mode 100644 index 0000000000..460ee8db8e --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/index.tsx @@ -0,0 +1,111 @@ +import { TokenWithLogo } from '@cowprotocol/common-const' +import { ExplorerDataType, getExplorerLink, shortenAddress } from '@cowprotocol/common-utils' +import { TokenLogo, useTokensByAddressMap } from '@cowprotocol/tokens' +import { ExternalLink, TokenSymbol } from '@cowprotocol/ui' + +import { usePoolsInfo } from 'modules/yield/shared' + +import { + InfoRow, + InfoTable, + SelectButton, + StyledTokenName, + StyledTokenSymbol, + TokenInfoWrapper, + TokenWrapper, + Wrapper, +} from './styled' + +import { ModalHeader } from '../../pure/ModalHeader' + +function renderValue(value: T | undefined, template: (v: T) => string, defaultValue?: string): string | undefined { + return value ? template(value) : defaultValue +} + +interface LpTokenPageProps { + poolAddress: string + onBack(): void + onDismiss(): void + onSelectToken(token: TokenWithLogo): void +} + +export function LpTokenPage({ poolAddress, onBack, onDismiss, onSelectToken }: LpTokenPageProps) { + const poolsInfo = usePoolsInfo() + const tokensByAddress = useTokensByAddressMap() + + const token = tokensByAddress[poolAddress] + const info = poolsInfo?.[poolAddress]?.info + + return ( + + + Pool description + + {token && ( + + + +
    +
    + +
    + +
    +
    +
    + { + onDismiss() + onSelectToken(token) + }} + > + Select + +
    +
    + )} + + +
    Symbol
    +
    + +
    +
    + +
    Fee tier
    +
    + {renderValue(info?.feeTier, (t) => `${t}%`, '-')} +
    +
    + +
    Volume (24h)
    +
    + {renderValue(info?.volume24h, (t) => `$${t}`, '-')} +
    +
    + +
    APY
    +
    + {renderValue(info?.apy, (t) => `${t}%`, '-')} +
    +
    + +
    TVL
    +
    + {renderValue(info?.tvl, (t) => `$${t}`, '-')} +
    +
    + +
    Pool address
    +
    + {token && ( + + {shortenAddress(token.address)} ↗ + + )} +
    +
    +
    +
    + ) +} diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/styled.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/styled.tsx new file mode 100644 index 0000000000..67b458556b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/styled.tsx @@ -0,0 +1,77 @@ +import { TokenName, TokenSymbol, UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + flex: 1; + width: 100%; + background: var(${UI.COLOR_PAPER}); + border-radius: 20px; +` + +export const TokenInfoWrapper = styled.div` + display: flex; + flex-direction: row; + width: 100%; + align-items: center; + gap: 10px; +` + +export const TokenWrapper = styled(TokenInfoWrapper)` + padding: 20px; + border-bottom: 1px solid var(${UI.COLOR_BORDER}); +` + +export const InfoTable = styled.div` + display: flex; + flex-direction: column; + padding: 0 20px; +` + +export const StyledTokenSymbol = styled(TokenSymbol)` + font-size: 21px; + font-weight: 600; +` + +export const StyledTokenName = styled(TokenName)` + font-size: 14px; + color: var(${UI.COLOR_TEXT_OPACITY_70}); +` + +export const InfoRow = styled.div` + display: grid; + grid-template-columns: 100px 1fr; + width: 100%; + gap: 10px; + border-bottom: 1px solid var(${UI.COLOR_BORDER}); + padding: 14px 0; + font-size: 14px; + + &:last-child { + border-bottom: none; + } + + > div:first-child { + color: var(${UI.COLOR_TEXT_OPACITY_50}); + } +` + +export const SelectButton = styled.button` + display: inline-block; + background: #bcec79; + color: #194d05; + font-size: 14px; + font-weight: bold; + border-radius: 24px; + padding: 10px 24px; + text-decoration: none; + border: 0; + outline: 0; + cursor: pointer; + + &:hover { + opacity: 0.8; + } +` diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index 9a30f04cb0..4f7ecdcc87 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -5,6 +5,7 @@ import { TokenWithLogo } from '@cowprotocol/common-const' import { isInjectedWidget } from '@cowprotocol/common-utils' import { ListState, + TokenListCategory, useAddList, useAddUserToken, useAllListsList, @@ -26,6 +27,7 @@ import { useUpdateSelectTokenWidgetState } from '../../hooks/useUpdateSelectToke import { ImportListModal } from '../../pure/ImportListModal' import { ImportTokenModal } from '../../pure/ImportTokenModal' import { SelectTokenModal } from '../../pure/SelectTokenModal' +import { LpTokenPage } from '../LpTokenPage' import { ManageListsAndTokens } from '../ManageListsAndTokens' const Wrapper = styled.div` @@ -41,10 +43,11 @@ interface SelectTokenWidgetProps { displayLpTokenLists?: boolean } -export function SelectTokenWidget({displayLpTokenLists}: SelectTokenWidgetProps) { - const { open, onSelectToken, tokenToImport, listToImport, selectedToken, onInputPressEnter } = +export function SelectTokenWidget({ displayLpTokenLists }: SelectTokenWidgetProps) { + const { open, onSelectToken, tokenToImport, listToImport, selectedToken, onInputPressEnter, selectedPoolAddress } = useSelectTokenWidgetState() const [isManageWidgetOpen, setIsManageWidgetOpen] = useState(false) + const tokenListCategoryState = useState(null) const updateSelectTokenWidget = useUpdateSelectTokenWidgetState() const { account } = useWalletInfo() @@ -70,19 +73,31 @@ export function SelectTokenWidget({displayLpTokenLists}: SelectTokenWidgetProps) onSelectToken: undefined, tokenToImport: undefined, listToImport: undefined, + selectedPoolAddress: undefined, }) }, [updateSelectTokenWidget]) - const resetTokenImport = () => { + const openPoolPage = useCallback( + (selectedPoolAddress: string) => { + updateSelectTokenWidget({ selectedPoolAddress }) + }, + [updateSelectTokenWidget], + ) + + const closePoolPage = useCallback(() => { + updateSelectTokenWidget({ selectedPoolAddress: undefined }) + }, [updateSelectTokenWidget]) + + const resetTokenImport = useCallback(() => { updateSelectTokenWidget({ tokenToImport: undefined, }) - } + }, [updateSelectTokenWidget]) - const onDismiss = () => { + const onDismiss = useCallback(() => { setIsManageWidgetOpen(false) closeTokenSelectWidget() - } + }, [closeTokenSelectWidget]) const importTokenAndClose = (tokens: TokenWithLogo[]) => { importTokenCallback(tokens) @@ -139,6 +154,17 @@ export function SelectTokenWidget({displayLpTokenLists}: SelectTokenWidgetProps) ) } + if (selectedPoolAddress) { + return ( + + ) + } + return ( setIsManageWidgetOpen(true)} hideFavoriteTokensTooltip={isInjectedWidgetMode} + openPoolPage={openPoolPage} + tokenListCategoryState={tokenListCategoryState} account={account} /> ) diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx index 2067456286..acf4499d80 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx @@ -1,14 +1,16 @@ -import { useCallback } from 'react' +import { MouseEventHandler, useCallback } from 'react' import { BalancesState } from '@cowprotocol/balances-and-allowances' -import { LpToken } from '@cowprotocol/common-const' +import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' import { useMediaQuery } from '@cowprotocol/common-hooks' -import { TokenLogo, TokensByAddress } from '@cowprotocol/tokens' -import { InfoTooltip, LoadingRows, LoadingRowSmall, TokenAmount, TokenName, TokenSymbol } from '@cowprotocol/ui' -import { Media } from '@cowprotocol/ui' +import { TokenLogo } from '@cowprotocol/tokens' +import { LoadingRows, LoadingRowSmall, Media, TokenAmount, TokenName, TokenSymbol } from '@cowprotocol/ui' import { CurrencyAmount } from '@uniswap/sdk-core' import { VirtualItem } from '@tanstack/react-virtual' +import { Info } from 'react-feather' + +import { PoolInfoStates } from 'modules/yield/shared' import { VirtualList } from 'common/pure/VirtualList' @@ -17,7 +19,6 @@ import { ListHeader, ListItem, LpTokenInfo, - LpTokenLogo, LpTokenWrapper, LpTokenYieldPercentage, LpTokenBalance, @@ -44,73 +45,70 @@ const MobileCardRowItem: React.FC<{ label: string; value: React.ReactNode }> = ( ) -const TokenInfo: React.FC<{ token: LpToken }> = ({ token }) => ( - <> - - - -

    - -

    - -) - -const LpTokenLogos: React.FC<{ token0: string; token1: string; tokensByAddress: TokensByAddress; size: number }> = ({ - token0, - token1, - tokensByAddress, - size, -}) => ( - - - - -) - interface LpTokenListsProps { + account: string | undefined lpTokens: LpToken[] - tokensByAddress: TokensByAddress balancesState: BalancesState displayCreatePoolBanner: boolean + poolsInfo: PoolInfoStates | undefined + onSelectToken(token: TokenWithLogo): void + openPoolPage(poolAddress: string): void } -export function LpTokenLists({ lpTokens, tokensByAddress, balancesState, displayCreatePoolBanner }: LpTokenListsProps) { +export function LpTokenLists({ + account, + onSelectToken, + openPoolPage, + lpTokens, + balancesState, + displayCreatePoolBanner, + poolsInfo, +}: LpTokenListsProps) { const { values: balances } = balancesState const isMobile = useMediaQuery(Media.upToSmall(false)) const getItemView = useCallback( (lpTokens: LpToken[], item: VirtualItem) => { const token = lpTokens[item.index] - const token0 = token.tokens?.[0]?.toLowerCase() - const token1 = token.tokens?.[1]?.toLowerCase() - const balance = balances ? balances[token.address.toLowerCase()] : undefined + + const tokenAddressLower = token.address.toLowerCase() + const balance = balances ? balances[tokenAddressLower] : undefined const balanceAmount = balance ? CurrencyAmount.fromRawAmount(token, balance.toHexString()) : undefined + const info = poolsInfo?.[tokenAddressLower]?.info + + const onInfoClick: MouseEventHandler = (e) => { + e.stopPropagation() + openPoolPage(tokenAddressLower) + } const commonContent = ( <> - + - + + + +

    + +

    ) + const BalanceDisplay = balanceAmount ? : LoadingElement + if (isMobile) { return ( {commonContent} - : LoadingElement} - /> - + + - - TODO - + + Pool details + } /> @@ -119,17 +117,17 @@ export function LpTokenLists({ lpTokens, tokensByAddress, balancesState, display } return ( - + onSelectToken(token)}> {commonContent} - {balanceAmount ? : LoadingElement} - 40% - - TODO + {BalanceDisplay} + {info?.apy ? `${info.apy}%` : ''} + + ) }, - [balances, tokensByAddress, isMobile], + [balances, onSelectToken, poolsInfo, openPoolPage, account, isMobile], ) return ( diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts index 578baf1b60..44971e2b5e 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts @@ -1,5 +1,4 @@ -import { TokenLogoWrapper } from '@cowprotocol/tokens' -import { ExternalLink, Media, UI } from '@cowprotocol/ui' +import { ExternalLink, UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' @@ -33,55 +32,14 @@ export const ListItem = styled.div` grid-template-columns: var(--grid-columns); padding: 10px 20px; cursor: pointer; + font-size: 14px; + align-items: center; &:hover { background: var(${UI.COLOR_PAPER_DARKER}); } ` -export const LpTokenLogo = styled.div` - --size: 36px; - position: relative; - width: var(--size); - height: var(--size); - object-fit: contain; - - ${Media.upToSmall()} { - --size: 32px; - } - - ${TokenLogoWrapper} { - position: relative; - z-index: 1; - border-radius: var(--size); - width: var(--size); - height: var(--size); - min-width: var(--size); - min-height: var(--size); - font-size: var(--size); - } - - ${TokenLogoWrapper}:nth-child(2) { - position: absolute; - left: 0; - top: 0; - z-index: 2; - clip-path: inset(0 0 0 50%); - } - - &::after { - content: ''; - position: absolute; - top: 0; - left: 50%; - width: 2px; - height: 100%; - background-color: var(${UI.COLOR_PAPER}); - transform: translateX(-50%); - z-index: 3; - } -` - export const LpTokenInfo = styled.div` display: flex; flex-direction: column; @@ -119,6 +77,13 @@ export const LpTokenTooltip = styled.div` display: flex; align-items: center; margin: auto; + gap: 6px; + opacity: 0.8; + cursor: pointer; + + &:hover { + opacity: 1; + } ` export const NoPoolWrapper = styled.div` diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/ModalHeader/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/ModalHeader/index.tsx index ad46d21ed0..63d921f2b1 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/ModalHeader/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/ModalHeader/index.tsx @@ -1,3 +1,5 @@ +import { ReactNode } from 'react' + import { UI } from '@cowprotocol/ui' import { BackButton } from '@cowprotocol/ui' @@ -24,7 +26,7 @@ const Header = styled.div` ` export interface ModalHeaderProps { - children: string + children: ReactNode onBack?(): void onClose?(): void className?: string diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx index 2123dca8c6..6ac2160670 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx @@ -31,6 +31,7 @@ const defaultProps: SelectTokenModalProps = { unsupportedTokens, allTokens: allTokensMock, favoriteTokens: favoriteTokensMock, + tokenListCategoryState: [null, () => void 0], balancesState: { values: balances, isLoading: false, @@ -45,6 +46,9 @@ const defaultProps: SelectTokenModalProps = { onDismiss() { console.log('onDismiss') }, + openPoolPage() { + console.log('openPoolPage') + }, } const Fixtures = { diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index e5248e3262..26ff5898c5 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { BalancesState } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' -import { UnsupportedTokensState } from '@cowprotocol/tokens' +import { TokenListCategory, UnsupportedTokensState } from '@cowprotocol/tokens' import { BackButton, SearchInput } from '@cowprotocol/ui' import { Edit } from 'react-feather' @@ -17,7 +17,7 @@ import { SelectTokenContext } from '../../types' import { FavoriteTokensList } from '../FavoriteTokensList' import { TokensVirtualList } from '../TokensVirtualList' -export interface SelectTokenModalProps { +export interface SelectTokenModalProps { allTokens: TokenWithLogo[] favoriteTokens: TokenWithLogo[] balancesState: BalancesState @@ -27,9 +27,12 @@ export interface SelectTokenModalProps { hideFavoriteTokensTooltip?: boolean displayLpTokenLists?: boolean account: string | undefined + tokenListCategoryState: [T, (category: T) => void] onSelectToken(token: TokenWithLogo): void + openPoolPage(poolAddress: string): void + onInputPressEnter?(): void defaultInputValue?: string @@ -54,7 +57,9 @@ export function SelectTokenModal(props: SelectTokenModalProps) { onOpenManageWidget, onInputPressEnter, account, - displayLpTokenLists + displayLpTokenLists, + openPoolPage, + tokenListCategoryState, } = props const [inputValue, setInputValue] = useState(defaultInputValue) @@ -64,31 +69,33 @@ export function SelectTokenModal(props: SelectTokenModalProps) { selectedToken, onSelectToken, unsupportedTokens, - permitCompatibleTokens + permitCompatibleTokens, } - const allListsContent = <> - - - - - {inputValue.trim() ? ( - - ) : ( - - )} - -
    - - Manage Token Lists - -
    - + const allListsContent = ( + <> + + + + + {inputValue.trim() ? ( + + ) : ( + + )} + +
    + + Manage Token Lists + +
    + + ) return ( @@ -106,9 +113,19 @@ export function SelectTokenModal(props: SelectTokenModalProps) { placeholder="Search name or paste address" /> - {displayLpTokenLists - ? {allListsContent} - : allListsContent} + {displayLpTokenLists ? ( + + {allListsContent} + + ) : ( + allListsContent + )} ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/state/selectTokenWidgetAtom.ts b/apps/cowswap-frontend/src/modules/tokensList/state/selectTokenWidgetAtom.ts index 8dd317439d..5e01b5ab42 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/state/selectTokenWidgetAtom.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/state/selectTokenWidgetAtom.ts @@ -10,9 +10,10 @@ export const { atom: selectTokenWidgetAtom, updateAtom: updateSelectTokenWidgetA atom<{ open: boolean selectedToken?: string + selectedPoolAddress?: string tokenToImport?: TokenWithLogo listToImport?: ListState onSelectToken?: (currency: Currency) => void onInputPressEnter?: Command - }>({ open: false }) + }>({ open: false }), ) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx index c0874b0fc3..f08534085a 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -67,20 +67,21 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) const isCurrentPathYield = location.pathname.startsWith(addChainIdToRoute(Routes.YIELD, chainId)) const itemTradeState = getTradeStateByType(item.route) - const routePath = isItemYield - ? addChainIdToRoute(item.route, chainId) - : parameterizeTradeRoute( - isCurrentPathYield - ? ({ - chainId, - inputCurrencyId: - itemTradeState.inputCurrencyId || (chainId && getDefaultTradeRawState(+chainId).inputCurrencyId), - outputCurrencyId: itemTradeState.outputCurrencyId, - } as TradeUrlParams) - : tradeContext, - item.route, - !isCurrentPathYield, - ) + const routePath = + isItemYield && !isCurrentPathYield + ? addChainIdToRoute(item.route, chainId) + : parameterizeTradeRoute( + isCurrentPathYield + ? ({ + chainId, + inputCurrencyId: + itemTradeState.inputCurrencyId || (chainId && getDefaultTradeRawState(+chainId).inputCurrencyId), + outputCurrencyId: itemTradeState.outputCurrencyId, + } as TradeUrlParams) + : tradeContext, + item.route, + !isCurrentPathYield, + ) const isActive = location.pathname.startsWith(routePath.split('?')[0]) diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useAutoImportTokensState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useAutoImportTokensState.ts index b7037a012e..efaa76740a 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useAutoImportTokensState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useAutoImportTokensState.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' -import { TokenWithLogo } from '@cowprotocol/common-const' +import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' import { isTruthy } from '@cowprotocol/common-utils' import { useSearchNonExistentToken } from '@cowprotocol/tokens' @@ -14,13 +14,13 @@ interface AutoImportTokensState { } export function useAutoImportTokensState( inputToken: Nullish, - outputToken: Nullish + outputToken: Nullish, ): AutoImportTokensState { const foundInputToken = useSearchNonExistentToken(inputToken || null) const foundOutputToken = useSearchNonExistentToken(outputToken || null) const tokensToImport = useMemo(() => { - return [foundInputToken, foundOutputToken].filter(isTruthy) + return [foundInputToken, foundOutputToken].filter(isTruthy).filter((token) => !(token instanceof LpToken)) }, [foundInputToken, foundOutputToken]) const tokensToImportCount = tokensToImport.length diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateOnCurrencySelection.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateOnCurrencySelection.ts index d3da2666a1..54d8edf293 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateOnCurrencySelection.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateOnCurrencySelection.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react' +import { LpToken } from '@cowprotocol/common-const' import { useAreThereTokensWithSameSymbol } from '@cowprotocol/tokens' import { Command } from '@cowprotocol/types' import { useWalletInfo } from '@cowprotocol/wallet' @@ -16,7 +17,7 @@ export type CurrencySelectionCallback = ( field: Field, currency: Currency | null, stateUpdateCallback?: Command, - searchParams?: TradeSearchParams + searchParams?: TradeSearchParams, ) => void function useResolveCurrencyAddressOrSymbol(): (currency: Currency | null) => string | null { @@ -26,9 +27,11 @@ function useResolveCurrencyAddressOrSymbol(): (currency: Currency | null) => str (currency: Currency | null): string | null => { if (!currency) return null - return areThereTokensWithSameSymbol(currency.symbol) ? (currency as Token).address : currency.symbol || null + return currency instanceof LpToken || areThereTokensWithSameSymbol(currency.symbol) + ? (currency as Token).address + : currency.symbol || null }, - [areThereTokensWithSameSymbol] + [areThereTokensWithSameSymbol], ) } @@ -63,11 +66,11 @@ export function useNavigateOnCurrencySelection(): CurrencySelectionCallback { inputCurrencyId: targetInputCurrencyId, outputCurrencyId: targetOutputCurrencyId, }, - searchParams + searchParams, ) stateUpdateCallback?.() }, - [navigate, chainId, state, resolveCurrencyAddressOrSymbol] + [navigate, chainId, state, resolveCurrencyAddressOrSymbol], ) } diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts new file mode 100644 index 0000000000..2f3be9e3d7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts @@ -0,0 +1,35 @@ +import { useMemo } from 'react' + +import { useTokensBalances } from '@cowprotocol/balances-and-allowances' +import { LpToken } from '@cowprotocol/common-const' +import { TokenListCategory, useAllLpTokens } from '@cowprotocol/tokens' +import { BigNumber } from '@ethersproject/bignumber' + +export type LpTokenWithBalance = { + token: LpToken + balance: BigNumber +} +export const LP_CATEGORY = [TokenListCategory.LP] + +export function useLpTokensWithBalances() { + const lpTokens = useAllLpTokens(LP_CATEGORY) + const { values: balances } = useTokensBalances() + + return useMemo(() => { + if (lpTokens.length === 0) return undefined + + return lpTokens.reduce( + (acc, token) => { + const addressLower = token.address.toLowerCase() + const balance = balances[addressLower] + + if (balance && !balance.isZero()) { + acc[addressLower] = { token, balance } + } + + return acc + }, + {} as Record, + ) + }, [lpTokens, balances]) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/usePoolsInfo.ts b/apps/cowswap-frontend/src/modules/yield/hooks/usePoolsInfo.ts new file mode 100644 index 0000000000..8ed7cd1f24 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/usePoolsInfo.ts @@ -0,0 +1,7 @@ +import { useAtomValue } from 'jotai' + +import { currentPoolsInfoAtom } from '../state/poolsInfoAtom' + +export function usePoolsInfo() { + return useAtomValue(currentPoolsInfoAtom) +} diff --git a/apps/cowswap-frontend/src/modules/yield/shared.ts b/apps/cowswap-frontend/src/modules/yield/shared.ts new file mode 100644 index 0000000000..24f46a452d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/shared.ts @@ -0,0 +1,3 @@ +export { PoolsInfoUpdater } from './updaters/PoolsInfoUpdater' +export { usePoolsInfo } from './hooks/usePoolsInfo' +export type { PoolInfo, PoolInfoStates } from './state/poolsInfoAtom' diff --git a/apps/cowswap-frontend/src/modules/yield/state/poolsInfoAtom.ts b/apps/cowswap-frontend/src/modules/yield/state/poolsInfoAtom.ts new file mode 100644 index 0000000000..11e3bb7046 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/state/poolsInfoAtom.ts @@ -0,0 +1,65 @@ +import { atom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +import { getJotaiIsolatedStorage } from '@cowprotocol/core' +import { SupportedChainId, mapSupportedNetworks } from '@cowprotocol/cow-sdk' +import { walletInfoAtom } from '@cowprotocol/wallet' + +export interface PoolInfo { + apy: number + tvl: number + feeTier: number + volume24h: number +} + +type PoolInfoState = { + info: PoolInfo + updatedAt: number +} + +export type PoolInfoStates = Record + +type PoolInfoStatesPerAccount = Record + +type PoolsInfoState = Record + +const poolsInfoAtom = atomWithStorage( + 'poolsInfoAtom:v0', + mapSupportedNetworks({}), + getJotaiIsolatedStorage(), +) + +export const currentPoolsInfoAtom = atom((get) => { + const { chainId, account } = get(walletInfoAtom) + const poolsInfo = get(poolsInfoAtom) + + return account ? poolsInfo[chainId]?.[account] : undefined +}) + +export const upsertPoolsInfoAtom = atom(null, (get, set, update: Record) => { + const { chainId, account } = get(walletInfoAtom) + const poolsInfo = get(poolsInfoAtom) + + if (!account) return + + const currentState = poolsInfo[chainId]?.[account] + const updatedState = { + ...currentState, + ...Object.keys(update).reduce((acc, address) => { + acc[address] = { + info: update[address], + updatedAt: Date.now(), + } + + return acc + }, {} as PoolInfoStates), + } + + set(poolsInfoAtom, { + ...poolsInfo, + [chainId]: { + ...poolsInfo[chainId], + [account]: updatedState, + }, + }) +}) diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx new file mode 100644 index 0000000000..66455246fe --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx @@ -0,0 +1,54 @@ +import { useSetAtom } from 'jotai' +import { useEffect, useMemo } from 'react' + +import ms from 'ms.macro' + +import { TradeType, useTradeTypeInfo } from 'modules/trade' + +import { MOCK_POOL_INFO } from './mockPoolInfo' + +import { useLpTokensWithBalances } from '../../hooks/useLpTokensWithBalances' +import { usePoolsInfo } from '../../hooks/usePoolsInfo' +import { upsertPoolsInfoAtom } from '../../state/poolsInfoAtom' + +const POOL_INFO_CACHE_TIME = ms`1h` + +/** + * The API should return info about requested pools + alternative COW AMM pools + * When tokenAddresses is null, it should return info about all pools + */ +function fetchPoolsInfo(tokenAddresses: string[] | null) { + console.log('TODO', tokenAddresses) + return Promise.resolve(MOCK_POOL_INFO) +} + +export function PoolsInfoUpdater() { + const poolsInfo = usePoolsInfo() + const upsertPoolsInfo = useSetAtom(upsertPoolsInfoAtom) + const tradeTypeInfo = useTradeTypeInfo() + const isYield = tradeTypeInfo?.tradeType === TradeType.YIELD + + const lpTokensWithBalances = useLpTokensWithBalances() + + const tokensToUpdate = useMemo(() => { + return lpTokensWithBalances + ? Object.keys(lpTokensWithBalances).filter((address) => { + const state = poolsInfo?.[address] + + if (!state) return true + + return state.updatedAt + POOL_INFO_CACHE_TIME > Date.now() + }) + : null + }, [lpTokensWithBalances, poolsInfo]) + + const tokensKey = useMemo(() => tokensToUpdate?.join(','), [tokensToUpdate]) + + useEffect(() => { + if ((tokensToUpdate && tokensToUpdate.length > 0) || isYield) { + fetchPoolsInfo(isYield ? null : tokensToUpdate).then(upsertPoolsInfo) + } + }, [isYield, tokensKey]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts new file mode 100644 index 0000000000..efe5801784 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts @@ -0,0 +1,18 @@ +import { PoolInfo } from '../../state/poolsInfoAtom' + +export const MOCK_POOL_INFO: Record = { + // Sushi AAVE/WETH + '0xd75ea151a61d06868e31f8988d28dfe5e9df57b4': { + apy: 1.89, + tvl: 157057, + feeTier: 0.3, + volume24h: 31.19, + }, + // CoW AMM AAVE/WETH + '0xf706c50513446d709f08d3e5126cd74fb6bfda19': { + apy: 0.07, + tvl: 52972, + feeTier: 0.3, + volume24h: 10, + }, +} diff --git a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx index e5cf06bff9..e4a81608d0 100644 --- a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx +++ b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx @@ -1,7 +1,7 @@ import { useSetAtom } from 'jotai' import { useEffect, useMemo } from 'react' -import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' +import { LpToken, NATIVE_CURRENCIES } from '@cowprotocol/common-const' import type { SupportedChainId } from '@cowprotocol/cow-sdk' import { useAllTokens } from '@cowprotocol/tokens' @@ -27,7 +27,10 @@ export function BalancesAndAllowancesUpdater({ account, chainId }: BalancesAndAl const allTokens = useAllTokens() const { data: nativeTokenBalance } = useNativeTokenBalance(account) - const tokenAddresses = useMemo(() => allTokens.map((token) => token.address), [allTokens]) + const tokenAddresses = useMemo( + () => allTokens.filter((token) => !(token instanceof LpToken)).map((token) => token.address), + [allTokens], + ) usePersistBalancesAndAllowances({ account, @@ -46,5 +49,5 @@ export function BalancesAndAllowancesUpdater({ account, chainId }: BalancesAndAl setBalances((state) => ({ ...state, values: { ...state.values, ...nativeBalanceState } })) }, [nativeTokenBalance, chainId, setBalances]) - return + return account ? : null } diff --git a/libs/tokens/src/const/tokensLists.ts b/libs/tokens/src/const/tokensLists.ts index f692865510..5a5517c813 100644 --- a/libs/tokens/src/const/tokensLists.ts +++ b/libs/tokens/src/const/tokensLists.ts @@ -5,9 +5,11 @@ import tokensList from './tokensList.json' import { ListSourceConfig, ListsSourcesByNetwork } from '../types' +export const LP_TOKEN_LISTS = lpTokensList as Array + export const DEFAULT_TOKENS_LISTS: ListsSourcesByNetwork = mapSupportedNetworks((chainId) => [ ...tokensList[chainId], - ...(lpTokensList as Array), + ...LP_TOKEN_LISTS, ]) export const UNISWAP_TOKENS_LIST = 'https://ipfs.io/ipns/tokens.uniswap.org' diff --git a/libs/tokens/src/hooks/tokens/useTokenBySymbolOrAddress.ts b/libs/tokens/src/hooks/tokens/useTokenBySymbolOrAddress.ts index fe3f1dd620..1d0f25f31c 100644 --- a/libs/tokens/src/hooks/tokens/useTokenBySymbolOrAddress.ts +++ b/libs/tokens/src/hooks/tokens/useTokenBySymbolOrAddress.ts @@ -3,10 +3,12 @@ import { useMemo } from 'react' import { TokenWithLogo } from '@cowprotocol/common-const' -import { tokensByAddressAtom, tokensBySymbolAtom } from '../../state/tokens/allTokensAtom' +import { useTokensByAddressMap } from './useTokensByAddressMap' + +import { tokensBySymbolAtom } from '../../state/tokens/allTokensAtom' export function useTokenBySymbolOrAddress(symbolOrAddress?: string | null): TokenWithLogo | null { - const tokensByAddress = useAtomValue(tokensByAddressAtom) + const tokensByAddress = useTokensByAddressMap() const tokensBySymbol = useAtomValue(tokensBySymbolAtom) return useMemo(() => { diff --git a/libs/tokens/src/index.ts b/libs/tokens/src/index.ts index 6c61741e59..6100e61f21 100644 --- a/libs/tokens/src/index.ts +++ b/libs/tokens/src/index.ts @@ -46,3 +46,4 @@ export { useAllLpTokens } from './hooks/tokens/useAllLpTokens' export { getTokenListViewLink } from './utils/getTokenListViewLink' export { getTokenLogoUrls } from './utils/getTokenLogoUrls' export { fetchTokenFromBlockchain } from './utils/fetchTokenFromBlockchain' +export { getTokenSearchFilter } from './utils/getTokenSearchFilter' diff --git a/libs/tokens/src/pure/TokenLogo/index.tsx b/libs/tokens/src/pure/TokenLogo/index.tsx index 8a37abef83..aca36412aa 100644 --- a/libs/tokens/src/pure/TokenLogo/index.tsx +++ b/libs/tokens/src/pure/TokenLogo/index.tsx @@ -1,7 +1,7 @@ import { atom, useAtom } from 'jotai' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' -import { cowprotocolTokenLogoUrl, NATIVE_CURRENCY_ADDRESS, TokenWithLogo } from '@cowprotocol/common-const' +import { cowprotocolTokenLogoUrl, LpToken, NATIVE_CURRENCY_ADDRESS, TokenWithLogo } from '@cowprotocol/common-const' import { uriToHttp } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Media, UI } from '@cowprotocol/ui' @@ -12,6 +12,7 @@ import styled, { css } from 'styled-components/macro' import { SingleLetterLogo } from './SingleLetterLogo' +import { useTokensByAddressMap } from '../../hooks/tokens/useTokensByAddressMap' import { getTokenLogoUrls } from '../../utils/getTokenLogoUrls' const invalidUrlsAtom = atom<{ [url: string]: boolean }>({}) @@ -23,12 +24,12 @@ export const TokenLogoWrapper = styled.div<{ size?: number; sizeMobile?: number justify-content: center; background: var(${UI.COLOR_DARK_IMAGE_PAPER}); color: var(${UI.COLOR_DARK_IMAGE_PAPER_TEXT}); - border-radius: ${({ size }) => size ?? defaultSize}px; - width: ${({ size }) => size ?? defaultSize}px; - height: ${({ size }) => size ?? defaultSize}px; - min-width: ${({ size }) => size ?? defaultSize}px; - min-height: ${({ size }) => size ?? defaultSize}px; - font-size: ${({ size }) => size ?? defaultSize}px; + border-radius: ${({ size = defaultSize }) => size}px; + width: ${({ size = defaultSize }) => size}px; + height: ${({ size = defaultSize }) => size}px; + min-width: ${({ size = defaultSize }) => size}px; + min-height: ${({ size = defaultSize }) => size}px; + font-size: ${({ size = defaultSize }) => size}px; overflow: hidden; > img, @@ -59,18 +60,55 @@ export const TokenLogoWrapper = styled.div<{ size?: number; sizeMobile?: number } ` +const LpTokenWrapper = styled.div<{ size?: number }>` + width: 100%; + height: 100%; + position: relative; + + > div { + width: 50%; + height: 100%; + overflow: hidden; + position: absolute; + } + + > div:last-child { + right: -1px; + } + + > div:last-child > img, + > div:last-child > svg { + right: 100%; + position: relative; + } + + > div > img, + > div > svg { + width: ${({ size = defaultSize }) => size}px; + height: ${({ size = defaultSize }) => size}px; + min-width: ${({ size = defaultSize }) => size}px; + min-height: ${({ size = defaultSize }) => size}px; + } +` + export interface TokenLogoProps { - token?: TokenWithLogo | Currency | null + token?: TokenWithLogo | LpToken | Currency | null logoURI?: string className?: string size?: number sizeMobile?: number + noWrap?: boolean } -export function TokenLogo({ logoURI, token, className, size = 36, sizeMobile }: TokenLogoProps) { +export function TokenLogo({ logoURI, token, className, size = 36, sizeMobile, noWrap }: TokenLogoProps) { + const tokensByAddress = useTokensByAddressMap() + const [invalidUrls, setInvalidUrls] = useAtom(invalidUrlsAtom) + const isLpToken = token instanceof LpToken const urls = useMemo(() => { + if (token instanceof LpToken) return + // TODO: get rid of Currency usage and remove type casting if (token) { if (token instanceof NativeCurrency) { @@ -83,25 +121,46 @@ export function TokenLogo({ logoURI, token, className, size = 36, sizeMobile }: return logoURI ? uriToHttp(logoURI) : [] }, [logoURI, token]) - const validUrls = useMemo(() => urls.filter((url) => !invalidUrls[url]), [urls, invalidUrls]) + const validUrls = useMemo(() => urls && urls.filter((url) => !invalidUrls[url]), [urls, invalidUrls]) - const currentUrl = validUrls[0] + const currentUrl = validUrls?.[0] + + const onError = useCallback(() => { + if (!currentUrl) return - const onError = () => { setInvalidUrls((state) => ({ ...state, [currentUrl]: true })) - } + }, [currentUrl, setInvalidUrls]) const initial = token?.symbol?.[0] || token?.name?.[0] + if (isLpToken) { + return ( + + +
    + +
    +
    + +
    +
    +
    + ) + } + + const content = currentUrl ? ( + token logo + ) : initial ? ( + + ) : ( + + ) + + if (noWrap) return content + return ( - {currentUrl ? ( - token logo - ) : initial ? ( - - ) : ( - - )} + {content} ) } diff --git a/libs/tokens/src/state/environmentAtom.ts b/libs/tokens/src/state/environmentAtom.ts index 687c77dc8c..1dabe73245 100644 --- a/libs/tokens/src/state/environmentAtom.ts +++ b/libs/tokens/src/state/environmentAtom.ts @@ -6,11 +6,12 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' interface TokensModuleEnvironment { chainId: SupportedChainId useCuratedListOnly?: boolean + enableLpTokensByDefault?: boolean widgetAppCode?: string selectedLists?: string[] } export const { atom: environmentAtom, updateAtom: updateEnvironmentAtom } = atomWithPartialUpdate( atom({ chainId: getCurrentChainIdFromUrl(), - }) + }), ) diff --git a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts index 3728e1baa7..c5a9205a7c 100644 --- a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts +++ b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts @@ -8,6 +8,7 @@ import { ARBITRUM_ONE_TOKENS_LIST, DEFAULT_TOKENS_LISTS, GNOSIS_UNISWAP_TOKENS_LIST, + LP_TOKEN_LISTS, UNISWAP_TOKENS_LIST, } from '../../const/tokensLists' import { @@ -47,7 +48,7 @@ export const allListsSourcesAtom = atom((get) => { const userAddedTokenLists = get(userAddedListsSourcesAtom) if (useCuratedListOnly) { - return [get(curatedListSourceAtom), ...userAddedTokenLists[chainId]] + return [get(curatedListSourceAtom), ...LP_TOKEN_LISTS, ...userAddedTokenLists[chainId]] } return [...DEFAULT_TOKENS_LISTS[chainId], ...(userAddedTokenLists[chainId] || [])] @@ -86,8 +87,13 @@ export const listsStatesMapAtom = atom((get) => { return acc }, {}) + const lpTokenListSources = LP_TOKEN_LISTS.reduce<{ [key: string]: boolean }>((acc, list) => { + acc[list.source] = true + return acc + }, {}) + const listsSources = Object.keys(currentNetworkLists).filter((source) => { - return useCuratedListOnly ? userAddedListSources[source] : true + return useCuratedListOnly ? userAddedListSources[source] || lpTokenListSources[source] : true }) const lists = useCuratedListOnly ? [get(curatedListSourceAtom).source, ...listsSources] : listsSources diff --git a/libs/tokens/src/state/tokens/allTokensAtom.ts b/libs/tokens/src/state/tokens/allTokensAtom.ts index fde9b92bea..c243d4e762 100644 --- a/libs/tokens/src/state/tokens/allTokensAtom.ts +++ b/libs/tokens/src/state/tokens/allTokensAtom.ts @@ -6,7 +6,7 @@ import { TokenInfo } from '@cowprotocol/types' import { favoriteTokensAtom } from './favoriteTokensAtom' import { userAddedTokensAtom } from './userAddedTokensAtom' -import { TokensMap } from '../../types' +import { LP_TOKEN_LIST_CATEGORIES, TokensMap } from '../../types' import { lowerCaseTokensMap } from '../../utils/lowerCaseTokensMap' import { parseTokenInfo } from '../../utils/parseTokenInfo' import { tokenMapToListWithLogo } from '../../utils/tokenMapToListWithLogo' @@ -58,23 +58,47 @@ export const tokensStateAtom = atom((get) => { ) }) +export const lpTokensMapAtom = atom((get) => { + const { chainId } = get(environmentAtom) + const listsStatesList = get(listsStatesListAtom) + + return listsStatesList.reduce((acc, list) => { + if (!list.category || !LP_TOKEN_LIST_CATEGORIES.includes(list.category)) { + return acc + } + + list.list.tokens.forEach((token) => { + const tokenInfo = parseTokenInfo(chainId, token) + const tokenAddressKey = tokenInfo?.address.toLowerCase() + + if (!tokenInfo || !tokenAddressKey) return + + acc[tokenAddressKey] = tokenInfo + }) + + return acc + }, {}) +}) + /** * Returns a list of tokens that are active and sorted alphabetically * The list includes: native token, user added tokens, favorite tokens and tokens from active lists * Native token is always the first element in the list */ export const activeTokensAtom = atom((get) => { - const { chainId } = get(environmentAtom) + const { chainId, enableLpTokensByDefault } = get(environmentAtom) const userAddedTokens = get(userAddedTokensAtom) const favoriteTokensState = get(favoriteTokensAtom) const tokensMap = get(tokensStateAtom) + const lpTokensMap = get(lpTokensMapAtom) const nativeToken = NATIVE_CURRENCIES[chainId] return tokenMapToListWithLogo( { [nativeToken.address.toLowerCase()]: nativeToken as TokenInfo, ...tokensMap.activeTokens, + ...(enableLpTokensByDefault ? lpTokensMap : null), ...lowerCaseTokensMap(userAddedTokens[chainId]), ...lowerCaseTokensMap(favoriteTokensState[chainId]), }, diff --git a/libs/tokens/src/types.ts b/libs/tokens/src/types.ts index d1e6d4a206..8cbad348c5 100644 --- a/libs/tokens/src/types.ts +++ b/libs/tokens/src/types.ts @@ -8,6 +8,8 @@ export enum TokenListCategory { COW_AMM_LP = 'COW_AMM_LP', } +export const LP_TOKEN_LIST_CATEGORIES = [TokenListCategory.LP, TokenListCategory.COW_AMM_LP] + export type ListSourceConfig = { widgetAppCode?: string priority?: number @@ -24,7 +26,7 @@ export type UnsupportedTokensState = { [tokenAddress: string]: { dateAdded: numb export type ListsEnabledState = { [listId: string]: boolean | undefined } -export interface ListState extends Pick{ +export interface ListState extends Pick { list: UniTokenList isEnabled?: boolean } diff --git a/libs/tokens/src/updaters/TokensListsUpdater/index.ts b/libs/tokens/src/updaters/TokensListsUpdater/index.ts index 7a882406e3..d14f61ae10 100644 --- a/libs/tokens/src/updaters/TokensListsUpdater/index.ts +++ b/libs/tokens/src/updaters/TokensListsUpdater/index.ts @@ -35,6 +35,7 @@ const NETWORKS_WITHOUT_RESTRICTIONS = [SupportedChainId.SEPOLIA] interface TokensListsUpdaterProps { chainId: SupportedChainId isGeoBlockEnabled: boolean + enableLpTokensByDefault: boolean } /** @@ -45,7 +46,11 @@ interface TokensListsUpdaterProps { */ const GEOBLOCK_ERRORS_TO_IGNORE = /(failed to fetch)|(load failed)/i -export function TokensListsUpdater({ chainId: currentChainId, isGeoBlockEnabled }: TokensListsUpdaterProps) { +export function TokensListsUpdater({ + chainId: currentChainId, + isGeoBlockEnabled, + enableLpTokensByDefault, +}: TokensListsUpdaterProps) { const { chainId } = useAtomValue(environmentAtom) const setEnvironment = useSetAtom(updateEnvironmentAtom) const allTokensLists = useAtomValue(allListsSourcesAtom) @@ -56,8 +61,8 @@ export function TokensListsUpdater({ chainId: currentChainId, isGeoBlockEnabled const upsertLists = useSetAtom(upsertListsAtom) useEffect(() => { - setEnvironment({ chainId: currentChainId }) - }, [setEnvironment, currentChainId]) + setEnvironment({ chainId: currentChainId, enableLpTokensByDefault }) + }, [setEnvironment, currentChainId, enableLpTokensByDefault]) // Fetch tokens lists once in 6 hours const { data: listsStates, isLoading } = useSWR( From 7c18b7d85de6feac9c7e64740a93572f3af3c273 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 29 Oct 2024 14:20:32 +0500 Subject: [PATCH 067/116] feat(yield): define token category by default for selection (#5018) --- .../CurrencyInputPanel/CurrencyInputPanel.tsx | 10 ++-- .../updaters/orders/OrdersFromApiUpdater.ts | 12 ++--- .../containers/RescueFundsFromProxy/index.tsx | 6 +-- .../containers/LpTokenListsWidget/index.tsx | 14 +++-- .../getDefaultTokenListCategories.ts | 38 ++++++++++++++ .../containers/SelectTokenWidget/index.tsx | 33 +++++++++--- .../hooks/useOpenTokenSelectWidget.ts | 13 +++-- .../pure/SelectTokenModal/index.tsx | 3 ++ .../tokensList/state/selectTokenWidgetAtom.ts | 6 ++- .../tokensList/utils/tokensListSorter.ts | 4 ++ .../TradeWidget/TradeWidgetForm.tsx | 19 ++++++- .../yield/hooks/useLpTokensWithBalances.ts | 41 ++++++++------- .../src/modules/yield/shared.ts | 1 + .../yield/updaters/PoolsInfoUpdater/index.tsx | 18 +++---- .../updaters/BalancesAndAllowancesUpdater.tsx | 4 +- libs/common-const/src/types.ts | 4 +- ...{useAllTokens.ts => useAllActiveTokens.ts} | 3 +- .../tokens/src/hooks/tokens/useAllLpTokens.ts | 41 ++++++--------- libs/tokens/src/index.ts | 2 +- libs/tokens/src/state/tokens/allTokensAtom.ts | 51 +++++++++---------- libs/tokens/src/types.ts | 1 + .../src/utils/tokenMapToListWithLogo.ts | 6 ++- libs/types/src/common.ts | 2 + 23 files changed, 220 insertions(+), 112 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/getDefaultTokenListCategories.ts rename libs/tokens/src/hooks/tokens/{useAllTokens.ts => useAllActiveTokens.ts} (78%) diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx index bbad0b8fd6..8b0050c278 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx @@ -43,7 +43,11 @@ export interface CurrencyInputPanelProps extends Partial { subsidyAndBalance?: BalanceAndSubsidy onCurrencySelection: (field: Field, currency: Currency) => void onUserInput: (field: Field, typedValue: string) => void - openTokenSelectWidget(selectedToken: string | undefined, onCurrencySelection: (currency: Currency) => void): void + openTokenSelectWidget( + selectedToken: string | undefined, + field: Field | undefined, + onCurrencySelection: (currency: Currency) => void, + ): void topLabel?: string } @@ -84,7 +88,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) { setTypedValue(typedValue) onUserInput(field, typedValue) }, - [onUserInput, field] + [onUserInput, field], ) const handleMaxInput = useCallback(() => { if (!maxBalance) { @@ -136,7 +140,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) { }, [_priceImpactParams, bothCurrenciesSet]) const onTokenSelectClick = useCallback(() => { - openTokenSelectWidget(selectedTokenAddress, (currency) => onCurrencySelection(field, currency)) + openTokenSelectWidget(selectedTokenAddress, field, (currency) => onCurrencySelection(field, currency)) }, [openTokenSelectWidget, selectedTokenAddress, onCurrencySelection, field]) return ( diff --git a/apps/cowswap-frontend/src/common/updaters/orders/OrdersFromApiUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/OrdersFromApiUpdater.ts index 3d0a497c21..94bdd650f5 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/OrdersFromApiUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/OrdersFromApiUpdater.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' import { EnrichedOrder, EthflowData, OrderClass, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' -import { TokensByAddress, useAllTokens } from '@cowprotocol/tokens' +import { TokensByAddress, useAllActiveTokens } from '@cowprotocol/tokens' import { useIsSafeWallet, useWalletInfo } from '@cowprotocol/wallet' import { Order, OrderStatus } from 'legacy/state/orders/actions' @@ -32,7 +32,7 @@ const statusMapping: Record = { function _transformOrderBookOrderToStoreOrder( order: EnrichedOrder, chainId: ChainId, - allTokens: TokensByAddress + allTokens: TokensByAddress, ): Order | undefined { const { uid: id, @@ -63,7 +63,7 @@ function _transformOrderBookOrderToStoreOrder( console.warn( `OrdersFromApiUpdater::Tokens not found for order ${id}: sellToken ${ !inputToken ? sellToken : 'found' - } - buyToken ${!outputToken ? buyToken : 'found'}` + } - buyToken ${!outputToken ? buyToken : 'found'}`, ) return } @@ -110,7 +110,7 @@ function _getInputToken( isEthFlow: boolean, chainId: ChainId, sellToken: string, - allTokens: TokensByAddress + allTokens: TokensByAddress, ): ReturnType { return isEthFlow ? NATIVE_CURRENCIES[chainId] : getTokenFromMapping(sellToken, chainId, allTokens) } @@ -141,7 +141,7 @@ export function OrdersFromApiUpdater(): null { const clearOrderStorage = useClearOrdersStorage() const { account, chainId } = useWalletInfo() - const allTokens = useAllTokens() + const allTokens = useAllActiveTokens() const tokensAreLoaded = useMemo(() => Object.keys(allTokens).length > 0, [allTokens]) const addOrUpdateOrders = useAddOrUpdateOrders() const updateApiOrders = useSetAtom(apiOrdersAtom) @@ -176,7 +176,7 @@ export function OrdersFromApiUpdater(): null { console.error(`OrdersFromApiUpdater::Failed to fetch orders`, e) } }, - [addOrUpdateOrders, ordersFromOrderBook, getTokensForOrdersList, isSafeWallet] + [addOrUpdateOrders, ordersFromOrderBook, getTokensForOrdersList, isSafeWallet], ) useEffect(() => { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx index a5034eaa04..720a722644 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx @@ -32,7 +32,7 @@ const BALANCE_UPDATE_INTERVAL = ms`5s` const selectedCurrencyAtom = atom(undefined) export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { - const [selectedCurrency, seSelectedCurrency] = useAtom(selectedCurrencyAtom) + const [selectedCurrency, setSelectedCurrency] = useAtom(selectedCurrencyAtom) const [tokenBalance, setTokenBalance] = useState | null>(null) const selectedTokenAddress = selectedCurrency ? getCurrencyAddress(selectedCurrency) : undefined @@ -81,8 +81,8 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { }, [rescueFundsCallback, addTransaction, handleSetError]) const onCurrencySelectClick = useCallback(() => { - onSelectToken(selectedTokenAddress, seSelectedCurrency) - }, [onSelectToken, selectedTokenAddress, seSelectedCurrency]) + onSelectToken(selectedTokenAddress, undefined, undefined, setSelectedCurrency) + }, [onSelectToken, selectedTokenAddress, setSelectedCurrency]) return ( diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx index 57defdf16b..338749af7a 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenListsWidget/index.tsx @@ -2,7 +2,13 @@ import { ReactNode, useMemo } from 'react' import { useTokensBalances } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' -import { getTokenSearchFilter, LP_TOKEN_LIST_CATEGORIES, TokenListCategory, useAllLpTokens } from '@cowprotocol/tokens' +import { + getTokenSearchFilter, + LP_TOKEN_LIST_CATEGORIES, + LP_TOKEN_LIST_COW_AMM_ONLY, + TokenListCategory, + useAllLpTokens, +} from '@cowprotocol/tokens' import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' import { usePoolsInfo } from 'modules/yield/shared' @@ -16,6 +22,7 @@ interface LpTokenListsProps { account: string | undefined children: ReactNode search: string + disableErc20?: boolean onSelectToken(token: TokenWithLogo): void openPoolPage(poolAddress: string): void tokenListCategoryState: [T, (category: T) => void] @@ -35,7 +42,7 @@ const tabs = [ />{' '} CoW AMM only - ), value: [TokenListCategory.COW_AMM_LP] }, + ), value: LP_TOKEN_LIST_COW_AMM_ONLY }, ] export function LpTokenListsWidget({ @@ -45,6 +52,7 @@ export function LpTokenListsWidget({ onSelectToken, openPoolPage, tokenListCategoryState, + disableErc20, }: LpTokenListsProps) { const [listsCategories, setListsCategories] = tokenListCategoryState const lpTokens = useAllLpTokens(listsCategories) @@ -62,7 +70,7 @@ export function LpTokenListsWidget({ return ( <> - {tabs.map((tab) => { + {(disableErc20 ? tabs.slice(1) : tabs).map((tab) => { return ( 0 ? LP_TOKEN_LIST_CATEGORIES : null +} diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index 4f7ecdcc87..5d4edd86c9 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' import { useTokensBalances } from '@cowprotocol/balances-and-allowances' -import { TokenWithLogo } from '@cowprotocol/common-const' +import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' import { isInjectedWidget } from '@cowprotocol/common-utils' import { ListState, @@ -9,7 +9,7 @@ import { useAddList, useAddUserToken, useAllListsList, - useAllTokens, + useAllActiveTokens, useFavoriteTokens, useUnsupportedTokens, useUserAddedTokens, @@ -18,8 +18,13 @@ import { useWalletInfo } from '@cowprotocol/wallet' import styled from 'styled-components/macro' +import { Field } from 'legacy/state/types' + import { addListAnalytics } from 'modules/analytics' import { usePermitCompatibleTokens } from 'modules/permit' +import { useLpTokensWithBalances } from 'modules/yield/shared' + +import { getDefaultTokenListCategories } from './getDefaultTokenListCategories' import { useOnTokenListAddingError } from '../../hooks/useOnTokenListAddingError' import { useSelectTokenWidgetState } from '../../hooks/useSelectTokenWidgetState' @@ -44,10 +49,25 @@ interface SelectTokenWidgetProps { } export function SelectTokenWidget({ displayLpTokenLists }: SelectTokenWidgetProps) { - const { open, onSelectToken, tokenToImport, listToImport, selectedToken, onInputPressEnter, selectedPoolAddress } = - useSelectTokenWidgetState() + const { + open, + onSelectToken, + tokenToImport, + listToImport, + selectedToken, + onInputPressEnter, + selectedPoolAddress, + field, + oppositeToken, + } = useSelectTokenWidgetState() + const { count: lpTokensWithBalancesCount } = useLpTokensWithBalances() + const [isManageWidgetOpen, setIsManageWidgetOpen] = useState(false) - const tokenListCategoryState = useState(null) + const isSellErc20Selected = field === Field.OUTPUT && !(oppositeToken instanceof LpToken) + + const tokenListCategoryState = useState( + getDefaultTokenListCategories(field, oppositeToken, lpTokensWithBalancesCount), + ) const updateSelectTokenWidget = useUpdateSelectTokenWidgetState() const { account } = useWalletInfo() @@ -55,7 +75,7 @@ export function SelectTokenWidget({ displayLpTokenLists }: SelectTokenWidgetProp const addCustomTokenLists = useAddList((source) => addListAnalytics('Success', source)) const importTokenCallback = useAddUserToken() - const allTokens = useAllTokens() + const allTokens = useAllActiveTokens() const favoriteTokens = useFavoriteTokens() const userAddedTokens = useUserAddedTokens() const allTokenLists = useAllListsList() @@ -181,6 +201,7 @@ export function SelectTokenWidget({ displayLpTokenLists }: SelectTokenWidgetProp hideFavoriteTokensTooltip={isInjectedWidgetMode} openPoolPage={openPoolPage} tokenListCategoryState={tokenListCategoryState} + disableErc20={isSellErc20Selected} account={account} /> ) diff --git a/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts b/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts index 88ca7efbd8..3ea5263e31 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/hooks/useOpenTokenSelectWidget.ts @@ -1,19 +1,26 @@ import { useCallback } from 'react' +import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' import { Currency } from '@uniswap/sdk-core' +import { Field } from 'legacy/state/types' + import { useUpdateSelectTokenWidgetState } from './useUpdateSelectTokenWidgetState' export function useOpenTokenSelectWidget(): ( selectedToken: string | undefined, - onSelectToken: (currency: Currency) => void + field: Field | undefined, + oppositeToken: TokenWithLogo | LpToken | Currency | undefined, + onSelectToken: (currency: Currency) => void, ) => void { const updateSelectTokenWidget = useUpdateSelectTokenWidgetState() return useCallback( - (selectedToken, onSelectToken) => { + (selectedToken, field, oppositeToken, onSelectToken) => { updateSelectTokenWidget({ selectedToken, + field, + oppositeToken, open: true, onSelectToken: (currency) => { updateSelectTokenWidget({ open: false }) @@ -21,6 +28,6 @@ export function useOpenTokenSelectWidget(): ( }, }) }, - [updateSelectTokenWidget] + [updateSelectTokenWidget], ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index 26ff5898c5..c4c294dac6 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -26,6 +26,7 @@ export interface SelectTokenModalProps { permitCompatibleTokens: PermitCompatibleTokens hideFavoriteTokensTooltip?: boolean displayLpTokenLists?: boolean + disableErc20?: boolean account: string | undefined tokenListCategoryState: [T, (category: T) => void] @@ -60,6 +61,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) { displayLpTokenLists, openPoolPage, tokenListCategoryState, + disableErc20, } = props const [inputValue, setInputValue] = useState(defaultInputValue) @@ -119,6 +121,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) { search={inputValue} onSelectToken={onSelectToken} openPoolPage={openPoolPage} + disableErc20={disableErc20} tokenListCategoryState={tokenListCategoryState} > {allListsContent} diff --git a/apps/cowswap-frontend/src/modules/tokensList/state/selectTokenWidgetAtom.ts b/apps/cowswap-frontend/src/modules/tokensList/state/selectTokenWidgetAtom.ts index 5e01b5ab42..3d700fa927 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/state/selectTokenWidgetAtom.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/state/selectTokenWidgetAtom.ts @@ -1,14 +1,18 @@ import { atom } from 'jotai' -import { TokenWithLogo } from '@cowprotocol/common-const' +import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' import { atomWithPartialUpdate } from '@cowprotocol/common-utils' import { ListState } from '@cowprotocol/tokens' import { Command } from '@cowprotocol/types' import { Currency } from '@uniswap/sdk-core' +import { Field } from 'legacy/state/types' + export const { atom: selectTokenWidgetAtom, updateAtom: updateSelectTokenWidgetAtom } = atomWithPartialUpdate( atom<{ open: boolean + field?: Field + oppositeToken?: TokenWithLogo | LpToken | Currency selectedToken?: string selectedPoolAddress?: string tokenToImport?: TokenWithLogo diff --git a/apps/cowswap-frontend/src/modules/tokensList/utils/tokensListSorter.ts b/apps/cowswap-frontend/src/modules/tokensList/utils/tokensListSorter.ts index abe65adedd..cc14d3e7d1 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/utils/tokensListSorter.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/utils/tokensListSorter.ts @@ -18,6 +18,10 @@ export function tokensListSorter(balances: BalancesState['values']): (a: TokenWi return +bBalance.sub(aBalance) } + if (aBalance && !bBalance) { + return -1 + } + return 0 } } diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index 52fb94db66..0c27d6c16b 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -4,12 +4,14 @@ import ICON_ORDERS from '@cowprotocol/assets/svg/orders.svg' import { isInjectedWidget, maxAmountSpend } from '@cowprotocol/common-utils' import { ButtonOutlined, Media, MY_ORDERS_ID } from '@cowprotocol/ui' import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' +import { Currency } from '@uniswap/sdk-core' import { t } from '@lingui/macro' import SVG from 'react-inlinesvg' import { AccountElement } from 'legacy/components/Header/AccountElement' import { upToLarge, useMediaQuery } from 'legacy/hooks/useMediaQuery' +import { Field } from 'legacy/state/types' import { useToggleAccountModal } from 'modules/account' import { useInjectedWidgetParams } from 'modules/injectedWidget' @@ -125,10 +127,23 @@ export function TradeWidgetForm(props: TradeWidgetProps) { onCurrencySelection, onUserInput, allowsOffchainSigning, - openTokenSelectWidget, tokenSelectorDisabled: alternativeOrderModalVisible, } + const openSellTokenSelect = useCallback( + (selectedToken: string | undefined, field: Field | undefined, onSelectToken: (currency: Currency) => void) => { + openTokenSelectWidget(selectedToken, field, outputCurrencyInfo.currency || undefined, onSelectToken) + }, + [openTokenSelectWidget, outputCurrencyInfo.currency], + ) + + const openBuyTokenSelect = useCallback( + (selectedToken: string | undefined, field: Field | undefined, onSelectToken: (currency: Currency) => void) => { + openTokenSelectWidget(selectedToken, field, inputCurrencyInfo.currency || undefined, onSelectToken) + }, + [openTokenSelectWidget, inputCurrencyInfo.currency], + ) + const toggleAccountModal = useToggleAccountModal() const handleMyOrdersClick = useCallback(() => { @@ -169,6 +184,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { showSetMax={showSetMax} maxBalance={maxBalance} topLabel={isWrapOrUnwrap ? undefined : inputCurrencyInfo.label} + openTokenSelectWidget={openSellTokenSelect} {...currencyInputCommonProps} />
    @@ -194,6 +210,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { currencyInfo={outputCurrencyInfo} priceImpactParams={!disablePriceImpact ? priceImpact : undefined} topLabel={isWrapOrUnwrap ? undefined : outputCurrencyInfo.label} + openTokenSelectWidget={openBuyTokenSelect} {...currencyInputCommonProps} />
    diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts index 2f3be9e3d7..ad360b0e5f 100644 --- a/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts @@ -1,35 +1,42 @@ -import { useMemo } from 'react' - import { useTokensBalances } from '@cowprotocol/balances-and-allowances' -import { LpToken } from '@cowprotocol/common-const' +import { LpToken, SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' import { TokenListCategory, useAllLpTokens } from '@cowprotocol/tokens' import { BigNumber } from '@ethersproject/bignumber' +import useSWR from 'swr' + export type LpTokenWithBalance = { token: LpToken balance: BigNumber } export const LP_CATEGORY = [TokenListCategory.LP] +const DEFAULT_STATE = { tokens: {} as Record, count: 0 } + export function useLpTokensWithBalances() { const lpTokens = useAllLpTokens(LP_CATEGORY) const { values: balances } = useTokensBalances() - return useMemo(() => { - if (lpTokens.length === 0) return undefined + return useSWR( + [lpTokens, balances, 'useLpTokensWithBalances'], + ([lpTokens, balances]) => { + if (lpTokens.length === 0) return DEFAULT_STATE - return lpTokens.reduce( - (acc, token) => { - const addressLower = token.address.toLowerCase() - const balance = balances[addressLower] + return lpTokens.reduce( + (acc, token) => { + const addressLower = token.address.toLowerCase() + const balance = balances[addressLower] - if (balance && !balance.isZero()) { - acc[addressLower] = { token, balance } - } + if (balance && !balance.isZero()) { + acc.count++ + acc.tokens[addressLower] = { token, balance } + } - return acc - }, - {} as Record, - ) - }, [lpTokens, balances]) + return acc + }, + { ...DEFAULT_STATE }, + ) + }, + { ...SWR_NO_REFRESH_OPTIONS, fallbackData: DEFAULT_STATE }, + ).data } diff --git a/apps/cowswap-frontend/src/modules/yield/shared.ts b/apps/cowswap-frontend/src/modules/yield/shared.ts index 24f46a452d..3ed6eae2de 100644 --- a/apps/cowswap-frontend/src/modules/yield/shared.ts +++ b/apps/cowswap-frontend/src/modules/yield/shared.ts @@ -1,3 +1,4 @@ export { PoolsInfoUpdater } from './updaters/PoolsInfoUpdater' export { usePoolsInfo } from './hooks/usePoolsInfo' +export { useLpTokensWithBalances } from './hooks/useLpTokensWithBalances' export type { PoolInfo, PoolInfoStates } from './state/poolsInfoAtom' diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx index 66455246fe..5b9561e3b3 100644 --- a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx @@ -28,24 +28,22 @@ export function PoolsInfoUpdater() { const tradeTypeInfo = useTradeTypeInfo() const isYield = tradeTypeInfo?.tradeType === TradeType.YIELD - const lpTokensWithBalances = useLpTokensWithBalances() + const { tokens: lpTokensWithBalances } = useLpTokensWithBalances() const tokensToUpdate = useMemo(() => { - return lpTokensWithBalances - ? Object.keys(lpTokensWithBalances).filter((address) => { - const state = poolsInfo?.[address] + return Object.keys(lpTokensWithBalances).filter((address) => { + const state = poolsInfo?.[address] - if (!state) return true + if (!state) return true - return state.updatedAt + POOL_INFO_CACHE_TIME > Date.now() - }) - : null + return state.updatedAt + POOL_INFO_CACHE_TIME > Date.now() + }) }, [lpTokensWithBalances, poolsInfo]) - const tokensKey = useMemo(() => tokensToUpdate?.join(','), [tokensToUpdate]) + const tokensKey = useMemo(() => tokensToUpdate.join(','), [tokensToUpdate]) useEffect(() => { - if ((tokensToUpdate && tokensToUpdate.length > 0) || isYield) { + if (tokensToUpdate.length > 0 || isYield) { fetchPoolsInfo(isYield ? null : tokensToUpdate).then(upsertPoolsInfo) } }, [isYield, tokensKey]) diff --git a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx index e4a81608d0..ebdc0f5300 100644 --- a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx +++ b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo } from 'react' import { LpToken, NATIVE_CURRENCIES } from '@cowprotocol/common-const' import type { SupportedChainId } from '@cowprotocol/cow-sdk' -import { useAllTokens } from '@cowprotocol/tokens' +import { useAllActiveTokens } from '@cowprotocol/tokens' import ms from 'ms.macro' @@ -24,7 +24,7 @@ export interface BalancesAndAllowancesUpdaterProps { export function BalancesAndAllowancesUpdater({ account, chainId }: BalancesAndAllowancesUpdaterProps) { const setBalances = useSetAtom(balancesAtom) - const allTokens = useAllTokens() + const allTokens = useAllActiveTokens() const { data: nativeTokenBalance } = useNativeTokenBalance(account) const tokenAddresses = useMemo( diff --git a/libs/common-const/src/types.ts b/libs/common-const/src/types.ts index ed3d668c4a..60e82423ac 100644 --- a/libs/common-const/src/types.ts +++ b/libs/common-const/src/types.ts @@ -22,9 +22,10 @@ export class TokenWithLogo extends Token { } export class LpToken extends TokenWithLogo { - static fromToken(token: Token | TokenInfo): LpToken { + static fromTokenToLp(token: Token | TokenInfo, isCowAmm: boolean): LpToken { return new LpToken( token instanceof Token ? emptyTokens : token.tokens || emptyTokens, + isCowAmm, token.chainId, token.address, token.decimals, @@ -35,6 +36,7 @@ export class LpToken extends TokenWithLogo { constructor( public tokens: string[], + public isCowAmm: boolean, chainId: number, address: string, decimals: number, diff --git a/libs/tokens/src/hooks/tokens/useAllTokens.ts b/libs/tokens/src/hooks/tokens/useAllActiveTokens.ts similarity index 78% rename from libs/tokens/src/hooks/tokens/useAllTokens.ts rename to libs/tokens/src/hooks/tokens/useAllActiveTokens.ts index 958701c96a..1e75125d32 100644 --- a/libs/tokens/src/hooks/tokens/useAllTokens.ts +++ b/libs/tokens/src/hooks/tokens/useAllActiveTokens.ts @@ -4,7 +4,6 @@ import { TokenWithLogo } from '@cowprotocol/common-const' import { activeTokensAtom } from '../../state/tokens/allTokensAtom' - -export function useAllTokens(): TokenWithLogo[] { +export function useAllActiveTokens(): TokenWithLogo[] { return useAtomValue(activeTokensAtom) } diff --git a/libs/tokens/src/hooks/tokens/useAllLpTokens.ts b/libs/tokens/src/hooks/tokens/useAllLpTokens.ts index 68d94a767e..2e53911261 100644 --- a/libs/tokens/src/hooks/tokens/useAllLpTokens.ts +++ b/libs/tokens/src/hooks/tokens/useAllLpTokens.ts @@ -1,42 +1,33 @@ -import { useAtomValue } from 'jotai' +import { useAtomValue } from 'jotai/index' import { LpToken, SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' import useSWR from 'swr' -import { environmentAtom } from '../../state/environmentAtom' -import { listsStatesListAtom } from '../../state/tokenLists/tokenListsStateAtom' -import { TokenListCategory, TokensMap } from '../../types' -import { parseTokenInfo } from '../../utils/parseTokenInfo' -import { tokenMapToListWithLogo } from '../../utils/tokenMapToListWithLogo' +import { activeTokensAtom, inactiveTokensAtom } from '../../state/tokens/allTokensAtom' +import { TokenListCategory } from '../../types' const fallbackData: LpToken[] = [] export function useAllLpTokens(categories: TokenListCategory[] | null): LpToken[] { - const { chainId } = useAtomValue(environmentAtom) - const state = useAtomValue(listsStatesListAtom) + const activeTokens = useAtomValue(activeTokensAtom) + const inactiveTokens = useAtomValue(inactiveTokensAtom) return useSWR( - categories ? [state, chainId, categories] : null, - ([state, chainId, categories]) => { - const tokensMap = state.reduce((acc, list) => { - if (!list.category || !categories.includes(list.category)) { - return acc - } - - list.list.tokens.forEach((token) => { - const tokenInfo = parseTokenInfo(chainId, token) - const tokenAddressKey = tokenInfo?.address.toLowerCase() - - if (!tokenInfo || !tokenAddressKey) return - - acc[tokenAddressKey] = tokenInfo - }) - + categories ? [activeTokens, inactiveTokens, categories] : null, + ([activeTokens, inactiveTokens, categories]) => { + const activeTokensMap = activeTokens.reduce>((acc, token) => { + acc[token.address] = true return acc }, {}) - return tokenMapToListWithLogo(tokensMap, chainId) as LpToken[] + const allTokens = [...activeTokens, ...inactiveTokens.filter((token) => !activeTokensMap[token.address])] + const selectOnlyCoWAmm = categories?.length === 1 && categories.includes(TokenListCategory.COW_AMM_LP) + + return allTokens.filter((token) => { + const isLp = token instanceof LpToken + return isLp ? (selectOnlyCoWAmm ? token.isCowAmm : true) : false + }) as LpToken[] }, { ...SWR_NO_REFRESH_OPTIONS, fallbackData }, ).data diff --git a/libs/tokens/src/index.ts b/libs/tokens/src/index.ts index 6100e61f21..825141feff 100644 --- a/libs/tokens/src/index.ts +++ b/libs/tokens/src/index.ts @@ -16,7 +16,7 @@ export type { TokenSearchResponse } from './hooks/tokens/useSearchToken' // Hooks export { useAllListsList } from './hooks/lists/useAllListsList' export { useAddList } from './hooks/lists/useAddList' -export { useAllTokens } from './hooks/tokens/useAllTokens' +export { useAllActiveTokens } from './hooks/tokens/useAllActiveTokens' export { useVirtualLists } from './hooks/lists/useVirtualLists' export { useFavoriteTokens } from './hooks/tokens/favorite/useFavoriteTokens' export { useUserAddedTokens } from './hooks/tokens/userAdded/useUserAddedTokens' diff --git a/libs/tokens/src/state/tokens/allTokensAtom.ts b/libs/tokens/src/state/tokens/allTokensAtom.ts index c243d4e762..d122bc6f30 100644 --- a/libs/tokens/src/state/tokens/allTokensAtom.ts +++ b/libs/tokens/src/state/tokens/allTokensAtom.ts @@ -6,7 +6,7 @@ import { TokenInfo } from '@cowprotocol/types' import { favoriteTokensAtom } from './favoriteTokensAtom' import { userAddedTokensAtom } from './userAddedTokensAtom' -import { LP_TOKEN_LIST_CATEGORIES, TokensMap } from '../../types' +import { TokenListCategory, TokensMap } from '../../types' import { lowerCaseTokensMap } from '../../utils/lowerCaseTokensMap' import { parseTokenInfo } from '../../utils/parseTokenInfo' import { tokenMapToListWithLogo } from '../../utils/tokenMapToListWithLogo' @@ -21,12 +21,12 @@ export interface TokensBySymbol { [address: string]: TokenWithLogo[] } -export interface TokensState { +interface TokensState { activeTokens: TokensMap inactiveTokens: TokensMap } -export const tokensStateAtom = atom((get) => { +const tokensStateAtom = atom((get) => { const { chainId } = get(environmentAtom) const listsStatesList = get(listsStatesListAtom) const listsEnabledState = get(listsEnabledStateAtom) @@ -36,11 +36,21 @@ export const tokensStateAtom = atom((get) => { const isListEnabled = listsEnabledState[list.source] list.list.tokens.forEach((token) => { + const category = list.category || TokenListCategory.ERC20 const tokenInfo = parseTokenInfo(chainId, token) const tokenAddressKey = tokenInfo?.address.toLowerCase() if (!tokenInfo || !tokenAddressKey) return + if (category === TokenListCategory.LP) { + tokenInfo.isLpToken = true + } + + if (category === TokenListCategory.COW_AMM_LP) { + tokenInfo.isLpToken = true + tokenInfo.isCoWAmmToken = true + } + if (isListEnabled) { if (!acc.activeTokens[tokenAddressKey]) { acc.activeTokens[tokenAddressKey] = tokenInfo @@ -58,28 +68,6 @@ export const tokensStateAtom = atom((get) => { ) }) -export const lpTokensMapAtom = atom((get) => { - const { chainId } = get(environmentAtom) - const listsStatesList = get(listsStatesListAtom) - - return listsStatesList.reduce((acc, list) => { - if (!list.category || !LP_TOKEN_LIST_CATEGORIES.includes(list.category)) { - return acc - } - - list.list.tokens.forEach((token) => { - const tokenInfo = parseTokenInfo(chainId, token) - const tokenAddressKey = tokenInfo?.address.toLowerCase() - - if (!tokenInfo || !tokenAddressKey) return - - acc[tokenAddressKey] = tokenInfo - }) - - return acc - }, {}) -}) - /** * Returns a list of tokens that are active and sorted alphabetically * The list includes: native token, user added tokens, favorite tokens and tokens from active lists @@ -91,16 +79,25 @@ export const activeTokensAtom = atom((get) => { const favoriteTokensState = get(favoriteTokensAtom) const tokensMap = get(tokensStateAtom) - const lpTokensMap = get(lpTokensMapAtom) const nativeToken = NATIVE_CURRENCIES[chainId] return tokenMapToListWithLogo( { [nativeToken.address.toLowerCase()]: nativeToken as TokenInfo, ...tokensMap.activeTokens, - ...(enableLpTokensByDefault ? lpTokensMap : null), ...lowerCaseTokensMap(userAddedTokens[chainId]), ...lowerCaseTokensMap(favoriteTokensState[chainId]), + ...(enableLpTokensByDefault + ? Object.keys(tokensMap.inactiveTokens).reduce((acc, key) => { + const token = tokensMap.inactiveTokens[key] + + if (token.isLpToken) { + acc[key] = token + } + + return acc + }, {}) + : null), }, chainId, ) diff --git a/libs/tokens/src/types.ts b/libs/tokens/src/types.ts index 8cbad348c5..b07e183a51 100644 --- a/libs/tokens/src/types.ts +++ b/libs/tokens/src/types.ts @@ -9,6 +9,7 @@ export enum TokenListCategory { } export const LP_TOKEN_LIST_CATEGORIES = [TokenListCategory.LP, TokenListCategory.COW_AMM_LP] +export const LP_TOKEN_LIST_COW_AMM_ONLY = [TokenListCategory.COW_AMM_LP] export type ListSourceConfig = { widgetAppCode?: string diff --git a/libs/tokens/src/utils/tokenMapToListWithLogo.ts b/libs/tokens/src/utils/tokenMapToListWithLogo.ts index d66c8a1d73..b3c7e77081 100644 --- a/libs/tokens/src/utils/tokenMapToListWithLogo.ts +++ b/libs/tokens/src/utils/tokenMapToListWithLogo.ts @@ -9,5 +9,9 @@ export function tokenMapToListWithLogo(tokenMap: TokensMap, chainId: number): To return Object.values(tokenMap) .filter((token) => token.chainId === chainId) .sort((a, b) => a.symbol.localeCompare(b.symbol)) - .map((token) => (token.tokens ? LpToken.fromToken(token) : TokenWithLogo.fromToken(token, token.logoURI))) + .map((token) => + token.isLpToken + ? LpToken.fromTokenToLp(token, !!token.isCoWAmmToken) + : TokenWithLogo.fromToken(token, token.logoURI), + ) } diff --git a/libs/types/src/common.ts b/libs/types/src/common.ts index 0228b65166..1b3e2ae88b 100644 --- a/libs/types/src/common.ts +++ b/libs/types/src/common.ts @@ -24,4 +24,6 @@ export type TokenInfo = { symbol: string logoURI?: string tokens?: string[] + isLpToken?: boolean + isCoWAmmToken?: boolean } From 90cd6e6d3e64af7a1f632a48a98007573891ca01 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 29 Oct 2024 14:22:04 +0500 Subject: [PATCH 068/116] refactor: clean up cow amm banner (#5033) --- .../common/containers/CoWAmmBanner/index.tsx | 67 +++ .../index.tsx} | 0 .../pure/CoWAMMBanner/CoWAmmBannerContent.tsx | 402 ------------------ .../pure/CoWAMMBanner/cowAmmBannerState.ts | 5 - .../src/common/pure/CoWAMMBanner/index.tsx | 84 ---- .../pure/CoWAmmBannerContent/Common/index.tsx | 39 ++ .../ComparisonMessage/index.tsx | 66 +++ .../GlobalContent/index.tsx | 84 ++++ .../CoWAmmBannerContent/LpEmblems/index.tsx | 76 ++++ .../CoWAmmBannerContent/PoolInfo/index.tsx | 52 +++ .../TokenSelectorContent/index.tsx | 76 ++++ .../dummyData.ts | 0 .../common/pure/CoWAmmBannerContent/index.tsx | 102 +++++ .../styled.ts | 2 +- .../common/pure/CoWAmmBannerContent/types.ts | 13 + .../src/common/pure/VirtualList/index.tsx | 11 +- .../application/containers/App/index.tsx | 6 +- .../pure/TokensVirtualList/index.tsx | 11 +- 18 files changed, 595 insertions(+), 501 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx rename apps/cowswap-frontend/src/common/pure/{CoWAMMBanner/arrowBackground.tsx => ArrowBackground/index.tsx} (100%) delete mode 100644 apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx delete mode 100644 apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts delete mode 100644 apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/Common/index.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/ComparisonMessage/index.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/GlobalContent/index.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/LpEmblems/index.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/PoolInfo/index.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/TokenSelectorContent/index.tsx rename apps/cowswap-frontend/src/common/pure/{CoWAMMBanner => CoWAmmBannerContent}/dummyData.ts (100%) create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx rename apps/cowswap-frontend/src/common/pure/{CoWAMMBanner => CoWAmmBannerContent}/styled.ts (99%) create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/types.ts diff --git a/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx new file mode 100644 index 0000000000..58a67bf19a --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx @@ -0,0 +1,67 @@ +import { useCallback } from 'react' + +import { isInjectedWidget } from '@cowprotocol/common-utils' +import { ClosableBanner } from '@cowprotocol/ui' +import { useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet' + +import { useIsDarkMode } from 'legacy/state/user/hooks' + +import { cowAnalytics } from 'modules/analytics' + +import { useIsProviderNetworkUnsupported } from '../../hooks/useIsProviderNetworkUnsupported' +import { CoWAmmBannerContent } from '../../pure/CoWAmmBannerContent' +import { dummyData, lpTokenConfig } from '../../pure/CoWAmmBannerContent/dummyData' + +const ANALYTICS_URL = 'https://cow.fi/pools?utm_source=swap.cow.fi&utm_medium=web&utm_content=cow_amm_banner' + +interface BannerProps { + isTokenSelectorView?: boolean +} + +export function CoWAmmBanner({ isTokenSelectorView }: BannerProps) { + const isDarkMode = useIsDarkMode() + const isInjectedWidgetMode = isInjectedWidget() + const { account } = useWalletInfo() + const isChainIdUnsupported = useIsProviderNetworkUnsupported() + + const key = isTokenSelectorView ? 'tokenSelector' : 'global' + const handleCTAClick = useCallback(() => { + cowAnalytics.sendEvent({ + category: 'CoW Swap', + action: `CoW AMM Banner [${key}] CTA Clicked`, + }) + + window.open(ANALYTICS_URL, '_blank') + }, [key]) + + const handleClose = useCallback(() => { + cowAnalytics.sendEvent({ + category: 'CoW Swap', + action: `CoW AMM Banner [${key}] Closed`, + }) + }, [key]) + + const bannerId = `cow_amm_banner_2024_va_${key}` + + const isSmartContractWallet = useIsSmartContractWallet() + + if (isInjectedWidgetMode || !account || isChainIdUnsupported) return null + + return ClosableBanner(bannerId, (close) => ( + { + handleClose() + close() + }} + /> + )) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx b/apps/cowswap-frontend/src/common/pure/ArrowBackground/index.tsx similarity index 100% rename from apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx rename to apps/cowswap-frontend/src/common/pure/ArrowBackground/index.tsx diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx deleted file mode 100644 index 9a6557cd21..0000000000 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import React, { useCallback, useMemo, useRef } from 'react' - -import ICON_ARROW from '@cowprotocol/assets/cow-swap/arrow.svg' -import ICON_CURVE from '@cowprotocol/assets/cow-swap/icon-curve.svg' -import ICON_PANCAKESWAP from '@cowprotocol/assets/cow-swap/icon-pancakeswap.svg' -import ICON_SUSHISWAP from '@cowprotocol/assets/cow-swap/icon-sushi.svg' -import ICON_UNISWAP from '@cowprotocol/assets/cow-swap/icon-uni.svg' -import ICON_STAR from '@cowprotocol/assets/cow-swap/star-shine.svg' -import { USDC, WBTC } from '@cowprotocol/common-const' -import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { TokenLogo } from '@cowprotocol/tokens' -import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' - -import SVG from 'react-inlinesvg' -import { Textfit } from 'react-textfit' - -import { upToSmall, useMediaQuery } from 'legacy/hooks/useMediaQuery' -import { useIsDarkMode } from 'legacy/state/user/hooks' - -import { ArrowBackground } from './arrowBackground' -import { - dummyData, - DummyDataType, - InferiorYieldScenario, - LpToken, - lpTokenConfig, - StateKey, - TwoLpScenario, -} from './dummyData' -import * as styledEl from './styled' - -import { BannerLocation, DEMO_DROPDOWN_OPTIONS } from './index' - -const lpTokenIcons: Record = { - [LpToken.UniswapV2]: ICON_UNISWAP, - [LpToken.Sushiswap]: ICON_SUSHISWAP, - [LpToken.PancakeSwap]: ICON_PANCAKESWAP, - [LpToken.Curve]: ICON_CURVE, -} - -interface CoWAmmBannerContentProps { - id: string - title: string - ctaText: string - location: BannerLocation - isDemo: boolean - selectedState: StateKey - setSelectedState: (state: StateKey) => void - dummyData: typeof dummyData - lpTokenConfig: typeof lpTokenConfig - onCtaClick: () => void - onClose: () => void -} - -function isTwoLpScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is TwoLpScenario { - return 'uniV2Apr' in scenario && 'sushiApr' in scenario -} - -function isInferiorYieldScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is InferiorYieldScenario { - return 'poolsCount' in scenario -} - -const renderTextfit = ( - content: React.ReactNode, - mode: 'single' | 'multi', - minFontSize: number, - maxFontSize: number, - key: string, -) => ( - - {content} - -) - -export function CoWAmmBannerContent({ - id, - title, - ctaText, - location, - isDemo, - selectedState, - setSelectedState, - dummyData, - lpTokenConfig, - onCtaClick, - onClose, -}: CoWAmmBannerContentProps) { - const isMobile = useMediaQuery(upToSmall) - const isDarkMode = useIsDarkMode() - const arrowBackgroundRef = useRef(null) - - const handleCTAMouseEnter = useCallback(() => { - if (arrowBackgroundRef.current) { - arrowBackgroundRef.current.style.visibility = 'visible' - arrowBackgroundRef.current.style.opacity = '1' - } - }, []) - - const handleCTAMouseLeave = useCallback(() => { - if (arrowBackgroundRef.current) { - arrowBackgroundRef.current.style.visibility = 'hidden' - arrowBackgroundRef.current.style.opacity = '0' - } - }, []) - - const { apr } = dummyData[selectedState] - - const aprMessage = useMemo(() => { - if (selectedState === 'uniV2InferiorWithLowAverageYield') { - const currentData = dummyData[selectedState] - if (isInferiorYieldScenario(currentData)) { - return `${currentData.poolsCount}+` - } - } - return `+${apr.toFixed(1)}%` - }, [selectedState, apr, dummyData]) - - const comparisonMessage = useMemo(() => { - const currentData = dummyData[selectedState] - - if (!currentData) { - return 'Invalid state selected' - } - - const renderPoolInfo = (poolName: string) => ( - - higher APR available for your {poolName} pool: - -
    - -
    - WBTC-USDC -
    -
    - ) - - if (isTwoLpScenario(currentData)) { - if (selectedState === 'twoLpsMixed') { - return renderPoolInfo('UNI-V2') - } else if (selectedState === 'twoLpsBothSuperior') { - const { uniV2Apr, sushiApr } = currentData - const higherAprPool = uniV2Apr > sushiApr ? 'UNI-V2' : 'SushiSwap' - return renderPoolInfo(higherAprPool) - } - } - - if (selectedState === 'uniV2Superior') { - return renderPoolInfo('UNI-V2') - } - - if (selectedState === 'uniV2InferiorWithLowAverageYield' && isInferiorYieldScenario(currentData)) { - return 'pools available to get yield on your assets!' - } - - if (currentData.hasCoWAmmPool) { - return `yield over average ${currentData.comparison} pool` - } else { - const tokens = lpTokenConfig[selectedState] - if (tokens.length > 1) { - const tokenNames = tokens - .map((token) => { - switch (token) { - case LpToken.UniswapV2: - return 'UNI-V2' - case LpToken.Sushiswap: - return 'Sushi' - case LpToken.PancakeSwap: - return 'PancakeSwap' - case LpToken.Curve: - return 'Curve' - default: - return '' - } - }) - .filter(Boolean) - - return `yield over average ${tokenNames.join(', ')} pool${tokenNames.length > 1 ? 's' : ''}` - } else { - return `yield over average UNI-V2 pool` - } - } - }, [selectedState, location, lpTokenConfig, isDarkMode, dummyData]) - - const lpEmblems = useMemo(() => { - const tokens = lpTokenConfig[selectedState] - const totalItems = tokens.length - - const renderEmblemContent = () => ( - <> - - - - - - - - ) - - if (totalItems === 0 || selectedState === 'uniV2InferiorWithLowAverageYield') { - return ( - - - {selectedState === 'uniV2InferiorWithLowAverageYield' ? ( - - ) : ( - - )} - - {renderEmblemContent()} - - ) - } - - return ( - - - {tokens.map((token, index) => ( - - - - ))} - - {renderEmblemContent()} - - ) - }, [selectedState, lpTokenConfig]) - - const renderProductLogo = useCallback( - (color: string) => ( - - ), - [], - ) - - const renderStarIcon = useCallback( - (props: any) => ( - - - - ), - [], - ) - - const renderTokenSelectorContent = () => ( - - - - - {renderProductLogo(isDarkMode ? UI.COLOR_COWAMM_LIGHT_GREEN : UI.COLOR_COWAMM_DARK_GREEN)} - {title} - - - {renderStarIcon({ size: 26, top: -16, right: 80, color: `var(${UI.COLOR_COWAMM_LIGHTER_GREEN})` })} -

    {renderTextfit(aprMessage, 'single', 35, 65, `apr-${selectedState}`)}

    - - {renderTextfit(comparisonMessage, 'multi', 15, isMobile ? 15 : 21, `comparison-${selectedState}`)} - - {renderStarIcon({ size: 16, bottom: 3, right: 20, color: `var(${UI.COLOR_COWAMM_LIGHTER_GREEN})` })} -
    - - {ctaText} - -
    -
    - ) - - const renderGlobalContent = () => { - return ( - - - - {renderProductLogo(UI.COLOR_COWAMM_LIGHT_GREEN)} - {title} - - - {renderStarIcon({ size: 36, top: -17, right: 80 })} -

    {renderTextfit(aprMessage, 'single', isMobile ? 40 : 80, isMobile ? 50 : 80, `apr-${selectedState}`)}

    - - {renderTextfit(comparisonMessage, 'multi', 10, isMobile ? 21 : 28, `comparison-${selectedState}`)} - - {renderStarIcon({ size: 26, bottom: -10, right: 20 })} -
    - - {!isMobile && ( - - - {renderTextfit( - <> - One-click convert, boost yield - , - 'multi', - 10, - 30, - `boost-yield-${selectedState}`, - )} - - {lpEmblems} - - )} - - - {ctaText} - - - Pool analytics ↗ - - -
    - ) - } - - const renderDemoDropdown = () => ( - setSelectedState(e.target.value as StateKey)}> - {DEMO_DROPDOWN_OPTIONS.map((option) => ( - - ))} - - ) - - const content = (() => { - switch (location) { - case BannerLocation.TokenSelector: - return renderTokenSelectorContent() - case BannerLocation.Global: - return renderGlobalContent() - default: - return null - } - })() - - if (!content) { - return null - } - - return ( -
    - {content} - {isDemo && renderDemoDropdown()} -
    - ) -} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts deleted file mode 100644 index bc0c9b3054..0000000000 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { atom } from 'jotai' - -import { StateKey } from './dummyData' - -export const cowAmmBannerStateAtom = atom('noLp') diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx deleted file mode 100644 index 5f831b0ece..0000000000 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useAtom } from 'jotai' -import { useCallback } from 'react' - -import { ClosableBanner } from '@cowprotocol/ui' -import { useIsSmartContractWallet } from '@cowprotocol/wallet' - -import { cowAnalytics } from 'modules/analytics' - -import { CoWAmmBannerContent } from './CoWAmmBannerContent' -import { cowAmmBannerStateAtom } from './cowAmmBannerState' -import { dummyData, lpTokenConfig } from './dummyData' - -const IS_DEMO_MODE = true -const ANALYTICS_URL = 'https://cow.fi/pools?utm_source=swap.cow.fi&utm_medium=web&utm_content=cow_amm_banner' - -export const DEMO_DROPDOWN_OPTIONS = [ - { value: 'noLp', label: '🚫 No LP Tokens' }, - { value: 'uniV2Superior', label: '⬆️ 🐴 UNI-V2 LP (Superior Yield)' }, - { value: 'uniV2Inferior', label: '⬇️ 🐴 UNI-V2 LP (Inferior Yield)' }, - { value: 'uniV2InferiorWithLowAverageYield', label: '⬇️ 🐴 UNI-V2 LP (Inferior Yield, Lower Average)' }, - { value: 'sushi', label: '⬇️ 🍣 SushiSwap LP (Inferior Yield)' }, - { value: 'curve', label: '⬇️ 🌈 Curve LP (Inferior Yield)' }, - { value: 'pancake', label: '⬇️ 🥞 PancakeSwap LP (Inferior Yield)' }, - { value: 'twoLpsMixed', label: '⬆️ 🐴 UNI-V2 (Superior) & ⬇️ 🍣 SushiSwap (Inferior) LPs' }, - { value: 'twoLpsBothSuperior', label: '⬆️ 🐴 UNI-V2 & ⬆️ 🍣 SushiSwap LPs (Both Superior, but UNI-V2 is higher)' }, - { value: 'threeLps', label: '⬇️ 🐴 UNI-V2, 🍣 SushiSwap & 🌈 Curve LPs (Inferior Yield)' }, - { value: 'fourLps', label: '⬇️ 🐴 UNI-V2, 🍣 SushiSwap, 🌈 Curve & 🥞 PancakeSwap LPs (Inferior Yield)' }, -] - -export enum BannerLocation { - Global = 'global', - TokenSelector = 'tokenSelector', -} - -interface BannerProps { - location: BannerLocation -} - -export function CoWAmmBanner({ location }: BannerProps) { - const [selectedState, setSelectedState] = useAtom(cowAmmBannerStateAtom) - - const handleCTAClick = useCallback(() => { - cowAnalytics.sendEvent({ - category: 'CoW Swap', - action: `CoW AMM Banner [${location}] CTA Clicked`, - }) - - window.open(ANALYTICS_URL, '_blank') - }, [location]) - - const handleClose = useCallback(() => { - cowAnalytics.sendEvent({ - category: 'CoW Swap', - action: `CoW AMM Banner [${location}] Closed`, - }) - }, [location]) - - const handleBannerClose = useCallback(() => { - handleClose() - }, [handleClose]) - - const bannerId = `cow_amm_banner_2024_va_${location}` - - const isSmartContractWallet = useIsSmartContractWallet() - - return ClosableBanner(bannerId, (close) => ( - { - handleBannerClose() - close() - }} - /> - )) -} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/Common/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/Common/index.tsx new file mode 100644 index 0000000000..dd85b7e4c3 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/Common/index.tsx @@ -0,0 +1,39 @@ +import React, { ReactNode } from 'react' + +import ICON_STAR from '@cowprotocol/assets/cow-swap/star-shine.svg' +import { UI } from '@cowprotocol/ui' + +import SVG from 'react-inlinesvg' +import { Textfit as ReactTextFit } from 'react-textfit' + +import * as styledEl from '../styled' + +interface TextFitProps { + children: ReactNode + mode: 'single' | 'multi' + minFontSize: number + maxFontSize: number +} + +export function TextFit({ mode, children, minFontSize, maxFontSize }: TextFitProps) { + return ( + + {children} + + ) +} + +interface StarIconProps { + size: number + top?: number + bottom?: number + right: number + color?: UI +} +export function StarIcon({ size, top, bottom, right, color }: StarIconProps) { + return ( + + + + ) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/ComparisonMessage/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/ComparisonMessage/index.tsx new file mode 100644 index 0000000000..c923a5cd02 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/ComparisonMessage/index.tsx @@ -0,0 +1,66 @@ +import React from 'react' + +import { DummyDataType, InferiorYieldScenario, LpToken, StateKey, TwoLpScenario } from '../dummyData' +import { PoolInfo } from '../PoolInfo' + +const lpTokenNames: Record = { + [LpToken.UniswapV2]: 'UNI-V2', + [LpToken.Sushiswap]: 'Sushi', + [LpToken.PancakeSwap]: 'PancakeSwap', + [LpToken.Curve]: 'Curve', +} + +function isTwoLpScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is TwoLpScenario { + return 'uniV2Apr' in scenario && 'sushiApr' in scenario +} + +function isInferiorYieldScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is InferiorYieldScenario { + return 'poolsCount' in scenario +} + +interface ComparisonMessageProps { + data: DummyDataType[keyof DummyDataType] + isDarkMode: boolean + isTokenSelectorView: boolean + selectedState: StateKey + tokens: LpToken[] +} + +export function ComparisonMessage({ + data, + tokens, + selectedState, + isDarkMode, + isTokenSelectorView, +}: ComparisonMessageProps) { + if (isTwoLpScenario(data)) { + if (selectedState === 'twoLpsMixed') { + return + } else if (selectedState === 'twoLpsBothSuperior') { + const { uniV2Apr, sushiApr } = data + const higherAprPool = uniV2Apr > sushiApr ? 'UNI-V2' : 'SushiSwap' + + return + } + } + + if (selectedState === 'uniV2Superior') { + return + } + + if (selectedState === 'uniV2InferiorWithLowAverageYield' && isInferiorYieldScenario(data)) { + return 'pools available to get yield on your assets!' + } + + if (data.hasCoWAmmPool) { + return `yield over average ${data.comparison} pool` + } else { + if (tokens.length > 1) { + const tokenNames = tokens.map((token) => lpTokenNames[token]).filter(Boolean) + + return `yield over average ${tokenNames.join(', ')} pool${tokenNames.length > 1 ? 's' : ''}` + } else { + return `yield over average UNI-V2 pool` + } + } +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/GlobalContent/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/GlobalContent/index.tsx new file mode 100644 index 0000000000..568b592a43 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/GlobalContent/index.tsx @@ -0,0 +1,84 @@ +import React, { RefObject } from 'react' + +import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' + +import { ArrowBackground } from '../../ArrowBackground' +import { StarIcon, TextFit } from '../Common' +import { LpToken } from '../dummyData' +import { LpEmblems } from '../LpEmblems' +import * as styledEl from '../styled' +import { CoWAmmBannerContext } from '../types' + +interface GlobalContentProps { + isUniV2InferiorWithLowAverageYield: boolean + tokens: LpToken[] + arrowBackgroundRef: RefObject + context: CoWAmmBannerContext +} + +export function GlobalContent({ + context, + isUniV2InferiorWithLowAverageYield, + tokens, + arrowBackgroundRef, +}: GlobalContentProps) { + const { + title, + ctaText, + aprMessage, + comparisonMessage, + onClose, + isMobile, + onCtaClick, + handleCTAMouseLeave, + handleCTAMouseEnter, + } = context + + return ( + + + + + {title} + + + +

    + + {aprMessage} + +

    + + + {comparisonMessage} + + + +
    + + {!isMobile && ( + + + + One-click convert, boost yield + + + + + )} + + + {ctaText} + + + Pool analytics ↗ + + +
    + ) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/LpEmblems/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/LpEmblems/index.tsx new file mode 100644 index 0000000000..2b40e00bfa --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/LpEmblems/index.tsx @@ -0,0 +1,76 @@ +import React from 'react' + +import ICON_ARROW from '@cowprotocol/assets/cow-swap/arrow.svg' +import ICON_CURVE from '@cowprotocol/assets/cow-swap/icon-curve.svg' +import ICON_PANCAKESWAP from '@cowprotocol/assets/cow-swap/icon-pancakeswap.svg' +import ICON_SUSHISWAP from '@cowprotocol/assets/cow-swap/icon-sushi.svg' +import ICON_UNISWAP from '@cowprotocol/assets/cow-swap/icon-uni.svg' +import { USDC } from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { TokenLogo } from '@cowprotocol/tokens' +import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' + +import SVG from 'react-inlinesvg' + +import { LpToken } from '../dummyData' +import * as styledEl from '../styled' + +const lpTokenIcons: Record = { + [LpToken.UniswapV2]: ICON_UNISWAP, + [LpToken.Sushiswap]: ICON_SUSHISWAP, + [LpToken.PancakeSwap]: ICON_PANCAKESWAP, + [LpToken.Curve]: ICON_CURVE, +} + +interface LpEmblemsProps { + isUniV2InferiorWithLowAverageYield: boolean + tokens: LpToken[] +} + +export function LpEmblems({ tokens, isUniV2InferiorWithLowAverageYield }: LpEmblemsProps) { + const totalItems = tokens.length + + const renderEmblemContent = () => ( + <> + + + + + + + + ) + + if (totalItems === 0 || isUniV2InferiorWithLowAverageYield) { + return ( + + + {isUniV2InferiorWithLowAverageYield ? ( + + ) : ( + + )} + + {renderEmblemContent()} + + ) + } + + return ( + + + {tokens.map((token, index) => ( + + + + ))} + + {renderEmblemContent()} + + ) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/PoolInfo/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/PoolInfo/index.tsx new file mode 100644 index 0000000000..5c512e5147 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/PoolInfo/index.tsx @@ -0,0 +1,52 @@ +import React from 'react' + +import { USDC, WBTC } from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { TokenLogo } from '@cowprotocol/tokens' +import { UI } from '@cowprotocol/ui' + +import * as styledEl from '../styled' + +interface PoolInfoProps { + isDarkMode: boolean + isTokenSelectorView: boolean + poolName: string +} + +export function PoolInfo({ poolName, isTokenSelectorView, isDarkMode }: PoolInfoProps) { + return ( + + higher APR available for your {poolName} pool: + +
    + +
    + WBTC-USDC +
    +
    + ) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/TokenSelectorContent/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/TokenSelectorContent/index.tsx new file mode 100644 index 0000000000..5d19ba7895 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/TokenSelectorContent/index.tsx @@ -0,0 +1,76 @@ +import React from 'react' + +import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' + +import { StarIcon, TextFit } from '../Common' +import * as styledEl from '../styled' +import { CoWAmmBannerContext } from '../types' + +interface TokenSelectorContentProps { + isDarkMode: boolean + context: CoWAmmBannerContext +} +export function TokenSelectorContent({ isDarkMode, context }: TokenSelectorContentProps) { + const { + title, + isMobile, + ctaText, + onClose, + onCtaClick, + aprMessage, + comparisonMessage, + handleCTAMouseEnter, + handleCTAMouseLeave, + } = context + + const mainColor = isDarkMode ? UI.COLOR_COWAMM_LIGHT_GREEN : UI.COLOR_COWAMM_DARK_GREEN + + return ( + + + + + + {title} + + + +

    + + {aprMessage} + +

    + + + {comparisonMessage} + + + +
    + + {ctaText} + +
    +
    + ) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/dummyData.ts similarity index 100% rename from apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts rename to apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/dummyData.ts diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx new file mode 100644 index 0000000000..cd31ef4ae3 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx @@ -0,0 +1,102 @@ +import React, { useCallback, useMemo, useRef } from 'react' + +import { upToSmall, useMediaQuery } from 'legacy/hooks/useMediaQuery' + +import { ComparisonMessage } from './ComparisonMessage' +import { dummyData, lpTokenConfig, StateKey } from './dummyData' +import { GlobalContent } from './GlobalContent' +import { TokenSelectorContent } from './TokenSelectorContent' +import { CoWAmmBannerContext } from './types' + +import { useSafeMemoObject } from '../../hooks/useSafeMemo' + +interface CoWAmmBannerContentProps { + id: string + title: string + ctaText: string + isTokenSelectorView: boolean + isDarkMode: boolean + selectedState: StateKey + dummyData: typeof dummyData + lpTokenConfig: typeof lpTokenConfig + onCtaClick: () => void + onClose: () => void +} + +export function CoWAmmBannerContent({ + id, + title, + ctaText, + isTokenSelectorView, + selectedState, + dummyData, + lpTokenConfig, + onCtaClick, + onClose, + isDarkMode, +}: CoWAmmBannerContentProps) { + const isMobile = useMediaQuery(upToSmall) + const arrowBackgroundRef = useRef(null) + + const tokens = lpTokenConfig[selectedState] + const data = dummyData[selectedState] + const isUniV2InferiorWithLowAverageYield = selectedState === 'uniV2InferiorWithLowAverageYield' + + const handleCTAMouseEnter = useCallback(() => { + if (arrowBackgroundRef.current) { + arrowBackgroundRef.current.style.visibility = 'visible' + arrowBackgroundRef.current.style.opacity = '1' + } + }, []) + + const handleCTAMouseLeave = useCallback(() => { + if (arrowBackgroundRef.current) { + arrowBackgroundRef.current.style.visibility = 'hidden' + arrowBackgroundRef.current.style.opacity = '0' + } + }, []) + + const aprMessage = useMemo(() => { + if (selectedState === 'uniV2InferiorWithLowAverageYield' && 'poolsCount' in data) { + return `${data.poolsCount}+` + } + return `+${data.apr.toFixed(1)}%` + }, [selectedState, data]) + + const comparisonMessage = ( + + ) + + const context: CoWAmmBannerContext = useSafeMemoObject({ + title, + ctaText, + aprMessage, + comparisonMessage, + isMobile, + onClose, + onCtaClick, + handleCTAMouseEnter, + handleCTAMouseLeave, + }) + + return ( +
    + {isTokenSelectorView ? ( + + ) : ( + + )} +
    + ) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/styled.ts similarity index 99% rename from apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts rename to apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/styled.ts index 9f506bdfb8..a798b1c3fb 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/styled.ts @@ -1,5 +1,5 @@ import { TokenLogoWrapper } from '@cowprotocol/tokens' -import { Media, UI } from '@cowprotocol/ui' +import { UI, Media } from '@cowprotocol/ui' import { X } from 'react-feather' import styled, { keyframes } from 'styled-components/macro' diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/types.ts b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/types.ts new file mode 100644 index 0000000000..c88756df4f --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/types.ts @@ -0,0 +1,13 @@ +import { ReactNode } from 'react' + +export interface CoWAmmBannerContext { + title: string + ctaText: string + aprMessage: string + comparisonMessage: ReactNode + isMobile: boolean + onClose(): void + onCtaClick(): void + handleCTAMouseEnter(): void + handleCTAMouseLeave(): void +} diff --git a/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx b/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx index 348f86a792..db2462dd59 100644 --- a/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/VirtualList/index.tsx @@ -21,9 +21,17 @@ interface VirtualListProps { getItemView(items: T[], item: VirtualItem): ReactNode loading?: boolean estimateSize?: () => number + children?: ReactNode } -export function VirtualList({ id, items, loading, getItemView, estimateSize = () => 56 }: VirtualListProps) { +export function VirtualList({ + id, + items, + loading, + getItemView, + children, + estimateSize = () => 56, +}: VirtualListProps) { const parentRef = useRef(null) const wrapperRef = useRef(null) const scrollTimeoutRef = useRef() @@ -52,6 +60,7 @@ export function VirtualList({ id, items, loading, getItemView, estimateSize = + {children} {virtualItems.map((item) => { if (loading) { return {threeDivs()} 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 09d586453e..edbd3719bb 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -23,6 +23,7 @@ import { useInjectedWidgetParams } from 'modules/injectedWidget' import { parameterizeTradeRoute, useTradeRouteContext } from 'modules/trade' import { useInitializeUtm } from 'modules/utm' +import { CoWAmmBanner } from 'common/containers/CoWAmmBanner' import { InvalidLocalTimeWarning } from 'common/containers/InvalidLocalTimeWarning' import { useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' import { useMenuItems } from 'common/hooks/useMenuItems' @@ -138,10 +139,7 @@ export function App() { /> )} - {/* CoW AMM banner */} - {/*{!isInjectedWidgetMode && account && !isChainIdUnsupported && (*/} - {/* */} - {/*)}*/} + diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index 8223f60ea3..20d5cd3979 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -1,9 +1,10 @@ -import { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { TokenWithLogo } from '@cowprotocol/common-const' import { VirtualItem } from '@tanstack/react-virtual' +import { CoWAmmBanner } from 'common/containers/CoWAmmBanner' import { VirtualList } from 'common/pure/VirtualList' import { SelectTokenContext } from '../../types' @@ -21,8 +22,6 @@ export function TokensVirtualList(props: TokensVirtualListProps) { const { values: balances } = balancesState const isWalletConnected = !!account - // const isInjectedWidgetMode = isInjectedWidget() - // const isChainIdUnsupported = useIsProviderNetworkUnsupported() const sortedTokens = useMemo(() => { return balances ? allTokens.sort(tokensListSorter(balances)) : allTokens @@ -49,5 +48,9 @@ export function TokensVirtualList(props: TokensVirtualListProps) { [balances, unsupportedTokens, permitCompatibleTokens, selectedToken, onSelectToken, isWalletConnected], ) - return + return ( + + + + ) } From 5580565b91abd15faa3146563206f8caf787fbbf Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 29 Oct 2024 15:06:01 +0500 Subject: [PATCH 069/116] chore: fix up multicall file (#5053) * chore: clean up file * chore: rename file * chore: revert to original file --- libs/multicall/src/multiCall.ts | 44 --------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 libs/multicall/src/multiCall.ts diff --git a/libs/multicall/src/multiCall.ts b/libs/multicall/src/multiCall.ts deleted file mode 100644 index 8aed7aff7b..0000000000 --- a/libs/multicall/src/multiCall.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Multicall3 } from '@cowprotocol/abis' -import type { Web3Provider } from '@ethersproject/providers' - -import { DEFAULT_BATCH_SIZE } from './const' -import { getMulticallContract } from './utils/getMulticallContract' - -export interface MultiCallOptions { - batchSize?: number -} - -/** - * TODO: return results just after batch execution - * TODO: add fallback for failed calls - * TODO: add providers fallback - */ -export async function multiCall( - provider: Web3Provider, - calls: Multicall3.CallStruct[], - options: MultiCallOptions = {} -): Promise { - const { batchSize = DEFAULT_BATCH_SIZE } = options - - const multicall = getMulticallContract(provider) - - const batches = splitIntoBatches(calls, batchSize) - - const requests = batches.map((batch) => { - return multicall.callStatic.tryAggregate(false, batch) - }) - - return (await Promise.all(requests)).flat() -} - -function splitIntoBatches(calls: Multicall3.CallStruct[], batchSize: number): Multicall3.CallStruct[][] { - const results: Multicall3.CallStruct[][] = [] - - for (let i = 0; i < calls.length; i += batchSize) { - const batch = calls.slice(i, i + batchSize) - - results.push(batch) - } - - return results -} From 32c9db9f29bf987ffa9b45c1b981ab28d387e25f Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 29 Oct 2024 18:48:52 +0500 Subject: [PATCH 070/116] style: set react-hooks/exhaustive-deps level to error (#5048) --- .../containers/SettingsTab/index.tsx | 2 +- .../orders/NumbersBreakdown/index.tsx | 101 ------------------ eslint.config.js | 1 + .../src/cms/updaters/SolversInfoUpdater.ts | 2 +- libs/tokens/src/hooks/lists/useAddList.ts | 2 +- libs/ui/src/analytics/useAnalyticsReporter.ts | 10 +- .../web3-react/hooks/useActivateConnector.ts | 4 +- libs/widget-react/src/lib/CowSwapWidget.tsx | 36 ++++--- 8 files changed, 30 insertions(+), 128 deletions(-) delete mode 100644 apps/explorer/src/components/orders/NumbersBreakdown/index.tsx 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 e4493cb2c1..9f1bbfcb07 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -97,7 +97,7 @@ function SettingsTabController({ buttonRef, children }: SettingsTabControllerPro const toggleMenu = useCallback(() => { buttonRef.current?.dispatchEvent(new Event('mousedown', { bubbles: true })) - }, [buttonRef.current]) + }, [buttonRef]) useEffect(() => { if (settingsTabState.open) { diff --git a/apps/explorer/src/components/orders/NumbersBreakdown/index.tsx b/apps/explorer/src/components/orders/NumbersBreakdown/index.tsx deleted file mode 100644 index 004c36f2a5..0000000000 --- a/apps/explorer/src/components/orders/NumbersBreakdown/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useCallback, useEffect } from 'react' - -import { Media } from '@cowprotocol/ui' - -import Spinner from 'components/common/Spinner' -import { Notification } from 'components/Notification' -import useSafeState from 'hooks/useSafeState' -import styled from 'styled-components/macro' - -const ShowMoreButton = styled.button` - font-size: 1.4rem; - border: none; - background: none; - color: ${({ theme }) => theme.textActive1}; - margin: 0; - padding: 0; - - &:hover { - text-decoration: underline; - cursor: pointer; - } -` - -const DetailsWrapper = styled.div` - display: flex; - margin: 1.8rem 0 1rem; - border-radius: 0.6rem; - line-height: 1.6; - width: max-content; - align-items: flex-start; - word-break: break-all; - overflow: auto; - border: 1px solid rgb(151 151 184 / 10%); - padding: 0.6rem; - background: rgb(151 151 184 / 10%); - - ${Media.upToSmall()} { - width: 100%; - } - - table { - width: 100%; - border-collapse: collapse; - } -` - -type BreakdownProps = { - fetchData: () => Promise - renderContent: (data: any) => React.ReactNode - showExpanded?: boolean -} - -export const NumbersBreakdown = ({ - fetchData, - renderContent, - showExpanded = false, -}: BreakdownProps): React.ReactNode => { - const [loading, setLoading] = useSafeState(false) - const [error, setError] = useSafeState(false) - const [detailedData, setDetailedData] = useSafeState(undefined) - const [showDetails, setShowDetails] = useSafeState(showExpanded) - - const handleFetchData = useCallback(async (): Promise => { - setLoading(true) - try { - const result = await fetchData() - setDetailedData(result) - } catch { - setError(true) - } finally { - setLoading(false) - } - }, [fetchData, setLoading, setError]) - - useEffect(() => { - if (showExpanded) { - handleFetchData().catch(console.error) - } - }, [showExpanded, handleFetchData]) - - const handleToggle = async (): Promise => { - if (!showDetails) { - await handleFetchData() - } - setShowDetails(!showDetails) - } - - const renderData = (): React.ReactNode | null => { - if (loading) return - if (error) - return - return detailedData ? renderContent(detailedData) : null - } - - return ( - <> - {showDetails ? '[-] Show less' : '[+] Show more'} - {showDetails && {renderData()}} - - ) -} diff --git a/eslint.config.js b/eslint.config.js index acefd1e200..b91e25a197 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,7 @@ module.exports = [ }, rules: { ...reactHooks.configs.recommended.rules, + 'react-hooks/exhaustive-deps': 'error', }, }, { diff --git a/libs/core/src/cms/updaters/SolversInfoUpdater.ts b/libs/core/src/cms/updaters/SolversInfoUpdater.ts index 85d5d9b62e..e6fccd1df8 100644 --- a/libs/core/src/cms/updaters/SolversInfoUpdater.ts +++ b/libs/core/src/cms/updaters/SolversInfoUpdater.ts @@ -14,7 +14,7 @@ export function SolversInfoUpdater() { const solversInfo = mapCmsSolversInfoToSolversInfo(cmsSolversInfo) solversInfo && setSolversInfo(solversInfo) - }, [cmsSolversInfo]) + }, [cmsSolversInfo, setSolversInfo]) return null } diff --git a/libs/tokens/src/hooks/lists/useAddList.ts b/libs/tokens/src/hooks/lists/useAddList.ts index e16df0ab77..228d337918 100644 --- a/libs/tokens/src/hooks/lists/useAddList.ts +++ b/libs/tokens/src/hooks/lists/useAddList.ts @@ -43,6 +43,6 @@ export function useAddList(onAddList: (source: string) => void) { addList(state) onAddList(state.source) }, - [addList, listsStatesByChain, chainId] + [addList, listsStatesByChain, chainId, onAddList], ) } diff --git a/libs/ui/src/analytics/useAnalyticsReporter.ts b/libs/ui/src/analytics/useAnalyticsReporter.ts index 38486b8d31..901c8a40e2 100644 --- a/libs/ui/src/analytics/useAnalyticsReporter.ts +++ b/libs/ui/src/analytics/useAnalyticsReporter.ts @@ -44,7 +44,7 @@ export function useAnalyticsReporter(props: UseAnalyticsReporterProps) { if (typeof window !== 'undefined') { cowAnalytics.setContext( AnalyticsContext.customBrowserType, - !isMobile ? 'desktop' : 'web3' in window || 'ethereum' in window ? 'mobileWeb3' : 'mobileRegular' + !isMobile ? 'desktop' : 'web3' in window || 'ethereum' in window ? 'mobileWeb3' : 'mobileRegular', ) } @@ -54,7 +54,7 @@ export function useAnalyticsReporter(props: UseAnalyticsReporterProps) { const hit = Boolean((window as any).__isDocumentCached) serviceWorkerLoad(cowAnalytics, installed, hit) } - }, [webVitalsAnalytics]) + }, [webVitalsAnalytics, cowAnalytics]) // Set analytics context: chainId useEffect(() => { @@ -63,7 +63,7 @@ export function useAnalyticsReporter(props: UseAnalyticsReporterProps) { } cowAnalytics.setContext(AnalyticsContext.chainId, chainId.toString()) - }, [chainId]) + }, [chainId, cowAnalytics]) // Set analytics context: user account and wallet name useEffect(() => { @@ -74,11 +74,11 @@ export function useAnalyticsReporter(props: UseAnalyticsReporterProps) { if (!prevAccount && account && pixelAnalytics) { pixelAnalytics.sendAllPixels(PixelEvent.CONNECT_WALLET) } - }, [account, walletName, prevAccount, pixelAnalytics]) + }, [account, walletName, prevAccount, pixelAnalytics, cowAnalytics]) useEffect(() => { cowAnalytics.sendPageView(`${pathname}${search}`) - }, [pathname, search]) + }, [pathname, search, cowAnalytics]) // Handle initiate pixel tracking useEffect(() => { diff --git a/libs/wallet/src/web3-react/hooks/useActivateConnector.ts b/libs/wallet/src/web3-react/hooks/useActivateConnector.ts index b04911a106..9a3fb0b12e 100644 --- a/libs/wallet/src/web3-react/hooks/useActivateConnector.ts +++ b/libs/wallet/src/web3-react/hooks/useActivateConnector.ts @@ -50,7 +50,7 @@ export function useActivateConnector({ onActivationError(error) } }, - [chainId, pendingConnector, skipNetworkChanging, afterActivation, beforeActivation, onActivationError] + [chainId, skipNetworkChanging, afterActivation, beforeActivation, onActivationError], ) return useMemo( @@ -62,6 +62,6 @@ export function useActivateConnector({ } }, }), - [tryActivation, pendingConnector] + [tryActivation, pendingConnector], ) } diff --git a/libs/widget-react/src/lib/CowSwapWidget.tsx b/libs/widget-react/src/lib/CowSwapWidget.tsx index 02f9656dae..ff4ee25c29 100644 --- a/libs/widget-react/src/lib/CowSwapWidget.tsx +++ b/libs/widget-react/src/lib/CowSwapWidget.tsx @@ -20,21 +20,18 @@ export function CowSwapWidget(props: CowSwapWidgetProps) { const widgetHandlerRef = useRef(null) // Error handling - const tryOrHandleError = useCallback( - (action: string, actionThatMightFail: Command) => { - try { - console.log(`[WIDGET] ${action}`) - actionThatMightFail() - } catch (error) { - const errorMessage = `Error ${action.toLowerCase()}` - console.error(`[WIDGET] ${errorMessage}`, error) - setError({ message: errorMessage, error }) - } - }, - [setError], - ) + const tryOrHandleError = useCallback((action: string, actionThatMightFail: Command) => { + try { + console.log(`[WIDGET] ${action}`) + actionThatMightFail() + } catch (error) { + const errorMessage = `Error ${action.toLowerCase()}` + console.error(`[WIDGET] ${errorMessage}`, error) + setError({ message: errorMessage, error }) + } + }, []) - // Cleanup widget + // Cleanup widget on mount useEffect(() => { return () => { // Cleanup references @@ -49,6 +46,7 @@ export function CowSwapWidget(props: CowSwapWidgetProps) { widgetHandlerRef.current = null } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Create/Update the widget if the parameters change @@ -69,7 +67,9 @@ export function CowSwapWidget(props: CowSwapWidgetProps) { } else { tryOrHandleError('Updating the widget', () => handler.updateParams(params)) } - }, [params]) + // Trigger only on params changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [params, tryOrHandleError]) // Update widget provider (if it changes) useEffect(() => { @@ -98,7 +98,9 @@ export function CowSwapWidget(props: CowSwapWidgetProps) { widgetHandlerRef.current = createCowSwapWidget(container, { params, provider: providerRef.current, listeners }) }) } - }, [provider]) + // Trigger only on provider changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [provider, tryOrHandleError]) // Update widget listeners (if they change) useEffect(() => { @@ -106,7 +108,7 @@ export function CowSwapWidget(props: CowSwapWidgetProps) { const handler = widgetHandlerRef.current tryOrHandleError('Updating the listeners', () => handler.updateListeners(listeners)) - }, [listeners]) + }, [listeners, tryOrHandleError]) // Handle errors if (error) { From fc1bf57ef36b1cb5cb166a4f826f51affd29fda4 Mon Sep 17 00:00:00 2001 From: Leandro Date: Tue, 29 Oct 2024 18:06:16 +0000 Subject: [PATCH 071/116] chore: fix linter errors (#5057) --- .../containers/HookRegistryList/index.tsx | 6 +++--- .../tokensList/pure/LpTokenLists/index.tsx | 15 +++++++-------- .../yield/updaters/PoolsInfoUpdater/index.tsx | 2 +- .../src/updaters/BalancesCacheUpdater.tsx | 4 ++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx index 38378027bc..2741c954a0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx @@ -94,8 +94,8 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit, walletType setDappDetails(null) }, [isAllHooksTab]) - const handleAddCustomHook = () => setIsAllHooksTab(false) - const handleClearSearch = () => setSearchQuery('') + const handleAddCustomHook = useCallback(() => setIsAllHooksTab(false), [setIsAllHooksTab]) + const handleClearSearch = useCallback(() => setSearchQuery(''), [setSearchQuery]) const emptyListMessage = useMemo( () => @@ -154,7 +154,7 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit, walletType )} ), - [isAllHooksTab, searchQuery, sortedFilteredDapps, handleAddCustomHook, handleClearSearch], + [isAllHooksTab, searchQuery, sortedFilteredDapps, handleAddCustomHook, handleClearSearch, emptyListMessage, removeCustomHookDapp, walletType], ) return ( diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx index acf4499d80..2fc067843c 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx @@ -16,20 +16,20 @@ import { VirtualList } from 'common/pure/VirtualList' import { CreatePoolLink, + EmptyList, ListHeader, ListItem, + LpTokenBalance, LpTokenInfo, + LpTokenTooltip, LpTokenWrapper, LpTokenYieldPercentage, - LpTokenBalance, - LpTokenTooltip, - NoPoolWrapper, - Wrapper, - EmptyList, MobileCard, - MobileCardRow, MobileCardLabel, + MobileCardRow, MobileCardValue, + NoPoolWrapper, + Wrapper, } from './styled' const LoadingElement = ( @@ -56,7 +56,6 @@ interface LpTokenListsProps { } export function LpTokenLists({ - account, onSelectToken, openPoolPage, lpTokens, @@ -127,7 +126,7 @@ export function LpTokenLists({ ) }, - [balances, onSelectToken, poolsInfo, openPoolPage, account, isMobile], + [balances, onSelectToken, poolsInfo, openPoolPage, isMobile], ) return ( diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx index 5b9561e3b3..b7bb59f0a6 100644 --- a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx @@ -46,7 +46,7 @@ export function PoolsInfoUpdater() { if (tokensToUpdate.length > 0 || isYield) { fetchPoolsInfo(isYield ? null : tokensToUpdate).then(upsertPoolsInfo) } - }, [isYield, tokensKey]) + }, [isYield, tokensKey, tokensToUpdate, upsertPoolsInfo]) return null } diff --git a/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx index 0cd33ae770..724b5f6c9c 100644 --- a/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx +++ b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx @@ -50,7 +50,7 @@ export function BalancesCacheUpdater({ chainId }: { chainId: SupportedChainId }) }, } }) - }, [chainId, balances.values]) + }, [chainId, balances.values, setBalancesCache, setBalances]) // Restore balances from cache once useEffect(() => { @@ -83,7 +83,7 @@ export function BalancesCacheUpdater({ chainId }: { chainId: SupportedChainId }) }) return - }, [balancesCache, chainId]) + }, [balancesCache, chainId, setBalances]) return null } From 0fbb9b585c4beb0978309c8ebda7e8aa1f8bf57c Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 30 Oct 2024 13:38:33 +0000 Subject: [PATCH 072/116] fix: remove isNotificationsFeedEnabled (#5054) --- .../src/legacy/components/Header/AccountElement/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx b/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx index a60eb2862a..91758f84ac 100644 --- a/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx @@ -2,7 +2,6 @@ import { useState } from 'react' import { useNativeCurrencyAmount } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' -import { useFeatureFlags } from '@cowprotocol/common-hooks' import { TokenAmount } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' @@ -35,7 +34,7 @@ export function AccountElement({ className, standaloneMode, pendingActivities }: const toggleAccountModal = useToggleAccountModal() const nativeTokenSymbol = NATIVE_CURRENCIES[chainId].symbol const isUpToLarge = useMediaQuery(upToLarge) - const { isNotificationsFeedEnabled } = useFeatureFlags() + const unreadNotifications = useUnreadNotifications() const unreadNotificationsCount = Object.keys(unreadNotifications).length @@ -51,7 +50,7 @@ export function AccountElement({ className, standaloneMode, pendingActivities }: )} account && toggleAccountModal()} /> - {account && isNotificationsFeedEnabled && ( + {account && ( { From ed176c3ab95fe51065a905e05ca184f3abf7e282 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 31 Oct 2024 11:53:14 +0000 Subject: [PATCH 073/116] feat(protocol-fees): arb1 protocol fee (#5055) * feat: feature flags can also be numbers * feat: add arb1 stable coins * feat: apply a/b testing fee to arb1 * chore: remove exports * fix: fix getDoNotQueryStatusEndpoint only accepting a boolean * feat: return fee even if not connected when percentage set to 100% * feat: add token logos for arb1 stablecoins * chore: added todo to remove gnosis chain feature flag * refactor: use a set for quicker lookup instead of array * chore: fix set usage --------- Co-authored-by: Alexandr Kazachenko --- .../useOrderProgressBarV2Props.ts | 24 ++-- .../src/common/state/featureFlagsState.ts | 2 +- .../modules/volumeFee/state/cowswapFeeAtom.ts | 42 +++++-- libs/common-const/src/tokens.ts | 109 +++++++++++++----- 4 files changed, 129 insertions(+), 48 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/orderProgressBarV2/useOrderProgressBarV2Props.ts b/apps/cowswap-frontend/src/common/hooks/orderProgressBarV2/useOrderProgressBarV2Props.ts index fb51edb913..ff931a07b9 100644 --- a/apps/cowswap-frontend/src/common/hooks/orderProgressBarV2/useOrderProgressBarV2Props.ts +++ b/apps/cowswap-frontend/src/common/hooks/orderProgressBarV2/useOrderProgressBarV2Props.ts @@ -61,7 +61,7 @@ export function useOrderProgressBarV2Props(chainId: SupportedChainId, order: Ord const showCancellationModal = useMemo( // Sort of duplicate cancellation logic since ethflow on creating state don't have progress bar props () => progressBarV2Props?.showCancellationModal || (order && getCancellation ? getCancellation(order) : null), - [progressBarV2Props?.showCancellationModal, order, getCancellation] + [progressBarV2Props?.showCancellationModal, order, getCancellation], ) const surplusData = useGetSurplusData(order) const receiverEnsName = useENS(order?.receiver).name || undefined @@ -87,7 +87,7 @@ export function useOrderProgressBarV2Props(chainId: SupportedChainId, order: Ord } function useOrderBaseProgressBarV2Props( - params: UseOrderProgressBarPropsParams + params: UseOrderProgressBarPropsParams, ): UseOrderProgressBarV2Result | undefined { const { activityDerivedState, chainId } = params @@ -129,7 +129,7 @@ function useOrderBaseProgressBarV2Props( const solversInfo = useSolversInfo(chainId) const totalSolvers = Object.keys(solversInfo).length - const doNotQuery = getDoNotQueryStatusEndpoint(order, apiSolverCompetition, disableProgressBar) + const doNotQuery = getDoNotQueryStatusEndpoint(order, apiSolverCompetition, !!disableProgressBar) // Local updaters of the respective atom useBackendApiStatusUpdater(chainId, orderId, doNotQuery) @@ -145,7 +145,7 @@ function useOrderBaseProgressBarV2Props( backendApiStatus, previousBackendApiStatus, lastTimeChangedSteps, - previousStepName + previousStepName, ) useCancellingOrderUpdater(orderId, isCancelling) useCountdownStartUpdater(orderId, countdown, backendApiStatus) @@ -156,7 +156,7 @@ function useOrderBaseProgressBarV2Props( ?.map((entry) => mergeSolverData(entry, solversInfo)) // Reverse it since backend returns the solutions ranked ascending. Winner is the last one. .reverse(), - [apiSolverCompetition, solversInfo] + [apiSolverCompetition, solversInfo], ) return useMemo(() => { @@ -184,7 +184,7 @@ function useOrderBaseProgressBarV2Props( function getDoNotQueryStatusEndpoint( order: Order | undefined, apiSolverCompetition: CompetitionOrderStatus['value'] | undefined, - disableProgressBar: boolean + disableProgressBar: boolean, ) { return ( !!( @@ -224,7 +224,7 @@ function useSetExecutingOrderProgressBarStepNameCallback() { function useCountdownStartUpdater( orderId: string, countdown: OrderProgressBarState['countdown'], - backendApiStatus: OrderProgressBarState['backendApiStatus'] + backendApiStatus: OrderProgressBarState['backendApiStatus'], ) { const setCountdown = useSetExecutingOrderCountdownCallback() @@ -259,7 +259,7 @@ function useProgressBarStepNameUpdater( backendApiStatus: OrderProgressBarState['backendApiStatus'], previousBackendApiStatus: OrderProgressBarState['previousBackendApiStatus'], lastTimeChangedSteps: OrderProgressBarState['lastTimeChangedSteps'], - previousStepName: OrderProgressBarState['previousStepName'] + previousStepName: OrderProgressBarState['previousStepName'], ) { const setProgressBarStepName = useSetExecutingOrderProgressBarStepNameCallback() @@ -273,7 +273,7 @@ function useProgressBarStepNameUpdater( countdown, backendApiStatus, previousBackendApiStatus, - previousStepName + previousStepName, ) // Update state with new step name @@ -321,7 +321,7 @@ function getProgressBarStepName( countdown: OrderProgressBarState['countdown'], backendApiStatus: OrderProgressBarState['backendApiStatus'], previousBackendApiStatus: OrderProgressBarState['previousBackendApiStatus'], - previousStepName: OrderProgressBarState['previousStepName'] + previousStepName: OrderProgressBarState['previousStepName'], ): OrderProgressBarStepName { if (isExpired) { return 'expired' @@ -391,7 +391,7 @@ function usePendingOrderStatus(chainId: SupportedChainId, orderId: string, doNot return useSWR( chainId && orderId && !doNotQuery ? ['getOrderCompetitionStatus', chainId, orderId] : null, async ([, _chainId, _orderId]) => getOrderCompetitionStatus(_chainId, _orderId), - doNotQuery ? SWR_NO_REFRESH_OPTIONS : POOLING_SWR_OPTIONS + doNotQuery ? SWR_NO_REFRESH_OPTIONS : POOLING_SWR_OPTIONS, ).data } @@ -404,7 +404,7 @@ function usePendingOrderStatus(chainId: SupportedChainId, orderId: string, doNot */ function mergeSolverData( solverCompetition: ApiSolverCompetition, - solversInfo: Record + solversInfo: Record, ): SolverCompetition { // Backend has the prefix `-solve` on some solvers. We should discard that for now. // In the future this prefix will be removed. diff --git a/apps/cowswap-frontend/src/common/state/featureFlagsState.ts b/apps/cowswap-frontend/src/common/state/featureFlagsState.ts index ac8f3a531d..8dadd38578 100644 --- a/apps/cowswap-frontend/src/common/state/featureFlagsState.ts +++ b/apps/cowswap-frontend/src/common/state/featureFlagsState.ts @@ -1,3 +1,3 @@ import { atom } from 'jotai' -export const featureFlagsAtom = atom>({}) +export const featureFlagsAtom = atom>({}) diff --git a/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts b/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts index 55aeb965d8..e8857a7cb2 100644 --- a/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts +++ b/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts @@ -1,19 +1,23 @@ import { atom } from 'jotai' -import { GNOSIS_CHAIN_STABLECOINS } from '@cowprotocol/common-const' +import { STABLECOINS } from '@cowprotocol/common-const' import { getCurrencyAddress, isInjectedWidget } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { walletInfoAtom } from '@cowprotocol/wallet' import { derivedTradeStateAtom } from 'modules/trade' +import { featureFlagsAtom } from 'common/state/featureFlagsState' + import { VolumeFee } from '../types' const COWSWAP_VOLUME_FEES: Record = { [SupportedChainId.MAINNET]: null, [SupportedChainId.SEPOLIA]: null, - [SupportedChainId.ARBITRUM_ONE]: null, - // Only Gnosis chain + [SupportedChainId.ARBITRUM_ONE]: { + bps: 10, // 0.1% + recipient: '0x451100Ffc88884bde4ce87adC8bB6c7Df7fACccd', // Arb1 Protocol fee safe + }, [SupportedChainId.GNOSIS_CHAIN]: { bps: 10, // 0.1% recipient: '0x6b3214fD11dc91De14718DeE98Ef59bCbFcfB432', // Gnosis Chain Protocol fee safe @@ -21,21 +25,45 @@ const COWSWAP_VOLUME_FEES: Record = { } export const cowSwapFeeAtom = atom((get) => { - const { chainId } = get(walletInfoAtom) + const { chainId, account } = get(walletInfoAtom) const tradeState = get(derivedTradeStateAtom) + const featureFlags = get(featureFlagsAtom) const { inputCurrency, outputCurrency } = tradeState || {} - // No widget mode + // Don't use it in the widget if (isInjectedWidget()) return null + // Don't user it when the currencies are not set if (!inputCurrency || !outputCurrency) return null - const isInputTokenStable = GNOSIS_CHAIN_STABLECOINS.includes(getCurrencyAddress(inputCurrency).toLowerCase()) - const isOutputTokenStable = GNOSIS_CHAIN_STABLECOINS.includes(getCurrencyAddress(outputCurrency).toLowerCase()) + // TODO: remove this feature flag in another PR + // Don't use it when isCowSwapFeeEnabled is not enabled + if (!featureFlags.isCowSwapFeeEnabled) return null + + // Don't use it when on arb1 and shouldn't apply fee based on percentage + if (chainId === SupportedChainId.ARBITRUM_ONE && !shouldApplyFee(account, featureFlags.arb1CowSwapFeePercentage)) + return null + + const isInputTokenStable = STABLECOINS[chainId].has(getCurrencyAddress(inputCurrency).toLowerCase()) + const isOutputTokenStable = STABLECOINS[chainId].has(getCurrencyAddress(outputCurrency).toLowerCase()) // No stable-stable trades if (isInputTokenStable && isOutputTokenStable) return null return COWSWAP_VOLUME_FEES[chainId] }) + +function shouldApplyFee(account: string | undefined, percentage: number | boolean | undefined): boolean { + // Early exit for 100%, meaning should be enabled for everyone + if (percentage === 100) { + return true + } + + // Falsy conditions + if (typeof percentage !== 'number' || !account || percentage < 0 || percentage > 100) { + return false + } + + return BigInt(account) % 100n < percentage +} diff --git a/libs/common-const/src/tokens.ts b/libs/common-const/src/tokens.ts index 91be310384..3be829756a 100644 --- a/libs/common-const/src/tokens.ts +++ b/libs/common-const/src/tokens.ts @@ -12,7 +12,7 @@ export const USDT = new TokenWithLogo( '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', - 'Tether USD' + 'Tether USD', ) export const WBTC = new TokenWithLogo( cowprotocolTokenLogoUrl('0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', SupportedChainId.MAINNET), @@ -20,7 +20,7 @@ export const WBTC = new TokenWithLogo( '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', - 'Wrapped BTC' + 'Wrapped BTC', ) export const USDC_MAINNET = new TokenWithLogo( @@ -29,7 +29,7 @@ export const USDC_MAINNET = new TokenWithLogo( '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', - 'USD Coin' + 'USD Coin', ) export const DAI = new TokenWithLogo( @@ -38,7 +38,7 @@ export const DAI = new TokenWithLogo( '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', - 'Dai Stablecoin' + 'Dai Stablecoin', ) const GNO_MAINNET = new TokenWithLogo( @@ -47,7 +47,7 @@ const GNO_MAINNET = new TokenWithLogo( '0x6810e776880c02933d47db1b9fc05908e5386b96', 18, 'GNO', - 'Gnosis' + 'Gnosis', ) // Gnosis chain @@ -59,7 +59,7 @@ export const USDT_GNOSIS_CHAIN = new TokenWithLogo( '0x4ECaBa5870353805a9F068101A40E0f32ed605C6', 6, 'USDT', - 'Tether USD' + 'Tether USD', ) export const USDC_GNOSIS_CHAIN = new TokenWithLogo( USDC_MAINNET.logoURI, @@ -67,7 +67,7 @@ export const USDC_GNOSIS_CHAIN = new TokenWithLogo( '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', 6, 'USDC', - 'USD Coin (old)' + 'USD Coin (old)', ) export const USDCe_GNOSIS_CHAIN = new TokenWithLogo( USDC_MAINNET.logoURI, @@ -75,7 +75,7 @@ export const USDCe_GNOSIS_CHAIN = new TokenWithLogo( '0x2a22f9c3b484c3629090feed35f17ff8f88f76f0', 6, 'USDC.e', - 'USD Coin' + 'USD Coin', ) export const WBTC_GNOSIS_CHAIN = new TokenWithLogo( WBTC.logoURI, @@ -83,7 +83,7 @@ export const WBTC_GNOSIS_CHAIN = new TokenWithLogo( '0x8e5bbbb09ed1ebde8674cda39a0c169401db4252', 8, 'WBTC', - 'Wrapped BTC' + 'Wrapped BTC', ) export const WETH_GNOSIS_CHAIN = new TokenWithLogo( WETH_MAINNET.logoURI, @@ -91,7 +91,7 @@ export const WETH_GNOSIS_CHAIN = new TokenWithLogo( '0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1', 18, 'WETH', - 'Wrapped Ether on Gnosis Chain' + 'Wrapped Ether on Gnosis Chain', ) export const GNO_GNOSIS_CHAIN = new TokenWithLogo( GNO_MAINNET.logoURI, @@ -99,7 +99,7 @@ export const GNO_GNOSIS_CHAIN = new TokenWithLogo( '0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb', 18, 'GNO', - 'Gnosis Token' + 'Gnosis Token', ) export const EURE_GNOSIS_CHAIN = new TokenWithLogo( @@ -108,7 +108,7 @@ export const EURE_GNOSIS_CHAIN = new TokenWithLogo( '0xcb444e90d8198415266c6a2724b7900fb12fc56e', 18, 'EURe', - 'Monerium EUR emoney' + 'Monerium EUR emoney', ) // Arbitrum @@ -119,7 +119,7 @@ export const USDT_ARBITRUM_ONE = new TokenWithLogo( '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', 6, 'USDT', - 'Tether USD' + 'Tether USD', ) export const WBTC_ARBITRUM_ONE = new TokenWithLogo( WBTC.logoURI, @@ -127,7 +127,7 @@ export const WBTC_ARBITRUM_ONE = new TokenWithLogo( '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f', 8, 'WBTC', - 'Wrapped BTC' + 'Wrapped BTC', ) export const USDC_ARBITRUM_ONE = new TokenWithLogo( @@ -136,7 +136,7 @@ export const USDC_ARBITRUM_ONE = new TokenWithLogo( '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', 6, 'USDC', - 'USD Coin' + 'USD Coin', ) export const DAI_ARBITRUM_ONE = new TokenWithLogo( @@ -145,7 +145,7 @@ export const DAI_ARBITRUM_ONE = new TokenWithLogo( '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', 18, 'DAI', - 'Dai Stablecoin' + 'Dai Stablecoin', ) export const ARB_ARBITRUM_ONE = new TokenWithLogo( @@ -154,7 +154,7 @@ export const ARB_ARBITRUM_ONE = new TokenWithLogo( '0x912ce59144191c1204e64559fe8253a0e49e6548', 18, 'ARB', - 'Arbitrum' + 'Arbitrum', ) export const GNO_ARBITRUM_ONE = new TokenWithLogo( @@ -163,7 +163,43 @@ export const GNO_ARBITRUM_ONE = new TokenWithLogo( '0xa0b862F60edEf4452F25B4160F177db44DeB6Cf1', 18, 'GNO', - 'Gnosis Token' + 'Gnosis Token', +) + +const USDE_ARBITRUM_ONE = new TokenWithLogo( + cowprotocolTokenLogoUrl('0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34', SupportedChainId.ARBITRUM_ONE), + SupportedChainId.ARBITRUM_ONE, + '0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34', + 18, + 'USDe', + 'USDe', +) + +const USDM_ARBITRUM_ONE = new TokenWithLogo( + cowprotocolTokenLogoUrl('0x59d9356e565ab3a36dd77763fc0d87feaf85508c', SupportedChainId.ARBITRUM_ONE), + SupportedChainId.ARBITRUM_ONE, + '0x59D9356E565Ab3A36dD77763Fc0d87fEaf85508C', + 18, + 'USDM', + 'Mountain Protocol USD', +) + +const FRAX_ARBITRUM_ONE = new TokenWithLogo( + cowprotocolTokenLogoUrl('0x17fc002b466eec40dae837fc4be5c67993ddbd6f', SupportedChainId.ARBITRUM_ONE), + SupportedChainId.ARBITRUM_ONE, + '0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F', + 18, + 'FRAX', + 'Frax', +) + +const MIM_ARBITRUM_ONE = new TokenWithLogo( + cowprotocolTokenLogoUrl('0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3', SupportedChainId.ARBITRUM_ONE), + SupportedChainId.ARBITRUM_ONE, + '0xFEa7a6a0B346362BF88A9e4A88416B77a57D6c2A', + 18, + 'MIM', + 'Magic Internet Money', ) // Sepolia @@ -174,7 +210,7 @@ const GNO_SEPOLIA = new TokenWithLogo( '0xd3f3d46FeBCD4CdAa2B83799b7A5CdcB69d135De', 18, 'GNO', - 'GNO (test)' + 'GNO (test)', ) // Sepolia @@ -184,7 +220,7 @@ export const USDC_SEPOLIA = new TokenWithLogo( '0xbe72E441BF55620febc26715db68d3494213D8Cb', 18, 'USDC', - 'USDC (test)' + 'USDC (test)', ) export const USDC: Record = { @@ -212,7 +248,7 @@ const V_COW_TOKEN_MAINNET = new TokenWithLogo( V_COW_CONTRACT_ADDRESS[SupportedChainId.MAINNET] || '', 18, 'vCOW', - 'CoW Protocol Virtual Token' + 'CoW Protocol Virtual Token', ) const V_COW_TOKEN_XDAI = new TokenWithLogo( @@ -221,7 +257,7 @@ const V_COW_TOKEN_XDAI = new TokenWithLogo( V_COW_CONTRACT_ADDRESS[SupportedChainId.GNOSIS_CHAIN] || '', 18, 'vCOW', - 'CoW Protocol Virtual Token' + 'CoW Protocol Virtual Token', ) const V_COW_TOKEN_SEPOLIA = new TokenWithLogo( @@ -230,7 +266,7 @@ const V_COW_TOKEN_SEPOLIA = new TokenWithLogo( V_COW_CONTRACT_ADDRESS[SupportedChainId.SEPOLIA] || '', 18, 'vCOW', - 'CoW Protocol Virtual Token' + 'CoW Protocol Virtual Token', ) // TODO: V_COW not present in all chains, make sure code using it can handle that @@ -250,7 +286,7 @@ const COW_TOKEN_MAINNET = new TokenWithLogo( COW_CONTRACT_ADDRESS[SupportedChainId.MAINNET] || '', 18, 'COW', - 'CoW Protocol Token' + 'CoW Protocol Token', ) const COW_TOKEN_XDAI = new TokenWithLogo( @@ -259,7 +295,7 @@ const COW_TOKEN_XDAI = new TokenWithLogo( COW_CONTRACT_ADDRESS[SupportedChainId.GNOSIS_CHAIN] || '', 18, 'COW', - 'CoW Protocol Token' + 'CoW Protocol Token', ) export const COW_TOKEN_ARBITRUM = new TokenWithLogo( @@ -268,7 +304,7 @@ export const COW_TOKEN_ARBITRUM = new TokenWithLogo( COW_CONTRACT_ADDRESS[SupportedChainId.ARBITRUM_ONE] || '', 18, 'COW', - 'CoW Protocol Token' + 'CoW Protocol Token', ) const COW_TOKEN_SEPOLIA = new TokenWithLogo( @@ -277,7 +313,7 @@ const COW_TOKEN_SEPOLIA = new TokenWithLogo( COW_CONTRACT_ADDRESS[SupportedChainId.SEPOLIA] || '', 18, 'COW', - 'CoW Protocol Token' + 'CoW Protocol Token', ) export const COW: Record = { @@ -299,7 +335,7 @@ const GBPE_GNOSIS_CHAIN_ADDRESS = '0x5cb9073902f2035222b9749f8fb0c9bfe5527108' // NOTE: whenever this list is updated, make sure to update the docs section regarding the volume fees // https://github.com/cowprotocol/docs/blob/main/docs/governance/fees/fees.md?plain=1#L40 -export const GNOSIS_CHAIN_STABLECOINS = [ +const GNOSIS_CHAIN_STABLECOINS = [ SDAI_GNOSIS_CHAIN_ADDRESS, NATIVE_CURRENCIES[SupportedChainId.GNOSIS_CHAIN].address, //xDAI WRAPPED_NATIVE_CURRENCIES[SupportedChainId.GNOSIS_CHAIN].address, //wxDAI @@ -310,6 +346,23 @@ export const GNOSIS_CHAIN_STABLECOINS = [ USDT_GNOSIS_CHAIN.address, ].map((t) => t.toLowerCase()) +const ARBITRUM_ONE_STABLECOINS = [ + USDC_ARBITRUM_ONE.address, + DAI_ARBITRUM_ONE.address, + USDT_ARBITRUM_ONE.address, + USDE_ARBITRUM_ONE.address, + USDM_ARBITRUM_ONE.address, + FRAX_ARBITRUM_ONE.address, + MIM_ARBITRUM_ONE.address, +].map((t) => t.toLowerCase()) + +export const STABLECOINS: Record> = { + [SupportedChainId.MAINNET]: new Set(), + [SupportedChainId.GNOSIS_CHAIN]: new Set(GNOSIS_CHAIN_STABLECOINS), + [SupportedChainId.ARBITRUM_ONE]: new Set(ARBITRUM_ONE_STABLECOINS), + [SupportedChainId.SEPOLIA]: new Set(), +} + /** * Addresses related to COW vesting for Locked GNO * These are used in src/custom/pages/Account/LockedGnoVesting hooks and index files From 725239531a458939154db0de29a6fb4bbf5ffe74 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 31 Oct 2024 16:17:14 +0000 Subject: [PATCH 074/116] chore: remove isCowSwapFeeEnabled feature flag (#5059) --- .../src/modules/volumeFee/state/cowswapFeeAtom.ts | 4 ---- .../src/modules/volumeFee/state/volumeFeeAtom.ts | 10 ++-------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts b/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts index e8857a7cb2..904fbd982e 100644 --- a/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts +++ b/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts @@ -37,10 +37,6 @@ export const cowSwapFeeAtom = atom((get) => { // Don't user it when the currencies are not set if (!inputCurrency || !outputCurrency) return null - // TODO: remove this feature flag in another PR - // Don't use it when isCowSwapFeeEnabled is not enabled - if (!featureFlags.isCowSwapFeeEnabled) return null - // Don't use it when on arb1 and shouldn't apply fee based on percentage if (chainId === SupportedChainId.ARBITRUM_ONE && !shouldApplyFee(account, featureFlags.arb1CowSwapFeePercentage)) return null diff --git a/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts b/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts index 60b30aa4e0..171720a3ac 100644 --- a/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts +++ b/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts @@ -7,22 +7,16 @@ import { injectedWidgetPartnerFeeAtom } from 'modules/injectedWidget' import { tradeTypeAtom } from 'modules/trade' import { TradeType } from 'modules/trade/types/TradeType' -import { featureFlagsAtom } from 'common/state/featureFlagsState' - import { cowSwapFeeAtom } from './cowswapFeeAtom' import { VolumeFee } from '../types' export const volumeFeeAtom = atom((get) => { - const featureFlags = get(featureFlagsAtom) const cowSwapFee = get(cowSwapFeeAtom) const widgetPartnerFee = get(widgetPartnerFeeAtom) - if (featureFlags.isCowSwapFeeEnabled) { - return cowSwapFee || widgetPartnerFee - } - - return widgetPartnerFee + // CoW Swap Fee won't be enabled when in Widget mode, thus it takes precedence here + return cowSwapFee || widgetPartnerFee }) const widgetPartnerFeeAtom = atom((get) => { From 1a517a3f21b94c10b8e59e68bc49a569c1be904b Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 1 Nov 2024 14:23:08 +0500 Subject: [PATCH 075/116] feat(yield): display cow amm banner conditionally (#5035) * feat(yield): setup yield widget * fix(yield): display correct output amount * feat(trade-quote): support fast quote requests * feat(yield): display trade buttons * feat(yield): display confirm details * feat(yield): scope context to trade * feat(yield): do trade after confirmation * feat(yield): display order progress bar * refactor: move useIsEoaEthFlow to trade module * refactor: move hooks to tradeSlippage module * refactor: rename swapSlippage to tradeSlippage * feat(trade-slippage): split slippage state by trade type * refactor: unlink TransactionSettings from swap module * refactor: use reach modal in Settings component * refactor: move Settings component in trade module * feat(yield): add settings widget * feat(yield): use deadline value from settings * fix(trade-quote): skip fast quote if it slower than optimal * refactor: generalise TradeRateDetails from swap module * refactor: move TradeRateDetails into tradeWidgetAddons module * refactor: move SettingsTab into tradeWidgetAddons module * refactor(swap): generalise useHighFeeWarning() * refactor(swap): move hooks to trade module * refactor: move HighFeeWarning to trade widget addons * refactor: make HighFeeWarning independent * feat(yield): display trade warnings ZeroApprovalWarning and HighFeeWarning * refactor(trade): generalise NoImpactWarning * refactor(trade): generalise ZeroApprovalWarning * refactor(trade): generalise bundle tx banners * refactor: extract TradeWarnings * chore: fix yield form displaying * refactor(swap): generalise trade flow * feat(yield): support safe bundle swaps * chore: hide yield under ff * chore: remove lazy loading * chore: fix imports * fix: generalize smart slippage usage * fix: don't sync url params while navigating with yield * feat: open settings menu on slippage click * chore: update btn text * fix: make slippage settings through * chore: merge develop * chore: fix yield widget * chore: remove old migration * feat: add default lp-token lists * feat(tokens): display custom token selector for Yield widget * feat(tokens): display lp-token lists * feat: display lp-token logo * chore: slippage label * fix: fix default trade state in menu * chore: fix lint * feat: fetch lp-token balances Also added optimization to the balances updater, because node started firing 429 error * feat: reuse virtual list for lp-tokens list * chore: adjust balances updater * chore: add yield in widget conf * chore: reset balances only when account changed * feat: cache balances into localStorage * feat: display cached token balances * feat: link to create pool * chore: merge develop * chore: fix hooks trade state * fix: fix smart slippage displaying * chore: center slippage banner * chore: condition to displayCreatePoolBanner * fix: poll lp-token balances only in yield widget * feat(yield): persist pools info * feat(yield): allow using lp-tokens in widget * feat(yield): display lp-token logo in widget * feat(yield): display pool apy * chore: fix lp token logo * feat(yield): pool info page * chore: fix build * chore(yield): fix balance loading state * chore(yield): fix pools info fetching * chore: fix yield menu link * chore: format pool displayed values * chore: always use address for lp-tokens in url * fix: fix lp-tokens usage * chore: fix lint * feat(yield): define token category by default for selection * chore: fix lint * feat: add isCoWAMM indicator to LpToken * refactor: fix lp tokens segregation * chore: fix apr default value * chore: remove lp tokens duplicates * fix: set lp tokens to sell wth cowamm is set as buy * refactor: simplify cow amm banner * refactor: extract PoolInfo * refactor: decompose CoWAmmBannerContent * refactor: remove BannerLocation * refactor: simplify callback * feat(yield): add lp-token provider info * feat(yield): detect lp-tokens cow amm alternative * feat(yield): display cow amm banner conditionally * chore: mocks overrides * chore: fix build * feat: modify pool token list layout (#5014) * feat: modify pool token list layout * feat: apply mobile widgetlinks menu * feat: add padding to pool token balance --------- Co-authored-by: Alexandr Kazachenko * chore: link to create pool * chore: merge changes * chore: fix balances cache * feat: navigate from cow amm banner * chore: fix crash * chore: fix analytics link * chore: reset balances cache * fix: show cow amm banner only when lp balances are loaded * chore: fix average calc * fix: balance cache clean up * chore: fix styles * chore: fix apr --------- Co-authored-by: fairlight <31534717+fairlighteth@users.noreply.github.com> --- .../common/containers/CoWAmmBanner/index.tsx | 47 +++++-- .../common/containers/CoWAmmBanner/types.ts | 24 ++++ .../CoWAmmBanner/useVampireAttack.ts | 101 ++++++++++++++ .../ComparisonMessage/index.tsx | 66 --------- .../GlobalContent/index.tsx | 42 ++---- .../CoWAmmBannerContent/LpEmblems/index.tsx | 45 +++---- .../CoWAmmBannerContent/PoolInfo/index.tsx | 27 ++-- .../TokenSelectorContent/index.tsx | 30 +---- .../common/pure/CoWAmmBannerContent/const.ts | 23 ++++ .../pure/CoWAmmBannerContent/dummyData.ts | 115 ---------------- .../common/pure/CoWAmmBannerContent/index.tsx | 126 +++++++++++++----- .../common/pure/CoWAmmBannerContent/styled.ts | 5 +- .../common/pure/CoWAmmBannerContent/types.ts | 4 - .../LpBalancesAndAllowancesUpdater.tsx | 5 + .../application/containers/App/Updaters.tsx | 3 +- .../containers/LpTokenPage/index.tsx | 2 +- .../getDefaultTokenListCategories.ts | 5 +- .../tokensList/pure/LpTokenLists/styled.ts | 4 +- .../hooks/useSetupTradeAmountsFromUrl.ts | 2 + .../modules/trade/hooks/useTradeNavigate.ts | 14 +- .../yield/hooks/useLpTokensWithBalances.ts | 41 +----- .../src/modules/yield/shared.ts | 1 + .../yield/state/lpTokensWithBalancesAtom.ts | 15 +++ .../LpTokensWithBalancesUpdater/index.ts | 38 ++++++ .../yield/updaters/PoolsInfoUpdater/index.tsx | 4 +- .../updaters/PoolsInfoUpdater/mockPoolInfo.ts | 60 ++++++--- .../SetupYieldAmountsFromUrlUpdater.tsx | 9 ++ .../src/modules/yield/updaters/index.tsx | 2 + .../hooks/usePersistBalancesAndAllowances.ts | 15 ++- libs/common-const/src/types.ts | 8 +- libs/tokens/src/const/lpTokensList.json | 12 +- .../tokens/src/hooks/tokens/useAllLpTokens.ts | 3 +- libs/tokens/src/services/fetchTokenList.ts | 2 +- .../state/tokenLists/tokenListsStateAtom.ts | 2 +- libs/tokens/src/state/tokens/allTokensAtom.ts | 16 +-- libs/tokens/src/types.ts | 6 +- .../src/updaters/TokensListsUpdater/index.ts | 2 +- libs/tokens/src/utils/parseTokenInfo.ts | 2 +- .../src/utils/tokenMapToListWithLogo.ts | 4 +- libs/types/src/common.ts | 12 +- 40 files changed, 513 insertions(+), 431 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/containers/CoWAmmBanner/types.ts create mode 100644 apps/cowswap-frontend/src/common/containers/CoWAmmBanner/useVampireAttack.ts delete mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/ComparisonMessage/index.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/const.ts delete mode 100644 apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/dummyData.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/state/lpTokensWithBalancesAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/LpTokensWithBalancesUpdater/index.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/SetupYieldAmountsFromUrlUpdater.tsx diff --git a/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx index 58a67bf19a..994d4f8869 100644 --- a/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx @@ -1,18 +1,22 @@ import { useCallback } from 'react' import { isInjectedWidget } from '@cowprotocol/common-utils' +import { OrderKind } from '@cowprotocol/cow-sdk' +import { useTokensByAddressMap } from '@cowprotocol/tokens' import { ClosableBanner } from '@cowprotocol/ui' import { useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet' +import { CurrencyAmount } from '@uniswap/sdk-core' import { useIsDarkMode } from 'legacy/state/user/hooks' import { cowAnalytics } from 'modules/analytics' +import { useTradeNavigate } from 'modules/trade' +import { useVampireAttack } from './useVampireAttack' + +import { Routes } from '../../constants/routes' import { useIsProviderNetworkUnsupported } from '../../hooks/useIsProviderNetworkUnsupported' import { CoWAmmBannerContent } from '../../pure/CoWAmmBannerContent' -import { dummyData, lpTokenConfig } from '../../pure/CoWAmmBannerContent/dummyData' - -const ANALYTICS_URL = 'https://cow.fi/pools?utm_source=swap.cow.fi&utm_medium=web&utm_content=cow_amm_banner' interface BannerProps { isTokenSelectorView?: boolean @@ -21,18 +25,37 @@ interface BannerProps { export function CoWAmmBanner({ isTokenSelectorView }: BannerProps) { const isDarkMode = useIsDarkMode() const isInjectedWidgetMode = isInjectedWidget() - const { account } = useWalletInfo() + const { chainId, account } = useWalletInfo() const isChainIdUnsupported = useIsProviderNetworkUnsupported() + const vampireAttackContext = useVampireAttack() + const tokensByAddress = useTokensByAddressMap() + const tradeNavigate = useTradeNavigate() const key = isTokenSelectorView ? 'tokenSelector' : 'global' const handleCTAClick = useCallback(() => { + const superiorAlternative = vampireAttackContext?.superiorAlternatives?.[0] + const alternative = vampireAttackContext?.alternatives?.[0] + const target = superiorAlternative || alternative + + const targetTrade = { + inputCurrencyId: target?.token.address || null, + outputCurrencyId: target?.alternative.address || null, + } + + const targetTradeParams = { + amount: target + ? CurrencyAmount.fromRawAmount(target.token, target.tokenBalance.toString()).toFixed(6) + : undefined, + kind: OrderKind.SELL, + } + cowAnalytics.sendEvent({ category: 'CoW Swap', action: `CoW AMM Banner [${key}] CTA Clicked`, }) - window.open(ANALYTICS_URL, '_blank') - }, [key]) + tradeNavigate(chainId, targetTrade, targetTradeParams, Routes.YIELD) + }, [key, chainId, vampireAttackContext, tradeNavigate]) const handleClose = useCallback(() => { cowAnalytics.sendEvent({ @@ -45,7 +68,7 @@ export function CoWAmmBanner({ isTokenSelectorView }: BannerProps) { const isSmartContractWallet = useIsSmartContractWallet() - if (isInjectedWidgetMode || !account || isChainIdUnsupported) return null + if (isInjectedWidgetMode || !account || isChainIdUnsupported || !vampireAttackContext) return null return ClosableBanner(bannerId, (close) => ( { + handleCTAClick() + close() + }} onClose={() => { handleClose() close() diff --git a/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/types.ts b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/types.ts new file mode 100644 index 0000000000..9e9b7a0fae --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/types.ts @@ -0,0 +1,24 @@ +import { LpToken } from '@cowprotocol/common-const' +import { LpTokenProvider } from '@cowprotocol/types' +import { BigNumber } from '@ethersproject/bignumber' + +import { PoolInfo } from 'modules/yield/state/poolsInfoAtom' + +export interface TokenWithAlternative { + token: LpToken + alternative: LpToken + tokenBalance: BigNumber +} + +export interface TokenWithSuperiorAlternative extends TokenWithAlternative { + tokenPoolInfo: PoolInfo + alternativePoolInfo: PoolInfo +} + +export interface VampireAttackContext { + alternatives: TokenWithAlternative[] | null + superiorAlternatives: TokenWithSuperiorAlternative[] | null + cowAmmLpTokensCount: number + poolsAverageData: Partial | undefined> + averageApyDiff: number | undefined +} diff --git a/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/useVampireAttack.ts b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/useVampireAttack.ts new file mode 100644 index 0000000000..e6bec7d64c --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/useVampireAttack.ts @@ -0,0 +1,101 @@ +import { useAtomValue } from 'jotai' +import { useMemo } from 'react' + +import { LP_TOKEN_LIST_COW_AMM_ONLY, useAllLpTokens } from '@cowprotocol/tokens' +import { LpTokenProvider } from '@cowprotocol/types' + +import { useLpTokensWithBalances, usePoolsInfo } from 'modules/yield/shared' +import { POOLS_AVERAGE_DATA_MOCK } from 'modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo' + +import { TokenWithAlternative, TokenWithSuperiorAlternative, VampireAttackContext } from './types' + +import { useSafeMemoObject } from '../../hooks/useSafeMemo' +import { areLpBalancesLoadedAtom } from '../../updaters/LpBalancesAndAllowancesUpdater' + +export function useVampireAttack(): VampireAttackContext | null { + const { tokens: lpTokensWithBalances, count: lpTokensWithBalancesCount } = useLpTokensWithBalances() + const cowAmmLpTokens = useAllLpTokens(LP_TOKEN_LIST_COW_AMM_ONLY) + const poolsInfo = usePoolsInfo() + const areLpBalancesLoaded = useAtomValue(areLpBalancesLoadedAtom) + + const alternativesResult = useMemo(() => { + if (lpTokensWithBalancesCount === 0) return null + + const result = Object.keys(lpTokensWithBalances).reduce( + (acc, tokenAddress) => { + const { token: lpToken, balance: tokenBalance } = lpTokensWithBalances[tokenAddress] + const alternative = cowAmmLpTokens.find((cowAmmLpToken) => { + return cowAmmLpToken.tokens.every((token) => lpToken.tokens.includes(token)) + }) + + if (alternative) { + const tokenPoolInfo = poolsInfo?.[lpToken.address.toLowerCase()]?.info + const alternativePoolInfo = poolsInfo?.[alternative.address.toLowerCase()]?.info + + // When CoW AMM pool has better APY + if (alternativePoolInfo?.apy && tokenPoolInfo?.apy && alternativePoolInfo.apy > tokenPoolInfo.apy) { + acc.superiorAlternatives.push({ + token: lpToken, + alternative, + tokenPoolInfo, + alternativePoolInfo, + tokenBalance, + }) + } else { + acc.alternatives.push({ token: lpToken, alternative, tokenBalance }) + } + } + + return acc + }, + { superiorAlternatives: [] as TokenWithSuperiorAlternative[], alternatives: [] as TokenWithAlternative[] }, + ) + + return { + superiorAlternatives: result.superiorAlternatives.sort((a, b) => { + if (!b.tokenPoolInfo || !a.tokenPoolInfo) return 0 + + return b.tokenPoolInfo.apy - a.tokenPoolInfo.apy + }), + alternatives: result.alternatives.sort((a, b) => { + const aBalance = lpTokensWithBalances[a.token.address.toLowerCase()].balance + const bBalance = lpTokensWithBalances[b.token.address.toLowerCase()].balance + + return +bBalance.sub(aBalance).toString() + }), + } + }, [lpTokensWithBalancesCount, lpTokensWithBalances, cowAmmLpTokens, poolsInfo]) + + const averageApy = useMemo(() => { + const keys = Object.keys(POOLS_AVERAGE_DATA_MOCK) + let count = 0 + + return ( + keys.reduce((result, _key) => { + const key = _key as LpTokenProvider + + if (key === LpTokenProvider.COW_AMM) return result + + count++ + const pool = POOLS_AVERAGE_DATA_MOCK[key] + + return result + (pool?.apy || 0) + }, 0) / count + ) + }, []) + + const { [LpTokenProvider.COW_AMM]: cowAmmData, ...poolsAverageData } = POOLS_AVERAGE_DATA_MOCK + const averageApyDiff = cowAmmData ? +(cowAmmData.apy - averageApy).toFixed(2) : 0 + + const context = useSafeMemoObject({ + superiorAlternatives: alternativesResult?.superiorAlternatives || null, + alternatives: alternativesResult?.alternatives || null, + cowAmmLpTokensCount: cowAmmLpTokens.length, + poolsAverageData, + averageApyDiff, + }) + + if (cowAmmLpTokens.length === 0 || !areLpBalancesLoaded) return null + + return context +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/ComparisonMessage/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/ComparisonMessage/index.tsx deleted file mode 100644 index c923a5cd02..0000000000 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/ComparisonMessage/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react' - -import { DummyDataType, InferiorYieldScenario, LpToken, StateKey, TwoLpScenario } from '../dummyData' -import { PoolInfo } from '../PoolInfo' - -const lpTokenNames: Record = { - [LpToken.UniswapV2]: 'UNI-V2', - [LpToken.Sushiswap]: 'Sushi', - [LpToken.PancakeSwap]: 'PancakeSwap', - [LpToken.Curve]: 'Curve', -} - -function isTwoLpScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is TwoLpScenario { - return 'uniV2Apr' in scenario && 'sushiApr' in scenario -} - -function isInferiorYieldScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is InferiorYieldScenario { - return 'poolsCount' in scenario -} - -interface ComparisonMessageProps { - data: DummyDataType[keyof DummyDataType] - isDarkMode: boolean - isTokenSelectorView: boolean - selectedState: StateKey - tokens: LpToken[] -} - -export function ComparisonMessage({ - data, - tokens, - selectedState, - isDarkMode, - isTokenSelectorView, -}: ComparisonMessageProps) { - if (isTwoLpScenario(data)) { - if (selectedState === 'twoLpsMixed') { - return - } else if (selectedState === 'twoLpsBothSuperior') { - const { uniV2Apr, sushiApr } = data - const higherAprPool = uniV2Apr > sushiApr ? 'UNI-V2' : 'SushiSwap' - - return - } - } - - if (selectedState === 'uniV2Superior') { - return - } - - if (selectedState === 'uniV2InferiorWithLowAverageYield' && isInferiorYieldScenario(data)) { - return 'pools available to get yield on your assets!' - } - - if (data.hasCoWAmmPool) { - return `yield over average ${data.comparison} pool` - } else { - if (tokens.length > 1) { - const tokenNames = tokens.map((token) => lpTokenNames[token]).filter(Boolean) - - return `yield over average ${tokenNames.join(', ')} pool${tokenNames.length > 1 ? 's' : ''}` - } else { - return `yield over average UNI-V2 pool` - } - } -} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/GlobalContent/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/GlobalContent/index.tsx index 568b592a43..78747b022c 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/GlobalContent/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/GlobalContent/index.tsx @@ -1,38 +1,23 @@ -import React, { RefObject } from 'react' +import React, { ReactNode, RefObject } from 'react' +import { LpTokenProvider } from '@cowprotocol/types' import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' import { ArrowBackground } from '../../ArrowBackground' import { StarIcon, TextFit } from '../Common' -import { LpToken } from '../dummyData' import { LpEmblems } from '../LpEmblems' import * as styledEl from '../styled' import { CoWAmmBannerContext } from '../types' interface GlobalContentProps { - isUniV2InferiorWithLowAverageYield: boolean - tokens: LpToken[] arrowBackgroundRef: RefObject context: CoWAmmBannerContext + comparedProviders: LpTokenProvider[] | undefined + children: ReactNode } -export function GlobalContent({ - context, - isUniV2InferiorWithLowAverageYield, - tokens, - arrowBackgroundRef, -}: GlobalContentProps) { - const { - title, - ctaText, - aprMessage, - comparisonMessage, - onClose, - isMobile, - onCtaClick, - handleCTAMouseLeave, - handleCTAMouseEnter, - } = context +export function GlobalContent({ context, children, arrowBackgroundRef, comparedProviders }: GlobalContentProps) { + const { title, ctaText, onClose, isMobile, onCtaClick, handleCTAMouseLeave, handleCTAMouseEnter } = context return ( @@ -48,16 +33,7 @@ export function GlobalContent({ -

    - - {aprMessage} - -

    - - - {comparisonMessage} - - + {children}
    @@ -68,7 +44,7 @@ export function GlobalContent({ One-click convert, boost yield - + )} @@ -76,7 +52,7 @@ export function GlobalContent({ {ctaText} - Pool analytics ↗ + Pool analytics ↗
    diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/LpEmblems/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/LpEmblems/index.tsx index 2b40e00bfa..f9b13c7ab1 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/LpEmblems/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/LpEmblems/index.tsx @@ -1,36 +1,23 @@ import React from 'react' import ICON_ARROW from '@cowprotocol/assets/cow-swap/arrow.svg' -import ICON_CURVE from '@cowprotocol/assets/cow-swap/icon-curve.svg' -import ICON_PANCAKESWAP from '@cowprotocol/assets/cow-swap/icon-pancakeswap.svg' -import ICON_SUSHISWAP from '@cowprotocol/assets/cow-swap/icon-sushi.svg' -import ICON_UNISWAP from '@cowprotocol/assets/cow-swap/icon-uni.svg' import { USDC } from '@cowprotocol/common-const' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { TokenLogo } from '@cowprotocol/tokens' +import { LpTokenProvider } from '@cowprotocol/types' import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' import SVG from 'react-inlinesvg' -import { LpToken } from '../dummyData' +import { LP_PROVIDER_ICONS } from '../const' import * as styledEl from '../styled' -const lpTokenIcons: Record = { - [LpToken.UniswapV2]: ICON_UNISWAP, - [LpToken.Sushiswap]: ICON_SUSHISWAP, - [LpToken.PancakeSwap]: ICON_PANCAKESWAP, - [LpToken.Curve]: ICON_CURVE, -} - interface LpEmblemsProps { - isUniV2InferiorWithLowAverageYield: boolean - tokens: LpToken[] + comparedProviders: LpTokenProvider[] | undefined } -export function LpEmblems({ tokens, isUniV2InferiorWithLowAverageYield }: LpEmblemsProps) { - const totalItems = tokens.length - - const renderEmblemContent = () => ( +export function LpEmblems({ comparedProviders }: LpEmblemsProps) { + const EmblemContent = ( <> @@ -46,31 +33,29 @@ export function LpEmblems({ tokens, isUniV2InferiorWithLowAverageYield }: LpEmbl ) - if (totalItems === 0 || isUniV2InferiorWithLowAverageYield) { + if (!comparedProviders?.length) { return ( - - {isUniV2InferiorWithLowAverageYield ? ( - - ) : ( - - )} + + - {renderEmblemContent()} + {EmblemContent} ) } + const totalItems = comparedProviders.length + return ( - {tokens.map((token, index) => ( - - + {comparedProviders.map((provider, index) => ( + + ))} - {renderEmblemContent()} + {EmblemContent} ) } diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/PoolInfo/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/PoolInfo/index.tsx index 5c512e5147..77fd3b4299 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/PoolInfo/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/PoolInfo/index.tsx @@ -1,19 +1,26 @@ import React from 'react' -import { USDC, WBTC } from '@cowprotocol/common-const' -import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { TokenLogo } from '@cowprotocol/tokens' -import { UI } from '@cowprotocol/ui' +import { LpToken } from '@cowprotocol/common-const' +import { TokenLogo, TokensByAddress } from '@cowprotocol/tokens' +import { TokenSymbol, UI } from '@cowprotocol/ui' +import { LP_PROVIDER_NAMES } from '../const' import * as styledEl from '../styled' interface PoolInfoProps { isDarkMode: boolean isTokenSelectorView: boolean - poolName: string + token: LpToken + tokensByAddress: TokensByAddress } -export function PoolInfo({ poolName, isTokenSelectorView, isDarkMode }: PoolInfoProps) { +export function PoolInfo({ token, tokensByAddress, isTokenSelectorView, isDarkMode }: PoolInfoProps) { + const poolName = token.lpTokenProvider ? LP_PROVIDER_NAMES[token.lpTokenProvider] : null + const token0 = tokensByAddress[token.tokens[0]] + const token1 = tokensByAddress[token.tokens[1]] + + if (!poolName) return null + return ( higher APR available for your {poolName} pool:
    - +
    - WBTC-USDC + + - +
    ) diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/TokenSelectorContent/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/TokenSelectorContent/index.tsx index 5d19ba7895..06ca0cc9b7 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/TokenSelectorContent/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/TokenSelectorContent/index.tsx @@ -1,27 +1,18 @@ -import React from 'react' +import React, { ReactNode } from 'react' import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' -import { StarIcon, TextFit } from '../Common' +import { StarIcon } from '../Common' import * as styledEl from '../styled' import { CoWAmmBannerContext } from '../types' interface TokenSelectorContentProps { isDarkMode: boolean context: CoWAmmBannerContext + children: ReactNode } -export function TokenSelectorContent({ isDarkMode, context }: TokenSelectorContentProps) { - const { - title, - isMobile, - ctaText, - onClose, - onCtaClick, - aprMessage, - comparisonMessage, - handleCTAMouseEnter, - handleCTAMouseLeave, - } = context +export function TokenSelectorContent({ isDarkMode, context, children }: TokenSelectorContentProps) { + const { title, ctaText, onClose, onCtaClick, handleCTAMouseEnter, handleCTAMouseLeave } = context const mainColor = isDarkMode ? UI.COLOR_COWAMM_LIGHT_GREEN : UI.COLOR_COWAMM_DARK_GREEN @@ -45,16 +36,7 @@ export function TokenSelectorContent({ isDarkMode, context }: TokenSelectorConte height={90} > -

    - - {aprMessage} - -

    - - - {comparisonMessage} - - + {children} = { + [LpTokenProvider.UNIV2]: 'UNI-V2', + [LpTokenProvider.SUSHI]: 'Sushi', + [LpTokenProvider.PANCAKE]: 'PancakeSwap', + [LpTokenProvider.CURVE]: 'Curve', + [LpTokenProvider.COW_AMM]: 'CoW AMM', + [LpTokenProvider.BALANCERV2]: 'Balancer', +} + +export const LP_PROVIDER_ICONS: Record = { + [LpTokenProvider.UNIV2]: ICON_UNISWAP, + [LpTokenProvider.SUSHI]: ICON_SUSHISWAP, + [LpTokenProvider.PANCAKE]: ICON_PANCAKESWAP, + [LpTokenProvider.CURVE]: ICON_CURVE, + [LpTokenProvider.COW_AMM]: '', + [LpTokenProvider.BALANCERV2]: '', +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/dummyData.ts b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/dummyData.ts deleted file mode 100644 index 26492684f6..0000000000 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/dummyData.ts +++ /dev/null @@ -1,115 +0,0 @@ -export enum LpToken { - UniswapV2 = 'UniswapV2', - Sushiswap = 'Sushiswap', - PancakeSwap = 'PancakeSwap', - Curve = 'Curve', -} - -type BaseScenario = { - readonly apr: number - readonly comparison: string - readonly hasCoWAmmPool: boolean -} - -export type TwoLpScenario = BaseScenario & { - readonly uniV2Apr: number - readonly sushiApr: number -} - -export type InferiorYieldScenario = BaseScenario & { - readonly poolsCount: number -} - -export type DummyDataType = { - noLp: BaseScenario - uniV2Superior: BaseScenario - uniV2Inferior: BaseScenario - sushi: BaseScenario - curve: BaseScenario - pancake: BaseScenario & { readonly isYieldSuperior: boolean } - twoLpsMixed: TwoLpScenario - twoLpsBothSuperior: TwoLpScenario - threeLps: BaseScenario - fourLps: BaseScenario - uniV2InferiorWithLowAverageYield: InferiorYieldScenario -} - -export const dummyData: DummyDataType = { - noLp: { - apr: 1.5, - comparison: 'average UNI-V2 pool', - hasCoWAmmPool: false, - }, - uniV2Superior: { - apr: 2.1, - comparison: 'UNI-V2', - hasCoWAmmPool: true, - }, - uniV2Inferior: { - apr: 1.2, - comparison: 'UNI-V2', - hasCoWAmmPool: true, - }, - sushi: { - apr: 1.8, - comparison: 'SushiSwap', - hasCoWAmmPool: true, - }, - curve: { - apr: 1.3, - comparison: 'Curve', - hasCoWAmmPool: true, - }, - pancake: { - apr: 2.5, - comparison: 'PancakeSwap', - hasCoWAmmPool: true, - isYieldSuperior: true, - }, - twoLpsMixed: { - apr: 2.5, - comparison: 'UNI-V2 and SushiSwap', - hasCoWAmmPool: true, - uniV2Apr: 3.0, - sushiApr: 1.8, - } as TwoLpScenario, - twoLpsBothSuperior: { - apr: 3.2, - comparison: 'UNI-V2 and SushiSwap', - hasCoWAmmPool: true, - uniV2Apr: 3.5, - sushiApr: 2.9, - } as TwoLpScenario, - threeLps: { - apr: 2.2, - comparison: 'UNI-V2, SushiSwap, and Curve', - hasCoWAmmPool: false, - }, - fourLps: { - apr: 2.4, - comparison: 'UNI-V2, SushiSwap, Curve, and PancakeSwap', - hasCoWAmmPool: false, - }, - uniV2InferiorWithLowAverageYield: { - apr: 1.2, - comparison: 'UNI-V2', - hasCoWAmmPool: true, - poolsCount: 195, - }, -} - -export type StateKey = keyof typeof dummyData - -export const lpTokenConfig: Record = { - noLp: [], - uniV2Superior: [LpToken.UniswapV2], - uniV2Inferior: [LpToken.UniswapV2], - sushi: [LpToken.Sushiswap], - curve: [LpToken.Curve], - pancake: [LpToken.PancakeSwap], - twoLpsMixed: [LpToken.UniswapV2, LpToken.Sushiswap], - twoLpsBothSuperior: [LpToken.UniswapV2, LpToken.Sushiswap], - threeLps: [LpToken.UniswapV2, LpToken.Sushiswap, LpToken.Curve], - fourLps: [LpToken.UniswapV2, LpToken.Sushiswap, LpToken.Curve, LpToken.PancakeSwap], - uniV2InferiorWithLowAverageYield: [LpToken.UniswapV2], -} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx index cd31ef4ae3..8034db984a 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/index.tsx @@ -1,13 +1,19 @@ import React, { useCallback, useMemo, useRef } from 'react' +import { isTruthy } from '@cowprotocol/common-utils' +import { TokensByAddress } from '@cowprotocol/tokens' +import { LpTokenProvider } from '@cowprotocol/types' + import { upToSmall, useMediaQuery } from 'legacy/hooks/useMediaQuery' -import { ComparisonMessage } from './ComparisonMessage' -import { dummyData, lpTokenConfig, StateKey } from './dummyData' +import { TextFit } from './Common' +import { LP_PROVIDER_NAMES } from './const' import { GlobalContent } from './GlobalContent' +import { PoolInfo } from './PoolInfo' import { TokenSelectorContent } from './TokenSelectorContent' import { CoWAmmBannerContext } from './types' +import { VampireAttackContext } from '../../containers/CoWAmmBanner/types' import { useSafeMemoObject } from '../../hooks/useSafeMemo' interface CoWAmmBannerContentProps { @@ -16,9 +22,8 @@ interface CoWAmmBannerContentProps { ctaText: string isTokenSelectorView: boolean isDarkMode: boolean - selectedState: StateKey - dummyData: typeof dummyData - lpTokenConfig: typeof lpTokenConfig + vampireAttackContext: VampireAttackContext + tokensByAddress: TokensByAddress onCtaClick: () => void onClose: () => void } @@ -28,19 +33,15 @@ export function CoWAmmBannerContent({ title, ctaText, isTokenSelectorView, - selectedState, - dummyData, - lpTokenConfig, onCtaClick, onClose, isDarkMode, + vampireAttackContext, + tokensByAddress, }: CoWAmmBannerContentProps) { const isMobile = useMediaQuery(upToSmall) const arrowBackgroundRef = useRef(null) - - const tokens = lpTokenConfig[selectedState] - const data = dummyData[selectedState] - const isUniV2InferiorWithLowAverageYield = selectedState === 'uniV2InferiorWithLowAverageYield' + const { superiorAlternatives, cowAmmLpTokensCount, averageApyDiff, poolsAverageData } = vampireAttackContext const handleCTAMouseEnter = useCallback(() => { if (arrowBackgroundRef.current) { @@ -56,28 +57,38 @@ export function CoWAmmBannerContent({ } }, []) - const aprMessage = useMemo(() => { - if (selectedState === 'uniV2InferiorWithLowAverageYield' && 'poolsCount' in data) { - return `${data.poolsCount}+` - } - return `+${data.apr.toFixed(1)}%` - }, [selectedState, data]) - - const comparisonMessage = ( - - ) + const firstItemWithBetterCowAmm = superiorAlternatives?.[0] + const isCowAmmAverageBetter = !!averageApyDiff && averageApyDiff > 0 + + const worseThanCoWAmmProviders = useMemo(() => { + return superiorAlternatives?.reduce((acc, item) => { + if (item.token.lpTokenProvider && !acc.includes(item.token.lpTokenProvider)) { + return acc.concat(item.token.lpTokenProvider) + } + + return acc + }, [] as LpTokenProvider[]) + }, [superiorAlternatives]) + + const sortedAverageProviders = useMemo(() => { + if (!poolsAverageData) return undefined + return Object.keys(poolsAverageData).sort((a, b) => { + const aVal = poolsAverageData[a as LpTokenProvider] + const bVal = poolsAverageData[b as LpTokenProvider] + + if (!aVal || !bVal) return 0 + + return bVal.apy - aVal.apy + }) as LpTokenProvider[] + }, [poolsAverageData]) + + const averageProvidersNames = useMemo(() => { + return sortedAverageProviders?.map((key) => LP_PROVIDER_NAMES[key as LpTokenProvider]).filter(isTruthy) + }, [sortedAverageProviders]) const context: CoWAmmBannerContext = useSafeMemoObject({ title, ctaText, - aprMessage, - comparisonMessage, isMobile, onClose, onCtaClick, @@ -85,17 +96,64 @@ export function CoWAmmBannerContent({ handleCTAMouseLeave, }) + const Content = ( + <> +

    + + {firstItemWithBetterCowAmm + ? `+${firstItemWithBetterCowAmm.alternativePoolInfo.apy.toFixed(1)}%` + : isCowAmmAverageBetter + ? `+${averageApyDiff}%` + : `${cowAmmLpTokensCount}+`} + +

    + + + {firstItemWithBetterCowAmm ? ( + + ) : isCowAmmAverageBetter && averageProvidersNames ? ( + `yield over average ${averageProvidersNames.join(', ')} pool${averageProvidersNames.length > 1 ? 's' : ''}` + ) : ( + 'pools available to get yield on your assets!' + )} + + + + ) + return (
    {isTokenSelectorView ? ( - + + {Content} + ) : ( + comparedProviders={ + firstItemWithBetterCowAmm + ? worseThanCoWAmmProviders + : isCowAmmAverageBetter + ? sortedAverageProviders + : undefined + } + > + {Content} + )}
    ) diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/styled.ts b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/styled.ts index a798b1c3fb..086e5022b3 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/styled.ts +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/styled.ts @@ -1,5 +1,5 @@ import { TokenLogoWrapper } from '@cowprotocol/tokens' -import { UI, Media } from '@cowprotocol/ui' +import { UI, Media, ExternalLink } from '@cowprotocol/ui' import { X } from 'react-feather' import styled, { keyframes } from 'styled-components/macro' @@ -188,6 +188,7 @@ export const PoolInfo = styled.div<{ border-radius: 62px; width: min-content; box-shadow: var(${UI.BOX_SHADOW_2}); + align-items: center; ${Media.upToSmall()} { margin: 0 auto; @@ -264,7 +265,7 @@ export const CTAButton = styled.button<{ } ` -export const SecondaryLink = styled.a` +export const SecondaryLink = styled(ExternalLink)` color: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); font-size: 14px; font-weight: 500; diff --git a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/types.ts b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/types.ts index c88756df4f..11f875856c 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/types.ts +++ b/apps/cowswap-frontend/src/common/pure/CoWAmmBannerContent/types.ts @@ -1,10 +1,6 @@ -import { ReactNode } from 'react' - export interface CoWAmmBannerContext { title: string ctaText: string - aprMessage: string - comparisonMessage: ReactNode isMobile: boolean onClose(): void onCtaClick(): void diff --git a/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx b/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx index f5b31128ac..1ce5c28e8e 100644 --- a/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx +++ b/apps/cowswap-frontend/src/common/updaters/LpBalancesAndAllowancesUpdater.tsx @@ -1,3 +1,4 @@ +import { atom, useSetAtom } from 'jotai' import { useEffect, useMemo, useState } from 'react' import { usePersistBalancesAndAllowances } from '@cowprotocol/balances-and-allowances' @@ -16,6 +17,8 @@ const LP_MULTICALL_OPTIONS = { consequentExecution: true } // We start the updater with a delay const LP_UPDATER_START_DELAY = ms`3s` +export const areLpBalancesLoadedAtom = atom(false) + export interface BalancesAndAllowancesUpdaterProps { account: string | undefined chainId: SupportedChainId @@ -24,6 +27,7 @@ export interface BalancesAndAllowancesUpdaterProps { export function LpBalancesAndAllowancesUpdater({ account, chainId, enablePolling }: BalancesAndAllowancesUpdaterProps) { const allLpTokens = useAllLpTokens(LP_TOKEN_LIST_CATEGORIES) const [isUpdaterPaused, setIsUpdaterPaused] = useState(true) + const setAreLpBalancesLoaded = useSetAtom(areLpBalancesLoadedAtom) const lpTokenAddresses = useMemo(() => allLpTokens.map((token) => token.address), [allLpTokens]) @@ -35,6 +39,7 @@ export function LpBalancesAndAllowancesUpdater({ account, chainId, enablePolling balancesSwrConfig: enablePolling ? LP_BALANCES_SWR_CONFIG : SWR_NO_REFRESH_OPTIONS, allowancesSwrConfig: enablePolling ? LP_ALLOWANCES_SWR_CONFIG : SWR_NO_REFRESH_OPTIONS, multicallOptions: LP_MULTICALL_OPTIONS, + onBalancesLoaded: setAreLpBalancesLoaded, }) useEffect(() => { diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index e6753d580d..3be8417c9f 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -15,7 +15,7 @@ import { EthFlowDeadlineUpdater } from 'modules/swap/state/EthFlow/updaters' import { useOnTokenListAddingError } from 'modules/tokensList' import { TradeType, useTradeTypeInfo } from 'modules/trade' import { UsdPricesUpdater } from 'modules/usdAmount' -import { PoolsInfoUpdater } from 'modules/yield/shared' +import { LpTokensWithBalancesUpdater, PoolsInfoUpdater } from 'modules/yield/shared' import { ProgressBarV2ExecutingOrdersUpdater } from 'common/hooks/orderProgressBarV2' import { TotalSurplusUpdater } from 'common/state/totalSurplusState' @@ -87,6 +87,7 @@ export function Updaters() { + ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/index.tsx index 460ee8db8e..bd6e91f505 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/LpTokenPage/index.tsx @@ -84,7 +84,7 @@ export function LpTokenPage({ poolAddress, onBack, onDismiss, onSelectToken }: L
    -
    APY
    +
    APR
    {renderValue(info?.apy, (t) => `${t}%`, '-')}
    diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/getDefaultTokenListCategories.ts b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/getDefaultTokenListCategories.ts index 6cea4a365f..f1e8f88e70 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/getDefaultTokenListCategories.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/getDefaultTokenListCategories.ts @@ -1,5 +1,6 @@ import { LpToken } from '@cowprotocol/common-const' import { LP_TOKEN_LIST_CATEGORIES, LP_TOKEN_LIST_COW_AMM_ONLY, TokenListCategory } from '@cowprotocol/tokens' +import { LpTokenProvider } from '@cowprotocol/types' import { Currency } from '@uniswap/sdk-core' import { Field } from 'legacy/state/types' @@ -16,7 +17,7 @@ export function getDefaultTokenListCategories( // If sell token is LP token if (isOppositeLp) { // And sell token is COW AMM LP token, propose all LP tokens by default as buy token - if (oppositeToken.isCowAmm) { + if (oppositeToken.lpTokenProvider === LpTokenProvider.COW_AMM) { return LP_TOKEN_LIST_CATEGORIES } else { // And sell token is not COW AMM LP token, propose COW AMM LP tokens by default as buy token @@ -28,7 +29,7 @@ export function getDefaultTokenListCategories( } } - if (isOppositeLp && oppositeToken.isCowAmm) { + if (isOppositeLp && oppositeToken.lpTokenProvider === LpTokenProvider.COW_AMM) { return LP_TOKEN_LIST_CATEGORIES } diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts index 44971e2b5e..2fac1a40d6 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/styled.ts @@ -3,7 +3,7 @@ import { ExternalLink, UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' export const Wrapper = styled.div` - --grid-columns: 1fr 100px 50px 20px; + --grid-columns: 1fr 90px 60px 20px; display: flex; flex-direction: column; overflow: auto; @@ -60,7 +60,7 @@ export const LpTokenInfo = styled.div` export const LpTokenYieldPercentage = styled.span` display: flex; align-items: center; - font-size: 16px; + font-size: 15px; font-weight: 600; ` diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useSetupTradeAmountsFromUrl.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useSetupTradeAmountsFromUrl.ts index 6effaee0e9..c7adec286a 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useSetupTradeAmountsFromUrl.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useSetupTradeAmountsFromUrl.ts @@ -41,6 +41,8 @@ export function useSetupTradeAmountsFromUrl({ onAmountsUpdate, onlySell }: Setup const { inputCurrency, outputCurrency } = state || {} const cleanParams = useCallback(() => { + if (!search) return + const queryParams = new URLSearchParams(search) queryParams.delete(TRADE_URL_BUY_AMOUNT_KEY) diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts index ede7a0d3ee..1b3167620b 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts @@ -4,6 +4,7 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useLocation } from 'react-router-dom' +import { RoutesValues } from 'common/constants/routes' import { useNavigate } from 'common/hooks/useNavigate' import { useTradeTypeInfo } from './useTradeTypeInfo' @@ -16,7 +17,8 @@ interface UseTradeNavigateCallback { ( chainId: SupportedChainId | null | undefined, { inputCurrencyId, outputCurrencyId }: TradeCurrenciesIds, - searchParams?: TradeSearchParams + searchParams?: TradeSearchParams, + customRoute?: RoutesValues, ): void } @@ -30,9 +32,11 @@ export function useTradeNavigate(): UseTradeNavigateCallback { ( chainId: SupportedChainId | null | undefined, { inputCurrencyId, outputCurrencyId }: TradeCurrenciesIds, - searchParams?: TradeSearchParams + searchParams?: TradeSearchParams, + customRoute?: RoutesValues, ) => { - if (!tradeRoute) return + const targetRoute = customRoute || tradeRoute + if (!targetRoute) return const route = parameterizeTradeRoute( { @@ -43,7 +47,7 @@ export function useTradeNavigate(): UseTradeNavigateCallback { outputCurrencyAmount: undefined, orderKind: undefined, }, - tradeRoute + targetRoute, ) if (location.pathname === route) return @@ -52,6 +56,6 @@ export function useTradeNavigate(): UseTradeNavigateCallback { navigate({ pathname: route, search }) }, - [tradeRoute, navigate, location.pathname, location.search] + [tradeRoute, navigate, location.pathname, location.search], ) } diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts index ad360b0e5f..5745d4312c 100644 --- a/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useLpTokensWithBalances.ts @@ -1,42 +1,7 @@ -import { useTokensBalances } from '@cowprotocol/balances-and-allowances' -import { LpToken, SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' -import { TokenListCategory, useAllLpTokens } from '@cowprotocol/tokens' -import { BigNumber } from '@ethersproject/bignumber' +import { useAtomValue } from 'jotai' -import useSWR from 'swr' - -export type LpTokenWithBalance = { - token: LpToken - balance: BigNumber -} -export const LP_CATEGORY = [TokenListCategory.LP] - -const DEFAULT_STATE = { tokens: {} as Record, count: 0 } +import { lpTokensWithBalancesAtom } from '../state/lpTokensWithBalancesAtom' export function useLpTokensWithBalances() { - const lpTokens = useAllLpTokens(LP_CATEGORY) - const { values: balances } = useTokensBalances() - - return useSWR( - [lpTokens, balances, 'useLpTokensWithBalances'], - ([lpTokens, balances]) => { - if (lpTokens.length === 0) return DEFAULT_STATE - - return lpTokens.reduce( - (acc, token) => { - const addressLower = token.address.toLowerCase() - const balance = balances[addressLower] - - if (balance && !balance.isZero()) { - acc.count++ - acc.tokens[addressLower] = { token, balance } - } - - return acc - }, - { ...DEFAULT_STATE }, - ) - }, - { ...SWR_NO_REFRESH_OPTIONS, fallbackData: DEFAULT_STATE }, - ).data + return useAtomValue(lpTokensWithBalancesAtom) } diff --git a/apps/cowswap-frontend/src/modules/yield/shared.ts b/apps/cowswap-frontend/src/modules/yield/shared.ts index 3ed6eae2de..cfd5c82b29 100644 --- a/apps/cowswap-frontend/src/modules/yield/shared.ts +++ b/apps/cowswap-frontend/src/modules/yield/shared.ts @@ -1,4 +1,5 @@ export { PoolsInfoUpdater } from './updaters/PoolsInfoUpdater' +export { LpTokensWithBalancesUpdater } from './updaters/LpTokensWithBalancesUpdater' export { usePoolsInfo } from './hooks/usePoolsInfo' export { useLpTokensWithBalances } from './hooks/useLpTokensWithBalances' export type { PoolInfo, PoolInfoStates } from './state/poolsInfoAtom' diff --git a/apps/cowswap-frontend/src/modules/yield/state/lpTokensWithBalancesAtom.ts b/apps/cowswap-frontend/src/modules/yield/state/lpTokensWithBalancesAtom.ts new file mode 100644 index 0000000000..8c8a9593ba --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/state/lpTokensWithBalancesAtom.ts @@ -0,0 +1,15 @@ +import { atom } from 'jotai' + +import { LpToken } from '@cowprotocol/common-const' +import { BigNumber } from '@ethersproject/bignumber' + +export type LpTokenWithBalance = { + token: LpToken + balance: BigNumber +} + +type LpTokensWithBalancesState = { tokens: Record; count: 0 } + +export const LP_TOKENS_WITH_BALANCES_DEFAULT_STATE: LpTokensWithBalancesState = { tokens: {}, count: 0 } + +export const lpTokensWithBalancesAtom = atom(LP_TOKENS_WITH_BALANCES_DEFAULT_STATE) diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/LpTokensWithBalancesUpdater/index.ts b/apps/cowswap-frontend/src/modules/yield/updaters/LpTokensWithBalancesUpdater/index.ts new file mode 100644 index 0000000000..9be3864707 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/updaters/LpTokensWithBalancesUpdater/index.ts @@ -0,0 +1,38 @@ +import { useSetAtom } from 'jotai' +import { useEffect } from 'react' + +import { useTokensBalances } from '@cowprotocol/balances-and-allowances' +import { TokenListCategory, useAllLpTokens } from '@cowprotocol/tokens' + +import { LP_TOKENS_WITH_BALANCES_DEFAULT_STATE, lpTokensWithBalancesAtom } from '../../state/lpTokensWithBalancesAtom' + +const LP_CATEGORY = [TokenListCategory.LP] + +export function LpTokensWithBalancesUpdater() { + const lpTokens = useAllLpTokens(LP_CATEGORY) + const { values: balances } = useTokensBalances() + const setState = useSetAtom(lpTokensWithBalancesAtom) + + useEffect(() => { + if (!lpTokens.length) return + + const state = lpTokens.reduce( + (acc, token) => { + const addressLower = token.address.toLowerCase() + const balance = balances[addressLower] + + if (balance && !balance.isZero()) { + acc.count++ + acc.tokens[addressLower] = { token, balance } + } + + return acc + }, + { ...LP_TOKENS_WITH_BALANCES_DEFAULT_STATE }, + ) + + setState(state) + }, [setState, lpTokens, balances]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx index b7bb59f0a6..78a0bf9ca1 100644 --- a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/index.tsx @@ -46,7 +46,9 @@ export function PoolsInfoUpdater() { if (tokensToUpdate.length > 0 || isYield) { fetchPoolsInfo(isYield ? null : tokensToUpdate).then(upsertPoolsInfo) } - }, [isYield, tokensKey, tokensToUpdate, upsertPoolsInfo]) + // To avoid excessive recalculations we use tokensKey instead of tokensToUpdate in deps + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isYield, tokensKey, upsertPoolsInfo]) return null } diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts index efe5801784..e4808dcf47 100644 --- a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts +++ b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts @@ -1,18 +1,46 @@ +import { LpTokenProvider } from '@cowprotocol/types' + import { PoolInfo } from '../../state/poolsInfoAtom' -export const MOCK_POOL_INFO: Record = { - // Sushi AAVE/WETH - '0xd75ea151a61d06868e31f8988d28dfe5e9df57b4': { - apy: 1.89, - tvl: 157057, - feeTier: 0.3, - volume24h: 31.19, - }, - // CoW AMM AAVE/WETH - '0xf706c50513446d709f08d3e5126cd74fb6bfda19': { - apy: 0.07, - tvl: 52972, - feeTier: 0.3, - volume24h: 10, - }, -} +const MOCK_POOL_INFO_OVERRIDE = localStorage.getItem('MOCK_POOL_INFO') +const POOLS_AVERAGE_DATA_MOCK_OVERRIDE = localStorage.getItem('POOLS_AVERAGE_DATA_MOCK') + +export const MOCK_POOL_INFO: Record = MOCK_POOL_INFO_OVERRIDE + ? JSON.parse(MOCK_POOL_INFO_OVERRIDE) + : { + // Sushi AAVE/WETH + '0xd75ea151a61d06868e31f8988d28dfe5e9df57b4': { + apy: 1.89, + tvl: 157057, + feeTier: 0.3, + volume24h: 31.19, + }, + // CoW AMM AAVE/WETH + '0xf706c50513446d709f08d3e5126cd74fb6bfda19': { + apy: 0.07, + tvl: 52972, + feeTier: 0.3, + volume24h: 10, + }, + } + +export const POOLS_AVERAGE_DATA_MOCK: Partial> = + POOLS_AVERAGE_DATA_MOCK_OVERRIDE + ? JSON.parse(POOLS_AVERAGE_DATA_MOCK_OVERRIDE) + : { + [LpTokenProvider.COW_AMM]: { + apy: 0.3, + }, + [LpTokenProvider.UNIV2]: { + apy: 3.1, + }, + [LpTokenProvider.CURVE]: { + apy: 0.4, + }, + [LpTokenProvider.PANCAKE]: { + apy: 0.2, + }, + [LpTokenProvider.SUSHI]: { + apy: 0.41, + }, + } diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/SetupYieldAmountsFromUrlUpdater.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/SetupYieldAmountsFromUrlUpdater.tsx new file mode 100644 index 0000000000..f374809e14 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/updaters/SetupYieldAmountsFromUrlUpdater.tsx @@ -0,0 +1,9 @@ +import { useSetupTradeAmountsFromUrl } from 'modules/trade' + +const params = { onlySell: true } + +export function SetupYieldAmountsFromUrlUpdater() { + useSetupTradeAmountsFromUrl(params) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx index e46923e292..1e22910c5e 100644 --- a/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx @@ -5,6 +5,7 @@ import { AppDataUpdater } from 'modules/appData' import { useSetTradeQuoteParams } from 'modules/tradeQuote' import { QuoteObserverUpdater } from './QuoteObserverUpdater' +import { SetupYieldAmountsFromUrlUpdater } from './SetupYieldAmountsFromUrlUpdater' import { useFillYieldDerivedState, useYieldDerivedState } from '../hooks/useYieldDerivedState' @@ -16,6 +17,7 @@ export function YieldUpdaters() { return ( <> + diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts index 267810206f..8bc4d0fd42 100644 --- a/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts @@ -5,14 +5,14 @@ import { useEffect, useMemo } from 'react' import { ERC_20_INTERFACE } from '@cowprotocol/abis' import { usePrevious } from '@cowprotocol/common-hooks' import { getIsNativeToken } from '@cowprotocol/common-utils' -import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' +import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { MultiCallOptions, useMultipleContractSingleData } from '@cowprotocol/multicall' import { BigNumber } from '@ethersproject/bignumber' import { SWRConfiguration } from 'swr' import { AllowancesState, allowancesFullState } from '../state/allowancesAtom' -import { balancesAtom, BalancesState } from '../state/balancesAtom' +import { balancesAtom, balancesCacheAtom, BalancesState } from '../state/balancesAtom' const MULTICALL_OPTIONS = {} @@ -24,6 +24,7 @@ export interface PersistBalancesAndAllowancesParams { allowancesSwrConfig: SWRConfiguration setLoadingState?: boolean multicallOptions?: MultiCallOptions + onBalancesLoaded?(loaded: boolean): void } export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowancesParams) { @@ -35,11 +36,13 @@ export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowa balancesSwrConfig, allowancesSwrConfig, multicallOptions = MULTICALL_OPTIONS, + onBalancesLoaded, } = params const prevAccount = usePrevious(account) const setBalances = useSetAtom(balancesAtom) const setAllowances = useSetAtom(allowancesFullState) + const setBalancesCache = useSetAtom(balancesCacheAtom) const resetBalances = useResetAtom(balancesAtom) const resetAllowances = useResetAtom(allowancesFullState) @@ -92,6 +95,8 @@ export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowa return acc }, {}) + onBalancesLoaded?.(true) + setBalances((state) => { return { ...state, @@ -99,7 +104,7 @@ export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowa ...(setLoadingState ? { isLoading: false } : {}), } }) - }, [balances, tokenAddresses, setBalances, chainId, setLoadingState]) + }, [balances, tokenAddresses, setBalances, chainId, setLoadingState, onBalancesLoaded]) // Set allowances to the store useEffect(() => { @@ -124,6 +129,8 @@ export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowa if (prevAccount && prevAccount !== account) { resetBalances() resetAllowances() + setBalancesCache(mapSupportedNetworks({})) + onBalancesLoaded?.(false) } - }, [account, prevAccount, resetAllowances, resetBalances]) + }, [account, prevAccount, resetAllowances, resetBalances, setBalancesCache, onBalancesLoaded]) } diff --git a/libs/common-const/src/types.ts b/libs/common-const/src/types.ts index 60e82423ac..05a64c8e4e 100644 --- a/libs/common-const/src/types.ts +++ b/libs/common-const/src/types.ts @@ -1,4 +1,4 @@ -import { TokenInfo } from '@cowprotocol/types' +import { LpTokenProvider, TokenInfo } from '@cowprotocol/types' import { Token } from '@uniswap/sdk-core' const emptyTokens = [] as string[] @@ -22,10 +22,10 @@ export class TokenWithLogo extends Token { } export class LpToken extends TokenWithLogo { - static fromTokenToLp(token: Token | TokenInfo, isCowAmm: boolean): LpToken { + static fromTokenToLp(token: Token | TokenInfo, lpTokenProvider?: LpTokenProvider): LpToken { return new LpToken( token instanceof Token ? emptyTokens : token.tokens || emptyTokens, - isCowAmm, + lpTokenProvider, token.chainId, token.address, token.decimals, @@ -36,7 +36,7 @@ export class LpToken extends TokenWithLogo { constructor( public tokens: string[], - public isCowAmm: boolean, + public lpTokenProvider: LpTokenProvider | undefined, chainId: number, address: string, decimals: number, diff --git a/libs/tokens/src/const/lpTokensList.json b/libs/tokens/src/const/lpTokensList.json index bffe99e65c..928f6b1ed3 100644 --- a/libs/tokens/src/const/lpTokensList.json +++ b/libs/tokens/src/const/lpTokensList.json @@ -1,32 +1,32 @@ [ { "priority": 100, - "category": "COW_AMM_LP", + "lpTokenProvider": "COW_AMM", "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/cow-amm.json" }, { "priority": 101, - "category": "LP", + "lpTokenProvider": "UNIV2", "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/uniswapv2.json" }, { "priority": 102, - "category": "LP", + "lpTokenProvider": "CURVE", "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/curve.json" }, { "priority": 103, - "category": "LP", + "lpTokenProvider": "BALANCERV2", "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/balancerv2.json" }, { "priority": 104, - "category": "LP", + "lpTokenProvider": "SUSHI", "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/sushiswap.json" }, { "priority": 105, - "category": "LP", + "lpTokenProvider": "PANCAKE", "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/055c8ccebe59e91874baf8600403a487ad33e93d/src/public/lp-tokens/pancakeswap.json" } ] diff --git a/libs/tokens/src/hooks/tokens/useAllLpTokens.ts b/libs/tokens/src/hooks/tokens/useAllLpTokens.ts index 2e53911261..323ac369e1 100644 --- a/libs/tokens/src/hooks/tokens/useAllLpTokens.ts +++ b/libs/tokens/src/hooks/tokens/useAllLpTokens.ts @@ -1,6 +1,7 @@ import { useAtomValue } from 'jotai/index' import { LpToken, SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' +import { LpTokenProvider } from '@cowprotocol/types' import useSWR from 'swr' @@ -26,7 +27,7 @@ export function useAllLpTokens(categories: TokenListCategory[] | null): LpToken[ return allTokens.filter((token) => { const isLp = token instanceof LpToken - return isLp ? (selectOnlyCoWAmm ? token.isCowAmm : true) : false + return isLp ? (selectOnlyCoWAmm ? token.lpTokenProvider === LpTokenProvider.COW_AMM : true) : false }) as LpToken[] }, { ...SWR_NO_REFRESH_OPTIONS, fallbackData }, diff --git a/libs/tokens/src/services/fetchTokenList.ts b/libs/tokens/src/services/fetchTokenList.ts index 80fb2db075..bfddca1363 100644 --- a/libs/tokens/src/services/fetchTokenList.ts +++ b/libs/tokens/src/services/fetchTokenList.ts @@ -87,7 +87,7 @@ function listStateFromSourceConfig(result: ListState, list: ListSourceConfig): L ...result, priority: list.priority, source: list.source, - category: list.category + lpTokenProvider: list.lpTokenProvider, } } diff --git a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts index c5a9205a7c..7b549a2a51 100644 --- a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts +++ b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts @@ -56,7 +56,7 @@ export const allListsSourcesAtom = atom((get) => { // Lists states export const listsStatesByChainAtom = atomWithStorage( - 'allTokenListsInfoAtom:v3', + 'allTokenListsInfoAtom:v4', mapSupportedNetworks({}), getJotaiMergerStorage(), ) diff --git a/libs/tokens/src/state/tokens/allTokensAtom.ts b/libs/tokens/src/state/tokens/allTokensAtom.ts index d122bc6f30..2254986fd2 100644 --- a/libs/tokens/src/state/tokens/allTokensAtom.ts +++ b/libs/tokens/src/state/tokens/allTokensAtom.ts @@ -6,7 +6,7 @@ import { TokenInfo } from '@cowprotocol/types' import { favoriteTokensAtom } from './favoriteTokensAtom' import { userAddedTokensAtom } from './userAddedTokensAtom' -import { TokenListCategory, TokensMap } from '../../types' +import { TokensMap } from '../../types' import { lowerCaseTokensMap } from '../../utils/lowerCaseTokensMap' import { parseTokenInfo } from '../../utils/parseTokenInfo' import { tokenMapToListWithLogo } from '../../utils/tokenMapToListWithLogo' @@ -34,21 +34,15 @@ const tokensStateAtom = atom((get) => { return listsStatesList.reduce( (acc, list) => { const isListEnabled = listsEnabledState[list.source] - + const lpTokenProvider = list.lpTokenProvider list.list.tokens.forEach((token) => { - const category = list.category || TokenListCategory.ERC20 const tokenInfo = parseTokenInfo(chainId, token) const tokenAddressKey = tokenInfo?.address.toLowerCase() if (!tokenInfo || !tokenAddressKey) return - if (category === TokenListCategory.LP) { - tokenInfo.isLpToken = true - } - - if (category === TokenListCategory.COW_AMM_LP) { - tokenInfo.isLpToken = true - tokenInfo.isCoWAmmToken = true + if (lpTokenProvider) { + tokenInfo.lpTokenProvider = lpTokenProvider } if (isListEnabled) { @@ -91,7 +85,7 @@ export const activeTokensAtom = atom((get) => { ? Object.keys(tokensMap.inactiveTokens).reduce((acc, key) => { const token = tokensMap.inactiveTokens[key] - if (token.isLpToken) { + if (token.lpTokenProvider) { acc[key] = token } diff --git a/libs/tokens/src/types.ts b/libs/tokens/src/types.ts index b07e183a51..1fa4982119 100644 --- a/libs/tokens/src/types.ts +++ b/libs/tokens/src/types.ts @@ -1,5 +1,5 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { TokenInfo } from '@cowprotocol/types' +import { LpTokenProvider, TokenInfo } from '@cowprotocol/types' import type { TokenList as UniTokenList } from '@uniswap/token-lists' export enum TokenListCategory { @@ -15,7 +15,7 @@ export type ListSourceConfig = { widgetAppCode?: string priority?: number enabledByDefault?: boolean - category?: TokenListCategory + lpTokenProvider?: LpTokenProvider source: string } @@ -27,7 +27,7 @@ export type UnsupportedTokensState = { [tokenAddress: string]: { dateAdded: numb export type ListsEnabledState = { [listId: string]: boolean | undefined } -export interface ListState extends Pick { +export interface ListState extends Pick { list: UniTokenList isEnabled?: boolean } diff --git a/libs/tokens/src/updaters/TokensListsUpdater/index.ts b/libs/tokens/src/updaters/TokensListsUpdater/index.ts index d14f61ae10..4c89c921b7 100644 --- a/libs/tokens/src/updaters/TokensListsUpdater/index.ts +++ b/libs/tokens/src/updaters/TokensListsUpdater/index.ts @@ -19,7 +19,7 @@ import { ListState } from '../../types' const { atom: lastUpdateTimeAtom, updateAtom: updateLastUpdateTimeAtom } = atomWithPartialUpdate( atomWithStorage>( - 'tokens:lastUpdateTimeAtom:v2', + 'tokens:lastUpdateTimeAtom:v3', mapSupportedNetworks(0), getJotaiMergerStorage(), ), diff --git a/libs/tokens/src/utils/parseTokenInfo.ts b/libs/tokens/src/utils/parseTokenInfo.ts index 2c9f2a67a1..34c82cc60c 100644 --- a/libs/tokens/src/utils/parseTokenInfo.ts +++ b/libs/tokens/src/utils/parseTokenInfo.ts @@ -21,6 +21,6 @@ export function parseTokenInfo(chainId: SupportedChainId, token: Erc20TokenInfo) return { ...token, address: tokenAddress, - ...(lpTokens ? { tokens: lpTokens.split(',') } : undefined), + ...(lpTokens ? { tokens: lpTokens.split(',').map((t) => t.toLowerCase()) } : undefined), } } diff --git a/libs/tokens/src/utils/tokenMapToListWithLogo.ts b/libs/tokens/src/utils/tokenMapToListWithLogo.ts index b3c7e77081..06528aab87 100644 --- a/libs/tokens/src/utils/tokenMapToListWithLogo.ts +++ b/libs/tokens/src/utils/tokenMapToListWithLogo.ts @@ -10,8 +10,8 @@ export function tokenMapToListWithLogo(tokenMap: TokensMap, chainId: number): To .filter((token) => token.chainId === chainId) .sort((a, b) => a.symbol.localeCompare(b.symbol)) .map((token) => - token.isLpToken - ? LpToken.fromTokenToLp(token, !!token.isCoWAmmToken) + token.lpTokenProvider + ? LpToken.fromTokenToLp(token, token.lpTokenProvider) : TokenWithLogo.fromToken(token, token.logoURI), ) } diff --git a/libs/types/src/common.ts b/libs/types/src/common.ts index 1b3e2ae88b..dfb4987fa5 100644 --- a/libs/types/src/common.ts +++ b/libs/types/src/common.ts @@ -24,6 +24,14 @@ export type TokenInfo = { symbol: string logoURI?: string tokens?: string[] - isLpToken?: boolean - isCoWAmmToken?: boolean + lpTokenProvider?: LpTokenProvider +} + +export enum LpTokenProvider { + COW_AMM = 'COW_AMM', + UNIV2 = 'UNIV2', + CURVE = 'CURVE', + BALANCERV2 = 'BALANCERV2', + SUSHI = 'SUSHI', + PANCAKE = 'PANCAKE', } From 562d0207d1acf4e1735c4b3f629ff63dd65d3725 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Mon, 4 Nov 2024 15:27:00 +0500 Subject: [PATCH 076/116] feat(yield): display pools info in widget (#5046) --- .../src/common/constants/routes.ts | 1 + .../common/containers/CoWAmmBanner/index.tsx | 36 ++-- .../common/pure/CoWAmmBannerContent/index.tsx | 10 +- .../CurrencyInputPanel/CurrencyInputPanel.tsx | 11 +- .../common/pure/CurrencyInputPanel/types.ts | 3 + .../pure/CurrencySelectButton/index.tsx | 27 ++- .../pure/CurrencySelectButton/styled.tsx | 25 ++- .../application/containers/App/Updaters.tsx | 3 +- .../containers/SelectTokenWidget/index.tsx | 6 +- .../tokensList/pure/LpTokenLists/index.tsx | 13 +- .../pure/SelectTokenModal/index.tsx | 7 +- .../pure/TokensVirtualList/index.tsx | 15 +- .../TradeWidget/TradeWidgetForm.tsx | 6 + .../trade/containers/TradeWidget/types.ts | 2 + .../pure/TradeFormBlankButton/index.tsx | 23 ++- .../pure/TradeFormButtons/index.tsx | 12 +- .../modules/volumeFee/state/cowswapFeeAtom.ts | 9 +- .../yield/containers/TradeButtons/index.tsx | 45 ++++- .../TradeButtons/yieldTradeButtonsMap.tsx | 31 +++ .../yield/containers/YieldWidget/elements.tsx | 48 +++++ .../yield/containers/YieldWidget/index.tsx | 68 ++++++- .../yield/containers/YieldWidget/styled.tsx | 43 ++++ .../modules/yield/hooks/useVampireAttack.ts | 33 +++ .../modules/yield/hooks/useYieldFormState.ts | 26 +++ .../src/modules/yield/lpPageLinks.ts | 42 ++++ .../src/modules/yield/pure/PoolApyPreview.tsx | 24 +++ .../yield/pure/TargetPoolPreviewInfo.tsx | 73 +++++++ .../src/modules/yield/shared.ts | 2 + .../src/modules/yield/state/poolsInfoAtom.ts | 18 +- .../modules/yield/state/vampireAttackAtom.ts | 5 + .../src/modules/yield/types.ts | 24 +++ .../updaters/PoolsInfoUpdater/mockPoolInfo.ts | 190 +++++++++++++++++- .../yield/updaters/VampireAttackUpdater.tsx | 113 +++++++++++ libs/tokens/src/const/lpTokensList.json | 12 +- .../src/updaters/TokensListsUpdater/index.ts | 2 +- libs/ui/src/containers/InlineBanner/index.tsx | 8 +- libs/ui/src/pure/ProductLogo/index.tsx | 3 + 37 files changed, 930 insertions(+), 89 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/yieldTradeButtonsMap.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/elements.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/styled.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useVampireAttack.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/hooks/useYieldFormState.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/lpPageLinks.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/pure/PoolApyPreview.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/pure/TargetPoolPreviewInfo.tsx create mode 100644 apps/cowswap-frontend/src/modules/yield/state/vampireAttackAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/types.ts create mode 100644 apps/cowswap-frontend/src/modules/yield/updaters/VampireAttackUpdater.tsx diff --git a/apps/cowswap-frontend/src/common/constants/routes.ts b/apps/cowswap-frontend/src/common/constants/routes.ts index 37045e433f..87f1d6f144 100644 --- a/apps/cowswap-frontend/src/common/constants/routes.ts +++ b/apps/cowswap-frontend/src/common/constants/routes.ts @@ -62,4 +62,5 @@ export const YIELD_MENU_ITEM = { label: 'Yield', fullLabel: 'Yield', description: 'Provide liquidity', + badge: 'New', } diff --git a/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx index 994d4f8869..d0895f6848 100644 --- a/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx @@ -11,8 +11,9 @@ import { useIsDarkMode } from 'legacy/state/user/hooks' import { cowAnalytics } from 'modules/analytics' import { useTradeNavigate } from 'modules/trade' - -import { useVampireAttack } from './useVampireAttack' +import { getDefaultTradeRawState } from 'modules/trade/types/TradeRawState' +import { useYieldRawState } from 'modules/yield' +import { useVampireAttack, useVampireAttackFirstTarget } from 'modules/yield/shared' import { Routes } from '../../constants/routes' import { useIsProviderNetworkUnsupported } from '../../hooks/useIsProviderNetworkUnsupported' @@ -30,17 +31,24 @@ export function CoWAmmBanner({ isTokenSelectorView }: BannerProps) { const vampireAttackContext = useVampireAttack() const tokensByAddress = useTokensByAddressMap() const tradeNavigate = useTradeNavigate() + const vampireAttackFirstTarget = useVampireAttackFirstTarget() + const isSmartContractWallet = useIsSmartContractWallet() + const yieldState = useYieldRawState() const key = isTokenSelectorView ? 'tokenSelector' : 'global' const handleCTAClick = useCallback(() => { - const superiorAlternative = vampireAttackContext?.superiorAlternatives?.[0] - const alternative = vampireAttackContext?.alternatives?.[0] - const target = superiorAlternative || alternative - - const targetTrade = { - inputCurrencyId: target?.token.address || null, - outputCurrencyId: target?.alternative.address || null, - } + const target = vampireAttackFirstTarget?.target + const defaulTradeState = getDefaultTradeRawState(chainId) + + const targetTrade = target + ? { + inputCurrencyId: target.token.address || null, + outputCurrencyId: target.alternative.address || null, + } + : { + inputCurrencyId: yieldState.inputCurrencyId || defaulTradeState.inputCurrencyId, + outputCurrencyId: yieldState.outputCurrencyId || defaulTradeState.outputCurrencyId, + } const targetTradeParams = { amount: target @@ -55,7 +63,7 @@ export function CoWAmmBanner({ isTokenSelectorView }: BannerProps) { }) tradeNavigate(chainId, targetTrade, targetTradeParams, Routes.YIELD) - }, [key, chainId, vampireAttackContext, tradeNavigate]) + }, [key, chainId, yieldState, vampireAttackFirstTarget, tradeNavigate]) const handleClose = useCallback(() => { cowAnalytics.sendEvent({ @@ -64,12 +72,10 @@ export function CoWAmmBanner({ isTokenSelectorView }: BannerProps) { }) }, [key]) - const bannerId = `cow_amm_banner_2024_va_${key}` - - const isSmartContractWallet = useIsSmartContractWallet() - if (isInjectedWidgetMode || !account || isChainIdUnsupported || !vampireAttackContext) return null + const bannerId = `cow_amm_banner_2024_va_${key}${isTokenSelectorView ? account : ''}` + return ClosableBanner(bannerId, (close) => ( 0 + const betterAlternativeApyDiff = firstItemWithBetterCowAmm + ? firstItemWithBetterCowAmm.alternativePoolInfo.apy - firstItemWithBetterCowAmm.tokenPoolInfo.apy + : undefined const worseThanCoWAmmProviders = useMemo(() => { return superiorAlternatives?.reduce((acc, item) => { @@ -104,8 +108,8 @@ export function CoWAmmBannerContent({ minFontSize={isTokenSelectorView ? 35 : isMobile ? 40 : isCowAmmAverageBetter ? 60 : 80} maxFontSize={isTokenSelectorView ? 65 : isMobile ? 50 : isCowAmmAverageBetter ? 60 : 80} > - {firstItemWithBetterCowAmm - ? `+${firstItemWithBetterCowAmm.alternativePoolInfo.apy.toFixed(1)}%` + {firstItemWithBetterCowAmm && betterAlternativeApyDiff && betterAlternativeApyDiff > 0 + ? `+${betterAlternativeApyDiff.toFixed(1)}%` : isCowAmmAverageBetter ? `+${averageApyDiff}%` : `${cowAmmLpTokensCount}+`} diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx index 8b0050c278..9020f7c004 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' import { formatInputAmount, getIsNativeToken } from '@cowprotocol/common-utils' @@ -34,6 +34,7 @@ export interface CurrencyInputPanelProps extends Partial { disabled?: boolean inputDisabled?: boolean tokenSelectorDisabled?: boolean + displayTokenName?: boolean inputTooltip?: string showSetMax?: boolean maxBalance?: CurrencyAmount | undefined @@ -49,6 +50,8 @@ export interface CurrencyInputPanelProps extends Partial { onCurrencySelection: (currency: Currency) => void, ): void topLabel?: string + topContent?: ReactNode + customSelectTokenButton?: ReactNode } export function CurrencyInputPanel(props: CurrencyInputPanelProps) { @@ -63,6 +66,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) { maxBalance, inputDisabled = false, tokenSelectorDisabled = false, + displayTokenName = false, inputTooltip, onUserInput, allowsOffchainSigning, @@ -76,6 +80,8 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) { }, }, topLabel, + topContent, + customSelectTokenButton, } = props const { field, currency, balance, fiatAmount, amount, isIndependent, receiveAmountInfo } = currencyInfo @@ -155,6 +161,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) { > {topLabel && {topLabel}} + {topContent}
    diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/types.ts b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/types.ts index 63b4eeda1e..076ecfb355 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/types.ts +++ b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/types.ts @@ -1,3 +1,5 @@ +import { ReactNode } from 'react' + import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Field } from 'legacy/state/types' @@ -13,4 +15,5 @@ export interface CurrencyInfo { receiveAmountInfo: ReceiveAmountInfo | null balance: CurrencyAmount | null fiatAmount: CurrencyAmount | null + topContent?: ReactNode } diff --git a/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/index.tsx b/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/index.tsx index a4cf28e4a5..52ef6d8156 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/index.tsx @@ -1,23 +1,30 @@ +import { ReactNode } from 'react' + import { TokenLogo } from '@cowprotocol/tokens' -import { TokenSymbol } from '@cowprotocol/ui' +import { TokenName } from '@cowprotocol/ui' import { Currency } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' import { Nullish } from 'types' import * as styledEl from './styled' +import { CurrencyName, StyledTokenSymbol } from './styled' export interface CurrencySelectButtonProps { currency?: Nullish loading: boolean readonlyMode?: boolean + displayTokenName?: boolean onClick?(): void + customSelectTokenButton?: ReactNode } export function CurrencySelectButton(props: CurrencySelectButtonProps) { - const { currency, onClick, loading, readonlyMode = false } = props + const { currency, onClick, loading, readonlyMode = false, displayTokenName = false, customSelectTokenButton } = props const $stubbed = !currency || false + if (!currency && customSelectTokenButton) return
    {customSelectTokenButton}
    + return ( - {currency ? :
    } + {currency ? :
    } - {currency ? : Select a token} + {currency ? ( + <> + + {displayTokenName && ( + + + + )} + + ) : ( + Select a token + )} {readonlyMode ? null : $stubbed ? : }
    diff --git a/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/styled.tsx b/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/styled.tsx index c60342723d..98674cc438 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/styled.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/styled.tsx @@ -1,8 +1,14 @@ import DropDown from '@cowprotocol/assets/images/dropdown.svg?react' -import { Media, UI } from '@cowprotocol/ui' +import { Media, TokenSymbol, UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' +export const CurrencyName = styled.div` + font-size: 12px; + color: var(${UI.COLOR_TEXT_OPACITY_70}); + transition: color var(${UI.ANIMATION_DURATION}) ease-in-out; +` + export const ArrowDown = styled((props) => )<{ $stubbed?: boolean }>` margin: 0 3px; width: 12px; @@ -21,7 +27,16 @@ export const ArrowDown = styled((props) => )<{ $stubbed?: } ` -export const CurrencySelectWrapper = styled.button<{ isLoading: boolean; $stubbed: boolean; readonlyMode: boolean }>` +export const StyledTokenSymbol = styled(TokenSymbol)<{ displayTokenName: boolean }>` + font-size: ${({ displayTokenName }) => (displayTokenName ? '15px' : null)}; +` + +export const CurrencySelectWrapper = styled.button<{ + isLoading: boolean + $stubbed: boolean + readonlyMode: boolean + displayTokenName: boolean +}>` display: flex; justify-content: space-between; align-items: center; @@ -34,10 +49,10 @@ export const CurrencySelectWrapper = styled.button<{ isLoading: boolean; $stubbe box-shadow: var(${UI.BOX_SHADOW_2}); opacity: ${({ isLoading }) => (isLoading ? 0.6 : 1)}; border-radius: var(${UI.BORDER_RADIUS_NORMAL}); - padding: 6px; ${({ readonlyMode }) => (readonlyMode ? 'padding-right: 10px;' : '')} transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out; max-width: 190px; + padding: ${({ displayTokenName }) => (displayTokenName ? '8px' : '6px')}; &:hover { color: ${({ $stubbed, readonlyMode }) => @@ -50,6 +65,10 @@ export const CurrencySelectWrapper = styled.button<{ isLoading: boolean; $stubbe transition: stroke var(${UI.ANIMATION_DURATION}) ease-in-out; stroke: ${({ $stubbed }) => ($stubbed ? 'currentColor' : `var(${UI.COLOR_BUTTON_TEXT})`)}; } + + &:hover ${CurrencyName} { + color: var(${UI.COLOR_BUTTON_TEXT}); + } ` export const CurrencySymbol = styled.div<{ $stubbed: boolean }>` diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index 3be8417c9f..e9f3d981ad 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -15,7 +15,7 @@ import { EthFlowDeadlineUpdater } from 'modules/swap/state/EthFlow/updaters' import { useOnTokenListAddingError } from 'modules/tokensList' import { TradeType, useTradeTypeInfo } from 'modules/trade' import { UsdPricesUpdater } from 'modules/usdAmount' -import { LpTokensWithBalancesUpdater, PoolsInfoUpdater } from 'modules/yield/shared' +import { LpTokensWithBalancesUpdater, PoolsInfoUpdater, VampireAttackUpdater } from 'modules/yield/shared' import { ProgressBarV2ExecutingOrdersUpdater } from 'common/hooks/orderProgressBarV2' import { TotalSurplusUpdater } from 'common/state/totalSurplusState' @@ -88,6 +88,7 @@ export function Updaters() { + ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index 5d4edd86c9..87303e3074 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' import { useTokensBalances } from '@cowprotocol/balances-and-allowances' -import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' +import { TokenWithLogo } from '@cowprotocol/common-const' import { isInjectedWidget } from '@cowprotocol/common-utils' import { ListState, @@ -63,7 +63,7 @@ export function SelectTokenWidget({ displayLpTokenLists }: SelectTokenWidgetProp const { count: lpTokensWithBalancesCount } = useLpTokensWithBalances() const [isManageWidgetOpen, setIsManageWidgetOpen] = useState(false) - const isSellErc20Selected = field === Field.OUTPUT && !(oppositeToken instanceof LpToken) + const disableErc20 = field === Field.OUTPUT && !!displayLpTokenLists const tokenListCategoryState = useState( getDefaultTokenListCategories(field, oppositeToken, lpTokensWithBalancesCount), @@ -201,7 +201,7 @@ export function SelectTokenWidget({ displayLpTokenLists }: SelectTokenWidgetProp hideFavoriteTokensTooltip={isInjectedWidgetMode} openPoolPage={openPoolPage} tokenListCategoryState={tokenListCategoryState} - disableErc20={isSellErc20Selected} + disableErc20={disableErc20} account={account} /> ) diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx index 2fc067843c..c5faf698ba 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/LpTokenLists/index.tsx @@ -1,4 +1,4 @@ -import { MouseEventHandler, useCallback } from 'react' +import { MouseEventHandler, ReactNode, useCallback } from 'react' import { BalancesState } from '@cowprotocol/balances-and-allowances' import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' @@ -38,7 +38,7 @@ const LoadingElement = ( ) -const MobileCardRowItem: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( +const MobileCardRowItem: React.FC<{ label: string; value: ReactNode }> = ({ label, value }) => ( {label}: {value} @@ -56,6 +56,7 @@ interface LpTokenListsProps { } export function LpTokenLists({ + account, onSelectToken, openPoolPage, lpTokens, @@ -94,11 +95,11 @@ export function LpTokenLists({ ) - const BalanceDisplay = balanceAmount ? : LoadingElement + const BalanceDisplay = balanceAmount ? : account ? LoadingElement : null if (isMobile) { return ( - + onSelectToken(token)}> {commonContent} @@ -116,7 +117,7 @@ export function LpTokenLists({ } return ( - onSelectToken(token)}> + onSelectToken(token)}> {commonContent} {BalanceDisplay} {info?.apy ? `${info.apy}%` : ''} @@ -126,7 +127,7 @@ export function LpTokenLists({ ) }, - [balances, onSelectToken, poolsInfo, openPoolPage, isMobile], + [balances, onSelectToken, poolsInfo, openPoolPage, isMobile, account], ) return ( diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index c4c294dac6..2e626a6994 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -88,7 +88,12 @@ export function SelectTokenModal(props: SelectTokenModalProps) { {inputValue.trim() ? ( ) : ( - + )}
    diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index 20d5cd3979..81575c9bc3 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -14,11 +14,20 @@ import { TokenListItem } from '../TokenListItem' export interface TokensVirtualListProps extends SelectTokenContext { allTokens: TokenWithLogo[] account: string | undefined + displayLpTokenLists?: boolean } export function TokensVirtualList(props: TokensVirtualListProps) { - const { allTokens, selectedToken, balancesState, onSelectToken, unsupportedTokens, permitCompatibleTokens, account } = - props + const { + allTokens, + selectedToken, + balancesState, + onSelectToken, + unsupportedTokens, + permitCompatibleTokens, + account, + displayLpTokenLists, + } = props const { values: balances } = balancesState const isWalletConnected = !!account @@ -50,7 +59,7 @@ export function TokensVirtualList(props: TokensVirtualListProps) { return ( - + {displayLpTokenLists ? null : } ) } diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index 0c27d6c16b..e4b10ac01e 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -65,6 +65,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { recipient, hideTradeWarnings, enableSmartSlippage, + displayTokenName = false, isMarketOrderWidget = false, } = params @@ -128,6 +129,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { onUserInput, allowsOffchainSigning, tokenSelectorDisabled: alternativeOrderModalVisible, + displayTokenName, } const openSellTokenSelect = useCallback( @@ -184,7 +186,9 @@ export function TradeWidgetForm(props: TradeWidgetProps) { showSetMax={showSetMax} maxBalance={maxBalance} topLabel={isWrapOrUnwrap ? undefined : inputCurrencyInfo.label} + topContent={inputCurrencyInfo.topContent} openTokenSelectWidget={openSellTokenSelect} + customSelectTokenButton={params.customSelectTokenButton} {...currencyInputCommonProps} />
    @@ -210,7 +214,9 @@ export function TradeWidgetForm(props: TradeWidgetProps) { currencyInfo={outputCurrencyInfo} priceImpactParams={!disablePriceImpact ? priceImpact : undefined} topLabel={isWrapOrUnwrap ? undefined : outputCurrencyInfo.label} + topContent={outputCurrencyInfo.topContent} openTokenSelectWidget={openBuyTokenSelect} + customSelectTokenButton={params.customSelectTokenButton} {...currencyInputCommonProps} />
    diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts index fbfdaceadd..0cf4606b84 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts @@ -29,6 +29,8 @@ interface TradeWidgetParams { hideTradeWarnings?: boolean enableSmartSlippage?: boolean isMarketOrderWidget?: boolean + displayTokenName?: boolean + customSelectTokenButton?: ReactNode } export interface TradeWidgetSlots { diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormBlankButton/index.tsx b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormBlankButton/index.tsx index 7af105f245..a1aec1b088 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormBlankButton/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormBlankButton/index.tsx @@ -24,7 +24,9 @@ const ActionButton = styled.button<{ hasLongText$: boolean }>` cursor: pointer; min-height: 58px; text-align: center; - transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out; + transition: + background var(${UI.ANIMATION_DURATION}) ease-in-out, + color var(${UI.ANIMATION_DURATION}) ease-in-out; border: none; outline: none; @@ -49,9 +51,17 @@ export interface TradeFormPrimaryButtonProps { loading?: boolean id?: string onClick?(): void + className?: string } -export function TradeFormBlankButton({ onClick, children, disabled, loading, id }: TradeFormPrimaryButtonProps) { +export function TradeFormBlankButton({ + onClick, + children, + disabled, + loading, + id, + className, +}: TradeFormPrimaryButtonProps) { const ref = useRef(null) const [hasLongText, setHasLongText] = useState(false) const [justClicked, setJustClicked] = useState(false) @@ -89,7 +99,14 @@ export function TradeFormBlankButton({ onClick, children, disabled, loading, id }, [justClicked]) return ( - + {showLoader ? ( <> Confirm with your wallet diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/index.tsx b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/index.tsx index 8be512c666..d4cd7b0d1a 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/index.tsx @@ -11,16 +11,22 @@ export interface TradeFormButtonsProps { validation: TradeFormValidation | null context: TradeFormButtonContext confirmText: string + className?: string isDisabled?: boolean } export function TradeFormButtons(props: TradeFormButtonsProps) { - const { validation, context, isDisabled, confirmText } = props + const { validation, context, isDisabled, confirmText, className } = props // When there are no validation errors if (validation === null) { return ( - context.confirmTrade()}> + context.confirmTrade()} + > {confirmText} ) @@ -33,7 +39,7 @@ export function TradeFormButtons(props: TradeFormButtonsProps) { } return ( - + {buttonFactory.text} ) diff --git a/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts b/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts index 904fbd982e..d613237a71 100644 --- a/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts +++ b/apps/cowswap-frontend/src/modules/volumeFee/state/cowswapFeeAtom.ts @@ -1,11 +1,11 @@ import { atom } from 'jotai' -import { STABLECOINS } from '@cowprotocol/common-const' +import { STABLECOINS, LpToken } from '@cowprotocol/common-const' import { getCurrencyAddress, isInjectedWidget } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { walletInfoAtom } from '@cowprotocol/wallet' -import { derivedTradeStateAtom } from 'modules/trade' +import { derivedTradeStateAtom, TradeType, tradeTypeAtom } from 'modules/trade' import { featureFlagsAtom } from 'common/state/featureFlagsState' @@ -27,6 +27,8 @@ const COWSWAP_VOLUME_FEES: Record = { export const cowSwapFeeAtom = atom((get) => { const { chainId, account } = get(walletInfoAtom) const tradeState = get(derivedTradeStateAtom) + const tradeTypeState = get(tradeTypeAtom) + const isYieldWidget = tradeTypeState?.tradeType === TradeType.YIELD const featureFlags = get(featureFlagsAtom) const { inputCurrency, outputCurrency } = tradeState || {} @@ -37,6 +39,9 @@ export const cowSwapFeeAtom = atom((get) => { // Don't user it when the currencies are not set if (!inputCurrency || !outputCurrency) return null + // No fee for Yield widget and LP tokens + if (isYieldWidget || inputCurrency instanceof LpToken || outputCurrency instanceof LpToken) return null + // Don't use it when on arb1 and shouldn't apply fee based on percentage if (chainId === SupportedChainId.ARBITRUM_ONE && !shouldApplyFee(account, featureFlags.arb1CowSwapFeePercentage)) return null diff --git a/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx index 5dc96fefef..a62024880e 100644 --- a/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx @@ -1,30 +1,63 @@ +import React from 'react' + +import { UI } from '@cowprotocol/ui' + +import { Trans } from '@lingui/macro' +import styled from 'styled-components/macro' + import { useIsNoImpactWarningAccepted, useTradeConfirmActions } from 'modules/trade' -import { TradeFormButtons, useGetTradeFormValidation, useTradeFormButtonContext } from 'modules/tradeFormValidation' +import { + TradeFormBlankButton, + TradeFormButtons, + useGetTradeFormValidation, + useTradeFormButtonContext, +} from 'modules/tradeFormValidation' import { useHighFeeWarning } from 'modules/tradeWidgetAddons' -const CONFIRM_TEXT = 'Swap' +import { yieldTradeButtonsMap } from './yieldTradeButtonsMap' + +import { useYieldFormState } from '../../hooks/useYieldFormState' + +const StyledTradeFormButtons = styled((props) => )<{ active: boolean }>` + background: ${({ active }) => (active ? `var(${UI.COLOR_COWAMM_DARK_GREEN})` : null)}; + color: ${({ active }) => (active ? `var(${UI.COLOR_COWAMM_LIGHT_GREEN})` : null)}; +` interface TradeButtonsProps { isTradeContextReady: boolean + isOutputLpToken: boolean } -export function TradeButtons({ isTradeContextReady }: TradeButtonsProps) { +export function TradeButtons({ isTradeContextReady, isOutputLpToken }: TradeButtonsProps) { const primaryFormValidation = useGetTradeFormValidation() const tradeConfirmActions = useTradeConfirmActions() const { feeWarningAccepted } = useHighFeeWarning() const isNoImpactWarningAccepted = useIsNoImpactWarningAccepted() + const localFormValidation = useYieldFormState() + const confirmText = primaryFormValidation ? 'Swap' : 'Deposit' const confirmTrade = tradeConfirmActions.onOpen - const tradeFormButtonContext = useTradeFormButtonContext(CONFIRM_TEXT, confirmTrade) + const tradeFormButtonContext = useTradeFormButtonContext(confirmText, confirmTrade) const isDisabled = !isTradeContextReady || !feeWarningAccepted || !isNoImpactWarningAccepted if (!tradeFormButtonContext) return null + if (localFormValidation) { + const button = yieldTradeButtonsMap[localFormValidation] + + return ( + + {button.text} + + ) + } + return ( - = { + [YieldFormState.Erc20BuyIsNotAllowed]: { + text: ( + + Swaps not supported{' '} + +
    Use the Swap tab for trades that don't involve an LP token.
    +
    +
    + ), + }, +} diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/elements.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/elements.tsx new file mode 100644 index 0000000000..cc03577e42 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/elements.tsx @@ -0,0 +1,48 @@ +import { LpToken } from '@cowprotocol/common-const' +import { + BannerOrientation, + DismissableInlineBanner, + ExternalLink, + ProductVariant, + TokenSymbol, + UI, +} from '@cowprotocol/ui' + +import { ChevronDown } from 'react-feather' + +import { CoWAmmLogo, SelectPoolBtn } from './styled' + +export const CoWAmmGreenLogo = ( + +) + +export const CoWAmmInlineBanner = ({ token, apyDiff }: { token: LpToken | undefined; apyDiff: number | undefined }) => { + return ( + + Boost Your Yield with One-Click Conversion + + {token && apyDiff && apyDiff > 0 ? ( + <> + Convert your LP tokens into CoW AMM pools and earn up to{' '} + +{apyDiff}% more yield compared to . Or, swap + + ) : ( + 'Swap' + )}{' '} + any token into CoW AMM pools to start benefiting from attractive APRs.{' '} + Learn more + + + ) +} + +export const SelectAPoolButton = ( + + {CoWAmmGreenLogo} Select a pool + +) diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx index 9d395d27c7..1888b6d921 100644 --- a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -1,5 +1,9 @@ -import React, { useCallback } from 'react' -import { ReactNode } from 'react' +import { ReactNode, useCallback, useMemo } from 'react' + +import { LpToken } from '@cowprotocol/common-const' +import { getCurrencyAddress } from '@cowprotocol/common-utils' +import { LpTokenProvider } from '@cowprotocol/types' +import { useWalletInfo } from '@cowprotocol/wallet' import { Field } from 'legacy/state/types' @@ -11,7 +15,7 @@ import { useTradeConfirmState, useTradePriceImpact, } from 'modules/trade' -import { UnlockWidgetScreen, BulletListItem } from 'modules/trade/pure/UnlockWidgetScreen' +import { BulletListItem, UnlockWidgetScreen } from 'modules/trade/pure/UnlockWidgetScreen' import { useHandleSwap } from 'modules/tradeFlow' import { useTradeQuote } from 'modules/tradeQuote' import { SettingsTab, TradeRateDetails } from 'modules/tradeWidgetAddons' @@ -20,6 +24,10 @@ import { useRateInfoParams } from 'common/hooks/useRateInfoParams' import { useSafeMemoObject } from 'common/hooks/useSafeMemo' import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' +import { CoWAmmInlineBanner, SelectAPoolButton } from './elements' + +import { usePoolsInfo } from '../../hooks/usePoolsInfo' +import { useVampireAttackFirstTarget } from '../../hooks/useVampireAttack' import { useYieldDerivedState } from '../../hooks/useYieldDerivedState' import { useYieldDeadlineState, @@ -28,6 +36,8 @@ import { useYieldUnlockState, } from '../../hooks/useYieldSettings' import { useYieldWidgetActions } from '../../hooks/useYieldWidgetActions' +import { PoolApyPreview } from '../../pure/PoolApyPreview' +import { TargetPoolPreviewInfo } from '../../pure/TargetPoolPreviewInfo' import { TradeButtons } from '../TradeButtons' import { Warnings } from '../Warnings' import { YieldConfirmModal } from '../YieldConfirmModal' @@ -48,6 +58,7 @@ const YIELD_UNLOCK_SCREEN = { } export function YieldWidget() { + const { chainId } = useWalletInfo() const { showRecipient } = useYieldSettings() const deadlineState = useYieldDeadlineState() const recipientToggleState = useYieldRecipientToggleState() @@ -57,6 +68,8 @@ export function YieldWidget() { const { isOpen: isConfirmOpen } = useTradeConfirmState() const widgetActions = useYieldWidgetActions() const receiveAmountInfo = useReceiveAmountInfo() + const poolsInfo = usePoolsInfo() + const vampireAttackTarget = useVampireAttackFirstTarget() const { inputCurrency, @@ -71,6 +84,22 @@ export function YieldWidget() { } = useYieldDerivedState() const doTrade = useHandleSwap(useSafeMemoObject({ deadline: deadlineState[0] }), widgetActions) + const inputPoolState = useMemo(() => { + if (!poolsInfo || !inputCurrency) return null + + return poolsInfo[getCurrencyAddress(inputCurrency).toLowerCase()] + }, [inputCurrency, poolsInfo]) + + const outputPoolState = useMemo(() => { + if (!poolsInfo || !outputCurrency) return null + + return poolsInfo[getCurrencyAddress(outputCurrency).toLowerCase()] + }, [outputCurrency, poolsInfo]) + + const isOutputLpToken = Boolean(outputCurrency && outputCurrency instanceof LpToken) + const inputApy = inputPoolState?.info.apy + const outputApy = outputPoolState?.info.apy + const inputCurrencyInfo: CurrencyInfo = { field: Field.INPUT, currency: inputCurrency, @@ -79,7 +108,21 @@ export function YieldWidget() { balance: inputCurrencyBalance, fiatAmount: inputCurrencyFiatAmount, receiveAmountInfo: null, + topContent: inputCurrency && ( + + outputApy : true), + )} + /> + + ), } + const outputCurrencyInfo: CurrencyInfo = { field: Field.OUTPUT, currency: outputCurrency, @@ -88,6 +131,18 @@ export function YieldWidget() { balance: outputCurrencyBalance, fiatAmount: outputCurrencyFiatAmount, receiveAmountInfo, + topContent: outputCurrency ? ( + + inputApy : true), + )} + /> + + ) : null, } const inputCurrencyPreviewInfo = { amount: inputCurrencyInfo.amount, @@ -106,6 +161,7 @@ export function YieldWidget() { const rateInfoParams = useRateInfoParams(inputCurrencyInfo.amount, outputCurrencyInfo.amount) const slots: TradeWidgetSlots = { + topContent: , selectTokenWidget: , settingsWidget: , bottomContent: useCallback( @@ -119,11 +175,11 @@ export function YieldWidget() { /> {tradeWarnings} - + ) }, - [doTrade.contextIsReady, isRateLoading, rateInfoParams, deadlineState], + [doTrade.contextIsReady, isRateLoading, rateInfoParams, deadlineState, isOutputLpToken], ), lockScreen: !isUnlocked ? ( @@ -142,12 +198,14 @@ export function YieldWidget() { const params = { compactView: true, enableSmartSlippage: true, + displayTokenName: true, isMarketOrderWidget: true, recipient, showRecipient, isTradePriceUpdating: isRateLoading, priceImpact, disableQuotePolling: isConfirmOpen, + customSelectTokenButton: SelectAPoolButton, } return ( diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/styled.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/styled.tsx new file mode 100644 index 0000000000..3a72c552cb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/styled.tsx @@ -0,0 +1,43 @@ +import { ProductLogo, UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +export const CoWAmmLogo = styled(ProductLogo)` + --size: 33px; + width: var(--size) !important; + height: var(--size); + border-radius: var(--size); + padding: 6px; + background: var(${UI.COLOR_COWAMM_DARK_GREEN}); + color: var(${UI.COLOR_COWAMM_LIGHTER_GREEN}); +` + +export const SelectPoolBtn = styled.div` + display: flex; + flex-direction: row; + border-radius: 64px; + align-items: center; + padding: 10px; + gap: 10px; + font-weight: 600; + font-size: 15px; + cursor: pointer; + background: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + color: var(${UI.COLOR_COWAMM_DARK_GREEN}); + box-shadow: var(${UI.BOX_SHADOW}); + + ${CoWAmmLogo} { + transition: none !important; + } + + &:hover { + background: var(${UI.COLOR_COWAMM_DARK_GREEN}); + color: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + box-shadow: none; + } + + &:hover ${CoWAmmLogo} { + background: var(${UI.COLOR_COWAMM_LIGHTER_GREEN}); + color: var(${UI.COLOR_COWAMM_DARK_GREEN}); + } +` diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useVampireAttack.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useVampireAttack.ts new file mode 100644 index 0000000000..5dbebadc9b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useVampireAttack.ts @@ -0,0 +1,33 @@ +import { useAtomValue } from 'jotai' +import { useMemo } from 'react' + +import { vampireAttackAtom } from '../state/vampireAttackAtom' + +export function useVampireAttack() { + return useAtomValue(vampireAttackAtom) +} + +export function useVampireAttackFirstTarget() { + const context = useVampireAttack() + + return useMemo(() => { + const superiorAlternative = context?.superiorAlternatives?.[0] + const alternative = context?.alternatives?.[0] + + if (superiorAlternative) { + return { + target: superiorAlternative, + apyDiff: superiorAlternative.alternativePoolInfo.apy - superiorAlternative.tokenPoolInfo.apy, + } + } + + if (alternative) { + return { + target: alternative, + apyDiff: undefined, + } + } + + return undefined + }, [context]) +} diff --git a/apps/cowswap-frontend/src/modules/yield/hooks/useYieldFormState.ts b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldFormState.ts new file mode 100644 index 0000000000..15e2385ec6 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/hooks/useYieldFormState.ts @@ -0,0 +1,26 @@ +import { useMemo } from 'react' + +import { LpToken } from '@cowprotocol/common-const' + +import { useYieldDerivedState } from './useYieldDerivedState' + +export enum YieldFormState { + Erc20BuyIsNotAllowed = 'Erc20BuyIsNotAllowed', +} + +export function useYieldFormState(): YieldFormState | null { + const state = useYieldDerivedState() + + return useMemo(() => { + if (state.outputCurrency && state.inputCurrency) { + const isInputLp = state.inputCurrency instanceof LpToken + const isOutputLp = state.outputCurrency instanceof LpToken + + if (!isInputLp && !isOutputLp) { + return YieldFormState.Erc20BuyIsNotAllowed + } + } + + return null + }, [state]) +} diff --git a/apps/cowswap-frontend/src/modules/yield/lpPageLinks.ts b/apps/cowswap-frontend/src/modules/yield/lpPageLinks.ts new file mode 100644 index 0000000000..9df218f5b3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/lpPageLinks.ts @@ -0,0 +1,42 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { LpTokenProvider } from '@cowprotocol/types' + +const COW_AMM_CHAINS = { + [SupportedChainId.MAINNET]: 'ethereum', + [SupportedChainId.GNOSIS_CHAIN]: 'gnosis', + [SupportedChainId.ARBITRUM_ONE]: 'arbitrum', + [SupportedChainId.SEPOLIA]: '', +} + +const UNI_CHAINS = { + [SupportedChainId.MAINNET]: 'ethereum', + [SupportedChainId.GNOSIS_CHAIN]: '', + [SupportedChainId.ARBITRUM_ONE]: 'arbitrum', + [SupportedChainId.SEPOLIA]: '', +} + +const SUSHI_CHAINS = { + [SupportedChainId.MAINNET]: 'ethereum', + [SupportedChainId.GNOSIS_CHAIN]: 'gnosis', + [SupportedChainId.ARBITRUM_ONE]: 'arbitrum', + [SupportedChainId.SEPOLIA]: '', +} + +const PANCAKE_CHAINS = { + [SupportedChainId.MAINNET]: 'eth', + [SupportedChainId.GNOSIS_CHAIN]: '', + [SupportedChainId.ARBITRUM_ONE]: 'arb', + [SupportedChainId.SEPOLIA]: '', +} + +export const LP_PAGE_LINKS: Record string> = { + [LpTokenProvider.COW_AMM]: (chainId, address) => + `https://balancer.fi/pools/${COW_AMM_CHAINS[chainId]}/cow/${address}`, + [LpTokenProvider.UNIV2]: (chainId, address) => + `https://app.uniswap.org/explore/pools/${UNI_CHAINS[chainId]}/${address}`, + [LpTokenProvider.CURVE]: () => `https://classic.curve.fi/pools`, + [LpTokenProvider.BALANCERV2]: () => `https://balancer.fi/pools`, + [LpTokenProvider.SUSHI]: (chainId, address) => `https://www.sushi.com/${SUSHI_CHAINS[chainId]}/pool/v2/${address}`, + [LpTokenProvider.PANCAKE]: (chainId, address) => + `https://pancakeswap.finance/liquidity/pool/${PANCAKE_CHAINS[chainId]}/${address}`, +} diff --git a/apps/cowswap-frontend/src/modules/yield/pure/PoolApyPreview.tsx b/apps/cowswap-frontend/src/modules/yield/pure/PoolApyPreview.tsx new file mode 100644 index 0000000000..8d4ec33dcb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/pure/PoolApyPreview.tsx @@ -0,0 +1,24 @@ +import { UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +const Wrapper = styled.div<{ isSuperior: boolean }>` + padding: 6px 10px; + border-radius: 16px; + font-size: 12px; + font-weight: 600; + background: ${({ isSuperior }) => + isSuperior ? `var(${UI.COLOR_COWAMM_LIGHT_GREEN})` : `var(${UI.COLOR_TEXT_OPACITY_25})`}; + color: ${({ isSuperior }) => (isSuperior ? `var(${UI.COLOR_COWAMM_DARK_GREEN})` : `var(${UI.COLOR_TEXT})`)}; +` + +interface PoolApyPreviewProps { + apy: number | undefined + isSuperior: boolean +} + +export function PoolApyPreview({ apy, isSuperior }: PoolApyPreviewProps) { + if (typeof apy !== 'number') return null + + return {apy}% APR +} diff --git a/apps/cowswap-frontend/src/modules/yield/pure/TargetPoolPreviewInfo.tsx b/apps/cowswap-frontend/src/modules/yield/pure/TargetPoolPreviewInfo.tsx new file mode 100644 index 0000000000..9214be3787 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/pure/TargetPoolPreviewInfo.tsx @@ -0,0 +1,73 @@ +import { ReactNode } from 'react' + +import { LpToken, TokenWithLogo } from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { ExternalLink, InfoTooltip, TokenSymbol, UI } from '@cowprotocol/ui' +import { Currency } from '@uniswap/sdk-core' + +import styled from 'styled-components/macro' + +import { LP_PAGE_LINKS } from '../lpPageLinks' + +const Wrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + align-items: center; +` + +const LeftPart = styled.div` + display: inline-flex; + flex-direction: row; + gap: 10px; +` + +const InfoButton = styled.button` + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + outline: 0; + border-radius: 16px; + background: transparent; + color: var(${UI.COLOR_TEXT_OPACITY_70}); + border: 1px solid var(${UI.COLOR_BORDER}); +` + +const StyledExternalLink = styled(ExternalLink)` + font-size: 13px; + color: var(${UI.COLOR_TEXT_OPACITY_70}); +` + +interface TargetPoolPreviewInfoProps { + chainId: SupportedChainId + children: ReactNode + sellToken: LpToken | TokenWithLogo | Currency + oppositeToken?: Currency | null +} + +export function TargetPoolPreviewInfo({ chainId, sellToken, oppositeToken, children }: TargetPoolPreviewInfoProps) { + if (!(sellToken instanceof LpToken) || !sellToken.lpTokenProvider) return null + + return ( + + + {children} + {oppositeToken && ( + + Details{' '} + + When you swap (sell) , solvers handle the transaction by purchasing + the required tokens, depositing them into the pool, and issuing LP tokens to you in return — all in a + gas-less operation. + + + )} + + + Analytics ↗ + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/yield/shared.ts b/apps/cowswap-frontend/src/modules/yield/shared.ts index cfd5c82b29..63755afa68 100644 --- a/apps/cowswap-frontend/src/modules/yield/shared.ts +++ b/apps/cowswap-frontend/src/modules/yield/shared.ts @@ -1,5 +1,7 @@ export { PoolsInfoUpdater } from './updaters/PoolsInfoUpdater' export { LpTokensWithBalancesUpdater } from './updaters/LpTokensWithBalancesUpdater' +export { VampireAttackUpdater } from './updaters/VampireAttackUpdater' export { usePoolsInfo } from './hooks/usePoolsInfo' export { useLpTokensWithBalances } from './hooks/useLpTokensWithBalances' +export { useVampireAttack, useVampireAttackFirstTarget } from './hooks/useVampireAttack' export type { PoolInfo, PoolInfoStates } from './state/poolsInfoAtom' diff --git a/apps/cowswap-frontend/src/modules/yield/state/poolsInfoAtom.ts b/apps/cowswap-frontend/src/modules/yield/state/poolsInfoAtom.ts index 11e3bb7046..2c73bcc9f4 100644 --- a/apps/cowswap-frontend/src/modules/yield/state/poolsInfoAtom.ts +++ b/apps/cowswap-frontend/src/modules/yield/state/poolsInfoAtom.ts @@ -19,30 +19,26 @@ type PoolInfoState = { export type PoolInfoStates = Record -type PoolInfoStatesPerAccount = Record - -type PoolsInfoState = Record +type PoolsInfoState = Record const poolsInfoAtom = atomWithStorage( - 'poolsInfoAtom:v0', + 'poolsInfoAtom:v1', mapSupportedNetworks({}), getJotaiIsolatedStorage(), ) export const currentPoolsInfoAtom = atom((get) => { - const { chainId, account } = get(walletInfoAtom) + const { chainId } = get(walletInfoAtom) const poolsInfo = get(poolsInfoAtom) - return account ? poolsInfo[chainId]?.[account] : undefined + return poolsInfo[chainId] }) export const upsertPoolsInfoAtom = atom(null, (get, set, update: Record) => { - const { chainId, account } = get(walletInfoAtom) + const { chainId } = get(walletInfoAtom) const poolsInfo = get(poolsInfoAtom) - if (!account) return - - const currentState = poolsInfo[chainId]?.[account] + const currentState = poolsInfo[chainId] const updatedState = { ...currentState, ...Object.keys(update).reduce((acc, address) => { @@ -59,7 +55,7 @@ export const upsertPoolsInfoAtom = atom(null, (get, set, update: Record(null) diff --git a/apps/cowswap-frontend/src/modules/yield/types.ts b/apps/cowswap-frontend/src/modules/yield/types.ts new file mode 100644 index 0000000000..0c0d2f61fe --- /dev/null +++ b/apps/cowswap-frontend/src/modules/yield/types.ts @@ -0,0 +1,24 @@ +import { LpToken } from '@cowprotocol/common-const' +import { LpTokenProvider } from '@cowprotocol/types' +import { BigNumber } from '@ethersproject/bignumber' + +import { PoolInfo } from './state/poolsInfoAtom' + +export interface TokenWithAlternative { + token: LpToken + alternative: LpToken + tokenBalance: BigNumber +} + +export interface TokenWithSuperiorAlternative extends TokenWithAlternative { + tokenPoolInfo: PoolInfo + alternativePoolInfo: PoolInfo +} + +export interface VampireAttackContext { + alternatives: TokenWithAlternative[] | null + superiorAlternatives: TokenWithSuperiorAlternative[] | null + cowAmmLpTokensCount: number + poolsAverageData: Partial | undefined> + averageApyDiff: number | undefined +} diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts index e4808dcf47..e06ff6099e 100644 --- a/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts +++ b/apps/cowswap-frontend/src/modules/yield/updaters/PoolsInfoUpdater/mockPoolInfo.ts @@ -8,20 +8,190 @@ const POOLS_AVERAGE_DATA_MOCK_OVERRIDE = localStorage.getItem('POOLS_AVERAGE_DAT export const MOCK_POOL_INFO: Record = MOCK_POOL_INFO_OVERRIDE ? JSON.parse(MOCK_POOL_INFO_OVERRIDE) : { - // Sushi AAVE/WETH - '0xd75ea151a61d06868e31f8988d28dfe5e9df57b4': { - apy: 1.89, - tvl: 157057, - feeTier: 0.3, - volume24h: 31.19, - }, - // CoW AMM AAVE/WETH + // COW AMM '0xf706c50513446d709f08d3e5126cd74fb6bfda19': { - apy: 0.07, + apy: 6.07, tvl: 52972, feeTier: 0.3, volume24h: 10, }, + '0xf8f5b88328dff3d19e5f4f11a9700293ac8f638f': { apy: 2.41, tvl: 4462967, feeTier: 0.3, volume24h: 226375 }, + '0x9bd702e05b9c97e4a4a3e47df1e0fe7a0c26d2f1': { apy: 2.37, tvl: 2767573, feeTier: 0.3, volume24h: 202029 }, + '0xdfee48c9df6d26c734296c0e6bd02401100a7217': { apy: 2.87, tvl: 4690794, feeTier: 0.3, volume24h: 120412 }, + '0x8ec257dc0b17b0c862d428c801fdcc8c382bf918': { apy: 2.94, tvl: 5483238, feeTier: 0.3, volume24h: 224116 }, + '0x3b124c8b4846836ba52df6cb6576ef66ca167dc1': { apy: 1.79, tvl: 210934, feeTier: 0.3, volume24h: 146902 }, + '0x41ff63c864097a7fbdf206fe676223e29f729fcb': { apy: 1.76, tvl: 4532413, feeTier: 0.3, volume24h: 209869 }, + '0x0c8ee93df5a4bad1c6f05e2676f87e6440b0b956': { apy: 2.77, tvl: 1043567, feeTier: 0.3, volume24h: 67002 }, + '0xfec04c31b6099ce76c4c5d6d754a34141884fd91': { apy: 1.62, tvl: 5533351, feeTier: 0.3, volume24h: 33251 }, + '0x96f8dfa1e922f88c313052d5357cc6a910e19c1e': { apy: 1.05, tvl: 1980623, feeTier: 0.3, volume24h: 193107 }, + '0x9fb7106c879fa48347796171982125a268ff0630': { apy: 2.9, tvl: 3377248, feeTier: 0.3, volume24h: 168811 }, + '0xb3d37552eebbbdbea36258ba0948f4bbcaa3584e': { apy: 1.92, tvl: 2229323, feeTier: 0.3, volume24h: 47080 }, + '0x9b8b93fc2a454f4f0f240aaf6644dbd77a528246': { apy: 1.45, tvl: 4685880, feeTier: 0.3, volume24h: 166951 }, + '0x42d9e44eed903a0ee477c9c04d1d1730c5e87272': { apy: 2.77, tvl: 3465509, feeTier: 0.3, volume24h: 132239 }, + '0x477a8982515e3a3d3aa6447b019b7c647e4162f8': { apy: 2.11, tvl: 2365870, feeTier: 0.3, volume24h: 122669 }, + '0xa7401066570960894a12b403f461cb61e8804b7b': { apy: 1.21, tvl: 5948103, feeTier: 0.3, volume24h: 184775 }, + '0xbf8868b754a77e90ea68ffc0b5b10a7c729457e1': { apy: 2.08, tvl: 5546198, feeTier: 0.3, volume24h: 62557 }, + '0x11f2a400de0a2fc93a32f88d8779d8199152c6a4': { apy: 1.47, tvl: 765773, feeTier: 0.3, volume24h: 140156 }, + '0xf08d4dea369c456d26a3168ff0024b904f2d8b91': { apy: 2.85, tvl: 2861876, feeTier: 0.3, volume24h: 104464 }, + '0xd7855be714943928236bda82d9cd7caf189f2806': { apy: 2.02, tvl: 650441, feeTier: 0.3, volume24h: 213673 }, + '0x4359a8ea4c353d93245c0b6b8608a28bb48a05e2': { apy: 2.09, tvl: 4596484, feeTier: 0.3, volume24h: 61945 }, + '0x6ff0531ee19272675b3c7d30401a5b2b2c7b0c67': { apy: 1.44, tvl: 4694812, feeTier: 0.3, volume24h: 140088 }, + '0xa62e2c047b65aee3c3ba7fc7c2bd95c82a514de2': { apy: 2.56, tvl: 2406025, feeTier: 0.3, volume24h: 191543 }, + '0x7c838b3ed3c15a5d5032e809b8714f0ae5e9a821': { apy: 1.64, tvl: 4647147, feeTier: 0.3, volume24h: 107370 }, + '0x9d0e8cdf137976e03ef92ede4c30648d05e25285': { apy: 1.41, tvl: 1911255, feeTier: 0.3, volume24h: 168747 }, + // UNIV2 + '0xc3689fbb396340be64c45b43a2aec84196bb207f': { apy: 2.74, tvl: 1932085, feeTier: 0.3, volume24h: 75383 }, + '0xc2adda861f89bbb333c90c492cb837741916a225': { apy: 1.34, tvl: 5047912, feeTier: 0.3, volume24h: 195365 }, + '0xcd6bcca48069f8588780dfa274960f15685aee0e': { apy: 2.94, tvl: 1197489, feeTier: 0.3, volume24h: 240967 }, + '0x5201523c0ad5ba792c40ce5aff7df2d1a721bbf8': { apy: 1.37, tvl: 3516485, feeTier: 0.3, volume24h: 222732 }, + '0x7054b0f980a7eb5b3a6b3446f3c947d80162775c': { apy: 2.99, tvl: 5539047, feeTier: 0.3, volume24h: 223551 }, + '0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f': { apy: 2.41, tvl: 3973854, feeTier: 0.3, volume24h: 14617 }, + '0x2947dc50cc24cc55afbf22807a49cc302d65568c': { apy: 1.57, tvl: 4143693, feeTier: 0.3, volume24h: 196902 }, + '0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60': { apy: 1.53, tvl: 2005446, feeTier: 0.3, volume24h: 3128 }, + '0x0ae8d75e6168420a7d52a791c2465b43307408b4': { apy: 2.44, tvl: 3661164, feeTier: 0.3, volume24h: 100091 }, + '0x539b4dfcd1e4dc3153e59204004c2141f796f432': { apy: 1.18, tvl: 2310777, feeTier: 0.3, volume24h: 180998 }, + '0x95f4408736988549212db071b1c8d20f7c4e6304': { apy: 1.93, tvl: 4697846, feeTier: 0.3, volume24h: 24634 }, + '0xd3772a963790fede65646cfdae08734a17cd0f47': { apy: 1.91, tvl: 4839359, feeTier: 0.3, volume24h: 219066 }, + '0xe45b4a84e0ad24b8617a489d743c52b84b7acebe': { apy: 1.08, tvl: 390105, feeTier: 0.3, volume24h: 186472 }, + '0x4e34da137f0b317c633838458e0c923a5e088752': { apy: 2.72, tvl: 3558018, feeTier: 0.3, volume24h: 226275 }, + '0x77010f531b1603d0a6dfd5e0ae96d04090ab6fda': { apy: 1.5, tvl: 6026291, feeTier: 0.3, volume24h: 243344 }, + '0xcb37089fc6a6faff231b96e000300a6994d7a625': { apy: 2.47, tvl: 1319306, feeTier: 0.3, volume24h: 120610 }, + '0xda3a20aad0c34fa742bd9813d45bbf67c787ae0b': { apy: 2.9, tvl: 2689994, feeTier: 0.3, volume24h: 70167 }, + '0x12490e03f079fa7e3c20e9bf797bdb7a7f495fd4': { apy: 1.96, tvl: 1919878, feeTier: 0.3, volume24h: 117471 }, + '0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0': { apy: 1.68, tvl: 496286, feeTier: 0.3, volume24h: 242005 }, + '0x4dd26482738be6c06c31467a19dcda9ad781e8c4': { apy: 1.57, tvl: 2934009, feeTier: 0.3, volume24h: 61483 }, + '0xa8f590c8eadf61769d32cb084d30a4b309146458': { apy: 1.7, tvl: 2099333, feeTier: 0.3, volume24h: 210062 }, + '0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed': { apy: 1.14, tvl: 2520024, feeTier: 0.3, volume24h: 109274 }, + '0xd9fe79a0b8d00b0eeeca68dd802d7c9574f7ed25': { apy: 1.34, tvl: 3331481, feeTier: 0.3, volume24h: 33975 }, + '0xc77efc40c20f1578a9bd714f9a871dfc1a81b234': { apy: 2.34, tvl: 774745, feeTier: 0.3, volume24h: 62087 }, + '0x11181bd3baf5ce2a478e98361985d42625de35d1': { apy: 1.21, tvl: 4778872, feeTier: 0.3, volume24h: 169155 }, + '0x4df1c47ecfbac8482a4811d373128e2acc007d02': { apy: 1.6, tvl: 4536199, feeTier: 0.3, volume24h: 82920 }, + '0xc730ef0f4973da9cc0ab8ab291890d3e77f58f79': { apy: 1.54, tvl: 1301355, feeTier: 0.3, volume24h: 166390 }, + '0x629d22e6eeac46a11dbc96be93b90aee9309be4c': { apy: 1.18, tvl: 1338149, feeTier: 0.3, volume24h: 16354 }, + '0xfa5562729fdc3ed3a52c3aab2e12bd504fd24991': { apy: 1.38, tvl: 5426365, feeTier: 0.3, volume24h: 239402 }, + '0x2c8f9bbae004854b9548f6c84720c70a8fceea23': { apy: 1.26, tvl: 1386582, feeTier: 0.3, volume24h: 2448 }, + '0x29c830864930c897efa2b9e9851342187b82010e': { apy: 2.3, tvl: 578601, feeTier: 0.3, volume24h: 32995 }, + '0x440bb7a5fc57764d9e7f89e55ad57a342f92b9a2': { apy: 2.91, tvl: 4997306, feeTier: 0.3, volume24h: 118960 }, + '0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6': { apy: 2.1, tvl: 5792305, feeTier: 0.3, volume24h: 59058 }, + '0xce5debe9dd76f96bb5fa00eb3cc084d43ec0dbf3': { apy: 1.34, tvl: 2013060, feeTier: 0.3, volume24h: 44202 }, + '0x8d58e202016122aae65be55694dbce1b810b4072': { apy: 1.52, tvl: 4208606, feeTier: 0.3, volume24h: 106742 }, + '0x0afd65ec6b286d353c42268738d66bbf56ba9de5': { apy: 1.02, tvl: 1423178, feeTier: 0.3, volume24h: 198749 }, + '0x3beeab9d5624e487045e01d12332975204a04a8a': { apy: 2.22, tvl: 1073944, feeTier: 0.3, volume24h: 166776 }, + '0x180efc1349a69390ade25667487a826164c9c6e4': { apy: 2.59, tvl: 3872670, feeTier: 0.3, volume24h: 241030 }, + '0x9ec96dcb54331626b79d8450a3daa9bcfa02e0b0': { apy: 2.34, tvl: 4281558, feeTier: 0.3, volume24h: 228909 }, + '0xe99191b4a562ccb416ea8e00085e13105cf80639': { apy: 2.78, tvl: 3818273, feeTier: 0.3, volume24h: 15755 }, + '0x8296d84e911e0c1f827e1e7d4b50c2568e807b36': { apy: 1.67, tvl: 3010265, feeTier: 0.3, volume24h: 142523 }, + '0x7c1c4a2cf81d2fc83b89bfd34f4d2c7e90044b32': { apy: 2.82, tvl: 3817577, feeTier: 0.3, volume24h: 168959 }, + '0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482': { apy: 2.1, tvl: 4819460, feeTier: 0.3, volume24h: 232346 }, + '0x56d8bbe2d518205b521aeb86a58e1b53b047a44f': { apy: 2.94, tvl: 1418687, feeTier: 0.3, volume24h: 3277 }, + '0x450a136224734a05a494323be8623ef668f74c0e': { apy: 2.55, tvl: 5585853, feeTier: 0.3, volume24h: 167677 }, + '0x5fa9569b0ed6aa01e234468e6a15b77988b950df': { apy: 1.41, tvl: 3092154, feeTier: 0.3, volume24h: 113477 }, + '0xcf4236db746dbc1855a4d095aaf58da9b030491e': { apy: 1.25, tvl: 5212187, feeTier: 0.3, volume24h: 206611 }, + '0xbe8bc29765e11894f803906ee1055a344fdf2511': { apy: 1.39, tvl: 88814, feeTier: 0.3, volume24h: 118087 }, + '0xe945683b3462d2603a18bdfbb19261c6a4f03ad1': { apy: 2.09, tvl: 5630317, feeTier: 0.3, volume24h: 118131 }, + '0xdeba8fd61c1c87b6321a501ebb19e61e610421bf': { apy: 2.04, tvl: 2082290, feeTier: 0.3, volume24h: 137404 }, + // CURVE + '0xb7ecb2aa52aa64a717180e030241bc75cd946726': { apy: 1.8, tvl: 4160844, feeTier: 0.3, volume24h: 170746 }, + '0xb9446c4ef5ebe66268da6700d26f96273de3d571': { apy: 2.49, tvl: 3641587, feeTier: 0.3, volume24h: 159109 }, + '0x42e03280c99579b048bc001531bf7015bc252653': { apy: 1.37, tvl: 2893007, feeTier: 0.3, volume24h: 126615 }, + '0x4e0915c88bc70750d68c481540f081fefaf22273': { apy: 2.4, tvl: 5590624, feeTier: 0.3, volume24h: 135895 }, + '0xb37d6c07482bc11cd28a1f11f1a6ad7b66dec933': { apy: 1.89, tvl: 5914771, feeTier: 0.3, volume24h: 186517 }, + '0xf05cfb8b4382c69f3b451c5fb55210b232e0edfa': { apy: 1.56, tvl: 2682101, feeTier: 0.3, volume24h: 26871 }, + '0x413928a25d6ea1a26f2625d633207755f67bf97c': { apy: 2.89, tvl: 5526764, feeTier: 0.3, volume24h: 44419 }, + '0xba3436fd341f2c8a928452db3c5a3670d1d5cc73': { apy: 2.81, tvl: 1053154, feeTier: 0.3, volume24h: 225832 }, + '0xf985005a3793dba4cce241b3c19ddcd3fe069ff4': { apy: 1.32, tvl: 2942848, feeTier: 0.3, volume24h: 233176 }, + '0x6663b6d50992ef4fc0380199397c87c2f5256075': { apy: 1.76, tvl: 4237073, feeTier: 0.3, volume24h: 226044 }, + '0x30bf3e17cad0baf1d6b64079ec219808d2708feb': { apy: 1.15, tvl: 4127474, feeTier: 0.3, volume24h: 73275 }, + '0x8efd02a0a40545f32dba5d664cbbc1570d3fedf6': { apy: 2.6, tvl: 5705857, feeTier: 0.3, volume24h: 22284 }, + '0xb657b895b265c38c53fff00166cf7f6a3c70587d': { apy: 2.7, tvl: 5513844, feeTier: 0.3, volume24h: 82295 }, + '0x43b4fdfd4ff969587185cdb6f0bd875c5fc83f8c': { apy: 1.85, tvl: 643616, feeTier: 0.3, volume24h: 223809 }, + '0xb30da2376f63de30b42dc055c93fa474f31330a5': { apy: 2.35, tvl: 4871716, feeTier: 0.3, volume24h: 48753 }, + '0xa8e14f03124ea156a4fc416537c82ff91a647d50': { apy: 2.6, tvl: 3323615, feeTier: 0.3, volume24h: 242323 }, + '0xf5d7c5484c02c03cf131b76c67c3eb969828bd3d': { apy: 1.32, tvl: 5180937, feeTier: 0.3, volume24h: 26865 }, + '0x8f476f43baa2d4b6c215d2408340919bf7de0520': { apy: 1.26, tvl: 3112951, feeTier: 0.3, volume24h: 24362 }, + '0xa4eddfe4ba5143ac17fd6bc1e53de0384df8c660': { apy: 2.56, tvl: 5921481, feeTier: 0.3, volume24h: 154626 }, + '0x09b2e090531228d1b8e3d948c73b990cb6e60720': { apy: 1.1, tvl: 2572855, feeTier: 0.3, volume24h: 191549 }, + '0x137469b55d1f15651ba46a89d0588e97dd0b6562': { apy: 1.69, tvl: 231789, feeTier: 0.3, volume24h: 193844 }, + '0x8ea96fc70c577d59528c98c31bc4bf39027c1c3e': { apy: 2.37, tvl: 3527695, feeTier: 0.3, volume24h: 138392 }, + '0x410e3e86ef427e30b9235497143881f717d93c2a': { apy: 2.93, tvl: 4041073, feeTier: 0.3, volume24h: 69969 }, + '0x5ca0313d44551e32e0d7a298ec024321c4bc59b4': { apy: 1.83, tvl: 852006, feeTier: 0.3, volume24h: 172126 }, + '0xd0fb39e59037fc6ae8af5cb495cea690ed501fdd': { apy: 2.35, tvl: 4470364, feeTier: 0.3, volume24h: 175572 }, + '0x4807862aa8b2bf68830e4c8dc86d0e9a998e085a': { apy: 1.49, tvl: 663743, feeTier: 0.3, volume24h: 217067 }, + '0x8fdb0bb9365a46b145db80d0b1c5c5e979c84190': { apy: 1.9, tvl: 4523632, feeTier: 0.3, volume24h: 141090 }, + '0x397c5908b1d8af00b2c2cce499cdd0cf42165d19': { apy: 1.55, tvl: 2296113, feeTier: 0.3, volume24h: 115446 }, + '0x839d6bdedff886404a6d7a788ef241e4e28f4802': { apy: 1.25, tvl: 965939, feeTier: 0.3, volume24h: 21183 }, + '0x5b6c539b224014a09b3388e51caaa8e354c959c8': { apy: 1.75, tvl: 976979, feeTier: 0.3, volume24h: 215284 }, + '0x845838df265dcd2c412a1dc9e959c7d08537f8a2': { apy: 1.71, tvl: 3935336, feeTier: 0.3, volume24h: 62730 }, + '0x9fc689ccada600b6df723d9e47d84d76664a1f23': { apy: 2.2, tvl: 333049, feeTier: 0.3, volume24h: 203086 }, + '0x6c280db098db673d30d5b34ec04b6387185d3620': { apy: 2.79, tvl: 3139046, feeTier: 0.3, volume24h: 208923 }, + '0x84c333e94aea4a51a21f6cf0c7f528c50dc7592c': { apy: 1.75, tvl: 184423, feeTier: 0.3, volume24h: 155274 }, + '0xf9835375f6b268743ea0a54d742aa156947f8c06': { apy: 2.66, tvl: 5962153, feeTier: 0.3, volume24h: 66558 }, + '0xb85010193fd15af8390dbd62790da70f46c1126b': { apy: 2.49, tvl: 1141984, feeTier: 0.3, volume24h: 83499 }, + '0xa3f152837492340daaf201f4dfec6cd73a8a9760': { apy: 2.14, tvl: 4385410, feeTier: 0.3, volume24h: 237960 }, + '0xb73527615c25ac2d226db2581525f69968926caf': { apy: 2.49, tvl: 5806095, feeTier: 0.3, volume24h: 136940 }, + '0xc4ad29ba4b3c580e6d59105fff484999997675ff': { apy: 1.9, tvl: 4518961, feeTier: 0.3, volume24h: 25625 }, + '0x2889302a794da87fbf1d6db415c1492194663d13': { apy: 2.36, tvl: 1405952, feeTier: 0.3, volume24h: 224999 }, + '0x3a283d9c08e8b55966afb64c515f5143cf907611': { apy: 2.95, tvl: 4451819, feeTier: 0.3, volume24h: 162487 }, + '0xc7de47b9ca2fc753d6a2f167d8b3e19c6d18b19a': { apy: 2.22, tvl: 2632062, feeTier: 0.3, volume24h: 206129 }, + '0xef484de8c07b6e2d732a92b5f78e81b38f99f95e': { apy: 2.79, tvl: 2942020, feeTier: 0.3, volume24h: 52701 }, + '0x3d229e1b4faab62f621ef2f6a610961f7bd7b23b': { apy: 2.68, tvl: 4642289, feeTier: 0.3, volume24h: 204976 }, + '0x4d1941a887ec788f059b3bfcc8ee1e97b968825b': { apy: 2.29, tvl: 2069435, feeTier: 0.3, volume24h: 29468 }, + '0xfc2838a17d8e8b1d5456e0a351b0708a09211147': { apy: 1.35, tvl: 5561116, feeTier: 0.3, volume24h: 101229 }, + '0xc34993c9adf6a5ab3b4ca27dc71b9c7894a53974': { apy: 1.3, tvl: 4216532, feeTier: 0.3, volume24h: 207313 }, + '0x5be6c45e2d074faa20700c49ada3e88a1cc0025d': { apy: 1.56, tvl: 5679638, feeTier: 0.3, volume24h: 162899 }, + '0x733a2a4f6d111e04040e88d6595083828965e01b': { apy: 2.16, tvl: 595954, feeTier: 0.3, volume24h: 141853 }, + '0x68e26daf88da63bebd3da05fc9c880fa37080d3e': { apy: 1.68, tvl: 2155124, feeTier: 0.3, volume24h: 11082 }, + // SUSHI + '0xba13afecda9beb75de5c56bbaf696b880a5a50dd': { apy: 1.99, tvl: 3485637, feeTier: 0.3, volume24h: 118895 }, + '0xd9a1df87e01b31f1154f788041625030af1927be': { apy: 1.19, tvl: 1017915, feeTier: 0.3, volume24h: 16103 }, + '0x86f518368e0d49d5916e2bd9eb162e9952b7b04d': { apy: 2.18, tvl: 4257965, feeTier: 0.3, volume24h: 110901 }, + '0xd75ea151a61d06868e31f8988d28dfe5e9df57b4': { apy: 1.92, tvl: 4670302, feeTier: 0.3, volume24h: 171243 }, + '0x9cbc2a6ab3f10edf7d71c9cf3b6bdb7ee5629550': { apy: 1.22, tvl: 4362129, feeTier: 0.3, volume24h: 131948 }, + '0x9e48fadf799e0513d2ef4631478ea186741fa617': { apy: 1.42, tvl: 3746364, feeTier: 0.3, volume24h: 41457 }, + '0x1f4c763bde1d4832b3ea0640e66da00b98831355': { apy: 1.68, tvl: 5519215, feeTier: 0.3, volume24h: 200575 }, + '0x0d15e893cf50724382368cafed222cf131b55307': { apy: 2.69, tvl: 1655574, feeTier: 0.3, volume24h: 203334 }, + '0x364248b2f1f57c5402d244b2d469a35b4c0e9dab': { apy: 1.27, tvl: 2921484, feeTier: 0.3, volume24h: 209688 }, + '0x7b98e476de2c50b6fa284dbd410dd516f9a72b30': { apy: 2.03, tvl: 388343, feeTier: 0.3, volume24h: 163216 }, + '0xf55c33d94150d93c2cfb833bcca30be388b14964': { apy: 2.96, tvl: 2011214, feeTier: 0.3, volume24h: 100661 }, + '0x0589e281d35ee1acf6d4fd32f1fba60effb5281b': { apy: 1.79, tvl: 3891323, feeTier: 0.3, volume24h: 6855 }, + '0x1241f4a348162d99379a23e73926cf0bfcbf131e': { apy: 2.53, tvl: 1705196, feeTier: 0.3, volume24h: 236308 }, + '0x201e6a9e75df132a8598720433af35fe8d73e94d': { apy: 2.94, tvl: 3784139, feeTier: 0.3, volume24h: 123848 }, + '0xb27c7b131cf4915bec6c4bc1ce2f33f9ee434b9f': { apy: 1.11, tvl: 594587, feeTier: 0.3, volume24h: 108357 }, + '0x130f4322e5838463ee460d5854f5d472cfc8f253': { apy: 1.37, tvl: 1175014, feeTier: 0.3, volume24h: 24410 }, + '0xa8aec03d5cf2824fd984ee249493d6d4d6740e61': { apy: 1.31, tvl: 3123438, feeTier: 0.3, volume24h: 186588 }, + '0x53162d78dca413d9e28cf62799d17a9e278b60e8': { apy: 1.57, tvl: 3128531, feeTier: 0.3, volume24h: 48653 }, + '0x17a2194d55f52fd0c711e0e42b41975494bb109b': { apy: 2.21, tvl: 4509973, feeTier: 0.3, volume24h: 172535 }, + '0x5654d65578cda52791879751141f2f3d7dc27fe4': { apy: 2.52, tvl: 4040513, feeTier: 0.3, volume24h: 101102 }, + '0x0c365789dbbb94a29f8720dc465554c587e897db': { apy: 2.21, tvl: 2713413, feeTier: 0.3, volume24h: 244300 }, + '0x57fbc21ca3c157a26b6ea57334d6b082be1060a7': { apy: 2.8, tvl: 1014029, feeTier: 0.3, volume24h: 97009 }, + '0xa75f7c2f025f470355515482bde9efa8153536a8': { apy: 2.57, tvl: 1027123, feeTier: 0.3, volume24h: 113090 }, + '0x938625591adb4e865b882377e2c965f9f9b85e34': { apy: 2.99, tvl: 661710, feeTier: 0.3, volume24h: 142896 }, + '0x2c51eaa1bcc7b013c3f1d5985cdcb3c56dc3fbc1': { apy: 1.73, tvl: 732769, feeTier: 0.3, volume24h: 111936 }, + '0x072b999fc3d82f9ea08b8adbb9d63a980ff2b14d': { apy: 2.48, tvl: 415034, feeTier: 0.3, volume24h: 83299 }, + '0x0eee7f7319013df1f24f5eaf83004fcf9cf49245': { apy: 2.84, tvl: 2238403, feeTier: 0.3, volume24h: 197261 }, + '0xf8937dc1ca081161527c0bc1eb6379420195c894': { apy: 2.28, tvl: 1301031, feeTier: 0.3, volume24h: 74966 }, + '0x5fa4370164a2fabeef159b893299d59ff5dc1e6d': { apy: 2.72, tvl: 5248047, feeTier: 0.3, volume24h: 47424 }, + '0x0bec54c89a7d9f15c4e7faa8d47adedf374462ed': { apy: 2.27, tvl: 3823505, feeTier: 0.3, volume24h: 87971 }, + '0xe12af1218b4e9272e9628d7c7dc6354d137d024e': { apy: 1.46, tvl: 1117439, feeTier: 0.3, volume24h: 94695 }, + '0xdd51121d1efc398b4c09fd0cb84d79ae2c923fc9': { apy: 2.23, tvl: 458749, feeTier: 0.3, volume24h: 102395 }, + '0x53813285cc60b13fcd2105c6472a47af01f8ac84': { apy: 1.88, tvl: 983255, feeTier: 0.3, volume24h: 227946 }, + '0x613c836df6695c10f0f4900528b6931441ac5d5a': { apy: 1.46, tvl: 5039905, feeTier: 0.3, volume24h: 29981 }, + '0x65f550d18cac8e19a97e73108618b443bf301d20': { apy: 2.66, tvl: 1010681, feeTier: 0.3, volume24h: 88790 }, + '0x44d34985826578e5ba24ec78c93be968549bb918': { apy: 1.93, tvl: 3294863, feeTier: 0.3, volume24h: 146703 }, + '0x57024267e8272618f9c5037d373043a8646507e5': { apy: 2.05, tvl: 4236986, feeTier: 0.3, volume24h: 238855 }, + '0x164fe0239d703379bddde3c80e4d4800a1cd452b': { apy: 2.94, tvl: 4002574, feeTier: 0.3, volume24h: 1344 }, + '0xd46004eed3885e0a74d452811c80eb41270a5728': { apy: 2.28, tvl: 296097, feeTier: 0.3, volume24h: 73247 }, + '0x557b08a0cab46bfe22471b65522757ea92a9e7a5': { apy: 1.12, tvl: 2723359, feeTier: 0.3, volume24h: 115824 }, + '0x4632ac4a94b57573f6b0297229fc8d54046f9be4': { apy: 1.02, tvl: 4226822, feeTier: 0.3, volume24h: 104692 }, + '0xf7ac1f571856d7dbfbb56728c48823a2a19e4326': { apy: 2.81, tvl: 1574589, feeTier: 0.3, volume24h: 143480 }, + '0x31503dcb60119a812fee820bb7042752019f2355': { apy: 2.85, tvl: 3327566, feeTier: 0.3, volume24h: 193759 }, + '0x7c03cf21483fa45006e81669b991af3a16733a44': { apy: 1.41, tvl: 2353206, feeTier: 0.3, volume24h: 19494 }, + '0xf169cea51eb51774cf107c88309717dda20be167': { apy: 2.5, tvl: 4924920, feeTier: 0.3, volume24h: 240489 }, + '0x840fdb210cf93318e8a16a51f24938c866a32d3c': { apy: 1.39, tvl: 858541, feeTier: 0.3, volume24h: 13334 }, + '0x2a93167ed63a31f35ca4788e2eb9fbd9fa6089d0': { apy: 2.23, tvl: 5268283, feeTier: 0.3, volume24h: 90308 }, + '0x1c580cc549d03171b13b55074dc1658f60641c73': { apy: 1.12, tvl: 875610, feeTier: 0.3, volume24h: 193969 }, + '0x05767d9ef41dc40689678ffca0608878fb3de906': { apy: 1.67, tvl: 4540867, feeTier: 0.3, volume24h: 214645 }, + '0x33f6ddaea2a8a54062e021873bcaee006cdf4007': { apy: 1.16, tvl: 4610326, feeTier: 0.3, volume24h: 56219 }, } export const POOLS_AVERAGE_DATA_MOCK: Partial> = @@ -29,7 +199,7 @@ export const POOLS_AVERAGE_DATA_MOCK: Partial { + if (lpTokensWithBalancesCount === 0) return null + + const result = Object.keys(lpTokensWithBalances).reduce( + (acc, tokenAddress) => { + const { token: lpToken, balance: tokenBalance } = lpTokensWithBalances[tokenAddress] + const alternative = cowAmmLpTokens.find((cowAmmLpToken) => { + return cowAmmLpToken.tokens.every((token) => lpToken.tokens.includes(token)) + }) + + if (alternative) { + const tokenPoolInfo = poolsInfo?.[lpToken.address.toLowerCase()]?.info + const alternativePoolInfo = poolsInfo?.[alternative.address.toLowerCase()]?.info + + // When CoW AMM pool has better APY + if (alternativePoolInfo?.apy && tokenPoolInfo?.apy && alternativePoolInfo.apy > tokenPoolInfo.apy) { + acc.superiorAlternatives.push({ + token: lpToken, + alternative, + tokenPoolInfo, + alternativePoolInfo, + tokenBalance, + }) + } else { + acc.alternatives.push({ + token: lpToken, + alternative, + tokenBalance, + }) + } + } + + return acc + }, + { superiorAlternatives: [] as TokenWithSuperiorAlternative[], alternatives: [] as TokenWithAlternative[] }, + ) + + return { + superiorAlternatives: result.superiorAlternatives.sort((a, b) => { + if (!b.tokenPoolInfo || !a.tokenPoolInfo) return 0 + + return b.tokenPoolInfo.apy - a.tokenPoolInfo.apy + }), + alternatives: result.alternatives.sort((a, b) => { + const aBalance = lpTokensWithBalances[a.token.address.toLowerCase()].balance + const bBalance = lpTokensWithBalances[b.token.address.toLowerCase()].balance + + return +bBalance.sub(aBalance).toString() + }), + } + }, [lpTokensWithBalancesCount, lpTokensWithBalances, cowAmmLpTokens, poolsInfo]) + + const averageApy = useMemo(() => { + const keys = Object.keys(POOLS_AVERAGE_DATA_MOCK) + let count = 0 + + return ( + keys.reduce((result, _key) => { + const key = _key as LpTokenProvider + + if (key === LpTokenProvider.COW_AMM) return result + + count++ + const pool = POOLS_AVERAGE_DATA_MOCK[key] + + return result + (pool?.apy || 0) + }, 0) / count + ) + }, []) + + const { [LpTokenProvider.COW_AMM]: cowAmmData, ...poolsAverageData } = POOLS_AVERAGE_DATA_MOCK + const averageApyDiff = cowAmmData ? +(cowAmmData.apy - averageApy).toFixed(2) : 0 + + const context = useSafeMemoObject({ + superiorAlternatives: alternativesResult?.superiorAlternatives || null, + alternatives: alternativesResult?.alternatives || null, + cowAmmLpTokensCount: cowAmmLpTokens.length, + poolsAverageData, + averageApyDiff, + }) + + useEffect(() => { + if (cowAmmLpTokens.length === 0 || !areLpBalancesLoaded) { + setVampireAttack(null) + } else { + setVampireAttack(context) + } + }, [context, cowAmmLpTokens.length, areLpBalancesLoaded, setVampireAttack]) + + return null +} diff --git a/libs/tokens/src/const/lpTokensList.json b/libs/tokens/src/const/lpTokensList.json index 928f6b1ed3..c1e25bf9cf 100644 --- a/libs/tokens/src/const/lpTokensList.json +++ b/libs/tokens/src/const/lpTokensList.json @@ -2,31 +2,31 @@ { "priority": 100, "lpTokenProvider": "COW_AMM", - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/cow-amm.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/refs/heads/main/src/public/lp-tokens/cow-amm.json" }, { "priority": 101, "lpTokenProvider": "UNIV2", - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/uniswapv2.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/refs/heads/main/src/public/lp-tokens/uniswapv2.json" }, { "priority": 102, "lpTokenProvider": "CURVE", - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/curve.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/refs/heads/main/src/public/lp-tokens/curve.json" }, { "priority": 103, "lpTokenProvider": "BALANCERV2", - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/balancerv2.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/refs/heads/main/src/public/lp-tokens/balancerv2.json" }, { "priority": 104, "lpTokenProvider": "SUSHI", - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/95687bdc19a91b2934eca8a11b8fb6f09546bbda/src/public/lp-tokens/sushiswap.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/refs/heads/main/src/public/lp-tokens/sushiswap.json" }, { "priority": 105, "lpTokenProvider": "PANCAKE", - "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/055c8ccebe59e91874baf8600403a487ad33e93d/src/public/lp-tokens/pancakeswap.json" + "source": "https://raw.githubusercontent.com/cowprotocol/token-lists/refs/heads/main/src/public/lp-tokens/pancakeswap.json" } ] diff --git a/libs/tokens/src/updaters/TokensListsUpdater/index.ts b/libs/tokens/src/updaters/TokensListsUpdater/index.ts index 4c89c921b7..e49aabd761 100644 --- a/libs/tokens/src/updaters/TokensListsUpdater/index.ts +++ b/libs/tokens/src/updaters/TokensListsUpdater/index.ts @@ -19,7 +19,7 @@ import { ListState } from '../../types' const { atom: lastUpdateTimeAtom, updateAtom: updateLastUpdateTimeAtom } = atomWithPartialUpdate( atomWithStorage>( - 'tokens:lastUpdateTimeAtom:v3', + 'tokens:lastUpdateTimeAtom:v4', mapSupportedNetworks(0), getJotaiMergerStorage(), ), diff --git a/libs/ui/src/containers/InlineBanner/index.tsx b/libs/ui/src/containers/InlineBanner/index.tsx index 1e9ae9195b..c43535ede2 100644 --- a/libs/ui/src/containers/InlineBanner/index.tsx +++ b/libs/ui/src/containers/InlineBanner/index.tsx @@ -164,7 +164,7 @@ export interface InlineBannerProps { orientation?: BannerOrientation iconSize?: number iconPadding?: string - customIcon?: string + customIcon?: string | ReactNode padding?: string margin?: string width?: string @@ -203,7 +203,11 @@ export function InlineBanner({ > {!hideIcon && customIcon ? ( - + typeof customIcon === 'string' ? ( + + ) : ( + customIcon + ) ) : !hideIcon && colorEnums.icon ? ( { const themeMode = useTheme() const selectedTheme = customThemeMode || (themeMode.darkMode ? 'dark' : 'light') @@ -297,6 +299,7 @@ export const ProductLogo = ({ return ( Date: Tue, 5 Nov 2024 14:21:08 +0500 Subject: [PATCH 077/116] fix(widget): remove irrelevant change trade params events (#5060) --- .../trade/hooks/useNotifyWidgetTrade.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts index 5e3037ad8b..b9b8e0ae5f 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useNotifyWidgetTrade.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { useEffect } from 'react' import { getCurrencyAddress } from '@cowprotocol/common-utils' import { AtomsAndUnits, CowWidgetEvents, OnTradeParamsPayload } from '@cowprotocol/events' @@ -15,17 +15,23 @@ import { TradeDerivedState } from '../types/TradeDerivedState' export function useNotifyWidgetTrade() { const state = useDerivedTradeState() - const isFirstLoad = useRef(true) useEffect(() => { - if (isFirstLoad.current && !!state) { - isFirstLoad.current = false - return - } + if (!state) return - if (!state?.tradeType) { + /** + * There is no way to select both empty sell and buy currencies in the widget UI. + * The only way when it is possible is when the widget integrator set the state, but in this case it doesn't make sense to notify them. + * + * In practice, the state has both currencies empty only at the beginning, when the widget is not ready to trade. + * So we skip the notification in this case. + */ + const stateIsNotReady = !state.inputCurrency && !state.outputCurrency + + if (!state.tradeType || stateIsNotReady) { return } + WIDGET_EVENT_EMITTER.emit( CowWidgetEvents.ON_CHANGE_TRADE_PARAMS, getTradeParamsEventPayload(state.tradeType, state), From 21f39fe3811fb65d6f1a98bb2185321f378afdd4 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 5 Nov 2024 18:34:30 +0500 Subject: [PATCH 078/116] fix(swap): take slippage into account for buy orders (#5067) --- .../src/modules/tradeFlow/hooks/useTradeFlowContext.ts | 10 +++------- .../services/safeBundleFlow/safeBundleEthFlow.ts | 6 +++--- .../src/modules/tradeFlow/types/TradeFlowContext.ts | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts index cc0b74bfef..18a77d1f9c 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -36,10 +36,9 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon const uiOrderType = tradeType ? TradeTypeToUiOrderType[tradeType] : null const sellCurrency = derivedTradeState?.inputCurrency - const inputAmount = receiveAmountInfo?.afterNetworkCosts.sellAmount + const inputAmount = receiveAmountInfo?.afterSlippage.sellAmount const outputAmount = receiveAmountInfo?.afterSlippage.buyAmount const sellAmountBeforeFee = receiveAmountInfo?.afterNetworkCosts.sellAmount - const inputAmountWithSlippage = receiveAmountInfo?.afterSlippage.sellAmount const networkFee = receiveAmountInfo?.costs.networkFee.amountInSellCurrency const permitInfo = usePermitInfo(sellCurrency, tradeType) @@ -56,7 +55,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId || SupportedChainId.MAINNET] const { enoughAllowance } = useEnoughBalanceAndAllowance({ account, - amount: inputAmountWithSlippage, + amount: inputAmount, checkAllowanceAddress, }) @@ -75,7 +74,6 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon useSWR( inputAmount && outputAmount && - inputAmountWithSlippage && sellAmountBeforeFee && networkFee && sellToken && @@ -103,7 +101,6 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon enoughAllowance, generatePermitHook, inputAmount, - inputAmountWithSlippage, networkFee, outputAmount, permitInfo, @@ -134,7 +131,6 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon enoughAllowance, generatePermitHook, inputAmount, - inputAmountWithSlippage, networkFee, outputAmount, permitInfo, @@ -155,7 +151,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon chainId, inputAmount, outputAmount, - inputAmountWithSlippage, + inputAmountWithSlippage: inputAmount, }, flags: { allowsOffchainSigning, 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 a718959824..0247b09d84 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/safeBundleFlow/safeBundleEthFlow.ts @@ -39,7 +39,7 @@ export async function safeBundleEthFlow( const { spender, settlementContract, safeAppsSdk, needsApproval, wrappedNativeContract } = safeBundleContext - const { inputAmountWithSlippage, chainId, inputAmount, outputAmount } = context + const { chainId, inputAmount, outputAmount } = context const orderParams: PostOrderParams = { ...tradeContext.orderParams, @@ -49,7 +49,7 @@ export async function safeBundleEthFlow( const { account, recipientAddressOrName, kind } = orderParams tradeFlowAnalytics.wrapApproveAndPresign(swapFlowAnalyticsContext) - const nativeAmountInWei = inputAmountWithSlippage.quotient.toString() + const nativeAmountInWei = inputAmount.quotient.toString() const tradeAmounts = { inputAmount, outputAmount } tradeConfirmActions.onSign(tradeAmounts) @@ -72,7 +72,7 @@ export async function safeBundleEthFlow( const approveTx = await buildApproveTx({ erc20Contract: wrappedNativeContract as unknown as Erc20, spender, - amountToApprove: inputAmountWithSlippage, + amountToApprove: inputAmount, }) txs.push({ diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts index 69ea103b01..16f6dc07b8 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/types/TradeFlowContext.ts @@ -23,7 +23,6 @@ export interface TradeFlowContext { chainId: number inputAmount: CurrencyAmount outputAmount: CurrencyAmount - inputAmountWithSlippage: CurrencyAmount } flags: { allowsOffchainSigning: boolean From 9054969abcc4daceb16bbc04e186918fb092e3db Mon Sep 17 00:00:00 2001 From: Leandro Date: Tue, 5 Nov 2024 13:48:40 +0000 Subject: [PATCH 079/116] fix(explorer): make sure txHash always has the 0x prefix (#5061) --- apps/explorer/src/explorer/pages/TransactionDetails.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/explorer/src/explorer/pages/TransactionDetails.tsx b/apps/explorer/src/explorer/pages/TransactionDetails.tsx index 1a63b4f42d..a2bb74c15c 100644 --- a/apps/explorer/src/explorer/pages/TransactionDetails.tsx +++ b/apps/explorer/src/explorer/pages/TransactionDetails.tsx @@ -17,12 +17,14 @@ const TransactionDetails = () => { return } + const txHashWithOx = !txHash || txHash.startsWith('0x') ? txHash : `0x${txHash}` + return ( Transaction Details - {APP_TITLE} - {txHash && } + {txHashWithOx && } ) } From 843e00b0e84e8060b9241c6d4767962df3de96eb Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 5 Nov 2024 19:33:48 +0500 Subject: [PATCH 080/116] fix(hooks): support native currency rescuing from proxy (#5062) * fix(hooks): support native currency rescuing from proxy * chore: alway display rescue funds * chore: display rescue funds always --------- Co-authored-by: Leandro --- .../containers/HooksStoreWidget/index.tsx | 21 ++++-- .../containers/RescueFundsFromProxy/index.tsx | 30 ++++++-- .../useRescueFundsFromProxy.ts | 75 ++++++++++++------- .../TradeWidget/TradeWidgetForm.tsx | 2 +- .../src/hooks/useNativeTokenBalance.ts | 9 ++- 5 files changed, 94 insertions(+), 43 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index ef0ad58b5d..c6217ef6d5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -6,7 +6,7 @@ import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' import { useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet' import { SwapWidget } from 'modules/swap' -import { useIsSellNative } from 'modules/trade' +import { useIsSellNative, useIsWrapOrUnwrap } from 'modules/trade' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' @@ -32,6 +32,7 @@ export function HooksStoreWidget() { const isNativeSell = useIsSellNative() const isChainIdUnsupported = useIsProviderNetworkUnsupported() + const isWrapOrUnwrap = useIsWrapOrUnwrap() const walletType = useIsSmartContractWallet() ? HookDappWalletCompatibility.SMART_CONTRACT @@ -72,13 +73,19 @@ export function HooksStoreWidget() { const shouldNotUseHooks = isNativeSell || isChainIdUnsupported - const TopContent = shouldNotUseHooks ? null : ( + const HooksTop = ( + + setRescueWidgetOpen(true)}>Rescue funds + + ) + + const TopContent = shouldNotUseHooks ? ( + HooksTop + ) : isWrapOrUnwrap ? ( + HooksTop + ) : ( <> - {!isRescueWidgetOpen && account && ( - - setRescueWidgetOpen(true)}>Rescue funds - - )} + {!isRescueWidgetOpen && account && HooksTop} (undefined) @@ -37,6 +39,7 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { const selectedTokenAddress = selectedCurrency ? getCurrencyAddress(selectedCurrency) : undefined const hasBalance = !!tokenBalance?.greaterThan(0) + const isNativeToken = !!selectedCurrency && getIsNativeToken(selectedCurrency) const { chainId } = useWalletInfo() const { ErrorModal, handleSetError } = useErrorModal() @@ -55,18 +58,33 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { callback: rescueFundsCallback, isTxSigningInProgress, proxyAddress, - } = useRescueFundsFromProxy(selectedTokenAddress, tokenBalance) + } = useRescueFundsFromProxy(selectedTokenAddress, tokenBalance, isNativeToken) - const { isLoading: isBalanceLoading } = useSWR( - erc20Contract && proxyAddress && selectedCurrency ? [erc20Contract, proxyAddress, selectedCurrency] : null, + const { isLoading: isErc20BalanceLoading } = useSWR( + !isNativeToken && erc20Contract && proxyAddress && selectedCurrency + ? [erc20Contract, proxyAddress, selectedCurrency] + : null, async ([erc20Contract, proxyAddress, selectedCurrency]) => { const balance = await erc20Contract.balanceOf(proxyAddress) setTokenBalance(CurrencyAmount.fromRawAmount(selectedCurrency, balance.toHexString())) }, - { refreshInterval: BALANCE_UPDATE_INTERVAL, revalidateOnFocus: true }, + BALANCE_SWR_CFG, ) + const { isLoading: isNativeBalanceLoading, data: nativeTokenBalance } = useNativeTokenBalance( + isNativeToken ? proxyAddress : undefined, + BALANCE_SWR_CFG, + ) + + useEffect(() => { + if (!selectedCurrency || !nativeTokenBalance) return + + setTokenBalance(CurrencyAmount.fromRawAmount(selectedCurrency, nativeTokenBalance.toHexString())) + }, [selectedCurrency, nativeTokenBalance]) + + const isBalanceLoading = isErc20BalanceLoading || isNativeBalanceLoading + const rescueFunds = useCallback(async () => { try { const txHash = await rescueFundsCallback() diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts index d5bafe2f30..ba687ecb40 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts @@ -21,6 +21,7 @@ const fnCalldata = (sig: string, encodedData: string) => pack(['bytes4', 'bytes' export function useRescueFundsFromProxy( selectedTokenAddress: string | undefined, tokenBalance: CurrencyAmount | null, + isNativeToken: boolean, ) { const [isTxSigningInProgress, setTxSigningInProgress] = useState(false) @@ -50,31 +51,44 @@ export function useRescueFundsFromProxy( setTxSigningInProgress(true) try { - const calls: ICoWShedCall[] = [ - { - target: selectedTokenAddress, - callData: fnCalldata( - 'approve(address,uint256)', - defaultAbiCoder.encode(['address', 'uint256'], [proxyAddress, tokenBalance.quotient.toString()]), - ), - value: 0n, - isDelegateCall: false, - allowFailure: false, - }, - { - target: selectedTokenAddress, - callData: fnCalldata( - 'transferFrom(address,address,uint256)', - defaultAbiCoder.encode( - ['address', 'address', 'uint256'], - [proxyAddress, account, tokenBalance.quotient.toString()], - ), - ), - value: 0n, - isDelegateCall: false, - allowFailure: false, - }, - ] + const calls: ICoWShedCall[] = isNativeToken + ? [ + { + target: account, + callData: fnCalldata( + 'send(uint256)', + defaultAbiCoder.encode(['uint256'], [tokenBalance.quotient.toString()]), + ), + value: BigInt(tokenBalance.quotient.toString()), + isDelegateCall: false, + allowFailure: false, + }, + ] + : [ + { + target: selectedTokenAddress, + callData: fnCalldata( + 'approve(address,uint256)', + defaultAbiCoder.encode(['address', 'uint256'], [proxyAddress, tokenBalance.quotient.toString()]), + ), + value: 0n, + isDelegateCall: false, + allowFailure: false, + }, + { + target: selectedTokenAddress, + callData: fnCalldata( + 'transferFrom(address,address,uint256)', + defaultAbiCoder.encode( + ['address', 'address', 'uint256'], + [proxyAddress, account, tokenBalance.quotient.toString()], + ), + ), + value: 0n, + isDelegateCall: false, + allowFailure: false, + }, + ] const nonce = formatBytes32String(Date.now().toString()) // This field is supposed to be used with orders, but here we just do a transaction @@ -96,7 +110,16 @@ export function useRescueFundsFromProxy( } finally { setTxSigningInProgress(false) } - }, [provider, proxyAddress, cowShedContract, selectedTokenAddress, account, tokenBalance, cowShedHooks]) + }, [ + provider, + proxyAddress, + cowShedContract, + selectedTokenAddress, + account, + tokenBalance, + cowShedHooks, + isNativeToken, + ]) return { callback, isTxSigningInProgress, proxyAddress } } diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index e4b10ac01e..6866a903ba 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -178,7 +178,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { lockScreen ) : ( <> - {!isWrapOrUnwrap && topContent} + {topContent}
    { +export function useNativeTokenBalance( + account: string | undefined, + swrConfig: SWRConfiguration = SWR_CONFIG, +): SWRResponse { const provider = useWalletProvider() return useSWR( @@ -17,6 +20,6 @@ export function useNativeTokenBalance(account: string | undefined): SWRResponse< return contract.callStatic.getEthBalance(_account) }, - SWR_CONFIG + swrConfig, ) } From 9c79c0cdaea7603627e11fcb631032817ac611a5 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 6 Nov 2024 13:54:38 +0500 Subject: [PATCH 081/116] chore: hide cow amm banner under feature flag (#5071) --- .../src/modules/application/containers/App/index.tsx | 3 ++- .../src/modules/tokensList/pure/TokensVirtualList/index.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 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 edbd3719bb..d3502104e3 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -53,6 +53,7 @@ export function App() { useInitializeUtm() const featureFlags = useFeatureFlags() + const { isYieldEnabled } = featureFlags const isInjectedWidgetMode = isInjectedWidget() const menuItems = useMenuItems() @@ -139,7 +140,7 @@ export function App() { /> )} - + {isYieldEnabled && } diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index 81575c9bc3..ee6e69e545 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useMemo } from 'react' import { TokenWithLogo } from '@cowprotocol/common-const' +import { useFeatureFlags } from '@cowprotocol/common-hooks' import { VirtualItem } from '@tanstack/react-virtual' @@ -31,6 +32,7 @@ export function TokensVirtualList(props: TokensVirtualListProps) { const { values: balances } = balancesState const isWalletConnected = !!account + const { isYieldEnabled } = useFeatureFlags() const sortedTokens = useMemo(() => { return balances ? allTokens.sort(tokensListSorter(balances)) : allTokens @@ -59,7 +61,7 @@ export function TokensVirtualList(props: TokensVirtualListProps) { return ( - {displayLpTokenLists ? null : } + {displayLpTokenLists || !isYieldEnabled ? null : } ) } From c43d79f0c459e5c8618cd4af66ffa44da0d56c7b Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 6 Nov 2024 14:25:47 +0500 Subject: [PATCH 082/116] chore: hide lp-token lists under feature flag (#5072) --- .../src/modules/application/containers/App/Updaters.tsx | 3 ++- libs/tokens/src/const/tokensLists.ts | 5 +---- libs/tokens/src/state/environmentAtom.ts | 1 + libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts | 8 +++++--- libs/tokens/src/updaters/TokensListsUpdater/index.ts | 6 ++++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index e9f3d981ad..e56d145d0f 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -38,7 +38,7 @@ export function Updaters() { const { chainId, account } = useWalletInfo() const { tokenLists, appCode, customTokens, standaloneMode } = useInjectedWidgetParams() const onTokenListAddingError = useOnTokenListAddingError() - const { isGeoBlockEnabled } = useFeatureFlags() + const { isGeoBlockEnabled, isYieldEnabled } = useFeatureFlags() const tradeTypeInfo = useTradeTypeInfo() const isYieldWidget = tradeTypeInfo?.tradeType === TradeType.YIELD @@ -74,6 +74,7 @@ export function Updaters() { chainId={chainId} isGeoBlockEnabled={isGeoBlockEnabled} enableLpTokensByDefault={isYieldWidget} + isYieldEnabled={isYieldEnabled} /> -export const DEFAULT_TOKENS_LISTS: ListsSourcesByNetwork = mapSupportedNetworks((chainId) => [ - ...tokensList[chainId], - ...LP_TOKEN_LISTS, -]) +export const DEFAULT_TOKENS_LISTS: ListsSourcesByNetwork = mapSupportedNetworks((chainId) => tokensList[chainId]) export const UNISWAP_TOKENS_LIST = 'https://ipfs.io/ipns/tokens.uniswap.org' diff --git a/libs/tokens/src/state/environmentAtom.ts b/libs/tokens/src/state/environmentAtom.ts index 1dabe73245..6b72dc028a 100644 --- a/libs/tokens/src/state/environmentAtom.ts +++ b/libs/tokens/src/state/environmentAtom.ts @@ -7,6 +7,7 @@ interface TokensModuleEnvironment { chainId: SupportedChainId useCuratedListOnly?: boolean enableLpTokensByDefault?: boolean + isYieldEnabled?: boolean widgetAppCode?: string selectedLists?: string[] } diff --git a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts index 7b549a2a51..f9616bebc6 100644 --- a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts +++ b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts @@ -44,14 +44,16 @@ export const userAddedListsSourcesAtom = atomWithStorage( ) export const allListsSourcesAtom = atom((get) => { - const { chainId, useCuratedListOnly } = get(environmentAtom) + const { chainId, useCuratedListOnly, isYieldEnabled } = get(environmentAtom) const userAddedTokenLists = get(userAddedListsSourcesAtom) + const lpLists = isYieldEnabled ? LP_TOKEN_LISTS : [] + if (useCuratedListOnly) { - return [get(curatedListSourceAtom), ...LP_TOKEN_LISTS, ...userAddedTokenLists[chainId]] + return [get(curatedListSourceAtom), ...lpLists, ...userAddedTokenLists[chainId]] } - return [...DEFAULT_TOKENS_LISTS[chainId], ...(userAddedTokenLists[chainId] || [])] + return [...DEFAULT_TOKENS_LISTS[chainId], ...lpLists, ...(userAddedTokenLists[chainId] || [])] }) // Lists states diff --git a/libs/tokens/src/updaters/TokensListsUpdater/index.ts b/libs/tokens/src/updaters/TokensListsUpdater/index.ts index e49aabd761..08eae3540d 100644 --- a/libs/tokens/src/updaters/TokensListsUpdater/index.ts +++ b/libs/tokens/src/updaters/TokensListsUpdater/index.ts @@ -36,6 +36,7 @@ interface TokensListsUpdaterProps { chainId: SupportedChainId isGeoBlockEnabled: boolean enableLpTokensByDefault: boolean + isYieldEnabled: boolean } /** @@ -50,6 +51,7 @@ export function TokensListsUpdater({ chainId: currentChainId, isGeoBlockEnabled, enableLpTokensByDefault, + isYieldEnabled, }: TokensListsUpdaterProps) { const { chainId } = useAtomValue(environmentAtom) const setEnvironment = useSetAtom(updateEnvironmentAtom) @@ -61,8 +63,8 @@ export function TokensListsUpdater({ const upsertLists = useSetAtom(upsertListsAtom) useEffect(() => { - setEnvironment({ chainId: currentChainId, enableLpTokensByDefault }) - }, [setEnvironment, currentChainId, enableLpTokensByDefault]) + setEnvironment({ chainId: currentChainId, enableLpTokensByDefault, isYieldEnabled }) + }, [setEnvironment, currentChainId, enableLpTokensByDefault, isYieldEnabled]) // Fetch tokens lists once in 6 hours const { data: listsStates, isLoading } = useSWR( From c64f6e3c5e73e7c98b762c4b5af179acdb99a9fe Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 6 Nov 2024 14:34:46 +0500 Subject: [PATCH 083/116] chore: release main (#5073) --- .release-please-manifest.json | 17 +++++++++-------- apps/cowswap-frontend/CHANGELOG.md | 19 +++++++++++++++++++ apps/cowswap-frontend/package.json | 2 +- apps/explorer/CHANGELOG.md | 7 +++++++ apps/explorer/package.json | 2 +- libs/balances-and-allowances/CHANGELOG.md | 15 +++++++++++++++ libs/balances-and-allowances/package.json | 2 +- libs/common-const/CHANGELOG.md | 9 +++++++++ libs/common-const/package.json | 2 +- libs/multicall/CHANGELOG.md | 8 ++++++++ libs/multicall/package.json | 2 +- libs/tokens/CHANGELOG.md | 11 +++++++++++ libs/tokens/package.json | 2 +- libs/types/CHANGELOG.md | 8 ++++++++ libs/types/package.json | 2 +- libs/ui/CHANGELOG.md | 7 +++++++ libs/ui/package.json | 2 +- 17 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 libs/multicall/CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 276d8577e5..e16061946e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,28 +1,29 @@ { - "apps/cowswap-frontend": "1.87.0", - "apps/explorer": "2.36.0", + "apps/cowswap-frontend": "1.88.0", + "apps/explorer": "2.36.1", "libs/permit-utils": "0.4.0", "libs/widget-lib": "0.17.0", "libs/widget-react": "0.11.0", "apps/widget-configurator": "1.9.0", "libs/analytics": "1.8.0", "libs/assets": "1.9.0", - "libs/common-const": "1.9.0", + "libs/common-const": "1.10.0", "libs/common-hooks": "1.4.0", "libs/common-utils": "1.7.2", "libs/core": "1.3.0", "libs/ens": "1.2.0", "libs/events": "1.5.0", "libs/snackbars": "1.1.0", - "libs/tokens": "1.10.0", - "libs/types": "1.3.0", - "libs/ui": "1.12.0", + "libs/tokens": "1.11.0", + "libs/types": "1.4.0", + "libs/ui": "1.13.0", "libs/wallet": "1.6.1", "apps/cow-fi": "1.16.0", "libs/wallet-provider": "1.0.0", "libs/ui-utils": "1.1.0", "libs/abis": "1.2.0", - "libs/balances-and-allowances": "1.0.0", + "libs/balances-and-allowances": "1.1.0", "libs/iframe-transport": "1.0.0", - "libs/hook-dapp-lib": "1.3.0" + "libs/hook-dapp-lib": "1.3.0", + "libs/multicall": "1.0.0" } diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index dfe323fc57..2e812231f2 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [1.88.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.87.0...cowswap-v1.88.0) (2024-11-06) + + +### Features + +* **protocol-fees:** arb1 protocol fee ([#5055](https://github.com/cowprotocol/cowswap/issues/5055)) ([ed176c3](https://github.com/cowprotocol/cowswap/commit/ed176c3ab95fe51065a905e05ca184f3abf7e282)) +* **yield:** define token category by default for selection ([#5018](https://github.com/cowprotocol/cowswap/issues/5018)) ([7c18b7d](https://github.com/cowprotocol/cowswap/commit/7c18b7d85de6feac9c7e64740a93572f3af3c273)) +* **yield:** display cow amm banner conditionally ([#5035](https://github.com/cowprotocol/cowswap/issues/5035)) ([1a517a3](https://github.com/cowprotocol/cowswap/commit/1a517a3f21b94c10b8e59e68bc49a569c1be904b)) +* **yield:** display pools info in widget ([#5046](https://github.com/cowprotocol/cowswap/issues/5046)) ([562d020](https://github.com/cowprotocol/cowswap/commit/562d0207d1acf4e1735c4b3f629ff63dd65d3725)) +* **yield:** use lp-token in widget ([#5013](https://github.com/cowprotocol/cowswap/issues/5013)) ([b66d206](https://github.com/cowprotocol/cowswap/commit/b66d2068a9f3bcaddc8da7df5499c17fc05f693f)) + + +### Bug Fixes + +* **hooks:** support native currency rescuing from proxy ([#5062](https://github.com/cowprotocol/cowswap/issues/5062)) ([843e00b](https://github.com/cowprotocol/cowswap/commit/843e00b0e84e8060b9241c6d4767962df3de96eb)) +* remove isNotificationsFeedEnabled ([#5054](https://github.com/cowprotocol/cowswap/issues/5054)) ([0fbb9b5](https://github.com/cowprotocol/cowswap/commit/0fbb9b585c4beb0978309c8ebda7e8aa1f8bf57c)) +* **swap:** take slippage into account for buy orders ([#5067](https://github.com/cowprotocol/cowswap/issues/5067)) ([21f39fe](https://github.com/cowprotocol/cowswap/commit/21f39fe3811fb65d6f1a98bb2185321f378afdd4)) +* **widget:** remove irrelevant change trade params events ([#5060](https://github.com/cowprotocol/cowswap/issues/5060)) ([6ae8ca1](https://github.com/cowprotocol/cowswap/commit/6ae8ca1569d9248bb8f82bb3fea777eb03f12d49)) + ## [1.87.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.86.1...cowswap-v1.87.0) (2024-10-29) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index e0510774fe..5dbe5ce687 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.87.0", + "version": "1.88.0", "description": "CoW Swap", "main": "index.js", "author": "", diff --git a/apps/explorer/CHANGELOG.md b/apps/explorer/CHANGELOG.md index b35339d854..ca26b0bc85 100644 --- a/apps/explorer/CHANGELOG.md +++ b/apps/explorer/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.36.1](https://github.com/cowprotocol/cowswap/compare/explorer-v2.36.0...explorer-v2.36.1) (2024-11-06) + + +### Bug Fixes + +* **explorer:** make sure txHash always has the 0x prefix ([#5061](https://github.com/cowprotocol/cowswap/issues/5061)) ([9054969](https://github.com/cowprotocol/cowswap/commit/9054969abcc4daceb16bbc04e186918fb092e3db)) + ## [2.36.0](https://github.com/cowprotocol/cowswap/compare/explorer-v2.35.2...explorer-v2.36.0) (2024-10-18) diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 23375d4751..916c8ab4bb 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/explorer", - "version": "2.36.0", + "version": "2.36.1", "description": "CoW Swap Explorer", "main": "src/main.tsx", "author": "", diff --git a/libs/balances-and-allowances/CHANGELOG.md b/libs/balances-and-allowances/CHANGELOG.md index f59e1ebd4e..dda07590fa 100644 --- a/libs/balances-and-allowances/CHANGELOG.md +++ b/libs/balances-and-allowances/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.1.0](https://github.com/cowprotocol/cowswap/compare/balances-and-allowances-v1.0.0...balances-and-allowances-v1.1.0) (2024-11-06) + + +### Features + +* **yield:** define token category by default for selection ([#5018](https://github.com/cowprotocol/cowswap/issues/5018)) ([7c18b7d](https://github.com/cowprotocol/cowswap/commit/7c18b7d85de6feac9c7e64740a93572f3af3c273)) +* **yield:** display cow amm banner conditionally ([#5035](https://github.com/cowprotocol/cowswap/issues/5035)) ([1a517a3](https://github.com/cowprotocol/cowswap/commit/1a517a3f21b94c10b8e59e68bc49a569c1be904b)) +* **yield:** fetch balances for LP-tokens ([#5005](https://github.com/cowprotocol/cowswap/issues/5005)) ([2877df5](https://github.com/cowprotocol/cowswap/commit/2877df52be2fd519a20157a1cd91a2e18e954dae)) +* **yield:** use lp-token in widget ([#5013](https://github.com/cowprotocol/cowswap/issues/5013)) ([b66d206](https://github.com/cowprotocol/cowswap/commit/b66d2068a9f3bcaddc8da7df5499c17fc05f693f)) + + +### Bug Fixes + +* **hooks:** support native currency rescuing from proxy ([#5062](https://github.com/cowprotocol/cowswap/issues/5062)) ([843e00b](https://github.com/cowprotocol/cowswap/commit/843e00b0e84e8060b9241c6d4767962df3de96eb)) + ## 1.0.0 (2024-07-12) diff --git a/libs/balances-and-allowances/package.json b/libs/balances-and-allowances/package.json index a315d1888f..1e83c54709 100644 --- a/libs/balances-and-allowances/package.json +++ b/libs/balances-and-allowances/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/balances-and-allowances", - "version": "1.0.0", + "version": "1.1.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/common-const/CHANGELOG.md b/libs/common-const/CHANGELOG.md index b47cf7ba50..c45296eaa0 100644 --- a/libs/common-const/CHANGELOG.md +++ b/libs/common-const/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.10.0](https://github.com/cowprotocol/cowswap/compare/common-const-v1.9.0...common-const-v1.10.0) (2024-11-06) + + +### Features + +* **protocol-fees:** arb1 protocol fee ([#5055](https://github.com/cowprotocol/cowswap/issues/5055)) ([ed176c3](https://github.com/cowprotocol/cowswap/commit/ed176c3ab95fe51065a905e05ca184f3abf7e282)) +* **yield:** define token category by default for selection ([#5018](https://github.com/cowprotocol/cowswap/issues/5018)) ([7c18b7d](https://github.com/cowprotocol/cowswap/commit/7c18b7d85de6feac9c7e64740a93572f3af3c273)) +* **yield:** display cow amm banner conditionally ([#5035](https://github.com/cowprotocol/cowswap/issues/5035)) ([1a517a3](https://github.com/cowprotocol/cowswap/commit/1a517a3f21b94c10b8e59e68bc49a569c1be904b)) + ## [1.9.0](https://github.com/cowprotocol/cowswap/compare/common-const-v1.8.0...common-const-v1.9.0) (2024-10-29) diff --git a/libs/common-const/package.json b/libs/common-const/package.json index 5e80a4a585..e0b8f061d9 100644 --- a/libs/common-const/package.json +++ b/libs/common-const/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/common-const", - "version": "1.9.0", + "version": "1.10.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/multicall/CHANGELOG.md b/libs/multicall/CHANGELOG.md new file mode 100644 index 0000000000..1fa54ddc70 --- /dev/null +++ b/libs/multicall/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 1.0.0 (2024-11-06) + + +### Features + +* **yield:** fetch balances for LP-tokens ([#5005](https://github.com/cowprotocol/cowswap/issues/5005)) ([2877df5](https://github.com/cowprotocol/cowswap/commit/2877df52be2fd519a20157a1cd91a2e18e954dae)) diff --git a/libs/multicall/package.json b/libs/multicall/package.json index 13a5e7a5a1..b5dcd1cc1b 100644 --- a/libs/multicall/package.json +++ b/libs/multicall/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/multicall", - "version": "0.0.1", + "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/tokens/CHANGELOG.md b/libs/tokens/CHANGELOG.md index 8d964b2abe..d515d5fab8 100644 --- a/libs/tokens/CHANGELOG.md +++ b/libs/tokens/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.11.0](https://github.com/cowprotocol/cowswap/compare/tokens-v1.10.0...tokens-v1.11.0) (2024-11-06) + + +### Features + +* **yield:** define token category by default for selection ([#5018](https://github.com/cowprotocol/cowswap/issues/5018)) ([7c18b7d](https://github.com/cowprotocol/cowswap/commit/7c18b7d85de6feac9c7e64740a93572f3af3c273)) +* **yield:** display cow amm banner conditionally ([#5035](https://github.com/cowprotocol/cowswap/issues/5035)) ([1a517a3](https://github.com/cowprotocol/cowswap/commit/1a517a3f21b94c10b8e59e68bc49a569c1be904b)) +* **yield:** display pools info in widget ([#5046](https://github.com/cowprotocol/cowswap/issues/5046)) ([562d020](https://github.com/cowprotocol/cowswap/commit/562d0207d1acf4e1735c4b3f629ff63dd65d3725)) +* **yield:** fetch balances for LP-tokens ([#5005](https://github.com/cowprotocol/cowswap/issues/5005)) ([2877df5](https://github.com/cowprotocol/cowswap/commit/2877df52be2fd519a20157a1cd91a2e18e954dae)) +* **yield:** use lp-token in widget ([#5013](https://github.com/cowprotocol/cowswap/issues/5013)) ([b66d206](https://github.com/cowprotocol/cowswap/commit/b66d2068a9f3bcaddc8da7df5499c17fc05f693f)) + ## [1.10.0](https://github.com/cowprotocol/cowswap/compare/tokens-v1.9.0...tokens-v1.10.0) (2024-10-10) diff --git a/libs/tokens/package.json b/libs/tokens/package.json index f78001ec0b..0c8f390999 100644 --- a/libs/tokens/package.json +++ b/libs/tokens/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/tokens", - "version": "1.10.0", + "version": "1.11.0", "main": "./index.js", "types": "./index.d.ts", "exports": { diff --git a/libs/types/CHANGELOG.md b/libs/types/CHANGELOG.md index eda2d52621..feeb61cb73 100644 --- a/libs/types/CHANGELOG.md +++ b/libs/types/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.4.0](https://github.com/cowprotocol/cowswap/compare/types-v1.3.0...types-v1.4.0) (2024-11-06) + + +### Features + +* **yield:** define token category by default for selection ([#5018](https://github.com/cowprotocol/cowswap/issues/5018)) ([7c18b7d](https://github.com/cowprotocol/cowswap/commit/7c18b7d85de6feac9c7e64740a93572f3af3c273)) +* **yield:** display cow amm banner conditionally ([#5035](https://github.com/cowprotocol/cowswap/issues/5035)) ([1a517a3](https://github.com/cowprotocol/cowswap/commit/1a517a3f21b94c10b8e59e68bc49a569c1be904b)) + ## [1.3.0](https://github.com/cowprotocol/cowswap/compare/types-v1.2.0...types-v1.3.0) (2024-10-29) diff --git a/libs/types/package.json b/libs/types/package.json index f56e8b501a..4d97b54f24 100644 --- a/libs/types/package.json +++ b/libs/types/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/types", - "version": "1.3.0", + "version": "1.4.0", "type": "commonjs", "description": "CoW Swap events", "main": "index.js", diff --git a/libs/ui/CHANGELOG.md b/libs/ui/CHANGELOG.md index 5969b9e077..f59104065d 100644 --- a/libs/ui/CHANGELOG.md +++ b/libs/ui/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.13.0](https://github.com/cowprotocol/cowswap/compare/ui-v1.12.0...ui-v1.13.0) (2024-11-06) + + +### Features + +* **yield:** display pools info in widget ([#5046](https://github.com/cowprotocol/cowswap/issues/5046)) ([562d020](https://github.com/cowprotocol/cowswap/commit/562d0207d1acf4e1735c4b3f629ff63dd65d3725)) + ## [1.12.0](https://github.com/cowprotocol/cowswap/compare/ui-v1.11.0...ui-v1.12.0) (2024-10-29) diff --git a/libs/ui/package.json b/libs/ui/package.json index 1458c7fdd0..721059f641 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/ui", - "version": "1.12.0", + "version": "1.13.0", "main": "./index.js", "types": "./index.d.ts", "exports": { From 03814381094b9ec02b4cb068b7e77594c5450ae6 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 11 Nov 2024 10:21:29 +0000 Subject: [PATCH 084/116] Tenderly Simulations + Fix lint (#5080) * chore: init tenderly module * feat: enable bundle simulations * refactor: change bundle simulation to use SWR * chore: consider custom recipient * chore: remove goldrush sdk * fix: error on post hooks get simulation * refactor: use bff on bundle simulation feature * chore: remove console.logs * chore: fix leandro comments * chore: remove unused tenderly consts * refactor: rename tenderly simulation hook * chore: refactor top token holder swr to jotai with cache * chore: rename hook to match file name * refactor: use seconds for cache time in toptokenholder state * chore: create hooks to use token balances with pre hooks * chore: use combined hooks on swap components * feat: expose balance diff to hook dapp context * chore: add testing console log for balances diff access on hook dapp * feat: use tenderly simulation gas on hook creation * refactor: implement PR code suggestions * chore: remove console logs * fix: use lower case address on combined balance hook * fix: not use combined balance on non hook trade type * fix: trigger simulation on order change if simulation data is null * refactor: simulate hooks even if order isnt filled * chore: improve variable documentation and simplify functions * chore: remove simulated gas buffer * feat: add refresh simulation button * fix: not use tenderly gas used if failed * fix: infinite load on order change * feat: trigger extra hook simulation on order review * fix: fix lint --------- Co-authored-by: Pedro Yves Fracari Co-authored-by: Pedro Yves Fracari <55461956+yvesfracari@users.noreply.github.com> --- .../containers/OrderHooksDetails/index.tsx | 13 +- .../containers/OrderHooksDetails/styled.tsx | 29 ++++ .../appData/updater/AppDataHooksUpdater.ts | 4 +- .../hooks/useCurrencyAmountBalanceCombined.ts | 22 +++ .../hooks/useTokensBalancesCombined.ts | 45 ++++++ .../src/modules/combinedBalances/index.ts | 2 + .../containers/HookDappContainer/index.tsx | 3 + .../hooksStore/hooks/useBalancesDiff.ts | 124 +++++++++++++++ .../hooks/useHooksStateWithSimulatedGas.ts | 30 ++++ .../src/modules/hooksStore/index.ts | 1 + .../hooksStore/pure/AppliedHookItem/index.tsx | 7 +- .../swap/containers/SwapWidget/index.tsx | 7 +- .../swap/hooks/useSwapButtonContext.ts | 7 +- .../src/modules/swap/hooks/useSwapState.tsx | 6 +- .../tenderly/hooks/useGetTopTokenHolders.ts | 5 +- .../hooks/useTenderlyBundleSimulation.ts | 146 ++++++++++-------- .../src/modules/tenderly/types.ts | 6 + .../tenderly/utils/bundleSimulation.ts | 22 +-- .../tenderly/utils/generateSimulationData.ts | 30 +++- .../modules/tokens/hooks/useEnoughBalance.ts | 15 +- .../containers/SelectTokenWidget/index.tsx | 4 +- .../trade/hooks/useBuildTradeDerivedState.ts | 12 +- libs/hook-dapp-lib/src/types.ts | 3 + 23 files changed, 430 insertions(+), 113 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts create mode 100644 apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts create mode 100644 apps/cowswap-frontend/src/modules/combinedBalances/index.ts create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index 06d2800412..a11e2272ab 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useMemo, useState } from 'react' +import { ReactElement, useEffect, useMemo, useState } from 'react' import { latest } from '@cowprotocol/app-data' import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' @@ -8,6 +8,7 @@ import { ChevronDown, ChevronUp } from 'react-feather' import { AppDataInfo, decodeAppData } from 'modules/appData' import { useCustomHookDapps } from 'modules/hooksStore' +import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' import { HookItem } from './HookItem' import * as styledEl from './styled' @@ -27,10 +28,18 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai const preCustomHookDapps = useCustomHookDapps(true) const postCustomHookDapps = useCustomHookDapps(false) + const { mutate, isValidating, data } = useTenderlyBundleSimulation() + + useEffect(() => { + mutate() + }, []) + if (!appDataDoc) return null const metadata = appDataDoc.metadata as latest.Metadata + const hasSomeFailedSimulation = Object.values(data || {}).some((hook) => !hook.status) + const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || [], preCustomHookDapps) const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || [], postCustomHookDapps) @@ -42,6 +51,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai Hooks + {hasSomeFailedSimulation && Simulation failed} + {isValidating && } setOpen(!isOpen)}> {preHooksToDapp.length > 0 && ( diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx index 55f511bc35..dfc2f9716c 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx @@ -33,6 +33,17 @@ export const Label = styled.span` gap: 4px; ` +export const ErrorLabel = styled.span` + display: flex; + align-items: center; + gap: 4px; + color: var(${UI.COLOR_DANGER_TEXT}); + background-color: var(${UI.COLOR_DANGER_BG}); + border-radius: 8px; + margin-left: 4px; + padding: 2px 6px; +` + export const Content = styled.div` display: flex; width: max-content; @@ -164,3 +175,21 @@ export const CircleCount = styled.span` font-weight: var(${UI.FONT_WEIGHT_BOLD}); margin: 0; ` + +export const Spinner = styled.div` + border: 5px solid transparent; + border-top-color: ${`var(${UI.COLOR_PRIMARY_LIGHTER})`}; + border-radius: 50%; + width: 12px; + height: 12px; + animation: spin 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +` diff --git a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts index b58c1e5130..29bc7a012d 100644 --- a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts +++ b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts @@ -6,7 +6,7 @@ import { useIsSmartContractWallet } from '@cowprotocol/wallet' import { Nullish } from 'types' -import { useHooks } from 'modules/hooksStore' +import { useHooksStateWithSimulatedGas } from 'modules/hooksStore' import { useAccountAgnosticPermitHookData } from 'modules/permit' import { useDerivedTradeState, useHasTradeEnoughAllowance, useIsHooksTradeType, useIsSellNative } from 'modules/trade' @@ -33,7 +33,7 @@ function useAgnosticPermitDataIfUserHasNoAllowance(): Nullish { export function AppDataHooksUpdater(): null { const tradeState = useDerivedTradeState() const isHooksTradeType = useIsHooksTradeType() - const hooksStoreState = useHooks() + const hooksStoreState = useHooksStateWithSimulatedGas() const preHooks = isHooksTradeType ? hooksStoreState.preHooks : null const postHooks = isHooksTradeType ? hooksStoreState.postHooks : null const updateAppDataHooks = useUpdateAppDataHooks() diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts new file mode 100644 index 0000000000..5977042e25 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts @@ -0,0 +1,22 @@ +import { useMemo } from 'react' + +import { TokenWithLogo } from '@cowprotocol/common-const' +import { CurrencyAmount } from '@uniswap/sdk-core' + +import { useTokensBalancesCombined } from './useTokensBalancesCombined' + +export function useCurrencyAmountBalanceCombined( + token: TokenWithLogo | undefined | null, +): CurrencyAmount | undefined { + const { values: balances } = useTokensBalancesCombined() + + return useMemo(() => { + if (!token) return undefined + + const balance = balances[token.address.toLowerCase()] + + if (!balance) return undefined + + return CurrencyAmount.fromRawAmount(token, balance.toHexString()) + }, [token, balances]) +} diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts new file mode 100644 index 0000000000..0ad8abb306 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts @@ -0,0 +1,45 @@ +import { useMemo } from 'react' + +import { BalancesState, useTokensBalances } from '@cowprotocol/balances-and-allowances' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { BigNumber } from 'ethers' + +import { usePreHookBalanceDiff } from 'modules/hooksStore/hooks/useBalancesDiff' +import { useIsHooksTradeType } from 'modules/trade' + +export function useTokensBalancesCombined() { + const { account } = useWalletInfo() + const preHooksBalancesDiff = usePreHookBalanceDiff() + const tokenBalances = useTokensBalances() + const isHooksTradeType = useIsHooksTradeType() + + return useMemo(() => { + if (!account || !isHooksTradeType) return tokenBalances + const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {} + return applyBalanceDiffs(tokenBalances, accountBalancesDiff) + }, [account, preHooksBalancesDiff, tokenBalances, isHooksTradeType]) +} + +function applyBalanceDiffs(currentBalances: BalancesState, balanceDiff: Record): BalancesState { + // Get all unique addresses from both objects + const allAddresses = [...new Set([...Object.keys(currentBalances.values), ...Object.keys(balanceDiff)])] + + const normalizedValues = allAddresses.reduce( + (acc, address) => { + const currentBalance = currentBalances.values[address] || BigNumber.from(0) + const diff = balanceDiff[address] ? BigNumber.from(balanceDiff[address]) : BigNumber.from(0) + + return { + ...acc, + [address]: currentBalance.add(diff), + } + }, + {} as Record, + ) + + return { + isLoading: currentBalances.isLoading, + values: normalizedValues, + } +} diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/index.ts b/apps/cowswap-frontend/src/modules/combinedBalances/index.ts new file mode 100644 index 0000000000..3360595102 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/combinedBalances/index.ts @@ -0,0 +1,2 @@ +export * from './hooks/useCurrencyAmountBalanceCombined' +export * from './hooks/useTokensBalancesCombined' diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index f678cefe26..d431207b07 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -9,6 +9,7 @@ import { useIsDarkMode } from 'legacy/state/user/hooks' import { useTradeState, useTradeNavigate } from 'modules/trade' import { useAddHook } from '../../hooks/useAddHook' +import { useHookBalancesDiff } from '../../hooks/useBalancesDiff' import { useEditHook } from '../../hooks/useEditHook' import { useHookById } from '../../hooks/useHookById' import { useOrderParams } from '../../hooks/useOrderParams' @@ -35,6 +36,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho const tradeState = useTradeState() const tradeNavigate = useTradeNavigate() const isDarkMode = useIsDarkMode() + const balancesDiff = useHookBalancesDiff(isPreHook, hookToEditDetails?.uuid) const { inputCurrencyId = null, outputCurrencyId = null } = tradeState.state || {} const signer = useMemo(() => provider?.getSigner(), [provider]) @@ -49,6 +51,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho isSmartContract, isPreHook, isDarkMode, + balancesDiff, editHook(...args) { editHook(...args) onDismiss() diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts new file mode 100644 index 0000000000..e701ba1bcc --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts @@ -0,0 +1,124 @@ +import { useMemo } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { BigNumber } from 'ethers' + +import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' +import { BalancesDiff } from 'modules/tenderly/types' + +import { useHooks } from './useHooks' +import { useOrderParams } from './useOrderParams' + +const EMPTY_BALANCE_DIFF: BalancesDiff = {} + +export function usePreHookBalanceDiff(): BalancesDiff { + const { data } = useTenderlyBundleSimulation() + + const { preHooks } = useHooks() + + return useMemo(() => { + if (!data || !preHooks.length) return EMPTY_BALANCE_DIFF + + const lastPreHook = preHooks[preHooks.length - 1] + return data[lastPreHook?.uuid]?.cumulativeBalancesDiff || EMPTY_BALANCE_DIFF + }, [data, preHooks]) +} + +// Returns all the ERC20 Balance Diff of the current hook to be passed to the iframe context +export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): BalancesDiff { + const { account } = useWalletInfo() + const { data } = useTenderlyBundleSimulation() + const orderParams = useOrderParams() + const { preHooks, postHooks } = useHooks() + const preHookBalanceDiff = usePreHookBalanceDiff() + + // balance diff expected from the order without the simulation + // this is used when the order isn't simulated like in the case of only preHooks + const orderExpectedBalanceDiff = useMemo(() => { + if (!account) return EMPTY_BALANCE_DIFF + const balanceDiff: Record = {} + + if (orderParams?.buyAmount && orderParams.buyTokenAddress && account) + balanceDiff[orderParams.buyTokenAddress.toLowerCase()] = orderParams.buyAmount + + if (orderParams?.sellAmount && orderParams.sellTokenAddress && account) + balanceDiff[orderParams.sellTokenAddress.toLowerCase()] = `-${orderParams.sellAmount}` + + return { account: balanceDiff } + }, [orderParams, account]) + + const firstPostHookBalanceDiff = useMemo(() => { + return mergeBalanceDiffs(preHookBalanceDiff, orderExpectedBalanceDiff) + }, [preHookBalanceDiff, orderExpectedBalanceDiff]) + + const postHookBalanceDiff = useMemo(() => { + // is adding the first post hook or simulation not available + if (!data || !postHooks) return firstPostHookBalanceDiff + + const lastPostHook = postHooks[postHooks.length - 1] + return data[lastPostHook?.uuid]?.cumulativeBalancesDiff || firstPostHookBalanceDiff + }, [data, postHooks, orderExpectedBalanceDiff, preHookBalanceDiff]) + + const hookToEditBalanceDiff = useMemo(() => { + if (!data || !hookToEditUid) return EMPTY_BALANCE_DIFF + + const otherHooks = isPreHook ? preHooks : postHooks + + const hookToEditIndex = otherHooks.findIndex((hook) => hook.uuid === hookToEditUid) + + // is editing first preHook -> return empty state + if (!hookToEditIndex && isPreHook) return EMPTY_BALANCE_DIFF + + // is editing first postHook -> return + if (!hookToEditIndex && !isPreHook) return firstPostHookBalanceDiff + + // is editing a non first hook, return the latest available hook state + const previousHookIndex = hookToEditIndex - 1 + + return data[otherHooks[previousHookIndex]?.uuid]?.cumulativeBalancesDiff || EMPTY_BALANCE_DIFF + }, [data, hookToEditUid, isPreHook, preHooks, postHooks, firstPostHookBalanceDiff]) + + return useMemo(() => { + if (hookToEditUid) return hookToEditBalanceDiff + if (isPreHook) return preHookBalanceDiff + return postHookBalanceDiff + }, [data, orderParams, preHooks, postHooks]) +} + +function mergeBalanceDiffs(first: BalancesDiff, second: BalancesDiff): BalancesDiff { + const result: BalancesDiff = {} + + // Helper function to add BigNumber strings + + // Process all addresses from first input + for (const address of Object.keys(first)) { + result[address] = { ...first[address] } + } + + // Process all addresses from second input + for (const address of Object.keys(second)) { + if (!result[address]) { + // If address doesn't exist in result, just copy the entire record + result[address] = { ...second[address] } + } else { + // If address exists, we need to merge token balances + for (const token of Object.keys(second[address])) { + if (!result[address][token]) { + // If token doesn't exist for this address, just copy the balance + result[address][token] = second[address][token] + } else { + // If token exists, sum up the balances + try { + result[address][token] = BigNumber.from(result[address][token]).add(second[address][token]).toString() + } catch (error) { + console.error(`Error adding balances for address ${address} and token ${token}:`, error) + throw error + } + } + } + } + } + + return result +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts new file mode 100644 index 0000000000..0bb2ba3aa2 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts @@ -0,0 +1,30 @@ +import { useCallback, useMemo } from 'react' + +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' + +import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' + +import { useHooks } from './useHooks' + +import { HooksStoreState } from '../state/hookDetailsAtom' + +export function useHooksStateWithSimulatedGas(): HooksStoreState { + const hooksRaw = useHooks() + const { data: tenderlyData } = useTenderlyBundleSimulation() + + const combineHookWithSimulatedGas = useCallback( + (hook: CowHookDetails): CowHookDetails => { + const hookTenderlyData = tenderlyData?.[hook.uuid] + if (!hookTenderlyData?.gasUsed || hookTenderlyData.gasUsed === '0' || !hookTenderlyData.status) return hook + const hookData = { ...hook.hook, gasLimit: hookTenderlyData.gasUsed } + return { ...hook, hook: hookData } + }, + [tenderlyData], + ) + + return useMemo(() => { + const preHooksCombined = hooksRaw.preHooks.map(combineHookWithSimulatedGas) + const postHooksCombined = hooksRaw.postHooks.map(combineHookWithSimulatedGas) + return { preHooks: preHooksCombined, postHooks: postHooksCombined } + }, [hooksRaw, combineHookWithSimulatedGas]) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/index.ts b/apps/cowswap-frontend/src/modules/hooksStore/index.ts index 5b922a2406..d54591c6b5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/index.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/index.ts @@ -2,3 +2,4 @@ export { HooksStoreWidget } from './containers/HooksStoreWidget' export { useHooks } from './hooks/useHooks' export { usePostHooksRecipientOverride } from './hooks/usePostHooksRecipientOverride' export { useCustomHookDapps } from './hooks/useCustomHookDapps' +export { useHooksStateWithSimulatedGas } from './hooks/useHooksStateWithSimulatedGas' diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx index d30afa2a05..362c2f0f33 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -7,7 +7,7 @@ import ICON_X from '@cowprotocol/assets/cow-swap/x.svg' import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { InfoTooltip } from '@cowprotocol/ui' -import { Edit2, Trash2, ExternalLink as ExternalLinkIcon } from 'react-feather' +import { Edit2, Trash2, ExternalLink as ExternalLinkIcon, RefreshCw } from 'react-feather' import SVG from 'react-inlinesvg' import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' @@ -31,7 +31,7 @@ interface HookItemProp { const isBundleSimulationReady = true export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) { - const { isValidating, data } = useTenderlyBundleSimulation() + const { isValidating, data, mutate } = useTenderlyBundleSimulation() const simulationData = useMemo(() => { if (!data) return @@ -56,6 +56,9 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo {isValidating && } + mutate()} disabled={isValidating}> + + editHook(hookDetails.uuid)}> 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 d5c0f61f5c..af1def7605 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -1,6 +1,6 @@ import { ReactNode, useCallback, useMemo, useState } from 'react' -import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' +// import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const' import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' @@ -12,6 +12,7 @@ import { ApplicationModal } from 'legacy/state/application/reducer' import { Field } from 'legacy/state/types' import { useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/user/hooks' +import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { EthFlowModal, EthFlowProps } from 'modules/swap/containers/EthFlow' import { SwapModals, SwapModalsProps } from 'modules/swap/containers/SwapModals' @@ -94,8 +95,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { return TokenWithLogo.fromToken(currencies.OUTPUT) }, [chainId, currencies.OUTPUT]) - const inputCurrencyBalance = useCurrencyAmountBalance(inputToken) || null - const outputCurrencyBalance = useCurrencyAmountBalance(outputToken) || null + const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputToken) || null + const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputToken) || null const isSellTrade = independentField === Field.INPUT diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index 933a0b6c14..7b2b65ae8b 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' -import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' +// import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { currencyAmountToTokenAmount, getWrappedToken } from '@cowprotocol/common-utils' import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { @@ -16,6 +16,7 @@ import { useToggleWalletModal } from 'legacy/state/application/hooks' import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/hooks' import { Field } from 'legacy/state/types' +import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useTokenSupportsPermit } from 'modules/permit' import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState' @@ -157,7 +158,9 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge function useHasEnoughWrappedBalanceForSwap(inputAmount?: CurrencyAmount): boolean { const { currencies } = useDerivedSwapInfo() - const wrappedBalance = useCurrencyAmountBalance(currencies.INPUT ? getWrappedToken(currencies.INPUT) : undefined) + const wrappedBalance = useCurrencyAmountBalanceCombined( + currencies.INPUT ? getWrappedToken(currencies.INPUT) : undefined, + ) // is a native currency trade but wrapped token has enough balance return !!(wrappedBalance && inputAmount && !wrappedBalance.lessThan(inputAmount)) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index 4953671631..c73ed33327 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -1,6 +1,5 @@ import { useCallback, useMemo } from 'react' -import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { formatSymbol, getIsNativeToken, isAddress, tryParseCurrencyAmount } from '@cowprotocol/common-utils' import { useENS } from '@cowprotocol/ens' import { useAreThereTokensWithSameSymbol, useTokenBySymbolOrAddress } from '@cowprotocol/tokens' @@ -19,6 +18,7 @@ import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { changeSwapAmountAnalytics, switchTokensAnalytics } from 'modules/analytics' +import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import type { TradeWidgetActions } from 'modules/trade' import { useNavigateOnCurrencySelection } from 'modules/trade/hooks/useNavigateOnCurrencySelection' import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate' @@ -125,8 +125,8 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { const recipientLookup = useENS(recipient ?? undefined) const to: string | null = (recipient ? recipientLookup.address : account) ?? null - const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency) - const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency) + const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputCurrency) + const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputCurrency) const isExactIn: boolean = independentField === Field.INPUT const parsedAmount = useMemo( diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts index 194a086a11..3f074a8009 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts @@ -10,10 +10,7 @@ export function useGetTopTokenHolders() { return useCallback( async (params: GetTopTokenHoldersParams) => { const key = `${params.chainId}-${params.tokenAddress}` - if (cachedData[key]?.value) { - return cachedData[key].value - } - return fetchTopTokenHolders(params) + return cachedData[key]?.value || fetchTopTokenHolders(params) }, [cachedData, fetchTopTokenHolders], ) diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index 7ab1478887..9749c0e8f9 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react' +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { useWalletInfo } from '@cowprotocol/wallet' import useSWR from 'swr' @@ -7,93 +8,106 @@ import useSWR from 'swr' import { useHooks } from 'modules/hooksStore' import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams' -import { useTokenContract } from 'common/hooks/useContract' - import { useGetTopTokenHolders } from './useGetTopTokenHolders' -import { completeBundleSimulation, preHooksBundleSimulation } from '../utils/bundleSimulation' +import { completeBundleSimulation, hooksBundleSimulation } from '../utils/bundleSimulation' import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData' import { getTokenTransferInfo } from '../utils/getTokenTransferInfo' +type BundleSimulationSwrParams = { + preHooks: CowHookDetails[] + postHooks: CowHookDetails[] +} + export function useTenderlyBundleSimulation() { const { account, chainId } = useWalletInfo() const { preHooks, postHooks } = useHooks() const orderParams = useOrderParams() - const tokenSell = useTokenContract(orderParams?.sellTokenAddress) - const tokenBuy = useTokenContract(orderParams?.buyTokenAddress) - const buyAmount = orderParams?.buyAmount - const sellAmount = orderParams?.sellAmount - const orderReceiver = orderParams?.receiver || account const getTopTokenHolder = useGetTopTokenHolders() - const simulateBundle = useCallback(async () => { - if (postHooks.length === 0 && preHooks.length === 0) return + const simulateBundle = useCallback( + async ({ preHooks, postHooks }: BundleSimulationSwrParams) => { + if (postHooks.length === 0 && preHooks.length === 0) return + + if (!postHooks.length) + return hooksBundleSimulation({ + chainId, + preHooks, + postHooks: [], + }) + + if ( + !account || + !orderParams?.buyTokenAddress || + !orderParams?.sellTokenAddress || + !orderParams?.buyAmount || + !orderParams?.sellAmount || + !orderParams?.receiver + ) { + return hooksBundleSimulation({ + chainId, + preHooks, + postHooks, + }) + } - if (!postHooks.length) - return preHooksBundleSimulation({ + const buyTokenTopHolders = await getTopTokenHolder({ + tokenAddress: orderParams.buyTokenAddress, chainId, - preHooks, }) - if (!account || !tokenBuy || !tokenSell || !buyAmount || !sellAmount || !orderReceiver) { - return - } - - const buyTokenTopHolders = await getTopTokenHolder({ - tokenAddress: tokenBuy.address, - chainId, - }) - - if (!buyTokenTopHolders) return - - const tokenBuyTransferInfo = getTokenTransferInfo({ - tokenHolders: buyTokenTopHolders, - amountToTransfer: buyAmount, - }) - - const paramsComplete = { - postHooks, - preHooks, - tokenBuy, - tokenBuyTransferInfo, - sellAmount, - orderReceiver, - tokenSell, - account, - chainId, - } - - return completeBundleSimulation(paramsComplete) - }, [ - account, - chainId, - getTopTokenHolder, - tokenBuy, - postHooks, - preHooks, - buyAmount, - sellAmount, - orderReceiver, - tokenSell, - ]) - - const getNewSimulationData = useCallback(async () => { - try { - const simulationData = await simulateBundle() + if (!buyTokenTopHolders) return + + const tokenBuyTransferInfo = getTokenTransferInfo({ + tokenHolders: buyTokenTopHolders, + amountToTransfer: orderParams.buyAmount, + }) + + const paramsComplete = { + postHooks, + preHooks, + tokenBuy: orderParams.buyTokenAddress, + tokenBuyTransferInfo, + sellAmount: orderParams.sellAmount, + buyAmount: orderParams.buyAmount, + orderReceiver: orderParams.receiver, + tokenSell: orderParams.sellTokenAddress, + account, + chainId, + } + + return completeBundleSimulation(paramsComplete) + }, + [account, chainId, getTopTokenHolder, orderParams], + ) + + const getNewSimulationData = useCallback( + async ([_, params]: [string, BundleSimulationSwrParams]) => { + const simulationData = await simulateBundle(params) if (!simulationData) { return {} } - return generateNewSimulationData(simulationData, { preHooks, postHooks }) - } catch { - return generateSimulationDataToError({ preHooks, postHooks }) - } - }, [preHooks, postHooks, simulateBundle]) + try { + return generateNewSimulationData(simulationData, { preHooks: params.preHooks, postHooks: params.postHooks }) + } catch (e) { + console.log(`error`, { e, simulationData }) + return generateSimulationDataToError({ preHooks: params.preHooks, postHooks: params.postHooks }) + } + }, + [simulateBundle], + ) - const { data, isValidating: isBundleSimulationLoading } = useSWR( - ['tenderly-bundle-simulation', postHooks, preHooks, orderParams?.sellTokenAddress, orderParams?.buyTokenAddress], + return useSWR( + [ + 'tenderly-bundle-simulation', + { + preHooks, + postHooks, + }, + ], getNewSimulationData, { revalidateOnFocus: false, @@ -101,6 +115,4 @@ export function useTenderlyBundleSimulation() { refreshWhenOffline: false, }, ) - - return { data, isValidating: isBundleSimulationLoading } } diff --git a/apps/cowswap-frontend/src/modules/tenderly/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts index 9ef8eb6b56..7c62f33d6b 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/types.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -9,10 +9,16 @@ export interface SimulationInput { gas_price?: string } +// { [address: string]: { [token: string]: balanceDiff: string } } +// example: { '0x123': { '0x456': '100', '0xabc': '-100' } } +export type BalancesDiff = Record> + export interface SimulationData { link: string status: boolean id: string + cumulativeBalancesDiff: BalancesDiff + gasUsed: string } export interface GetTopTokenHoldersParams { diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index 63d7c8dd0a..2606308869 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -1,17 +1,20 @@ -import { Erc20 } from '@cowprotocol/abis' +import { Erc20Abi } from '@cowprotocol/abis' import { BFF_BASE_URL } from '@cowprotocol/common-const' import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' +import { Interface } from 'ethers/lib/utils' + import { CowHook } from 'modules/hooksStore/types/hooks' import { SimulationData, SimulationInput } from '../types' +const erc20Interface = new Interface(Erc20Abi) export interface GetTransferTenderlySimulationInput { currencyAmount: string from: string receiver: string - token: Erc20 + token: string } export type TokenBuyTransferInfo = { @@ -21,8 +24,8 @@ export type TokenBuyTransferInfo = { export interface PostBundleSimulationParams { account: string chainId: SupportedChainId - tokenSell: Erc20 - tokenBuy: Erc20 + tokenSell: string + tokenBuy: string preHooks: CowHookDetails[] postHooks: CowHookDetails[] sellAmount: string @@ -35,10 +38,11 @@ export const completeBundleSimulation = async (params: PostBundleSimulationParam return simulateBundle(input, params.chainId) } -export const preHooksBundleSimulation = async ( - params: Pick, +export const hooksBundleSimulation = async ( + params: Pick, ): Promise => { - const input = params.preHooks.map((hook) => + const hooks = [...params.preHooks, ...params.postHooks] + const input = hooks.map((hook) => getCoWHookTenderlySimulationInput(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[params.chainId], hook.hook), ) return simulateBundle(input, params.chainId) @@ -70,11 +74,11 @@ export function getTransferTenderlySimulationInput({ receiver, token, }: GetTransferTenderlySimulationInput): SimulationInput { - const callData = token.interface.encodeFunctionData('transfer', [receiver, currencyAmount]) + const callData = erc20Interface.encodeFunctionData('transfer', [receiver, currencyAmount]) return { input: callData, - to: token.address, + to: token, from, } } diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts index 5d3416b736..54b9f11ab1 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -1,6 +1,6 @@ import { PostBundleSimulationParams } from './bundleSimulation' -import { SimulationData } from '../types' +import { BalancesDiff, SimulationData } from '../types' export function generateSimulationDataToError( postParams: Pick, @@ -18,6 +18,25 @@ export function generateSimulationDataToError( ) } +function convertBalanceDiffToLowerCaseKeys(data: BalancesDiff): BalancesDiff { + return Object.entries(data).reduce((acc, [tokenHolder, tokenHolderDiffs]) => { + const lowerOuterKey = tokenHolder.toLowerCase() + + const processedInnerObj = Object.entries(tokenHolderDiffs || {}).reduce((innerAcc, [tokenAddress, balanceDiff]) => { + const lowerInnerKey = tokenAddress.toLowerCase() + return { + ...innerAcc, + [lowerInnerKey]: balanceDiff, + } + }, {}) + + return { + ...acc, + [lowerOuterKey]: processedInnerObj, + } + }, {}) +} + export function generateNewSimulationData( simulationData: SimulationData[], postParams: Pick, @@ -25,9 +44,14 @@ export function generateNewSimulationData( const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid) const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid) - const preHooksData = simulationData.slice(0, preHooksKeys.length) + const simulationDataWithLowerCaseBalanceDiffKeys = simulationData.map((data) => ({ + ...data, + cumulativeBalancesDiff: convertBalanceDiffToLowerCaseKeys(data.cumulativeBalancesDiff), + })) + + const preHooksData = simulationDataWithLowerCaseBalanceDiffKeys.slice(0, preHooksKeys.length) - const postHooksData = simulationData.slice(-postHooksKeys.length) + const postHooksData = simulationDataWithLowerCaseBalanceDiffKeys.slice(-postHooksKeys.length) return { ...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}), diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts index 6f3bd35770..dde426c17a 100644 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts +++ b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts @@ -1,12 +1,9 @@ -import { - AllowancesState, - BalancesState, - useTokensAllowances, - useTokensBalances, -} from '@cowprotocol/balances-and-allowances' +import { AllowancesState, BalancesState, useTokensAllowances } from '@cowprotocol/balances-and-allowances' import { isEnoughAmount, getAddress, getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { useTokensBalancesCombined } from 'modules/combinedBalances' + export interface UseEnoughBalanceParams { /** * Address of the account to check balance (and optionally the allowance) @@ -39,7 +36,7 @@ const DEFAULT_BALANCE_AND_ALLOWANCE = { enoughBalance: undefined, enoughAllowanc export function useEnoughBalanceAndAllowance(params: UseEnoughBalanceParams): UseEnoughBalanceAndAllowanceResult { const { checkAllowanceAddress } = params - const { values: balances } = useTokensBalances() + const { values: balances } = useTokensBalancesCombined() const { values: allowances } = useTokensAllowances() return hasEnoughBalanceAndAllowance({ @@ -86,7 +83,7 @@ export function hasEnoughBalanceAndAllowance(params: EnoughBalanceParams): UseEn function _enoughBalance( tokenAddress: string | undefined, amount: CurrencyAmount, - balances: BalancesState['values'] + balances: BalancesState['values'], ): boolean | undefined { const balance = tokenAddress ? balances[tokenAddress] : undefined @@ -97,7 +94,7 @@ function _enoughAllowance( tokenAddress: string | undefined, amount: CurrencyAmount, allowances: AllowancesState['values'] | undefined, - isNativeCurrency: boolean + isNativeCurrency: boolean, ): boolean | undefined { if (!tokenAddress || !allowances) { return undefined diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index 87303e3074..3ea98e232c 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -1,6 +1,5 @@ import { useCallback, useState } from 'react' -import { useTokensBalances } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { isInjectedWidget } from '@cowprotocol/common-utils' import { @@ -21,6 +20,7 @@ import styled from 'styled-components/macro' import { Field } from 'legacy/state/types' import { addListAnalytics } from 'modules/analytics' +import { useTokensBalancesCombined } from 'modules/combinedBalances' import { usePermitCompatibleTokens } from 'modules/permit' import { useLpTokensWithBalances } from 'modules/yield/shared' @@ -79,7 +79,7 @@ export function SelectTokenWidget({ displayLpTokenLists }: SelectTokenWidgetProp const favoriteTokens = useFavoriteTokens() const userAddedTokens = useUserAddedTokens() const allTokenLists = useAllListsList() - const balancesState = useTokensBalances() + const balancesState = useTokensBalancesCombined() const unsupportedTokens = useUnsupportedTokens() const permitCompatibleTokens = usePermitCompatibleTokens() const onTokenListAddingError = useOnTokenListAddingError() diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts index bf389ec6b8..5475d25914 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts @@ -1,13 +1,13 @@ import { Atom, useAtomValue } from 'jotai' import { useMemo } from 'react' -import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { tryParseFractionalAmount } from '@cowprotocol/common-utils' import { useTokenBySymbolOrAddress } from '@cowprotocol/tokens' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Nullish } from 'types' +import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { ExtendedTradeRawState } from 'modules/trade/types/TradeRawState' import { useTradeUsdAmounts } from 'modules/usdAmount' @@ -24,14 +24,14 @@ export function useBuildTradeDerivedState(stateAtom: Atom const outputCurrency = useTokenBySymbolOrAddress(rawState.outputCurrencyId) const inputCurrencyAmount = useMemo( () => getCurrencyAmount(inputCurrency, rawState.inputCurrencyAmount), - [inputCurrency, rawState.inputCurrencyAmount] + [inputCurrency, rawState.inputCurrencyAmount], ) const outputCurrencyAmount = useMemo( () => getCurrencyAmount(outputCurrency, rawState.outputCurrencyAmount), - [outputCurrency, rawState.outputCurrencyAmount] + [outputCurrency, rawState.outputCurrencyAmount], ) - const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency) || null - const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency) || null + const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputCurrency) || null + const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputCurrency) || null const { inputAmount: { value: inputCurrencyFiatAmount }, @@ -61,7 +61,7 @@ export function useBuildTradeDerivedState(stateAtom: Atom function getCurrencyAmount( currency: Nullish | null, - currencyAmount: Nullish + currencyAmount: Nullish, ): CurrencyAmount | null { if (!currency || !currencyAmount) { return null diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index a1f7abf594..479cde549e 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -58,6 +58,9 @@ export interface HookDappContext { isSmartContract: boolean | undefined isPreHook: boolean isDarkMode: boolean + // { [address: string]: { [token: string]: balanceDiff: string } } + // example: { '0x123': { '0x456': '100', '0xabc': '-100' } } + balancesDiff: Record> } export interface HookDappBase { From 566caf23ed471419950c85a190c980ecdd7d2bbc Mon Sep 17 00:00:00 2001 From: Jean Neiverth <79885562+JeanNeiverth@users.noreply.github.com> Date: Mon, 11 Nov 2024 08:03:51 -0300 Subject: [PATCH 085/116] Add Create LlamaPay Vesting iframe hook (#5045) * feat: add create-llamapay-vesting iframe hook dapp * fix: wallet compability * fix: wallet support for both llama pay hooks * chore: remove from name --------- Co-authored-by: Pedro Yves Fracari Co-authored-by: Pedro Yves Fracari <55461956+yvesfracari@users.noreply.github.com> --- .../src/modules/hooksStore/hookRegistry.tsx | 1 + libs/hook-dapp-lib/src/hookDappsRegistry.json | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index 423e66c8da..e7ab5e3363 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -25,4 +25,5 @@ export const ALL_HOOK_DAPPS = [ }, hookDappsRegistry.COW_AMM_WITHDRAW, hookDappsRegistry.CLAIM_LLAMAPAY_VESTING, + hookDappsRegistry.CREATE_LLAMAPAY_VESTING ] as HookDapp[] diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json index c3e72565ce..b51b64fd65 100644 --- a/libs/hook-dapp-lib/src/hookDappsRegistry.json +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -77,6 +77,25 @@ "website": "https://github.com/bleu/cow-hooks-dapps", "url": "https://cow-hooks-dapps-claim-vesting.vercel.app", "conditions": { + "supportedNetworks": [1, 100, 42161], + "walletCompatibility": ["EOA"], + "smartContractWalletSupported": false + } + }, + "CREATE_LLAMAPAY_VESTING": { + "id": "a316488cecc23fde8c39d3e748e0cb12bd4d18305826d36576d4bba74bd97baf", + "type": "IFRAME", + "name": "Create LlamaPay Vesting", + "descriptionShort": "Create a LlamaPay vesting contract", + "description": "This hook allows you to easily set up vesting contracts with LlamaPay. Enter the recipient’s address or ENS name, then choose how much to transfer: the token buy will be automatically detected by the hook and the contracts will be linked to your LlamaPay dashboard for seamless tracking.", + "version": "0.1.0", + "website": "https://llamapay.io/vesting", + "image": "https://cow-hooks-dapps-create-vesting.vercel.app/llama-pay-icon.png", + "url": "https://cow-hooks-dapps-create-vesting.vercel.app", + "conditions": { + "position": "post", + "walletCompatibility": ["EOA"], + "smartContractWalletSupported": false, "supportedNetworks": [1, 100, 42161] } } From aa4467ad167ba5d8d27e3f2e5fefb37eca8cebd0 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 13 Nov 2024 21:00:07 +0700 Subject: [PATCH 086/116] chore: bump hook dapps lib version --- libs/hook-dapp-lib/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hook-dapp-lib/package.json b/libs/hook-dapp-lib/package.json index 68806134a5..46f8e82833 100644 --- a/libs/hook-dapp-lib/package.json +++ b/libs/hook-dapp-lib/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/hook-dapp-lib", - "version": "1.3.0", + "version": "1.3.1", "type": "commonjs", "description": "CoW Swap Hook Dapp Library. Allows you to develop pre/post hooks dapps for CoW Protocol.", "main": "index.js", From 27a8d4d0f827495cefb16c09c228151fc9f89426 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 14 Nov 2024 05:00:22 +0000 Subject: [PATCH 087/116] feat: make hooks use partially fillable by default (#5086) * feat: make hooks use partially fillable by default * fix: fix broken yield * fix: simplify implementation --- .../modules/tradeFlow/hooks/useTradeFlowContext.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts index 18a77d1f9c..1adf5b628e 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -12,13 +12,20 @@ import { useCloseModals } from 'legacy/state/application/hooks' import { useAppData, useAppDataHooks } from 'modules/appData' import { useGeneratePermitHook, useGetCachedPermit, usePermitInfo } from 'modules/permit' import { useEnoughBalanceAndAllowance } from 'modules/tokens' -import { useDerivedTradeState, useReceiveAmountInfo, useTradeConfirmActions, useTradeTypeInfo } from 'modules/trade' +import { + useDerivedTradeState, + useIsHooksTradeType, + useReceiveAmountInfo, + useTradeConfirmActions, + useTradeTypeInfo, +} from 'modules/trade' import { getOrderValidTo, useTradeQuote } from 'modules/tradeQuote' import { useGP2SettlementContract } from 'common/hooks/useContract' import { TradeTypeToUiOrderType } from '../../trade/const/common' import { TradeFlowContext } from '../types/TradeFlowContext' +import { UiOrderType } from '@cowprotocol/types' export interface TradeFlowParams { deadline: number @@ -34,6 +41,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon const tradeTypeInfo = useTradeTypeInfo() const tradeType = tradeTypeInfo?.tradeType const uiOrderType = tradeType ? TradeTypeToUiOrderType[tradeType] : null + const isHooksTradeType = useIsHooksTradeType() const sellCurrency = derivedTradeState?.inputCurrency const inputAmount = receiveAmountInfo?.afterSlippage.sellAmount @@ -194,7 +202,7 @@ export function useTradeFlowContext({ deadline }: TradeFlowParams): TradeFlowCon allowsOffchainSigning, appData, class: OrderClass.MARKET, - partiallyFillable: false, // SWAP orders are always fill or kill - for now + partiallyFillable: isHooksTradeType, quoteId: quoteResponse.id, isSafeWallet, }, From 20f543a4690e99e2df18a73995e476ce0efc4a6a Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 14 Nov 2024 05:16:56 +0000 Subject: [PATCH 088/116] feat: enable hooks using settings (#5081) * feat: enable hooks using settings * fix: change the spelling of DeFI * fix: hide setting for widget --------- Co-authored-by: fairlight <31534717+fairlighteth@users.noreply.github.com> --- .../src/common/constants/routes.ts | 1 - .../src/common/hooks/useMenuItems.ts | 7 +++-- .../src/legacy/state/user/hooks.tsx | 23 +++++++++++++- .../src/legacy/state/user/reducer.ts | 6 ++++ .../src/modules/analytics/events.ts | 8 +++++ .../swap/containers/SwapWidget/index.tsx | 11 +++++-- .../containers/SettingsTab/index.tsx | 30 +++++++++++++++++-- .../yield/containers/YieldWidget/index.tsx | 1 + 8 files changed, 78 insertions(+), 9 deletions(-) diff --git a/apps/cowswap-frontend/src/common/constants/routes.ts b/apps/cowswap-frontend/src/common/constants/routes.ts index 87f1d6f144..7a08ee146f 100644 --- a/apps/cowswap-frontend/src/common/constants/routes.ts +++ b/apps/cowswap-frontend/src/common/constants/routes.ts @@ -54,7 +54,6 @@ export const HOOKS_STORE_MENU_ITEM = { route: Routes.HOOKS, label: 'Hooks', description: 'Powerful tool to generate pre/post interaction for CoW Protocol', - badge: 'New', } export const YIELD_MENU_ITEM = { diff --git a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts index c416b3c0b1..b916bec12c 100644 --- a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts +++ b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts @@ -4,15 +4,16 @@ import { useFeatureFlags } from '@cowprotocol/common-hooks' import { isLocal } from '@cowprotocol/common-utils' import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS, YIELD_MENU_ITEM } from '../constants/routes' +import { useHooksEnabled } from 'legacy/state/user/hooks' export function useMenuItems() { - const { isHooksStoreEnabled } = useFeatureFlags() + const isHooksEnabled = useHooksEnabled() const { isYieldEnabled } = useFeatureFlags() return useMemo(() => { const items = [...MENU_ITEMS] - if (isHooksStoreEnabled || isLocal) { + if (isHooksEnabled) { items.push(HOOKS_STORE_MENU_ITEM) } @@ -21,5 +22,5 @@ export function useMenuItems() { } return items - }, [isHooksStoreEnabled, isYieldEnabled]) + }, [isHooksEnabled, isYieldEnabled]) } diff --git a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx index c24357a2fb..2f86b41a32 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx +++ b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx @@ -8,7 +8,13 @@ import { Currency } from '@uniswap/sdk-core' import { shallowEqual } from 'react-redux' -import { updateRecipientToggleVisible, updateUserDarkMode, updateUserDeadline, updateUserLocale } from './reducer' +import { + updateHooksEnabled, + updateRecipientToggleVisible, + updateUserDarkMode, + updateUserDeadline, + updateUserLocale, +} from './reducer' import { SerializedToken } from './types' import { useAppDispatch, useAppSelector } from '../hooks' @@ -83,6 +89,21 @@ export function useRecipientToggleManager(): [boolean, (value: boolean) => void] return [isVisible, toggleVisibility] } +export function useHooksEnabled(): boolean { + return useAppSelector((state) => state.user.hooksEnabled) +} + +export function useHooksEnabledManager(): [boolean, Command] { + const dispatch = useAppDispatch() + const hooksEnabled = useHooksEnabled() + + const toggleHooksEnabled = useCallback(() => { + dispatch(updateHooksEnabled({ hooksEnabled: !hooksEnabled })) + }, [hooksEnabled, dispatch]) + + return [hooksEnabled, toggleHooksEnabled] +} + export function useUserTransactionTTL(): [number, (slippage: number) => void] { const dispatch = useAppDispatch() const deadline = useAppSelector((state) => state.user.userDeadline) diff --git a/apps/cowswap-frontend/src/legacy/state/user/reducer.ts b/apps/cowswap-frontend/src/legacy/state/user/reducer.ts index 70cc8b6e4f..a9470d45b3 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/reducer.ts +++ b/apps/cowswap-frontend/src/legacy/state/user/reducer.ts @@ -17,6 +17,7 @@ export interface UserState { // TODO: mod, shouldn't be here recipientToggleVisible: boolean + hooksEnabled: boolean // deadline set by user in minutes, used in all txns userDeadline: number @@ -28,6 +29,7 @@ export const initialState: UserState = { userDarkMode: null, // TODO: mod, shouldn't be here recipientToggleVisible: false, + hooksEnabled: false, userLocale: null, userDeadline: DEFAULT_DEADLINE_FROM_NOW, } @@ -42,6 +44,9 @@ const userSlice = createSlice({ updateUserDarkMode(state, action) { state.userDarkMode = action.payload.userDarkMode }, + updateHooksEnabled(state, action) { + state.hooksEnabled = action.payload.hooksEnabled + }, updateMatchesDarkMode(state, action) { state.matchesDarkMode = action.payload.matchesDarkMode }, @@ -61,6 +66,7 @@ export const { updateSelectedWallet, updateMatchesDarkMode, updateUserDarkMode, + updateHooksEnabled, updateUserDeadline, updateUserLocale, updateRecipientToggleVisible, diff --git a/apps/cowswap-frontend/src/modules/analytics/events.ts b/apps/cowswap-frontend/src/modules/analytics/events.ts index cfbb5757c4..efb57403b5 100644 --- a/apps/cowswap-frontend/src/modules/analytics/events.ts +++ b/apps/cowswap-frontend/src/modules/analytics/events.ts @@ -93,6 +93,14 @@ export function toggleRecipientAddressAnalytics(enable: boolean) { }) } +export function toggleHooksEnabledAnalytics(enable: boolean) { + cowAnalytics.sendEvent({ + category: Category.HOOKS, + action: 'Toggle Hooks Enabled', + label: enable ? 'Enabled' : 'Disabled', + }) +} + export function searchByAddressAnalytics(isAddressSearch: string) { cowAnalytics.sendEvent({ category: Category.CURRENCY_SELECT, 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 af1def7605..2d337cc379 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -10,7 +10,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 { 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' @@ -75,6 +75,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const tradeQuoteStateOverride = useTradeQuoteStateFromLegacy() const receiveAmountInfo = useReceiveAmountInfo() const recipientToggleState = useRecipientToggleManager() + const hooksEnabledState = useHooksEnabledManager() const deadlineState = useUserTransactionTTL() const isTradePriceUpdating = useTradePricesUpdate() @@ -213,7 +214,13 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { ) const slots: TradeWidgetSlots = { - settingsWidget: , + settingsWidget: ( + + ), topContent, bottomContent: useCallback( 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 9f1bbfcb07..283e6eb33d 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -12,21 +12,23 @@ import { ThemedText } from 'theme' import { AutoColumn } from 'legacy/components/Column' import { Toggle } from 'legacy/components/Toggle' -import { toggleRecipientAddressAnalytics } from 'modules/analytics' +import { toggleHooksEnabledAnalytics, toggleRecipientAddressAnalytics } from 'modules/analytics' import { SettingsIcon } from 'modules/trade/pure/Settings' import * as styledEl from './styled' import { settingsTabStateAtom } from '../../state/settingsTabState' import { TransactionSettings } from '../TransactionSettings' +import { isInjectedWidget } from '@cowprotocol/common-utils' interface SettingsTabProps { className?: string recipientToggleState: StatefulValue + hooksEnabledState?: StatefulValue deadlineState: StatefulValue } -export function SettingsTab({ className, recipientToggleState, deadlineState }: SettingsTabProps) { +export function SettingsTab({ className, recipientToggleState, hooksEnabledState, deadlineState }: SettingsTabProps) { const menuButtonRef = useRef(null) const [recipientToggleVisible, toggleRecipientVisibilityAux] = recipientToggleState @@ -39,6 +41,18 @@ export function SettingsTab({ className, recipientToggleState, deadlineState }: [toggleRecipientVisibilityAux, recipientToggleVisible], ) + const [hooksEnabled, toggleHooksEnabledAux] = hooksEnabledState || [null, null] + const toggleHooksEnabled = useCallback( + (value?: boolean) => { + if (hooksEnabled === null || toggleHooksEnabledAux === null) return + + const isEnabled = value ?? !hooksEnabled + toggleHooksEnabledAnalytics(isEnabled) + toggleHooksEnabledAux(isEnabled) + }, + [toggleRecipientVisibilityAux, hooksEnabled], + ) + return ( @@ -75,6 +89,18 @@ export function SettingsTab({ className, recipientToggleState, deadlineState }: toggle={toggleRecipientVisibility} /> + + {!isInjectedWidget() && hooksEnabled !== null && ( + + + + Enable Hooks + + 🧪 Add DeFI interactions before and after your trade} /> + + + + )} diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx index 1888b6d921..1d8092f525 100644 --- a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -35,6 +35,7 @@ import { useYieldSettings, useYieldUnlockState, } from '../../hooks/useYieldSettings' + import { useYieldWidgetActions } from '../../hooks/useYieldWidgetActions' import { PoolApyPreview } from '../../pure/PoolApyPreview' import { TargetPoolPreviewInfo } from '../../pure/TargetPoolPreviewInfo' From 38aae718e54fb50634706eafb973e8027d2b28df Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari <55461956+yvesfracari@users.noreply.github.com> Date: Thu, 14 Nov 2024 02:17:55 -0300 Subject: [PATCH 089/116] fix(combinedBalances): Optimize balance diff calculations (#5082) * chore: create state for balance combined and optimize applyBalanceDiffs function * fix: avoid negative user balance * fix: remove refetch tendernly simulation on mount * fix: build error * feat: add simulation link on order review --- .../OrderHooksDetails/HookItem/index.tsx | 17 +++++- .../OrderHooksDetails/HookItem/styled.tsx | 9 +++ .../containers/OrderHooksDetails/index.tsx | 21 ++++--- .../application/containers/App/Updaters.tsx | 2 + .../hooks/useTokensBalancesCombined.ts | 44 +-------------- .../state/balanceCombinedAtom.ts | 5 ++ .../updater/BalancesCombinedUpdater.tsx | 55 +++++++++++++++++++ .../hooksStore/pure/AppliedHookItem/index.tsx | 10 +--- .../tenderly/hooks/useSimulationData.ts | 12 ++++ .../hooks/useTenderlyBundleSimulation.ts | 1 + 10 files changed, 118 insertions(+), 58 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/combinedBalances/state/balanceCombinedAtom.ts create mode 100644 apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx create mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useSimulationData.ts diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx index 5edb9fce43..21d30abdda 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx @@ -1,16 +1,19 @@ import { useState } from 'react' -import { HookToDappMatch } from '@cowprotocol/hook-dapp-lib' +import { CowHookDetails, HookToDappMatch } from '@cowprotocol/hook-dapp-lib' import { ChevronDown, ChevronUp } from 'react-feather' import { clickOnHooks } from 'modules/analytics' +import { useSimulationData } from 'modules/tenderly/hooks/useSimulationData' import * as styledEl from './styled' -export function HookItem({ item, index }: { item: HookToDappMatch; index: number }) { +export function HookItem({ details, item, index }: { details?: CowHookDetails; item: HookToDappMatch; index: number }) { const [isOpen, setIsOpen] = useState(false) + const simulationData = useSimulationData(details?.uuid) + const handleLinkClick = () => { clickOnHooks(item.dapp?.name || 'Unknown hook dapp') } @@ -37,6 +40,16 @@ export function HookItem({ item, index }: { item: HookToDappMatch; index: number {item.dapp && ( <> + {simulationData && ( +

    + Simulation: + + + {simulationData.status ? 'Simulation successful' : 'Simulation failed'} + + +

    + )}

    Description: {item.dapp.descriptionShort}

    diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx index c04ab34ff6..f354c7565b 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx @@ -146,3 +146,12 @@ export const ToggleIcon = styled.div<{ isOpen: boolean }>` } } ` + +export const SimulationLink = styled.span<{ status: boolean }>` + color: var(${({ status }) => (status ? UI.COLOR_SUCCESS : UI.COLOR_DANGER)}); + border-radius: 8px; + + &:hover { + color: var(${({ status }) => (status ? UI.COLOR_SUCCESS_TEXT : UI.COLOR_DANGER_TEXT)}); + } +` diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index a11e2272ab..80c258b294 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -1,13 +1,13 @@ import { ReactElement, useEffect, useMemo, useState } from 'react' import { latest } from '@cowprotocol/app-data' -import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' +import { CowHookDetails, HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' import { InfoTooltip } from '@cowprotocol/ui' import { ChevronDown, ChevronUp } from 'react-feather' import { AppDataInfo, decodeAppData } from 'modules/appData' -import { useCustomHookDapps } from 'modules/hooksStore' +import { useCustomHookDapps, useHooks, useHooksStateWithSimulatedGas } from 'modules/hooksStore' import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' import { HookItem } from './HookItem' @@ -28,6 +28,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai const preCustomHookDapps = useCustomHookDapps(true) const postCustomHookDapps = useCustomHookDapps(false) + const hooks = useHooksStateWithSimulatedGas() + const { mutate, isValidating, data } = useTenderlyBundleSimulation() useEffect(() => { @@ -74,8 +76,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai {isOpen && ( - - + + )} , @@ -85,9 +87,10 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai interface HooksInfoProps { data: HookToDappMatch[] title: string + hooks: CowHookDetails[] } -function HooksInfo({ data, title }: HooksInfoProps) { +function HooksInfo({ data, title, hooks }: HooksInfoProps) { return ( <> {data.length ? ( @@ -96,9 +99,11 @@ function HooksInfo({ data, title }: HooksInfoProps) { {title} {data.length} - {data.map((item, index) => ( - - ))} + {data.map((item, index) => { + const key = item.hook.callData + item.hook.target + item.hook.gasLimit + const details = hooks.find(({ hook }) => key === hook.callData + hook.target + hook.gasLimit) + return + })} ) : null} diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index e56d145d0f..05fd6154f6 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -8,6 +8,7 @@ import { GasPriceStrategyUpdater } from 'legacy/state/gas/gas-price-strategy-upd import { addListAnalytics, removeListAnalytics } from 'modules/analytics' import { UploadToIpfsUpdater } from 'modules/appData/updater/UploadToIpfsUpdater' +import { BalancesCombinedUpdater } from 'modules/combinedBalances/updater/BalancesCombinedUpdater' import { CowEventsUpdater, InjectedWidgetUpdater, useInjectedWidgetParams } from 'modules/injectedWidget' import { FinalizeTxUpdater } from 'modules/onchainTransactions' import { OrdersNotificationsUpdater } from 'modules/orders' @@ -90,6 +91,7 @@ export function Updaters() { + ) } diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts index 0ad8abb306..1b2abef22d 100644 --- a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts +++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts @@ -1,45 +1,7 @@ -import { useMemo } from 'react' +import { useAtomValue } from 'jotai' -import { BalancesState, useTokensBalances } from '@cowprotocol/balances-and-allowances' -import { useWalletInfo } from '@cowprotocol/wallet' - -import { BigNumber } from 'ethers' - -import { usePreHookBalanceDiff } from 'modules/hooksStore/hooks/useBalancesDiff' -import { useIsHooksTradeType } from 'modules/trade' +import { balancesCombinedAtom } from '../state/balanceCombinedAtom' export function useTokensBalancesCombined() { - const { account } = useWalletInfo() - const preHooksBalancesDiff = usePreHookBalanceDiff() - const tokenBalances = useTokensBalances() - const isHooksTradeType = useIsHooksTradeType() - - return useMemo(() => { - if (!account || !isHooksTradeType) return tokenBalances - const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {} - return applyBalanceDiffs(tokenBalances, accountBalancesDiff) - }, [account, preHooksBalancesDiff, tokenBalances, isHooksTradeType]) -} - -function applyBalanceDiffs(currentBalances: BalancesState, balanceDiff: Record): BalancesState { - // Get all unique addresses from both objects - const allAddresses = [...new Set([...Object.keys(currentBalances.values), ...Object.keys(balanceDiff)])] - - const normalizedValues = allAddresses.reduce( - (acc, address) => { - const currentBalance = currentBalances.values[address] || BigNumber.from(0) - const diff = balanceDiff[address] ? BigNumber.from(balanceDiff[address]) : BigNumber.from(0) - - return { - ...acc, - [address]: currentBalance.add(diff), - } - }, - {} as Record, - ) - - return { - isLoading: currentBalances.isLoading, - values: normalizedValues, - } + return useAtomValue(balancesCombinedAtom) } diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/state/balanceCombinedAtom.ts b/apps/cowswap-frontend/src/modules/combinedBalances/state/balanceCombinedAtom.ts new file mode 100644 index 0000000000..43cd123b97 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/combinedBalances/state/balanceCombinedAtom.ts @@ -0,0 +1,5 @@ +import { atomWithReset } from 'jotai/utils' + +import { BalancesState } from '@cowprotocol/balances-and-allowances' + +export const balancesCombinedAtom = atomWithReset({ isLoading: false, values: {} }) diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx b/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx new file mode 100644 index 0000000000..2058bf8f55 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx @@ -0,0 +1,55 @@ +import { useEffect } from 'react' + +import { BalancesState, useTokensBalances } from '@cowprotocol/balances-and-allowances' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { BigNumber } from 'ethers' + +import { useHooks } from 'modules/hooksStore' +import { usePreHookBalanceDiff } from 'modules/hooksStore/hooks/useBalancesDiff' +import { useIsHooksTradeType } from 'modules/trade' +import { useSetAtom } from 'jotai' +import { balancesCombinedAtom } from '../state/balanceCombinedAtom' + +export function BalancesCombinedUpdater() { + const { account } = useWalletInfo() + const setBalancesCombined = useSetAtom(balancesCombinedAtom) + const preHooksBalancesDiff = usePreHookBalanceDiff() + const { preHooks } = useHooks() + const tokenBalances = useTokensBalances() + const isHooksTradeType = useIsHooksTradeType() + + useEffect(() => { + if (!account || !isHooksTradeType || !preHooks.length) { + setBalancesCombined(tokenBalances) + return + } + const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {} + setBalancesCombined(applyBalanceDiffs(tokenBalances, accountBalancesDiff)) + }, [account, preHooksBalancesDiff, isHooksTradeType, tokenBalances]) + + return null +} + +function applyBalanceDiffs(currentBalances: BalancesState, balanceDiff: Record): BalancesState { + const normalizedValues = { ...currentBalances.values } + + // Only process addresses that have balance differences + // This optimizes since the balances diff object is usually smaller than the balances object + Object.entries(balanceDiff).forEach(([address, diff]) => { + const currentBalance = normalizedValues[address] + if (currentBalance === undefined) return + const balanceWithDiff = currentBalance.add(BigNumber.from(diff)) + + // If the balance with diff is negative, set the balance to 0 + // This avoid the UI crashing in case of some error + normalizedValues[address] = balanceWithDiff.isNegative() + ? BigNumber.from(0) + : currentBalance.add(BigNumber.from(diff)) + }) + + return { + isLoading: currentBalances.isLoading, + values: normalizedValues, + } +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx index 362c2f0f33..1bfe495c5a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -1,5 +1,3 @@ -import { useMemo } from 'react' - import ICON_CHECK_ICON from '@cowprotocol/assets/cow-swap/check-singular.svg' import ICON_GRID from '@cowprotocol/assets/cow-swap/grid.svg' import TenderlyLogo from '@cowprotocol/assets/cow-swap/tenderly-logo.svg' @@ -10,6 +8,7 @@ import { InfoTooltip } from '@cowprotocol/ui' import { Edit2, Trash2, ExternalLink as ExternalLinkIcon, RefreshCw } from 'react-feather' import SVG from 'react-inlinesvg' +import { useSimulationData } from 'modules/tenderly/hooks/useSimulationData' import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' import * as styledEl from './styled' @@ -31,12 +30,9 @@ interface HookItemProp { const isBundleSimulationReady = true export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) { - const { isValidating, data, mutate } = useTenderlyBundleSimulation() + const { isValidating, mutate } = useTenderlyBundleSimulation() - const simulationData = useMemo(() => { - if (!data) return - return data[hookDetails.uuid] - }, [data, hookDetails.uuid]) + const simulationData = useSimulationData(hookDetails.uuid) const simulationStatus = simulationData?.status ? 'Simulation successful' : 'Simulation failed' const simulationTooltip = simulationData?.status diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useSimulationData.ts new file mode 100644 index 0000000000..8e3abf585d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useSimulationData.ts @@ -0,0 +1,12 @@ +import { useMemo } from 'react' + +import { useTenderlyBundleSimulation } from './useTenderlyBundleSimulation' + +export function useSimulationData(hookUuid?: string) { + const { data } = useTenderlyBundleSimulation() + + return useMemo(() => { + if (!data || !hookUuid) return + return data[hookUuid] + }, [data, hookUuid]) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index 9749c0e8f9..a5a5a913c8 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -113,6 +113,7 @@ export function useTenderlyBundleSimulation() { revalidateOnFocus: false, revalidateOnReconnect: false, refreshWhenOffline: false, + revalidateOnMount: false, }, ) } From adec6fecaed225531140ae947801be9d5ee52532 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:18:15 +0000 Subject: [PATCH 090/116] feat: misc hooks improvements (#5079) --- .../containers/OrderHooksDetails/index.tsx | 4 +-- .../src/common/hooks/useMenuItems.ts | 3 +- .../updater/BalancesCombinedUpdater.tsx | 5 +-- .../containers/HookDappContainer/index.tsx | 1 + .../containers/HookRegistryList/index.tsx | 31 ++++++++++++------- .../dapps/ClaimGnoHookApp/index.tsx | 2 +- .../src/modules/hooksStore/dapps/styled.tsx | 13 ++++---- .../hooksStore/hooks/useBalancesDiff.ts | 4 +-- .../hooksStore/pure/HookListItem/index.tsx | 2 +- .../tradeFlow/hooks/useTradeFlowContext.ts | 1 - .../containers/SettingsTab/index.tsx | 4 +-- .../yield/containers/YieldWidget/index.tsx | 1 - 12 files changed, 41 insertions(+), 30 deletions(-) diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index 80c258b294..2199b81928 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -7,7 +7,7 @@ import { InfoTooltip } from '@cowprotocol/ui' import { ChevronDown, ChevronUp } from 'react-feather' import { AppDataInfo, decodeAppData } from 'modules/appData' -import { useCustomHookDapps, useHooks, useHooksStateWithSimulatedGas } from 'modules/hooksStore' +import { useCustomHookDapps, useHooksStateWithSimulatedGas } from 'modules/hooksStore' import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' import { HookItem } from './HookItem' @@ -34,7 +34,7 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai useEffect(() => { mutate() - }, []) + }, []) // eslint-disable-line react-hooks/exhaustive-deps if (!appDataDoc) return null diff --git a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts index b916bec12c..aa97c78208 100644 --- a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts +++ b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts @@ -3,9 +3,10 @@ import { useMemo } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' import { isLocal } from '@cowprotocol/common-utils' -import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS, YIELD_MENU_ITEM } from '../constants/routes' import { useHooksEnabled } from 'legacy/state/user/hooks' +import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS, YIELD_MENU_ITEM } from '../constants/routes' + export function useMenuItems() { const isHooksEnabled = useHooksEnabled() const { isYieldEnabled } = useFeatureFlags() diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx b/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx index 2058bf8f55..f4c5efb865 100644 --- a/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/combinedBalances/updater/BalancesCombinedUpdater.tsx @@ -1,3 +1,4 @@ +import { useSetAtom } from 'jotai' import { useEffect } from 'react' import { BalancesState, useTokensBalances } from '@cowprotocol/balances-and-allowances' @@ -8,7 +9,7 @@ import { BigNumber } from 'ethers' import { useHooks } from 'modules/hooksStore' import { usePreHookBalanceDiff } from 'modules/hooksStore/hooks/useBalancesDiff' import { useIsHooksTradeType } from 'modules/trade' -import { useSetAtom } from 'jotai' + import { balancesCombinedAtom } from '../state/balanceCombinedAtom' export function BalancesCombinedUpdater() { @@ -26,7 +27,7 @@ export function BalancesCombinedUpdater() { } const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {} setBalancesCombined(applyBalanceDiffs(tokenBalances, accountBalancesDiff)) - }, [account, preHooksBalancesDiff, isHooksTradeType, tokenBalances]) + }, [account, preHooksBalancesDiff, isHooksTradeType, tokenBalances, preHooks.length, setBalancesCombined]) return null } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index d431207b07..7aa1550073 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -82,6 +82,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho outputCurrencyId, isDarkMode, orderParams, + balancesDiff, ]) const dappProps = useMemo(() => ({ context, dapp, isPreHook }), [context, dapp, isPreHook]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx index 2741c954a0..8c247a26ff 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx @@ -110,13 +110,21 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit, walletType const DappsListContent = useMemo( () => ( <> + setSearchQuery(e.target.value?.trim())} + placeholder="Search hooks by title or description" + ariaLabel="Search hooks" + onClear={handleClearSearch} + /> + {isAllHooksTab && (

    @@ -128,14 +136,6 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit, walletType )} - setSearchQuery(e.target.value?.trim())} - placeholder="Search hooks by title or description" - ariaLabel="Search hooks" - onClear={handleClearSearch} - /> - {sortedFilteredDapps.length > 0 ? ( {sortedFilteredDapps.map((dapp) => ( @@ -154,7 +154,16 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit, walletType )} ), - [isAllHooksTab, searchQuery, sortedFilteredDapps, handleAddCustomHook, handleClearSearch, emptyListMessage, removeCustomHookDapp, walletType], + [ + isAllHooksTab, + searchQuery, + sortedFilteredDapps, + handleAddCustomHook, + handleClearSearch, + emptyListMessage, + removeCustomHookDapp, + walletType, + ], ) return ( @@ -166,7 +175,7 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit, walletType contentPadding="0" justifyContent="flex-start" > - {!dappDetails && !hookToEditDetails && ( + {!dappDetails && !hookToEditDetails && !selectedDapp && ( - + {context.chainId !== SupportedChainId.GNOSIS_CHAIN ? ( 'Unsupported network. Please change to Gnosis Chain' ) : !account ? ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/styled.tsx index 644ffae2b5..71734aa1bd 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/styled.tsx @@ -6,24 +6,25 @@ export const Wrapper = styled.div` display: flex; flex-flow: column wrap; width: 100%; - padding: 24px 8px 16px; + padding: 10px; justify-content: flex-end; flex: 1 1 auto; - gap: 24px; + gap: 10px; font-size: 14px; ` -export const ContentWrapper = styled.div` +export const ContentWrapper = styled.div<{ minHeight?: number }>` flex-flow: column wrap; display: flex; - justify-content: flex-end; + justify-content: center; align-items: center; padding: 0; text-align: center; - gap: 16px; + gap: 10px; flex: 1 1 auto; color: var(${UI.COLOR_TEXT}); font-size: inherit; + min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : 'initial')}; ` export const Row = styled.div` @@ -42,7 +43,7 @@ export const Row = styled.div` width: 100%; padding: 14px 16px; border: 1px solid transparent; - border-radius: 9px; + border-radius: 16px; font-size: 16px; background: var(${UI.COLOR_PAPER_DARKER}); color: inherit; diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts index e701ba1bcc..94509b169a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts @@ -58,7 +58,7 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): const lastPostHook = postHooks[postHooks.length - 1] return data[lastPostHook?.uuid]?.cumulativeBalancesDiff || firstPostHookBalanceDiff - }, [data, postHooks, orderExpectedBalanceDiff, preHookBalanceDiff]) + }, [data, firstPostHookBalanceDiff, postHooks]) const hookToEditBalanceDiff = useMemo(() => { if (!data || !hookToEditUid) return EMPTY_BALANCE_DIFF @@ -83,7 +83,7 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): if (hookToEditUid) return hookToEditBalanceDiff if (isPreHook) return preHookBalanceDiff return postHookBalanceDiff - }, [data, orderParams, preHooks, postHooks]) + }, [hookToEditBalanceDiff, hookToEditUid, isPreHook, postHookBalanceDiff, preHookBalanceDiff]) } function mergeBalanceDiffs(first: BalancesDiff, second: BalancesDiff): BalancesDiff { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx index 87661dc041..c02f264e6d 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx @@ -44,7 +44,7 @@ export function HookListItem({ dapp, walletType, onSelect, onOpenDetails, onRemo {isCompatible ? ( - Add + Open ) : ( diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts index 1adf5b628e..fafa6e7594 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/hooks/useTradeFlowContext.ts @@ -25,7 +25,6 @@ import { useGP2SettlementContract } from 'common/hooks/useContract' import { TradeTypeToUiOrderType } from '../../trade/const/common' import { TradeFlowContext } from '../types/TradeFlowContext' -import { UiOrderType } from '@cowprotocol/types' export interface TradeFlowParams { deadline: number 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 283e6eb33d..0e1f7065ea 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -1,6 +1,7 @@ import { useAtom } from 'jotai' import { ReactElement, RefObject, useCallback, useEffect, useRef } from 'react' +import { isInjectedWidget } from '@cowprotocol/common-utils' import { StatefulValue } from '@cowprotocol/types' import { HelpTooltip, RowBetween, RowFixed } from '@cowprotocol/ui' @@ -19,7 +20,6 @@ import * as styledEl from './styled' import { settingsTabStateAtom } from '../../state/settingsTabState' import { TransactionSettings } from '../TransactionSettings' -import { isInjectedWidget } from '@cowprotocol/common-utils' interface SettingsTabProps { className?: string @@ -50,7 +50,7 @@ export function SettingsTab({ className, recipientToggleState, hooksEnabledState toggleHooksEnabledAnalytics(isEnabled) toggleHooksEnabledAux(isEnabled) }, - [toggleRecipientVisibilityAux, hooksEnabled], + [hooksEnabled, toggleHooksEnabledAux], ) return ( diff --git a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx index 1d8092f525..1888b6d921 100644 --- a/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/containers/YieldWidget/index.tsx @@ -35,7 +35,6 @@ import { useYieldSettings, useYieldUnlockState, } from '../../hooks/useYieldSettings' - import { useYieldWidgetActions } from '../../hooks/useYieldWidgetActions' import { PoolApyPreview } from '../../pure/PoolApyPreview' import { TargetPoolPreviewInfo } from '../../pure/TargetPoolPreviewInfo' From 1102691f8f8260dbbcae9f2fba2629ef59b4384d Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 14 Nov 2024 09:25:36 +0000 Subject: [PATCH 091/116] fix: add cow-shed page (#5089) * fix: add cow-shed page * feat: add experimental badge --- .../src/common/constants/routes.ts | 2 + .../application/containers/App/RoutesApp.tsx | 2 + .../containers/HooksStoreWidget/index.tsx | 32 +++----------- .../containers/RescueFundsFromProxy/index.tsx | 44 ++++++++++++++++--- .../RescueFundsFromProxy/styled.tsx | 14 ++++++ .../swap/containers/SwapWidget/index.tsx | 30 ++++++++++++- .../src/pages/Hooks/cowShed.tsx | 12 +++++ 7 files changed, 101 insertions(+), 35 deletions(-) create mode 100644 apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx diff --git a/apps/cowswap-frontend/src/common/constants/routes.ts b/apps/cowswap-frontend/src/common/constants/routes.ts index 7a08ee146f..afd07b9d8a 100644 --- a/apps/cowswap-frontend/src/common/constants/routes.ts +++ b/apps/cowswap-frontend/src/common/constants/routes.ts @@ -6,6 +6,7 @@ export const Routes = { HOME: '/', SWAP: `/:chainId?${TRADE_WIDGET_PREFIX}/swap/:inputCurrencyId?/:outputCurrencyId?`, HOOKS: `/:chainId?${TRADE_WIDGET_PREFIX}/swap/hooks/:inputCurrencyId?/:outputCurrencyId?`, + COW_SHED: `/:chainId?${TRADE_WIDGET_PREFIX}/cowShed`, LIMIT_ORDER: `/:chainId?${TRADE_WIDGET_PREFIX}/limit/:inputCurrencyId?/:outputCurrencyId?`, YIELD: `/:chainId?${TRADE_WIDGET_PREFIX}/yield/:inputCurrencyId?/:outputCurrencyId?`, ADVANCED_ORDERS: `/:chainId?${TRADE_WIDGET_PREFIX}/advanced/:inputCurrencyId?/:outputCurrencyId?`, @@ -54,6 +55,7 @@ export const HOOKS_STORE_MENU_ITEM = { route: Routes.HOOKS, label: 'Hooks', description: 'Powerful tool to generate pre/post interaction for CoW Protocol', + badge: '🧪', } export const YIELD_MENU_ITEM = { diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx index 7469615b85..bc6f28c656 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx @@ -24,6 +24,7 @@ import { HooksPage } from 'pages/Hooks' import LimitOrderPage from 'pages/LimitOrders' import { SwapPage } from 'pages/Swap' import YieldPage from 'pages/Yield' +import { CowShed } from 'pages/Hooks/cowShed' // Async routes const NotFound = lazy(() => import(/* webpackChunkName: "not_found" */ 'pages/error/NotFound')) @@ -87,6 +88,7 @@ export function RoutesApp() { {/*Swap*/} } /> } /> + } /> } /> {lazyRoutes.map((item, key) => LazyRoute({ ...item, key }))} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index c6217ef6d5..704a1ee9c0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -10,7 +10,7 @@ import { useIsSellNative, useIsWrapOrUnwrap } from 'modules/trade' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' -import { HooksTopActions, RescueFundsToggle, TradeWidgetWrapper } from './styled' +import { TradeWidgetWrapper } from './styled' import { useSetRecipientOverride } from '../../hooks/useSetRecipientOverride' import { useSetupHooksStoreOrderParams } from '../../hooks/useSetupHooksStoreOrderParams' @@ -18,7 +18,6 @@ import { IframeDappsManifestUpdater } from '../../updaters/iframeDappsManifestUp import { HookRegistryList } from '../HookRegistryList' import { PostHookButton } from '../PostHookButton' import { PreHookButton } from '../PreHookButton' -import { RescueFundsFromProxy } from '../RescueFundsFromProxy' type HookPosition = 'pre' | 'post' @@ -26,7 +25,6 @@ console.log(ICON_HOOK) export function HooksStoreWidget() { const { account, chainId } = useWalletInfo() - const [isRescueWidgetOpen, setRescueWidgetOpen] = useState(false) const [selectedHookPosition, setSelectedHookPosition] = useState(null) const [hookToEdit, setHookToEdit] = useState(undefined) @@ -53,39 +51,20 @@ export function HooksStoreWidget() { setHookToEdit(uuid) }, []) - useEffect(() => { - if (!account) { - setRescueWidgetOpen(false) - } - }, [account]) - // Close all screens on network changes (including unsupported chain case) - useEffect(() => { - setRescueWidgetOpen(false) - onDismiss() - }, [chainId, isChainIdUnsupported, onDismiss]) + useEffect(onDismiss, [chainId, isChainIdUnsupported, onDismiss]) useSetupHooksStoreOrderParams() useSetRecipientOverride() const isHookSelectionOpen = !!(selectedHookPosition || hookToEdit) - const hideSwapWidget = isHookSelectionOpen || isRescueWidgetOpen + const hideSwapWidget = isHookSelectionOpen const shouldNotUseHooks = isNativeSell || isChainIdUnsupported - const HooksTop = ( - - setRescueWidgetOpen(true)}>Rescue funds - - ) - - const TopContent = shouldNotUseHooks ? ( - HooksTop - ) : isWrapOrUnwrap ? ( - HooksTop - ) : ( + const TopContent = shouldNotUseHooks ? undefined : isWrapOrUnwrap ? undefined : ( <> - {!isRescueWidgetOpen && account && HooksTop} + {account} )} - {isRescueWidgetOpen && setRescueWidgetOpen(false)} />} ) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx index b179ab5a97..386b6e6d31 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx @@ -102,11 +102,13 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { onSelectToken(selectedTokenAddress, undefined, undefined, setSelectedCurrency) }, [onSelectToken, selectedTokenAddress, setSelectedCurrency]) + const etherscanLink = proxyAddress ? getEtherscanLink(chainId, 'address', proxyAddress) : undefined + return ( {!isSelectTokenWidgetOpen && ( <> +

    + CoW Shed is a helper + contract that enhances user experience inside CoW Swap for features like{' '} + + CoW Hooks + + . +

    + +

    + This contract is deployed only once per account. This account becomes the only owner. CoW Shed will act as + an intermediary account who will do the trading on your behalf. +

    + +

    Rescue funds

    +

    + Because this contract holds the funds temporarily, it's possible the funds are stuck in some edge cases. + This tool will help you recover your funds. +

    -

    - In some cases, when orders contain a post-hook using a proxy account, something may go wrong and funds - may remain on the proxy account. Select a currency and get your funds back. -

    + How do I unstuck my funds in CoW Shed? +
      +
    1. + {etherscanLink ? ( + Check in Etherscan + ) : ( + 'Check in Etherscan' + )}{' '} + if your own CoW Shed has any token +
    2. +
    3. Select the token you want to withdraw from CoW Shed
    4. +
    5. Withdraw!
    6. +

    Proxy account:

    - {proxyAddress && ( - + {etherscanLink && ( + {proxyAddress} ↗ )} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx index 2bada3e019..4e9c11ccea 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx @@ -8,6 +8,20 @@ export const Wrapper = styled.div` max-width: ${WIDGET_MAX_WIDTH.swap}; margin: 0 auto; position: relative; + + h3 { + font-size: 24px; + font-weight: 600; + margin: 10px 0; + } + + p { + padding: 0.8rem 0 0.8rem 0; + } + + li { + padding: 0.3rem; + } ` export const ProxyInfo = styled.div` 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 2d337cc379..1b532468f3 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -11,6 +11,7 @@ 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 { Routes } from 'common/constants/routes' import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' @@ -28,10 +29,12 @@ import { SwapWarningsTopProps, } from 'modules/swap/pure/warnings' import { + parameterizeTradeRoute, TradeWidget, TradeWidgetContainer, TradeWidgetSlots, useIsEoaEthFlow, + useIsHooksTradeType, useIsNoImpactWarningAccepted, useReceiveAmountInfo, useTradePriceImpact, @@ -51,6 +54,8 @@ import { SWAP_QUOTE_CHECK_INTERVAL } from 'common/updaters/FeesUpdater' import { useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../hooks/useSwapState' import { useTradeQuoteStateFromLegacy } from '../../hooks/useTradeQuoteStateFromLegacy' import { ConfirmSwapModalSetup } from '../ConfirmSwapModalSetup' +import { InlineBanner } from '@cowprotocol/ui' +import { Link } from 'react-router-dom' export interface SwapWidgetProps { topContent?: ReactNode @@ -77,6 +82,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const recipientToggleState = useRecipientToggleManager() const hooksEnabledState = useHooksEnabledManager() const deadlineState = useUserTransactionTTL() + const isHookTradeType = useIsHooksTradeType() const isTradePriceUpdating = useTradePricesUpdate() @@ -267,6 +273,22 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { useSetLocalTimeOffset(getQuoteTimeOffset(swapButtonContext.quoteDeadlineParams)) + const cowShedLink = useMemo( + () => + parameterizeTradeRoute( + { + chainId: chainId.toString(), + inputCurrencyId: undefined, + outputCurrencyId: undefined, + inputCurrencyAmount: undefined, + outputCurrencyAmount: undefined, + orderKind: undefined, + }, + Routes.COW_SHED, + ), + [chainId], + ) + return ( <> @@ -293,7 +315,13 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { } genericModal={showNativeWrapModal && } /> - + + {!isHookTradeType && } + {isHookTradeType && ( + + CoW Shed: Recover funds + + )} ) diff --git a/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx b/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx new file mode 100644 index 0000000000..cc2810d949 --- /dev/null +++ b/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx @@ -0,0 +1,12 @@ +import { RescueFundsFromProxy } from 'modules/hooksStore/containers/RescueFundsFromProxy' +import { useNavigate } from 'react-router-dom' + +export function CowShed() { + const navigate = useNavigate() + + return ( + <> + navigate(-1)} /> + + ) +} From a0bc92cf3ce8263c620ccd488cdf663028490324 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 14 Nov 2024 09:54:28 +0000 Subject: [PATCH 092/116] fix: add cow-shed page (#5088) * fix: dont show the wallet on the top * fix: dont use etherscan, use generic block explorer * fix: fix title * fix: show alert only when user connected * fix: fix lint * fix: reorder imports --- .../src/common/hooks/useNavigate.ts | 10 +++++++++- .../application/containers/App/RoutesApp.tsx | 2 +- .../containers/HooksStoreWidget/index.tsx | 3 +-- .../containers/RescueFundsFromProxy/index.tsx | 16 ++++++++-------- .../containers/RescueFundsFromProxy/styled.tsx | 12 ++++++------ .../modules/swap/containers/SwapWidget/index.tsx | 12 +++++++----- .../cowswap-frontend/src/pages/Hooks/cowShed.tsx | 8 +++++--- 7 files changed, 37 insertions(+), 26 deletions(-) diff --git a/apps/cowswap-frontend/src/common/hooks/useNavigate.ts b/apps/cowswap-frontend/src/common/hooks/useNavigate.ts index dab6c9de8d..bca86d6fc2 100644 --- a/apps/cowswap-frontend/src/common/hooks/useNavigate.ts +++ b/apps/cowswap-frontend/src/common/hooks/useNavigate.ts @@ -18,6 +18,14 @@ export function useNavigate(): NavigateFunction { ...options, }) }, - [navigate, isWidget] + [navigate, isWidget], ) } + +export function useNavigateBack(): () => void { + const navigate = useNavigateOriginal() + + return useCallback(() => { + navigate(-1) + }, [navigate]) +} diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx index bc6f28c656..373213ea84 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/RoutesApp.tsx @@ -21,10 +21,10 @@ import Account, { AccountOverview } from 'pages/Account' import AdvancedOrdersPage from 'pages/AdvancedOrders' import AnySwapAffectedUsers from 'pages/error/AnySwapAffectedUsers' import { HooksPage } from 'pages/Hooks' +import { CowShed } from 'pages/Hooks/cowShed' import LimitOrderPage from 'pages/LimitOrders' import { SwapPage } from 'pages/Swap' import YieldPage from 'pages/Yield' -import { CowShed } from 'pages/Hooks/cowShed' // Async routes const NotFound = lazy(() => import(/* webpackChunkName: "not_found" */ 'pages/error/NotFound')) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index 704a1ee9c0..a6e367146f 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -24,7 +24,7 @@ type HookPosition = 'pre' | 'post' console.log(ICON_HOOK) export function HooksStoreWidget() { - const { account, chainId } = useWalletInfo() + const { chainId } = useWalletInfo() const [selectedHookPosition, setSelectedHookPosition] = useState(null) const [hookToEdit, setHookToEdit] = useState(undefined) @@ -64,7 +64,6 @@ export function HooksStoreWidget() { const TopContent = shouldNotUseHooks ? undefined : isWrapOrUnwrap ? undefined : ( <> - {account} @@ -131,7 +131,7 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { an intermediary account who will do the trading on your behalf.

    -

    Rescue funds

    + Rescue funds

    Because this contract holds the funds temporarily, it's possible the funds are stuck in some edge cases. This tool will help you recover your funds. @@ -140,10 +140,10 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { How do I unstuck my funds in CoW Shed?

    1. - {etherscanLink ? ( - Check in Etherscan + {explorerLink ? ( + Check in the block explorer ) : ( - 'Check in Etherscan' + 'Check in block explorer' )}{' '} if your own CoW Shed has any token
    2. @@ -153,8 +153,8 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) {

      Proxy account:

      - {etherscanLink && ( - + {explorerLink && ( + {proxyAddress} ↗ )} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx index 4e9c11ccea..0f2b6ecccf 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx @@ -9,12 +9,6 @@ export const Wrapper = styled.div` margin: 0 auto; position: relative; - h3 { - font-size: 24px; - font-weight: 600; - margin: 10px 0; - } - p { padding: 0.8rem 0 0.8rem 0; } @@ -67,3 +61,9 @@ export const Content = styled.div` text-align: center; } ` + +export const Title = styled.div` + font-size: 24px; + font-weight: 600; + margin: 10px 0; +` 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 1b532468f3..95099718d2 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -3,15 +3,18 @@ import { ReactNode, useCallback, useMemo, useState } from 'react' // import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const' import { useIsTradeUnsupported } from '@cowprotocol/tokens' +import { InlineBanner } from '@cowprotocol/ui' import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { TradeType } from '@cowprotocol/widget-lib' +import { Link } from 'react-router-dom' + 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 { Routes } from 'common/constants/routes' + import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' @@ -46,6 +49,7 @@ import { useTradeSlippage } from 'modules/tradeSlippage' import { SettingsTab, TradeRateDetails, useHighFeeWarning } from 'modules/tradeWidgetAddons' import { useTradeUsdAmounts } from 'modules/usdAmount' +import { Routes } from 'common/constants/routes' import { useSetLocalTimeOffset } from 'common/containers/InvalidLocalTimeWarning/localTimeOffsetState' import { useRateInfoParams } from 'common/hooks/useRateInfoParams' import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' @@ -54,8 +58,6 @@ import { SWAP_QUOTE_CHECK_INTERVAL } from 'common/updaters/FeesUpdater' import { useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../hooks/useSwapState' import { useTradeQuoteStateFromLegacy } from '../../hooks/useTradeQuoteStateFromLegacy' import { ConfirmSwapModalSetup } from '../ConfirmSwapModalSetup' -import { InlineBanner } from '@cowprotocol/ui' -import { Link } from 'react-router-dom' export interface SwapWidgetProps { topContent?: ReactNode @@ -63,7 +65,7 @@ export interface SwapWidgetProps { } export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { - const { chainId } = useWalletInfo() + const { chainId, account } = useWalletInfo() const { currencies, trade } = useDerivedSwapInfo() const slippage = useTradeSlippage() const parsedAmounts = useSwapCurrenciesAmounts() @@ -317,7 +319,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { /> {!isHookTradeType && } - {isHookTradeType && ( + {isHookTradeType && !!account && ( CoW Shed: Recover funds diff --git a/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx b/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx index cc2810d949..bcab5e8572 100644 --- a/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx +++ b/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx @@ -1,12 +1,14 @@ import { RescueFundsFromProxy } from 'modules/hooksStore/containers/RescueFundsFromProxy' -import { useNavigate } from 'react-router-dom' + +import { useNavigateBack } from 'common/hooks/useNavigate' + export function CowShed() { - const navigate = useNavigate() + const navigateBack = useNavigateBack() return ( <> - navigate(-1)} /> + ) } From d0f558ad4511e996940474da0aea0c0ca50ca3cf Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Thu, 14 Nov 2024 18:12:51 +0700 Subject: [PATCH 093/116] chore: release main --- .release-please-manifest.json | 2 +- apps/cowswap-frontend/CHANGELOG.md | 16 ++++++++++++++++ apps/cowswap-frontend/package.json | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e16061946e..936bb909ed 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { - "apps/cowswap-frontend": "1.88.0", + "apps/cowswap-frontend": "1.89.0", "apps/explorer": "2.36.1", "libs/permit-utils": "0.4.0", "libs/widget-lib": "0.17.0", diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index 2e812231f2..9f0a46f159 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [1.89.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.88.0...cowswap-v1.89.0) (2024-11-14) + + +### Features + +* enable hooks using settings ([#5081](https://github.com/cowprotocol/cowswap/issues/5081)) ([20f543a](https://github.com/cowprotocol/cowswap/commit/20f543a4690e99e2df18a73995e476ce0efc4a6a)) +* make hooks use partially fillable by default ([#5086](https://github.com/cowprotocol/cowswap/issues/5086)) ([27a8d4d](https://github.com/cowprotocol/cowswap/commit/27a8d4d0f827495cefb16c09c228151fc9f89426)) +* misc hooks improvements ([#5079](https://github.com/cowprotocol/cowswap/issues/5079)) ([adec6fe](https://github.com/cowprotocol/cowswap/commit/adec6fecaed225531140ae947801be9d5ee52532)) + + +### Bug Fixes + +* add cow-shed page ([#5088](https://github.com/cowprotocol/cowswap/issues/5088)) ([a0bc92c](https://github.com/cowprotocol/cowswap/commit/a0bc92cf3ce8263c620ccd488cdf663028490324)) +* add cow-shed page ([#5089](https://github.com/cowprotocol/cowswap/issues/5089)) ([1102691](https://github.com/cowprotocol/cowswap/commit/1102691f8f8260dbbcae9f2fba2629ef59b4384d)) +* **combinedBalances:** Optimize balance diff calculations ([#5082](https://github.com/cowprotocol/cowswap/issues/5082)) ([38aae71](https://github.com/cowprotocol/cowswap/commit/38aae718e54fb50634706eafb973e8027d2b28df)) + ## [1.88.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.87.0...cowswap-v1.88.0) (2024-11-06) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index 5dbe5ce687..814e078d5d 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.88.0", + "version": "1.89.0", "description": "CoW Swap", "main": "index.js", "author": "", From c0ee32262def9d19b04f326bc10ec32886d8b2b0 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Fri, 15 Nov 2024 10:36:27 +0700 Subject: [PATCH 094/116] refactor(hooks-store): use readable string as hook-dapp id (#5093) Co-authored-by: Anxo Rodriguez --- .../src/modules/hooksStore/hookRegistry.tsx | 41 ++++++++----------- libs/hook-dapp-lib/src/hookDappsRegistry.json | 7 ---- libs/hook-dapp-lib/src/utils.ts | 24 ++++++++++- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index e7ab5e3363..d2623fda64 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -1,29 +1,24 @@ -import { hookDappsRegistry } from '@cowprotocol/hook-dapp-lib' +import { HookDappBase, hookDappsRegistry } from '@cowprotocol/hook-dapp-lib' import { AirdropHookApp } from './dapps/AirdropHookApp' import { BuildHookApp } from './dapps/BuildHookApp' import { ClaimGnoHookApp } from './dapps/ClaimGnoHookApp' import { PermitHookApp } from './dapps/PermitHookApp' -import { HookDapp } from './types/hooks' +import { HookDapp, HookDappInternal } from './types/hooks' -export const ALL_HOOK_DAPPS = [ - { - ...hookDappsRegistry.BUILD_CUSTOM_HOOK, - component: (props) => , - }, - { - ...hookDappsRegistry.CLAIM_GNO_FROM_VALIDATORS, - component: (props) => , - }, - { - ...hookDappsRegistry.PERMIT_TOKEN, - component: (props) => , - }, - { - ...hookDappsRegistry.CLAIM_COW_AIRDROP, - component: (props) => , - }, - hookDappsRegistry.COW_AMM_WITHDRAW, - hookDappsRegistry.CLAIM_LLAMAPAY_VESTING, - hookDappsRegistry.CREATE_LLAMAPAY_VESTING -] as HookDapp[] +const HOOK_DAPPS_OVERRIDES: Record> = { + BUILD_CUSTOM_HOOK: { component: (props) => }, + CLAIM_GNO_FROM_VALIDATORS: { component: (props) => }, + PERMIT_TOKEN: { component: (props) => }, + CLAIM_COW_AIRDROP: { component: (props) => }, +} + +export const ALL_HOOK_DAPPS = Object.keys(hookDappsRegistry).map((id) => { + const item = (hookDappsRegistry as Record>)[id] + + return { + ...item, + ...HOOK_DAPPS_OVERRIDES[id], + id, + } +}) as HookDapp[] diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json index b51b64fd65..9583278172 100644 --- a/libs/hook-dapp-lib/src/hookDappsRegistry.json +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -1,6 +1,5 @@ { "BUILD_CUSTOM_HOOK": { - "id": "c768665aa144bcf18c14eea0249b6322050e5daeba046d7e94df743a2e504586", "type": "INTERNAL", "name": "Build your own hook", "descriptionShort": "Call any smart contract with your own parameters", @@ -10,7 +9,6 @@ "website": "https://docs.cow.fi/cow-protocol/tutorials/hook-dapp" }, "CLAIM_GNO_FROM_VALIDATORS": { - "id": "ee4a6b1065cda592972b9ff7448ec111f29a566f137fef101ead7fbf8b01dd0b", "type": "INTERNAL", "name": "Claim GNO from validators", "descriptionShort": "Withdraw rewards from your Gnosis validators.", @@ -24,7 +22,6 @@ } }, "PERMIT_TOKEN": { - "id": "1db4bacb661a90fb6b475fd5b585acba9745bc373573c65ecc3e8f5bfd5dee1f", "type": "INTERNAL", "name": "Permit a token", "descriptionShort": "Infinite permit an address to spend one token on your behalf.", @@ -38,7 +35,6 @@ } }, "CLAIM_COW_AIRDROP": { - "id": "40ed08569519f3b58c410ba35a8e684612663a7c9b58025e0a9c3a54551fb0ff", "type": "INTERNAL", "name": "Claim COW Airdrop", "descriptionShort": "Retrieve COW tokens before or after a swap.", @@ -51,7 +47,6 @@ } }, "COW_AMM_WITHDRAW": { - "id": "5dc71fa5829d976c462bdf37b38b6fd9bbc289252a5a18e61525f8c8a3c775df", "name": "CoW AMM Withdraw Liquidity", "type": "IFRAME", "descriptionShort": "Remove liquidity from a CoW AMM pool before the swap", @@ -67,7 +62,6 @@ } }, "CLAIM_LLAMAPAY_VESTING": { - "id": "5d2c081d11a01ca0b76e2fafbc0d3c62a4c9945ce404706fb1e49e826c0f99eb", "type": "IFRAME", "name": "Claim LlamaPay Vesting Hook", "description": "The Claim LlamaPay Vesting Hook is a powerful and user-friendly feature designed to streamline the process of claiming funds from LlamaPay vesting contracts. This tool empowers users to easily access and manage their vested tokens, ensuring a smooth and efficient experience in handling time-locked assets.", @@ -83,7 +77,6 @@ } }, "CREATE_LLAMAPAY_VESTING": { - "id": "a316488cecc23fde8c39d3e748e0cb12bd4d18305826d36576d4bba74bd97baf", "type": "IFRAME", "name": "Create LlamaPay Vesting", "descriptionShort": "Create a LlamaPay vesting contract", diff --git a/libs/hook-dapp-lib/src/utils.ts b/libs/hook-dapp-lib/src/utils.ts index 56425126a3..cae8c30afb 100644 --- a/libs/hook-dapp-lib/src/utils.ts +++ b/libs/hook-dapp-lib/src/utils.ts @@ -1,6 +1,18 @@ import * as hookDappsRegistry from './hookDappsRegistry.json' import { CowHook, HookDappBase } from './types' +const hookDapps = Object.keys(hookDappsRegistry).reduce((acc, id) => { + const dapp = (hookDappsRegistry as Record>)[id] + + acc.push({ id, ...dapp }) + return acc +}, [] as HookDappBase[]) + +// permit() function selector +const PERMIT_SELECTOR = '0xd505accf' +// TODO: remove it after 01.01.2025 +const PERMIT_DAPP_ID = '1db4bacb661a90fb6b475fd5b585acba9745bc373573c65ecc3e8f5bfd5dee1f' + // Before the hooks store the dappId wasn't included in the hook object type StrictCowHook = Omit & { dappId?: string } @@ -26,6 +38,16 @@ export function matchHooksToDapps(hooks: StrictCowHook[], dapps: HookDappBase[]) const hook = _hook as CowHook const dapp = dappsMap[hook.dappId] + /** + * Permit token is a special case, as it's not a dapp, but a hook + */ + if ((!dapp || hook.dappId === PERMIT_DAPP_ID) && hook.callData.startsWith(PERMIT_SELECTOR)) { + return { + hook, + dapp: hookDappsRegistry.PERMIT_TOKEN as HookDappBase, + } + } + return { hook, dapp: dapp || null, @@ -38,5 +60,5 @@ export function matchHooksToDappsRegistry( hooks: StrictCowHook[], additionalHookDapps: HookDappBase[] = [], ): HookToDappMatch[] { - return matchHooksToDapps(hooks, (Object.values(hookDappsRegistry) as HookDappBase[]).concat(additionalHookDapps)) + return matchHooksToDapps(hooks, hookDapps.concat(additionalHookDapps)) } From 84f5d5e3252f78564821be67ad738009426150f1 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari <55461956+yvesfracari@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:52:56 -0300 Subject: [PATCH 095/116] fix(hooks-store): log simulation only on trade simulation (#5101) --- .../common/containers/OrderHooksDetails/index.tsx | 13 +++++++------ .../modules/trade/pure/TradeConfirmation/index.tsx | 8 +++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index 2199b81928..3c8a623983 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -18,9 +18,10 @@ interface OrderHooksDetailsProps { appData: string | AppDataInfo children: (content: ReactElement) => ReactElement margin?: string + isTradeConfirmation?: boolean } -export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetailsProps) { +export function OrderHooksDetails({ appData, children, margin, isTradeConfirmation }: OrderHooksDetailsProps) { const [isOpen, setOpen] = useState(false) const appDataDoc = useMemo(() => { return typeof appData === 'string' ? decodeAppData(appData) : appData.doc @@ -33,14 +34,14 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai const { mutate, isValidating, data } = useTenderlyBundleSimulation() useEffect(() => { - mutate() - }, []) // eslint-disable-line react-hooks/exhaustive-deps + if (isTradeConfirmation) mutate() + }, [isTradeConfirmation, mutate]) if (!appDataDoc) return null const metadata = appDataDoc.metadata as latest.Metadata - const hasSomeFailedSimulation = Object.values(data || {}).some((hook) => !hook.status) + const hasSomeFailedSimulation = isTradeConfirmation && Object.values(data || {}).some((hook) => !hook.status) const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || [], preCustomHookDapps) const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || [], postCustomHookDapps) @@ -76,8 +77,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai {isOpen && ( - - + + )} , 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 d8b6e7ab8b..979c49913d 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx @@ -127,7 +127,13 @@ export function TradeConfirmation(props: TradeConfirmationProps) { } const hookDetailsElement = ( - <>{appData && {(hookChildren) => hookChildren}} + <> + {appData && ( + + {(hookChildren) => hookChildren} + + )} + ) return ( From 8a8484c58acd8a327d6749b2180c504e873a135a Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 19 Nov 2024 11:22:52 +0500 Subject: [PATCH 096/116] chore: fix hooks store registry import (#5109) --- libs/hook-dapp-lib/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hook-dapp-lib/src/index.ts b/libs/hook-dapp-lib/src/index.ts index aecd63a181..a1c5405fea 100644 --- a/libs/hook-dapp-lib/src/index.ts +++ b/libs/hook-dapp-lib/src/index.ts @@ -3,5 +3,5 @@ export * from './hookDappIframeTransport' export * from './types' export * from './consts' export * from './utils' -import * as hookDappsRegistry from './hookDappsRegistry.json' +import hookDappsRegistry from './hookDappsRegistry.json' export { hookDappsRegistry } From 29ab5c45971767716df2d048b87198ac1acb2c90 Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:33:56 +0000 Subject: [PATCH 097/116] feat: refactor trade container styles (#5103) * feat: refactor trade container styles * feat: break long digits --- apps/cowswap-frontend/src/common/pure/RateInfo/index.tsx | 6 ++++-- .../src/common/pure/TradeDetailsAccordion/styled.ts | 5 ++--- .../limitOrders/containers/LimitOrdersWidget/styled.tsx | 2 ++ .../src/modules/trade/containers/TradeWidget/styled.tsx | 6 ++++++ .../src/modules/trade/containers/TradeWidgetLinks/styled.ts | 2 -- libs/ui/src/pure/FiatAmount/index.tsx | 1 + 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/cowswap-frontend/src/common/pure/RateInfo/index.tsx b/apps/cowswap-frontend/src/common/pure/RateInfo/index.tsx index 107c7a37db..63ff4d9f56 100644 --- a/apps/cowswap-frontend/src/common/pure/RateInfo/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/RateInfo/index.tsx @@ -84,7 +84,9 @@ const InvertIcon = styled.div` min-width: var(--size); min-height: var(--size); border-radius: var(--size); - transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, var(${UI.ANIMATION_DURATION}) ease-in-out; + transition: + background var(${UI.ANIMATION_DURATION}) ease-in-out, + var(${UI.ANIMATION_DURATION}) ease-in-out; } > svg { @@ -118,7 +120,7 @@ export const RateWrapper = styled.button` color: inherit; font-size: 13px; letter-spacing: -0.1px; - text-align: right; + text-align: left; font-weight: 500; width: 100%; ` diff --git a/apps/cowswap-frontend/src/common/pure/TradeDetailsAccordion/styled.ts b/apps/cowswap-frontend/src/common/pure/TradeDetailsAccordion/styled.ts index 13f1713dc6..11fa8f843d 100644 --- a/apps/cowswap-frontend/src/common/pure/TradeDetailsAccordion/styled.ts +++ b/apps/cowswap-frontend/src/common/pure/TradeDetailsAccordion/styled.ts @@ -25,18 +25,17 @@ export const Details = styled.div` export const Summary = styled.div` display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: 1fr auto; justify-content: space-between; align-items: center; width: 100%; - gap: 8px; + gap: 10px; font-size: inherit; font-weight: inherit; span { font-size: inherit; font-weight: inherit; - white-space: nowrap; } ` diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/styled.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/styled.tsx index 541854ffff..c23762aa09 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/styled.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/styled.tsx @@ -14,6 +14,7 @@ export const TradeButtonBox = styled.div` export const FooterBox = styled.div` display: flex; flex-flow: column wrap; + max-width: 100%; margin: 0 4px; padding: 0; ` @@ -22,6 +23,7 @@ export const RateWrapper = styled.div` display: grid; grid-template-columns: auto 151px; grid-template-rows: max-content; + max-width: 100%; gap: 6px; text-align: right; color: inherit; diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx index e1546d3080..5254e05d5a 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx @@ -14,6 +14,7 @@ export const ContainerBox = styled.div` display: flex; flex-flow: column wrap; gap: 10px; + max-width: 100%; background: var(${UI.COLOR_PAPER}); color: var(${UI.COLOR_TEXT_PAPER}); border: none; @@ -22,6 +23,11 @@ export const ContainerBox = styled.div` padding: 10px; position: relative; + > div, + > span { + max-width: 100%; + } + .modalMode & { box-shadow: none; } diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts index fe44cdacf6..8de0c0f7ae 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts @@ -86,8 +86,6 @@ export const MenuItem = styled.div<{ isActive?: boolean; isDropdownVisible: bool width: 100%; margin: 0 0 10px; `} - - } } ` diff --git a/libs/ui/src/pure/FiatAmount/index.tsx b/libs/ui/src/pure/FiatAmount/index.tsx index 7e817cc845..7fd52fa489 100644 --- a/libs/ui/src/pure/FiatAmount/index.tsx +++ b/libs/ui/src/pure/FiatAmount/index.tsx @@ -14,6 +14,7 @@ export interface FiatAmountProps { const Wrapper = styled.span` color: inherit; + word-break: break-all; ` export function FiatAmount({ amount, defaultValue, className, accurate = false }: FiatAmountProps) { From f642ce5d5a8f2f81dbc72cb1d8942eaa8905bb3b Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:44:54 +0000 Subject: [PATCH 098/116] feat: refactor badge component and experimental icon (#5102) * feat: refactor badge component and experimental icon * feat: refactor link color * feat: style and refactor cow shed banner and widget * feat: refactor trade container styles --- .../src/common/constants/routes.ts | 18 +- .../src/common/hooks/useMenuItems.ts | 4 +- .../application/containers/App/index.tsx | 9 +- .../application/containers/App/menuConsts.tsx | 4 +- .../containers/HooksStoreWidget/styled.tsx | 2 +- .../index.tsx | 155 +++++++++++------- .../RecoverFundsFromProxy/styled.tsx | 155 ++++++++++++++++++ .../useRecoverFundsFromProxy.ts} | 2 +- .../RescueFundsFromProxy/styled.tsx | 69 -------- .../swap/containers/SwapWidget/index.tsx | 14 +- .../containers/TradeWidgetLinks/index.tsx | 12 +- .../containers/TradeWidgetLinks/styled.ts | 16 +- .../containers/SettingsTab/index.tsx | 13 +- .../src/pages/Hooks/cowShed.tsx | 5 +- libs/assets/src/cow-swap/experiment.svg | 1 + libs/assets/src/cow-swap/hand.svg | 1 + libs/ui/src/containers/InlineBanner/index.tsx | 64 ++++---- libs/ui/src/pure/Badge/index.tsx | 14 +- libs/ui/src/pure/MenuBar/index.tsx | 21 ++- libs/ui/src/types.ts | 10 +- 20 files changed, 402 insertions(+), 187 deletions(-) rename apps/cowswap-frontend/src/modules/hooksStore/containers/{RescueFundsFromProxy => RecoverFundsFromProxy}/index.tsx (54%) create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/styled.tsx rename apps/cowswap-frontend/src/modules/hooksStore/containers/{RescueFundsFromProxy/useRescueFundsFromProxy.ts => RecoverFundsFromProxy/useRecoverFundsFromProxy.ts} (98%) delete mode 100644 apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx create mode 100644 libs/assets/src/cow-swap/experiment.svg create mode 100644 libs/assets/src/cow-swap/hand.svg diff --git a/apps/cowswap-frontend/src/common/constants/routes.ts b/apps/cowswap-frontend/src/common/constants/routes.ts index afd07b9d8a..9aed8399b7 100644 --- a/apps/cowswap-frontend/src/common/constants/routes.ts +++ b/apps/cowswap-frontend/src/common/constants/routes.ts @@ -1,4 +1,6 @@ +import EXPERIMENT_ICON from '@cowprotocol/assets/cow-swap/experiment.svg' import { isInjectedWidget } from '@cowprotocol/common-utils' +import { BadgeTypes } from '@cowprotocol/ui' export const TRADE_WIDGET_PREFIX = isInjectedWidget() ? '/widget' : '' @@ -39,29 +41,35 @@ export const Routes = { export type RoutesKeys = keyof typeof Routes export type RoutesValues = (typeof Routes)[RoutesKeys] -export const MENU_ITEMS: { +export interface IMenuItem { route: RoutesValues label: string fullLabel?: string description: string badge?: string -}[] = [ + badgeImage?: string + badgeType?: (typeof BadgeTypes)[keyof typeof BadgeTypes] +} + +export const MENU_ITEMS: IMenuItem[] = [ { route: Routes.SWAP, label: 'Swap', description: 'Trade tokens' }, { route: Routes.LIMIT_ORDER, label: 'Limit', fullLabel: 'Limit order', description: 'Set your own price' }, { route: Routes.ADVANCED_ORDERS, label: 'TWAP', description: 'Place orders with a time-weighted average price' }, ] -export const HOOKS_STORE_MENU_ITEM = { +export const HOOKS_STORE_MENU_ITEM: IMenuItem = { route: Routes.HOOKS, label: 'Hooks', description: 'Powerful tool to generate pre/post interaction for CoW Protocol', - badge: '🧪', + badgeImage: EXPERIMENT_ICON, + badgeType: BadgeTypes.INFORMATION, } -export const YIELD_MENU_ITEM = { +export const YIELD_MENU_ITEM: IMenuItem = { route: Routes.YIELD, label: 'Yield', fullLabel: 'Yield', description: 'Provide liquidity', badge: 'New', + badgeType: BadgeTypes.ALERT, } diff --git a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts index aa97c78208..eae6b1488b 100644 --- a/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts +++ b/apps/cowswap-frontend/src/common/hooks/useMenuItems.ts @@ -5,9 +5,9 @@ import { isLocal } from '@cowprotocol/common-utils' import { useHooksEnabled } from 'legacy/state/user/hooks' -import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS, YIELD_MENU_ITEM } from '../constants/routes' +import { HOOKS_STORE_MENU_ITEM, MENU_ITEMS, IMenuItem, YIELD_MENU_ITEM } from '../constants/routes' -export function useMenuItems() { +export function useMenuItems(): IMenuItem[] { const isHooksEnabled = useHooksEnabled() const { isYieldEnabled } = useFeatureFlags() 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 d3502104e3..8f47e30a73 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -6,6 +6,7 @@ import { useFeatureFlags } from '@cowprotocol/common-hooks' import { isInjectedWidget } from '@cowprotocol/common-utils' import { Color, Footer, GlobalCoWDAOStyles, Media, MenuBar, CowSwapTheme } from '@cowprotocol/ui' +import SVG from 'react-inlinesvg' import { NavLink } from 'react-router-dom' import { ThemeProvider } from 'theme' @@ -79,7 +80,13 @@ export function App() { children: menuItems.map((item) => { const href = parameterizeTradeRoute(tradeContext, item.route, true) - return { href, label: item.label, description: item.description, badge: item.badge } + return { + href, + label: item.label, + description: item.description, + badge: item.badgeImage ? : item.badge, + badgeType: item.badgeType, + } }), }, ...NAV_ITEMS, diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx index f8bf52d06a..eb73ad7d7b 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx @@ -1,4 +1,4 @@ -import { MenuItem, ProductVariant } from '@cowprotocol/ui' +import { BadgeTypes, MenuItem, ProductVariant } from '@cowprotocol/ui' import AppziButton from 'legacy/components/AppziButton' import { Version } from 'legacy/components/Version' @@ -42,6 +42,7 @@ export const NAV_ITEMS: MenuItem[] = [ { label: 'More', badge: 'New', + badgeType: BadgeTypes.ALERT, children: [ { href: 'https://cow.fi/cow-protocol', @@ -52,6 +53,7 @@ export const NAV_ITEMS: MenuItem[] = [ href: 'https://cow.fi/cow-amm', label: 'CoW AMM', badge: 'New', + badgeType: BadgeTypes.ALERT, external: true, }, { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/styled.tsx index 6a6e5faed3..8e2dcc3037 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/styled.tsx @@ -21,7 +21,7 @@ export const HooksTopActions = styled.div` background: var(${UI.COLOR_PAPER_DARKER}); ` -export const RescueFundsToggle = styled.button` +export const RecoverFundsToggle = styled.button` background: transparent; font-size: inherit; color: inherit; diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/index.tsx similarity index 54% rename from apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx rename to apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/index.tsx index a21595ec44..e74b5a773a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/index.tsx @@ -1,14 +1,17 @@ import { atom, useAtom } from 'jotai' import { useCallback, useEffect, useState } from 'react' +import IMG_ICON_MINUS from '@cowprotocol/assets/images/icon-minus.svg' +import IMG_ICON_PLUS from '@cowprotocol/assets/images/icon-plus.svg' import { useNativeTokenBalance } from '@cowprotocol/balances-and-allowances' import { getCurrencyAddress, getEtherscanLink, getIsNativeToken } from '@cowprotocol/common-utils' import { Command } from '@cowprotocol/types' -import { BannerOrientation, ButtonPrimary, ExternalLink, InlineBanner, Loader, TokenAmount } from '@cowprotocol/ui' +import { ButtonPrimary, ExternalLink, Loader, TokenAmount } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import ms from 'ms.macro' +import SVG from 'react-inlinesvg' import useSWR from 'swr' import { useErrorModal } from 'legacy/hooks/useErrorMessageAndModal' @@ -25,15 +28,81 @@ import { useTokenContract } from 'common/hooks/useContract' import { CurrencySelectButton } from 'common/pure/CurrencySelectButton' import { NewModal } from 'common/pure/NewModal' -import { Content, ProxyInfo, Wrapper, Title } from './styled' -import { useRescueFundsFromProxy } from './useRescueFundsFromProxy' +import { Content, FAQItem, FAQWrapper, ProxyInfo, Title, Wrapper } from './styled' +import { useRecoverFundsFromProxy } from './useRecoverFundsFromProxy' const BALANCE_UPDATE_INTERVAL = ms`5s` const BALANCE_SWR_CFG = { refreshInterval: BALANCE_UPDATE_INTERVAL, revalidateOnFocus: true } const selectedCurrencyAtom = atom(undefined) -export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { +function FAQ({ explorerLink }: { explorerLink: string | undefined }) { + const [openItems, setOpenItems] = useState>({}) + + const handleToggle = (index: number) => (e: React.MouseEvent) => { + e.preventDefault() + setOpenItems((prev) => ({ ...prev, [index]: !prev[index] })) + } + + const FAQ_DATA = [ + { + question: 'What is CoW Shed?', + answer: ( + <> + CoW Shed is a helper contract + that enhances user experience inside CoW Swap for features like{' '} + CoW Hooks + . +
      +
      + This contract is deployed only once per account. This account becomes the only owner. CoW Shed will act as an + intermediary account who will do the trading on your behalf. +
      +
      + Because this contract holds the funds temporarily, it's possible the funds are stuck in some edge cases. This + tool will help you recover your funds. + + ), + }, + { + question: 'How do I recover my funds from CoW Shed?', + answer: ( + <> +
        +
      1. + {explorerLink ? ( + Check in the block explorer + ) : ( + 'Check in block explorer' + )}{' '} + if your own CoW Shed has any token +
      2. +
      3. Select the token you want to recover from CoW Shed
      4. +
      5. Recover!
      6. +
      + + ), + }, + ] + + return ( + + {FAQ_DATA.map((faq, index) => ( + + + {faq.question} + + + + + {openItems[index] &&
      {faq.answer}
      } +
      + ))} +
      + ) +} + +export function RecoverFundsFromProxy({ onDismiss }: { onDismiss: Command }) { const [selectedCurrency, setSelectedCurrency] = useAtom(selectedCurrencyAtom) const [tokenBalance, setTokenBalance] = useState | null>(null) @@ -55,10 +124,10 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { }, [updateSelectTokenWidget, onDismiss]) const { - callback: rescueFundsCallback, + callback: recoverFundsCallback, isTxSigningInProgress, proxyAddress, - } = useRescueFundsFromProxy(selectedTokenAddress, tokenBalance, isNativeToken) + } = useRecoverFundsFromProxy(selectedTokenAddress, tokenBalance, isNativeToken) const { isLoading: isErc20BalanceLoading } = useSWR( !isNativeToken && erc20Contract && proxyAddress && selectedCurrency @@ -85,18 +154,18 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { const isBalanceLoading = isErc20BalanceLoading || isNativeBalanceLoading - const rescueFunds = useCallback(async () => { + const recoverFunds = useCallback(async () => { try { - const txHash = await rescueFundsCallback() + const txHash = await recoverFundsCallback() if (txHash) { - addTransaction({ hash: txHash, summary: 'Rescue funds from CoW Shed Proxy' }) + addTransaction({ hash: txHash, summary: 'Recover funds from CoW Shed Proxy' }) } } catch (e) { console.error(e) handleSetError(e.message || e.toString()) } - }, [rescueFundsCallback, addTransaction, handleSetError]) + }, [recoverFundsCallback, addTransaction, handleSetError]) const onCurrencySelectClick = useCallback(() => { onSelectToken(selectedTokenAddress, undefined, undefined, setSelectedCurrency) @@ -117,55 +186,24 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { {!isSelectTokenWidgetOpen && ( <> -

      - CoW Shed is a helper - contract that enhances user experience inside CoW Swap for features like{' '} - - CoW Hooks - - . -

      - -

      - This contract is deployed only once per account. This account becomes the only owner. CoW Shed will act as - an intermediary account who will do the trading on your behalf. -

      - - Rescue funds -

      - Because this contract holds the funds temporarily, it's possible the funds are stuck in some edge cases. - This tool will help you recover your funds. -

      - - How do I unstuck my funds in CoW Shed? -
        -
      1. - {explorerLink ? ( - Check in the block explorer - ) : ( - 'Check in block explorer' - )}{' '} - if your own CoW Shed has any token -
      2. -
      3. Select the token you want to withdraw from CoW Shed
      4. -
      5. Withdraw!
      6. -
      -
      - -

      Proxy account:

      - {explorerLink && ( - - {proxyAddress} ↗ - - )} -
      + Recover funds + + +

      Proxy account:

      + {explorerLink && ( + + {proxyAddress} ↗ + + )} +
      + {selectedTokenAddress ? ( <>

      - Balance to be rescued: + Balance to be recovered:
      {tokenBalance ? ( @@ -175,14 +213,21 @@ export function RescueFundsFromProxy({ onDismiss }: { onDismiss: Command }) { ) : null}

      - - {isTxSigningInProgress ? : hasBalance ? 'Rescue funds' : 'No funds to rescue'} + + {isTxSigningInProgress ? ( + + ) : hasBalance ? ( + 'Recover funds' + ) : ( + No funds to recover + )} ) : (
      )}
      + )} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/styled.tsx new file mode 100644 index 0000000000..48c674f85b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/styled.tsx @@ -0,0 +1,155 @@ +import { UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' +import { WIDGET_MAX_WIDTH } from 'theme' + +export const Wrapper = styled.div` + width: 100%; + max-width: ${WIDGET_MAX_WIDTH.swap}; + margin: 0 auto; + position: relative; + + p { + padding: 0.8rem 0 0.8rem 0; + } + + li { + padding: 0.3rem; + } + + .noFunds { + color: var(${UI.COLOR_ALERT_TEXT}); + background: var(${UI.COLOR_ALERT_BG}); + padding: 10px; + border-radius: 16px; + } +` + +export const ProxyInfo = styled.div` + display: flex; + flex-flow: column wrap; + gap: 10px; + margin: 0; + text-align: center; + font-size: 15px; + + > h4 { + font-size: 16px; + font-weight: 600; + margin: 0 auto; + } + + > a { + color: inherit; + width: 100%; + } + + > a > span { + font-size: 100%; + background: var(${UI.COLOR_PAPER}); + border-radius: 16px; + padding: 10px; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + margin: 0 auto; + word-break: break-all; + } +` + +export const Content = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + padding: 20px; + background: var(${UI.COLOR_PAPER_DARKER}); + border-radius: 16px; + + p { + text-align: center; + } +` + +export const Title = styled.div` + font-size: 24px; + font-weight: 600; + margin: 10px 0; +` + +export const FAQWrapper = styled.div` + display: flex; + flex-flow: column wrap; + align-items: flex-start; + gap: 10px; + margin: 24px 0 0; + width: 100%; +` + +export const FAQItem = styled.details` + display: flex; + flex-flow: column wrap; + width: 100%; + margin: 0 auto; + padding: 0; + line-height: 1; + position: relative; + border-radius: ${({ open }) => (open ? '32px' : '56px')}; + + > summary { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + margin: 0; + padding: 8px 8px 8px 10px; + list-style-type: none; + line-height: 1.2; + font-weight: 600; + font-size: 16px; + + &::marker, + &::-webkit-details-marker { + display: none; + } + + > i { + --size: 26px; + width: var(--size); + height: var(--size); + min-height: var(--size); + min-width: var(--size); + border-radius: var(--size); + background: transparent; + transition: background 0.2s ease-in-out; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: ${({ theme }) => theme.bg3}; + } + + > svg { + width: 100%; + height: 100%; + padding: 0; + fill: currentColor; + } + } + } + + > div { + padding: 0 21px 21px; + font-size: 15px; + line-height: 1.8; + color: var(${UI.COLOR_TEXT_OPACITY_70}); + } + + > div > ol { + margin: 0; + padding: 0 0 0 20px; + } +` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts b/apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/useRecoverFundsFromProxy.ts similarity index 98% rename from apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts rename to apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/useRecoverFundsFromProxy.ts index ba687ecb40..d56b76bda1 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/useRescueFundsFromProxy.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/RecoverFundsFromProxy/useRecoverFundsFromProxy.ts @@ -18,7 +18,7 @@ const fnSelector = (sig: string) => keccak256(toUtf8Bytes(sig)).slice(0, 10) const fnCalldata = (sig: string, encodedData: string) => pack(['bytes4', 'bytes'], [fnSelector(sig), encodedData]) -export function useRescueFundsFromProxy( +export function useRecoverFundsFromProxy( selectedTokenAddress: string | undefined, tokenBalance: CurrencyAmount | null, isNativeToken: boolean, diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx deleted file mode 100644 index 0f2b6ecccf..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/RescueFundsFromProxy/styled.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { UI } from '@cowprotocol/ui' - -import styled from 'styled-components/macro' -import { WIDGET_MAX_WIDTH } from 'theme' - -export const Wrapper = styled.div` - width: 100%; - max-width: ${WIDGET_MAX_WIDTH.swap}; - margin: 0 auto; - position: relative; - - p { - padding: 0.8rem 0 0.8rem 0; - } - - li { - padding: 0.3rem; - } -` - -export const ProxyInfo = styled.div` - display: flex; - flex-flow: column wrap; - gap: 10px; - margin: 20px 0; - text-align: center; - - > h4 { - font-size: 14px; - font-weight: 600; - margin: 10px auto 0; - } - - > a { - color: inherit; - width: 100%; - } - - > a > span { - font-size: 100%; - background: var(${UI.COLOR_PAPER_DARKER}); - border-radius: 16px; - padding: 10px; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - margin: 0 auto; - word-break: break-all; - } -` - -export const Content = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 20px; - - p { - text-align: center; - } -` - -export const Title = styled.div` - font-size: 24px; - font-weight: 600; - margin: 10px 0; -` 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 95099718d2..e077b5c717 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -1,9 +1,10 @@ import { ReactNode, useCallback, useMemo, useState } from 'react' // import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' +import HAND_SVG from '@cowprotocol/assets/cow-swap/hand.svg' import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const' import { useIsTradeUnsupported } from '@cowprotocol/tokens' -import { InlineBanner } from '@cowprotocol/ui' +import { BannerOrientation, InlineBanner } from '@cowprotocol/ui' import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { TradeType } from '@cowprotocol/widget-lib' @@ -15,7 +16,6 @@ import { ApplicationModal } from 'legacy/state/application/reducer' import { Field } from 'legacy/state/types' import { useHooksEnabledManager, useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/user/hooks' - import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { EthFlowModal, EthFlowProps } from 'modules/swap/containers/EthFlow' @@ -320,8 +320,14 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { {!isHookTradeType && } {isHookTradeType && !!account && ( - - CoW Shed: Recover funds + + Funds stuck? Recover your funds )} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx index f08534085a..481e965aba 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useMemo, useState } from 'react' import { Command } from '@cowprotocol/types' -import { Badge } from '@cowprotocol/ui' +import { Badge, BadgeTypes } from '@cowprotocol/ui' import type { TradeType } from '@cowprotocol/widget-lib' import { Trans } from '@lingui/macro' @@ -26,6 +26,8 @@ interface MenuItemConfig { route: RoutesValues label: string badge?: string + badgeImage?: string + badgeType?: (typeof BadgeTypes)[keyof typeof BadgeTypes] } const TRADE_TYPE_TO_ROUTE: Record = { @@ -150,11 +152,11 @@ const MenuItem = ({ {item.label} - {!isActive && item.badge && ( - - {item.badge} + {(!isActive && item.badgeImage) || item.badge ? ( + + {item.badgeImage ? : {item.badge}} - )} + ) : null} ) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts index 8de0c0f7ae..2e83e8e7bc 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts @@ -16,16 +16,28 @@ export const Link = styled(NavLink)` color var(${UI.ANIMATION_DURATION}) ease-in-out, fill var(${UI.ANIMATION_DURATION}) ease-in-out; + svg { + width: 10px; + height: 10px; + margin: 0 auto; + object-fit: contain; + + path { + fill: currentColor; + transition: fill var(${UI.ANIMATION_DURATION}) ease-in-out; + } + } + &:hover { color: inherit; text-decoration: none; - > svg > path { + svg > path { fill: currentColor; } } - > svg > path { + svg > path { fill: currentColor; } ` 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 0e1f7065ea..fc7273935d 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/SettingsTab/index.tsx @@ -1,12 +1,14 @@ import { useAtom } from 'jotai' import { ReactElement, RefObject, useCallback, useEffect, useRef } from 'react' +import EXPERIMENT_ICON from '@cowprotocol/assets/cow-swap/experiment.svg' import { isInjectedWidget } from '@cowprotocol/common-utils' import { StatefulValue } from '@cowprotocol/types' import { HelpTooltip, RowBetween, RowFixed } from '@cowprotocol/ui' import { Trans } from '@lingui/macro' import { Menu, useMenuButtonContext } from '@reach/menu-button' +import SVG from 'react-inlinesvg' import { Text } from 'rebass' import { ThemedText } from 'theme' @@ -96,7 +98,16 @@ export function SettingsTab({ className, recipientToggleState, hooksEnabledState Enable Hooks - 🧪 Add DeFI interactions before and after your trade} /> + + + Experimental: + {' '} + Add DeFI interactions before and after your trade + + } + /> diff --git a/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx b/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx index bcab5e8572..c1ee914949 100644 --- a/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx +++ b/apps/cowswap-frontend/src/pages/Hooks/cowShed.tsx @@ -1,14 +1,13 @@ -import { RescueFundsFromProxy } from 'modules/hooksStore/containers/RescueFundsFromProxy' +import { RecoverFundsFromProxy } from 'modules/hooksStore/containers/RecoverFundsFromProxy' import { useNavigateBack } from 'common/hooks/useNavigate' - export function CowShed() { const navigateBack = useNavigateBack() return ( <> - + ) } diff --git a/libs/assets/src/cow-swap/experiment.svg b/libs/assets/src/cow-swap/experiment.svg new file mode 100644 index 0000000000..b66db88c6b --- /dev/null +++ b/libs/assets/src/cow-swap/experiment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/cow-swap/hand.svg b/libs/assets/src/cow-swap/hand.svg new file mode 100644 index 0000000000..71a2e32594 --- /dev/null +++ b/libs/assets/src/cow-swap/hand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/ui/src/containers/InlineBanner/index.tsx b/libs/ui/src/containers/InlineBanner/index.tsx index c43535ede2..a599327ba5 100644 --- a/libs/ui/src/containers/InlineBanner/index.tsx +++ b/libs/ui/src/containers/InlineBanner/index.tsx @@ -67,6 +67,7 @@ const Wrapper = styled.span<{ margin?: string width?: string dismissable?: boolean + backDropBlur?: boolean }>` display: flex; align-items: center; @@ -82,6 +83,7 @@ const Wrapper = styled.span<{ font-weight: 400; line-height: 1.2; width: ${({ width = '100%' }) => width}; + backdrop-filter: ${({ backDropBlur }) => backDropBlur && 'blur(20px)'}; // Icon + Text content wrapper > span { @@ -91,7 +93,7 @@ const Wrapper = styled.span<{ flex-flow: ${({ orientation = BannerOrientation.Vertical }) => orientation === BannerOrientation.Horizontal ? 'row' : 'column wrap'}; gap: 10px; - width: 100%; + width: auto; } > span > svg > path { @@ -107,34 +109,37 @@ const Wrapper = styled.span<{ gap: 10px; justify-content: ${({ orientation = BannerOrientation.Vertical }) => orientation === BannerOrientation.Horizontal ? 'flex-start' : 'center'}; - } - - > span > span a { - color: inherit; - text-decoration: underline; - } - - > span > span > strong { - display: flex; - align-items: center; - text-align: center; - gap: 6px; - color: ${({ colorEnums }) => `var(${colorEnums.text})`}; - } - - > span > span > p { - line-height: 1.4; - margin: auto; - padding: 0; - width: 100%; - text-align: ${({ orientation = BannerOrientation.Vertical }) => - orientation === BannerOrientation.Horizontal ? 'left' : 'center'}; - } - > span > span > i { - font-style: normal; - font-size: 32px; - line-height: 1; + a { + color: inherit; + text-decoration: underline; + } + + > strong { + display: flex; + align-items: center; + text-align: center; + gap: 6px; + } + + > p { + line-height: 1.4; + margin: auto; + padding: 0; + width: 100%; + text-align: ${({ orientation = BannerOrientation.Vertical }) => + orientation === BannerOrientation.Horizontal ? 'left' : 'center'}; + } + + > i { + font-style: normal; + font-size: 32px; + line-height: 1; + } + + > ol { + padding-left: 20px; + } } ` @@ -170,6 +175,7 @@ export interface InlineBannerProps { width?: string noWrapContent?: boolean onClose?: () => void + backDropBlur?: boolean } export function InlineBanner({ @@ -187,6 +193,7 @@ export function InlineBanner({ width, onClose, noWrapContent, + backDropBlur, }: InlineBannerProps) { const colorEnums = getColorEnums(bannerType) @@ -200,6 +207,7 @@ export function InlineBanner({ margin={margin} width={width} dismissable={!!onClose} + backDropBlur={backDropBlur} > {!hideIcon && customIcon ? ( diff --git a/libs/ui/src/pure/Badge/index.tsx b/libs/ui/src/pure/Badge/index.tsx index 5346854894..9ef7e476f2 100644 --- a/libs/ui/src/pure/Badge/index.tsx +++ b/libs/ui/src/pure/Badge/index.tsx @@ -8,7 +8,7 @@ const badgeBackgrounds: Record = { alert: `var(${UI.COLOR_ALERT_BG})`, alert2: `var(${UI.COLOR_BADGE_YELLOW_BG})`, success: `var(${UI.COLOR_SUCCESS_BG})`, - default: 'transparent', // text only + default: 'transparent', } const badgeColors: Record = { @@ -16,7 +16,7 @@ const badgeColors: Record = { alert: `var(${UI.COLOR_ALERT_TEXT})`, alert2: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, success: `var(${UI.COLOR_SUCCESS_TEXT})`, - default: `var(${UI.COLOR_DISABLED_TEXT})`, // text only + default: `var(${UI.COLOR_DISABLED_TEXT})`, } export const Badge = styled.div<{ type?: BadgeType }>` @@ -31,12 +31,18 @@ export const Badge = styled.div<{ type?: BadgeType }>` padding: ${({ type }) => (!type || type === 'default' ? '0' : '4px 6px')}; letter-spacing: 0.2px; font-weight: 600; - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out; + transition: all var(${UI.ANIMATION_DURATION}) ease-in-out; margin: 0; - a & { + svg { + width: 10px; + height: 10px; color: ${({ type }) => badgeColors[type || 'default']}; } + + svg > path { + fill: ${({ type }) => badgeColors[type || 'default']}; + } ` Badge.defaultProps = { diff --git a/libs/ui/src/pure/MenuBar/index.tsx b/libs/ui/src/pure/MenuBar/index.tsx index 5e27a41623..faa781c469 100644 --- a/libs/ui/src/pure/MenuBar/index.tsx +++ b/libs/ui/src/pure/MenuBar/index.tsx @@ -34,6 +34,7 @@ import { import { Color } from '../../consts' import { Media } from '../../consts' +import { BadgeType } from '../../types' import { Badge } from '../Badge' import { ProductLogo, ProductVariant } from '../ProductLogo' @@ -87,7 +88,7 @@ type LinkComponentType = ComponentType> export interface MenuItem { href?: string label?: string - badge?: string + badge?: string | JSX.Element children?: DropdownMenuItem[] productVariant?: ProductVariant icon?: string @@ -102,6 +103,8 @@ export interface MenuItem { hasDivider?: boolean utmContent?: string utmSource?: string + badgeImage?: string + badgeType?: BadgeType } interface DropdownMenuItem { @@ -109,7 +112,7 @@ interface DropdownMenuItem { external?: boolean label?: string icon?: string - badge?: string + badge?: string | JSX.Element description?: string isButton?: boolean children?: DropdownMenuItem[] @@ -123,6 +126,8 @@ interface DropdownMenuItem { hasDivider?: boolean utmContent?: string utmSource?: string + badgeImage?: string + badgeType?: BadgeType } interface DropdownMenuContent { @@ -414,7 +419,11 @@ const GenericDropdown: React.FC = ({ {item.label} - {item.badge && {item.badge}} + {(item.badge || item.badgeImage) && ( + + {item.badgeImage ? : item.badge} + + )} {item.children && } {isOpen && ( @@ -485,7 +494,11 @@ const DropdownContentWrapper: React.FC = ({ {item.label} - {item.badge && {item.badge}} + {(item.badge || item.badgeImage) && ( + + {item.badgeImage ? : item.badge} + + )} {item.description && {item.description}} diff --git a/libs/ui/src/types.ts b/libs/ui/src/types.ts index 02a65e50bc..92ca4d493d 100644 --- a/libs/ui/src/types.ts +++ b/libs/ui/src/types.ts @@ -11,6 +11,14 @@ export type ComposableCowInfo = { isTheLastPart?: boolean } -export type BadgeType = 'information' | 'success' | 'alert' | 'alert2' | 'default' +export const BadgeTypes = { + INFORMATION: 'information', + SUCCESS: 'success', + ALERT: 'alert', + ALERT2: 'alert2', + DEFAULT: 'default', +} as const + +export type BadgeType = (typeof BadgeTypes)[keyof typeof BadgeTypes] export type CowSwapTheme = 'dark' | 'light' | 'darkHalloween' From 4858b7c1cfd220b98d2f185682c6f71af2f8edfc Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:14:55 +0000 Subject: [PATCH 099/116] fix(hooks-store): fix custom hook alert title and trim slash from url (#5117) * feat: fix custom hook alert title and trim slash from url * feat: fix inline banner margin --- .../common/pure/ExternalSourceAlert/index.tsx | 2 +- .../pure/ExternalSourceAlert/styled.tsx | 19 ++++++++----------- .../pure/AddCustomHookForm/index.tsx | 2 +- .../swap/containers/SwapWidget/index.tsx | 1 + 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/index.tsx b/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/index.tsx index e9337c3357..fefc45c272 100644 --- a/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/index.tsx @@ -16,7 +16,7 @@ export function ExternalSourceAlert({ className, onChange, title, children }: Ex return ( -

      {title}

      + {title} {children} diff --git a/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/styled.tsx b/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/styled.tsx index a3580527c5..f3af0ef9c9 100644 --- a/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/styled.tsx +++ b/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/styled.tsx @@ -14,17 +14,6 @@ export const Contents = styled.div` color: var(${UI.COLOR_DANGER_TEXT}); background: var(${UI.COLOR_DANGER_BG}); - h3 { - font-size: 24px; - text-align: center; - margin: 16px 0; - font-weight: bold; - } - - p { - margin: 6px 0; - } - > svg > path, > svg > line { stroke: var(${UI.COLOR_DANGER_TEXT}); @@ -32,6 +21,14 @@ export const Contents = styled.div` } ` +export const Title = styled.h4` + font-size: 24px; + text-align: center; + margin: 16px 0; + font-weight: bold; + width: 100%; +` + export const AcceptanceBox = styled.label` display: flex; gap: 6px; diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx index 31cb491b0e..2f9600e2f5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx @@ -82,7 +82,7 @@ export function AddCustomHookForm({ addHookDapp, children, isPreHook, walletType type="text" placeholder="Enter a hook dapp URL" value={input} - onChange={(e) => setInput(e.target.value?.trim())} + onChange={(e) => setInput(e.target.value?.trim().replace(/\/+$/, ''))} /> {/* Validation and Error Messages */} 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 e077b5c717..d52653c29a 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -326,6 +326,7 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { iconSize={24} orientation={BannerOrientation.Horizontal} backDropBlur + margin="10px auto auto" > Funds stuck? Recover your funds
      From 4b7267fb6ea37c7bfcc896c7953b70fa8af2729d Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:17:53 +0000 Subject: [PATCH 100/116] feat(hooks-store): add loading logo for hook-dapp (#5112) * feat: add loading logo for hook * feat: hide tooltip when hook modal open * feat: format quote update banner --- .../containers/HooksStoreWidget/index.tsx | 12 +++- .../containers/IframeDappContainer/index.tsx | 59 ++++++++++++++++++- .../containers/PostHookButton/index.tsx | 9 +-- .../containers/PreHookButton/index.tsx | 7 ++- .../trade/pure/PriceUpdatedBanner/index.tsx | 4 +- 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index a6e367146f..dce55edd8e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -81,12 +81,20 @@ export function HooksStoreWidget() {

      - setSelectedHookPosition('pre')} onEditHook={onPreHookEdit} /> + setSelectedHookPosition('pre')} + onEditHook={onPreHookEdit} + hideTooltip={isHookSelectionOpen} + /> ) const BottomContent = shouldNotUseHooks ? null : ( - setSelectedHookPosition('post')} onEditHook={onPostHookEdit} /> + setSelectedHookPosition('post')} + onEditHook={onPostHookEdit} + hideTooltip={isHookSelectionOpen} + /> ) return ( diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx index c463218c7b..ae103c6e1a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx @@ -2,6 +2,7 @@ import { useLayoutEffect, useRef, useState } from 'react' import { CoWHookDappEvents, hookDappIframeTransport } from '@cowprotocol/hook-dapp-lib' import { EthereumProvider, IframeRpcProviderBridge } from '@cowprotocol/iframe-transport' +import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' import { useWalletProvider } from '@cowprotocol/wallet-provider' import styled from 'styled-components/macro' @@ -11,6 +12,41 @@ import { HookDappContext as HookDappContextType, HookDappIframe } from '../../ty const Iframe = styled.iframe` border: 0; min-height: 300px; + opacity: ${({ $isLoading }: { $isLoading: boolean }) => ($isLoading ? 0 : 1)}; + transition: opacity 0.2s ease-in-out; +` + +const LoadingWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 200px; + gap: 16px; +` + +const LoadingText = styled.div` + color: var(${UI.COLOR_TEXT_OPACITY_70}); + font-size: 15px; +` + +const StyledProductLogo = styled(ProductLogo)` + animation: pulse 1.5s ease-in-out infinite; + + @keyframes pulse { + 0% { + opacity: 0; + transform: scale(0.95); + } + 50% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.95); + } + } ` interface IframeDappContainerProps { @@ -26,6 +62,7 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps) const setBuyTokenRef = useRef(context.setBuyToken) const [isIframeActive, setIsIframeActive] = useState(false) + const [isLoading, setIsLoading] = useState(true) const walletProvider = useWalletProvider() @@ -34,6 +71,10 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps) setSellTokenRef.current = context.setSellToken setBuyTokenRef.current = context.setBuyToken + const handleIframeLoad = () => { + setIsLoading(false) + } + useLayoutEffect(() => { const iframeWindow = iframeRef.current?.contentWindow @@ -85,5 +126,21 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps) hookDappIframeTransport.postMessageToWindow(iframeWindow, CoWHookDappEvents.CONTEXT_UPDATE, iframeContext) }, [context, isIframeActive]) - return