diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx index 72505167f2..883652f690 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx @@ -209,7 +209,16 @@ export function CoWAmmBanner() { return ClosableBanner('cow_amm_banner', (close) => ( - + { + cowAnalytics.sendEvent({ + category: 'CoW Swap', + action: 'CoW AMM Banner CTA Closed', + }) + close() + }} + />
Now live: the first MEV-capturing AMM 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 3764770f64..a60eb2862a 100644 --- a/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx @@ -8,10 +8,14 @@ import { useWalletInfo } from '@cowprotocol/wallet' import ReactDOM from 'react-dom' + + import { upToLarge, useMediaQuery } from 'legacy/hooks/useMediaQuery' import { useToggleAccountModal } from 'modules/account' +import { clickNotifications } from 'modules/analytics' import { NotificationBell, NotificationSidebar } from 'modules/notifications' +import { useUnreadNotifications } from 'modules/notifications/hooks/useUnreadNotifications' import { Web3Status } from 'modules/wallet/containers/Web3Status' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' @@ -33,6 +37,9 @@ export function AccountElement({ className, standaloneMode, pendingActivities }: const isUpToLarge = useMediaQuery(upToLarge) const { isNotificationsFeedEnabled } = useFeatureFlags() + const unreadNotifications = useUnreadNotifications() + const unreadNotificationsCount = Object.keys(unreadNotifications).length + const [isSidebarOpen, setSidebarOpen] = useState(false) return ( @@ -44,7 +51,17 @@ export function AccountElement({ className, standaloneMode, pendingActivities }: )} account && toggleAccountModal()} /> - {account && isNotificationsFeedEnabled && setSidebarOpen(true)} />} + {account && isNotificationsFeedEnabled && ( + { + clickNotifications( + unreadNotificationsCount === 0 ? 'click-bell' : 'click-bell-with-pending-notifications' + ) + setSidebarOpen(true) + }} + /> + )} {ReactDOM.createPortal( diff --git a/apps/cowswap-frontend/src/legacy/utils/priceLegacy.ts b/apps/cowswap-frontend/src/legacy/utils/priceLegacy.ts index bf6c37a92e..93580d648c 100644 --- a/apps/cowswap-frontend/src/legacy/utils/priceLegacy.ts +++ b/apps/cowswap-frontend/src/legacy/utils/priceLegacy.ts @@ -15,11 +15,6 @@ import { } from 'api/1inch' import { getQuote } from 'api/cowProtocol' import QuoteApiError, { QuoteApiErrorCodes } from 'api/cowProtocol/errors/QuoteError' -import { - getPriceQuote as getPriceQuoteMatcha, - MatchaPriceQuote, - toPriceInformation as toPriceInformationMatcha, -} from 'api/matcha-0x' import { LegacyPriceInformationWithSource, @@ -74,9 +69,6 @@ function _filterWinningPrice(params: FilterWinningPriceParams) { export type QuoteResult = [PromiseSettledResult, PromiseSettledResult] export type AllPricesResult = { - gpPriceResult: PromiseSettledResult - paraSwapPriceResult: PromiseSettledResult - matcha0xPriceResult: PromiseSettledResult oneInchPriceResult: PromiseSettledResult } @@ -84,19 +76,12 @@ export type AllPricesResult = { * Return all price estimations from all price sources */ async function getAllPrices(params: LegacyPriceQuoteParams): Promise { - const matchaPricePromise = withTimeout(getPriceQuoteMatcha(params), PRICE_API_TIMEOUT_MS, 'Matcha(0x): Get Price API') - const oneInchPricePromise = withTimeout(getPriceQuote1inch(params), PRICE_API_TIMEOUT_MS, '1inch: Get Price API') // Get results from API queries - const [matchaPrice, oneInchPrice] = await Promise.allSettled([matchaPricePromise, oneInchPricePromise]) + const [oneInchPrice] = await Promise.allSettled([oneInchPricePromise]) return { - // Warning! - // /markets endpoint was deleted, so we just skip it - gpPriceResult: { status: 'fulfilled', value: null }, - paraSwapPriceResult: { status: 'fulfilled', value: null }, - matcha0xPriceResult: matchaPrice, oneInchPriceResult: oneInchPrice, } } @@ -106,35 +91,12 @@ async function getAllPrices(params: LegacyPriceQuoteParams): Promise, - paraSwapPriceResult: PromiseSettledResult, - matchaPriceResult: PromiseSettledResult, oneInchPriceResult: PromiseSettledResult ): [Array, Array] { // Prepare an array with all successful estimations const priceQuotes: Array = [] const errorsGetPrice: Array = [] - if (isPromiseFulfilled(gpPriceResult)) { - const gpPrice = gpPriceResult.value - if (gpPrice) { - priceQuotes.push({ ...gpPrice, source: 'gnosis-protocol' }) - } - } else { - errorsGetPrice.push({ ...gpPriceResult, source: 'gnosis-protocol' }) - } - - if (isPromiseFulfilled(matchaPriceResult)) { - const matchaPrice = toPriceInformationMatcha(matchaPriceResult.value, kind) - if (matchaPrice) { - priceQuotes.push({ ...matchaPrice, source: 'matcha-0x', data: matchaPriceResult.value }) - } - } else { - errorsGetPrice.push({ ...matchaPriceResult, source: 'matcha-0x' }) - } - if (isPromiseFulfilled(oneInchPriceResult)) { const oneInchPrice = toPriceInformation1inch(oneInchPriceResult.value) if (oneInchPrice) { @@ -172,17 +134,10 @@ export async function getBestPrice( options?: GetBestPriceOptions ): Promise { // Get all prices - const { gpPriceResult, paraSwapPriceResult, matcha0xPriceResult, oneInchPriceResult } = await getAllPrices(params) + const { oneInchPriceResult } = await getAllPrices(params) // Aggregate successful and error prices - const [priceQuotes, errorsGetPrice] = _extractPriceAndErrorPromiseValues( - // we pass the kind of trade here as matcha doesn't have an easy way to differentiate - params.kind, - gpPriceResult, - paraSwapPriceResult, - matcha0xPriceResult, - oneInchPriceResult - ) + const [priceQuotes, errorsGetPrice] = _extractPriceAndErrorPromiseValues(oneInchPriceResult) // Print prices who failed to be fetched if (errorsGetPrice.length > 0) { @@ -199,11 +154,7 @@ export async function getBestPrice( return _filterWinningPrice({ ...options, kind: params.kind, amounts, priceQuotes }) } else { // It was not possible to get a price estimation - const priceQuoteError = new LegacyPriceQuoteError('Error querying price from APIs', params, [ - gpPriceResult, - paraSwapPriceResult, - matcha0xPriceResult, - ]) + const priceQuoteError = new LegacyPriceQuoteError('Error querying price from APIs', params, [oneInchPriceResult]) const sentryError = new Error() Object.assign(sentryError, priceQuoteError, { diff --git a/apps/cowswap-frontend/src/modules/account/containers/OrdersPanel/index.tsx b/apps/cowswap-frontend/src/modules/account/containers/OrdersPanel/index.tsx index acb08e1d04..050d1c350e 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/OrdersPanel/index.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/OrdersPanel/index.tsx @@ -45,7 +45,7 @@ const SideBar = styled.div` height: 100%; max-width: 100%; border-radius: ${({ theme }) => (theme.isInjectedWidgetMode ? '24px' : '0')}; - z-index: 10000; + z-index: 10; } ` diff --git a/apps/cowswap-frontend/src/modules/analytics/events.ts b/apps/cowswap-frontend/src/modules/analytics/events.ts index 71d842e614..4d72a1c519 100644 --- a/apps/cowswap-frontend/src/modules/analytics/events.ts +++ b/apps/cowswap-frontend/src/modules/analytics/events.ts @@ -24,6 +24,7 @@ enum Category { TWAP = 'TWAP', COW_FORTUNE = 'CoWFortune', SURPLUS_MODAL = 'Surplus Modal', + NOTIFICATIONS = 'Notifications', } export function shareFortuneTwitterAnalytics() { @@ -321,3 +322,12 @@ export function shareSurplusOnTwitter() { action: `Share on Twitter`, }) } + +export function clickNotifications(event: string, notificationId?: number, title?: string) { + cowAnalytics.sendEvent({ + category: Category.NOTIFICATIONS, + action: event, + value: notificationId, + label: title, + }) +} 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 8ffd6d9a32..6cd7c3ad37 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx @@ -8,18 +8,18 @@ import styled from 'styled-components/macro' import { HookDappContext } from '../../context' import buildImg from '../../images/build.png' -const TITLE = 'Build your own Pre-hook' -const DESCRIPTION = 'Add an arbitrary calldata to be executed before your hook' - -export const PRE_BUILD: HookDappInternal = { - name: TITLE, - description: DESCRIPTION, +const getAppDetails = (isPreHook: boolean): HookDappInternal => ({ + name: `Build your own ${isPreHook ? 'Pre' : 'Post'}-hook`, + description: `Add an arbitrary calldata to be executed ${isPreHook ? 'before' : 'after'} your hook`, type: HookDappType.INTERNAL, path: '/hooks-dapps/pre/build', image: buildImg, - component: , + component: , version: 'v0.1.0', -} +}) + +export const PRE_BUILD = getAppDetails(true) +export const POST_BUILD = getAppDetails(false) const Wrapper = styled.div` display: flex; @@ -80,7 +80,11 @@ const Row = styled.div` } ` -export function ClaimGnoHookApp() { +export interface BuildHookAppProps { + isPreHook: boolean +} + +export function BuildHookApp({ isPreHook }: BuildHookAppProps) { const hookDappContext = useContext(HookDappContext) const [hook, setHook] = useState({ target: '', @@ -88,6 +92,8 @@ export function ClaimGnoHookApp() { gasLimit: '', }) + const dapp = isPreHook ? PRE_BUILD : POST_BUILD + const clickOnAddHook = useCallback(() => { const { callData, gasLimit, target } = hook if (!hookDappContext || !callData || !gasLimit || !target) { @@ -97,10 +103,10 @@ export function ClaimGnoHookApp() { hookDappContext.addHook( { hook: hook, - dapp: PRE_BUILD, + dapp, outputTokens: undefined, // TODO: Simulate and extract the output tokens }, - true + isPreHook ) }, [hook, hookDappContext]) @@ -111,8 +117,8 @@ export function ClaimGnoHookApp() { return (
- {TITLE} -

{DESCRIPTION}

+ {dapp.name} +

{dapp.description}

@@ -141,7 +147,7 @@ export function ClaimGnoHookApp() { /> - +Add Pre-hook + +Add {isPreHook ? 'Pre' : 'Post'}-hook { e.preventDefault() diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index 089ed89421..ce7540ad8b 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -1,10 +1,9 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { HookDapp, HookDappIframe, HookDappType } from '@cowprotocol/types' -import { PRE_BUILD } from './dapps/BuildHookApp' +import { PRE_BUILD, POST_BUILD } from './dapps/BuildHookApp' import { PRE_CLAIM_GNO } from './dapps/ClaimGnoHookApp' import bridgeImg from './images/bridge.svg' -import buildImg from './images/build.png' import cowAMM from './images/cowAMM.png' import curveImg from './images/curve.svg' import daiImg from './images/dai.svg' @@ -74,15 +73,6 @@ const POST_MAKER: HookDappIframe = { version: FAKE_VERSION, } -const POST_BUILD: HookDappIframe = { - name: 'Build your own Post-hook', - description: 'Add an arbitrary calldata to be executed after your hook', - type: HookDappType.IFRAME, - url: FAKE_URL, - image: buildImg, - version: FAKE_VERSION, -} - const POST_HOOK_DAPPS_ALL = [POST_BRIDGE, POST_MAKER, POST_BUILD] export const PRE_HOOK_REGISTRY: Record = { @@ -95,6 +85,6 @@ export const PRE_HOOK_REGISTRY: Record = { export const POST_HOOK_REGISTRY: Record = { [SupportedChainId.MAINNET]: POST_HOOK_DAPPS_ALL, [SupportedChainId.GNOSIS_CHAIN]: POST_HOOK_DAPPS_ALL, - [SupportedChainId.SEPOLIA]: [], - [SupportedChainId.ARBITRUM_ONE]: [], + [SupportedChainId.SEPOLIA]: [POST_BUILD], + [SupportedChainId.ARBITRUM_ONE]: [POST_BUILD], } diff --git a/apps/cowswap-frontend/src/modules/notifications/containers/NotificationBell.tsx b/apps/cowswap-frontend/src/modules/notifications/containers/NotificationBell.tsx index 82c359f819..67823bee82 100644 --- a/apps/cowswap-frontend/src/modules/notifications/containers/NotificationBell.tsx +++ b/apps/cowswap-frontend/src/modules/notifications/containers/NotificationBell.tsx @@ -7,7 +7,6 @@ import { UI } from '@cowprotocol/ui' import SVG from 'react-inlinesvg' import styled from 'styled-components/macro' -import { useUnreadNotifications } from '../hooks/useUnreadNotifications' const Icon = styled.div<{ hasNotification?: boolean }>` --size: 18px; @@ -59,14 +58,12 @@ const Icon = styled.div<{ hasNotification?: boolean }>` interface NotificationBellProps { onClick: Command + unreadCount: number } -export function NotificationBell({ onClick }: NotificationBellProps) { - const unreadNotifications = useUnreadNotifications() - const unreadNotificationsCount = Object.keys(unreadNotifications).length - +export function NotificationBell({ onClick, unreadCount }: NotificationBellProps) { return ( - 0} onClick={onClick}> + 0} onClick={onClick}> ) diff --git a/apps/cowswap-frontend/src/modules/notifications/containers/NotificationsList/index.tsx b/apps/cowswap-frontend/src/modules/notifications/containers/NotificationsList/index.tsx index 38915f4fa4..a3f5f76b30 100644 --- a/apps/cowswap-frontend/src/modules/notifications/containers/NotificationsList/index.tsx +++ b/apps/cowswap-frontend/src/modules/notifications/containers/NotificationsList/index.tsx @@ -1,6 +1,8 @@ import { useSetAtom } from 'jotai/index' import React, { ReactNode, useEffect, useMemo } from 'react' +import { clickNotifications } from 'modules/analytics' + import { ListWrapper, NoNotifications, NotificationCard, NotificationsListWrapper, NotificationThumb } from './styled' import { useAccountNotifications } from '../../hooks/useAccountNotifications' @@ -50,6 +52,7 @@ export function NotificationsList({ children }: { children: ReactNode }) { target={target} noImage={!thumbnail} rel={target === '_blank' ? 'noopener noreferrer' : ''} + onClick={() => clickNotifications('click-notification-card', id, title)} > {thumbnail && ( diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.tsx index a8ecc79e41..98aec92994 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.tsx @@ -1,5 +1,3 @@ -import { useCallback } from 'react' - import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' import { UI } from '@cowprotocol/ui' @@ -15,7 +13,6 @@ import { OrderSubmittedContent } from 'common/pure/OrderSubmittedContent' import { TransactionErrorContent } from 'common/pure/TransactionErrorContent' import { TradeAmounts } from 'common/types' -import { useSetShowFollowPendingTxPopup } from '../../../wallet/hooks/useSetShowFollowPendingTxPopup' import { useTradeConfirmActions } from '../../hooks/useTradeConfirmActions' import { useTradeConfirmState } from '../../hooks/useTradeConfirmState' @@ -43,15 +40,9 @@ export function TradeConfirmModal(props: TradeConfirmModalProps) { const isSafeWallet = useIsSafeWallet() const { permitSignatureState, pendingTrade, transactionHash, error } = useTradeConfirmState() const { onDismiss } = useTradeConfirmActions() - const setShowFollowPendingTxPopup = useSetShowFollowPendingTxPopup() const order = useOrder({ chainId, id: transactionHash || undefined }) - const dismissConfirmation = useCallback(() => { - setShowFollowPendingTxPopup(true) - onDismiss() - }, [onDismiss, setShowFollowPendingTxPopup]) - if (!account) return null return ( @@ -63,7 +54,7 @@ export function TradeConfirmModal(props: TradeConfirmModalProps) { title={title} pendingTrade={pendingTrade} transactionHash={transactionHash} - onDismiss={dismissConfirmation} + onDismiss={onDismiss} permitSignatureState={permitSignatureState} isSafeWallet={isSafeWallet} submittedContent={submittedContent} @@ -89,6 +80,7 @@ type InnerComponentProps = { submittedContent?: CustomSubmittedContent order?: Order } + function InnerComponent(props: InnerComponentProps) { const { account, 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 fb8bb55242..cb7c298f86 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -87,22 +87,22 @@ export function TradeWidgetLinks({ location.pathname, highlightedBadgeText, highlightedBadgeType, - handleMenuItemClick + handleMenuItemClick, ]) const singleMenuItem = menuItemsElements.length === 1 + const selectedMenuItem = menuItemsElements.find((item) => item.props.isActive) || menuItemsElements[0] + return isDropdown ? ( <> !singleMenuItem && setDropdownVisible(!isDropdownVisible)} isDropdownVisible={isDropdownVisible} > - item.props.isActive)?.props.routePath || '#'}> - - {menuItemsElements.find((item) => item.props.isActive)?.props.item.label} - {!singleMenuItem ? : null} - + + {selectedMenuItem.props.item.label} + {!singleMenuItem ? : null} diff --git a/apps/cowswap-frontend/src/modules/wallet/containers/FollowPendingTxPopup/FollowPendingTxPopupUI.tsx b/apps/cowswap-frontend/src/modules/wallet/containers/FollowPendingTxPopup/FollowPendingTxPopupUI.tsx deleted file mode 100644 index afe5fde4e9..0000000000 --- a/apps/cowswap-frontend/src/modules/wallet/containers/FollowPendingTxPopup/FollowPendingTxPopupUI.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react' - -import { Command } from '@cowprotocol/types' -import { Media, Tooltip, TooltipProps, UI } from '@cowprotocol/ui' - -import { X } from 'react-feather' -import { Text } from 'rebass' -import styled from 'styled-components/macro' - -import { AutoColumn } from 'legacy/components/Column' - -interface PopupContentProps { - onCheck: Command - onClose: Command -} -type FollowingTxPopupProps = Omit & PopupContentProps - -const IconClose = styled(X)` - position: absolute; - right: 10px; - top: 10px; - color: inherit; - opacity: 0.7; - transition: opacity ${UI.ANIMATION_DURATION} ease-in-out; - - &:hover { - opacity: 1; - cursor: pointer; - } - - svg { - stroke: currentColor; - } -` - -const TooltipWrapper = styled(Tooltip)` - > .arrow- { - z-index: 1; - } - - > div { - max-width: 370px; - } - - ${Media.upToLarge()} { - padding-right: 0.8rem; - } - ${Media.upToExtraSmall()} { - padding-left: 0; - padding-right: 0.5rem; - } -` - -const BodyWrapper = styled(AutoColumn)` - display: flex; - gap: 1rem; - padding-top: 0.3rem; - - > div:nth-child(2) { - padding-top: 0.5rem; - font-size: 18px; - } - - ${Media.upToLarge()} { - gap: 0.8rem; - } - ${Media.upToSmall()} { - gap: 0.6rem; - padding-top: 0.5rem; - padding-top: auto; - } - ${Media.upToExtraSmall()} { - gap: 0.4rem; - } -` - -const AutoColumnWrapper = styled(AutoColumn)` - min-width: 21rem; - * input { - margin-left: 0; - } - ${Media.upToSmall()} { - max-width: 9rem; - min-width: auto; - } -` - -const StyledClose = styled(IconClose)` - top: 0.5rem; - ${Media.upToExtraSmall()} { - right: 0.5rem; - } -` - -const PopupContent = ({ onCheck, onClose }: PopupContentProps) => { - const _onCheckout = (event: React.ChangeEvent) => { - event.stopPropagation() - onCheck() - } - - return ( - ) => e.stopPropagation()}> - -
💡
- - - Follow your pending transactions here! - - - - - -
- ) -} - -export function FollowPendingTxPopupUI({ - show, - children, - onCheck, - onClose, - ...rest -}: FollowingTxPopupProps): JSX.Element { - return ( - } - {...rest} - > -
- {children} -
-
- ) -} diff --git a/apps/cowswap-frontend/src/modules/wallet/containers/FollowPendingTxPopup/index.tsx b/apps/cowswap-frontend/src/modules/wallet/containers/FollowPendingTxPopup/index.tsx deleted file mode 100644 index bd33661487..0000000000 --- a/apps/cowswap-frontend/src/modules/wallet/containers/FollowPendingTxPopup/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useAtomValue, useSetAtom } from 'jotai' -import { selectAtom } from 'jotai/utils' -import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef } from 'react' - -import { Command } from '@cowprotocol/types' - -import { useRecentActivityLastPendingOrder } from 'legacy/hooks/useRecentActivity' -import { Order } from 'legacy/state/orders/actions' - -import { FollowPendingTxPopupUI } from './FollowPendingTxPopupUI' - -import { - followPendingTxPopupAtom, - handleCloseOrderPopupAtom, - handleFollowPendingTxPopupAtom, - handleHidePopupPermanentlyAtom, - showFollowTxPopupAtom, -} from '../../state/followPendingTxPopupAtom' - -export function useLastPendingOrder(): { lastPendingOrder: Order | null; onClose: Command } { - const setShowFollowPendingTxPopup = useSetAtom(handleFollowPendingTxPopupAtom) - const setLastOrderClosed = useSetAtom(handleCloseOrderPopupAtom) - const lastPendingOrder = useRecentActivityLastPendingOrder() - - const onClose = useCallback(() => { - lastPendingOrder && setLastOrderClosed(lastPendingOrder.id) - setShowFollowPendingTxPopup(false) - }, [lastPendingOrder, setLastOrderClosed, setShowFollowPendingTxPopup]) - - return useMemo(() => ({ lastPendingOrder, onClose }), [lastPendingOrder, onClose]) -} - -// Set pop up closed if it has not been closed and not fulfill a condition such as not pending tx -export function useCloseFollowTxPopupIfNotPendingOrder() { - const showingPopup = useAtomValue(showFollowTxPopupAtom) - const { lastPendingOrder, onClose } = useLastPendingOrder() - const onCloseRef = useRef() - - useEffect(() => { - if (lastPendingOrder && showingPopup) { - onCloseRef.current = onClose - } else if (!lastPendingOrder && showingPopup && onCloseRef.current) { - onCloseRef.current() - } - }, [lastPendingOrder, onClose, showingPopup]) -} - -const useShowingPopupFirstTime = (orderId: string | undefined) => { - const showingPopup = useAtomValue(showFollowTxPopupAtom) - const _firstTimePopupOrderAppears = useMemo( - () => - selectAtom(followPendingTxPopupAtom, ({ lastOrderPopupClosed }) => { - if (!orderId) return false - - return lastOrderPopupClosed !== orderId - }), - [orderId] - ) - - const firstTimePopupOrderAppears = useAtomValue(_firstTimePopupOrderAppears) - - return useMemo(() => ({ showPopup: firstTimePopupOrderAppears && showingPopup, firstTimePopupOrderAppears }), [firstTimePopupOrderAppears, showingPopup]) -} - -export const FollowPendingTxPopup: React.FC = ({ children }): JSX.Element => { - const setHidePendingTxPopupPermanently = useSetAtom(handleHidePopupPermanentlyAtom) - const { lastPendingOrder, onClose } = useLastPendingOrder() - const { showPopup: showFollowPendingTxPopup } = useShowingPopupFirstTime(lastPendingOrder?.id) - - return ( - setHidePendingTxPopupPermanently(true)} - onClose={onClose} - > - {children} - - ) -} diff --git a/apps/cowswap-frontend/src/modules/wallet/containers/Web3Status/index.tsx b/apps/cowswap-frontend/src/modules/wallet/containers/Web3Status/index.tsx index 59d8aa9b12..73df792457 100644 --- a/apps/cowswap-frontend/src/modules/wallet/containers/Web3Status/index.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/containers/Web3Status/index.tsx @@ -1,11 +1,10 @@ -import { useWalletDetails, useWalletInfo, useConnectionType } from '@cowprotocol/wallet' +import { useConnectionType, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { useToggleWalletModal } from 'legacy/state/application/hooks' import { Web3StatusInner } from '../../pure/Web3StatusInner' import { Wrapper } from '../../pure/Web3StatusInner/styled' import { AccountSelectorModal } from '../AccountSelectorModal' -import { useCloseFollowTxPopupIfNotPendingOrder } from '../FollowPendingTxPopup' import { WalletModal } from '../WalletModal' export interface Web3StatusProps { @@ -13,13 +12,13 @@ export interface Web3StatusProps { className?: string onClick?: () => void } + export function Web3Status({ pendingActivities, className, onClick }: Web3StatusProps) { const connectionType = useConnectionType() const { account } = useWalletInfo() const { ensName } = useWalletDetails() const toggleWalletModal = useToggleWalletModal() - useCloseFollowTxPopupIfNotPendingOrder() return ( diff --git a/apps/cowswap-frontend/src/modules/wallet/hooks/useSetShowFollowPendingTxPopup.ts b/apps/cowswap-frontend/src/modules/wallet/hooks/useSetShowFollowPendingTxPopup.ts deleted file mode 100644 index 575c233e82..0000000000 --- a/apps/cowswap-frontend/src/modules/wallet/hooks/useSetShowFollowPendingTxPopup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useSetAtom } from 'jotai/index' - -import { handleFollowPendingTxPopupAtom } from '../state/followPendingTxPopupAtom' - -export function useSetShowFollowPendingTxPopup() { - return useSetAtom(handleFollowPendingTxPopupAtom) -} diff --git a/apps/cowswap-frontend/src/modules/wallet/pure/Web3StatusInner/index.tsx b/apps/cowswap-frontend/src/modules/wallet/pure/Web3StatusInner/index.tsx index f68e801a1b..f73b7c5f94 100644 --- a/apps/cowswap-frontend/src/modules/wallet/pure/Web3StatusInner/index.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/pure/Web3StatusInner/index.tsx @@ -7,11 +7,10 @@ import { Trans } from '@lingui/macro' import ICON_WALLET from 'assets/icon/wallet.svg' import SVG from 'react-inlinesvg' -import { upToTiny, upToExtraSmall, useMediaQuery } from 'legacy/hooks/useMediaQuery' +import { upToExtraSmall, upToTiny, useMediaQuery } from 'legacy/hooks/useMediaQuery' import { Text, Web3StatusConnect, Web3StatusConnected } from './styled' -import { FollowPendingTxPopup } from '../../containers/FollowPendingTxPopup' import { StatusIcon } from '../StatusIcon' export interface Web3StatusInnerProps { @@ -34,11 +33,9 @@ export function Web3StatusInner(props: Web3StatusInnerProps) { {hasPendingTransactions ? ( - - - {pendingCount} Pending - {' '} - + + {pendingCount} Pending + {' '} ) : ( diff --git a/apps/cowswap-frontend/src/modules/wallet/state/followPendingTxPopupAtom.ts b/apps/cowswap-frontend/src/modules/wallet/state/followPendingTxPopupAtom.ts deleted file mode 100644 index 8920fd994f..0000000000 --- a/apps/cowswap-frontend/src/modules/wallet/state/followPendingTxPopupAtom.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { atom } from 'jotai' -import { atomWithStorage, selectAtom } from 'jotai/utils' - -type FollowPendingTxPopup = { - showPopup: boolean - lastOrderPopupClosed: string | undefined - hidePopupPermanently: boolean -} - -/** - * Base atom that store the popup state that indicate how to follow a pending tx - */ -export const followPendingTxPopupAtom = atomWithStorage('followPendingTxPopup', { - showPopup: false, - lastOrderPopupClosed: undefined, - hidePopupPermanently: false, -}) - -export const handleFollowPendingTxPopupAtom = atom(null, (_get, set, showPopup: boolean) => { - set(followPendingTxPopupAtom, (prev) => ({ ...prev, showPopup })) -}) - -export const handleHidePopupPermanentlyAtom = atom(null, (_get, set, hidePopupPermanently: boolean) => { - set(followPendingTxPopupAtom, (prev) => ({ ...prev, hidePopupPermanently })) -}) - -export const handleCloseOrderPopupAtom = atom(null, (_get, set, orderIdClosed: string) => { - set(followPendingTxPopupAtom, (prev) => ({ ...prev, lastOrderPopupClosed: orderIdClosed })) -}) - -export const showFollowTxPopupAtom = selectAtom( - followPendingTxPopupAtom, - ({ showPopup, hidePopupPermanently }) => showPopup && !hidePopupPermanently -) diff --git a/apps/explorer/.env.example b/apps/explorer/.env.example index e3ffb9a9c4..f24d674468 100644 --- a/apps/explorer/.env.example +++ b/apps/explorer/.env.example @@ -5,7 +5,7 @@ MOCK=true AUTOCONNECT=true # Public IPFS gateway -REACT_APP_IPFS_READ_URI=https://cloudflare-ipfs.com/ipfs +REACT_APP_IPFS_READ_URI=https://ipfs.io/ipfs # Sentry #REACT_APP_EXPLORER_SENTRY_DSN='https://' diff --git a/apps/explorer/src/const.ts b/apps/explorer/src/const.ts index 2d5c5b3ff7..9d9bf8d878 100644 --- a/apps/explorer/src/const.ts +++ b/apps/explorer/src/const.ts @@ -129,7 +129,7 @@ export const NATIVE_TOKEN_PER_NETWORK: Record = { } export const TENDERLY_API_URL = 'https://api.tenderly.co/api/v1/public-contract' -export const DEFAULT_IPFS_READ_URI = process.env.REACT_APP_IPFS_READ_URI || 'https://cloudflare-ipfs.com/ipfs' +export const DEFAULT_IPFS_READ_URI = process.env.REACT_APP_IPFS_READ_URI || 'https://ipfs.io/ipfs' export const IPFS_INVALID_APP_IDS = [ '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000001', diff --git a/apps/explorer/src/utils/format.ts b/apps/explorer/src/utils/format.ts index b7c3ea1ebf..78efe6cfef 100644 --- a/apps/explorer/src/utils/format.ts +++ b/apps/explorer/src/utils/format.ts @@ -245,7 +245,7 @@ export function formatCalculatedPriceToDisplay( const buySymbol = safeTokenName(buyToken) const sellSymbol = safeTokenName(sellToken) - const [quoteSymbol] = isPriceInverted ? [sellSymbol, buySymbol] : [buySymbol, sellSymbol] + const quoteSymbol = isPriceInverted ? buySymbol : sellSymbol return `${formattedPrice} ${quoteSymbol}` } diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts index af1d838865..b635a86844 100644 --- a/apps/widget-configurator/src/app/configurator/consts.ts +++ b/apps/widget-configurator/src/app/configurator/consts.ts @@ -1,5 +1,5 @@ import { CowEventListeners, CowEvents, ToastMessageType } from '@cowprotocol/events' -import { TradeType, TokenInfo, CowSwapWidgetPaletteParams } from '@cowprotocol/widget-lib' +import { CowSwapWidgetPaletteParams, TokenInfo, TradeType } from '@cowprotocol/widget-lib' import { TokenListItem } from './types' @@ -31,7 +31,7 @@ export const DEFAULT_TOKEN_LISTS: TokenListItem[] = [ { url: 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json', enabled: false }, { url: 'https://synths.snx.eth.link', enabled: false }, { url: 'https://testnet.tokenlist.eth.link', enabled: false }, - { url: 'https://gateway.ipfs.io/ipns/tokens.uniswap.org', enabled: false }, + { url: 'https://ipfs.io/ipns/tokens.uniswap.org', enabled: false }, { url: 'https://wrapped.tokensoft.eth.link', enabled: false }, ] // TODO: Move default palette to a new lib that only exposes the palette colors. diff --git a/libs/common-utils/src/environments.test.ts b/libs/common-utils/src/environments.test.ts index c61f7810b2..1e3bb07b8f 100644 --- a/libs/common-utils/src/environments.test.ts +++ b/libs/common-utils/src/environments.test.ts @@ -45,8 +45,8 @@ describe('Detect environments using host and path', () => { ).toEqual(isEns) }) - it('cloudflare-ipfs.com/ipfs/', () => { - expect(checkEnvironment('cloudflare-ipfs.com', '/ipfs/whatever')).toEqual(isEns) + it('ipfs.io/ipfs/', () => { + expect(checkEnvironment('ipfs.io', '/ipfs/whatever')).toEqual(isEns) }) it('gateway.pinata.cloud/ipfs/', () => { diff --git a/libs/common-utils/src/uriToHttp.test.ts b/libs/common-utils/src/uriToHttp.test.ts index 482fb4720a..2caa48622a 100644 --- a/libs/common-utils/src/uriToHttp.test.ts +++ b/libs/common-utils/src/uriToHttp.test.ts @@ -12,15 +12,11 @@ describe('uriToHttp', () => { }) it('returns ipfs gateways for ipfs:// urls', () => { expect(uriToHttp('ipfs://QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ')).toEqual([ - 'https://cloudflare-ipfs.com/ipfs/QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ/', 'https://ipfs.io/ipfs/QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ/', ]) }) it('returns ipns gateways for ipns:// urls', () => { - expect(uriToHttp('ipns://app.uniswap.org')).toEqual([ - 'https://cloudflare-ipfs.com/ipns/app.uniswap.org/', - 'https://ipfs.io/ipns/app.uniswap.org/', - ]) + expect(uriToHttp('ipns://app.uniswap.org')).toEqual(['https://ipfs.io/ipns/app.uniswap.org/']) }) it('returns empty array for invalid scheme', () => { expect(uriToHttp('blah:test')).toEqual([]) diff --git a/libs/common-utils/src/uriToHttp.ts b/libs/common-utils/src/uriToHttp.ts index 2edb001d35..3d6c5fd858 100644 --- a/libs/common-utils/src/uriToHttp.ts +++ b/libs/common-utils/src/uriToHttp.ts @@ -17,15 +17,12 @@ export function uriToHttp(uri: string): string[] { case 'http': return ['https' + uri.substr(4), uri] case 'ipfs': - // eslint-disable-next-line no-case-declarations const hash = uri.match(/^ipfs:(\/\/)?(ipfs\/)?(.*)$/i)?.[3] // TODO: probably a bug on original code - return [`https://cloudflare-ipfs.com/ipfs/${hash}/`, `https://ipfs.io/ipfs/${hash}/`] + return [`https://ipfs.io/ipfs/${hash}/`] case 'ipns': - // eslint-disable-next-line no-case-declarations const name = uri.match(/^ipns:(\/\/)?(.*)$/i)?.[2] - return [`https://cloudflare-ipfs.com/ipns/${name}/`, `https://ipfs.io/ipns/${name}/`] + return [`https://ipfs.io/ipns/${name}/`] case 'ar': - // eslint-disable-next-line no-case-declarations const tx = uri.match(/^ar:(\/\/)?(.*)$/i)?.[2] return [`https://arweave.net/${tx}`] default: diff --git a/libs/tokens/src/const/tokensLists.ts b/libs/tokens/src/const/tokensLists.ts index 35a7667728..d30a403e82 100644 --- a/libs/tokens/src/const/tokensLists.ts +++ b/libs/tokens/src/const/tokensLists.ts @@ -4,7 +4,7 @@ import { ListsSourcesByNetwork } from '../types' export const DEFAULT_TOKENS_LISTS: ListsSourcesByNetwork = tokensList -export const UNISWAP_TOKENS_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org' +export const UNISWAP_TOKENS_LIST = 'https://ipfs.io/ipns/tokens.uniswap.org' export const GNOSIS_UNISWAP_TOKENS_LIST = 'https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/GnosisUniswapTokensList.json' diff --git a/libs/tokens/src/hooks/tokens/useSearchToken.ts b/libs/tokens/src/hooks/tokens/useSearchToken.ts index 67fc126c35..fde44d8eac 100644 --- a/libs/tokens/src/hooks/tokens/useSearchToken.ts +++ b/libs/tokens/src/hooks/tokens/useSearchToken.ts @@ -159,7 +159,7 @@ function useSearchTokensInApi(input: string | undefined, isTokenAlreadyFoundByAd return null } - return searchTokensInApi(input).then((result) => parseTokensFromApi(result, chainId)) + return searchTokensInApi(chainId, input).then((result) => parseTokensFromApi(result, chainId)) }) } diff --git a/libs/tokens/src/services/searchTokensInApi.ts b/libs/tokens/src/services/searchTokensInApi.ts index f21e5707eb..9b49250caa 100644 --- a/libs/tokens/src/services/searchTokensInApi.ts +++ b/libs/tokens/src/services/searchTokensInApi.ts @@ -7,11 +7,6 @@ type Address = `0x${string}` type Chain = 'ARBITRUM' | 'ETHEREUM' | 'ETHEREUM_SEPOLIA' | 'OPTIMISM' | 'POLYGON' | 'CELO' | 'BNB' | 'UNKNOWN_CHAIN' -const CHAIN_TO_CHAIN_ID: { [key: string]: SupportedChainId } = { - ETHEREUM: SupportedChainId.MAINNET, - ETHEREUM_SEPOLIA: SupportedChainId.SEPOLIA, -} - interface FetchTokensResult { id: string decimals: number @@ -22,7 +17,12 @@ interface FetchTokensResult { symbol: string project: { id: string + name: string logoUrl: string + logo: { + id: string + url: string + } safetyLevel: string } } @@ -36,8 +36,9 @@ export interface TokenSearchFromApiResult extends FetchTokensResult { } const SEARCH_TOKENS = gql` - query SearchTokens($searchQuery: String!) { - searchTokens(searchQuery: $searchQuery) { + query SearchTokensWeb($searchQuery: String!, $chains: [Chain!]) { + searchTokens(searchQuery: $searchQuery, chains: $chains) { + ...SimpleTokenDetails id decimals name @@ -45,28 +46,110 @@ const SEARCH_TOKENS = gql` standard address symbol + market(currency: USD) { + id + price { + id + value + currency + __typename + } + pricePercentChange(duration: DAY) { + id + value + __typename + } + volume24H: volume(duration: DAY) { + id + value + currency + __typename + } + __typename + } project { id - logoUrl + name + logo { + id + url + __typename + } safetyLevel + logoUrl + isSpam + __typename + } + __typename + } + } + + fragment SimpleTokenDetails on Token { + id + address + chain + symbol + name + decimals + standard + project { + id + name + logo { + id + url __typename } + safetyLevel + logoUrl + isSpam __typename } + __typename } ` const BASE_URL = `${BFF_BASE_URL}/proxies/tokens` const GQL_CLIENT = new GraphQLClient(BASE_URL) -export async function searchTokensInApi(searchQuery: string): Promise { +const CHAIN_NAMES: Record = { + [SupportedChainId.MAINNET]: 'ETHEREUM', + [SupportedChainId.ARBITRUM_ONE]: 'ARBITRUM', + [SupportedChainId.SEPOLIA]: null, + [SupportedChainId.GNOSIS_CHAIN]: null, +} + +const CHAIN_IDS = Object.entries(CHAIN_NAMES).reduce((acc, [supportedChainId, chain]) => { + if (chain) { + acc[chain] = parseInt(supportedChainId) + } + + return acc +}, {} as Record) + +export async function searchTokensInApi( + chainId: SupportedChainId, + searchQuery: string +): Promise { + const chain = CHAIN_NAMES[chainId] + + if (!chain) { + return [] + } + return await GQL_CLIENT.request(SEARCH_TOKENS, { searchQuery, + chains: [chain], }).then((result) => { if (!result?.searchTokens?.length) { return [] } - return result.searchTokens.map((token) => ({ ...token, chainId: CHAIN_TO_CHAIN_ID[token.chain] })) + return result.searchTokens.map((token) => { + return { + ...token, + chainId: CHAIN_IDS[token.chain], + } + }) }) } diff --git a/libs/ui/src/pure/MenuBar/styled.ts b/libs/ui/src/pure/MenuBar/styled.ts index b704a6c948..01afaea824 100644 --- a/libs/ui/src/pure/MenuBar/styled.ts +++ b/libs/ui/src/pure/MenuBar/styled.ts @@ -47,7 +47,7 @@ export const MenuBarWrapper = styled.div<{ display: flex; width: 100%; padding: ${({ padding }) => padding || '10px'}; - z-index: 10; + z-index: 9; position: sticky; top: 0; color: var(--color); diff --git a/libs/ui/src/pure/Popover/index.tsx b/libs/ui/src/pure/Popover/index.tsx index 4511374ab1..b0a9e1f07a 100644 --- a/libs/ui/src/pure/Popover/index.tsx +++ b/libs/ui/src/pure/Popover/index.tsx @@ -56,7 +56,10 @@ export default function Popover(props: PopoverProps) { className={className} show={show} ref={setPopperElement as any} - style={styles.popper} + style={{ + ...styles.popper, + zIndex: 999999, + }} {...attributes.popper} bgColor={bgColor} color={color}