feat(ui): кошелек на device.key и проверки серверов (тоже пока не проверено)
This commit is contained in:
parent
8be56192cb
commit
e63c53a855
@ -1,5 +1,6 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import { checkServerAvailability, saveEntrySettings, state } from '../state.js';
|
import { saveEntrySettings, state } from '../state.js';
|
||||||
|
import { checkServerAvailabilityByKey } from '../services/server-health-service.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false };
|
export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false };
|
||||||
|
|
||||||
@ -86,9 +87,17 @@ export function render({ navigate }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const runCheck = () => {
|
const runCheck = async () => {
|
||||||
draft[field.key] = input.value.trim();
|
draft[field.key] = input.value.trim();
|
||||||
applyStatus(checkServerAvailability(input.value));
|
checkButton.disabled = true;
|
||||||
|
checkButton.textContent = 'Проверка...';
|
||||||
|
try {
|
||||||
|
const next = await checkServerAvailabilityByKey(field.key, input.value);
|
||||||
|
applyStatus(next);
|
||||||
|
} finally {
|
||||||
|
checkButton.disabled = false;
|
||||||
|
checkButton.textContent = 'Проверить';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
applyStatus(draft.statuses[field.key]);
|
applyStatus(draft.statuses[field.key]);
|
||||||
@ -98,13 +107,17 @@ export function render({ navigate }) {
|
|||||||
draft[field.key] = input.value;
|
draft[field.key] = input.value;
|
||||||
applyStatus('idle');
|
applyStatus('idle');
|
||||||
window.clearTimeout(timers.get(field.key));
|
window.clearTimeout(timers.get(field.key));
|
||||||
timers.set(field.key, window.setTimeout(runCheck, 3000));
|
timers.set(field.key, window.setTimeout(() => {
|
||||||
|
void runCheck();
|
||||||
|
}, 3000));
|
||||||
|
});
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
void runCheck();
|
||||||
});
|
});
|
||||||
input.addEventListener('blur', runCheck);
|
|
||||||
input.addEventListener('keydown', (event) => {
|
input.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
runCheck();
|
void runCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import {
|
import {
|
||||||
authService,
|
authService,
|
||||||
refreshRegistrationBalance,
|
|
||||||
setAuthError,
|
setAuthError,
|
||||||
setAuthInfo,
|
setAuthInfo,
|
||||||
state,
|
state,
|
||||||
} from '../state.js';
|
} from '../state.js';
|
||||||
import { toUserMessage } from '../services/ui-error-texts.js';
|
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||||
|
import { deriveWalletFromPassword, formatSol, getBalanceSol } from '../services/solana-wallet-service.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ export function render({ navigate }) {
|
|||||||
const walletValue = document.createElement('input');
|
const walletValue = document.createElement('input');
|
||||||
walletValue.className = 'input';
|
walletValue.className = 'input';
|
||||||
walletValue.type = 'text';
|
walletValue.type = 'text';
|
||||||
walletValue.value = state.registrationPayment.walletAddress;
|
walletValue.value = state.registrationPayment.walletAddress || '';
|
||||||
walletValue.addEventListener('input', () => {
|
walletValue.addEventListener('input', () => {
|
||||||
state.registrationPayment.walletAddress = walletValue.value;
|
state.registrationPayment.walletAddress = walletValue.value;
|
||||||
});
|
});
|
||||||
@ -71,15 +71,36 @@ export function render({ navigate }) {
|
|||||||
balanceRow.className = 'row wrap-row';
|
balanceRow.className = 'row wrap-row';
|
||||||
|
|
||||||
const balanceValue = document.createElement('strong');
|
const balanceValue = document.createElement('strong');
|
||||||
balanceValue.textContent = `${state.registrationPayment.balanceSOL} SOL`;
|
balanceValue.textContent = `${formatSol(parseBalanceSol(state.registrationPayment.balanceSOL), 6)} SOL`;
|
||||||
|
|
||||||
const refreshButton = document.createElement('button');
|
const refreshButton = document.createElement('button');
|
||||||
refreshButton.className = 'square-btn';
|
refreshButton.className = 'square-btn';
|
||||||
refreshButton.type = 'button';
|
refreshButton.type = 'button';
|
||||||
refreshButton.textContent = '↻';
|
refreshButton.textContent = '↻';
|
||||||
refreshButton.title = 'Обновить';
|
refreshButton.title = 'Обновить';
|
||||||
|
|
||||||
|
const refreshBalance = async () => {
|
||||||
|
const address = String(walletValue.value || '').trim();
|
||||||
|
if (!address) return;
|
||||||
|
refreshButton.disabled = true;
|
||||||
|
try {
|
||||||
|
const balance = await getBalanceSol({
|
||||||
|
endpoint: state.entrySettings.solanaServer,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
state.registrationPayment.balanceSOL = String(balance.sol);
|
||||||
|
balanceValue.textContent = `${formatSol(balance.sol, 6)} SOL`;
|
||||||
|
} catch (error) {
|
||||||
|
status.className = 'status-line is-unavailable';
|
||||||
|
status.textContent = `Не удалось обновить баланс: ${error?.message || 'unknown'}`;
|
||||||
|
status.style.display = '';
|
||||||
|
} finally {
|
||||||
|
refreshButton.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
refreshButton.addEventListener('click', () => {
|
refreshButton.addEventListener('click', () => {
|
||||||
balanceValue.textContent = `${refreshRegistrationBalance()} SOL`;
|
void refreshBalance();
|
||||||
});
|
});
|
||||||
|
|
||||||
balanceRow.append(balanceValue, refreshButton);
|
balanceRow.append(balanceValue, refreshButton);
|
||||||
@ -100,7 +121,7 @@ export function render({ navigate }) {
|
|||||||
const balanceSol = parseBalanceSol(state.registrationPayment.balanceSOL);
|
const balanceSol = parseBalanceSol(state.registrationPayment.balanceSOL);
|
||||||
if (balanceSol < MIN_REGISTER_BALANCE_SOL) {
|
if (balanceSol < MIN_REGISTER_BALANCE_SOL) {
|
||||||
status.className = 'status-line is-unavailable';
|
status.className = 'status-line is-unavailable';
|
||||||
status.textContent = `Недостаточный баланс для регистрации: ${state.registrationPayment.balanceSOL} SOL. Нужно минимум ${MIN_REGISTER_BALANCE_SOL.toFixed(2)} SOL.`;
|
status.textContent = `Недостаточный баланс для регистрации: ${formatSol(balanceSol, 6)} SOL. Нужно минимум ${MIN_REGISTER_BALANCE_SOL.toFixed(2)} SOL.`;
|
||||||
status.style.display = '';
|
status.style.display = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -141,9 +162,9 @@ export function render({ navigate }) {
|
|||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<p class="auth-copy">Для регистрации в Solana нужно заплатить 0,01 SOL (примерно 1 доллар).</p>
|
<p class="auth-copy">Для регистрации в Solana нужно заплатить 0,01 SOL (примерно 1 доллар).</p>
|
||||||
<label class="stack"><span class="field-label">Номер кошелька</span></label>
|
<label class="stack"><span class="field-label">Номер кошелька (wallet.key = device.key)</span></label>
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<span class="field-label">Баланс</span>
|
<span class="field-label">Баланс (Solana)</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
card.children[1].append(walletRow);
|
card.children[1].append(walletRow);
|
||||||
@ -158,5 +179,20 @@ export function render({ navigate }) {
|
|||||||
card,
|
card,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const draftPassword = String(state.registrationDraft.password ?? '');
|
||||||
|
const wallet = await deriveWalletFromPassword(draftPassword);
|
||||||
|
state.registrationPayment.walletAddress = wallet.address;
|
||||||
|
walletValue.value = wallet.address;
|
||||||
|
await refreshBalance();
|
||||||
|
} catch (error) {
|
||||||
|
status.className = 'status-line is-unavailable';
|
||||||
|
status.textContent = `Не удалось подготовить wallet.key: ${error?.message || 'unknown'}`;
|
||||||
|
status.style.display = '';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import { checkServerAvailability, saveEntrySettings, state } from '../state.js';
|
import { saveEntrySettings, state } from '../state.js';
|
||||||
|
import { checkServerAvailabilityByKey } from '../services/server-health-service.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' };
|
export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' };
|
||||||
|
|
||||||
@ -68,9 +69,17 @@ export function render({ navigate }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const runCheck = () => {
|
const runCheck = async () => {
|
||||||
draft[field.key] = input.value.trim();
|
draft[field.key] = input.value.trim();
|
||||||
applyStatus(checkServerAvailability(input.value));
|
checkButton.disabled = true;
|
||||||
|
checkButton.textContent = 'Проверка...';
|
||||||
|
try {
|
||||||
|
const next = await checkServerAvailabilityByKey(field.key, input.value);
|
||||||
|
applyStatus(next);
|
||||||
|
} finally {
|
||||||
|
checkButton.disabled = false;
|
||||||
|
checkButton.textContent = 'Проверить';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
applyStatus(draft.statuses[field.key]);
|
applyStatus(draft.statuses[field.key]);
|
||||||
@ -80,13 +89,17 @@ export function render({ navigate }) {
|
|||||||
draft[field.key] = input.value;
|
draft[field.key] = input.value;
|
||||||
applyStatus('idle');
|
applyStatus('idle');
|
||||||
window.clearTimeout(timers.get(field.key));
|
window.clearTimeout(timers.get(field.key));
|
||||||
timers.set(field.key, window.setTimeout(runCheck, 3000));
|
timers.set(field.key, window.setTimeout(() => {
|
||||||
|
void runCheck();
|
||||||
|
}, 3000));
|
||||||
|
});
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
void runCheck();
|
||||||
});
|
});
|
||||||
input.addEventListener('blur', runCheck);
|
|
||||||
input.addEventListener('keydown', (event) => {
|
input.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
runCheck();
|
void runCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import {
|
||||||
|
deriveWalletFromPassword,
|
||||||
|
formatSol,
|
||||||
|
getBalanceSol,
|
||||||
|
getTopupSiteUrl,
|
||||||
|
requestAirdropSol,
|
||||||
|
} from '../services/solana-wallet-service.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false };
|
export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false };
|
||||||
|
|
||||||
const BUY_LINK = 'https://www.moonpay.com/buy/sol';
|
|
||||||
|
|
||||||
export function render({ navigate }) {
|
export function render({ navigate }) {
|
||||||
const screen = document.createElement('section');
|
const screen = document.createElement('section');
|
||||||
screen.className = 'stack';
|
screen.className = 'stack';
|
||||||
@ -12,10 +17,14 @@ export function render({ navigate }) {
|
|||||||
const walletValue = document.createElement('input');
|
const walletValue = document.createElement('input');
|
||||||
walletValue.className = 'input';
|
walletValue.className = 'input';
|
||||||
walletValue.type = 'text';
|
walletValue.type = 'text';
|
||||||
walletValue.value = state.registrationPayment.walletAddress;
|
walletValue.value = state.registrationPayment.walletAddress || '';
|
||||||
walletValue.readOnly = true;
|
walletValue.readOnly = true;
|
||||||
walletValue.style.fontSize = '13px';
|
walletValue.style.fontSize = '13px';
|
||||||
|
|
||||||
|
const status = document.createElement('p');
|
||||||
|
status.className = 'meta-muted';
|
||||||
|
status.textContent = 'Проверяем кошелек...';
|
||||||
|
|
||||||
const copyButton = document.createElement('button');
|
const copyButton = document.createElement('button');
|
||||||
copyButton.className = 'ghost-btn';
|
copyButton.className = 'ghost-btn';
|
||||||
copyButton.type = 'button';
|
copyButton.type = 'button';
|
||||||
@ -39,15 +48,14 @@ export function render({ navigate }) {
|
|||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card stack';
|
card.className = 'card stack';
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<p class="auth-copy">Для пополнения счета скопируйте номер кошелька.</p>
|
<p class="auth-copy">Кнопка «Пополнить» в кошельке будет переводить на отдельный сайт. Пока доступно тестовое пополнение.</p>
|
||||||
<div class="stack" style="gap:6px;">
|
<div class="stack" style="gap:6px;">
|
||||||
<p class="meta-muted">1. Пополните через любое свое приложение, используя этот кошелек в сети Solana.</p>
|
<p class="meta-muted">1. Вы можете открыть сайт для покупки SOL.</p>
|
||||||
<p class="meta-muted">2. Либо откройте страницу для покупки SOL.</p>
|
<p class="meta-muted">2. Либо нажать «Тестовое пополнение» и получить 1 SOL через DevNet airdrop.</p>
|
||||||
<p class="meta-muted">3. Либо используйте кнопку «Тестовое пополнение» (работает в тестовой Solana).</p>
|
|
||||||
</div>
|
</div>
|
||||||
<a class="link-card" href="${BUY_LINK}" target="_blank" rel="noreferrer">Открыть страницу покупки SOL</a>
|
<a class="link-card" href="${getTopupSiteUrl()}" target="_blank" rel="noreferrer">Открыть сайт пополнения</a>
|
||||||
<div class="card stack" style="padding:12px; max-width:320px;">
|
<div class="card stack" style="padding:12px; max-width:320px;">
|
||||||
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения</div>
|
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (wallet.key)</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
card.children[3].append(walletRow);
|
card.children[3].append(walletRow);
|
||||||
@ -55,10 +63,31 @@ export function render({ navigate }) {
|
|||||||
const testButton = document.createElement('button');
|
const testButton = document.createElement('button');
|
||||||
testButton.className = 'ghost-btn';
|
testButton.className = 'ghost-btn';
|
||||||
testButton.type = 'button';
|
testButton.type = 'button';
|
||||||
testButton.textContent = 'Тестовое пополнение';
|
testButton.textContent = 'Тестовое пополнение (1 SOL)';
|
||||||
testButton.addEventListener('click', () => {
|
testButton.addEventListener('click', async () => {
|
||||||
state.registrationPayment.balanceSOL = '0.0250';
|
const address = String(walletValue.value || '').trim();
|
||||||
window.alert('Тестовое пополнение выполнено. Баланс обновлён.');
|
if (!address) {
|
||||||
|
window.alert('Адрес кошелька не найден.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
testButton.disabled = true;
|
||||||
|
try {
|
||||||
|
const drop = await requestAirdropSol({
|
||||||
|
endpoint: state.entrySettings.solanaServer,
|
||||||
|
address,
|
||||||
|
amountSol: 1,
|
||||||
|
});
|
||||||
|
const bal = await getBalanceSol({
|
||||||
|
endpoint: state.entrySettings.solanaServer,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
state.registrationPayment.balanceSOL = String(bal.sol);
|
||||||
|
status.textContent = `Тестовое пополнение выполнено. Новый баланс: ${formatSol(bal.sol, 6)} SOL. Signature: ${drop.signature}`;
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = `Ошибка тестового пополнения: ${error?.message || 'unknown'}`;
|
||||||
|
} finally {
|
||||||
|
testButton.disabled = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const backButton = document.createElement('button');
|
const backButton = document.createElement('button');
|
||||||
@ -71,14 +100,34 @@ export function render({ navigate }) {
|
|||||||
actions.className = 'auth-footer-actions';
|
actions.className = 'auth-footer-actions';
|
||||||
actions.append(testButton, backButton);
|
actions.append(testButton, backButton);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (!walletValue.value) {
|
||||||
|
const wallet = await deriveWalletFromPassword(String(state.registrationDraft.password ?? ''));
|
||||||
|
state.registrationPayment.walletAddress = wallet.address;
|
||||||
|
walletValue.value = wallet.address;
|
||||||
|
}
|
||||||
|
const balance = await getBalanceSol({
|
||||||
|
endpoint: state.entrySettings.solanaServer,
|
||||||
|
address: walletValue.value,
|
||||||
|
});
|
||||||
|
state.registrationPayment.balanceSOL = String(balance.sol);
|
||||||
|
status.textContent = `Текущий баланс: ${formatSol(balance.sol, 6)} SOL`;
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = `Не удалось получить баланс: ${error?.message || 'unknown'}`;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
screen.append(
|
screen.append(
|
||||||
renderHeader({
|
renderHeader({
|
||||||
title: 'Пополнение счета',
|
title: 'Пополнение счета',
|
||||||
leftAction: { label: '←', onClick: () => navigate('registration-payment-view') },
|
leftAction: { label: '←', onClick: () => navigate('registration-payment-view') },
|
||||||
}),
|
}),
|
||||||
card,
|
card,
|
||||||
|
status,
|
||||||
actions,
|
actions,
|
||||||
);
|
);
|
||||||
|
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,42 +1,67 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import { wallet } from '../mock-data.js';
|
import { state } from '../state.js';
|
||||||
|
import {
|
||||||
|
formatSol,
|
||||||
|
getBalanceSol,
|
||||||
|
getTopupSiteUrl,
|
||||||
|
getWalletFromStoredDeviceKey,
|
||||||
|
requestAirdropSol,
|
||||||
|
transferSol,
|
||||||
|
} from '../services/solana-wallet-service.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' };
|
export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' };
|
||||||
|
|
||||||
|
function nowRu() {
|
||||||
|
return new Date().toLocaleString('ru-RU');
|
||||||
|
}
|
||||||
|
|
||||||
export function render({ navigate }) {
|
export function render({ navigate }) {
|
||||||
const screen = document.createElement('section');
|
const screen = document.createElement('section');
|
||||||
screen.className = 'stack';
|
screen.className = 'stack';
|
||||||
let statusText = 'Данные демонстрационные';
|
|
||||||
|
let walletCtx = null;
|
||||||
|
let walletAddress = '';
|
||||||
|
|
||||||
const status = document.createElement('p');
|
const status = document.createElement('p');
|
||||||
status.className = 'meta-muted';
|
status.className = 'meta-muted';
|
||||||
|
status.textContent = 'Инициализация wallet.key...';
|
||||||
|
|
||||||
const updateStatus = (text) => {
|
const screenTitle = 'Кошелёк';
|
||||||
statusText = text;
|
|
||||||
status.textContent = statusText;
|
|
||||||
};
|
|
||||||
|
|
||||||
screen.append(
|
screen.append(
|
||||||
renderHeader({
|
renderHeader({
|
||||||
title: 'Кошелёк',
|
title: screenTitle,
|
||||||
leftAction: { label: '←', onClick: () => navigate('profile-view') },
|
leftAction: { label: '←', onClick: () => navigate('profile-view') },
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card stack';
|
card.className = 'card stack';
|
||||||
card.innerHTML = `
|
|
||||||
<div>
|
const balanceWrap = document.createElement('div');
|
||||||
<p class="meta-muted">Баланс</p>
|
const balanceLabel = document.createElement('p');
|
||||||
<h2 style="font-size:30px;">${wallet.balanceSOL} SOL</h2>
|
balanceLabel.className = 'meta-muted';
|
||||||
<p class="meta-muted">Обновлено: ${wallet.updatedAt}</p>
|
balanceLabel.textContent = 'Баланс (Solana)';
|
||||||
</div>
|
const balanceValue = document.createElement('h2');
|
||||||
<div class="card" style="padding:10px;">
|
balanceValue.style.fontSize = '30px';
|
||||||
<p class="meta-muted" style="margin-bottom:6px;">Публичный адрес</p>
|
balanceValue.textContent = '— SOL';
|
||||||
<p style="font-size:13px; line-height:1.4; word-break:break-all;">${wallet.publicAddress}</p>
|
const updatedLabel = document.createElement('p');
|
||||||
</div>
|
updatedLabel.className = 'meta-muted';
|
||||||
|
updatedLabel.textContent = 'Обновлено: —';
|
||||||
|
const endpointLabel = document.createElement('p');
|
||||||
|
endpointLabel.className = 'meta-muted';
|
||||||
|
endpointLabel.textContent = `RPC: ${state.entrySettings.solanaServer}`;
|
||||||
|
balanceWrap.append(balanceLabel, balanceValue, updatedLabel, endpointLabel);
|
||||||
|
|
||||||
|
const addressCard = document.createElement('div');
|
||||||
|
addressCard.className = 'card';
|
||||||
|
addressCard.style.padding = '10px';
|
||||||
|
addressCard.innerHTML = `
|
||||||
|
<p class="meta-muted" style="margin-bottom:6px;">Публичный адрес (wallet.key = device.key)</p>
|
||||||
|
<p style="font-size:13px; line-height:1.4; word-break:break-all;" id="wallet-address-value">—</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
card.append(balanceWrap, addressCard);
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'stack';
|
actions.className = 'stack';
|
||||||
actions.innerHTML = `
|
actions.innerHTML = `
|
||||||
@ -50,29 +75,126 @@ export function render({ navigate }) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
actions.querySelector('#copy-address').addEventListener('click', async () => {
|
const copyBtn = actions.querySelector('#copy-address');
|
||||||
|
const refreshBtn = actions.querySelector('#refresh-balance');
|
||||||
|
const sendBtn = actions.querySelector('#send-sol');
|
||||||
|
const topupBtn = actions.querySelector('#topup-sol');
|
||||||
|
const addressEl = addressCard.querySelector('#wallet-address-value');
|
||||||
|
|
||||||
|
const setStatus = (text) => {
|
||||||
|
status.textContent = String(text || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshBalance = async () => {
|
||||||
|
if (!walletAddress) {
|
||||||
|
setStatus('Кошелёк не инициализирован.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(wallet.publicAddress);
|
const balance = await getBalanceSol({
|
||||||
updateStatus('Адрес скопирован в буфер обмена');
|
endpoint: state.entrySettings.solanaServer,
|
||||||
|
address: walletAddress,
|
||||||
|
});
|
||||||
|
balanceValue.textContent = `${formatSol(balance.sol, 6)} SOL`;
|
||||||
|
updatedLabel.textContent = `Обновлено: ${nowRu()}`;
|
||||||
|
endpointLabel.textContent = `RPC: ${balance.endpoint}`;
|
||||||
|
setStatus('Баланс обновлён.');
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`Не удалось получить баланс: ${error?.message || 'unknown'}`);
|
||||||
|
} finally {
|
||||||
|
refreshBtn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', async () => {
|
||||||
|
if (!walletAddress) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(walletAddress);
|
||||||
|
setStatus('Адрес скопирован в буфер обмена');
|
||||||
} catch {
|
} catch {
|
||||||
updateStatus('Не удалось скопировать в этом браузере');
|
setStatus('Не удалось скопировать адрес в этом браузере');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.querySelector('#refresh-balance').addEventListener('click', () => {
|
refreshBtn.addEventListener('click', () => {
|
||||||
updateStatus(`Баланс обновлен: ${wallet.balanceSOL} SOL`);
|
void refreshBalance();
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.querySelector('#send-sol').addEventListener('click', () => {
|
sendBtn.addEventListener('click', async () => {
|
||||||
updateStatus('Демо-функция: перевод будет добавлен позже');
|
if (!walletCtx?.keypair) {
|
||||||
|
setStatus('Перевод недоступен: wallet.key не загружен.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toAddress = window.prompt('Введите адрес получателя (Solana):', '');
|
||||||
|
if (!toAddress) return;
|
||||||
|
const amountRaw = window.prompt('Введите сумму SOL для перевода:', '0.01');
|
||||||
|
if (!amountRaw) return;
|
||||||
|
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
const tx = await transferSol({
|
||||||
|
endpoint: state.entrySettings.solanaServer,
|
||||||
|
fromKeypair: walletCtx.keypair,
|
||||||
|
toAddress,
|
||||||
|
amountSol: Number(String(amountRaw || '').replace(',', '.')),
|
||||||
|
});
|
||||||
|
setStatus(`Перевод отправлен. Signature: ${tx.signature}`);
|
||||||
|
await refreshBalance();
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`Ошибка перевода: ${error?.message || 'unknown'}`);
|
||||||
|
} finally {
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.querySelector('#topup-sol').addEventListener('click', () => {
|
topupBtn.addEventListener('click', async () => {
|
||||||
updateStatus('Демо-функция: пополнение будет добавлено позже');
|
if (!walletAddress) {
|
||||||
|
setStatus('Кошелёк не инициализирован.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSite = window.confirm(
|
||||||
|
'Кнопка будет переводить на отдельный сайт пополнения.\n\nНажмите OK, чтобы открыть сайт.\nНажмите Отмена, чтобы выполнить тестовое пополнение (airdrop 1 SOL на DevNet).'
|
||||||
|
);
|
||||||
|
if (openSite) {
|
||||||
|
window.open(getTopupSiteUrl(), '_blank', 'noopener,noreferrer');
|
||||||
|
setStatus('Открыт сайт пополнения. Пока также доступно тестовое пополнение.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
topupBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
const drop = await requestAirdropSol({
|
||||||
|
endpoint: state.entrySettings.solanaServer,
|
||||||
|
address: walletAddress,
|
||||||
|
amountSol: 1,
|
||||||
|
});
|
||||||
|
setStatus(`Тестовое пополнение выполнено (airdrop 1 SOL). Signature: ${drop.signature}`);
|
||||||
|
await refreshBalance();
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`Ошибка тестового пополнения: ${error?.message || 'unknown'}`);
|
||||||
|
} finally {
|
||||||
|
topupBtn.disabled = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updateStatus(statusText);
|
(async () => {
|
||||||
|
try {
|
||||||
|
walletCtx = await getWalletFromStoredDeviceKey({
|
||||||
|
login: state.session.login,
|
||||||
|
storagePwd: state.session.storagePwdInMemory,
|
||||||
|
});
|
||||||
|
walletAddress = walletCtx.address;
|
||||||
|
addressEl.textContent = walletAddress;
|
||||||
|
await refreshBalance();
|
||||||
|
} catch (error) {
|
||||||
|
addressEl.textContent = 'wallet.key недоступен';
|
||||||
|
setStatus(`Не удалось инициализировать кошелёк: ${error?.message || 'unknown'}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
screen.append(card, actions, status);
|
screen.append(card, actions, status);
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -645,7 +645,7 @@ export class AuthService {
|
|||||||
const addResp = await this.ws.request('AddUser', {
|
const addResp = await this.ws.request('AddUser', {
|
||||||
login: cleanLogin,
|
login: cleanLogin,
|
||||||
blockchainName: `${cleanLogin}-${BCH_SUFFIX}`,
|
blockchainName: `${cleanLogin}-${BCH_SUFFIX}`,
|
||||||
solanaKey: keyBundle.rootPair.publicKeyB64,
|
solanaKey: keyBundle.devicePair.publicKeyB64,
|
||||||
blockchainKey: keyBundle.blockchainPair.publicKeyB64,
|
blockchainKey: keyBundle.blockchainPair.publicKeyB64,
|
||||||
deviceKey: keyBundle.devicePair.publicKeyB64,
|
deviceKey: keyBundle.devicePair.publicKeyB64,
|
||||||
bchLimit: 1000000,
|
bchLimit: 1000000,
|
||||||
|
|||||||
118
shine-UI/js/services/server-health-service.js
Normal file
118
shine-UI/js/services/server-health-service.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
function normalizeUrl(value) {
|
||||||
|
return String(value || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeShineWsUrl(rawUrl) {
|
||||||
|
const value = normalizeUrl(rawUrl);
|
||||||
|
if (!value) return '';
|
||||||
|
if (value.startsWith('ws://') || value.startsWith('wss://')) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(value);
|
||||||
|
if (!parsed.pathname || parsed.pathname === '/') parsed.pathname = '/ws';
|
||||||
|
return parsed.toString();
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value.startsWith('http://') || value.startsWith('https://')) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(value);
|
||||||
|
parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
if (!parsed.pathname || parsed.pathname === '/') parsed.pathname = '/ws';
|
||||||
|
return parsed.toString();
|
||||||
|
} catch {
|
||||||
|
return `${value.replace(/^http/, 'ws').replace(/\/$/, '')}/ws`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSolanaRpc(url) {
|
||||||
|
const endpoint = normalizeUrl(url);
|
||||||
|
if (!endpoint) return false;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
method: 'getVersion',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!resp.ok) return false;
|
||||||
|
const json = await resp.json();
|
||||||
|
return Boolean(json?.result?.['solana-core'] || json?.result);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkArweave(url) {
|
||||||
|
const base = normalizeUrl(url).replace(/\/+$/, '');
|
||||||
|
if (!base) return false;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${base}/info`, { method: 'GET', cache: 'no-store' });
|
||||||
|
if (!resp.ok) return false;
|
||||||
|
const json = await resp.json();
|
||||||
|
return Boolean(json?.network);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkShineWs(url, timeoutMs = 7000) {
|
||||||
|
const wsUrl = normalizeShineWsUrl(url);
|
||||||
|
if (!wsUrl) return Promise.resolve(false);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let done = false;
|
||||||
|
const finish = (ok) => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
try { ws.close(); } catch {}
|
||||||
|
resolve(Boolean(ok));
|
||||||
|
};
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => finish(false), timeoutMs);
|
||||||
|
let ws;
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
} catch {
|
||||||
|
finish(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.addEventListener('open', () => {
|
||||||
|
try {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
op: 'Ping',
|
||||||
|
requestId: `check-${Date.now()}`,
|
||||||
|
payload: { ts: Date.now() },
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
finish(false);
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
ws.addEventListener('message', () => finish(true), { once: true });
|
||||||
|
ws.addEventListener('error', () => finish(false), { once: true });
|
||||||
|
ws.addEventListener('close', () => finish(false), { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkServerAvailabilityByKey(key, url) {
|
||||||
|
if (key === 'solanaServer') {
|
||||||
|
return (await checkSolanaRpc(url)) ? 'available' : 'unavailable';
|
||||||
|
}
|
||||||
|
if (key === 'shineServer') {
|
||||||
|
return (await checkShineWs(url)) ? 'available' : 'unavailable';
|
||||||
|
}
|
||||||
|
if (key === 'arweaveServer') {
|
||||||
|
return (await checkArweave(url)) ? 'available' : 'unavailable';
|
||||||
|
}
|
||||||
|
return 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
126
shine-UI/js/services/solana-wallet-service.js
Normal file
126
shine-UI/js/services/solana-wallet-service.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { base64ToBytes, deriveEd25519FromPassword } from './crypto-utils.js';
|
||||||
|
import { loadEncryptedUserSecrets } from './key-vault.js';
|
||||||
|
|
||||||
|
const DEFAULT_SOLANA_ENDPOINT = 'https://api.devnet.solana.com';
|
||||||
|
const TOPUP_SITE_URL = 'https://www.moonpay.com/buy/sol';
|
||||||
|
|
||||||
|
let solanaLibPromise = null;
|
||||||
|
|
||||||
|
function normalizeEndpoint(url) {
|
||||||
|
const raw = String(url || '').trim();
|
||||||
|
if (!raw) return DEFAULT_SOLANA_ENDPOINT;
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSolanaLib() {
|
||||||
|
if (!solanaLibPromise) {
|
||||||
|
solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
|
||||||
|
}
|
||||||
|
return solanaLibPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSeed32FromPkcs8B64(pkcs8B64) {
|
||||||
|
const bytes = base64ToBytes(String(pkcs8B64 || '').trim());
|
||||||
|
if (bytes.length < 32) throw new Error('Некорректный PKCS8 ключ device.key');
|
||||||
|
return bytes.slice(bytes.length - 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function keypairFromPkcs8(pkcs8B64) {
|
||||||
|
const solana = await loadSolanaLib();
|
||||||
|
const seed32 = extractSeed32FromPkcs8B64(pkcs8B64);
|
||||||
|
return solana.Keypair.fromSeed(seed32);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deriveWalletFromPassword(password) {
|
||||||
|
const keyBundle = await deriveEd25519FromPassword(String(password ?? ''), 'dev.key');
|
||||||
|
const keypair = await keypairFromPkcs8(keyBundle.privatePkcs8B64);
|
||||||
|
return {
|
||||||
|
address: keypair.publicKey.toBase58(),
|
||||||
|
keypair,
|
||||||
|
devicePublicKeyB64: keyBundle.publicKeyB64,
|
||||||
|
devicePrivatePkcs8B64: keyBundle.privatePkcs8B64,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWalletFromStoredDeviceKey({ login, storagePwd }) {
|
||||||
|
const cleanLogin = String(login || '').trim();
|
||||||
|
const cleanPwd = String(storagePwd || '').trim();
|
||||||
|
if (!cleanLogin || !cleanPwd) {
|
||||||
|
throw new Error('Нет активной сессии для доступа к wallet.key');
|
||||||
|
}
|
||||||
|
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
|
||||||
|
const devicePrivate = String(secrets?.deviceKey || '').trim();
|
||||||
|
if (!devicePrivate) {
|
||||||
|
throw new Error('На устройстве не найден device.key (wallet.key)');
|
||||||
|
}
|
||||||
|
const keypair = await keypairFromPkcs8(devicePrivate);
|
||||||
|
return {
|
||||||
|
address: keypair.publicKey.toBase58(),
|
||||||
|
keypair,
|
||||||
|
devicePrivatePkcs8B64: devicePrivate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBalanceSol({ endpoint, address }) {
|
||||||
|
const solana = await loadSolanaLib();
|
||||||
|
const rpc = normalizeEndpoint(endpoint);
|
||||||
|
const conn = new solana.Connection(rpc, 'confirmed');
|
||||||
|
const pubkey = new solana.PublicKey(String(address || '').trim());
|
||||||
|
const lamports = await conn.getBalance(pubkey, 'confirmed');
|
||||||
|
return {
|
||||||
|
endpoint: rpc,
|
||||||
|
lamports,
|
||||||
|
sol: lamports / solana.LAMPORTS_PER_SOL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestAirdropSol({ endpoint, address, amountSol = 1 }) {
|
||||||
|
const solana = await loadSolanaLib();
|
||||||
|
const rpc = normalizeEndpoint(endpoint);
|
||||||
|
const conn = new solana.Connection(rpc, 'confirmed');
|
||||||
|
const pubkey = new solana.PublicKey(String(address || '').trim());
|
||||||
|
const lamports = Math.max(1, Math.floor(Number(amountSol) * solana.LAMPORTS_PER_SOL));
|
||||||
|
const signature = await conn.requestAirdrop(pubkey, lamports);
|
||||||
|
await conn.confirmTransaction(signature, 'confirmed');
|
||||||
|
return { endpoint: rpc, signature, lamports };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transferSol({ endpoint, fromKeypair, toAddress, amountSol }) {
|
||||||
|
const solana = await loadSolanaLib();
|
||||||
|
const rpc = normalizeEndpoint(endpoint);
|
||||||
|
const cleanTo = String(toAddress || '').trim();
|
||||||
|
const amount = Number(amountSol);
|
||||||
|
if (!cleanTo) throw new Error('Не указан адрес получателя');
|
||||||
|
if (!Number.isFinite(amount) || amount <= 0) throw new Error('Сумма перевода должна быть больше 0');
|
||||||
|
|
||||||
|
const conn = new solana.Connection(rpc, 'confirmed');
|
||||||
|
const lamports = Math.floor(amount * solana.LAMPORTS_PER_SOL);
|
||||||
|
if (lamports <= 0) throw new Error('Сумма слишком мала');
|
||||||
|
|
||||||
|
const tx = new solana.Transaction().add(
|
||||||
|
solana.SystemProgram.transfer({
|
||||||
|
fromPubkey: fromKeypair.publicKey,
|
||||||
|
toPubkey: new solana.PublicKey(cleanTo),
|
||||||
|
lamports,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const signature = await solana.sendAndConfirmTransaction(conn, tx, [fromKeypair], {
|
||||||
|
commitment: 'confirmed',
|
||||||
|
});
|
||||||
|
return { endpoint: rpc, signature, lamports };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSol(value, digits = 6) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n)) return '0';
|
||||||
|
return n.toLocaleString('ru-RU', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: digits,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTopupSiteUrl() {
|
||||||
|
return TOPUP_SITE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { wallet } from './mock-data.js';
|
|
||||||
import { AuthService } from './services/auth-service.js';
|
import { AuthService } from './services/auth-service.js';
|
||||||
import { clearClientAuthData } from './services/key-vault.js';
|
import { clearClientAuthData } from './services/key-vault.js';
|
||||||
import { clearStoredMessages, listStoredMessages, putStoredMessage } from './services/message-store.js';
|
import { clearStoredMessages, listStoredMessages, putStoredMessage } from './services/message-store.js';
|
||||||
@ -7,6 +6,7 @@ const clone = (value) => JSON.parse(JSON.stringify(value));
|
|||||||
const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1';
|
const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1';
|
||||||
const REACTIONS_STORAGE_KEY = 'shine-ui-message-reactions-v2';
|
const REACTIONS_STORAGE_KEY = 'shine-ui-message-reactions-v2';
|
||||||
const WEB_PUSH_SUBSCRIPTION_KEY = 'shine-ui-webpush-subscription-v1';
|
const WEB_PUSH_SUBSCRIPTION_KEY = 'shine-ui-webpush-subscription-v1';
|
||||||
|
const ENTRY_SETTINGS_STORAGE_KEY = 'shine-ui-entry-settings-v1';
|
||||||
const CHANNEL_NOTIFY_KEY = 'shine-channels-notify-v1';
|
const CHANNEL_NOTIFY_KEY = 'shine-channels-notify-v1';
|
||||||
const CHANNELS_DEMO_KEY = 'shine-channels-demo';
|
const CHANNELS_DEMO_KEY = 'shine-channels-demo';
|
||||||
const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
|
const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
|
||||||
@ -77,7 +77,9 @@ function inferTunnelWsUrl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LOCAL_WS_OVERRIDE_URL = readLocalWsOverrideUrl() || inferTunnelWsUrl();
|
const LOCAL_WS_OVERRIDE_URL = readLocalWsOverrideUrl() || inferTunnelWsUrl();
|
||||||
|
const DEFAULT_SOLANA_SERVER = 'https://api.devnet.solana.com';
|
||||||
const DEFAULT_SHINE_SERVER = 'wss://shineup.me/ws';
|
const DEFAULT_SHINE_SERVER = 'wss://shineup.me/ws';
|
||||||
|
const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
|
||||||
|
|
||||||
function loadStoredSession() {
|
function loadStoredSession() {
|
||||||
try {
|
try {
|
||||||
@ -125,6 +127,37 @@ function clearStoredSession() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadStoredEntrySettings() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(ENTRY_SETTINGS_STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed || typeof parsed !== 'object') return null;
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistEntrySettings(settings) {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
language: String(settings?.language || 'ru'),
|
||||||
|
solanaServer: String(settings?.solanaServer || DEFAULT_SOLANA_SERVER),
|
||||||
|
shineServer: String(settings?.shineServer || DEFAULT_SHINE_SERVER),
|
||||||
|
arweaveServer: String(settings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
|
||||||
|
statuses: {
|
||||||
|
solanaServer: String(settings?.statuses?.solanaServer || 'idle'),
|
||||||
|
shineServer: String(settings?.statuses?.shineServer || 'idle'),
|
||||||
|
arweaveServer: String(settings?.statuses?.arweaveServer || 'idle'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
localStorage.setItem(ENTRY_SETTINGS_STORAGE_KEY, JSON.stringify(payload));
|
||||||
|
} catch {
|
||||||
|
// ignore storage errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clearBrowserClientData() {
|
function clearBrowserClientData() {
|
||||||
const localKeys = [
|
const localKeys = [
|
||||||
SESSION_STORAGE_KEY,
|
SESSION_STORAGE_KEY,
|
||||||
@ -151,6 +184,7 @@ function clearBrowserClientData() {
|
|||||||
function createInitialState({ withStoredSession = true } = {}) {
|
function createInitialState({ withStoredSession = true } = {}) {
|
||||||
const storedSession = withStoredSession ? loadStoredSession() : null;
|
const storedSession = withStoredSession ? loadStoredSession() : null;
|
||||||
const storedReactions = loadStoredReactions();
|
const storedReactions = loadStoredReactions();
|
||||||
|
const storedEntrySettings = loadStoredEntrySettings();
|
||||||
const initialShineServer = LOCAL_WS_OVERRIDE_URL || DEFAULT_SHINE_SERVER;
|
const initialShineServer = LOCAL_WS_OVERRIDE_URL || DEFAULT_SHINE_SERVER;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -170,14 +204,14 @@ function createInitialState({ withStoredSession = true } = {}) {
|
|||||||
},
|
},
|
||||||
startHint: '',
|
startHint: '',
|
||||||
entrySettings: {
|
entrySettings: {
|
||||||
language: 'ru',
|
language: String(storedEntrySettings?.language || 'ru'),
|
||||||
solanaServer: 'https://api.mainnet-beta.solana.com',
|
solanaServer: String(storedEntrySettings?.solanaServer || DEFAULT_SOLANA_SERVER),
|
||||||
shineServer: initialShineServer,
|
shineServer: String(LOCAL_WS_OVERRIDE_URL || storedEntrySettings?.shineServer || initialShineServer),
|
||||||
arweaveServer: 'https://arweave.net',
|
arweaveServer: String(storedEntrySettings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
|
||||||
statuses: {
|
statuses: {
|
||||||
solanaServer: 'idle',
|
solanaServer: String(storedEntrySettings?.statuses?.solanaServer || 'idle'),
|
||||||
shineServer: 'idle',
|
shineServer: String(storedEntrySettings?.statuses?.shineServer || 'idle'),
|
||||||
arweaveServer: 'idle',
|
arweaveServer: String(storedEntrySettings?.statuses?.arweaveServer || 'idle'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
registrationDraft: {
|
registrationDraft: {
|
||||||
@ -194,8 +228,8 @@ function createInitialState({ withStoredSession = true } = {}) {
|
|||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
registrationPayment: {
|
registrationPayment: {
|
||||||
walletAddress: wallet.publicAddress,
|
walletAddress: '',
|
||||||
balanceSOL: '0.0068',
|
balanceSOL: '0.0000',
|
||||||
},
|
},
|
||||||
keyStorage: {
|
keyStorage: {
|
||||||
rootKey: 'Ключ root хранится в зашифрованном виде',
|
rootKey: 'Ключ root хранится в зашифрованном виде',
|
||||||
@ -478,12 +512,9 @@ export function ensureChat(chatId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function checkServerAvailability(address) {
|
export function checkServerAvailability(address) {
|
||||||
const normalized = address.trim().toLowerCase();
|
const normalized = String(address || '').trim().toLowerCase();
|
||||||
if (!normalized) return 'unavailable';
|
if (!normalized) return 'unavailable';
|
||||||
|
return /^(https?:\/\/|wss?:\/\/)/i.test(normalized) ? 'available' : 'unavailable';
|
||||||
const looksLikeUrl = /^(https?:\/\/|wss?:\/\/)[a-z0-9.-]+/i.test(normalized);
|
|
||||||
const blockedWord = /(offline|down|fail|bad|broken|invalid)/i.test(normalized);
|
|
||||||
return looksLikeUrl && !blockedWord ? 'available' : 'unavailable';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveEntrySettings(nextSettings) {
|
export async function saveEntrySettings(nextSettings) {
|
||||||
@ -497,6 +528,7 @@ export async function saveEntrySettings(nextSettings) {
|
|||||||
...(nextSettings.statuses || {}),
|
...(nextSettings.statuses || {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
persistEntrySettings(state.entrySettings);
|
||||||
await authService.reconnect(state.entrySettings.shineServer);
|
await authService.reconnect(state.entrySettings.shineServer);
|
||||||
state.startHint = 'Настройки входа сохранены, адреса серверов обновлены.';
|
state.startHint = 'Настройки входа сохранены, адреса серверов обновлены.';
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user