diff --git a/src/hooks/useWeb3.tsx b/src/hooks/useWeb3.tsx index 7cceccc7c..7e48df152 100644 --- a/src/hooks/useWeb3.tsx +++ b/src/hooks/useWeb3.tsx @@ -16,11 +16,17 @@ import { isMiniPay, getMiniPayProvider } from 'utils/minipay' type NetworkSettings = { currentNetwork: string rpcs: { - MAINNET_RPC: string | undefined - FUSE_RPC: string | undefined - CELO_RPC: string | undefined - XDC_RPC: string | undefined + 1: string + 122: string + 42220: string + 50: string } + testedRpcs: Record | null +} + +type RpcCacheEntry = { + rpcs: Record + timestamp: number } const gasSettings = { @@ -32,32 +38,219 @@ const gasSettings = { // 50: { maxFeePerGas: BigNumber.from(12.5e9).toHexString() }, // eip-1559 is only supported on XDC testnet. Last checked 15 november 2025. } +const CHAINLIST_URL = 'https://raw.githubusercontent.com/DefiLlama/chainlist/refs/heads/main/constants/extraRpcs.js' +const RPC_CACHE_KEY = 'GD_RPC_CACHE' +const CACHE_DURATION_MS = 24 * 60 * 60 * 1000 // 24 hours +const RPC_TEST_TIMEOUT_MS = 5000 + +let rpcInitializationPromise: Promise> | null = null + +async function testRpc(rpcUrl: string): Promise { + try { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), RPC_TEST_TIMEOUT_MS) + + const response = await fetch(rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + id: 1, + }), + signal: controller.signal, + }) + + clearTimeout(timeoutId) + + if (!response.ok) return false + + const data = await response.json() + return !data.error && (data.result !== undefined || data.result !== null) + } catch { + return false + } +} + +async function fetchAndTestRpcs(): Promise> { + const rpcsByChain: Record = { + MAINNET_RPC: [], + FUSE_RPC: [], + CELO_RPC: [], + XDC_RPC: [], + } + + try { + console.log('[fetchAndTestRpcs] Starting RPC fetch and test...') + const response = await fetch(CHAINLIST_URL) + if (!response.ok) throw new Error('Failed to fetch chainlist') + + const text = await response.text() + console.log('[fetchAndTestRpcs] Chainlist fetched, parsing extraRpcs...') + // Parse "export const extraRpcs" from the JS file + const match = text.match(/export\s+const\s+extraRpcs\s*=\s*(\{[\s\S]*?\n\})/m) + if (!match) throw new Error('Could not parse extraRpcs from chainlist') + + // Create a mock privacyStatement object for eval context + const privacyStatement = {} + + // Safe evaluation of the RPC object with privacyStatement in scope + const extraRpcs = eval( + `(function() { const privacyStatement = ${JSON.stringify(privacyStatement)}; return ${match[1]}; })()` + ) + console.log('[fetchAndTestRpcs] Successfully parsed extraRpcs', extraRpcs) + + // Map chainlist chain IDs to our RPC keys + const chainMapping: Record = { + 1: 'MAINNET_RPC', + 122: 'FUSE_RPC', + 42220: 'CELO_RPC', + 50: 'XDC_RPC', + } + + // Test RPCs for each chain + for (const [chainId] of Object.entries(chainMapping)) { + const chainIdNum = Number(chainId) + console.log(`[fetchAndTestRpcs] Processing chain ${chainIdNum}...`) + + const chainRpcsData = extraRpcs[chainIdNum] || { rpcs: [] } + + // Handle both old format (array) and new format (object with rpcs property) + const chainRpcs = Array.isArray(chainRpcsData) ? chainRpcsData : chainRpcsData.rpcs || [] + console.log(`[fetchAndTestRpcs] Found ${chainRpcs.length} RPC entries for ${chainId}`) + + if (Array.isArray(chainRpcs)) { + // Extract URLs and filter out WebSocket protocols + const rpcUrlsToTest = chainRpcs + .map((rpcEntry) => { + if (typeof rpcEntry === 'string') { + return rpcEntry + } + if (typeof rpcEntry === 'object' && rpcEntry !== null && 'url' in rpcEntry) { + return rpcEntry.url + } + return null + }) + .filter((url): url is string => url !== null && !url.startsWith('wss://')) + + console.log( + `[fetchAndTestRpcs] Testing ${rpcUrlsToTest.length} HTTP(S) RPCs for ${chainId}:`, + rpcUrlsToTest + ) + + // Test all RPCs in parallel + const testResults = await Promise.all( + rpcUrlsToTest.slice(0, 10).map(async (rpcUrl) => ({ + rpcUrl, + isValid: await testRpc(rpcUrl), + })) + ) + + // Log individual test results + testResults.forEach((result) => { + console.log(`[fetchAndTestRpcs] ${result.rpcUrl}: ${result.isValid ? '✓ VALID' : '✗ INVALID'}`) + }) + + // Collect valid RPCs + const validRpcs = testResults.filter((result) => result.isValid).map((result) => result.rpcUrl) + rpcsByChain[chainId] = validRpcs + console.log(`[fetchAndTestRpcs] ${chainId} has ${validRpcs.length} valid RPCs`) + } + } + console.log('[fetchAndTestRpcs] RPC testing complete:', rpcsByChain) + } catch (error) { + console.warn('[fetchAndTestRpcs] Error during RPC fetch/test:', error) + } + + return rpcsByChain +} + +async function getRpcCache(): Promise | null> { + try { + const cached = await AsyncStorage.getItem(RPC_CACHE_KEY) + if (!cached) return null + + const cacheEntry: RpcCacheEntry = JSON.parse(cached) + const isExpired = Date.now() - cacheEntry.timestamp > CACHE_DURATION_MS + + if (isExpired) { + await AsyncStorage.removeItem(RPC_CACHE_KEY) + return null + } + + return cacheEntry.rpcs + } catch { + return null + } +} + +async function setRpcCache(rpcs: Record): Promise { + try { + const cacheEntry: RpcCacheEntry = { + rpcs, + timestamp: Date.now(), + } + await AsyncStorage.setItem(RPC_CACHE_KEY, JSON.stringify(cacheEntry)) + } catch (error) { + console.warn('Failed to cache RPCs:', error) + } +} + +export const initializeRpcs = async () => { + // Return existing promise if already in progress + if (rpcInitializationPromise) { + return rpcInitializationPromise + } + + // Create initialization promise + rpcInitializationPromise = (async () => { + // Try to get cached RPCs first + let cachedRpcs = await getRpcCache() + + if (!cachedRpcs) { + // Fetch and test RPCs if cache miss or expired + cachedRpcs = await fetchAndTestRpcs() + if (Object.values(cachedRpcs).some((arr) => arr.length > 0)) { + await setRpcCache(cachedRpcs) + } + } + return cachedRpcs + })() + + return rpcInitializationPromise +} + export function useNetwork(): NetworkSettings { - const celoRpcList = sample(process.env.REACT_APP_CELO_RPC?.split(',')) ?? '' + const [testifiedRpcs, setTestifiedRpcs] = React.useState | null>(null) + + const celoRpcList = sample(process.env.REACT_APP_CELO_RPC?.split(',')) ?? 'https://forno.celo.org' const fuseRpcList = sample(process.env.REACT_APP_FUSE_RPC?.split(',')) ?? 'https://rpc.fuse.io' const xdcRpcList = sample(process.env.REACT_APP_XDC_RPC?.split(',')) ?? 'https://rpc.xinfin.network' - const mainnetList = sample(['https://eth.llamarpc.com', 'https://1rpc.io/eth']) - const [currentNetwork, rpcs] = useMemo( - () => [ - process.env.REACT_APP_NETWORK || 'fuse', - { - MAINNET_RPC: - mainnetList || - process.env.REACT_APP_MAINNET_RPC || - (ethers.getDefaultProvider('mainnet') as any).providerConfigs[0].provider.connection.url, - FUSE_RPC: fuseRpcList || 'https://rpc.fuse.io', - CELO_RPC: celoRpcList || 'https://forno.celo.org', - XDC_RPC: xdcRpcList, - }, - ], - [] - ) + const mainnetList = sample(['https://eth.llamarpc.com', 'https://1rpc.io/eth']) ?? 'https://eth.llamarpc.com' + + const [currentNetwork, rpcs] = useMemo(() => { + const selectedRpcs = { + 1: sample(testifiedRpcs?.['1'] || []) || mainnetList, + 122: sample(testifiedRpcs?.['122'] || []) || fuseRpcList, + 42220: sample(testifiedRpcs?.['42220'] || []) || celoRpcList, + 50: sample(testifiedRpcs?.['50'] || []) || xdcRpcList, + } + + return [process.env.REACT_APP_NETWORK || 'fuse', selectedRpcs] + }, [testifiedRpcs, mainnetList, fuseRpcList, celoRpcList, xdcRpcList]) useEffect(() => { - AsyncStorage.safeSet('GD_RPCS', rpcs) + void initializeRpcs().then((rpcs) => { + setTestifiedRpcs(rpcs) + }) }, []) - return { currentNetwork, rpcs } + useEffect(() => { + AsyncStorage.safeSet('GD_RPCS', rpcs) + }, [rpcs]) + + return { currentNetwork, rpcs, testedRpcs: testifiedRpcs } } export function Web3ContextProvider({ children }: { children: ReactNode | ReactNodeArray }): JSX.Element { @@ -112,12 +305,16 @@ export function Web3ContextProvider({ children }: { children: ReactNode | ReactN const contractsEnv = network const contractsEnvV2 = network === 'development' ? 'fuse' : network + if (!rpcs) return <> return ( {children} diff --git a/src/reown/reownprovider.tsx b/src/reown/reownprovider.tsx index 81bf2cf4f..4b97ff407 100644 --- a/src/reown/reownprovider.tsx +++ b/src/reown/reownprovider.tsx @@ -1,9 +1,9 @@ -import React from 'react' +import React, { useEffect } from 'react' import { createAppKit } from '@reown/appkit/react' import type { AppKitNetwork } from '@reown/appkit-common' -import { defineChain } from 'viem' +import { defineChain, http } from 'viem' -import { WagmiProvider } from 'wagmi' +import { fallback, WagmiProvider } from 'wagmi' import { celo, fuse, mainnet } from '@reown/appkit/networks' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { WagmiAdapter } from '@reown/appkit-adapter-wagmi' @@ -11,16 +11,17 @@ import { injected, coinbaseWallet } from 'wagmi/connectors' import { APPKIT_FEATURED_WALLET_IDS, APPKIT_SOCIAL_PROVIDER_IDS } from 'utils/walletConfig' import { SupportedChains } from '@gooddollar/web3sdk-v2' import { getEnv } from 'utils/env' -import { sample } from 'lodash' +import { set } from 'lodash' import { getMiniPayProvider } from 'utils/minipay' import { miniPayConnector } from './minipayConnector' +import { useNetwork } from 'hooks/useWeb3' const queryClient = new QueryClient() -const projectId = process.env.REOWN_PROJECT_ID -if (!projectId) { - throw new Error('REOWN_PROJECT_ID environment variable is required') -} +const projectId = process.env.REOWN_PROJECT_ID || '' +// if (!projectId) { +// throw new Error('REOWN_PROJECT_ID environment variable is required') +// } const metadata = { name: 'GoodProtocolUI', @@ -74,7 +75,7 @@ const getAllowedNetworks = (): SupportedChains[] => { } const createXdcNetwork = (): AppKitNetwork => { - const xdcRpc = sample(process.env.REACT_APP_XDC_RPC?.split(',')) ?? 'https://rpc.xdc.network' + const xdcRpcs = process.env.REACT_APP_XDC_RPC?.split(',') ?? ['https://rpc.xdc.network'] return defineChain({ id: 50, name: 'XDC Network', @@ -85,7 +86,7 @@ const createXdcNetwork = (): AppKitNetwork => { }, rpcUrls: { default: { - http: [xdcRpc], + http: xdcRpcs, }, }, blockExplorers: { @@ -146,26 +147,50 @@ const baseConnectors = [ const connectors = typeof window !== 'undefined' && getMiniPayProvider() ? [miniPayConnector(), ...baseConnectors] : baseConnectors -const wagmiAdapter = new WagmiAdapter({ - networks, - projectId, - ssr: true, - connectors, -}) - -createAppKit({ - adapters: [wagmiAdapter], - networks, - projectId, - metadata, - features: { - analytics: true, - socials: APPKIT_SOCIAL_PROVIDER_IDS as any, - }, - featuredWalletIds: [...APPKIT_FEATURED_WALLET_IDS], -}) - +let wagmiAdapter: WagmiAdapter = null as any export function AppKitProvider({ children }: { children: React.ReactNode }) { + const [initialized, setInitialized] = React.useState(false) + const { testedRpcs } = useNetwork() + useEffect(() => { + if (testedRpcs === null) return + console.log("initializing Reown's AppKitProvider with tested RPCs:", testedRpcs) + const transports = {} + networks.map((network) => { + const rpcUrls = testedRpcs[network.id] + if (rpcUrls) { + set(network, 'rpcUrls.default.http', rpcUrls) + console.log(`Reown: Updated RPC for ${network.name} to ${rpcUrls}`) + transports[network.id] = fallback(rpcUrls.map((_) => http(_))) + // network.rpcUrls = updatedNetwork.rpcUrls + } + }) + console.log('reown networks:', networks) + wagmiAdapter = new WagmiAdapter({ + networks, + projectId, + ssr: true, + connectors, + transports, + }) + + console.log(`wagmiAdapter.wagmiConfig:`, wagmiAdapter.wagmiConfig) + createAppKit({ + adapters: [wagmiAdapter], + networks, + projectId, + metadata, + features: { + analytics: true, + socials: APPKIT_SOCIAL_PROVIDER_IDS as any, + }, + featuredWalletIds: [...APPKIT_FEATURED_WALLET_IDS], + }) + setInitialized(true) + }, [testedRpcs === null]) + + if (initialized === false) { + return null + } return ( {children}