Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions apps/web/src/components/NavBar/PoolSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
import styled from 'lib/styled-components'
import React, { useCallback, useEffect, useState } from 'react';
import { useActiveSmartPool, useSelectActiveSmartPool } from 'state/application/hooks';
import { PoolWithChain } from 'state/application/reducer';
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types';

const PoolSelectButton = styled(ButtonGray)<{
Expand Down Expand Up @@ -65,45 +66,66 @@ const StyledTokenName = styled.span<{ active?: boolean }>`
`;

interface PoolSelectProps {
operatedPools: Token[];
operatedPools: PoolWithChain[];
}

const PoolSelect: React.FC<PoolSelectProps> = ({ operatedPools }) => {
const [showModal, setShowModal] = useState(false);
const activeSmartPool = useActiveSmartPool();
const onPoolSelect = useSelectActiveSmartPool();

// on chain switch revert to default pool if selected does not exist on new chain
const activePoolExistsOnChain = operatedPools?.some(pool => pool.address === activeSmartPool?.address);
// Create a map for quick chainId lookup by address
const poolChainMap = React.useMemo(() =>
new Map(operatedPools.map(pool => [pool.address, pool.chainId])),
[operatedPools]
);

// Convert PoolWithChain[] to Token[] for display
const poolsAsTokens = React.useMemo(() =>
operatedPools.map((pool) =>
new Token(pool.chainId, pool.address, pool.decimals, pool.symbol, pool.name)
),
[operatedPools]
);

// on chain switch revert to default pool if selected does not exist
const activePoolExistsInList = operatedPools?.some(pool => pool.address === activeSmartPool?.address);

// initialize selected pool - use ref to prevent re-initialization
const hasInitialized = React.useRef(false);

useEffect(() => {
if (!hasInitialized.current && (!activeSmartPool?.name || !activePoolExistsOnChain)) {
onPoolSelect(operatedPools[0]);
if (!hasInitialized.current && (!activeSmartPool?.name || !activePoolExistsInList)) {
if (poolsAsTokens.length > 0 && operatedPools.length > 0) {
const firstPool = operatedPools[0];
onPoolSelect(poolsAsTokens[0], firstPool.chainId);
}
hasInitialized.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activePoolExistsOnChain, activeSmartPool?.name])
}, [activePoolExistsInList, activeSmartPool?.name, poolsAsTokens, operatedPools, onPoolSelect])

// Memoize poolsAsCurrrencies to prevent recreation on every render
const poolsAsCurrrencies = React.useMemo(() =>
operatedPools.map((pool: Token) => ({
poolsAsTokens.map((pool: Token) => ({
currency: pool,
currencyId: pool.address,
safetyLevel: null,
safetyInfo: null,
spamCode: null,
logoUrl: null,
isSpam: null
isSpam: null,
// Store chainId using our map for safer access
chainId: poolChainMap.get(pool.address),
})) as CurrencyInfo[]
, [operatedPools]);
, [poolsAsTokens, poolChainMap]);

const handleSelectPool = useCallback((pool: Currency) => {
onPoolSelect(pool);
// Find the chain ID for the selected pool using our map
const poolAddress = pool.isToken ? pool.address : '';
const chainId = poolChainMap.get(poolAddress);
onPoolSelect(pool, chainId);
setShowModal(false);
}, [onPoolSelect]);
}, [onPoolSelect, poolChainMap]);

return (
<>
Expand Down
40 changes: 14 additions & 26 deletions apps/web/src/components/NavBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { PageType, useIsPage } from 'hooks/useIsPage'
import deprecatedStyled, { css } from 'lib/styled-components'
import { useProfilePageState } from 'nft/hooks'
import { ProfilePageStateType } from 'nft/types'
import { useEffect, useMemo, useRef } from 'react'
import { useOperatedPools } from 'state/pool/hooks'
import { useMemo } from 'react'
import { useInitializeMultiChainPools, useOperatedPoolsMultiChain } from 'state/pool/hooks'
import { Flex, Nav as TamaguiNav, styled, useMedia } from 'ui/src'
import { INTERFACE_NAV_HEIGHT, breakpoints, zIndexes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
Expand Down Expand Up @@ -140,41 +140,29 @@ export default function Navbar() {

const isSignInExperimentControl = !isEmbeddedWalletEnabled && isControl
const shouldDisplayCreateAccountButton = false
const rawOperatedPools = useOperatedPools()
const cachedPoolsRef = useRef<typeof rawOperatedPools | undefined>(undefined)
const prevChainIdRef = useRef<number | undefined>(account.chainId)

useEffect(() => {
if (account.chainId !== prevChainIdRef.current) {
cachedPoolsRef.current = undefined
prevChainIdRef.current = account.chainId
}
}, [account.chainId])
// Initialize multi-chain pools
useInitializeMultiChainPools()

useEffect(() => {
if (rawOperatedPools && rawOperatedPools.length > 0) {
cachedPoolsRef.current = rawOperatedPools
}
}, [rawOperatedPools])

const cachedOperatedPools = cachedPoolsRef.current ?? rawOperatedPools
const cachedUserIsOperator = useMemo(() =>
Boolean(cachedOperatedPools && cachedOperatedPools.length > 0),
[cachedOperatedPools]
// Get all operated pools from state (no chain filtering)
const operatedPools = useOperatedPoolsMultiChain()
const userIsOperator = useMemo(() =>
Boolean(operatedPools && operatedPools.length > 0),
[operatedPools]
)

return (
<Nav>
<Flex row centered width="100%" style={{ position: 'relative' }}>
<Left style={{ flexShrink: 0 }}>
<CompanyMenu />
{areTabsVisible && <Tabs userIsOperator={cachedUserIsOperator} />}
{areTabsVisible && <Tabs userIsOperator={userIsOperator} />}
</Left>

<SearchContainer>
{!collapseSearchBar && cachedOperatedPools && cachedOperatedPools.length > 0 && (
{!collapseSearchBar && operatedPools && operatedPools.length > 0 && (
<SelectedPoolContainer>
<PoolSelect operatedPools={cachedOperatedPools} />
<PoolSelect operatedPools={operatedPools} />
</SelectedPoolContainer>
)}
{!collapseSearchBar && (
Expand All @@ -188,9 +176,9 @@ export default function Navbar() {
{collapseSearchBar && (
<Flex row gap={-12} alignItems="center" mr={-15} ml={-12}>
<SearchBar maxHeight={NAV_SEARCH_MAX_HEIGHT} fullScreen={isSmallScreen} />
{cachedOperatedPools && cachedOperatedPools.length > 0 && (
{operatedPools && operatedPools.length > 0 && (
<Flex mt={8}>
<PoolSelect operatedPools={cachedOperatedPools} />
<PoolSelect operatedPools={operatedPools} />
</Flex>
)}
{!hideChainSelector && <ChainSelector />}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useAccount } from 'hooks/useAccount'
import { LanguageProvider } from 'i18n/LanguageProvider'
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
import { MulticallUpdater } from 'lib/state/multicall'
import { PoolMulticallUpdater } from 'lib/state/multicallForPools'
import App from 'pages/App'
import { PropsWithChildren, StrictMode, useEffect, useMemo } from 'react'
import { createRoot } from 'react-dom/client'
Expand Down Expand Up @@ -74,6 +75,7 @@ function Updaters() {
<ApplicationUpdater />
<ActivityStateUpdater />
<MulticallUpdater />
<PoolMulticallUpdater />
<LogsUpdater />
<FiatOnRampTransactionsUpdater />
<Web3ProviderUpdater />
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/lib/hooks/multicallForPools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { usePoolCallContext } from 'lib/hooks/usePoolCallContext'
import poolMulticall from 'lib/state/multicallForPools'
import { SkipFirst } from 'types/tuple'

export { NEVER_RELOAD } from '@uniswap/redux-multicall' // re-export for convenience
export type { CallStateResult } from '@uniswap/redux-multicall' // re-export for convenience

// Create wrappers for pool-specific multicall hooks that use a separate multicall instance
// dedicated to pool queries. This instance uses the wallet's chainId and is not affected
// by multicallUpdaterSwapChainId from the swap context.
//
// This prevents pool queries from being invalidated when users swap tokens on different chains.

type SkipFirstTwoParams<T extends (...args: any) => any> = SkipFirst<Parameters<T>, 2>

export function useMultipleContractSingleDataForPools(
...args: SkipFirstTwoParams<typeof poolMulticall.hooks.useMultipleContractSingleData>
) {
const { chainId, latestBlock } = usePoolCallContext()
return poolMulticall.hooks.useMultipleContractSingleData(chainId, latestBlock, ...args)
}

export function useSingleCallResultForPools(...args: SkipFirstTwoParams<typeof poolMulticall.hooks.useSingleCallResult>) {
const { chainId, latestBlock } = usePoolCallContext()
return poolMulticall.hooks.useSingleCallResult(chainId, latestBlock, ...args)
}

export function useSingleContractMultipleDataForPools(
...args: SkipFirstTwoParams<typeof poolMulticall.hooks.useSingleContractMultipleData>
) {
const { chainId, latestBlock } = usePoolCallContext()
return poolMulticall.hooks.useSingleContractMultipleData(chainId, latestBlock, ...args)
}
34 changes: 34 additions & 0 deletions apps/web/src/lib/hooks/usePoolCallContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useAccount } from 'hooks/useAccount'
import { useContext } from 'react'
import { BlockNumberContext } from 'lib/hooks/useBlockNumber'

/**
* Get block number for the wallet's actual chainId.
* This accesses the BlockNumberContext which tracks blocks independently,
* so we get the current block for whatever chain is active in the context.
*/
function useWalletBlockNumber(): number | undefined {
const blockNumberContext = useContext(BlockNumberContext)
if (!blockNumberContext) {
return undefined
}

// Return the current block number from context
// The PoolMulticallUpdater ensures this is for the wallet's actual chainId
const latestBlockNumber = typeof blockNumberContext === 'object' ? blockNumberContext?.block : undefined
return latestBlockNumber
}

/**
* Hook to provide call context for pool-related multicall queries.
* This is used by pool-specific multicall hooks to ensure they always query
* the wallet's actual chainId, not the swap context's chainId.
*
* The key is that pool queries use PoolMulticallUpdater which never looks at
* multicallUpdaterSwapChainId, so pool data remains stable when swapping across chains.
*/
export function usePoolCallContext() {
const account = useAccount()
const latestBlock = useWalletBlockNumber()
return { chainId: account.chainId, latestBlock }
}
44 changes: 44 additions & 0 deletions apps/web/src/lib/state/multicallForPools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createMulticall, ListenerOptions } from '@uniswap/redux-multicall'
import { useAccount } from 'hooks/useAccount'
import { useInterfaceMulticall } from 'hooks/useContract'
import { useContext, useMemo } from 'react'
import { BlockNumberContext } from 'lib/hooks/useBlockNumber'
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'

// Create a separate multicall instance specifically for pool queries
// This is independent from the main multicall system and won't be affected
// by chain switches in the swap context
const poolMulticall = createMulticall()

export default poolMulticall

/**
* Multicall updater specifically for pool queries.
* Unlike the main MulticallUpdater, this one ONLY uses the wallet's actual chainId
* and is not affected by multicallUpdaterSwapChainId from the swap context.
*
* This prevents pool queries from being invalidated when users swap tokens on different chains.
*/
export function PoolMulticallUpdater() {
const account = useAccount()
// CRITICAL: Only use account.chainId, never use multicallUpdaterSwapChainId
const chainId = account.chainId

// Get block number from context
const blockNumberContext = useContext(BlockNumberContext)
const latestBlockNumber = typeof blockNumberContext === 'object' ? blockNumberContext?.block : undefined
const contract = useInterfaceMulticall(chainId)
const listenerOptions: ListenerOptions = useMemo(
() => ({ blocksPerFetch: chainId ? getChainInfo(chainId).blockPerMainnetEpochForChainId : 1 }),
[chainId],
)

return (
<poolMulticall.Updater
chainId={chainId}
latestBlockNumber={latestBlockNumber}
contract={contract}
listenerOptions={listenerOptions}
/>
)
}
9 changes: 7 additions & 2 deletions apps/web/src/pages/CreatePool/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import styled from 'lib/styled-components'
import { Trans } from 'react-i18next'
import { useCloseModal, useModalIsOpen, useToggleCreateModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { useAllPoolsData } from 'state/pool/hooks'
import { useInitializeMultiChainPools, useOperatedPoolsDataFromCache } from 'state/pool/hooks'
import { ThemedText } from 'theme/components/text'
import Trace from 'uniswap/src/features/telemetry/Trace'

Expand Down Expand Up @@ -74,7 +74,12 @@ export default function CreatePool() {
const open = useModalIsOpen(ApplicationModal.CREATE)
const closeModal = useCloseModal(ApplicationModal.CREATE)
const toggleCreateModal = useToggleCreateModal()
const { data: allPools } = useAllPoolsData()

// Initialize multi-chain pools
useInitializeMultiChainPools()

// Use cached operated pools instead of querying
const { data: allPools } = useOperatedPoolsDataFromCache()

return (
<Trace logImpression page={InterfacePageName.POOL_PAGE}>
Expand Down
9 changes: 6 additions & 3 deletions apps/web/src/pages/Stake/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Trans } from 'react-i18next'
import JSBI from 'jsbi'
import { useMemo, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { PoolRegisteredLog, useAllPoolsData, useStakingPools } from 'state/pool/hooks'
import { PoolRegisteredLog, useAllPoolsDataStable, useInitializeMultiChainPools, useStakingPools } from 'state/pool/hooks'
import { useFreeStakeBalance, useUnclaimedRewards, useUserStakeBalances } from 'state/stake/hooks'
import styled from 'lib/styled-components'
import { ThemedText } from 'theme/components/text'
Expand Down Expand Up @@ -86,8 +86,11 @@ export default function Stake() {
const [hasMore, setHasMore] = useState(true)
const [records, setRecords] = useState(itemsPerPage)

// we retrieve logs again as we want to be able to load pools when switching chain from stake page.
const { data: allPools } = useAllPoolsData()
// Initialize multi-chain pools to ensure pools persist across chain switches
useInitializeMultiChainPools()

// Get all registered pools with stable chainId (won't break on swap chain changes)
const { data: allPools } = useAllPoolsDataStable()

const account = useAccount()
const accountDrawer = useAccountDrawer()
Expand Down
24 changes: 22 additions & 2 deletions apps/web/src/state/application/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ import {
ApplicationModal,
CloseModalParams,
OpenModalParams,
PoolWithChain,
addSuppressedPopups,
removeSuppressedPopups,
setCloseModal,
setOpenModal,
setOperatedPools,
setSmartPoolValue,
} from 'state/application/reducer'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { InterfaceState } from 'state/webReducer'
import { ModalNameType } from 'uniswap/src/features/telemetry/constants'

// Re-export PoolWithChain for external use
export type { PoolWithChain } from 'state/application/reducer'

export function useModalIsOpen(modal: ApplicationModal | ModalNameType): boolean {
const openModal = useAppSelector((state: InterfaceState) => state.application.openModal?.name)
return openModal === modal
Expand Down Expand Up @@ -81,15 +86,16 @@ export function useTogglePrivacyPolicy(): () => void {
return useToggleModal(ApplicationModal.PRIVACY_POLICY)
}

export function useSelectActiveSmartPool(): (smartPoolValue?: Currency) => void {
export function useSelectActiveSmartPool(): (smartPoolValue?: Currency, chainId?: number) => void {
const dispatch = useAppDispatch()
return useCallback(
(smartPoolValue?: Currency) => {
(smartPoolValue?: Currency, chainId?: number) => {
dispatch(
setSmartPoolValue({
smartPool: {
address: smartPoolValue?.isToken ? smartPoolValue.address : undefined,
name: smartPoolValue?.isToken && smartPoolValue.name ? smartPoolValue.name : undefined,
chainId,
},
})
)
Expand All @@ -98,6 +104,20 @@ export function useSelectActiveSmartPool(): (smartPoolValue?: Currency) => void
)
}

export function useSetOperatedPools(): (pools: PoolWithChain[]) => void {
const dispatch = useAppDispatch()
return useCallback(
(pools: PoolWithChain[]) => {
dispatch(setOperatedPools(pools))
},
[dispatch]
)
}

export function useOperatedPoolsFromState(): PoolWithChain[] {
return useAppSelector((state: InterfaceState) => state.application.operatedPools)
}

// returns functions to suppress and unsuppress popups by type
export function useSuppressPopups(popupTypes: PopupType[]): {
suppressPopups: () => void
Expand Down
Loading