diff --git a/apps/web/src/ui/pool/AddSectionReviewModalConcentrated.tsx b/apps/web/src/ui/pool/AddSectionReviewModalConcentrated.tsx
index 0613704b81..3c8ad1640c 100644
--- a/apps/web/src/ui/pool/AddSectionReviewModalConcentrated.tsx
+++ b/apps/web/src/ui/pool/AddSectionReviewModalConcentrated.tsx
@@ -33,7 +33,7 @@ import {
getDefaultTTL,
useTransactionDeadline,
} from 'src/lib/wagmi/hooks/utils/hooks/useTransactionDeadline'
-import { Chain, ChainId } from 'sushi/chain'
+import { EvmChain, EvmChainId } from 'sushi/chain'
import {
SUSHISWAP_V3_POSTIION_MANAGER,
SushiSwapV3FeeAmount,
@@ -58,7 +58,7 @@ interface AddSectionReviewModalConcentratedProps
ReturnType
,
'noLiquidity' | 'position' | 'price' | 'pricesAtTicks' | 'ticksAtLimit'
> {
- chainId: ChainId
+ chainId: EvmChainId
feeAmount: SushiSwapV3FeeAmount | undefined
token0: Type | undefined
token1: Type | undefined
@@ -341,7 +341,7 @@ export const AddSectionReviewModalConcentrated: FC<
- {Chain.from(chainId)?.name}
+ {EvmChain.from(chainId)?.name}
{feeAmount && (
{`${
diff --git a/apps/web/src/ui/pool/AddSectionWidget.tsx b/apps/web/src/ui/pool/AddSectionWidget.tsx
index 2dee1829f5..68fdb44a62 100644
--- a/apps/web/src/ui/pool/AddSectionWidget.tsx
+++ b/apps/web/src/ui/pool/AddSectionWidget.tsx
@@ -14,13 +14,13 @@ import React, { FC, ReactNode } from 'react'
import { isZapSupportedChainId } from 'src/config'
import { Web3Input } from 'src/lib/wagmi/components/web3-input'
import { getDefaultTTL } from 'src/lib/wagmi/hooks/utils/hooks/useTransactionDeadline'
-import { ChainId } from 'sushi/chain'
+import { EvmChainId } from 'sushi/chain'
import { Type } from 'sushi/currency'
import { ToggleZapCard } from './ToggleZapCard'
interface AddSectionWidgetProps {
isFarm: boolean
- chainId: ChainId
+ chainId: EvmChainId
input0: string
input1: string
token0: Type | undefined
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityCollectAllWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityCollectAllWidget.tsx
new file mode 100644
index 0000000000..825abe454d
--- /dev/null
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityCollectAllWidget.tsx
@@ -0,0 +1,346 @@
+'use client'
+
+import { createErrorToast, createToast } from '@sushiswap/notifications'
+import {
+ DialogConfirm,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogProvider,
+ DialogReview,
+ DialogTitle,
+ DialogTrigger,
+ Dots,
+ Switch,
+} from '@sushiswap/ui'
+import { Button } from '@sushiswap/ui'
+import { Currency } from '@sushiswap/ui'
+import React, { FC, useCallback, useMemo, useState } from 'react'
+import { ConcentratedLiquidityPositionWithV3Pool } from 'src/lib/wagmi/hooks/positions/types'
+import { Checker } from 'src/lib/wagmi/systems/Checker'
+import { EvmChain, EvmChainId } from 'sushi/chain'
+import {
+ SUSHISWAP_V3_POSTIION_MANAGER,
+ isSushiSwapV3ChainId,
+} from 'sushi/config'
+import { Amount, Native, Type, unwrapToken } from 'sushi/currency'
+import { NonfungiblePositionManager } from 'sushi/pool/sushiswap-v3'
+import { Hex, SendTransactionReturnType, UserRejectedRequestError } from 'viem'
+import {
+ useCall,
+ useSendTransaction,
+ useWaitForTransactionReceipt,
+} from 'wagmi'
+import { useAccount } from 'wagmi'
+import { usePublicClient } from 'wagmi'
+import { useRefetchBalances } from '~evm/_common/ui/balance-provider/use-refetch-balances'
+import { useTokenAmountDollarValues } from '../../lib/hooks'
+
+interface ConcentratedLiquidityCollectAllWidget {
+ positions: ConcentratedLiquidityPositionWithV3Pool[]
+ chainId: EvmChainId
+ account: `0x${string}` | undefined
+}
+
+export const ConcentratedLiquidityCollectAllWidget: FC<
+ ConcentratedLiquidityCollectAllWidget
+> = ({ positions, chainId, account }) => {
+ const { chain } = useAccount()
+ const client = usePublicClient()
+ const { refetchChain: refetchBalances } = useRefetchBalances()
+ const [receiveWrapped, setReceiveWrapped] = useState(false)
+
+ const nativeToken = useMemo(() => Native.onChain(chainId), [chainId])
+
+ const hasNativeToken = useMemo(() => {
+ return positions.some(({ pool: { token0, token1 } }) => {
+ if (!token0 || !token1 || !nativeToken) return false
+ return (
+ token0.isNative ||
+ token1.isNative ||
+ token0.address === nativeToken.wrapped?.address ||
+ token1.address === nativeToken.wrapped?.address
+ )
+ })
+ }, [positions, nativeToken])
+
+ const positionsToCollect = useMemo(() => {
+ return positions.flatMap((position) => {
+ const { token0, token1 } = position.pool
+ if (!token0 || !token1 || !position?.fees || !account) return []
+
+ const expectedToken0 = receiveWrapped
+ ? token0.wrapped
+ : unwrapToken(token0)
+ const expectedToken1 = receiveWrapped
+ ? token1.wrapped
+ : unwrapToken(token1)
+
+ const feeValue0 = Amount.fromRawAmount(expectedToken0, position.fees[0])
+ const feeValue1 = Amount.fromRawAmount(expectedToken1, position.fees[1])
+
+ if (feeValue0.equalTo(0n) && feeValue1.equalTo(0n)) return []
+
+ return [
+ {
+ tokenId: position.tokenId.toString(),
+ expectedCurrencyOwed0: feeValue0,
+ expectedCurrencyOwed1: feeValue1,
+ recipient: account,
+ },
+ ]
+ })
+ }, [positions, receiveWrapped, account])
+
+ const aggregatedAmounts = useMemo(() => {
+ const aggregatedAmounts = new Map>()
+
+ positionsToCollect.forEach((position) => {
+ const { expectedCurrencyOwed0, expectedCurrencyOwed1 } = position
+
+ if (expectedCurrencyOwed0.greaterThan(0n)) {
+ const existing = aggregatedAmounts.get(
+ expectedCurrencyOwed0.currency.id,
+ )
+ aggregatedAmounts.set(
+ expectedCurrencyOwed0.currency.id,
+ existing
+ ? existing.add(expectedCurrencyOwed0)
+ : expectedCurrencyOwed0,
+ )
+ }
+
+ if (expectedCurrencyOwed1.greaterThan(0n)) {
+ const existing = aggregatedAmounts.get(
+ expectedCurrencyOwed1.currency.id,
+ )
+ aggregatedAmounts.set(
+ expectedCurrencyOwed1.currency.id,
+ existing
+ ? existing.add(expectedCurrencyOwed1)
+ : expectedCurrencyOwed1,
+ )
+ }
+ })
+
+ return Array.from(aggregatedAmounts.values())
+ }, [positionsToCollect])
+
+ const feeValues = useTokenAmountDollarValues({
+ chainId,
+ amounts: aggregatedAmounts,
+ })
+
+ const totalFeeValue = useMemo(() => {
+ return feeValues.reduce((sum, value) => sum + value, 0)
+ }, [feeValues])
+
+ const prepare = useMemo(() => {
+ if (!isSushiSwapV3ChainId(chainId)) return
+
+ if (positionsToCollect.length === 0) return
+
+ const { calldata, value } =
+ NonfungiblePositionManager.collectCallParameters(positionsToCollect)
+
+ return {
+ to: SUSHISWAP_V3_POSTIION_MANAGER[chainId],
+ data: calldata as Hex,
+ value: BigInt(value),
+ chainId,
+ }
+ }, [positionsToCollect, chainId])
+
+ const onSuccess = useCallback(
+ (hash: SendTransactionReturnType) => {
+ const receipt = client.waitForTransactionReceipt({ hash })
+ receipt.then(() => {
+ refetchBalances(chainId)
+ })
+
+ const ts = new Date().getTime()
+ void createToast({
+ account,
+ type: 'claimRewards',
+ chainId,
+ txHash: hash,
+ promise: receipt,
+ summary: {
+ pending: 'Collecting fees from all your pool positions',
+ completed: 'Successfully collected fees from all your pool positions',
+ failed: 'Something went wrong when trying to collect fees',
+ },
+ timestamp: ts,
+ groupTimestamp: ts,
+ })
+ },
+ [account, chainId, client, refetchBalances],
+ )
+
+ const onError = useCallback((e: Error) => {
+ if (!(e.cause instanceof UserRejectedRequestError)) {
+ createErrorToast(e?.message, true)
+ }
+ }, [])
+
+ const { isError: isSimulationError } = useCall({
+ ...prepare,
+ chainId,
+ query: {
+ enabled: Boolean(prepare && chainId === chain?.id),
+ },
+ })
+
+ const {
+ sendTransactionAsync,
+ isPending: isWritePending,
+ data: hash,
+ } = useSendTransaction({
+ mutation: {
+ onSuccess,
+ onError,
+ },
+ })
+
+ const { status } = useWaitForTransactionReceipt({
+ chainId,
+ hash,
+ })
+
+ const send = useMemo(() => {
+ if (!prepare || isSimulationError) return undefined
+
+ return async (confirm: () => void) => {
+ try {
+ await sendTransactionAsync(prepare)
+ confirm()
+ } catch {}
+ }
+ }, [isSimulationError, prepare, sendTransactionAsync])
+
+ return (
+
+
+ {({ confirm }) => (
+ <>
+ {aggregatedAmounts.length > 0 ? (
+
+
+
+
+
+
+
+
+
+ ) : null}
+
+
+
+ Claim V3 Fees
+
+ On {EvmChain.from(chainId)?.name}
+
+
+
+
+
+ Total Value
+
+
+ ${totalFeeValue.toFixed(2)}
+
+
+
+
+ You'll receive collected fees:
+
+
+ {aggregatedAmounts.map((amount, i) => (
+
+
+
+
+ {amount.currency.symbol}
+
+
+
+
+ {amount.toSignificant(6)}
+
+
+ ${feeValues[i].toFixed(1)}
+
+
+
+ ))}
+
+
+ {hasNativeToken && (
+
+
+ Receive {nativeToken?.wrapped.symbol} instead of{' '}
+ {nativeToken?.symbol}
+
+
+
+ )}
+
+
+
+
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx
index 9674c15112..dc342d6cc7 100644
--- a/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx
@@ -8,7 +8,7 @@ import {
} from '@sushiswap/telemetry'
import { FC, ReactElement, useCallback, useMemo } from 'react'
import { ConcentratedLiquidityPosition } from 'src/lib/wagmi/hooks/positions/types'
-import { ChainId } from 'sushi/chain'
+import { EvmChainId } from 'sushi/chain'
import {
SUSHISWAP_V3_POSTIION_MANAGER,
isSushiSwapV3ChainId,
@@ -31,7 +31,7 @@ interface ConcentratedLiquidityCollectButton {
token0: Type | undefined
token1: Type | undefined
account: `0x${string}` | undefined
- chainId: ChainId
+ chainId: EvmChainId
children(
params: Omit<
ReturnType,
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx
index 3b207f9c4e..089e021b47 100644
--- a/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx
@@ -12,7 +12,7 @@ import { Button } from '@sushiswap/ui'
import { FC, useMemo, useState } from 'react'
import { ConcentratedLiquidityPosition } from 'src/lib/wagmi/hooks/positions/types'
import { Checker } from 'src/lib/wagmi/systems/Checker'
-import { Address, ChainId, Position } from 'sushi'
+import { Address, EvmChainId, Position } from 'sushi'
import { Amount, Native, Type, unwrapToken } from 'sushi/currency'
import { formatUSD } from 'sushi/format'
import { ConcentratedLiquidityCollectButton } from './ConcentratedLiquidityCollectButton'
@@ -22,7 +22,7 @@ interface ConcentratedLiquidityCollectWidget {
positionDetails: ConcentratedLiquidityPosition | undefined
token0: Type | undefined
token1: Type | undefined
- chainId: ChainId
+ chainId: EvmChainId
isLoading: boolean
address: Address | undefined
amounts: undefined[] | Amount[]
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityHarvestButton.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityHarvestButton.tsx
index 5991ebbeb6..4bf8b395f8 100644
--- a/apps/web/src/ui/pool/ConcentratedLiquidityHarvestButton.tsx
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityHarvestButton.tsx
@@ -3,13 +3,13 @@
import { FC, ReactElement, useMemo } from 'react'
import { useAngleRewards } from 'src/lib/hooks/react-query'
import { useHarvestAngleRewards } from 'src/lib/wagmi/hooks/rewards/hooks/useHarvestAngleRewards'
-import { ChainId } from 'sushi/chain'
+import { EvmChainId } from 'sushi/chain'
import { Address } from 'viem'
interface ConcentratedLiquidityHarvestButton {
account: Address | undefined
enabled?: boolean
- chainId: ChainId
+ chainId: EvmChainId
children(params: ReturnType): ReactElement
}
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx
index dc1bd826eb..21dddbc43b 100644
--- a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx
@@ -47,7 +47,7 @@ import {
useTransactionDeadline,
} from 'src/lib/wagmi/hooks/utils/hooks/useTransactionDeadline'
import { Checker } from 'src/lib/wagmi/systems/Checker'
-import { Chain } from 'sushi/chain'
+import { EvmChain } from 'sushi/chain'
import {
SUSHISWAP_V3_POSTIION_MANAGER,
SushiSwapV3ChainId,
@@ -513,7 +513,7 @@ export const ConcentratedLiquidityRemoveWidget: FC<
- {Chain.from(chainId)?.name}
+ {EvmChain.from(chainId)?.name}
{slippageTolerance?.toSignificant(2)}%
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityURLStateProvider.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityURLStateProvider.tsx
index ddd8371b72..1ca8dd2c79 100644
--- a/apps/web/src/ui/pool/ConcentratedLiquidityURLStateProvider.tsx
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityURLStateProvider.tsx
@@ -3,7 +3,7 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'
import { useTokenWithCache } from 'src/lib/wagmi/hooks/tokens/useTokenWithCache'
-import { ChainId } from 'sushi/chain'
+import { EvmChainId } from 'sushi/chain'
import {
SUSHISWAP_V3_SUPPORTED_CHAIN_IDS,
SushiSwapV3ChainId,
@@ -52,11 +52,11 @@ export const ConcentratedLiquidityUrlStateContext = createContext(
interface ConcentratedLiquidityURLStateProvider {
children: ReactNode | ((state: State) => ReactNode)
chainId: SushiSwapV3ChainId
- supportedNetworks?: ChainId[]
+ supportedNetworks?: EvmChainId[]
}
const getTokenFromUrl = (
- chainId: ChainId,
+ chainId: EvmChainId,
currencyId: string | undefined | null,
token: Token | undefined,
isLoading: boolean,
@@ -70,7 +70,7 @@ const getTokenFromUrl = (
} else if (!currencyId || !isWNativeSupported(chainId)) {
return undefined
} else {
- return Native.onChain(chainId ? chainId : ChainId.ETHEREUM)
+ return Native.onChain(chainId ? chainId : EvmChainId.ETHEREUM)
}
}
@@ -95,7 +95,7 @@ export const ConcentratedLiquidityURLStateProvider: FC<
const _chainId = supportedNetworks?.includes(chainId)
? chainId
- : ChainId.ETHEREUM
+ : EvmChainId.ETHEREUM
const { data: tokenFrom, isInitialLoading: isTokenFromLoading } =
useTokenWithCache({
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityWidget.tsx
index d6467a3acc..669c323024 100644
--- a/apps/web/src/ui/pool/ConcentratedLiquidityWidget.tsx
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityWidget.tsx
@@ -5,7 +5,6 @@ import { LockClosedIcon, PlusIcon } from '@heroicons/react-v1/solid'
import { DialogTrigger, FormSection, Message, classNames } from '@sushiswap/ui'
import { Button } from '@sushiswap/ui'
import { FC, Fragment, useCallback, useMemo } from 'react'
-import { ChainId } from 'sushi/chain'
import {
SUSHISWAP_V3_POSTIION_MANAGER,
SushiSwapV3ChainId,
@@ -272,7 +271,7 @@ export const ConcentratedLiquidityWidget: FC = ({
enabled={!depositBDisabled}
>
=
hideNewSmartPositionButton = true,
hideNewPositionButton = false,
hideClosedPositions = true,
+ hideCollectAllButton = false,
actions,
}) => {
const { address } = useAccount()
@@ -125,11 +128,17 @@ export const ConcentratedPositionsTable: FC =
({_positions.length})
- {actions}
+ {!hideCollectAllButton ? (
+
+ ) : null}
{!hideNewSmartPositionButton ? (
@@ -161,7 +171,7 @@ export const ConcentratedPositionsTable: FC