Skip to content

Commit

Permalink
Merge pull request #5068 from cowprotocol/release/2024-11-05
Browse files Browse the repository at this point in the history
chore(release): 2024 11 05
  • Loading branch information
alfetopito authored Nov 6, 2024
2 parents 739b7db + c43d79f commit 0893b55
Show file tree
Hide file tree
Showing 154 changed files with 4,015 additions and 1,509 deletions.
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ export const YIELD_MENU_ITEM = {
label: 'Yield',
fullLabel: 'Yield',
description: 'Provide liquidity',
badge: 'New',
}
98 changes: 98 additions & 0 deletions apps/cowswap-frontend/src/common/containers/CoWAmmBanner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 { 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'
import { CoWAmmBannerContent } from '../../pure/CoWAmmBannerContent'

interface BannerProps {
isTokenSelectorView?: boolean
}

export function CoWAmmBanner({ isTokenSelectorView }: BannerProps) {
const isDarkMode = useIsDarkMode()
const isInjectedWidgetMode = isInjectedWidget()
const { chainId, account } = useWalletInfo()
const isChainIdUnsupported = useIsProviderNetworkUnsupported()
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 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
? 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`,
})

tradeNavigate(chainId, targetTrade, targetTradeParams, Routes.YIELD)
}, [key, chainId, yieldState, vampireAttackFirstTarget, tradeNavigate])

const handleClose = useCallback(() => {
cowAnalytics.sendEvent({
category: 'CoW Swap',
action: `CoW AMM Banner [${key}] Closed`,
})
}, [key])

if (isInjectedWidgetMode || !account || isChainIdUnsupported || !vampireAttackContext) return null

const bannerId = `cow_amm_banner_2024_va_${key}${isTokenSelectorView ? account : ''}`

return ClosableBanner(bannerId, (close) => (
<CoWAmmBannerContent
id={bannerId}
isDarkMode={isDarkMode}
title="CoW AMM"
ctaText={isSmartContractWallet ? 'Booooost APR!' : 'Booooost APR gas-free!'}
isTokenSelectorView={!!isTokenSelectorView}
vampireAttackContext={vampireAttackContext}
tokensByAddress={tokensByAddress}
onCtaClick={() => {
handleCTAClick()
close()
}}
onClose={() => {
handleClose()
close()
}}
/>
))
}
24 changes: 24 additions & 0 deletions apps/cowswap-frontend/src/common/containers/CoWAmmBanner/types.ts
Original file line number Diff line number Diff line change
@@ -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<Record<LpTokenProvider, { apy: number }> | undefined>
averageApyDiff: number | undefined
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -87,7 +87,7 @@ export function useOrderProgressBarV2Props(chainId: SupportedChainId, order: Ord
}

function useOrderBaseProgressBarV2Props(
params: UseOrderProgressBarPropsParams
params: UseOrderProgressBarPropsParams,
): UseOrderProgressBarV2Result | undefined {
const { activityDerivedState, chainId } = params

Expand Down Expand Up @@ -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)
Expand All @@ -145,7 +145,7 @@ function useOrderBaseProgressBarV2Props(
backendApiStatus,
previousBackendApiStatus,
lastTimeChangedSteps,
previousStepName
previousStepName,
)
useCancellingOrderUpdater(orderId, isCancelling)
useCountdownStartUpdater(orderId, countdown, backendApiStatus)
Expand All @@ -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(() => {
Expand Down Expand Up @@ -184,7 +184,7 @@ function useOrderBaseProgressBarV2Props(
function getDoNotQueryStatusEndpoint(
order: Order | undefined,
apiSolverCompetition: CompetitionOrderStatus['value'] | undefined,
disableProgressBar: boolean
disableProgressBar: boolean,
) {
return (
!!(
Expand Down Expand Up @@ -224,7 +224,7 @@ function useSetExecutingOrderProgressBarStepNameCallback() {
function useCountdownStartUpdater(
orderId: string,
countdown: OrderProgressBarState['countdown'],
backendApiStatus: OrderProgressBarState['backendApiStatus']
backendApiStatus: OrderProgressBarState['backendApiStatus'],
) {
const setCountdown = useSetExecutingOrderCountdownCallback()

Expand Down Expand Up @@ -259,7 +259,7 @@ function useProgressBarStepNameUpdater(
backendApiStatus: OrderProgressBarState['backendApiStatus'],
previousBackendApiStatus: OrderProgressBarState['previousBackendApiStatus'],
lastTimeChangedSteps: OrderProgressBarState['lastTimeChangedSteps'],
previousStepName: OrderProgressBarState['previousStepName']
previousStepName: OrderProgressBarState['previousStepName'],
) {
const setProgressBarStepName = useSetExecutingOrderProgressBarStepNameCallback()

Expand All @@ -273,7 +273,7 @@ function useProgressBarStepNameUpdater(
countdown,
backendApiStatus,
previousBackendApiStatus,
previousStepName
previousStepName,
)

// Update state with new step name
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
}

Expand All @@ -404,7 +404,7 @@ function usePendingOrderStatus(chainId: SupportedChainId, orderId: string, doNot
*/
function mergeSolverData(
solverCompetition: ApiSolverCompetition,
solversInfo: Record<string, SolverInfo>
solversInfo: Record<string, SolverInfo>,
): SolverCompetition {
// Backend has the prefix `-solve` on some solvers. We should discard that for now.
// In the future this prefix will be removed.
Expand Down
Loading

0 comments on commit 0893b55

Please sign in to comment.