SHA256
259 lines
9.0 KiB
JavaScript
259 lines
9.0 KiB
JavaScript
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 = `
|
||
<strong>Проверка mainnet RPC из браузера</strong>
|
||
<p class="meta-muted">
|
||
Этот экран делает реальный browser fetch к публичным Solana mainnet RPC.
|
||
Если ответ приходит прямо здесь, значит нода годится для клиентского UI без промежуточного сервера.
|
||
</p>
|
||
`;
|
||
|
||
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;
|
||
}
|