Skip to content
Open
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
252 changes: 222 additions & 30 deletions src/hooks/useWeb3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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: [],
Comment on lines +77 to +81
Copy link

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 rpcsByChain with semantic keys (MAINNET_RPC, FUSE_RPC, etc.), but later you assign values using numeric string keys (rpcsByChain[chainId] = validRpcs, where chainId is '1' | '122' | ...). This mixes two key styles in the same object, while only the numeric keys are actually used (e.g. in useNetwork). Consider initializing rpcsByChain directly with numeric keys to match its usage and return type, and avoid misleading, unused keys.

Suggested implementation:

async function fetchAndTestRpcs(): Promise<Record<string, string[]>> {
    // Keep keys numeric to match chainId usage (e.g. rpcsByChain[chainId])
    const rpcsByChain: Record<string, string[]> = {}

type RpcCacheEntry = {

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.

}

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Using eval on remote content from GitHub is a security and reliability risk.

This downloads JS from GitHub and evals a substring of it, which is equivalent to executing arbitrary remote code in the client. A repo compromise, MITM, or even a benign upstream change could lead to code execution or breakage. Please replace this with a safer approach (e.g., a JSON endpoint, build-time/server-side processing, or structured parsing) and avoid eval on network data entirely.

`(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 {
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Loading
Loading