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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/hooks/rpcParsing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* eslint-env jest */
import { fetchRpcsFromChainlist, FALLBACK_RPCS_BY_CHAIN } from './rpcParsing'

describe('rpcParsing', () => {
it('fetches and parses HTTP(S) RPCs for all required chains from chainlist', async () => {
const extraRpcs = await fetchRpcsFromChainlist()

expect(extraRpcs).toBeDefined()
;[1, 122, 42220, 50].forEach((chainId) => {
const key = String(chainId)
expect(extraRpcs[key]).toBeDefined()
expect(extraRpcs[key].length).toBeGreaterThan(0)
expect(extraRpcs[key].every((url) => /^https?:\/\//.test(url))).toBe(true)
})
}, 30000)

it('FALLBACK_RPCS_BY_CHAIN covers all required chains with HTTP(S) URLs', () => {
;['1', '122', '42220', '50'].forEach((chainId) => {
expect(FALLBACK_RPCS_BY_CHAIN[chainId]).toBeDefined()
expect(FALLBACK_RPCS_BY_CHAIN[chainId].length).toBeGreaterThan(0)
expect(FALLBACK_RPCS_BY_CHAIN[chainId].every((url) => /^https?:\/\//.test(url))).toBe(true)
})
})
})
26 changes: 26 additions & 0 deletions src/hooks/rpcParsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const FALLBACK_RPCS_BY_CHAIN: Record<string, string[]> = {
'1': ['https://eth.llamarpc.com', 'https://1rpc.io/eth'],
'122': ['https://rpc.fuse.io'],
'42220': ['https://forno.celo.org'],
'50': ['https://rpc.xinfin.network'],
}

const CHAINLIST_JSON_URL = 'https://chainid.network/chains.json'
const TARGET_CHAIN_IDS = new Set([1, 122, 42220, 50])

export async function fetchRpcsFromChainlist(): Promise<Record<string, string[]>> {
const response = await fetch(CHAINLIST_JSON_URL)
if (!response.ok) throw new Error('Failed to fetch chainlist')

const chains: Array<{ chainId: number; rpc: string[] }> = await response.json()

const result: Record<string, string[]> = {}
for (const chain of chains) {
if (TARGET_CHAIN_IDS.has(chain.chainId)) {
result[String(chain.chainId)] = chain.rpc.filter(
(url) => url.startsWith('http://') || url.startsWith('https://')
)
}
}
return result
}
100 changes: 19 additions & 81 deletions src/hooks/useWeb3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useAppKitNetwork, useAppKitProvider } from '@reown/appkit/react'
import type { Provider } from '@reown/appkit/react'
import { useAccount } from 'wagmi'

import { FALLBACK_RPCS_BY_CHAIN, fetchRpcsFromChainlist } from './rpcParsing'
import { getEnv } from 'utils/env'
import { isMiniPay, getMiniPayProvider } from 'utils/minipay'

Expand All @@ -34,7 +35,6 @@ 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
Expand Down Expand Up @@ -73,85 +73,23 @@ async function fetchAndTestRpcs(): Promise<Record<string, string[]>> {
const rpcsByChain: Record<string, string[]> = {}

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<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`)
}
const extraRpcs = await fetchRpcsFromChainlist()

for (const [chainId] of Object.entries(FALLBACK_RPCS_BY_CHAIN)) {
const chainRpcs = extraRpcs[chainId] || []
const testResults = await Promise.all(
chainRpcs.slice(0, 10).map(async (rpcUrl) => ({
rpcUrl,
isValid: await testRpc(rpcUrl),
}))
)
const validRpcs = testResults.filter((r) => r.isValid).map((r) => r.rpcUrl)
rpcsByChain[chainId] = validRpcs.length ? validRpcs : FALLBACK_RPCS_BY_CHAIN[chainId]
}
console.log('[fetchAndTestRpcs] RPC testing complete:', rpcsByChain)
} catch (error) {
console.warn('[fetchAndTestRpcs] Error during RPC fetch/test:', error)
rpcInitializationPromise = null
return FALLBACK_RPCS_BY_CHAIN
}

return rpcsByChain
Expand Down Expand Up @@ -236,10 +174,10 @@ export function useNetwork(): NetworkSettings {
])
)

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']) ?? 'https://eth.llamarpc.com'
const celoRpcList = sample(process.env.REACT_APP_CELO_RPC?.split(',')) ?? FALLBACK_RPCS_BY_CHAIN['42220'][0]
const fuseRpcList = sample(process.env.REACT_APP_FUSE_RPC?.split(',')) ?? FALLBACK_RPCS_BY_CHAIN['122'][0]
const xdcRpcList = sample(process.env.REACT_APP_XDC_RPC?.split(',')) ?? FALLBACK_RPCS_BY_CHAIN['50'][0]
const mainnetList = sample(FALLBACK_RPCS_BY_CHAIN['1']) ?? FALLBACK_RPCS_BY_CHAIN['1'][0]

const [currentNetwork, rpcs] = useMemo(() => {
const selectedRpcs = {
Expand Down
Loading