feat(ui): кошелек на device.key и проверки серверов (тоже пока не проверено)

This commit is contained in:
AidarKC
2026-04-21 03:12:22 +03:00
parent 8be56192cb
commit e63c53a855
9 changed files with 588 additions and 79 deletions
+19 -6
View File
@@ -1,5 +1,6 @@
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 };
@@ -86,9 +87,17 @@ export function render({ navigate }) {
}
};
const runCheck = () => {
const runCheck = async () => {
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]);
@@ -98,13 +107,17 @@ export function render({ navigate }) {
draft[field.key] = input.value;
applyStatus('idle');
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) => {
if (event.key === 'Enter') {
event.preventDefault();
runCheck();
void runCheck();
}
});
+44 -8
View File
@@ -1,12 +1,12 @@
import { renderHeader } from '../components/header.js';
import { renderHeader } from '../components/header.js';
import {
authService,
refreshRegistrationBalance,
setAuthError,
setAuthInfo,
state,
} from '../state.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 };
@@ -39,7 +39,7 @@ export function render({ navigate }) {
const walletValue = document.createElement('input');
walletValue.className = 'input';
walletValue.type = 'text';
walletValue.value = state.registrationPayment.walletAddress;
walletValue.value = state.registrationPayment.walletAddress || '';
walletValue.addEventListener('input', () => {
state.registrationPayment.walletAddress = walletValue.value;
});
@@ -71,15 +71,36 @@ export function render({ navigate }) {
balanceRow.className = 'row wrap-row';
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');
refreshButton.className = 'square-btn';
refreshButton.type = 'button';
refreshButton.textContent = '↻';
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', () => {
balanceValue.textContent = `${refreshRegistrationBalance()} SOL`;
void refreshBalance();
});
balanceRow.append(balanceValue, refreshButton);
@@ -100,7 +121,7 @@ export function render({ navigate }) {
const balanceSol = parseBalanceSol(state.registrationPayment.balanceSOL);
if (balanceSol < MIN_REGISTER_BALANCE_SOL) {
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 = '';
return;
}
@@ -141,9 +162,9 @@ export function render({ navigate }) {
card.innerHTML = `
<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">
<span class="field-label">Баланс</span>
<span class="field-label">Баланс (Solana)</span>
</div>
`;
card.children[1].append(walletRow);
@@ -158,5 +179,20 @@ export function render({ navigate }) {
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;
}
+19 -6
View File
@@ -1,5 +1,6 @@
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: 'Настройки серверов' };
@@ -68,9 +69,17 @@ export function render({ navigate }) {
}
};
const runCheck = () => {
const runCheck = async () => {
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]);
@@ -80,13 +89,17 @@ export function render({ navigate }) {
draft[field.key] = input.value;
applyStatus('idle');
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) => {
if (event.key === 'Enter') {
event.preventDefault();
runCheck();
void runCheck();
}
});
+62 -13
View File
@@ -1,10 +1,15 @@
import { renderHeader } from '../components/header.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 };
const BUY_LINK = 'https://www.moonpay.com/buy/sol';
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
@@ -12,10 +17,14 @@ export function render({ navigate }) {
const walletValue = document.createElement('input');
walletValue.className = 'input';
walletValue.type = 'text';
walletValue.value = state.registrationPayment.walletAddress;
walletValue.value = state.registrationPayment.walletAddress || '';
walletValue.readOnly = true;
walletValue.style.fontSize = '13px';
const status = document.createElement('p');
status.className = 'meta-muted';
status.textContent = 'Проверяем кошелек...';
const copyButton = document.createElement('button');
copyButton.className = 'ghost-btn';
copyButton.type = 'button';
@@ -39,15 +48,14 @@ export function render({ navigate }) {
const card = document.createElement('div');
card.className = 'card stack';
card.innerHTML = `
<p class="auth-copy">Для пополнения счета скопируйте номер кошелька.</p>
<p class="auth-copy">Кнопка «Пополнить» в кошельке будет переводить на отдельный сайт. Пока доступно тестовое пополнение.</p>
<div class="stack" style="gap:6px;">
<p class="meta-muted">1. Пополните через любое свое приложение, используя этот кошелек в сети Solana.</p>
<p class="meta-muted">2. Либо откройте страницу для покупки SOL.</p>
<p class="meta-muted">3. Либо используйте кнопку «Тестовое пополнение» (работает в тестовой Solana).</p>
<p class="meta-muted">1. Вы можете открыть сайт для покупки SOL.</p>
<p class="meta-muted">2. Либо нажать «Тестовое пополнение» и получить 1 SOL через DevNet airdrop.</p>
</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="field-label" style="margin-bottom:6px;">Кошелёк для пополнения</div>
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (wallet.key)</div>
</div>
`;
card.children[3].append(walletRow);
@@ -55,10 +63,31 @@ export function render({ navigate }) {
const testButton = document.createElement('button');
testButton.className = 'ghost-btn';
testButton.type = 'button';
testButton.textContent = 'Тестовое пополнение';
testButton.addEventListener('click', () => {
state.registrationPayment.balanceSOL = '0.0250';
window.alert('Тестовое пополнение выполнено. Баланс обновлён.');
testButton.textContent = 'Тестовое пополнение (1 SOL)';
testButton.addEventListener('click', async () => {
const address = String(walletValue.value || '').trim();
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');
@@ -71,14 +100,34 @@ export function render({ navigate }) {
actions.className = 'auth-footer-actions';
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(
renderHeader({
title: 'Пополнение счета',
leftAction: { label: '←', onClick: () => navigate('registration-payment-view') },
}),
card,
status,
actions,
);
return screen;
}
+152 -30
View File
@@ -1,42 +1,67 @@
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: 'Кошелёк' };
function nowRu() {
return new Date().toLocaleString('ru-RU');
}
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
let statusText = 'Данные демонстрационные';
let walletCtx = null;
let walletAddress = '';
const status = document.createElement('p');
status.className = 'meta-muted';
status.textContent = 'Инициализация wallet.key...';
const updateStatus = (text) => {
statusText = text;
status.textContent = statusText;
};
const screenTitle = 'Кошелёк';
screen.append(
renderHeader({
title: 'Кошелёк',
title: screenTitle,
leftAction: { label: '←', onClick: () => navigate('profile-view') },
})
}),
);
const card = document.createElement('div');
card.className = 'card stack';
card.innerHTML = `
<div>
<p class="meta-muted">Баланс</p>
<h2 style="font-size:30px;">${wallet.balanceSOL} SOL</h2>
<p class="meta-muted">Обновлено: ${wallet.updatedAt}</p>
</div>
<div class="card" style="padding:10px;">
<p class="meta-muted" style="margin-bottom:6px;">Публичный адрес</p>
<p style="font-size:13px; line-height:1.4; word-break:break-all;">${wallet.publicAddress}</p>
</div>
const balanceWrap = document.createElement('div');
const balanceLabel = document.createElement('p');
balanceLabel.className = 'meta-muted';
balanceLabel.textContent = 'Баланс (Solana)';
const balanceValue = document.createElement('h2');
balanceValue.style.fontSize = '30px';
balanceValue.textContent = '— SOL';
const updatedLabel = document.createElement('p');
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');
actions.className = 'stack';
actions.innerHTML = `
@@ -50,29 +75,126 @@ export function render({ navigate }) {
</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 {
await navigator.clipboard.writeText(wallet.publicAddress);
updateStatus('Адрес скопирован в буфер обмена');
const balance = await getBalanceSol({
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 {
updateStatus('Не удалось скопировать в этом браузере');
setStatus('Не удалось скопировать адрес в этом браузере');
}
});
actions.querySelector('#refresh-balance').addEventListener('click', () => {
updateStatus(`Баланс обновлен: ${wallet.balanceSOL} SOL`);
refreshBtn.addEventListener('click', () => {
void refreshBalance();
});
actions.querySelector('#send-sol').addEventListener('click', () => {
updateStatus('Демо-функция: перевод будет добавлен позже');
sendBtn.addEventListener('click', async () => {
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', () => {
updateStatus('Демо-функция: пополнение будет добавлено позже');
topupBtn.addEventListener('click', async () => {
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);
return screen;
}