diff --git a/apps/haust-dex/.env.production b/apps/haust-dex/.env.production index a472d597fef..ed1bd966652 100644 --- a/apps/haust-dex/.env.production +++ b/apps/haust-dex/.env.production @@ -8,6 +8,6 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E" REACT_APP_SENTRY_ENABLED=true REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003 REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy" -REACT_APP_THE_GRAPH_SCHEMA_ENDPOINT="https://graph.testnet.haust.app/subgraphs/name/haust/uniswap-v3" +REACT_APP_THE_GRAPH_SCHEMA_ENDPOINT="https://graph.stage.haust.app/subgraphs/name/haust/uniswap-v3" REACT_APP_API_URL="https://haust-v3-stats.rocknblock.io/api/v1/" REACT_APP_WALLET_CONNECT_PROJECT_ID="4f9a1d1c515fa8f9dcd2c305e2e4e9ee" diff --git a/apps/haust-dex/codegen.yml b/apps/haust-dex/codegen.yml index 0029a9c55d3..6fb9887d867 100644 --- a/apps/haust-dex/codegen.yml +++ b/apps/haust-dex/codegen.yml @@ -1,5 +1,5 @@ overrideExisting: false -schema: 'https://graph.testnet.haust.app/subgraphs/name/haust/uniswap-v3' +schema: 'https://graph.stage.haust.app/subgraphs/name/haust/uniswap-v3' generates: ./src/graphql/thegraph/schema/schema.graphql: plugins: diff --git a/apps/haust-dex/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts b/apps/haust-dex/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts index a5130ae9d62..ccc2246d12f 100644 --- a/apps/haust-dex/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts +++ b/apps/haust-dex/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts @@ -1,22 +1,41 @@ -import { Token } from '@uniswap/sdk-core' -import { AddressMap } from '@uniswap/smart-order-router' -import { useWeb3React } from '@web3-react/core' -import { MULTICALL_ADDRESS, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES } from 'constants/addresses' -import { isSupportedChain, SupportedChainId } from 'constants/chains' -import { RPC_PROVIDERS } from 'constants/providers' -import { BaseContract } from 'ethers/lib/ethers' -import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks' -import { toContractInput } from 'graphql/data/util' -import useStablecoinPrice from 'hooks/useStablecoinPrice' -import { useMemo } from 'react' -import { NonfungiblePositionManagerJson as NFTPositionManagerABI, UniswapInterfaceMulticallJson as MulticallABI } from 'sdks/v3-periphery' -import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'types/v3' -import { getContract } from 'utils' -import { CurrencyKey, currencyKey, currencyKeyFromGraphQL } from 'utils/currencyKey' - -import { PositionInfo } from './cache' - -type ContractMap = { [key: number]: T } +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Token } from "@uniswap/sdk-core"; +import { AddressMap } from "@uniswap/smart-order-router"; +import { useWeb3React } from "@web3-react/core"; +import { + MULTICALL_ADDRESS, + NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES, +} from "constants/addresses"; +import { isSupportedChain, SupportedChainId } from "constants/chains"; +import { RPC_PROVIDERS } from "constants/providers"; +import { BaseContract } from "ethers/lib/ethers"; +import { + ContractInput, + useUniswapPricesQuery, +} from "graphql/data/__generated__/types-and-hooks"; +import { toContractInput } from "graphql/data/util"; +import useStablecoinPrice from "hooks/useStablecoinPrice"; +import { useMemo } from "react"; +import { + NonfungiblePositionManagerJson as NFTPositionManagerABI, + UniswapInterfaceMulticallJson as MulticallABI, +} from "sdks/v3-periphery"; +import { + NonfungiblePositionManager, + UniswapInterfaceMulticall, +} from "types/v3"; +import { getContract } from "utils"; +import { + CurrencyKey, + currencyKey, + currencyKeyFromGraphQL, +} from "utils/currencyKey"; + +import { PositionInfo } from "./cache"; + +type ContractMap = { [key: number]: T }; + +const FALLBACK_RPC_URL = "https://rpc-testnet.haust.app"; // Constructs a chain-to-contract map, using the wallet's provider when available function useContractMultichain( @@ -24,72 +43,140 @@ function useContractMultichain( ABI: any, chainIds?: SupportedChainId[] ): ContractMap { - const { chainId: walletChainId, provider: walletProvider } = useWeb3React() + const { chainId: walletChainId, provider: walletProvider } = useWeb3React(); return useMemo(() => { const relevantChains = chainIds ?? Object.keys(addressMap) .map((chainId) => parseInt(chainId)) - .filter(isSupportedChain) + .filter(isSupportedChain); return relevantChains.reduce((acc: ContractMap, chainId) => { - const provider = walletProvider && walletChainId === chainId ? walletProvider : RPC_PROVIDERS[chainId] - acc[chainId] = getContract(addressMap[chainId], ABI, provider) as T - return acc - }, {}) - }, [ABI, addressMap, chainIds, walletChainId, walletProvider]) + let provider = + walletProvider && walletChainId === chainId + ? walletProvider + : RPC_PROVIDERS[chainId]; + + // Проверяем провайдер через простой запрос + if (provider) { + provider + .getBlockNumber() + .then(() => { + console.log( + `[useContractMultichain] Provider healthy for chain ${chainId}` + ); + }) + .catch((error) => { + console.error( + `[useContractMultichain] Provider failed for chain ${chainId}:`, + error + ); + // При ошибке создаем и используем fallback провайдер + const fallbackProvider = new JsonRpcProvider(FALLBACK_RPC_URL); + provider = fallbackProvider; + + // Обновляем контракт с новым провайдером + acc[chainId] = getContract( + addressMap[chainId], + ABI, + fallbackProvider + ) as T; + }); + } + + // Изначально создаем контракт с текущим провайдером + acc[chainId] = getContract(addressMap[chainId], ABI, provider) as T; + return acc; + }, {}); + }, [ABI, addressMap, chainIds, walletChainId, walletProvider]); } -export function useV3ManagerContracts(chainIds: SupportedChainId[]): ContractMap { - return useContractMultichain(V3NFT_ADDRESSES, NFTPositionManagerABI, chainIds) +export function useV3ManagerContracts( + chainIds: SupportedChainId[] +): ContractMap { + return useContractMultichain( + V3NFT_ADDRESSES, + NFTPositionManagerABI, + chainIds + ); } -export function useInterfaceMulticallContracts(chainIds: SupportedChainId[]): ContractMap { - return useContractMultichain(MULTICALL_ADDRESS, MulticallABI, chainIds) +export function useInterfaceMulticallContracts( + chainIds: SupportedChainId[] +): ContractMap { + return useContractMultichain( + MULTICALL_ADDRESS, + MulticallABI, + chainIds + ); } -type PriceMap = { [key: CurrencyKey]: number | undefined } +type PriceMap = { [key: CurrencyKey]: number | undefined }; export function usePoolPriceMap(positions: PositionInfo[] | undefined) { const contracts = useMemo(() => { - if (!positions || !positions.length) return [] + if (!positions || !positions.length) return []; // Avoids fetching duplicate tokens by placing in map - const contractMap = positions.reduce((acc: { [key: string]: ContractInput }, { pool: { token0, token1 } }) => { - acc[currencyKey(token0)] = toContractInput(token0) - acc[currencyKey(token1)] = toContractInput(token1) - return acc - }, {}) - return Object.values(contractMap) - }, [positions]) + const contractMap = positions.reduce( + (acc: { [key: string]: ContractInput }, { pool: { token0, token1 } }) => { + acc[currencyKey(token0)] = toContractInput(token0); + acc[currencyKey(token1)] = toContractInput(token1); + return acc; + }, + {} + ); + return Object.values(contractMap); + }, [positions]); - const { data, loading } = useUniswapPricesQuery({ variables: { contracts }, skip: !contracts.length }) + const { data, loading } = useUniswapPricesQuery({ + variables: { contracts }, + skip: !contracts.length, + }); const priceMap = useMemo( () => data?.tokens?.reduce((acc: PriceMap, current) => { - if (current) acc[currencyKeyFromGraphQL(current)] = current.project?.markets?.[0]?.price?.value - return acc + if (current) + acc[currencyKeyFromGraphQL(current)] = + current.project?.markets?.[0]?.price?.value; + return acc; }, {}) ?? {}, [data?.tokens] - ) + ); - return { priceMap, pricesLoading: loading && !data } + return { priceMap, pricesLoading: loading && !data }; } -function useFeeValue(token: Token, fee: number | undefined, queriedPrice: number | undefined) { - const stablecoinPrice = useStablecoinPrice(!queriedPrice ? token : undefined) +function useFeeValue( + token: Token, + fee: number | undefined, + queriedPrice: number | undefined +) { + const stablecoinPrice = useStablecoinPrice(!queriedPrice ? token : undefined); return useMemo(() => { // Prefers gql price, as fetching stablecoinPrice will trigger multiple infura calls for each pool position - const price = queriedPrice ?? (stablecoinPrice ? parseFloat(stablecoinPrice.toSignificant()) : undefined) - const feeValue = fee && price ? fee * price : undefined + const price = + queriedPrice ?? + (stablecoinPrice + ? parseFloat(stablecoinPrice.toSignificant()) + : undefined); + const feeValue = fee && price ? fee * price : undefined; - return [price, feeValue] - }, [fee, queriedPrice, stablecoinPrice]) + return [price, feeValue]; + }, [fee, queriedPrice, stablecoinPrice]); } export function useFeeValues(position: PositionInfo) { - const [priceA, feeValueA] = useFeeValue(position.pool.token0, position.fees?.[0], position.prices?.[0]) - const [priceB, feeValueB] = useFeeValue(position.pool.token1, position.fees?.[1], position.prices?.[1]) + const [priceA, feeValueA] = useFeeValue( + position.pool.token0, + position.fees?.[0], + position.prices?.[0] + ); + const [priceB, feeValueB] = useFeeValue( + position.pool.token1, + position.fees?.[1], + position.prices?.[1] + ); - return { priceA, priceB, fees: (feeValueA || 0) + (feeValueB || 0) } + return { priceA, priceB, fees: (feeValueA || 0) + (feeValueB || 0) }; } diff --git a/apps/haust-dex/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx b/apps/haust-dex/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx index 66f6019d55e..08203133cf6 100644 --- a/apps/haust-dex/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx +++ b/apps/haust-dex/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx @@ -50,7 +50,8 @@ export default function Tokens({ totalBalance }: { totalBalance?: number }) { const tokensList = useMemo(() => { const allTokens = [nativeCurrency, ...Object.values(tokens)] - + .filter(token => token?.symbol !== 'MYR') + return allTokens .filter(Boolean) .sort((a, b) => { diff --git a/apps/haust-dex/src/components/Tokens/TokenTable/TokenRow.tsx b/apps/haust-dex/src/components/Tokens/TokenTable/TokenRow.tsx index 8b4b81ea93c..ad6c8b0fd0b 100644 --- a/apps/haust-dex/src/components/Tokens/TokenTable/TokenRow.tsx +++ b/apps/haust-dex/src/components/Tokens/TokenTable/TokenRow.tsx @@ -160,7 +160,7 @@ const StyledHeaderRow = styled.div` } ` -const ListNumberCell = styled(Cell)` +const ListNumberCell = styled(Cell)<{ header?: boolean }>` color: ${({ theme }) => theme.textSecondary}; min-width: 32px; font-size: 14px; diff --git a/apps/haust-dex/src/constants/lists.ts b/apps/haust-dex/src/constants/lists.ts index 7ba7d2e0256..466d540c4ae 100644 --- a/apps/haust-dex/src/constants/lists.ts +++ b/apps/haust-dex/src/constants/lists.ts @@ -14,7 +14,7 @@ const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json' const KLEROS_LIST = 't2crtokens.eth' const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json' const WRAPPED_LIST = 'wrapped.tokensoft.eth' -export const HAUST_TOKENLIST = 'https://raw.githubusercontent.com/Haust-Labs/haust-dex-tokenlist/refs/heads/master/tokenlist.json' +export const HAUST_TOKENLIST = 'https://raw.githubusercontent.com/Haust-Labs/haust-dex-tokenlist/test/tokenlist.json' export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST, UNI_UNSUPPORTED_LIST] diff --git a/apps/haust-dex/src/graphql/thegraph/apollo.ts b/apps/haust-dex/src/graphql/thegraph/apollo.ts index 25ad81aa4c2..027d3452e1e 100644 --- a/apps/haust-dex/src/graphql/thegraph/apollo.ts +++ b/apps/haust-dex/src/graphql/thegraph/apollo.ts @@ -5,9 +5,9 @@ import store from '../../state/index' const CHAIN_SUBGRAPH_URL: Record = { // TODO: change subgraph path when mainnet is ready - [SupportedChainId.HAUST]: 'https://graph.testnet.haust.app/subgraphs/name/haust/uniswap-v3', + [SupportedChainId.HAUST]: 'https://graph.stage.haust.app/subgraphs/name/haust/uniswap-v3', [SupportedChainId.HAUST_TESTNET]: - 'https://graph.testnet.haust.app/subgraphs/name/haust/uniswap-v3', + 'https://graph.stage.haust.app/subgraphs/name/haust/uniswap-v3', } const httpLink = new HttpLink({ uri: CHAIN_SUBGRAPH_URL[SupportedChainId.HAUST_TESTNET] }) diff --git a/apps/haust-dex/src/lib/hooks/useBlockNumber.tsx b/apps/haust-dex/src/lib/hooks/useBlockNumber.tsx index aedf4336ba6..040373a7b2a 100644 --- a/apps/haust-dex/src/lib/hooks/useBlockNumber.tsx +++ b/apps/haust-dex/src/lib/hooks/useBlockNumber.tsx @@ -1,3 +1,4 @@ +import { JsonRpcProvider } from '@ethersproject/providers' import { useWeb3React } from '@web3-react/core' import useIsWindowVisible from 'hooks/useIsWindowVisible' import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' @@ -11,9 +12,13 @@ const BlockNumberContext = createContext< | typeof MISSING_PROVIDER >(MISSING_PROVIDER) +// RPC URL for fallback provider +const FALLBACK_RPC_URL = 'https://rpc-testnet.haust.app' // Можно будет заменить на нужный URL + function useBlockNumberContext() { const blockNumber = useContext(BlockNumberContext) if (blockNumber === MISSING_PROVIDER) { + console.error('[useBlockNumberContext] Missing BlockNumberProvider') throw new Error('BlockNumber hooks must be wrapped in a ') } return blockNumber @@ -29,7 +34,40 @@ export function useFastForwardBlockNumber(): (block: number) => void { } export function BlockNumberProvider({ children }: { children: ReactNode }) { - const { chainId: activeChainId, provider } = useWeb3React() + const { chainId: web3ChainId, provider: web3Provider } = useWeb3React() + const [fallbackProvider, setFallbackProvider] = useState(null) + const [fallbackChainId, setFallbackChainId] = useState() + + // Use web3Provider if available, otherwise use fallback + const provider = web3Provider || fallbackProvider + const activeChainId = web3ChainId || fallbackChainId + + useEffect(() => { + if (!web3Provider) { + const provider = new JsonRpcProvider(FALLBACK_RPC_URL) + setFallbackProvider(provider) + + // Get chainId from fallback provider + provider.getNetwork().then( + network => { + setFallbackChainId(network.chainId) + }, + error => console.error('[BlockNumberProvider] Failed to get fallback network:', error) + ) + } else { + setFallbackProvider(null) + setFallbackChainId(undefined) + } + }, [web3Provider]) + + if (provider) { + // Safe way to inspect provider + provider.getNetwork().then( + network => console.log('[BlockNumberProvider] Provider network:', network), + error => console.error('[BlockNumberProvider] Failed to get network:', error) + ) + } + const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId }) const onBlock = useCallback( @@ -51,22 +89,74 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) { let stale = false if (provider && activeChainId && windowVisible) { - // If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data. - setChainBlock((chainBlock) => (chainBlock.chainId === activeChainId ? chainBlock : { chainId: activeChainId })) + setChainBlock((chainBlock) => { + const newState = chainBlock.chainId === activeChainId ? chainBlock : { chainId: activeChainId } + return newState + }) + + // Check RPC availability and capabilities + const tryGetBlockNumber = async (provider: JsonRpcProvider | typeof web3Provider, isFallback = false) => { + const prefix = isFallback ? '[Fallback] ' : '' + try { + // Add timeout to detect hanging requests + const blockNumberPromise = provider?.getBlockNumber() + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Block number request timeout')), 5000) + }) + + const block = await Promise.race([blockNumberPromise, timeoutPromise]) + return block + } catch (error) { + console.error(`[BlockNumberProvider] ${prefix}Failed to get block number:`, error) + throw error + } + } - provider - .getBlockNumber() + // Try with primary provider first + tryGetBlockNumber(provider) + .catch(async (error) => { + // If primary fails and we don't have fallback yet, create it + let fbProvider = fallbackProvider + if (!fbProvider) { + fbProvider = new JsonRpcProvider(FALLBACK_RPC_URL) + setFallbackProvider(fbProvider) + } + // Try with fallback provider + return tryGetBlockNumber(fbProvider, true) + }) .then((block) => { - if (!stale) onBlock(block) + if (!stale) { + onBlock(block || 0) + } else { + console.log('[BlockNumberProvider] Block number fetched, but stale:', block) + } }) .catch((error) => { - console.error(`Failed to get block number for chainId ${activeChainId}`, error) + console.error(`[BlockNumberProvider] All providers failed to get block number:`, error) + }) + + + // Set up listeners for both providers + const setupBlockListener = (provider: JsonRpcProvider | typeof web3Provider, isFallback = false) => { + const prefix = isFallback ? '[Fallback] ' : '' + provider?.on('block', (block: number) => { + if (!stale) onBlock(block || 0) }) + } + + setupBlockListener(provider) + if (fallbackProvider) { + setupBlockListener(fallbackProvider, true) + } - provider.on('block', onBlock) return () => { stale = true - provider.removeListener('block', onBlock) + if (provider) { + provider.removeListener('block', onBlock) + } + if (fallbackProvider) { + fallbackProvider.removeListener('block', onBlock) + } } } @@ -74,14 +164,17 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) { }, [activeChainId, provider, onBlock, setChainBlock, windowVisible]) const value = useMemo( - () => ({ - value: chainId === activeChainId ? block : undefined, - fastForward: (update: number) => { - if (block && update > block) { - setChainBlock({ chainId: activeChainId, block: update }) - } - }, - }), + () => { + const result = { + value: chainId === activeChainId ? block : undefined, + fastForward: (update: number) => { + if (block && update > block) { + setChainBlock({ chainId: activeChainId, block: update }) + } + }, + } + return result + }, [activeChainId, block, chainId] ) return {children}