From fa78399832ecb7800ad33651113c6c44be229943 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 08:26:50 +0000 Subject: [PATCH 1/5] Initial plan From 6ace874cd4ba22bc74045b5b29b1a87f4ce156e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 08:44:08 +0000 Subject: [PATCH 2/5] test: cover chainlist rpc parsing and fallback helpers Agent-Logs-Url: https://github.com/GoodDollar/GoodProtocolUI/sessions/cd79a809-ceb1-4685-8196-2855b4e01243 --- src/hooks/rpcParsing.test.ts | 20 ++++++ src/hooks/rpcParsing.ts | 116 +++++++++++++++++++++++++++++++++++ src/hooks/useWeb3.tsx | 28 ++++----- 3 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 src/hooks/rpcParsing.test.ts create mode 100644 src/hooks/rpcParsing.ts diff --git a/src/hooks/rpcParsing.test.ts b/src/hooks/rpcParsing.test.ts new file mode 100644 index 000000000..e2d614835 --- /dev/null +++ b/src/hooks/rpcParsing.test.ts @@ -0,0 +1,20 @@ +/* eslint-env jest */ +import { parseExtraRpcsFromChainlist } from './rpcParsing' + +const CHAINLIST_URL = 'https://raw.githubusercontent.com/DefiLlama/chainlist/refs/heads/main/constants/extraRpcs.js' + +describe('rpcParsing', () => { + it('parses extraRpcs from DefiLlama chainlist source', async () => { + const response = await fetch(CHAINLIST_URL) + expect(response.ok).toBe(true) + + const content = await response.text() + const extraRpcs = parseExtraRpcsFromChainlist(content) + + expect(extraRpcs).toBeDefined() + expect(extraRpcs[1]).toBeDefined() + expect(extraRpcs[122]).toBeDefined() + expect(extraRpcs[42220]).toBeDefined() + expect(extraRpcs[50]).toBeDefined() + }, 30000) +}) diff --git a/src/hooks/rpcParsing.ts b/src/hooks/rpcParsing.ts new file mode 100644 index 000000000..120f2a563 --- /dev/null +++ b/src/hooks/rpcParsing.ts @@ -0,0 +1,116 @@ +export const FALLBACK_RPCS_BY_CHAIN: Record = { + '1': ['https://eth.llamarpc.com', 'https://1rpc.io/eth'], + '122': ['https://rpc.fuse.io'], + '42220': ['https://forno.celo.org'], + '50': ['https://rpc.xinfin.network'], +} + +export function parseExtraRpcsFromChainlist(chainlistContent: string): Record { + const declarationIndex = chainlistContent.indexOf('export const extraRpcs') + if (declarationIndex < 0) { + throw new Error('Could not find extraRpcs export in chainlist') + } + + const objectStart = chainlistContent.indexOf('{', declarationIndex) + if (objectStart < 0) { + throw new Error('Could not parse extraRpcs from chainlist') + } + + let depth = 0 + let inSingleQuote = false + let inDoubleQuote = false + let inTemplate = false + let inLineComment = false + let inBlockComment = false + let isEscaped = false + let objectEnd = -1 + + for (let i = objectStart; i < chainlistContent.length; i++) { + const char = chainlistContent[i] + const next = chainlistContent[i + 1] + + if (inLineComment) { + if (char === '\n') inLineComment = false + continue + } + + if (inBlockComment) { + if (char === '*' && next === '/') { + inBlockComment = false + i++ + } + continue + } + + if (inSingleQuote) { + if (!isEscaped && char === "'") inSingleQuote = false + isEscaped = !isEscaped && char === '\\' + continue + } + + if (inDoubleQuote) { + if (!isEscaped && char === '"') inDoubleQuote = false + isEscaped = !isEscaped && char === '\\' + continue + } + + if (inTemplate) { + if (!isEscaped && char === '`') inTemplate = false + isEscaped = !isEscaped && char === '\\' + continue + } + + if (char === '/' && next === '/') { + inLineComment = true + i++ + continue + } + + if (char === '/' && next === '*') { + inBlockComment = true + i++ + continue + } + + if (char === "'") { + inSingleQuote = true + isEscaped = false + continue + } + + if (char === '"') { + inDoubleQuote = true + isEscaped = false + continue + } + + if (char === '`') { + inTemplate = true + isEscaped = false + continue + } + + if (char === '{') { + depth++ + } else if (char === '}') { + depth-- + if (depth === 0) { + objectEnd = i + break + } + } + } + + if (objectEnd < 0) { + throw new Error('Could not parse extraRpcs object boundaries from chainlist') + } + + const extraRpcsObjectLiteral = chainlistContent.slice(objectStart, objectEnd + 1) + const privacyStatement = {} + + return eval( + `(function() { const privacyStatement = ${JSON.stringify( + privacyStatement + )}; return ${extraRpcsObjectLiteral}; })()` + ) as Record +} diff --git a/src/hooks/useWeb3.tsx b/src/hooks/useWeb3.tsx index aa31c7ca0..3346ce98e 100644 --- a/src/hooks/useWeb3.tsx +++ b/src/hooks/useWeb3.tsx @@ -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, parseExtraRpcsFromChainlist } from './rpcParsing' import { getEnv } from 'utils/env' import { isMiniPay, getMiniPayProvider } from 'utils/minipay' @@ -79,17 +80,7 @@ async function fetchAndTestRpcs(): Promise> { 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]}; })()` - ) + const extraRpcs = parseExtraRpcsFromChainlist(text) console.log('[fetchAndTestRpcs] Successfully parsed extraRpcs', extraRpcs) // Map chainlist chain IDs to our RPC keys @@ -149,9 +140,16 @@ async function fetchAndTestRpcs(): Promise> { console.log(`[fetchAndTestRpcs] ${chainId} has ${validRpcs.length} valid RPCs`) } } + + for (const [chainId, fallbackRpcs] of Object.entries(FALLBACK_RPCS_BY_CHAIN)) { + if (!rpcsByChain[chainId]?.length) { + rpcsByChain[chainId] = fallbackRpcs + } + } console.log('[fetchAndTestRpcs] RPC testing complete:', rpcsByChain) } catch (error) { console.warn('[fetchAndTestRpcs] Error during RPC fetch/test:', error) + return FALLBACK_RPCS_BY_CHAIN } return rpcsByChain @@ -236,10 +234,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 = { From b82e8fb04fbc8eb6c1483a84ae76d135c3f4c3b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 09:01:51 +0000 Subject: [PATCH 3/5] fix: harden rpc parser and remove eval usage Agent-Logs-Url: https://github.com/GoodDollar/GoodProtocolUI/sessions/cd79a809-ceb1-4685-8196-2855b4e01243 --- src/hooks/rpcParsing.test.ts | 11 ++-- src/hooks/rpcParsing.ts | 115 +++++++++++++++++++++++++++-------- src/hooks/useWeb3.tsx | 17 +----- 3 files changed, 97 insertions(+), 46 deletions(-) diff --git a/src/hooks/rpcParsing.test.ts b/src/hooks/rpcParsing.test.ts index e2d614835..e9c38af98 100644 --- a/src/hooks/rpcParsing.test.ts +++ b/src/hooks/rpcParsing.test.ts @@ -12,9 +12,10 @@ describe('rpcParsing', () => { const extraRpcs = parseExtraRpcsFromChainlist(content) expect(extraRpcs).toBeDefined() - expect(extraRpcs[1]).toBeDefined() - expect(extraRpcs[122]).toBeDefined() - expect(extraRpcs[42220]).toBeDefined() - expect(extraRpcs[50]).toBeDefined() - }, 30000) + ;[1, 122, 42220, 50].forEach((chainId) => { + expect(extraRpcs[chainId]).toBeDefined() + expect(extraRpcs[chainId].length).toBeGreaterThan(0) + expect(extraRpcs[chainId].every((url) => /^https?:\/\//.test(url))).toBe(true) + }) + }, 10000) }) diff --git a/src/hooks/rpcParsing.ts b/src/hooks/rpcParsing.ts index 120f2a563..d50007bcd 100644 --- a/src/hooks/rpcParsing.ts +++ b/src/hooks/rpcParsing.ts @@ -5,17 +5,9 @@ export const FALLBACK_RPCS_BY_CHAIN: Record = { '50': ['https://rpc.xinfin.network'], } -export function parseExtraRpcsFromChainlist(chainlistContent: string): Record { - const declarationIndex = chainlistContent.indexOf('export const extraRpcs') - if (declarationIndex < 0) { - throw new Error('Could not find extraRpcs export in chainlist') - } - - const objectStart = chainlistContent.indexOf('{', declarationIndex) - if (objectStart < 0) { - throw new Error('Could not parse extraRpcs from chainlist') - } +const TARGET_CHAIN_IDS = ['1', '122', '42220', '50'] as const +function findBalancedSegment(source: string, startIndex: number, openChar: string, closeChar: string): string | null { let depth = 0 let inSingleQuote = false let inDoubleQuote = false @@ -23,11 +15,10 @@ export function parseExtraRpcsFromChainlist(chainlistContent: string): Record() + const urlRegex = /(['"`])(https?:\/\/[\s\S]*?)\1/g + + for (const match of arrayLiteral.matchAll(urlRegex)) { + const candidate = match[2].trim() + try { + const parsed = new URL(candidate) + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { + urls.add(candidate) } + } catch { + // ignore malformed URL literals } } - if (objectEnd < 0) { + return [...urls] +} + +function extractExtraRpcsObjectLiteral(chainlistContent: string): string { + const declarationIndex = chainlistContent.indexOf('export const extraRpcs') + if (declarationIndex < 0) { + throw new Error('Could not find extraRpcs export in chainlist') + } + + const objectStart = chainlistContent.indexOf('{', declarationIndex) + if (objectStart < 0) { + throw new Error('Could not parse extraRpcs from chainlist') + } + + const objectLiteral = findBalancedSegment(chainlistContent, objectStart, '{', '}') + if (!objectLiteral) { throw new Error('Could not parse extraRpcs object boundaries from chainlist') } - const extraRpcsObjectLiteral = chainlistContent.slice(objectStart, objectEnd + 1) - const privacyStatement = {} + return objectLiteral +} + +function extractChainRpcArrayLiteral(extraRpcsObjectLiteral: string, chainId: string): string | null { + const escapedChainId = chainId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const chainPattern = new RegExp(`(?:^|[,{\\n])\\s*["']?${escapedChainId}["']?\\s*:`, 'm') + const chainMatch = chainPattern.exec(extraRpcsObjectLiteral) + if (!chainMatch) return null + + let valueStart = chainMatch.index + chainMatch[0].length + while (/\s/.test(extraRpcsObjectLiteral[valueStart] ?? '')) { + valueStart++ + } + + if (extraRpcsObjectLiteral[valueStart] === '[') { + return findBalancedSegment(extraRpcsObjectLiteral, valueStart, '[', ']') + } + + if (extraRpcsObjectLiteral[valueStart] === '{') { + const chainObjectLiteral = findBalancedSegment(extraRpcsObjectLiteral, valueStart, '{', '}') + if (!chainObjectLiteral) return null + + const rpcsMatch = /\brpcs\s*:\s*/.exec(chainObjectLiteral) + if (!rpcsMatch) return null + + let rpcsStart = rpcsMatch.index + rpcsMatch[0].length + while (/\s/.test(chainObjectLiteral[rpcsStart] ?? '')) { + rpcsStart++ + } + + if (chainObjectLiteral[rpcsStart] !== '[') return null + return findBalancedSegment(chainObjectLiteral, rpcsStart, '[', ']') + } + + return null +} + +export function parseExtraRpcsFromChainlist(chainlistContent: string): Record { + const extraRpcsObjectLiteral = extractExtraRpcsObjectLiteral(chainlistContent) + + return TARGET_CHAIN_IDS.reduce>((acc, chainId) => { + const rpcArrayLiteral = extractChainRpcArrayLiteral(extraRpcsObjectLiteral, chainId) + if (!rpcArrayLiteral) return acc - return eval( - `(function() { const privacyStatement = ${JSON.stringify( - privacyStatement - )}; return ${extraRpcsObjectLiteral}; })()` - ) as Record + acc[chainId] = extractUrlsFromArrayLiteral(rpcArrayLiteral) + return acc + }, {}) } diff --git a/src/hooks/useWeb3.tsx b/src/hooks/useWeb3.tsx index 3346ce98e..c54d1b2d0 100644 --- a/src/hooks/useWeb3.tsx +++ b/src/hooks/useWeb3.tsx @@ -96,25 +96,12 @@ async function fetchAndTestRpcs(): Promise> { 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 || [] + const chainRpcs = extraRpcs[chainIdNum] || [] 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://')) + const rpcUrlsToTest = chainRpcs.filter((url): url is string => !url.startsWith('wss://')) console.log( `[fetchAndTestRpcs] Testing ${rpcUrlsToTest.length} HTTP(S) RPCs for ${chainId}:`, From 8beaa6ac0c65c932480cf6f142c31499f8572bd5 Mon Sep 17 00:00:00 2001 From: Gutopro Date: Thu, 21 May 2026 11:54:16 +0100 Subject: [PATCH 4/5] fix: simplify fetchAndTestRpcs to use chainid.network JSON, add promise reset on failure --- src/hooks/rpcParsing.test.ts | 33 ++++--- src/hooks/rpcParsing.ts | 179 +++-------------------------------- src/hooks/useWeb3.tsx | 75 +++------------ 3 files changed, 45 insertions(+), 242 deletions(-) diff --git a/src/hooks/rpcParsing.test.ts b/src/hooks/rpcParsing.test.ts index e9c38af98..b6e922bb9 100644 --- a/src/hooks/rpcParsing.test.ts +++ b/src/hooks/rpcParsing.test.ts @@ -1,21 +1,24 @@ /* eslint-env jest */ -import { parseExtraRpcsFromChainlist } from './rpcParsing' - -const CHAINLIST_URL = 'https://raw.githubusercontent.com/DefiLlama/chainlist/refs/heads/main/constants/extraRpcs.js' +import { fetchRpcsFromChainlist, FALLBACK_RPCS_BY_CHAIN } from './rpcParsing' describe('rpcParsing', () => { - it('parses extraRpcs from DefiLlama chainlist source', async () => { - const response = await fetch(CHAINLIST_URL) - expect(response.ok).toBe(true) + it('fetches and parses HTTP(S) RPCs for all required chains from chainlist', async () => { + const extraRpcs = await fetchRpcsFromChainlist() - const content = await response.text() - const extraRpcs = parseExtraRpcsFromChainlist(content) + 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) - expect(extraRpcs).toBeDefined() - ;[1, 122, 42220, 50].forEach((chainId) => { - expect(extraRpcs[chainId]).toBeDefined() - expect(extraRpcs[chainId].length).toBeGreaterThan(0) - expect(extraRpcs[chainId].every((url) => /^https?:\/\//.test(url))).toBe(true) - }) - }, 10000) + 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) + }) + }) }) diff --git a/src/hooks/rpcParsing.ts b/src/hooks/rpcParsing.ts index d50007bcd..7aab9dc89 100644 --- a/src/hooks/rpcParsing.ts +++ b/src/hooks/rpcParsing.ts @@ -5,175 +5,22 @@ export const FALLBACK_RPCS_BY_CHAIN: Record = { '50': ['https://rpc.xinfin.network'], } -const TARGET_CHAIN_IDS = ['1', '122', '42220', '50'] as const +const CHAINLIST_JSON_URL = 'https://chainid.network/chains.json' +const TARGET_CHAIN_IDS = new Set([1, 122, 42220, 50]) -function findBalancedSegment(source: string, startIndex: number, openChar: string, closeChar: string): string | null { - let depth = 0 - let inSingleQuote = false - let inDoubleQuote = false - let inTemplate = false - let inLineComment = false - let inBlockComment = false - let isEscaped = false +export async function fetchRpcsFromChainlist(): Promise> { + const response = await fetch(CHAINLIST_JSON_URL) + if (!response.ok) throw new Error('Failed to fetch chainlist') - for (let i = startIndex; i < source.length; i++) { - const char = source[i] - const next = source[i + 1] + const chains: Array<{ chainId: number; rpc: string[] }> = await response.json() - if (inLineComment) { - if (char === '\n') inLineComment = false - continue - } - - if (inBlockComment) { - if (char === '*' && next === '/') { - inBlockComment = false - i++ - } - continue - } - - if (inSingleQuote) { - if (!isEscaped && char === "'") inSingleQuote = false - isEscaped = !isEscaped && char === '\\' - continue - } - - if (inDoubleQuote) { - if (!isEscaped && char === '"') inDoubleQuote = false - isEscaped = !isEscaped && char === '\\' - continue - } - - if (inTemplate) { - if (!isEscaped && char === '`') inTemplate = false - isEscaped = !isEscaped && char === '\\' - continue - } - - if (char === '/' && next === '/') { - inLineComment = true - i++ - continue - } - - if (char === '/' && next === '*') { - inBlockComment = true - i++ - continue - } - - if (char === "'") { - inSingleQuote = true - isEscaped = false - continue - } - - if (char === '"') { - inDoubleQuote = true - isEscaped = false - continue - } - - if (char === '`') { - inTemplate = true - isEscaped = false - continue - } - - if (char === openChar) { - depth++ - } else if (char === closeChar) { - depth-- - if (depth === 0) { - return source.slice(startIndex, i + 1) - } - } - } - - return null -} - -function extractUrlsFromArrayLiteral(arrayLiteral: string): string[] { - const urls = new Set() - const urlRegex = /(['"`])(https?:\/\/[\s\S]*?)\1/g - - for (const match of arrayLiteral.matchAll(urlRegex)) { - const candidate = match[2].trim() - try { - const parsed = new URL(candidate) - if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { - urls.add(candidate) - } - } catch { - // ignore malformed URL literals + const result: Record = {} + 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 [...urls] -} - -function extractExtraRpcsObjectLiteral(chainlistContent: string): string { - const declarationIndex = chainlistContent.indexOf('export const extraRpcs') - if (declarationIndex < 0) { - throw new Error('Could not find extraRpcs export in chainlist') - } - - const objectStart = chainlistContent.indexOf('{', declarationIndex) - if (objectStart < 0) { - throw new Error('Could not parse extraRpcs from chainlist') - } - - const objectLiteral = findBalancedSegment(chainlistContent, objectStart, '{', '}') - if (!objectLiteral) { - throw new Error('Could not parse extraRpcs object boundaries from chainlist') - } - - return objectLiteral -} - -function extractChainRpcArrayLiteral(extraRpcsObjectLiteral: string, chainId: string): string | null { - const escapedChainId = chainId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const chainPattern = new RegExp(`(?:^|[,{\\n])\\s*["']?${escapedChainId}["']?\\s*:`, 'm') - const chainMatch = chainPattern.exec(extraRpcsObjectLiteral) - if (!chainMatch) return null - - let valueStart = chainMatch.index + chainMatch[0].length - while (/\s/.test(extraRpcsObjectLiteral[valueStart] ?? '')) { - valueStart++ - } - - if (extraRpcsObjectLiteral[valueStart] === '[') { - return findBalancedSegment(extraRpcsObjectLiteral, valueStart, '[', ']') - } - - if (extraRpcsObjectLiteral[valueStart] === '{') { - const chainObjectLiteral = findBalancedSegment(extraRpcsObjectLiteral, valueStart, '{', '}') - if (!chainObjectLiteral) return null - - const rpcsMatch = /\brpcs\s*:\s*/.exec(chainObjectLiteral) - if (!rpcsMatch) return null - - let rpcsStart = rpcsMatch.index + rpcsMatch[0].length - while (/\s/.test(chainObjectLiteral[rpcsStart] ?? '')) { - rpcsStart++ - } - - if (chainObjectLiteral[rpcsStart] !== '[') return null - return findBalancedSegment(chainObjectLiteral, rpcsStart, '[', ']') - } - - return null -} - -export function parseExtraRpcsFromChainlist(chainlistContent: string): Record { - const extraRpcsObjectLiteral = extractExtraRpcsObjectLiteral(chainlistContent) - - return TARGET_CHAIN_IDS.reduce>((acc, chainId) => { - const rpcArrayLiteral = extractChainRpcArrayLiteral(extraRpcsObjectLiteral, chainId) - if (!rpcArrayLiteral) return acc - - acc[chainId] = extractUrlsFromArrayLiteral(rpcArrayLiteral) - return acc - }, {}) + return result } diff --git a/src/hooks/useWeb3.tsx b/src/hooks/useWeb3.tsx index c54d1b2d0..3bea4da45 100644 --- a/src/hooks/useWeb3.tsx +++ b/src/hooks/useWeb3.tsx @@ -10,7 +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, parseExtraRpcsFromChainlist } from './rpcParsing' +import { FALLBACK_RPCS_BY_CHAIN, fetchRpcsFromChainlist } from './rpcParsing' import { getEnv } from 'utils/env' import { isMiniPay, getMiniPayProvider } from 'utils/minipay' @@ -35,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 @@ -74,68 +73,22 @@ async function fetchAndTestRpcs(): Promise> { const rpcsByChain: Record = {} 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...') - const extraRpcs = parseExtraRpcsFromChainlist(text) - 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', + 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] } - - // Test RPCs for each chain - for (const [chainId] of Object.entries(chainMapping)) { - const chainIdNum = Number(chainId) - console.log(`[fetchAndTestRpcs] Processing chain ${chainIdNum}...`) - - const chainRpcs = extraRpcs[chainIdNum] || [] - console.log(`[fetchAndTestRpcs] Found ${chainRpcs.length} RPC entries for ${chainId}`) - - if (Array.isArray(chainRpcs)) { - // Extract URLs and filter out WebSocket protocols - const rpcUrlsToTest = chainRpcs.filter((url): url is string => !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`) - } - } - - for (const [chainId, fallbackRpcs] of Object.entries(FALLBACK_RPCS_BY_CHAIN)) { - if (!rpcsByChain[chainId]?.length) { - rpcsByChain[chainId] = fallbackRpcs - } - } - 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 } From 5f6f98361aa5213c1c5072dc21f3db4e9bd582d6 Mon Sep 17 00:00:00 2001 From: LewisB Date: Mon, 25 May 2026 18:11:14 +0700 Subject: [PATCH 5/5] fix: multicall fails for native balance on ethereum mainnet --- src/components/NetworkModal/index.tsx | 26 ++++++++++--- src/components/WalletModal/Option.tsx | 7 +++- src/components/Web3Status/index.tsx | 54 +++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/components/NetworkModal/index.tsx b/src/components/NetworkModal/index.tsx index 92b98b5ac..56b80583a 100644 --- a/src/components/NetworkModal/index.tsx +++ b/src/components/NetworkModal/index.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' import { SupportedChains, useSwitchNetwork } from '@gooddollar/web3sdk-v2' -import { Text, Link } from 'native-base' +import { Text, Link, Spinner, HStack } from 'native-base' import { SwitchChainModal } from '@gooddollar/good-design' import { ChainId } from '@sushiswap/sdk' import { UnsupportedChainId } from '@gooddollar/web3sdk' @@ -39,11 +39,11 @@ const TextWrapper = styled.div` } ` -const ChainOption = ({ chainId, chain, toggleNetworkModal, switchChain, labels, icons, error }: any) => { +const ChainOption = ({ chainId, chain, switchChain, labels, icons, error, disabled }: any) => { const onOptionClick = useCallback(() => { - toggleNetworkModal() + if (disabled) return switchChain(chain) - }, [switchChain, toggleNetworkModal, chain]) + }, [disabled, switchChain, chain]) const isUnsupported = error instanceof UnsupportedChainId @@ -56,6 +56,7 @@ const ChainOption = ({ chainId, chain, toggleNetworkModal, switchChain, labels, icon={icons[chain]} id={String(chain)} onClick={onOptionClick} + disabled={disabled} /> ) } @@ -72,6 +73,7 @@ export default function NetworkModal(): JSX.Element | null { const networkModalOpen = useModalOpen(ApplicationModal.NETWORK) const toggleNetworkModal = useNetworkModalToggle() const [toAddNetwork, setToAddNetwork] = useState() + const [switchingChain, setSwitchingChain] = useState(false) const networkLabel: string | null = error ? null : (NETWORK_LABEL as any)[+(chainId ?? 42220)] const network = getEnv() @@ -102,17 +104,19 @@ export default function NetworkModal(): JSX.Element | null { const switchChain = useCallback( async (chain: SupportedChains) => { + setSwitchingChain(true) try { + await new Promise((resolve) => setTimeout(resolve, 250)) await switchNetwork(chain) sendData({ event: 'network_switch', action: 'network_switch_success', network: ChainId[chain], }) + toggleNetworkModal() } catch (e: any) { if (e?.code === 4902) { setToAddNetwork(chain) - toggleNetworkModal() return } @@ -124,6 +128,7 @@ export default function NetworkModal(): JSX.Element | null { network: ChainId[chain], }) console.warn('Wallet not initialized. Network preference saved.') + toggleNetworkModal() return } @@ -135,6 +140,8 @@ export default function NetworkModal(): JSX.Element | null { error: e?.message || 'Unknown error', }) + } finally { + setSwitchingChain(false) } }, @@ -174,6 +181,13 @@ export default function NetworkModal(): JSX.Element | null { )} + {switchingChain && ( + + + {i18n._(t`Switching network...`)} + + )} +
{allowedNetworks.map((chain: SupportedChains) => ( ))}
diff --git a/src/components/WalletModal/Option.tsx b/src/components/WalletModal/Option.tsx index 04589a48c..68b79514c 100644 --- a/src/components/WalletModal/Option.tsx +++ b/src/components/WalletModal/Option.tsx @@ -72,6 +72,7 @@ export default function Option({ icon, active = false, id, + disabled = false, }: { clickable?: boolean size?: number | null @@ -81,15 +82,17 @@ export default function Option({ icon: string active?: boolean id: string + disabled?: boolean /** @deprecated */ color?: string }) { const content = (
diff --git a/src/components/Web3Status/index.tsx b/src/components/Web3Status/index.tsx index 8d28fb158..34383d9ad 100644 --- a/src/components/Web3Status/index.tsx +++ b/src/components/Web3Status/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import useENSName from '../../hooks/useENSName' import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks' import { TransactionDetails } from '../../state/transactions/reducer' @@ -12,6 +12,8 @@ import { Text, HStack } from 'native-base' import { useNativeBalance } from '@gooddollar/web3sdk-v2' import { Currency } from '@sushiswap/sdk' import { useAppKitAccount, useAppKitNetwork } from '@reown/appkit/react' +import { useEthers } from '@usedapp/core' +import { Spinner } from 'native-base' // we want the latest one to come first, so return negative if a is after b function newTransactionsFirst(a: TransactionDetails, b: TransactionDetails) { @@ -23,11 +25,54 @@ function Web3StatusInner() { const sendData = useSendAnalyticsData() const { address } = useAppKitAccount() const { chainId } = useAppKitNetwork() + const { library } = useEthers() as any const { ENSName } = useENSName(address ?? undefined) const allTransactions = useAllTransactions() const nativeBalance = useNativeBalance() + const [directNativeBalance, setDirectNativeBalance] = useState() + const [showBalance, setShowBalance] = useState(false) + + // added a timed-out fallback for when native-balance does not seem to complete (happening on mainnet) + // it uses a timeout to avoid mis-formatted amounts based on old-chain decimals. + useEffect(() => { + let cancelled = false + setShowBalance(false) + setDirectNativeBalance(undefined) + const timeoutId = window.setTimeout(() => { + if (!cancelled) { + setShowBalance(true) + } + }, 500) + + async function readBalance() { + if (!address || !library?.getBalance) { + return + } + + try { + const balance = await library.getBalance(address) + if (!cancelled) { + setDirectNativeBalance(balance.toString()) + } + } catch { + if (!cancelled) { + setDirectNativeBalance(undefined) + } + } + } + + void readBalance() + + return () => { + cancelled = true + window.clearTimeout(timeoutId) + } + }, [address, library, /*used*/ chainId]) + + const displayNativeBalance = nativeBalance ?? directNativeBalance + const shouldShowBalance = showBalance && !!displayNativeBalance const sortedRecentTransactions = useMemo(() => { const txs = Object.values(allTransactions) @@ -46,10 +91,13 @@ function Web3StatusInner() { {address && (
- {nativeBalance && ( + {shouldShowBalance ? ( - {parseFloat(nativeBalance).toFixed(4)} {Currency.getNativeCurrencySymbol(+(chainId ?? 1))} + {parseFloat(displayNativeBalance!).toFixed(4)}{' '} + {Currency.getNativeCurrencySymbol(+(chainId ?? 1))} + ) : ( + )} {hasPendingTransactions ? (