diff --git a/VERSION.properties b/VERSION.properties index 9b48272..0271d5b 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.326 -server.version=1.2.298 +client.version=1.2.327 +server.version=1.2.299 diff --git a/docs/Pending_Features/2026-07-16_1935_проверка_public_solana_rpc_в_ui.md b/docs/Pending_Features/2026-07-16_1935_проверка_public_solana_rpc_в_ui.md new file mode 100644 index 0000000..6008e2a --- /dev/null +++ b/docs/Pending_Features/2026-07-16_1935_проверка_public_solana_rpc_в_ui.md @@ -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 и не требует горизонтального скролла. diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 52e0b37..f8e8a19 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -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, diff --git a/shine-UI/js/pages/developer-settings-view.js b/shine-UI/js/pages/developer-settings-view.js index f8f90e6..b570a90 100644 --- a/shine-UI/js/pages/developer-settings-view.js +++ b/shine-UI/js/pages/developer-settings-view.js @@ -264,6 +264,7 @@ export function render({ navigate }) { + @@ -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({ diff --git a/shine-UI/js/pages/solana-rpc-check-view.js b/shine-UI/js/pages/solana-rpc-check-view.js new file mode 100644 index 0000000..1974acc --- /dev/null +++ b/shine-UI/js/pages/solana-rpc-check-view.js @@ -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 = ` + Проверка 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; +} diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 340f5b3..b318ef7 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -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;