@@ -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 ;
}