SHA256
Compare commits
4
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
daa516babe | ||
|
|
2890cdd457 | ||
|
|
e4dfb43b5e | ||
|
|
3926d561c0 |
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.324
|
client.version=1.2.329
|
||||||
server.version=1.2.296
|
server.version=1.2.301
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'application'
|
id 'application'
|
||||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
проверь ещё id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
def appVersionProps = new Properties()
|
def appVersionProps = new Properties()
|
||||||
@@ -216,7 +216,7 @@ tasks.register('deployServer', Exec) {
|
|||||||
dependsOn shadowJar
|
dependsOn shadowJar
|
||||||
workingDir = rootDir
|
workingDir = rootDir
|
||||||
environment 'LOCAL_JAR', file('SHiNE-server/build/libs/shine-server.jar').absolutePath
|
environment 'LOCAL_JAR', file('SHiNE-server/build/libs/shine-server.jar').absolutePath
|
||||||
commandLine 'bash', file('deploy_shine-server_test2.sh').absolutePath
|
commandLine 'bash', file('deploy_shine-server_server2shineupme.sh').absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('deployUI', Exec) {
|
tasks.register('deployUI', Exec) {
|
||||||
|
|||||||
@@ -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 и не требует горизонтального скролла.
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
## Кратко
|
||||||
|
- Переделан финальный flow регистрации: сначала экран ожидания подтверждения Solana с прогрессом и повторными проверками, потом отдельный экран успешной регистрации с кнопкой входа в стандартный экран сохранения ключей.
|
||||||
|
|
||||||
|
## Что проверять
|
||||||
|
- После отправки регистрации открывается экран ожидания с текстом про подтверждение Solana и кликабельным `Tx ID`.
|
||||||
|
- Прогресс-бар сначала идёт быстрее, потом заметно замедляется.
|
||||||
|
- Первая проверка регистрации начинается примерно через 4 секунды, далее повторяется раз в 2 секунды.
|
||||||
|
- Пока подтверждения нет, снизу показываются понятные статусы ожидания.
|
||||||
|
- После подтверждения открывается экран «Поздравляем с регистрацией» с одной кнопкой `Войти в аккаунт`.
|
||||||
|
- Кнопка `Войти в аккаунт` открывает штатный экран сохранения ключей с тремя галочками.
|
||||||
|
- Стрелка назад в левом верхнем углу на обоих экранах возвращает в главное меню.
|
||||||
|
- После успешного сохранения ключей регистрационный черновик и адрес кошелька очищаются.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
- Пользователь не видит преждевременное поздравление до подтверждения регистрации в Solana.
|
||||||
|
- Финальный успех показывается только после появления подтверждения регистрации.
|
||||||
|
- Вход после регистрации идёт через тот же стандартный сценарий сохранения ключей, что и в обычном login-flow.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
- pending
|
||||||
@@ -72,6 +72,7 @@ import * as languageView from './pages/language-view.js';
|
|||||||
import * as appLogView from './pages/app-log-view.js';
|
import * as appLogView from './pages/app-log-view.js';
|
||||||
import * as pwaDiagnosticsView from './pages/pwa-diagnostics-view.js';
|
import * as pwaDiagnosticsView from './pages/pwa-diagnostics-view.js';
|
||||||
import * as solanaUsersInitView from './pages/solana-users-init-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 messagesList from './pages/messages-list.js';
|
||||||
import * as contactSearchView from './pages/contact-search-view.js';
|
import * as contactSearchView from './pages/contact-search-view.js';
|
||||||
import * as chatView from './pages/chat-view.js';
|
import * as chatView from './pages/chat-view.js';
|
||||||
@@ -132,6 +133,7 @@ const routes = {
|
|||||||
'app-log-view': appLogView,
|
'app-log-view': appLogView,
|
||||||
'pwa-diagnostics-view': pwaDiagnosticsView,
|
'pwa-diagnostics-view': pwaDiagnosticsView,
|
||||||
'solana-users-init-view': solanaUsersInitView,
|
'solana-users-init-view': solanaUsersInitView,
|
||||||
|
'solana-rpc-check-view': solanaRpcCheckView,
|
||||||
'messages-list': messagesList,
|
'messages-list': messagesList,
|
||||||
'contact-search-view': contactSearchView,
|
'contact-search-view': contactSearchView,
|
||||||
'chat-view': chatView,
|
'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-force-update-help">Клиент не обновляется?</button>
|
||||||
<button class="text-btn" type="button" id="settings-ui-error-reporting">Отправлять ошибки на сервер</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-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-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-diagnostics">Диагностика PWA / Push</button>
|
||||||
<button class="text-btn" type="button" id="settings-pwa-install">Как установить PWA</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 forceUpdateHelpBtn = card.querySelector('#settings-force-update-help');
|
||||||
const uiErrorReportingBtn = card.querySelector('#settings-ui-error-reporting');
|
const uiErrorReportingBtn = card.querySelector('#settings-ui-error-reporting');
|
||||||
const solanaUsersInitBtn = card.querySelector('#settings-solana-users-init');
|
const solanaUsersInitBtn = card.querySelector('#settings-solana-users-init');
|
||||||
|
const solanaRpcCheckBtn = card.querySelector('#settings-solana-rpc-check');
|
||||||
|
|
||||||
appLogBtn?.addEventListener('click', () => navigate('app-log-view'));
|
appLogBtn?.addEventListener('click', () => navigate('app-log-view'));
|
||||||
solanaUsersInitBtn?.addEventListener('click', () => navigate('solana-users-init-view'));
|
solanaUsersInitBtn?.addEventListener('click', () => navigate('solana-users-init-view'));
|
||||||
|
solanaRpcCheckBtn?.addEventListener('click', () => navigate('solana-rpc-check-view'));
|
||||||
diagnosticsBtn?.addEventListener('click', () => navigate('pwa-diagnostics-view'));
|
diagnosticsBtn?.addEventListener('click', () => navigate('pwa-diagnostics-view'));
|
||||||
uploadAvatarBtn?.addEventListener('click', () => {
|
uploadAvatarBtn?.addEventListener('click', () => {
|
||||||
openDeveloperAvatarUploadModal({
|
openDeveloperAvatarUploadModal({
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ export function render({ navigate }) {
|
|||||||
state.registrationDraft.sessionId = '';
|
state.registrationDraft.sessionId = '';
|
||||||
state.registrationDraft.pendingKeyBundle = null;
|
state.registrationDraft.pendingKeyBundle = null;
|
||||||
state.registrationDraft.pendingSessionMaterial = null;
|
state.registrationDraft.pendingSessionMaterial = null;
|
||||||
|
state.registrationDraft.preGeneratedKeyBundle = null;
|
||||||
|
state.registrationPayment.walletAddress = '';
|
||||||
|
state.registrationPayment.balanceSOL = '0.0000';
|
||||||
|
|
||||||
await refreshSessions();
|
await refreshSessions();
|
||||||
setAuthInfo(isLoginFlow
|
setAuthInfo(isLoginFlow
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import {
|
import {
|
||||||
authService,
|
authService,
|
||||||
authorizeSession,
|
|
||||||
refreshSessions,
|
|
||||||
setAuthError,
|
setAuthError,
|
||||||
setAuthInfo,
|
setAuthInfo,
|
||||||
state,
|
state,
|
||||||
@@ -16,6 +14,7 @@ import {
|
|||||||
} from '../services/solana-wallet-service.js';
|
} from '../services/solana-wallet-service.js';
|
||||||
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
|
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
|
||||||
import {
|
import {
|
||||||
|
checkLoginExistsOnSolana,
|
||||||
formatSolanaErrorDetails,
|
formatSolanaErrorDetails,
|
||||||
isUserAlreadyExistsSolanaError,
|
isUserAlreadyExistsSolanaError,
|
||||||
registerUserOnSolana,
|
registerUserOnSolana,
|
||||||
@@ -24,9 +23,30 @@ import { defaultServerLogin } from '../deploy-config.js';
|
|||||||
|
|
||||||
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
||||||
const MIN_REQUIRED_SOL = 0.01;
|
const MIN_REQUIRED_SOL = 0.01;
|
||||||
const AUTO_LOGIN_INITIAL_DELAY_MS = 5000;
|
const REGISTRATION_PROGRESS_DURATION_MS = 12000;
|
||||||
const AUTO_LOGIN_RETRY_MS = 1000;
|
const REGISTRATION_POLL_START_DELAY_MS = 4000;
|
||||||
const EMPTY_PASSWORD_WORDS = Array.from({ length: 12 }, () => '');
|
const REGISTRATION_POLL_INTERVAL_MS = 2000;
|
||||||
|
const REGISTRATION_CONFIRM_TIMEOUT_MS = 25000;
|
||||||
|
const REGISTRATION_SUCCESS_SETTLE_DELAY_MS = 2000;
|
||||||
|
|
||||||
|
function getExplorerClusterName(endpoint) {
|
||||||
|
const source = String(endpoint || '').trim().toLowerCase();
|
||||||
|
if (!source) return 'mainnet-beta';
|
||||||
|
if (source.includes('devnet')) return 'devnet';
|
||||||
|
if (source.includes('testnet')) return 'testnet';
|
||||||
|
return 'mainnet-beta';
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSolanaExplorerTxUrl(signature, endpoint) {
|
||||||
|
const cleanSignature = String(signature || '').trim();
|
||||||
|
if (!cleanSignature) return '';
|
||||||
|
const cluster = getExplorerClusterName(endpoint);
|
||||||
|
const url = new URL(`https://explorer.solana.com/tx/${encodeURIComponent(cleanSignature)}`);
|
||||||
|
if (cluster !== 'mainnet-beta') {
|
||||||
|
url.searchParams.set('cluster', cluster);
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
function parseBalanceSol(value) {
|
function parseBalanceSol(value) {
|
||||||
const parsed = Number.parseFloat(String(value || '').replace(',', '.'));
|
const parsed = Number.parseFloat(String(value || '').replace(',', '.'));
|
||||||
@@ -41,70 +61,23 @@ function getCryptoRuntimeState() {
|
|||||||
return { hasCrypto, hasGetRandomValues, hasSubtle, secureContext };
|
return { hasCrypto, hasGetRandomValues, hasSubtle, secureContext };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeRegistrationLogin({ navigate, keyBundle }) {
|
function clamp(value, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareRegistrationLoginForKeyStorage({ keyBundle }) {
|
||||||
await authService.reconnect(state.entrySettings.shineServer);
|
await authService.reconnect(state.entrySettings.shineServer);
|
||||||
const result = await authService.createSessionForExistingUser(
|
const result = await authService.createSessionForExistingUser(
|
||||||
state.registrationDraft.login,
|
state.registrationDraft.login,
|
||||||
state.registrationDraft.password,
|
state.registrationDraft.password,
|
||||||
);
|
);
|
||||||
|
state.registrationDraft.flowType = 'register';
|
||||||
state.keyStorage.saveRoot = Boolean(state.keyStorage.saveRoot);
|
state.registrationDraft.login = result.login;
|
||||||
state.keyStorage.saveBlockchain = Boolean(state.keyStorage.saveBlockchain);
|
state.registrationDraft.sessionId = result.sessionId;
|
||||||
|
state.registrationDraft.storagePwd = result.storagePwd;
|
||||||
await authService.persistSelectedKeys(
|
state.registrationDraft.pendingKeyBundle = keyBundle || result.keyBundle;
|
||||||
result.login,
|
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
|
||||||
result.storagePwd,
|
return result;
|
||||||
keyBundle,
|
|
||||||
{
|
|
||||||
saveRoot: state.keyStorage.saveRoot,
|
|
||||||
saveBlockchain: state.keyStorage.saveBlockchain,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await authService.persistSessionMaterial(result.login, result.sessionMaterial);
|
|
||||||
await clearStoredMessages().catch(() => {});
|
|
||||||
|
|
||||||
const resumed = await authService.resumeSession(result.login, result.sessionId);
|
|
||||||
const resumedLogin = resumed.login || result.login;
|
|
||||||
const resumedSessionId = resumed.sessionId || result.sessionId;
|
|
||||||
const resumedStoragePwd = resumed.storagePwd || result.storagePwd;
|
|
||||||
|
|
||||||
authorizeSession({
|
|
||||||
login: resumedLogin,
|
|
||||||
sessionId: resumedSessionId,
|
|
||||||
storagePwd: resumedStoragePwd,
|
|
||||||
});
|
|
||||||
|
|
||||||
state.loginDraft.login = resumedLogin;
|
|
||||||
state.loginDraft.password = '';
|
|
||||||
state.loginDraft.passwordMode = 'single';
|
|
||||||
state.loginDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice();
|
|
||||||
|
|
||||||
state.registrationDraft.flowType = '';
|
|
||||||
state.registrationDraft.password = '';
|
|
||||||
state.registrationDraft.passwordMode = 'single';
|
|
||||||
state.registrationDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice();
|
|
||||||
state.registrationDraft.passwordWordsLinked = false;
|
|
||||||
state.registrationDraft.usePromoCode = false;
|
|
||||||
state.registrationDraft.promoCode = '';
|
|
||||||
state.registrationDraft.storagePwd = '';
|
|
||||||
state.registrationDraft.sessionId = '';
|
|
||||||
state.registrationDraft.pendingKeyBundle = null;
|
|
||||||
state.registrationDraft.pendingSessionMaterial = null;
|
|
||||||
state.registrationDraft.preGeneratedKeyBundle = null;
|
|
||||||
|
|
||||||
state.registrationPayment.walletAddress = '';
|
|
||||||
state.registrationPayment.balanceSOL = '0.0000';
|
|
||||||
|
|
||||||
await refreshSessions();
|
|
||||||
setAuthInfo(`Регистрация завершена. Вы автоматически вошли как @${resumedLogin}.`);
|
|
||||||
|
|
||||||
const nextHash = String(state.authReturnHash || '').trim();
|
|
||||||
state.authReturnHash = '';
|
|
||||||
if (nextHash.startsWith('/')) {
|
|
||||||
navigate(nextHash.slice(1));
|
|
||||||
} else {
|
|
||||||
navigate('profile-view');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function render({ navigate }) {
|
export function render({ navigate }) {
|
||||||
@@ -262,14 +235,16 @@ export function render({ navigate }) {
|
|||||||
|
|
||||||
// Регистрация на Solana (смарт контракт)
|
// Регистрация на Solana (смарт контракт)
|
||||||
submitButton.textContent = 'Регистрация в Solana...';
|
submitButton.textContent = 'Регистрация в Solana...';
|
||||||
|
let registrationTxId = '';
|
||||||
try {
|
try {
|
||||||
await registerUserOnSolana({
|
const registrationResult = await registerUserOnSolana({
|
||||||
login: state.registrationDraft.login,
|
login: state.registrationDraft.login,
|
||||||
keyBundle,
|
keyBundle,
|
||||||
solanaEndpoint: state.entrySettings.solanaServer,
|
solanaEndpoint: state.entrySettings.solanaServer,
|
||||||
accessServers: [state.entrySettings.shineServerLogin || defaultServerLogin],
|
accessServers: [state.entrySettings.shineServerLogin || defaultServerLogin],
|
||||||
promoCode: '',
|
promoCode: '',
|
||||||
});
|
});
|
||||||
|
registrationTxId = String(registrationResult?.signature || '').trim();
|
||||||
} catch (solanaError) {
|
} catch (solanaError) {
|
||||||
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
||||||
// Пользователь уже зарегистрирован в Solana — продолжаем
|
// Пользователь уже зарегистрирован в Solana — продолжаем
|
||||||
@@ -278,7 +253,7 @@ export function render({ navigate }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSolanaDoneStage({ navigate, status, keyBundle });
|
renderSolanaRegistrationStage({ navigate, status, keyBundle, registrationTxId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = toUserMessage(error, 'Не удалось завершить регистрацию.');
|
const message = toUserMessage(error, 'Не удалось завершить регистрацию.');
|
||||||
setAuthError(message);
|
setAuthError(message);
|
||||||
@@ -324,82 +299,261 @@ export function render({ navigate }) {
|
|||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSolanaDoneStage({ navigate, status, keyBundle }) {
|
function renderSolanaRegistrationStage({ navigate, status, keyBundle, registrationTxId = '' }) {
|
||||||
const screen = document.querySelector('section.stack');
|
const screen = document.querySelector('section.stack');
|
||||||
if (!screen) return;
|
if (!screen) return;
|
||||||
|
const headerBackButton = screen.querySelector('.page-header .header-left .icon-btn');
|
||||||
|
const card = screen.querySelector('.card.stack');
|
||||||
|
if (!card) return;
|
||||||
|
card.classList.add('registration-finish-card');
|
||||||
|
|
||||||
|
let progressTimerId = null;
|
||||||
|
let pollStartTimerId = null;
|
||||||
|
let pollTimerId = null;
|
||||||
|
let successTimerId = null;
|
||||||
|
let confirmationInFlight = false;
|
||||||
|
let stageClosed = false;
|
||||||
|
let successPending = false;
|
||||||
|
let timeoutShown = false;
|
||||||
|
const stageStartedAt = Date.now();
|
||||||
|
const txExplorerUrl = makeSolanaExplorerTxUrl(registrationTxId, state.entrySettings.solanaServer);
|
||||||
|
|
||||||
|
const title = document.createElement('h2');
|
||||||
|
title.className = 'registration-finish-title';
|
||||||
|
title.textContent = 'Идёт регистрация в блокчейне...';
|
||||||
|
|
||||||
|
const hint = document.createElement('p');
|
||||||
|
hint.className = 'auth-copy registration-finish-text';
|
||||||
|
hint.textContent = 'Ждём, пока транзакция будет полностью одобрена сетью Solana.';
|
||||||
|
|
||||||
|
const txIdLine = document.createElement('p');
|
||||||
|
txIdLine.className = 'meta-muted registration-finish-tx';
|
||||||
|
if (registrationTxId && txExplorerUrl) {
|
||||||
|
const txLabel = document.createElement('span');
|
||||||
|
txLabel.textContent = 'Tx ID регистрации: ';
|
||||||
|
const txLink = document.createElement('a');
|
||||||
|
txLink.className = 'registration-finish-tx-link';
|
||||||
|
txLink.href = txExplorerUrl;
|
||||||
|
txLink.target = '_blank';
|
||||||
|
txLink.rel = 'noopener noreferrer';
|
||||||
|
txLink.textContent = registrationTxId;
|
||||||
|
txIdLine.append(txLabel, txLink);
|
||||||
|
} else {
|
||||||
|
txIdLine.textContent = 'Tx ID регистрации: ожидаем подтверждённую подпись';
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressWrap = document.createElement('div');
|
||||||
|
progressWrap.className = 'registration-progress';
|
||||||
|
|
||||||
|
const progressBar = document.createElement('div');
|
||||||
|
progressBar.className = 'registration-progress-bar';
|
||||||
|
progressWrap.append(progressBar);
|
||||||
|
|
||||||
|
const progress = document.createElement('p');
|
||||||
|
progress.className = 'meta-muted registration-finish-progress';
|
||||||
|
progress.textContent = 'Подготавливаем проверку подтверждения...';
|
||||||
|
|
||||||
|
const pollStatus = document.createElement('p');
|
||||||
|
pollStatus.className = 'meta-muted registration-finish-progress';
|
||||||
|
pollStatus.textContent = 'Через несколько секунд начнём проверять подтверждение регистрации.';
|
||||||
|
|
||||||
|
const stopTimers = () => {
|
||||||
|
if (progressTimerId) {
|
||||||
|
window.clearInterval(progressTimerId);
|
||||||
|
progressTimerId = null;
|
||||||
|
}
|
||||||
|
if (pollStartTimerId) {
|
||||||
|
window.clearTimeout(pollStartTimerId);
|
||||||
|
pollStartTimerId = null;
|
||||||
|
}
|
||||||
|
if (pollTimerId) {
|
||||||
|
window.clearInterval(pollTimerId);
|
||||||
|
pollTimerId = null;
|
||||||
|
}
|
||||||
|
if (successTimerId) {
|
||||||
|
window.clearTimeout(successTimerId);
|
||||||
|
successTimerId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVisualProgress = () => {
|
||||||
|
const elapsed = Date.now() - stageStartedAt;
|
||||||
|
let widthPercent = 0;
|
||||||
|
if (elapsed <= REGISTRATION_PROGRESS_DURATION_MS) {
|
||||||
|
const phaseRatio = clamp(elapsed / REGISTRATION_PROGRESS_DURATION_MS, 0, 1);
|
||||||
|
widthPercent = 100 * (1 - ((1 - phaseRatio) ** 1.85));
|
||||||
|
} else {
|
||||||
|
const extraRatio = 1 - Math.exp(-(elapsed - REGISTRATION_PROGRESS_DURATION_MS) / 9000);
|
||||||
|
widthPercent = 88 + (10 * clamp(extraRatio, 0, 1));
|
||||||
|
}
|
||||||
|
progressBar.style.width = `${clamp(widthPercent, 0, 98)}%`;
|
||||||
|
|
||||||
|
if (elapsed < REGISTRATION_POLL_START_DELAY_MS) {
|
||||||
|
progress.textContent = 'Отправили регистрацию в сеть. Даём транзакции несколько секунд на обработку.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (elapsed < REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
||||||
|
progress.textContent = 'Идёт ожидание подтверждения Solana. Индикатор может замедлиться, это нормально.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
progress.textContent = 'Подтверждение затянулось. Продолжаем автоматическую проверку.';
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalizeSuccess = () => {
|
||||||
|
if (stageClosed || successPending) return;
|
||||||
|
successPending = true;
|
||||||
|
stopTimers();
|
||||||
|
progress.textContent = 'Подтверждение получено. Завершаем регистрацию...';
|
||||||
|
pollStatus.textContent = 'Ещё секунду: запись уже найдена в Solana, синхронизируем финальный экран.';
|
||||||
|
status.style.display = 'none';
|
||||||
|
progressBar.style.transition = `width ${REGISTRATION_SUCCESS_SETTLE_DELAY_MS}ms ease-out`;
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
successTimerId = window.setTimeout(() => {
|
||||||
|
if (stageClosed) return;
|
||||||
|
stageClosed = true;
|
||||||
|
renderRegistrationSuccessStage({ navigate, status, keyBundle, registrationTxId });
|
||||||
|
}, REGISTRATION_SUCCESS_SETTLE_DELAY_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showTimeoutState = () => {
|
||||||
|
if (timeoutShown || stageClosed) return;
|
||||||
|
timeoutShown = true;
|
||||||
|
status.className = 'status-line is-unavailable';
|
||||||
|
status.textContent = 'Подтверждение не пришло вовремя. Возможно, регистрация уже прошла, а возможно ещё нет. Проверьте Tx ID ниже: мы продолжим автоматическую проверку.';
|
||||||
|
status.style.display = '';
|
||||||
|
pollStatus.textContent = 'Пока подтверждения нет. Подождите ещё секундочку, мы продолжаем проверять регистрацию.';
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryCheckRegistration = async () => {
|
||||||
|
if (stageClosed || confirmationInFlight) return;
|
||||||
|
confirmationInFlight = true;
|
||||||
|
progress.textContent = 'Проверяем подтверждение регистрации в Solana...';
|
||||||
|
status.style.display = timeoutShown ? '' : 'none';
|
||||||
|
try {
|
||||||
|
const result = await checkLoginExistsOnSolana({
|
||||||
|
login: state.registrationDraft.login,
|
||||||
|
solanaEndpoint: state.entrySettings.solanaServer,
|
||||||
|
});
|
||||||
|
if (result?.exists) {
|
||||||
|
finalizeSuccess();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pollStatus.textContent = 'Пока ещё не прошла, подождите ещё секундочку...';
|
||||||
|
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
||||||
|
showTimeoutState();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Registration confirmation check failed', toUserMessage(error, 'registration-confirmation'));
|
||||||
|
pollStatus.textContent = 'Проверяем регистрацию повторно...';
|
||||||
|
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
||||||
|
showTimeoutState();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
confirmationInFlight = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (headerBackButton) {
|
||||||
|
const replacement = headerBackButton.cloneNode(true);
|
||||||
|
replacement.addEventListener('click', () => {
|
||||||
|
stageClosed = true;
|
||||||
|
stopTimers();
|
||||||
|
navigate('start-view');
|
||||||
|
});
|
||||||
|
headerBackButton.replaceWith(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.innerHTML = '';
|
||||||
|
status.style.display = 'none';
|
||||||
|
card.append(title, hint, txIdLine, progressWrap, progress, pollStatus, status);
|
||||||
|
|
||||||
|
updateVisualProgress();
|
||||||
|
progressTimerId = window.setInterval(() => {
|
||||||
|
if (stageClosed) return;
|
||||||
|
updateVisualProgress();
|
||||||
|
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
||||||
|
showTimeoutState();
|
||||||
|
}
|
||||||
|
}, 160);
|
||||||
|
|
||||||
|
pollStartTimerId = window.setTimeout(() => {
|
||||||
|
if (stageClosed) return;
|
||||||
|
void tryCheckRegistration();
|
||||||
|
pollTimerId = window.setInterval(() => {
|
||||||
|
void tryCheckRegistration();
|
||||||
|
}, REGISTRATION_POLL_INTERVAL_MS);
|
||||||
|
}, REGISTRATION_POLL_START_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRegistrationSuccessStage({ navigate, status, keyBundle, registrationTxId = '' }) {
|
||||||
|
const screen = document.querySelector('section.stack');
|
||||||
|
if (!screen) return;
|
||||||
|
const headerBackButton = screen.querySelector('.page-header .header-left .icon-btn');
|
||||||
const card = screen.querySelector('.card.stack');
|
const card = screen.querySelector('.card.stack');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
let initialDelayId = null;
|
const txExplorerUrl = makeSolanaExplorerTxUrl(registrationTxId, state.entrySettings.solanaServer);
|
||||||
let retryTimerId = null;
|
|
||||||
let loginInFlight = false;
|
let loginInFlight = false;
|
||||||
let loginCompleted = false;
|
|
||||||
|
|
||||||
const title = document.createElement('h2');
|
const title = document.createElement('h2');
|
||||||
title.textContent = 'Поздравляем, регистрация завершена';
|
title.className = 'registration-finish-title';
|
||||||
title.style.margin = '0';
|
title.textContent = 'Поздравляем с регистрацией';
|
||||||
|
|
||||||
const hint = document.createElement('p');
|
const hint = document.createElement('p');
|
||||||
hint.className = 'auth-copy';
|
hint.className = 'auth-copy registration-finish-text';
|
||||||
hint.textContent = 'Осталось подождать несколько секунд, пока обновится запись в блокчейне. После этого приложение автоматически войдёт в ваш аккаунт.';
|
hint.textContent = 'Регистрация подтверждена в блокчейне Solana. Теперь можно войти в аккаунт и выбрать, какие ключи сохранить на устройстве.';
|
||||||
|
|
||||||
const progress = document.createElement('p');
|
const txIdLine = document.createElement('p');
|
||||||
progress.className = 'meta-muted';
|
txIdLine.className = 'meta-muted registration-finish-tx';
|
||||||
progress.textContent = 'Подготавливаем автоматический вход...';
|
if (registrationTxId && txExplorerUrl) {
|
||||||
|
const txLabel = document.createElement('span');
|
||||||
|
txLabel.textContent = 'Tx ID регистрации: ';
|
||||||
|
const txLink = document.createElement('a');
|
||||||
|
txLink.className = 'registration-finish-tx-link';
|
||||||
|
txLink.href = txExplorerUrl;
|
||||||
|
txLink.target = '_blank';
|
||||||
|
txLink.rel = 'noopener noreferrer';
|
||||||
|
txLink.textContent = registrationTxId;
|
||||||
|
txIdLine.append(txLabel, txLink);
|
||||||
|
} else {
|
||||||
|
txIdLine.textContent = 'Регистрация подтверждена. Tx ID недоступен.';
|
||||||
|
}
|
||||||
|
|
||||||
const altHint = document.createElement('p');
|
const enterButton = document.createElement('button');
|
||||||
altHint.className = 'meta-muted';
|
enterButton.className = 'primary-btn';
|
||||||
altHint.textContent = 'Или вы можете';
|
enterButton.type = 'button';
|
||||||
|
enterButton.textContent = 'Войти в аккаунт';
|
||||||
const backBtn = document.createElement('button');
|
enterButton.addEventListener('click', async () => {
|
||||||
backBtn.className = 'ghost-btn';
|
if (loginInFlight) return;
|
||||||
backBtn.type = 'button';
|
|
||||||
backBtn.textContent = 'Вернуться в главное меню';
|
|
||||||
|
|
||||||
const stopAutoLogin = () => {
|
|
||||||
if (initialDelayId) {
|
|
||||||
window.clearTimeout(initialDelayId);
|
|
||||||
initialDelayId = null;
|
|
||||||
}
|
|
||||||
if (retryTimerId) {
|
|
||||||
window.clearInterval(retryTimerId);
|
|
||||||
retryTimerId = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tryAutoLogin = async () => {
|
|
||||||
if (loginCompleted || loginInFlight) return;
|
|
||||||
loginInFlight = true;
|
loginInFlight = true;
|
||||||
progress.textContent = 'Пробуем автоматически войти в аккаунт...';
|
status.className = 'status-line';
|
||||||
status.style.display = 'none';
|
status.textContent = 'Готовим вход и экран сохранения ключей...';
|
||||||
|
status.style.display = '';
|
||||||
|
enterButton.disabled = true;
|
||||||
try {
|
try {
|
||||||
await completeRegistrationLogin({ navigate, keyBundle });
|
await prepareRegistrationLoginForKeyStorage({ keyBundle });
|
||||||
loginCompleted = true;
|
await clearStoredMessages().catch(() => {});
|
||||||
stopAutoLogin();
|
setAuthInfo(`Регистрация подтверждена для @${state.registrationDraft.login}. Выберите, какие ключи сохранить на устройстве.`);
|
||||||
|
navigate('registration-keys-view');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!loginCompleted) {
|
const message = toUserMessage(error, 'Не удалось подготовить вход после регистрации.');
|
||||||
progress.textContent = 'Ждём обновления записи и скоро попробуем снова...';
|
setAuthError(message);
|
||||||
console.warn('Auto login after registration is not ready yet', toUserMessage(error, 'auto-login-wait'));
|
status.className = 'status-line is-unavailable';
|
||||||
}
|
status.textContent = message;
|
||||||
|
status.style.display = '';
|
||||||
} finally {
|
} finally {
|
||||||
loginInFlight = false;
|
loginInFlight = false;
|
||||||
|
enterButton.disabled = false;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
backBtn.addEventListener('click', () => {
|
|
||||||
loginCompleted = true;
|
|
||||||
stopAutoLogin();
|
|
||||||
navigate('start-view');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
card.innerHTML = '';
|
if (headerBackButton) {
|
||||||
card.append(title, hint, progress, altHint, backBtn, status);
|
const replacement = headerBackButton.cloneNode(true);
|
||||||
|
replacement.addEventListener('click', () => navigate('start-view'));
|
||||||
|
headerBackButton.replaceWith(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
initialDelayId = window.setTimeout(() => {
|
card.innerHTML = '';
|
||||||
if (loginCompleted) return;
|
status.style.display = 'none';
|
||||||
void tryAutoLogin();
|
card.append(title, hint, txIdLine, enterButton, status);
|
||||||
retryTimerId = window.setInterval(() => {
|
|
||||||
void tryAutoLogin();
|
|
||||||
}, AUTO_LOGIN_RETRY_MS);
|
|
||||||
}, AUTO_LOGIN_INITIAL_DELAY_MS);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -755,6 +755,43 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.registration-finish-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-finish-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.9rem, 7vw, 2.45rem);
|
||||||
|
line-height: 1.18;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-finish-text,
|
||||||
|
.registration-finish-tx,
|
||||||
|
.registration-finish-progress {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-finish-card .primary-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-finish-tx-link {
|
||||||
|
color: #9fd8ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-finish-tx-link:hover,
|
||||||
|
.registration-finish-tx-link:focus-visible {
|
||||||
|
color: #c9ebff;
|
||||||
|
}
|
||||||
|
|
||||||
.session-status {
|
.session-status {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -1267,6 +1304,59 @@
|
|||||||
margin-top: 4px;
|
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 {
|
.settings-developer-panel {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user