Доработать клиентский UI Solana-регистрации
This commit is contained in:
parent
6bf5d1d5ed
commit
689f35fea2
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.133
|
client.version=1.2.134
|
||||||
server.version=1.2.125
|
server.version=1.2.126
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { state } from '../state.js';
|
|||||||
export const pageMeta = { id: 'devnet-topup-view', title: 'Пополнение DEVNET', showAppChrome: false };
|
export const pageMeta = { id: 'devnet-topup-view', title: 'Пополнение DEVNET', showAppChrome: false };
|
||||||
|
|
||||||
const SENDER_PRIVATE_32_BASE58 = '6xqAuKYvA8qrCdAkcw7Y8aMgvBnYk8JLxWLma5BzbAvu';
|
const SENDER_PRIVATE_32_BASE58 = '6xqAuKYvA8qrCdAkcw7Y8aMgvBnYk8JLxWLma5BzbAvu';
|
||||||
const TRANSFER_AMOUNT_SOL = 0.01;
|
const TRANSFER_AMOUNT_SOL = 0.02;
|
||||||
|
|
||||||
function readWalletFromUrl() {
|
function readWalletFromUrl() {
|
||||||
try {
|
try {
|
||||||
@ -40,6 +40,7 @@ export function render({ navigate }) {
|
|||||||
<strong>Кошелёк получателя</strong>
|
<strong>Кошелёк получателя</strong>
|
||||||
<p class="meta-muted" style="word-break:break-all;">${targetWallet || 'Не передан параметр wallet'}</p>
|
<p class="meta-muted" style="word-break:break-all;">${targetWallet || 'Не передан параметр wallet'}</p>
|
||||||
<p class="meta-muted">Сумма перевода: ${TRANSFER_AMOUNT_SOL} SOL</p>
|
<p class="meta-muted">Сумма перевода: ${TRANSFER_AMOUNT_SOL} SOL</p>
|
||||||
|
<p class="meta-muted" id="devnet-topup-target-balance">Баланс: ...</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const status = document.createElement('p');
|
const status = document.createElement('p');
|
||||||
@ -50,22 +51,32 @@ export function render({ navigate }) {
|
|||||||
status.style.whiteSpace = 'pre-wrap';
|
status.style.whiteSpace = 'pre-wrap';
|
||||||
status.textContent = 'Готово к пополнению.';
|
status.textContent = 'Готово к пополнению.';
|
||||||
|
|
||||||
|
const successHint = document.createElement('p');
|
||||||
|
successHint.style.width = 'min(100%, 320px)';
|
||||||
|
successHint.style.display = 'none';
|
||||||
|
successHint.style.margin = '0';
|
||||||
|
successHint.style.padding = '14px 16px';
|
||||||
|
successHint.style.borderRadius = '16px';
|
||||||
|
successHint.style.background = 'rgba(15, 159, 99, 0.14)';
|
||||||
|
successHint.style.border = '2px solid rgba(15, 159, 99, 0.32)';
|
||||||
|
successHint.style.fontSize = '24px';
|
||||||
|
successHint.style.fontWeight = '800';
|
||||||
|
successHint.style.lineHeight = '1.25';
|
||||||
|
successHint.style.color = '#0f9f63';
|
||||||
|
successHint.style.textAlign = 'center';
|
||||||
|
successHint.style.whiteSpace = 'pre-wrap';
|
||||||
|
successHint.textContent = '';
|
||||||
|
|
||||||
const fillBtn = document.createElement('button');
|
const fillBtn = document.createElement('button');
|
||||||
fillBtn.className = 'primary-btn';
|
fillBtn.className = 'primary-btn';
|
||||||
fillBtn.type = 'button';
|
fillBtn.type = 'button';
|
||||||
fillBtn.textContent = `Пополнить на ${TRANSFER_AMOUNT_SOL} SOL`;
|
fillBtn.textContent = `Пополнить на ${TRANSFER_AMOUNT_SOL} SOL`;
|
||||||
|
|
||||||
const backBtn = document.createElement('button');
|
|
||||||
backBtn.className = 'secondary-btn';
|
|
||||||
backBtn.type = 'button';
|
|
||||||
backBtn.textContent = 'Назад';
|
|
||||||
backBtn.addEventListener('click', () => navigate('registration-payment-view'));
|
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'auth-footer-actions';
|
actions.className = 'auth-footer-actions';
|
||||||
actions.style.width = 'min(100%, 320px)';
|
actions.style.width = 'min(100%, 320px)';
|
||||||
actions.style.justifySelf = 'center';
|
actions.style.justifySelf = 'center';
|
||||||
actions.append(fillBtn, backBtn);
|
actions.append(fillBtn);
|
||||||
|
|
||||||
let senderAddress = '';
|
let senderAddress = '';
|
||||||
let senderKeypair = null;
|
let senderKeypair = null;
|
||||||
@ -78,6 +89,15 @@ export function render({ navigate }) {
|
|||||||
if (senderBalanceEl) senderBalanceEl.textContent = `Баланс: ${formatSol(balance.sol, 6)} SOL`;
|
if (senderBalanceEl) senderBalanceEl.textContent = `Баланс: ${formatSol(balance.sol, 6)} SOL`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateTargetBalance = async () => {
|
||||||
|
if (!targetWallet) return null;
|
||||||
|
const endpoint = state.entrySettings.solanaServer;
|
||||||
|
const balance = await getBalanceSol({ endpoint, address: targetWallet });
|
||||||
|
const targetBalanceEl = targetBox.querySelector('#devnet-topup-target-balance');
|
||||||
|
if (targetBalanceEl) targetBalanceEl.textContent = `Баланс: ${formatSol(balance.sol, 6)} SOL`;
|
||||||
|
return balance.sol;
|
||||||
|
};
|
||||||
|
|
||||||
fillBtn.addEventListener('click', async () => {
|
fillBtn.addEventListener('click', async () => {
|
||||||
if (!targetWallet) {
|
if (!targetWallet) {
|
||||||
status.textContent = 'Ошибка: в URL не передан параметр wallet.';
|
status.textContent = 'Ошибка: в URL не передан параметр wallet.';
|
||||||
@ -90,6 +110,7 @@ export function render({ navigate }) {
|
|||||||
|
|
||||||
fillBtn.disabled = true;
|
fillBtn.disabled = true;
|
||||||
status.textContent = 'Отправляем перевод...';
|
status.textContent = 'Отправляем перевод...';
|
||||||
|
successHint.style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = state.entrySettings.solanaServer;
|
const endpoint = state.entrySettings.solanaServer;
|
||||||
@ -100,9 +121,21 @@ export function render({ navigate }) {
|
|||||||
amountSol: TRANSFER_AMOUNT_SOL,
|
amountSol: TRANSFER_AMOUNT_SOL,
|
||||||
});
|
});
|
||||||
await updateSenderBalance();
|
await updateSenderBalance();
|
||||||
status.textContent = `Готово.\nSignature: ${tx.signature}`;
|
const targetBalanceSol = await updateTargetBalance();
|
||||||
|
status.textContent = `Транзакция прошла.\nSignature: ${tx.signature}`;
|
||||||
|
status.style.fontSize = '18px';
|
||||||
|
status.style.fontWeight = '700';
|
||||||
|
status.style.color = '#0f9f63';
|
||||||
|
fillBtn.style.display = 'none';
|
||||||
|
actions.style.display = 'none';
|
||||||
|
successHint.style.display = '';
|
||||||
|
successHint.textContent = `Кошелёк пополнен на ${TRANSFER_AMOUNT_SOL} SOL.\nНовый баланс: ${formatSol(targetBalanceSol || 0, 6)} SOL.\nМожете закрыть эту страницу и продолжить регистрацию.`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
status.textContent = `Ошибка перевода: ${error?.message || 'unknown'}`;
|
status.textContent = `Ошибка перевода: ${error?.message || 'unknown'}`;
|
||||||
|
status.style.fontSize = '';
|
||||||
|
status.style.fontWeight = '';
|
||||||
|
status.style.color = '';
|
||||||
|
successHint.style.display = 'none';
|
||||||
} finally {
|
} finally {
|
||||||
fillBtn.disabled = false;
|
fillBtn.disabled = false;
|
||||||
}
|
}
|
||||||
@ -116,6 +149,7 @@ export function render({ navigate }) {
|
|||||||
const senderAddressEl = senderBox.querySelector('#devnet-topup-sender-address');
|
const senderAddressEl = senderBox.querySelector('#devnet-topup-sender-address');
|
||||||
if (senderAddressEl) senderAddressEl.textContent = `Адрес: ${senderAddress}`;
|
if (senderAddressEl) senderAddressEl.textContent = `Адрес: ${senderAddress}`;
|
||||||
await updateSenderBalance();
|
await updateSenderBalance();
|
||||||
|
await updateTargetBalance();
|
||||||
if (!targetWallet) {
|
if (!targetWallet) {
|
||||||
fillBtn.disabled = true;
|
fillBtn.disabled = true;
|
||||||
status.textContent = 'Передайте адрес получателя в параметре wallet.';
|
status.textContent = 'Передайте адрес получателя в параметре wallet.';
|
||||||
@ -129,11 +163,11 @@ export function render({ navigate }) {
|
|||||||
screen.append(
|
screen.append(
|
||||||
renderHeader({
|
renderHeader({
|
||||||
title: 'DEVNET пополнение',
|
title: 'DEVNET пополнение',
|
||||||
leftAction: { label: '←', onClick: () => navigate('registration-payment-view') },
|
|
||||||
}),
|
}),
|
||||||
senderBox,
|
senderBox,
|
||||||
targetBox,
|
targetBox,
|
||||||
status,
|
status,
|
||||||
|
successHint,
|
||||||
actions,
|
actions,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export function render({ navigate }) {
|
|||||||
advanced.className = 'card stack';
|
advanced.className = 'card stack';
|
||||||
advanced.innerHTML = `
|
advanced.innerHTML = `
|
||||||
<summary>Расширенные</summary>
|
<summary>Расширенные</summary>
|
||||||
<p class="meta-muted">Схема derivation ключей: логин нормализуется как `trim().toLowerCase()`. При непустом пароле используется Argon2id, затем из результата строится секрет для root/bch/dev ключей.</p>
|
<p class="meta-muted">Схема derivation ключей: логин нормализуется как trim().toLowerCase(). При непустом пароле используется Argon2id, затем из результата строится секрет для root/bch/dev ключей.</p>
|
||||||
<p class="meta-muted">Если пароль пустой — используется прежний детерминированный режим совместимости.</p>
|
<p class="meta-muted">Если пароль пустой — используется прежний детерминированный режим совместимости.</p>
|
||||||
<p class="meta-muted">Для тестов можно оставить пустой пароль.</p>
|
<p class="meta-muted">Для тестов можно оставить пустой пароль.</p>
|
||||||
<p class="meta-muted">Профиль Argon2id сейчас фиксированный: t=2, m=65536 KiB (64 MiB), p=1, dkLen=32. Выбор уровня будет добавлен позже.</p>
|
<p class="meta-muted">Профиль Argon2id сейчас фиксированный: t=2, m=65536 KiB (64 MiB), p=1, dkLen=32. Выбор уровня будет добавлен позже.</p>
|
||||||
|
|||||||
@ -11,7 +11,11 @@ import {
|
|||||||
getBalanceSol,
|
getBalanceSol,
|
||||||
getTopupSiteUrl,
|
getTopupSiteUrl,
|
||||||
} from '../services/solana-wallet-service.js';
|
} from '../services/solana-wallet-service.js';
|
||||||
import { formatSolanaErrorDetails, registerUserOnSolana } from '../services/solana-register-service.js';
|
import {
|
||||||
|
formatSolanaErrorDetails,
|
||||||
|
isUserAlreadyExistsSolanaError,
|
||||||
|
registerUserOnSolana,
|
||||||
|
} from '../services/solana-register-service.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;
|
||||||
@ -194,7 +198,7 @@ export function render({ navigate }) {
|
|||||||
} catch (solanaError) {
|
} catch (solanaError) {
|
||||||
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
||||||
// Пользователь уже зарегистрирован в Solana — продолжаем
|
// Пользователь уже зарегистрирован в Solana — продолжаем
|
||||||
if (!solanaMsg.includes('already') && !solanaMsg.includes('UserAlreadyExists')) {
|
if (!solanaMsg.includes('already') && !isUserAlreadyExistsSolanaError(solanaError)) {
|
||||||
throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`);
|
throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
|
|
||||||
const CLASSIFY_LOGIN_INSTRUCTION_TAG = 1;
|
const CLASSIFY_LOGIN_INSTRUCTION_TAG = 1;
|
||||||
const PRECHECK_SIM_PAYER = 'FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P';
|
const PRECHECK_SIM_PAYER = 'FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P';
|
||||||
|
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
|
||||||
|
|
||||||
let solanaLibPromise = null;
|
let solanaLibPromise = null;
|
||||||
function loadSolanaLib() {
|
function loadSolanaLib() {
|
||||||
@ -71,6 +72,15 @@ export function formatSolanaErrorDetails(error) {
|
|||||||
return parts.join(' :: ');
|
return parts.join(' :: ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUserAlreadyExistsSolanaError(error) {
|
||||||
|
const details = formatSolanaErrorDetails(error);
|
||||||
|
return details.includes('UserAlreadyExists')
|
||||||
|
|| details.includes('custom program error: 0x4')
|
||||||
|
|| details.includes('"Custom":4')
|
||||||
|
|| details.includes('"InstructionError":[2,{"Custom":4}]')
|
||||||
|
|| details.includes('Instruction 2: custom program error: 0x4');
|
||||||
|
}
|
||||||
|
|
||||||
export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) {
|
export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) {
|
||||||
const solana = await loadSolanaLib();
|
const solana = await loadSolanaLib();
|
||||||
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
|
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
|
||||||
@ -121,7 +131,7 @@ export async function checkLoginExistsOnSolana({ login, solanaEndpoint }) {
|
|||||||
throw new Error('EMPTY_LOGIN');
|
throw new Error('EMPTY_LOGIN');
|
||||||
}
|
}
|
||||||
const [userPda] = solana.PublicKey.findProgramAddressSync(
|
const [userPda] = solana.PublicKey.findProgramAddressSync(
|
||||||
[enc.encode('login='), enc.encode(loginNorm)],
|
[enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(loginNorm)],
|
||||||
usersProgram,
|
usersProgram,
|
||||||
);
|
);
|
||||||
const ai = await connection.getAccountInfo(userPda, 'confirmed');
|
const ai = await connection.getAccountInfo(userPda, 'confirmed');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user