Разделить покупку билета на два экрана
This commit is contained in:
parent
408b0eeb39
commit
9324da5cb7
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.287
|
client.version=1.2.288
|
||||||
server.version=1.2.267
|
server.version=1.2.268
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import { authService, state } from '../state.js';
|
import { authService, state } from '../state.js';
|
||||||
import {
|
import {
|
||||||
createRandomSolanaWallet,
|
|
||||||
createSolanaWalletFromPrivateBase58,
|
|
||||||
formatSol,
|
formatSol,
|
||||||
getBalanceSol,
|
getBalanceSol,
|
||||||
getTopupSiteUrl,
|
getTopupSiteUrl,
|
||||||
@ -27,7 +25,6 @@ import {
|
|||||||
} from '../services/shine-blockchain-wallet-service.js?v=202605300007';
|
} from '../services/shine-blockchain-wallet-service.js?v=202605300007';
|
||||||
|
|
||||||
export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' };
|
export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' };
|
||||||
const SOLANA_PRIVATE_BASE58_MAX_LEN = 44;
|
|
||||||
|
|
||||||
function nowRu() {
|
function nowRu() {
|
||||||
return new Date().toLocaleString('ru-RU');
|
return new Date().toLocaleString('ru-RU');
|
||||||
@ -102,6 +99,32 @@ function concatBytes(...parts) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||||
|
function encodeBase58(bytes) {
|
||||||
|
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||||||
|
if (source.length === 0) return '';
|
||||||
|
|
||||||
|
const digits = [0];
|
||||||
|
for (let i = 0; i < source.length; i += 1) {
|
||||||
|
let carry = source[i];
|
||||||
|
for (let j = 0; j < digits.length; j += 1) {
|
||||||
|
const value = (digits[j] << 8) + carry;
|
||||||
|
digits[j] = value % 58;
|
||||||
|
carry = Math.floor(value / 58);
|
||||||
|
}
|
||||||
|
while (carry > 0) {
|
||||||
|
digits.push(carry % 58);
|
||||||
|
carry = Math.floor(carry / 58);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < source.length && source[i] === 0; i += 1) {
|
||||||
|
digits.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return digits.reverse().map((digit) => BASE58_ALPHABET[digit]).join('');
|
||||||
|
}
|
||||||
|
|
||||||
function u64ToBytes(value) {
|
function u64ToBytes(value) {
|
||||||
const out = new Uint8Array(8);
|
const out = new Uint8Array(8);
|
||||||
let current = BigInt(value || 0);
|
let current = BigInt(value || 0);
|
||||||
@ -271,7 +294,7 @@ async function deriveSupportRandomWallet(extraText) {
|
|||||||
const keypair = solana.Keypair.fromSeed(seed);
|
const keypair = solana.Keypair.fromSeed(seed);
|
||||||
return {
|
return {
|
||||||
address: keypair.publicKey.toBase58(),
|
address: keypair.publicKey.toBase58(),
|
||||||
privateKey32Base58: solana.bs58.encode(seed),
|
privateKey32Base58: encodeBase58(seed),
|
||||||
keypair,
|
keypair,
|
||||||
generatedAt: new Date().toLocaleString('ru-RU'),
|
generatedAt: new Date().toLocaleString('ru-RU'),
|
||||||
};
|
};
|
||||||
@ -406,6 +429,14 @@ export function render({ navigate }) {
|
|||||||
arweaveWalletCtx = null;
|
arweaveWalletCtx = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function styleSupportInputField(field) {
|
||||||
|
if (!field) return;
|
||||||
|
field.style.color = '#111111';
|
||||||
|
field.style.webkitTextFillColor = '#111111';
|
||||||
|
field.style.caretColor = '#111111';
|
||||||
|
field.style.backgroundColor = '#ffffff';
|
||||||
|
}
|
||||||
|
|
||||||
function renderSupportHub() {
|
function renderSupportHub() {
|
||||||
activeModeToken += 1;
|
activeModeToken += 1;
|
||||||
clearArweaveSecretsInMemory();
|
clearArweaveSecretsInMemory();
|
||||||
@ -433,7 +464,7 @@ export function render({ navigate }) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
actions.querySelector('#support-buy')?.addEventListener('click', () => {
|
actions.querySelector('#support-buy')?.addEventListener('click', () => {
|
||||||
void renderSupportBuy();
|
void renderSupportBuyIntro();
|
||||||
});
|
});
|
||||||
actions.querySelector('#support-queue')?.addEventListener('click', () => {
|
actions.querySelector('#support-queue')?.addEventListener('click', () => {
|
||||||
void renderSupportQueue();
|
void renderSupportQueue();
|
||||||
@ -446,7 +477,7 @@ export function render({ navigate }) {
|
|||||||
setStatus('Выберите действие в разделе поддержки.');
|
setStatus('Выберите действие в разделе поддержки.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderSupportHelp(backTarget = renderSupportBuy) {
|
async function renderSupportHelp(backTarget = renderSupportBuyForm) {
|
||||||
const modeToken = ++activeModeToken;
|
const modeToken = ++activeModeToken;
|
||||||
clearArweaveSecretsInMemory();
|
clearArweaveSecretsInMemory();
|
||||||
content.innerHTML = '';
|
content.innerHTML = '';
|
||||||
@ -492,6 +523,7 @@ export function render({ navigate }) {
|
|||||||
saltInput.rows = 3;
|
saltInput.rows = 3;
|
||||||
saltInput.placeholder = 'Можно оставить пустым или добавить любой текст как дополнительную примесь';
|
saltInput.placeholder = 'Можно оставить пустым или добавить любой текст как дополнительную примесь';
|
||||||
saltInput.spellcheck = false;
|
saltInput.spellcheck = false;
|
||||||
|
styleSupportInputField(saltInput);
|
||||||
|
|
||||||
const timeLabel = document.createElement('p');
|
const timeLabel = document.createElement('p');
|
||||||
timeLabel.className = 'meta-muted';
|
timeLabel.className = 'meta-muted';
|
||||||
@ -507,6 +539,7 @@ export function render({ navigate }) {
|
|||||||
generatedPublicInput.type = 'text';
|
generatedPublicInput.type = 'text';
|
||||||
generatedPublicInput.readOnly = true;
|
generatedPublicInput.readOnly = true;
|
||||||
generatedPublicInput.placeholder = 'Появится после генерации';
|
generatedPublicInput.placeholder = 'Появится после генерации';
|
||||||
|
styleSupportInputField(generatedPublicInput);
|
||||||
|
|
||||||
const generatedSecretLabel = document.createElement('label');
|
const generatedSecretLabel = document.createElement('label');
|
||||||
generatedSecretLabel.className = 'meta-muted';
|
generatedSecretLabel.className = 'meta-muted';
|
||||||
@ -519,6 +552,7 @@ export function render({ navigate }) {
|
|||||||
generatedSecretInput.readOnly = true;
|
generatedSecretInput.readOnly = true;
|
||||||
generatedSecretInput.placeholder = 'Появится после генерации';
|
generatedSecretInput.placeholder = 'Появится после генерации';
|
||||||
generatedSecretInput.spellcheck = false;
|
generatedSecretInput.spellcheck = false;
|
||||||
|
styleSupportInputField(generatedSecretInput);
|
||||||
|
|
||||||
const generatedAddressNote = document.createElement('p');
|
const generatedAddressNote = document.createElement('p');
|
||||||
generatedAddressNote.className = 'meta-muted';
|
generatedAddressNote.className = 'meta-muted';
|
||||||
@ -661,6 +695,7 @@ export function render({ navigate }) {
|
|||||||
queryInput.placeholder = 'Например: 12, 2-5 или 3 8';
|
queryInput.placeholder = 'Например: 12, 2-5 или 3 8';
|
||||||
queryInput.autocomplete = 'off';
|
queryInput.autocomplete = 'off';
|
||||||
queryInput.spellcheck = false;
|
queryInput.spellcheck = false;
|
||||||
|
styleSupportInputField(queryInput);
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'row';
|
actions.className = 'row';
|
||||||
@ -737,19 +772,89 @@ export function render({ navigate }) {
|
|||||||
setStatus('Просмотр билета готов. Введите номер в нужном формате.');
|
setStatus('Просмотр билета готов. Введите номер в нужном формате.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderSupportBuy() {
|
async function renderSupportBuyIntro() {
|
||||||
const modeToken = ++activeModeToken;
|
const modeToken = ++activeModeToken;
|
||||||
clearArweaveSecretsInMemory();
|
clearArweaveSecretsInMemory();
|
||||||
content.innerHTML = '';
|
content.innerHTML = '';
|
||||||
|
|
||||||
const backBtn = createModeBackButton(renderSupportHub);
|
const backBtn = createModeBackButton(renderSupportHub);
|
||||||
|
const introCard = document.createElement('div');
|
||||||
|
introCard.className = 'card stack';
|
||||||
|
introCard.innerHTML = `
|
||||||
|
<h2 style="margin:0;">Купить билет</h2>
|
||||||
|
<p class="meta-muted" style="margin:0; line-height:1.55;">
|
||||||
|
Сначала показываем условия и текущий лимит. После этого можно перейти к покупке и ввести сумму в долларах.
|
||||||
|
Оплата идет в SOL, а расчет строится по курсу USD/USDT на момент транзакции.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const stateCard = document.createElement('div');
|
||||||
|
stateCard.className = 'card stack';
|
||||||
|
stateCard.innerHTML = `<p class="meta-muted" style="margin:0;">Загрузка условий...</p>`;
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'stack';
|
||||||
|
actions.innerHTML = `
|
||||||
|
<button class="primary-btn" type="button" id="support-buy-next" style="width:100%;">Купить на сумму в долларах</button>
|
||||||
|
<button class="text-btn" type="button" id="support-buy-help" style="width:100%;">Справка</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const nextBtn = actions.querySelector('#support-buy-next');
|
||||||
|
const helpBtn = actions.querySelector('#support-buy-help');
|
||||||
|
nextBtn.disabled = true;
|
||||||
|
|
||||||
|
let currentCore = null;
|
||||||
|
try {
|
||||||
|
currentCore = await loadSupportPaymentsCore(state.entrySettings.solanaServer);
|
||||||
|
if (modeToken !== activeModeToken) return;
|
||||||
|
const queue = queueStateView(currentCore.queues, 1);
|
||||||
|
const remainingUsdCents = currentCore.coef.limitUsdCents > queue.sumTotalUsdCents
|
||||||
|
? currentCore.coef.limitUsdCents - queue.sumTotalUsdCents
|
||||||
|
: 0n;
|
||||||
|
stateCard.innerHTML = `
|
||||||
|
<div><b>Коэффициент:</b> ${formatPpmCoefText(currentCore.coef.coefPpm)}</div>
|
||||||
|
<div><b>Лимит текущего коэффициента:</b> ${formatUsdCentsText(currentCore.coef.limitUsdCents)} USD</div>
|
||||||
|
<div><b>Уже куплено в очереди 1:</b> ${formatUsdCentsText(queue.sumTotalUsdCents)} USD</div>
|
||||||
|
<div><b>Осталось купить по текущему коэффициенту:</b> ${formatUsdCentsText(remainingUsdCents)} USD</div>
|
||||||
|
<div><b>Сколько билетов уже в очереди:</b> ${queue.ticketsTotal.toString()}</div>
|
||||||
|
<div><b>Следующий билет:</b> №${(queue.ticketsTotal + 1n).toString()}</div>
|
||||||
|
<div><b>Курс:</b> 1 SOL = ${formatPythSolUsdText(currentCore.pyth)} USD</div>
|
||||||
|
<div class="meta-muted" style="margin:0; line-height:1.5;">
|
||||||
|
После заполнения лимита будет новый лимит, но уже с более низким коэффициентом.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
nextBtn.disabled = false;
|
||||||
|
setStatus('Условия покупки загружены. Можно переходить к форме покупки.');
|
||||||
|
} catch (error) {
|
||||||
|
if (modeToken !== activeModeToken) return;
|
||||||
|
stateCard.innerHTML = `<p class="meta-muted" style="margin:0;">${String(error?.message || 'Не удалось загрузить состояние')}</p>`;
|
||||||
|
nextBtn.disabled = true;
|
||||||
|
setStatus(`Не удалось загрузить условия покупки: ${error?.message || 'unknown'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextBtn?.addEventListener('click', () => {
|
||||||
|
void renderSupportBuyForm();
|
||||||
|
});
|
||||||
|
helpBtn?.addEventListener('click', () => {
|
||||||
|
void renderSupportHelp(renderSupportBuyIntro);
|
||||||
|
});
|
||||||
|
|
||||||
|
content.append(backBtn, introCard, stateCard, actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderSupportBuyForm() {
|
||||||
|
const modeToken = ++activeModeToken;
|
||||||
|
clearArweaveSecretsInMemory();
|
||||||
|
content.innerHTML = '';
|
||||||
|
|
||||||
|
const backBtn = createModeBackButton(renderSupportBuyIntro);
|
||||||
const helpCard = document.createElement('div');
|
const helpCard = document.createElement('div');
|
||||||
helpCard.className = 'card stack';
|
helpCard.className = 'card stack';
|
||||||
helpCard.innerHTML = `
|
helpCard.innerHTML = `
|
||||||
<h2 style="margin:0;">Купить билет</h2>
|
<h2 style="margin:0;">Купить билет на сумму в долларах</h2>
|
||||||
<p class="meta-muted" style="margin:0; line-height:1.55;">
|
<p class="meta-muted" style="margin:0; line-height:1.55;">
|
||||||
Вы покупаете только билет очереди 1. Сумма задается в USD, а оплата уходит в SOL по курсу USDT/SOL на момент транзакции.
|
Здесь вводится сумма покупки и адрес получателя. Если адрес не указан, можно купить на тот же кошелёк, с которого идет оплата.
|
||||||
Покупка подписывается вашим client key. Интерфейс проверяет допуск по курсу до 3 процентов и не дает выходить за текущий лимит по коэффициенту.
|
Покупка подписывается вашим client key и отклоняется, если курс уходит дальше допустимого порога.
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -770,6 +875,7 @@ export function render({ navigate }) {
|
|||||||
amountInput.value = '20';
|
amountInput.value = '20';
|
||||||
amountInput.inputMode = 'decimal';
|
amountInput.inputMode = 'decimal';
|
||||||
amountInput.autocomplete = 'off';
|
amountInput.autocomplete = 'off';
|
||||||
|
styleSupportInputField(amountInput);
|
||||||
|
|
||||||
const recipientWrap = document.createElement('div');
|
const recipientWrap = document.createElement('div');
|
||||||
recipientWrap.className = 'stack';
|
recipientWrap.className = 'stack';
|
||||||
@ -779,6 +885,7 @@ export function render({ navigate }) {
|
|||||||
recipientInput.placeholder = 'Можно оставить пустым';
|
recipientInput.placeholder = 'Можно оставить пустым';
|
||||||
recipientInput.autocomplete = 'off';
|
recipientInput.autocomplete = 'off';
|
||||||
recipientInput.spellcheck = false;
|
recipientInput.spellcheck = false;
|
||||||
|
styleSupportInputField(recipientInput);
|
||||||
const recipientLabel = document.createElement('label');
|
const recipientLabel = document.createElement('label');
|
||||||
recipientLabel.className = 'meta-muted';
|
recipientLabel.className = 'meta-muted';
|
||||||
recipientLabel.setAttribute('for', 'support-buy-recipient');
|
recipientLabel.setAttribute('for', 'support-buy-recipient');
|
||||||
@ -830,6 +937,7 @@ export function render({ navigate }) {
|
|||||||
} else {
|
} else {
|
||||||
recipientInput.disabled = false;
|
recipientInput.disabled = false;
|
||||||
}
|
}
|
||||||
|
styleSupportInputField(recipientInput);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateQuote = () => {
|
const updateQuote = () => {
|
||||||
@ -918,7 +1026,7 @@ export function render({ navigate }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
helpBtn?.addEventListener('click', () => {
|
helpBtn?.addEventListener('click', () => {
|
||||||
void renderSupportHelp(renderSupportBuy);
|
void renderSupportHelp(renderSupportBuyForm);
|
||||||
});
|
});
|
||||||
|
|
||||||
refreshBtn?.addEventListener('click', () => {
|
refreshBtn?.addEventListener('click', () => {
|
||||||
@ -1417,203 +1525,6 @@ export function render({ navigate }) {
|
|||||||
const sendBtn = actions.querySelector('#send-sol');
|
const sendBtn = actions.querySelector('#send-sol');
|
||||||
const topupBtn = actions.querySelector('#topup-sol');
|
const topupBtn = actions.querySelector('#topup-sol');
|
||||||
|
|
||||||
const generatedCard = document.createElement('div');
|
|
||||||
generatedCard.className = 'card stack';
|
|
||||||
generatedCard.innerHTML = `
|
|
||||||
<h3 style="margin:0;">Создание нового кошелька Solana</h3>
|
|
||||||
<p class="meta-muted" style="margin:0;">Введите приватный ключ Base58 (32 байта) или сгенерируйте случайный.</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const privateLabel = document.createElement('label');
|
|
||||||
privateLabel.className = 'meta-muted';
|
|
||||||
privateLabel.textContent = 'Приватный ключ (Base58, 32 байта)';
|
|
||||||
privateLabel.setAttribute('for', 'solana-private-base58-input');
|
|
||||||
|
|
||||||
const privateInput = document.createElement('input');
|
|
||||||
privateInput.id = 'solana-private-base58-input';
|
|
||||||
privateInput.type = 'text';
|
|
||||||
privateInput.placeholder = 'Введите приватный ключ Base58';
|
|
||||||
privateInput.maxLength = SOLANA_PRIVATE_BASE58_MAX_LEN;
|
|
||||||
privateInput.autocomplete = 'off';
|
|
||||||
privateInput.spellcheck = false;
|
|
||||||
|
|
||||||
const privateState = document.createElement('p');
|
|
||||||
privateState.className = 'meta-muted';
|
|
||||||
privateState.textContent = 'Ожидается Base58-строка приватного ключа.';
|
|
||||||
|
|
||||||
const generatedPublicLabel = document.createElement('label');
|
|
||||||
generatedPublicLabel.className = 'meta-muted';
|
|
||||||
generatedPublicLabel.textContent = 'Публичный ключ (Base58)';
|
|
||||||
generatedPublicLabel.setAttribute('for', 'solana-generated-public-key');
|
|
||||||
|
|
||||||
const generatedPublicInput = document.createElement('input');
|
|
||||||
generatedPublicInput.id = 'solana-generated-public-key';
|
|
||||||
generatedPublicInput.type = 'text';
|
|
||||||
generatedPublicInput.readOnly = true;
|
|
||||||
generatedPublicInput.placeholder = 'Будет сгенерирован после нажатия кнопки';
|
|
||||||
|
|
||||||
const generatedPrivateLabel = document.createElement('label');
|
|
||||||
generatedPrivateLabel.className = 'meta-muted';
|
|
||||||
generatedPrivateLabel.textContent = 'Сгенерированный приватный ключ (Base58)';
|
|
||||||
generatedPrivateLabel.setAttribute('for', 'solana-generated-private-key');
|
|
||||||
|
|
||||||
const generatedPrivateInput = document.createElement('input');
|
|
||||||
generatedPrivateInput.id = 'solana-generated-private-key';
|
|
||||||
generatedPrivateInput.type = 'text';
|
|
||||||
generatedPrivateInput.readOnly = true;
|
|
||||||
generatedPrivateInput.placeholder = 'Появится после генерации';
|
|
||||||
|
|
||||||
const generationActions = document.createElement('div');
|
|
||||||
generationActions.className = 'row';
|
|
||||||
generationActions.innerHTML = `
|
|
||||||
<button class="primary-btn" id="generate-random-solana" style="width:100%;">Сгенерировать случайный кошелёк</button>
|
|
||||||
<button class="primary-btn" id="generate-from-private-solana" style="width:100%;">Сгенерировать из приватного ключа</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const copyGeneratedActions = document.createElement('div');
|
|
||||||
copyGeneratedActions.className = 'row';
|
|
||||||
copyGeneratedActions.innerHTML = `
|
|
||||||
<button class="text-btn" id="copy-generated-private-solana" style="width:100%;">Копировать приватный</button>
|
|
||||||
<button class="text-btn" id="copy-generated-public-solana" style="width:100%;">Копировать публичный</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
generatedCard.append(
|
|
||||||
privateLabel,
|
|
||||||
privateInput,
|
|
||||||
privateState,
|
|
||||||
generationActions,
|
|
||||||
generatedPrivateLabel,
|
|
||||||
generatedPrivateInput,
|
|
||||||
generatedPublicLabel,
|
|
||||||
generatedPublicInput,
|
|
||||||
copyGeneratedActions,
|
|
||||||
);
|
|
||||||
|
|
||||||
const randomGenerateBtn = generationActions.querySelector('#generate-random-solana');
|
|
||||||
const fromPrivateGenerateBtn = generationActions.querySelector('#generate-from-private-solana');
|
|
||||||
const copyGeneratedPrivateBtn = copyGeneratedActions.querySelector('#copy-generated-private-solana');
|
|
||||||
const copyGeneratedPublicBtn = copyGeneratedActions.querySelector('#copy-generated-public-solana');
|
|
||||||
|
|
||||||
const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]+$/;
|
|
||||||
const validatePrivateInput = () => {
|
|
||||||
const value = String(privateInput.value || '').trim();
|
|
||||||
if (!value) {
|
|
||||||
privateState.textContent = 'Ожидается Base58-строка приватного ключа.';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!BASE58_RE.test(value)) {
|
|
||||||
privateState.textContent = 'Недопустимый формат: используйте только Base58.';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (value.length > SOLANA_PRIVATE_BASE58_MAX_LEN) {
|
|
||||||
privateState.textContent = 'Слишком длинное значение.';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
||||||
let num = 0n;
|
|
||||||
for (const c of value) {
|
|
||||||
num = num * 58n + BigInt(alphabet.indexOf(c));
|
|
||||||
}
|
|
||||||
let hex = num.toString(16);
|
|
||||||
if (hex.length % 2) hex = `0${hex}`;
|
|
||||||
const decoded = hex ? hex.match(/.{1,2}/g)?.map((h) => parseInt(h, 16)) || [] : [];
|
|
||||||
let leadingZeros = 0;
|
|
||||||
while (leadingZeros < value.length && value[leadingZeros] === '1') leadingZeros += 1;
|
|
||||||
const byteLen = leadingZeros + decoded.length;
|
|
||||||
if (byteLen < 32) {
|
|
||||||
privateState.textContent = 'Слишком короткое значение: нужно 32 байта.';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (byteLen > 32) {
|
|
||||||
privateState.textContent = 'Слишком длинное значение: нужно ровно 32 байта.';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
privateState.textContent = 'Ошибка декодирования Base58.';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
privateState.textContent = 'Подходит';
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
privateInput.addEventListener('input', () => {
|
|
||||||
validatePrivateInput();
|
|
||||||
});
|
|
||||||
|
|
||||||
const setGenerationDisabled = (disabled) => {
|
|
||||||
randomGenerateBtn.disabled = disabled;
|
|
||||||
fromPrivateGenerateBtn.disabled = disabled;
|
|
||||||
copyGeneratedPrivateBtn.disabled = disabled;
|
|
||||||
copyGeneratedPublicBtn.disabled = disabled;
|
|
||||||
};
|
|
||||||
|
|
||||||
randomGenerateBtn.addEventListener('click', async () => {
|
|
||||||
setGenerationDisabled(true);
|
|
||||||
try {
|
|
||||||
const generated = await createRandomSolanaWallet();
|
|
||||||
if (modeToken !== activeModeToken) return;
|
|
||||||
generatedPrivateInput.value = generated.privateKey32Base58;
|
|
||||||
generatedPublicInput.value = generated.address;
|
|
||||||
privateState.textContent = 'Случайный кошелёк создан.';
|
|
||||||
setStatus('Случайный кошелёк Solana успешно сгенерирован.');
|
|
||||||
} catch (error) {
|
|
||||||
if (modeToken !== activeModeToken) return;
|
|
||||||
setStatus(`Ошибка генерации случайного кошелька: ${error?.message || 'unknown'}`);
|
|
||||||
} finally {
|
|
||||||
setGenerationDisabled(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fromPrivateGenerateBtn.addEventListener('click', async () => {
|
|
||||||
if (!validatePrivateInput()) {
|
|
||||||
setStatus('Исправьте приватный ключ перед генерацией.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setGenerationDisabled(true);
|
|
||||||
try {
|
|
||||||
const generated = await createSolanaWalletFromPrivateBase58(privateInput.value);
|
|
||||||
if (modeToken !== activeModeToken) return;
|
|
||||||
generatedPrivateInput.value = generated.privateKey32Base58;
|
|
||||||
generatedPublicInput.value = generated.address;
|
|
||||||
privateState.textContent = 'Подходит';
|
|
||||||
setStatus('Публичный ключ сгенерирован из введённого приватного ключа.');
|
|
||||||
} catch (error) {
|
|
||||||
if (modeToken !== activeModeToken) return;
|
|
||||||
setStatus(`Ошибка генерации из приватного ключа: ${error?.message || 'unknown'}`);
|
|
||||||
} finally {
|
|
||||||
setGenerationDisabled(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
copyGeneratedPrivateBtn.addEventListener('click', async () => {
|
|
||||||
const value = String(generatedPrivateInput.value || '').trim();
|
|
||||||
if (!value) {
|
|
||||||
setStatus('Сначала сгенерируйте приватный ключ.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(value);
|
|
||||||
setStatus('Приватный ключ скопирован.');
|
|
||||||
} catch {
|
|
||||||
setStatus('Не удалось скопировать приватный ключ в этом браузере.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
copyGeneratedPublicBtn.addEventListener('click', async () => {
|
|
||||||
const value = String(generatedPublicInput.value || '').trim();
|
|
||||||
if (!value) {
|
|
||||||
setStatus('Сначала сгенерируйте публичный ключ.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(value);
|
|
||||||
setStatus('Публичный ключ скопирован.');
|
|
||||||
} catch {
|
|
||||||
setStatus('Не удалось скопировать публичный ключ в этом браузере.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const refreshBalance = async () => {
|
const refreshBalance = async () => {
|
||||||
if (!walletAddress) {
|
if (!walletAddress) {
|
||||||
setStatus('Кошелёк не инициализирован.');
|
setStatus('Кошелёк не инициализирован.');
|
||||||
@ -1689,7 +1600,7 @@ export function render({ navigate }) {
|
|||||||
window.location.assign(getTopupSiteUrl(walletAddress));
|
window.location.assign(getTopupSiteUrl(walletAddress));
|
||||||
});
|
});
|
||||||
|
|
||||||
content.append(backBtn, card, actions, generatedCard);
|
content.append(backBtn, card, actions);
|
||||||
setStatus('Инициализация client.key...');
|
setStatus('Инициализация client.key...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user