Добавить экран проверки public Solana RPC

This commit is contained in:
AidarKC
2026-07-16 20:33:48 +04:00
parent e4dfb43b5e
commit 2890cdd457
6 changed files with 341 additions and 2 deletions
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.2.326
server.version=1.2.298
client.version=1.2.327
server.version=1.2.299
@@ -0,0 +1,23 @@
# Экран проверки public Solana RPC в UI
- статус: `pending`
## Кратко
Добавлен отдельный экран разработчика для проверки публичных mainnet Solana RPC прямо из браузера.
Экран отправляет реальный JSON-RPC `getVersion` запрос к списку public endpoint-ов и показывает,
какие ноды реально подходят для browser use.
## Что проверять
- в `Настройки разработчика` появилась кнопка `Solana: проверить public RPC`;
- экран открывается без ошибок;
- по каждой mainnet ноде показывается итоговый статус;
- для доступных нод видно HTTP-статус, время ответа и версию `solana-core`;
- длинные URL и ошибки не распирают экран по ширине на телефоне.
## Ожидаемый результат
- можно визуально увидеть, какие public Solana RPC доступны именно из браузера;
- проверка работает без промежуточного сервера;
- экран остаётся mobile-first и не требует горизонтального скролла.
+2
View File
@@ -72,6 +72,7 @@ import * as languageView from './pages/language-view.js';
import * as appLogView from './pages/app-log-view.js';
import * as pwaDiagnosticsView from './pages/pwa-diagnostics-view.js';
import * as solanaUsersInitView from './pages/solana-users-init-view.js';
import * as solanaRpcCheckView from './pages/solana-rpc-check-view.js';
import * as messagesList from './pages/messages-list.js';
import * as contactSearchView from './pages/contact-search-view.js';
import * as chatView from './pages/chat-view.js';
@@ -132,6 +133,7 @@ const routes = {
'app-log-view': appLogView,
'pwa-diagnostics-view': pwaDiagnosticsView,
'solana-users-init-view': solanaUsersInitView,
'solana-rpc-check-view': solanaRpcCheckView,
'messages-list': messagesList,
'contact-search-view': contactSearchView,
'chat-view': chatView,
@@ -264,6 +264,7 @@ export function render({ navigate }) {
<button class="text-btn" type="button" id="settings-force-update-help">Клиент не обновляется?</button>
<button class="text-btn" type="button" id="settings-ui-error-reporting">Отправлять ошибки на сервер</button>
<button class="text-btn" type="button" id="settings-solana-users-init">Solana: init регистрации</button>
<button class="text-btn" type="button" id="settings-solana-rpc-check">Solana: проверить public RPC</button>
<button class="text-btn" type="button" id="settings-app-log">Лог приложения</button>
<button class="text-btn" type="button" id="settings-pwa-diagnostics">Диагностика PWA / Push</button>
<button class="text-btn" type="button" id="settings-pwa-install">Как установить PWA</button>
@@ -278,9 +279,11 @@ export function render({ navigate }) {
const forceUpdateHelpBtn = card.querySelector('#settings-force-update-help');
const uiErrorReportingBtn = card.querySelector('#settings-ui-error-reporting');
const solanaUsersInitBtn = card.querySelector('#settings-solana-users-init');
const solanaRpcCheckBtn = card.querySelector('#settings-solana-rpc-check');
appLogBtn?.addEventListener('click', () => navigate('app-log-view'));
solanaUsersInitBtn?.addEventListener('click', () => navigate('solana-users-init-view'));
solanaRpcCheckBtn?.addEventListener('click', () => navigate('solana-rpc-check-view'));
diagnosticsBtn?.addEventListener('click', () => navigate('pwa-diagnostics-view'));
uploadAvatarBtn?.addEventListener('click', () => {
openDeveloperAvatarUploadModal({
+258
View File
@@ -0,0 +1,258 @@
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;
}
+53
View File
@@ -1300,6 +1300,59 @@
margin-top: 4px;
}
.solana-rpc-card {
width: min(100%, 420px);
justify-self: center;
}
.solana-rpc-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.solana-rpc-badge {
flex: 0 0 auto;
text-align: center;
}
.solana-rpc-badge.is-ok {
border-color: rgba(132, 244, 161, 0.42);
background: rgba(132, 244, 161, 0.12);
color: #d7ffe3;
}
.solana-rpc-badge.is-warn {
border-color: rgba(242, 210, 129, 0.42);
background: rgba(242, 210, 129, 0.12);
color: #fff0c7;
}
.solana-rpc-badge.is-bad {
border-color: rgba(255, 113, 143, 0.42);
background: rgba(255, 113, 143, 0.12);
color: #ffd7df;
}
.solana-rpc-url {
color: #9fd8ff;
text-decoration: underline;
overflow-wrap: anywhere;
word-break: break-all;
font-size: 13px;
}
.solana-rpc-meta {
display: grid;
gap: 6px;
}
.solana-rpc-details {
overflow-wrap: anywhere;
word-break: break-word;
}
.settings-developer-panel {
margin-top: 2px;
padding-top: 8px;