Регистрация Solana: промо-topup URL с wallet(base58) и порог 0.01 SOL

This commit is contained in:
AidarKC 2026-04-27 01:44:07 +03:00
parent 2c68dedea2
commit 50da3e868d
5 changed files with 65 additions and 20 deletions

View File

@ -1,2 +1,2 @@
client.version=1.2.17 client.version=1.2.18
server.version=1.2.17 server.version=1.2.18

View File

@ -6,9 +6,15 @@ import {
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'; import {
deriveWalletFromPassword,
formatSol,
getBalanceSol,
getTopupSiteUrl,
} 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 };
const MIN_REQUIRED_SOL = 0.01;
function parseBalanceSol(value) { function parseBalanceSol(value) {
const parsed = Number.parseFloat(String(value || '').replace(',', '.')); const parsed = Number.parseFloat(String(value || '').replace(',', '.'));
@ -77,9 +83,9 @@ export function render({ navigate }) {
refreshButton.textContent = '↻'; refreshButton.textContent = '↻';
refreshButton.title = 'Обновить'; refreshButton.title = 'Обновить';
const refreshBalance = async () => { const refreshBalance = async ({ showError = true } = {}) => {
const address = String(walletValue.value || '').trim(); const address = String(walletValue.value || '').trim();
if (!address) return; if (!address) return null;
refreshButton.disabled = true; refreshButton.disabled = true;
try { try {
const balance = await getBalanceSol({ const balance = await getBalanceSol({
@ -88,10 +94,14 @@ export function render({ navigate }) {
}); });
state.registrationPayment.balanceSOL = String(balance.sol); state.registrationPayment.balanceSOL = String(balance.sol);
balanceValue.textContent = `${formatSol(balance.sol, 6)} SOL`; balanceValue.textContent = `${formatSol(balance.sol, 6)} SOL`;
return Number(balance.sol) || 0;
} catch (error) { } catch (error) {
status.className = 'status-line is-unavailable'; if (showError) {
status.textContent = `Не удалось обновить баланс: ${error?.message || 'unknown'}`; status.className = 'status-line is-unavailable';
status.style.display = ''; status.textContent = `Не удалось обновить баланс: ${error?.message || 'unknown'}`;
status.style.display = '';
}
return null;
} finally { } finally {
refreshButton.disabled = false; refreshButton.disabled = false;
} }
@ -106,8 +116,11 @@ export function render({ navigate }) {
const topupButton = document.createElement('button'); const topupButton = document.createElement('button');
topupButton.className = 'ghost-btn'; topupButton.className = 'ghost-btn';
topupButton.type = 'button'; topupButton.type = 'button';
topupButton.textContent = 'Пополнить счет'; topupButton.textContent = 'Пополнить кошелёк';
topupButton.addEventListener('click', () => navigate('topup-view')); topupButton.addEventListener('click', () => {
const walletAddress = String(walletValue.value || '').trim();
window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer');
});
const submitButton = document.createElement('button'); const submitButton = document.createElement('button');
submitButton.className = 'primary-btn'; submitButton.className = 'primary-btn';
@ -116,8 +129,8 @@ export function render({ navigate }) {
submitButton.addEventListener('click', async () => { submitButton.addEventListener('click', async () => {
status.style.display = 'none'; status.style.display = 'none';
const cryptoState = getCryptoRuntimeState(); const cryptoState = getCryptoRuntimeState();
if (!cryptoState.hasCrypto || !cryptoState.hasGetRandomValues || !cryptoState.hasSubtle) { if (!cryptoState.hasCrypto || !cryptoState.hasGetRandomValues || !cryptoState.hasSubtle) {
status.className = 'status-line is-unavailable'; status.className = 'status-line is-unavailable';
status.textContent = 'Криптография браузера недоступна. Откройте приложение через HTTPS tunnel или localhost и повторите регистрацию.'; status.textContent = 'Криптография браузера недоступна. Откройте приложение через HTTPS tunnel или localhost и повторите регистрацию.';
status.style.display = ''; status.style.display = '';
@ -128,6 +141,27 @@ export function render({ navigate }) {
submitButton.disabled = true; submitButton.disabled = true;
submitButton.textContent = 'Регистрация...'; submitButton.textContent = 'Регистрация...';
const walletAddress = String(walletValue.value || '').trim();
if (!walletAddress) {
status.className = 'status-line is-unavailable';
status.textContent = 'Кошелёк не подготовлен. Нажмите «Обновить» и попробуйте снова.';
status.style.display = '';
return;
}
const currentBalance = await refreshBalance({ showError: true });
if (currentBalance == null) return;
if (currentBalance < MIN_REQUIRED_SOL) {
status.className = 'status-line is-unavailable';
status.textContent = `Для регистрации нужно минимум ${MIN_REQUIRED_SOL} SOL. Сейчас на кошельке ${formatSol(currentBalance, 6)} SOL. Пополните на промо-странице или попросите перевод у знакомого с тестовыми SOL.`;
status.style.display = '';
const openTopup = window.confirm('Открыть страницу пополнения с вашим кошельком?');
if (openTopup) {
window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer');
}
return;
}
await authService.reconnect(state.entrySettings.shineServer); await authService.reconnect(state.entrySettings.shineServer);
const result = await authService.registerUser(state.registrationDraft.login, state.registrationDraft.password); const result = await authService.registerUser(state.registrationDraft.login, state.registrationDraft.password);
state.registrationDraft.flowType = 'registration'; state.registrationDraft.flowType = 'registration';
@ -151,7 +185,7 @@ 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 на вашем кошельке.</p>
<label class="stack"><span class="field-label">Номер кошелька (wallet.key = device.key)</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">Баланс (Solana)</span> <span class="field-label">Баланс (Solana)</span>

View File

@ -50,10 +50,10 @@ export function render({ navigate }) {
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. Вы можете открыть сайт для покупки SOL.</p> <p class="meta-muted">1. Вы можете открыть промо-сайт пополнения с подставленным кошельком.</p>
<p class="meta-muted">2. Либо нажать «Тестовое пополнение» и получить 1 SOL через DevNet airdrop.</p> <p class="meta-muted">2. Либо нажать «Тестовое пополнение» и получить 1 SOL через DevNet airdrop.</p>
</div> </div>
<a class="link-card" href="${getTopupSiteUrl()}" target="_blank" rel="noreferrer">Открыть сайт пополнения</a> <a class="link-card" id="topup-site-link" href="${getTopupSiteUrl(state.registrationPayment.walletAddress || '')}" 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;">Кошелёк для пополнения (wallet.key)</div> <div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (wallet.key)</div>
</div> </div>
@ -107,6 +107,10 @@ export function render({ navigate }) {
state.registrationPayment.walletAddress = wallet.address; state.registrationPayment.walletAddress = wallet.address;
walletValue.value = wallet.address; walletValue.value = wallet.address;
} }
const topupSiteLink = card.querySelector('#topup-site-link');
if (topupSiteLink instanceof HTMLAnchorElement) {
topupSiteLink.href = getTopupSiteUrl(walletValue.value);
}
const balance = await getBalanceSol({ const balance = await getBalanceSol({
endpoint: state.entrySettings.solanaServer, endpoint: state.entrySettings.solanaServer,
address: walletValue.value, address: walletValue.value,
@ -130,4 +134,3 @@ export function render({ navigate }) {
return screen; return screen;
} }

View File

@ -242,7 +242,7 @@ export function render({ navigate }) {
'Кнопка будет переводить на отдельный сайт пополнения.\n\nНажмите OK, чтобы открыть сайт.\nНажмите Отмена, чтобы выполнить тестовое пополнение (airdrop 1 SOL на DevNet).', 'Кнопка будет переводить на отдельный сайт пополнения.\n\nНажмите OK, чтобы открыть сайт.\nНажмите Отмена, чтобы выполнить тестовое пополнение (airdrop 1 SOL на DevNet).',
); );
if (openSite) { if (openSite) {
window.open(getTopupSiteUrl(), '_blank', 'noopener,noreferrer'); window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer');
setStatus('Открыт сайт пополнения. Пока также доступно тестовое пополнение.'); setStatus('Открыт сайт пополнения. Пока также доступно тестовое пополнение.');
return; return;
} }

View File

@ -3,7 +3,7 @@ import { extractDeviceKey32FromStoredValue } from './device-key-utils.js';
import { loadEncryptedUserSecrets } from './key-vault.js'; import { loadEncryptedUserSecrets } from './key-vault.js';
const DEFAULT_SOLANA_ENDPOINT = 'https://api.devnet.solana.com'; const DEFAULT_SOLANA_ENDPOINT = 'https://api.devnet.solana.com';
const TOPUP_SITE_URL = 'https://www.moonpay.com/buy/sol'; const TOPUP_SITE_URL = 'https://shine-promo-solana-devnet.shineup.me/';
let solanaLibPromise = null; let solanaLibPromise = null;
@ -115,6 +115,14 @@ export function formatSol(value, digits = 6) {
}); });
} }
export function getTopupSiteUrl() { export function getTopupSiteUrl(walletAddress = '') {
return TOPUP_SITE_URL; const cleanWallet = String(walletAddress || '').trim();
if (!cleanWallet) return TOPUP_SITE_URL;
try {
const url = new URL(TOPUP_SITE_URL);
url.searchParams.set('wallet', cleanWallet);
return url.toString();
} catch {
return `${TOPUP_SITE_URL}?wallet=${encodeURIComponent(cleanWallet)}`;
}
} }