import { renderHeader } from '../components/header.js'; export const pageMeta = { id: 'solana-rpc-check-view', title: 'Проверка Solana RPC' }; const MAINNET_RPC_ENDPOINTS = [ { name: 'GetBlock', url: 'https://go.getblock.us/86aac42ad4484f3c813079afc201451c' }, { name: 'PublicNode', url: 'https://solana-rpc.publicnode.com' }, { name: 'BlockEden', url: 'https://api.blockeden.xyz/solana/KeCh6p22EX5AeRHxMSmc' }, { name: 'dRPC', url: 'https://solana.drpc.org/' }, { name: 'Grove', url: 'https://solana.rpc.grove.city/v1/01fdb492' }, { name: 'LeoRPC', url: 'https://solana.leorpc.com/?api_key=FREE' }, { name: 'OnFinality', url: 'https://solana.api.onfinality.io/public' }, { name: 'Solana Foundation', url: 'https://api.mainnet-beta.solana.com' }, { name: 'Solana Vibe Station', url: 'https://public.rpc.solanavibestation.com/' }, { name: 'TheRPC', url: 'https://solana.therpc.io' }, ]; const RPC_PAYLOAD = { jsonrpc: '2.0', id: 1, method: 'getVersion', params: [], }; function classifyResult(result) { if (!result) return { label: 'Нет данных', tone: 'neutral' }; if (result.ok) return { label: 'Работает в браузере', tone: 'ok' }; if (result.httpStatus === 402) return { label: 'Требует платный план', tone: 'bad' }; if (result.httpStatus === 403) return { label: 'Доступ запрещён', tone: 'bad' }; if (result.httpStatus === 429) return { label: 'Упёрлась в rate limit', tone: 'warn' }; if (result.httpStatus >= 500) return { label: 'Ошибка на стороне RPC', tone: 'bad' }; if (result.errorCode === 35) return { label: 'Free tier не даёт mainnet', tone: 'bad' }; if (result.networkError) return { label: 'Не проходит из браузера', tone: 'bad' }; return { label: 'Отвечает, но не подходит', tone: 'warn' }; } function shortenText(value, max = 140) { const text = String(value || '').trim(); if (!text) return '—'; return text.length > max ? `${text.slice(0, max)}...` : text; } async function testEndpoint(url, timeoutMs = 12000) { const startedAt = performance.now(); const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { method: 'POST', mode: 'cors', cache: 'no-store', credentials: 'omit', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(RPC_PAYLOAD), signal: controller.signal, }); const elapsedMs = Math.round(performance.now() - startedAt); let data = null; let raw = ''; try { raw = await response.text(); data = raw ? JSON.parse(raw) : null; } catch { data = null; } return { url, ok: response.ok && !!data?.result, httpStatus: response.status, elapsedMs, result: data?.result || null, errorCode: data?.error?.code ?? null, errorMessage: data?.error?.message || '', raw: shortenText(raw, 180), networkError: '', }; } catch (error) { const elapsedMs = Math.round(performance.now() - startedAt); return { url, ok: false, httpStatus: 0, elapsedMs, result: null, errorCode: null, errorMessage: '', raw: '', networkError: error?.name === 'AbortError' ? 'Timeout' : (error?.message || 'Network error'), }; } finally { window.clearTimeout(timeoutId); } } function makeResultCard(endpoint) { const card = document.createElement('article'); card.className = 'card stack solana-rpc-card'; const head = document.createElement('div'); head.className = 'solana-rpc-card-head'; const title = document.createElement('strong'); title.textContent = endpoint.name; const badge = document.createElement('span'); badge.className = 'badge alt solana-rpc-badge'; badge.textContent = 'Ожидает проверки'; head.append(title, badge); const endpointLine = document.createElement('a'); endpointLine.className = 'solana-rpc-url'; endpointLine.href = endpoint.url; endpointLine.target = '_blank'; endpointLine.rel = 'noopener noreferrer'; endpointLine.textContent = endpoint.url; const meta = document.createElement('div'); meta.className = 'solana-rpc-meta'; const statusLine = document.createElement('div'); statusLine.className = 'meta-muted'; statusLine.textContent = 'Ещё не проверяли.'; const details = document.createElement('div'); details.className = 'meta-muted solana-rpc-details'; details.textContent = '—'; meta.append(statusLine, details); card.append(head, endpointLine, meta); return { card, badge, statusLine, details }; } export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; const intro = document.createElement('div'); intro.className = 'card stack'; intro.innerHTML = ` Проверка mainnet RPC из браузера
`; const controls = document.createElement('div'); controls.className = 'auth-footer-actions'; const runBtn = document.createElement('button'); runBtn.className = 'primary-btn'; runBtn.type = 'button'; runBtn.textContent = 'Проверить все ноды'; const resetBtn = document.createElement('button'); resetBtn.className = 'ghost-btn'; resetBtn.type = 'button'; resetBtn.textContent = 'Сбросить'; controls.append(runBtn, resetBtn); intro.append(controls); const summary = document.createElement('p'); summary.className = 'status-line'; summary.textContent = 'Пока проверки не запускались.'; const grid = document.createElement('div'); grid.className = 'stack'; const cards = MAINNET_RPC_ENDPOINTS.map((endpoint) => { const ui = makeResultCard(endpoint); grid.append(ui.card); return { endpoint, ui }; }); let runToken = 0; const resetState = () => { runToken += 1; summary.className = 'status-line'; summary.textContent = 'Пока проверки не запускались.'; cards.forEach(({ ui }) => { ui.badge.className = 'badge alt solana-rpc-badge'; ui.badge.textContent = 'Ожидает проверки'; ui.statusLine.textContent = 'Ещё не проверяли.'; ui.details.textContent = '—'; }); }; const runChecks = async () => { const token = ++runToken; runBtn.disabled = true; resetBtn.disabled = true; summary.className = 'status-line'; summary.textContent = 'Проверяем mainnet RPC из браузера...'; let okCount = 0; let warnCount = 0; let badCount = 0; const tasks = cards.map(async ({ endpoint, ui }) => { ui.badge.className = 'badge alt solana-rpc-badge'; ui.badge.textContent = 'Проверяем...'; ui.statusLine.textContent = 'Отправляем JSON-RPC запрос getVersion...'; ui.details.textContent = 'Ждём ответ.'; const result = await testEndpoint(endpoint.url); if (token !== runToken) return; const verdict = classifyResult(result); if (verdict.tone === 'ok') okCount += 1; else if (verdict.tone === 'warn') warnCount += 1; else if (verdict.tone === 'bad') badCount += 1; ui.badge.className = `badge solana-rpc-badge is-${verdict.tone}`; ui.badge.textContent = verdict.label; ui.statusLine.textContent = `HTTP ${result.httpStatus || 'ERR'} • ${result.elapsedMs} мс`; if (result.ok) { ui.details.textContent = `solana-core: ${result.result?.['solana-core'] || 'unknown'} • feature-set: ${result.result?.['feature-set'] || 'unknown'}`; } else if (result.networkError) { ui.details.textContent = `Browser error: ${result.networkError}`; } else if (result.errorMessage) { ui.details.textContent = shortenText(result.errorMessage, 180); } else { ui.details.textContent = result.raw || 'Нода ответила без пригодного JSON-RPC result.'; } }); await Promise.all(tasks); if (token !== runToken) return; summary.className = 'status-line is-available'; summary.textContent = `Готово. Для браузера подходят: ${okCount}. С оговорками: ${warnCount}. Не подходят: ${badCount}.`; runBtn.disabled = false; resetBtn.disabled = false; }; runBtn.addEventListener('click', () => { void runChecks(); }); resetBtn.addEventListener('click', resetState); screen.append( renderHeader({ title: 'Проверка Solana RPC', leftAction: { label: '←', onClick: () => navigate('developer-settings-view') }, }), intro, summary, grid, ); void runChecks(); return screen; }