fix: wallet x-ray RPC failover with 5 endpoints (Alchemy, PublicNode, Ankr, Solana, Helius)

This commit is contained in:
jae 2026-04-18 22:10:10 +00:00
parent 660bb842ec
commit f484cf94b1

View file

@ -6,8 +6,13 @@
'use strict'; 'use strict';
// ── Config ── // ── Config ──
const RPC = 'https://api.mainnet-beta.solana.com'; const RPC_ENDPOINTS = [
const RPC_BACKUP = 'https://rpc.ankr.com/solana'; 'https://solana-mainnet.g.alchemy.com/v2/demo',
'https://solana.publicnode.com',
'https://rpc.ankr.com/solana',
'https://api.mainnet-beta.solana.com',
'https://mainnet.helius-rpc.com/?api-key=1d8740dc-e5f4-421c-b823-e1bad1889eff'
];
const JUP_PRICE = 'https://price.jup.ag/v6/price'; const JUP_PRICE = 'https://price.jup.ag/v6/price';
const JUP_TOKEN_LIST = 'https://token.jup.ag/all'; const JUP_TOKEN_LIST = 'https://token.jup.ag/all';
const SOLSCAN_TX = 'https://solscan.io/tx/'; const SOLSCAN_TX = 'https://solscan.io/tx/';
@ -30,7 +35,8 @@
// ── State ── // ── State ──
let tokenListCache = null; let tokenListCache = null;
let tokenMapCache = null; // mint → {name, symbol, logoURI, decimals} let tokenMapCache = null; // mint → {name, symbol, logoURI, decimals}
let activeRpc = RPC; let activeRpc = RPC_ENDPOINTS[0];
let rpcIndex = 0;
let scanning = false; let scanning = false;
// ── Helpers ── // ── Helpers ──
@ -104,34 +110,44 @@
} }
// ── RPC Call ── // ── RPC Call ──
async function rpc(method, params, retries = 2) { async function rpc(method, params, retries = 4) {
let lastError;
for (let i = 0; i <= retries; i++) { for (let i = 0; i <= retries; i++) {
const url = RPC_ENDPOINTS[(rpcIndex + i) % RPC_ENDPOINTS.length];
try { try {
const url = i === 0 ? activeRpc : RPC_BACKUP;
const res = await fetch(url, { const res = await fetch(url, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
}); });
if (res.status === 429) { if (res.status === 429 || res.status === 403) {
await sleep(1000 * (i + 1)); lastError = new Error('Rate limited (HTTP ' + res.status + ')');
await sleep(800 * (i + 1));
continue;
}
if (!res.ok) {
lastError = new Error('HTTP ' + res.status);
continue; continue;
} }
const json = await res.json(); const json = await res.json();
if (json.error) { if (json.error) {
if (json.error.code === -32429 || json.error.message?.includes('rate')) { if (json.error.code === -32429 || (json.error.message && json.error.message.includes('rate'))) {
await sleep(1000 * (i + 1)); lastError = new Error(json.error.message);
await sleep(800 * (i + 1));
continue; continue;
} }
throw new Error(json.error.message || 'RPC error'); throw new Error(json.error.message || 'RPC error');
} }
// Success — remember this working RPC
rpcIndex = (rpcIndex + i) % RPC_ENDPOINTS.length;
activeRpc = RPC_ENDPOINTS[rpcIndex];
return json.result; return json.result;
} catch (e) { } catch (e) {
if (i === retries) throw e; lastError = e;
activeRpc = RPC_BACKUP; if (i < retries) await sleep(500);
await sleep(500);
} }
} }
throw lastError || new Error('All RPC endpoints failed');
} }
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }