-
Notifications
You must be signed in to change notification settings - Fork 22
add: verify rpcs and fallback for reown/viem #617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string, string[]> | null | ||
| } | ||
|
|
||
| type RpcCacheEntry = { | ||
| rpcs: Record<string, string[]> | ||
| 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<Record<string, string[]>> | null = null | ||
|
|
||
| async function testRpc(rpcUrl: string): Promise<boolean> { | ||
| 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<Record<string, string[]>> { | ||
| const rpcsByChain: Record<string, string[]> = { | ||
| 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( | ||
|
Comment on lines
+92
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 issue (security): Using This downloads JS from GitHub and |
||
| `(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<number, string> = { | ||
| 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<Record<string, string[]> | 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<string, string[]>): Promise<void> { | ||
| 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<Record<string, string[]> | 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 ( | ||
| <GdSdkContext.Provider | ||
| value={{ | ||
| web3: web3, | ||
| contractsEnv, | ||
| rpcs: rpcs, | ||
| rpcs: { | ||
| MAINNET_RPC: rpcs['1'], | ||
| FUSE_RPC: rpcs['122'], | ||
| }, | ||
| }} | ||
| > | ||
| <Web3Provider | ||
|
|
@@ -127,12 +324,7 @@ export function Web3ContextProvider({ children }: { children: ReactNode | ReactN | |
| pollingInterval: 15000, | ||
| networks: [Mainnet, Fuse, Celo, Xdc], | ||
| readOnlyChainId: undefined, | ||
| readOnlyUrls: { | ||
| 1: sample(process.env.REACT_APP_MAINNET_RPC?.split(',')) ?? 'https://eth.llamarpc.com', | ||
| 122: sample(process.env.REACT_APP_FUSE_RPC?.split(',')) || 'https://rpc.fuse.io', | ||
| 42220: sample(process.env.REACT_APP_CELO_RPC?.split(',')) || 'https://forno.celo.org', | ||
| 50: sample(process.env.REACT_APP_XDC_RPC?.split(',')) || 'https://rpc.xinfin.network', | ||
| }, | ||
| readOnlyUrls: rpcs, | ||
| }} | ||
| > | ||
| {children} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Initial RPC cache keys don’t match the numeric chain-id keys used later, which makes the object shape confusing and potentially error-prone.
You’re initializing
rpcsByChainwith semantic keys (MAINNET_RPC,FUSE_RPC, etc.), but later you assign values using numeric string keys (rpcsByChain[chainId] = validRpcs, wherechainIdis'1' | '122' | ...). This mixes two key styles in the same object, while only the numeric keys are actually used (e.g. inuseNetwork). Consider initializingrpcsByChaindirectly with numeric keys to match its usage and return type, and avoid misleading, unused keys.Suggested implementation:
If any code relies on the semantic keys (e.g.
rpcsByChain.MAINNET_RPC), it should be updated to use numeric string chain-ids instead (e.g.rpcsByChain['1']). Ensure that all assignments use numeric keys consistently:rpcsByChain[chainId] = validRpcs.