diff --git a/apps/frontend-v3/app/(app)/pools/[chain]/[variant]/[id]/stake/page.tsx b/apps/frontend-v3/app/(app)/pools/[chain]/[variant]/[id]/stake/page.tsx
index 29cc7053..cbb41e24 100644
--- a/apps/frontend-v3/app/(app)/pools/[chain]/[variant]/[id]/stake/page.tsx
+++ b/apps/frontend-v3/app/(app)/pools/[chain]/[variant]/[id]/stake/page.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { PoolActionsLayout } from '@repo/lib/modules/pool/actions/PoolActionsLayout'
diff --git a/apps/frontend-v3/app/(app)/pools/[chain]/[variant]/[id]/swap/[[...txHash]]/page.tsx b/apps/frontend-v3/app/(app)/pools/[chain]/[variant]/[id]/swap/[[...txHash]]/page.tsx
new file mode 100644
index 00000000..40530854
--- /dev/null
+++ b/apps/frontend-v3/app/(app)/pools/[chain]/[variant]/[id]/swap/[[...txHash]]/page.tsx
@@ -0,0 +1,44 @@
+'use client'
+
+import { PoolActionsLayout } from '@repo/lib/modules/pool/actions/PoolActionsLayout'
+import { getPoolTokens } from '@repo/lib/modules/pool/pool.helpers'
+import { usePoolRedirect } from '@repo/lib/modules/pool/pool.hooks'
+import { chainToSlugMap } from '@repo/lib/modules/pool/pool.utils'
+import { usePool } from '@repo/lib/modules/pool/PoolProvider'
+import { SwapForm } from '@repo/lib/modules/swap/SwapForm'
+import SwapLayout from '@repo/lib/modules/swap/SwapLayout'
+import { PathParams } from '@repo/lib/modules/swap/SwapProvider'
+import { useTokens } from '@repo/lib/modules/tokens/TokensProvider'
+import { Hash } from 'viem'
+
+type Props = {
+ params: { txHash?: string[] }
+}
+// Page for swapping from a pool page
+export default function PoolSwapPage({ params: { txHash } }: Props) {
+ const { getToken } = useTokens()
+ const { pool, isLoading } = usePool()
+ const { redirectToPoolPage } = usePoolRedirect(pool)
+
+ const poolTokens = getPoolTokens(pool, getToken)
+
+ const maybeTxHash = (txHash?.[0] as Hash) || undefined
+
+ const pathParams: PathParams = {
+ chain: chainToSlugMap[pool.chain],
+ tokenIn: poolTokens[0].address,
+ tokenOut: poolTokens[1].address,
+ poolTokens,
+ urlTxHash: maybeTxHash,
+ }
+
+ return (
+
+ {isLoading ? null : (
+
+
+
+ )}
+
+ )
+}
diff --git a/apps/frontend-v3/app/(app)/swap/[[...slug]]/layout.tsx b/apps/frontend-v3/app/(app)/swap/[[...slug]]/layout.tsx
index 7f4cf9ff..fe321954 100644
--- a/apps/frontend-v3/app/(app)/swap/[[...slug]]/layout.tsx
+++ b/apps/frontend-v3/app/(app)/swap/[[...slug]]/layout.tsx
@@ -1,44 +1,20 @@
'use client'
-import { ChainSlug, slugToChainMap } from '@repo/lib/modules/pool/pool.utils'
-import { SwapProvider } from '@repo/lib/modules/swap/SwapProvider'
-import { TokenBalancesProvider } from '@repo/lib/modules/tokens/TokenBalancesProvider'
-import { TokenInputsValidationProvider } from '@repo/lib/modules/tokens/TokenInputsValidationProvider'
-import { useTokens } from '@repo/lib/modules/tokens/TokensProvider'
-import { TransactionStateProvider } from '@repo/lib/modules/transactions/transaction-steps/TransactionStateProvider'
-import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { PropsWithChildren } from 'react'
-import { PriceImpactProvider } from '@repo/lib/modules/price-impact/PriceImpactProvider'
-import { DefaultPageContainer } from '@repo/lib/shared/components/containers/DefaultPageContainer'
import { getSwapPathParams } from '@repo/lib/modules/swap/getSwapPathParams'
-import { RelayerSignatureProvider } from '@repo/lib/modules/relayer/RelayerSignatureProvider'
+import SwapLayout from '../../../../../../packages/lib/modules/swap/SwapLayout'
+import { DefaultPageContainer } from '@repo/lib/shared/components/containers/DefaultPageContainer'
type Props = PropsWithChildren<{
params: { slug?: string[] }
}>
-export default function SwapLayout({ params: { slug }, children }: Props) {
+export default function Layout({ params: { slug }, children }: Props) {
const pathParams = getSwapPathParams(slug)
- const { getTokensByChain } = useTokens()
- const initChain = pathParams.chain
- ? slugToChainMap[pathParams.chain as ChainSlug]
- : GqlChain.Mainnet
- const initTokens = getTokensByChain(initChain)
-
return (
-
-
-
-
-
- {children}
-
-
-
-
-
+ {children}
)
}
diff --git a/packages/lib/modules/pool/PoolDetail/PoolHeader/PoolAdvancedOptions.tsx b/packages/lib/modules/pool/PoolDetail/PoolHeader/PoolAdvancedOptions.tsx
new file mode 100644
index 00000000..adb92fd3
--- /dev/null
+++ b/packages/lib/modules/pool/PoolDetail/PoolHeader/PoolAdvancedOptions.tsx
@@ -0,0 +1,70 @@
+'use client'
+
+import {
+ Box,
+ Button,
+ HStack,
+ Link,
+ Popover,
+ PopoverArrow,
+ PopoverBody,
+ PopoverCloseButton,
+ PopoverContent,
+ PopoverTrigger,
+ VStack,
+} from '@chakra-ui/react'
+import { SwapIcon } from '@repo/lib/shared/components/icons/SwapIcon'
+import { staggeredFadeInUp } from '@repo/lib/shared/utils/animations'
+import { AnimatePresence, motion } from 'framer-motion'
+import NextLink from 'next/link'
+import { usePathname } from 'next/navigation'
+import { useState } from 'react'
+import { MoreVertical } from 'react-feather'
+
+export function PoolAdvancedOptions() {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false)
+ const pathname = usePathname()
+
+ return (
+ setIsPopoverOpen(true)}
+ onClose={() => setIsPopoverOpen(false)}
+ placement="bottom-end"
+ >
+
+
+
+
+
+
+
+
+
+ {isPopoverOpen && (
+
+
+
+
+ Swap tokens directly via this pool
+
+
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/packages/lib/modules/pool/PoolDetail/PoolHeader/PoolHeader.tsx b/packages/lib/modules/pool/PoolDetail/PoolHeader/PoolHeader.tsx
index 37c7571a..18369650 100644
--- a/packages/lib/modules/pool/PoolDetail/PoolHeader/PoolHeader.tsx
+++ b/packages/lib/modules/pool/PoolDetail/PoolHeader/PoolHeader.tsx
@@ -1,4 +1,4 @@
-import { Stack, Button, VStack, useDisclosure } from '@chakra-ui/react'
+import { Stack, Button, VStack, useDisclosure, HStack } from '@chakra-ui/react'
import { usePathname, useRouter } from 'next/navigation'
import PoolMetaBadges from './PoolMetaBadges'
@@ -13,6 +13,7 @@ import {
} from '@repo/lib/shared/components/modals/PartnerRedirectModal'
import { useState } from 'react'
import { getXavePoolLink } from '../../pool.utils'
+import { PoolAdvancedOptions } from './PoolAdvancedOptions'
export function PoolHeader() {
const pathname = usePathname()
@@ -55,14 +56,18 @@ export function PoolHeader() {
-
+
+
+
+
+
+
+ }
+ />
+
+
+
+ {pool.name} (swap route)
+
+
+
+ )
+}
diff --git a/packages/lib/modules/swap/SwapForm.tsx b/packages/lib/modules/swap/SwapForm.tsx
index 4a0d526f..bb4372fd 100644
--- a/packages/lib/modules/swap/SwapForm.tsx
+++ b/packages/lib/modules/swap/SwapForm.tsx
@@ -38,8 +38,14 @@ import { useUserAccount } from '../web3/UserAccountProvider'
import { ConnectWallet } from '../web3/ConnectWallet'
import { SafeAppAlert } from '@repo/lib/shared/components/alerts/SafeAppAlert'
import { useTokens } from '../tokens/TokensProvider'
+import { useIsPoolSwap } from './useIsPoolSwap'
+import { CompactTokenSelectModal } from '../tokens/TokenSelectModal/TokenSelectList/CompactTokenSelectModal'
+import { PoolSwapCard } from './PoolSwapCard'
-export function SwapForm() {
+type Props = {
+ redirectToPoolPage?: () => void // Only used for pool swaps
+}
+export function SwapForm({ redirectToPoolPage }: Props) {
const {
tokenIn,
tokenOut,
@@ -72,6 +78,7 @@ export function SwapForm() {
const isMounted = useIsMounted()
const { isConnected } = useUserAccount()
const { startTokenPricePolling } = useTokens()
+ const isPoolSwap = useIsPoolSwap()
const isLoadingSwaps = simulationQuery.isLoading
const isLoading = isLoadingSwaps || !isMounted
@@ -107,8 +114,9 @@ export function SwapForm() {
if (swapTxHash) {
resetSwapAmounts()
- replaceUrlPath()
transactionSteps.resetTransactionSteps()
+ if (isPoolSwap) return redirectToPoolPage?.()
+ replaceUrlPath()
}
}
@@ -124,7 +132,7 @@ export function SwapForm() {
>
- {capitalize(swapAction)}
+ {isPoolSwap ? 'Single pool swap' : capitalize(swapAction)}
+ {isPoolSwap && }
- {
- setSelectedChain(newValue as GqlChain)
- setTokenInAmount('')
- }}
- />
+ {!isPoolSwap && (
+ {
+ setSelectedChain(newValue as GqlChain)
+ setTokenInAmount('')
+ }}
+ />
+ )}
-
+ {isPoolSwap ? (
+
+ ) : (
+
+ )}
+
+
+// Layout shared by standard swap page (/swap) and pool swap page (/poolid/swap)
+export default function SwapLayout({ pathParams, children }: Props) {
+ const { getTokensByChain } = useTokens()
+ const initChain = pathParams.chain
+ ? slugToChainMap[pathParams.chain as ChainSlug]
+ : GqlChain.Mainnet
+ const initTokens = pathParams.poolTokens || getTokensByChain(initChain)
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ )
+}
diff --git a/packages/lib/modules/swap/SwapProvider.tsx b/packages/lib/modules/swap/SwapProvider.tsx
index df1f0c6d..fdea9b79 100644
--- a/packages/lib/modules/swap/SwapProvider.tsx
+++ b/packages/lib/modules/swap/SwapProvider.tsx
@@ -48,6 +48,7 @@ import { usePriceImpact } from '../price-impact/PriceImpactProvider'
import { calcMarketPriceImpact } from '../price-impact/price-impact.utils'
import { isAuraBalSwap } from './swap.helpers'
import { AuraBalSwapHandler } from './handlers/AuraBalSwap.handler'
+import { useIsPoolSwap } from './useIsPoolSwap'
export type UseSwapResponse = ReturnType
export const SwapContext = createContext(null)
@@ -60,6 +61,8 @@ export type PathParams = {
amountOut?: string
// When urlTxHash is present the rest of the params above are not used
urlTxHash?: Hash
+ // Only used by pool swap
+ poolTokens?: GqlToken[]
}
function selectSwapHandler(
@@ -83,6 +86,7 @@ function selectSwapHandler(
}
export function _useSwap({ urlTxHash, ...pathParams }: PathParams) {
+ const isPoolSwap = useIsPoolSwap()
const swapStateVar = useMakeVarPersisted(
{
tokenIn: {
@@ -335,6 +339,7 @@ export function _useSwap({ urlTxHash, ...pathParams }: PathParams) {
}
function replaceUrlPath() {
+ if (isPoolSwap) return // Avoid redirection when the swap is within a pool page
const { selectedChain, tokenIn, tokenOut, swapType } = swapState
const { popularTokens } = networkConfig.tokens
const chainSlug = chainToSlugMap[selectedChain]
@@ -467,6 +472,10 @@ export function _useSwap({ urlTxHash, ...pathParams }: PathParams) {
setInitialTokenOut(tokenOut)
setInitialAmounts(amountIn, amountOut)
+ if (isPoolSwap) {
+ setTokens(pathParams.poolTokens!)
+ }
+
if (!swapState.tokenIn.address && !swapState.tokenOut.address) setDefaultTokens()
}, [])
@@ -509,6 +518,7 @@ export function _useSwap({ urlTxHash, ...pathParams }: PathParams) {
// Update selecteable tokens when the chain changes
useEffect(() => {
+ if (isPoolSwap) return
setTokens(getTokensByChain(swapState.selectedChain))
}, [swapState.selectedChain])
diff --git a/packages/lib/modules/swap/modal/SwapModal.tsx b/packages/lib/modules/swap/modal/SwapModal.tsx
index 27db8d0c..ca983711 100644
--- a/packages/lib/modules/swap/modal/SwapModal.tsx
+++ b/packages/lib/modules/swap/modal/SwapModal.tsx
@@ -20,6 +20,7 @@ import { SwapSummary } from './SwapSummary'
import { useSwapReceipt } from '../../transactions/transaction-steps/receipts/receipt.hooks'
import { useUserAccount } from '../../web3/UserAccountProvider'
import { useTokens } from '../../tokens/TokensProvider'
+import { useIsPoolSwap } from '../useIsPoolSwap'
type Props = {
isOpen: boolean
@@ -34,6 +35,7 @@ export function SwapPreviewModal({
finalFocusRef,
...rest
}: Props & Omit) {
+ const isPoolSwap = useIsPoolSwap()
const { isDesktop } = useBreakpoints()
const initialFocusRef = useRef(null)
const { userAddress } = useUserAccount()
@@ -59,7 +61,10 @@ export function SwapPreviewModal({
useEffect(() => {
if (!isWrap && swapTxHash && !window.location.pathname.includes(swapTxHash)) {
- window.history.pushState({}, '', `/swap/${chainToSlugMap[selectedChain]}/${swapTxHash}`)
+ const url = isPoolSwap
+ ? `${window.location.pathname}/${swapTxHash}`
+ : `/swap/${chainToSlugMap[selectedChain]}/${swapTxHash}`
+ window.history.pushState({}, '', url)
}
}, [swapTxHash])
@@ -102,7 +107,7 @@ export function SwapPreviewModal({
diff --git a/packages/lib/modules/swap/useIsPoolSwap.tsx b/packages/lib/modules/swap/useIsPoolSwap.tsx
new file mode 100644
index 00000000..503dfb00
--- /dev/null
+++ b/packages/lib/modules/swap/useIsPoolSwap.tsx
@@ -0,0 +1,6 @@
+import { usePathname } from 'next/navigation'
+
+export function useIsPoolSwap() {
+ const pathname = usePathname()
+ return pathname.includes('/pools') && pathname.includes('/swap')
+}
diff --git a/packages/lib/modules/tokens/NativeAssetSelectModal.tsx b/packages/lib/modules/tokens/NativeAssetSelectModal.tsx
index 14da0419..99107507 100644
--- a/packages/lib/modules/tokens/NativeAssetSelectModal.tsx
+++ b/packages/lib/modules/tokens/NativeAssetSelectModal.tsx
@@ -13,7 +13,7 @@ import {
} from '@chakra-ui/react'
import { RefObject } from 'react'
import { GqlChain, GqlToken } from '@repo/lib/shared/services/api/generated/graphql'
-import { NativeAssetSelectList } from './NativeAssetSelectList'
+import { CompactTokenSelectList } from './TokenSelectModal/TokenSelectList/CompactTokenSelectList'
type Props = {
chain: GqlChain
@@ -54,7 +54,7 @@ export function NativeAssetSelectModal({
-
+
diff --git a/packages/lib/modules/tokens/NativeAssetSelectList.tsx b/packages/lib/modules/tokens/TokenSelectModal/TokenSelectList/CompactTokenSelectList.tsx
similarity index 88%
rename from packages/lib/modules/tokens/NativeAssetSelectList.tsx
rename to packages/lib/modules/tokens/TokenSelectModal/TokenSelectList/CompactTokenSelectList.tsx
index 2c05be3f..2f1154d2 100644
--- a/packages/lib/modules/tokens/NativeAssetSelectList.tsx
+++ b/packages/lib/modules/tokens/TokenSelectModal/TokenSelectList/CompactTokenSelectList.tsx
@@ -1,20 +1,20 @@
'use client'
import { Box, BoxProps, Center, Text } from '@chakra-ui/react'
-import { TokenSelectListRow } from './TokenSelectModal/TokenSelectList/TokenSelectListRow'
import { GqlToken } from '@repo/lib/shared/services/api/generated/graphql'
-import { useTokenBalances } from './TokenBalancesProvider'
import { useUserAccount } from '@repo/lib/modules/web3/UserAccountProvider'
import { useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { Virtuoso } from 'react-virtuoso'
+import { useTokenBalances } from '../../TokenBalancesProvider'
+import { TokenSelectListRow } from './TokenSelectListRow'
type Props = {
tokens: GqlToken[]
onTokenSelect: (token: GqlToken) => void
}
-export function NativeAssetSelectList({ tokens, onTokenSelect, ...rest }: Props & BoxProps) {
+export function CompactTokenSelectList({ tokens, onTokenSelect, ...rest }: Props & BoxProps) {
const [activeIndex, setActiveIndex] = useState(0)
const { balanceFor, isBalancesLoading } = useTokenBalances()
const { isConnected } = useUserAccount()
@@ -40,6 +40,8 @@ export function NativeAssetSelectList({ tokens, onTokenSelect, ...rest }: Props
return `${token.address}:${token.chain}:${index}`
}
+ const style = { height: `${tokens.length * 75}px` }
+
return (
{tokens.length === 0 ? (
@@ -50,7 +52,7 @@ export function NativeAssetSelectList({ tokens, onTokenSelect, ...rest }: Props
) : (
{
const token = tokens[index]
diff --git a/packages/lib/modules/tokens/TokenSelectModal/TokenSelectList/CompactTokenSelectModal.tsx b/packages/lib/modules/tokens/TokenSelectModal/TokenSelectList/CompactTokenSelectModal.tsx
new file mode 100644
index 00000000..fa73a5a2
--- /dev/null
+++ b/packages/lib/modules/tokens/TokenSelectModal/TokenSelectList/CompactTokenSelectModal.tsx
@@ -0,0 +1,64 @@
+'use client'
+
+import {
+ Box,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ ModalProps,
+ VStack,
+} from '@chakra-ui/react'
+import { RefObject } from 'react'
+import { GqlChain, GqlToken } from '@repo/lib/shared/services/api/generated/graphql'
+import { CompactTokenSelectList } from './CompactTokenSelectList'
+
+type Props = {
+ chain: GqlChain
+ isOpen: boolean
+ onClose(): void
+ onOpen(): void
+ finalFocusRef?: RefObject
+ onTokenSelect: (token: GqlToken) => void
+ tokens: GqlToken[]
+}
+
+export function CompactTokenSelectModal({
+ isOpen,
+ onClose,
+ finalFocusRef,
+ onTokenSelect,
+ tokens,
+ ...rest
+}: Props & Omit) {
+ function closeOnSelect(token: GqlToken) {
+ onClose()
+ onTokenSelect(token)
+ }
+
+ return (
+
+
+
+ Select a token
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/lib/shared/components/icons/SwapIcon.tsx b/packages/lib/shared/components/icons/SwapIcon.tsx
new file mode 100644
index 00000000..5fa8b414
--- /dev/null
+++ b/packages/lib/shared/components/icons/SwapIcon.tsx
@@ -0,0 +1,48 @@
+/* eslint-disable max-len */
+export function SwapIcon({ size = 24 }: { size?: number }) {
+ return (
+
+ )
+}