diff --git a/src/provider.ts b/src/provider.ts index 319aa73b6..a7bf1e05d 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -461,6 +461,29 @@ export const getOriginHeader = () => { return 'impact-graph-' + SERVICE_NAME || 'unnamed'; }; +// Get fallback RPC URLs for networks that have multiple providers configured +export function getFallbackProviderUrls(networkId: number): string[] { + const fallbackUrls: string[] = []; + + switch (networkId) { + case NETWORK_IDS.XDAI: + // Add all available Gnosis/xDai RPC endpoints + if (process.env.XDAI_NODE_HTTP_URL) { + fallbackUrls.push(process.env.XDAI_NODE_HTTP_URL); + } + if (process.env.XDAI_NODE_HTTP_URL_QUICKNODE) { + fallbackUrls.push(process.env.XDAI_NODE_HTTP_URL_QUICKNODE); + } + // Public fallback endpoints + fallbackUrls.push('https://rpc.gnosischain.com'); + fallbackUrls.push('https://rpc.ankr.com/gnosis'); + break; + // Add more networks with fallback support here as needed + } + + return fallbackUrls; +} + export function getProvider(networkId: number) { let url; let options; @@ -474,7 +497,10 @@ export function getProvider(networkId: number) { url = process.env.ETC_NODE_HTTP_URL as string; break; case NETWORK_IDS.XDAI: - url = process.env.XDAI_NODE_HTTP_URL as string; + url = + process.env.XDAI_NODE_HTTP_URL || + process.env.XDAI_NODE_HTTP_URL_QUICKNODE || + 'https://rpc.gnosischain.com'; break; case NETWORK_IDS.BSC: @@ -561,6 +587,67 @@ export function getProvider(networkId: number) { ); } +// Retry an RPC call with automatic fallback to alternate providers +export async function retryWithFallbackProvider( + networkId: number, + rpcCall: (provider: ethers.providers.JsonRpcProvider) => Promise, + maxRetries = 2, +): Promise { + const fallbackUrls = getFallbackProviderUrls(networkId); + + // If no fallbacks are configured, just use the primary provider + if (fallbackUrls.length === 0) { + const provider = getProvider(networkId); + return await rpcCall(provider); + } + + let lastError: Error | undefined; + + // Try each provider URL in sequence + for (let i = 0; i < fallbackUrls.length && i < maxRetries; i++) { + try { + const provider = new ethers.providers.JsonRpcProvider({ + url: fallbackUrls[i], + headers: { + Origin: getOriginHeader(), + }, + }); + + logger.debug(`Trying RPC provider ${i + 1}/${fallbackUrls.length}`, { + networkId, + url: fallbackUrls[i], + }); + + const result = await rpcCall(provider); + + // If successful and not the first provider, log for monitoring + if (i > 0) { + logger.info(`Successfully used fallback RPC provider #${i + 1}`, { + networkId, + url: fallbackUrls[i], + }); + } + + return result; + } catch (error: any) { + lastError = error; + logger.warn(`RPC provider ${i + 1}/${fallbackUrls.length} failed`, { + networkId, + url: fallbackUrls[i], + error: error.message, + status: error.status, + }); + + // If this is the last attempt, throw the error + if (i === fallbackUrls.length - 1 || i === maxRetries - 1) { + throw error; + } + } + } + + throw lastError || new Error('All RPC providers failed'); +} + export function getBlockExplorerApiUrl(networkId: number): string { let apiUrl; // After the migration to Etherscan V2, we can use the Etherscan API key for all networks diff --git a/src/services/chains/evm/transactionService.ts b/src/services/chains/evm/transactionService.ts index e95c0ef7f..998706eb1 100644 --- a/src/services/chains/evm/transactionService.ts +++ b/src/services/chains/evm/transactionService.ts @@ -15,6 +15,7 @@ import { getBlockExplorerApiUrl, getNetworkNativeToken, getProvider, + retryWithFallbackProvider, NETWORK_IDS, } from '../../../provider'; import { logger } from '../../../utils/logger'; @@ -34,13 +35,15 @@ export async function getEvmTransactionInfoFromNetwork( ): Promise { const { networkId, nonce } = input; - const provider = getProvider(networkId); logger.debug( 'NODE RPC request count - getTransactionInfoFromNetwork provider.getTransactionCount txHash:', input.txHash, ); - const userTransactionsCount = await provider.getTransactionCount( - input.fromAddress, + + // Use retry mechanism for getTransactionCount to handle RPC failures + const userTransactionsCount = await retryWithFallbackProvider( + networkId, + async provider => await provider.getTransactionCount(input.fromAddress), ); if (typeof nonce === 'number' && userTransactionsCount <= nonce) { logger.debug('getTransactionDetail check nonce', {