From 2dcf828dcf09911e027a4ec50f805c1a154dc91d Mon Sep 17 00:00:00 2001 From: Daryna Shpankova Date: Mon, 16 Jun 2025 21:36:26 +0200 Subject: [PATCH 1/2] update pools --- apps/haust-dex/.env.production | 2 +- apps/haust-dex/codegen.yml | 2 +- .../MiniPortfolio/Pools/hooks.ts | 193 +++++++++---- .../MiniPortfolio/Tokens/index.tsx | 3 +- .../components/Tokens/TokenTable/TokenRow.tsx | 2 +- apps/haust-dex/src/constants/lists.ts | 2 +- apps/haust-dex/src/graphql/thegraph/apollo.ts | 4 +- .../src/lib/hooks/useBlockNumber.tsx | 127 +++++++-- .../src/redux-multicall/constants.ts | 25 +- .../haust-dex/src/redux-multicall/updater.tsx | 269 ++++++++++++------ .../src/redux-multicall/utils/chunkCalls.ts | 75 +++-- 11 files changed, 483 insertions(+), 221 deletions(-) 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..c52a6c346be 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/refs/heads/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} diff --git a/apps/haust-dex/src/redux-multicall/constants.ts b/apps/haust-dex/src/redux-multicall/constants.ts index 1d627156cfa..42f890f3e07 100644 --- a/apps/haust-dex/src/redux-multicall/constants.ts +++ b/apps/haust-dex/src/redux-multicall/constants.ts @@ -1,16 +1,21 @@ -import type { CallResult, CallState, ListenerOptions } from './types' +import type { CallResult, CallState, ListenerOptions } from "./types"; -export const DEFAULT_BLOCKS_PER_FETCH = 1 -export const DEFAULT_CALL_GAS_REQUIRED = 15_000_000 -export const DEFAULT_CHUNK_GAS_REQUIRED = 30_000 -export const CHUNK_GAS_LIMIT = 15_000_000 -export const CONSERVATIVE_BLOCK_GAS_LIMIT = 10_000_000 // conservative, hard-coded estimate of the current block gas limit +export const DEFAULT_BLOCKS_PER_FETCH = 1; +export const DEFAULT_CALL_GAS_REQUIRED = 15_000_000; +export const DEFAULT_CHUNK_GAS_REQUIRED = 30_000; +export const CHUNK_GAS_LIMIT = 15_000_000; +export const CONSERVATIVE_BLOCK_GAS_LIMIT = 10_000_000; // conservative, hard-coded estimate of the current block gas limit +export const MAX_CHUNK_SIZE = 30; // new constant to limit chunk size // Consts for hooks -export const INVALID_RESULT: CallResult = { valid: false, blockNumber: undefined, data: undefined } +export const INVALID_RESULT: CallResult = { + valid: false, + blockNumber: undefined, + data: undefined, +}; export const NEVER_RELOAD: ListenerOptions = { blocksPerFetch: Infinity, -} +}; export const INVALID_CALL_STATE: CallState = { valid: false, @@ -18,11 +23,11 @@ export const INVALID_CALL_STATE: CallState = { loading: false, syncing: false, error: false, -} +}; export const LOADING_CALL_STATE: CallState = { valid: true, result: undefined, loading: true, syncing: true, error: false, -} +}; diff --git a/apps/haust-dex/src/redux-multicall/updater.tsx b/apps/haust-dex/src/redux-multicall/updater.tsx index 52421ee1316..d7b63897d61 100644 --- a/apps/haust-dex/src/redux-multicall/updater.tsx +++ b/apps/haust-dex/src/redux-multicall/updater.tsx @@ -1,28 +1,59 @@ -import React, { Dispatch, useEffect, useMemo, useRef } from 'react' +import React, { Dispatch, useCallback, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import type { UniswapInterfaceMulticall } from "../types/v3"; -import { CHUNK_GAS_LIMIT, DEFAULT_CALL_GAS_REQUIRED } from './constants' +import type { UniswapInterfaceMulticall } from "../types/v3" +import { DEFAULT_CALL_GAS_REQUIRED } from './constants' import type { MulticallContext } from './context' import type { MulticallActions } from './slice' -import type { Call, ListenerOptions,MulticallState, WithMulticallState } from './types' +import type { Call, ListenerOptions, MulticallState, WithMulticallState } from './types' import { parseCallKey, toCallKey } from './utils/callKeys' import chunkCalls from './utils/chunkCalls' -import { retry, RetryableError } from './utils/retry' +import { RetryableError } from './utils/retry' import useDebounce from './utils/useDebounce' const FETCH_RETRY_CONFIG = { - n: Infinity, + n: 2, minWait: 1000, - maxWait: 2500, + maxWait: 2000 +} + +const POLLING_INTERVAL = 2000 +const UPDATE_DEBOUNCE = 100 +const MAX_CHUNK_SIZE = 50 +const CACHE_TTL = 3000 + +const failedCallsCache = new Map(); +const FAILED_CALLS_TTL = 30000; + +const requestCache = new Map() + +let isInitialized = false +let initializationPromise: Promise | null = null + +async function initializeMulticall(contract: UniswapInterfaceMulticall) { + if (isInitialized || initializationPromise) return initializationPromise + + initializationPromise = new Promise((resolve) => { + contract.callStatic.multicall([], { blockTag: 'latest' }) + .then(() => { + isInitialized = true + resolve() + }) + .catch((error) => { + console.error('Failed to initialize multicall:', error) + isInitialized = false + resolve() + }) + }) + + return initializationPromise } /** - * Fetches a chunk of calls, enforcing a minimum block number constraint - * @param multicall multicall contract to fetch against - * @param chunk chunk of calls to make - * @param blockNumber block number passed as the block tag in the eth_call - * @param isDebug + * Оптимизированная функция для получения чанка данных */ async function fetchChunk( multicall: UniswapInterfaceMulticall, @@ -30,44 +61,47 @@ async function fetchChunk( blockNumber: number, isDebug?: boolean ): Promise<{ success: boolean; returnData: string }[]> { + if (!isInitialized) { + await initializeMulticall(multicall) + } + + const cacheKey = `${blockNumber}-${chunk.map(c => `${c.address}-${c.callData}`).join('-')}` + const now = Date.now() + const cached = requestCache.get(cacheKey) + + if (cached && now - cached.timestamp < CACHE_TTL) { + return cached.result + } + try { const { returnData } = await multicall.callStatic.multicall( chunk.map((obj) => ({ target: obj.address, callData: obj.callData, - gasLimit: DEFAULT_CALL_GAS_REQUIRED, + gasLimit: obj.gasRequired ?? DEFAULT_CALL_GAS_REQUIRED, })), - // we aren't passing through the block gas limit we used to create the chunk, because it causes a problem with the integ tests { blockTag: blockNumber } ) - if (isDebug) { - returnData.forEach(({ gasUsed, returnData, success }, i) => { - if ( - !success && - returnData.length === 2 && - gasUsed.gte(Math.floor((chunk[i].gasRequired ?? DEFAULT_CALL_GAS_REQUIRED) * 0.95)) - ) { - console.warn( - `A call failed due to requiring ${gasUsed.toString()} vs. allowed ${ - chunk[i].gasRequired ?? DEFAULT_CALL_GAS_REQUIRED - }`, - chunk[i] - ) - } - }) + requestCache.set(cacheKey, { timestamp: now, result: returnData }) + + for (const [key, value] of requestCache.entries()) { + if (now - value.timestamp > CACHE_TTL) { + requestCache.delete(key) + } } return returnData - } catch (e) { - const error = e as any + } catch (error: any) { if (error.code === -32000 || error.message?.indexOf('header not found') !== -1) { throw new RetryableError(`header not found for block number ${blockNumber}`) - } else if (error.code === -32603 || error.message?.indexOf('execution ran out of gas') !== -1) { + } + + if (error.code === -32603 || + error.message?.indexOf('execution ran out of gas') !== -1 || + error.message?.indexOf('missing revert data') !== -1 || + error.message?.indexOf('returndata.limit') !== -1) { if (chunk.length > 1) { - if (process.env.NODE_ENV === 'development') { - console.debug('Splitting a chunk in 2', chunk) - } const half = Math.floor(chunk.length / 2) const [c0, c1] = await Promise.all([ fetchChunk(multicall, chunk.slice(0, half), blockNumber), @@ -76,7 +110,12 @@ async function fetchChunk( return c0.concat(c1) } } - console.error('(Updater) Failed to fetch chunk', error) + + if (error.code === -32603 || error.message?.indexOf('Internal JSON-RPC error') !== -1) { + isInitialized = false + initializationPromise = null + } + throw error } } @@ -212,10 +251,9 @@ function onFetchChunkFailure(context: FetchChunkContext, chunk: Call[], error: a const { actions, dispatch, chainId, latestBlockNumber } = context if (error.isCancelledError) { - console.debug('Cancelled fetch for blockNumber', latestBlockNumber, chunk, chainId) return } - console.error('(Updater.onFetchChunkFailure) Failed to fetch multicall chunk', chunk, chainId, error) + dispatch( actions.errorFetchingMulticallResults({ calls: chunk, @@ -238,81 +276,124 @@ function Updater(props: UpdaterProps): null { const { context, chainId, latestBlockNumber, contract, isDebug, listenerOptions } = props const { actions, reducerPath } = context const dispatch = useDispatch() - - // set user configured listenerOptions in state for given chain ID. - useEffect(() => { - if (chainId && listenerOptions) { - dispatch(actions.updateListenerOptions({ chainId, listenerOptions })) - } - }, [chainId, listenerOptions, actions, dispatch]) - const state = useSelector((state: WithMulticallState) => state[reducerPath]) + + const [isUpdating, setIsUpdating] = useState(false) + const lastUpdateRef = useRef(0) + const pendingUpdatesRef = useRef>(new Set()) + const errorCountRef = useRef<{ [key: string]: number }>({}) + + const debouncedListeners = useDebounce(state.callListeners, UPDATE_DEBOUNCE) + + const getUpdateKeys = useCallback(() => { + if (!chainId || !latestBlockNumber) return [] + + const listeningKeys = activeListeningKeys(debouncedListeners, chainId) + return outdatedListeningKeys( + state.callResults, + listeningKeys, + chainId, + latestBlockNumber + ) + }, [chainId, latestBlockNumber, state.callResults, debouncedListeners]) - // wait for listeners to settle before triggering updates - const debouncedListeners = useDebounce(state.callListeners, 100) - const cancellations = useRef<{ blockNumber: number; cancellations: (() => void)[] }>() + const performUpdate = useCallback(async () => { + if (!chainId || !latestBlockNumber || !contract || isUpdating) return - const listeningKeys: { [callKey: string]: number } = useMemo(() => { - return activeListeningKeys(debouncedListeners, chainId) - }, [debouncedListeners, chainId]) + const outdatedCallKeys = getUpdateKeys() + if (outdatedCallKeys.length === 0) return - const serializedOutdatedCallKeys = useMemo(() => { - const outdatedCallKeys = outdatedListeningKeys(state.callResults, listeningKeys, chainId, latestBlockNumber) - return JSON.stringify(outdatedCallKeys.sort()) - }, [chainId, state.callResults, listeningKeys, latestBlockNumber]) + const now = Date.now() + if (now - lastUpdateRef.current < POLLING_INTERVAL) return + + setIsUpdating(true) + lastUpdateRef.current = now - useEffect(() => { - if (!latestBlockNumber || !chainId || !contract) return + try { + const calls = outdatedCallKeys + .filter(key => { + const errorCount = errorCountRef.current[key] || 0 + return errorCount < 3 && !pendingUpdatesRef.current.has(key) + }) + .map(key => parseCallKey(key)) - const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys) - if (outdatedCallKeys.length === 0) return - const calls = outdatedCallKeys.map((key) => parseCallKey(key)) + if (calls.length === 0) return - const chunkedCalls = chunkCalls(calls, CHUNK_GAS_LIMIT) + calls.forEach(call => { + pendingUpdatesRef.current.add(toCallKey(call)) + }) - if (cancellations.current && cancellations.current.blockNumber !== latestBlockNumber) { - cancellations.current.cancellations.forEach((c) => c()) - } + dispatch( + actions.fetchingMulticallResults({ + calls, + chainId, + fetchingBlockNumber: latestBlockNumber, + }) + ) - dispatch( - actions.fetchingMulticallResults({ - calls, - chainId, - fetchingBlockNumber: latestBlockNumber, - }) - ) + const chunks = chunkCalls(calls, MAX_CHUNK_SIZE) + await Promise.all( + chunks.map(async chunk => { + try { + const result = await fetchChunk(contract, chunk, latestBlockNumber, isDebug) + + const { results } = chunk.reduce<{ results: { [callKey: string]: string | null } }>( + (memo, call, i) => { + const key = toCallKey(call) + memo.results[key] = result[i].success ? result[i].returnData : null + pendingUpdatesRef.current.delete(key) + errorCountRef.current[key] = 0 + return memo + }, + { results: {} } + ) + + dispatch( + actions.updateMulticallResults({ + chainId, + results, + blockNumber: latestBlockNumber, + }) + ) + } catch (error) { + console.error('Failed to fetch chunk:', error) + chunk.forEach(call => { + const key = toCallKey(call) + pendingUpdatesRef.current.delete(key) + errorCountRef.current[key] = (errorCountRef.current[key] || 0) + 1 + }) + } + }) + ) + } finally { + setIsUpdating(false) + } + }, [chainId, latestBlockNumber, contract, isUpdating, getUpdateKeys, dispatch, actions, isDebug]) - const fetchChunkContext = { - actions, - dispatch, - chainId, - latestBlockNumber, - isDebug, + useEffect(() => { + const interval = setInterval(performUpdate, POLLING_INTERVAL) + return () => { + clearInterval(interval) + setIsUpdating(false) + pendingUpdatesRef.current.clear() + errorCountRef.current = {} } - // Execute fetches and gather cancellation callbacks - const newCancellations = chunkedCalls.map((chunk) => { - const { cancel, promise } = retry( - () => fetchChunk(contract, chunk, latestBlockNumber, isDebug), - FETCH_RETRY_CONFIG - ) - promise - .then((result) => onFetchChunkSuccess(fetchChunkContext, chunk, result)) - .catch((error) => onFetchChunkFailure(fetchChunkContext, chunk, error)) - return cancel - }) + }, [performUpdate]) - cancellations.current = { - blockNumber: latestBlockNumber, - cancellations: newCancellations, + useEffect(() => { + if (chainId && listenerOptions) { + dispatch(actions.updateListenerOptions({ chainId, listenerOptions })) } - }, [actions, chainId, contract, dispatch, serializedOutdatedCallKeys, latestBlockNumber, isDebug]) + }, [chainId, listenerOptions, actions, dispatch]) return null } +export const MemoizedUpdater = React.memo(Updater) + export function createUpdater(context: MulticallContext) { const UpdaterContextBound = (props: Omit) => { - return + return } return UpdaterContextBound } diff --git a/apps/haust-dex/src/redux-multicall/utils/chunkCalls.ts b/apps/haust-dex/src/redux-multicall/utils/chunkCalls.ts index 88e306eee68..73e3e176676 100644 --- a/apps/haust-dex/src/redux-multicall/utils/chunkCalls.ts +++ b/apps/haust-dex/src/redux-multicall/utils/chunkCalls.ts @@ -1,43 +1,38 @@ -import { DEFAULT_CHUNK_GAS_REQUIRED } from '../constants' +import { + CHUNK_GAS_LIMIT, + DEFAULT_CALL_GAS_REQUIRED, + MAX_CHUNK_SIZE, +} from "../constants"; +import type { Call } from "../types"; -interface Bin { - calls: T[] - cumulativeGasLimit: number -} +export default function chunkCalls( + calls: Call[], + gasLimit = CHUNK_GAS_LIMIT +): Call[][] { + const chunks: Call[][] = []; + let currentChunk: Call[] = []; + let currentChunkGasLimit = 0; + + calls.forEach((call) => { + const gasRequired = call.gasRequired ?? DEFAULT_CALL_GAS_REQUIRED; + + // Split if either gas limit is reached or max chunk size + if ( + currentChunkGasLimit + gasRequired > gasLimit || + currentChunk.length >= MAX_CHUNK_SIZE + ) { + chunks.push(currentChunk); + currentChunk = []; + currentChunkGasLimit = 0; + } + + currentChunk.push(call); + currentChunkGasLimit += gasRequired; + }); + + if (currentChunk.length > 0) { + chunks.push(currentChunk); + } -/** - * Tries to pack a list of items into as few bins as possible using the first-fit bin packing algorithm - * @param calls the calls to chunk - * @param chunkGasLimit the gas limit of any one chunk of calls, i.e. bin capacity - * @param defaultGasRequired the default amount of gas an individual call should cost if not specified - */ -export default function chunkCalls( - calls: T[], - chunkGasLimit: number, - defaultGasRequired: number = DEFAULT_CHUNK_GAS_REQUIRED -): T[][] { - return ( - calls - // first sort by gas required - .sort((c1, c2) => (c2.gasRequired ?? defaultGasRequired) - (c1.gasRequired ?? defaultGasRequired)) - // then bin the calls according to the first fit algorithm - .reduce[]>((bins, call) => { - const gas = call.gasRequired ?? defaultGasRequired - for (const bin of bins) { - if (bin.cumulativeGasLimit + gas <= chunkGasLimit) { - bin.calls.push(call) - bin.cumulativeGasLimit += gas - return bins - } - } - // didn't find a bin for the call, make a new bin - bins.push({ - calls: [call], - cumulativeGasLimit: gas, - }) - return bins - }, []) - // pull out just the calls from each bin - .map((b) => b.calls) - ) + return chunks; } From a2228ff2e8071a9cbe75adb5170c1ae3519e3af9 Mon Sep 17 00:00:00 2001 From: Daryna Shpankova Date: Mon, 16 Jun 2025 22:00:15 +0200 Subject: [PATCH 2/2] add myr coin --- apps/haust-dex/.env.production | 2 +- apps/haust-dex/codegen.yml | 2 +- .../MiniPortfolio/Pools/hooks.ts | 193 +++++++++++++----- .../MiniPortfolio/Tokens/index.tsx | 3 +- .../components/Tokens/TokenTable/TokenRow.tsx | 2 +- apps/haust-dex/src/constants/lists.ts | 2 +- apps/haust-dex/src/graphql/thegraph/apollo.ts | 4 +- .../src/lib/hooks/useBlockNumber.tsx | 127 ++++++++++-- 8 files changed, 258 insertions(+), 77 deletions(-) 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}