/** * Comma-separated RPC URL pool with round-robin and read-call failover. */ export function parseRpcPool(...sources) { const seen = new Set(); const urls = []; for (const source of sources) { if (!source || String(source).includes('${')) continue; for (const part of String(source).split(',')) { const url = part.trim(); if (!url || seen.has(url)) continue; seen.add(url); urls.push(url); } } return urls; } export class RpcUrlPool { constructor(urls, logger) { this.urls = Array.isArray(urls) ? urls.filter(Boolean) : []; this.logger = logger; this._cursor = 0; } get size() { return this.urls.length; } primaryUrl() { if (this.urls.length === 0) return ''; return this.urls[0]; } nextUrl() { if (this.urls.length === 0) return ''; const url = this.urls[this._cursor % this.urls.length]; this._cursor += 1; return url; } /** Run async read fn(url) until one succeeds or all fail. */ async withFailover(label, fn) { if (this.urls.length === 0) { throw new Error(`${label}: RPC pool is empty`); } let lastError; for (const url of this.urls) { try { return await fn(url); } catch (error) { lastError = error; this.logger?.warn?.(`${label} RPC failed (${url}): ${error?.message || error}`); } } throw lastError || new Error(`${label}: all RPC pool URLs failed`); } }