From 9324da5cb73031b571f2750c5ebaebf41c885cb0281c97c1cac5b95121607f18 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Sun, 28 Jun 2026 15:11:05 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BA=D1=83=D0=BF=D0=BA=D1=83=20?= =?UTF-8?q?=D0=B1=D0=B8=D0=BB=D0=B5=D1=82=D0=B0=20=D0=BD=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=B2=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 4 +- shine-UI/js/pages/wallet-view.js | 329 +++++++++++-------------------- 2 files changed, 122 insertions(+), 211 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index 7ec9670..b7eec9f 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.287 -server.version=1.2.267 +client.version=1.2.288 +server.version=1.2.268 diff --git a/shine-UI/js/pages/wallet-view.js b/shine-UI/js/pages/wallet-view.js index 95dda6b..4e71b68 100644 --- a/shine-UI/js/pages/wallet-view.js +++ b/shine-UI/js/pages/wallet-view.js @@ -1,8 +1,6 @@ import { renderHeader } from '../components/header.js'; import { authService, state } from '../state.js'; import { - createRandomSolanaWallet, - createSolanaWalletFromPrivateBase58, formatSol, getBalanceSol, getTopupSiteUrl, @@ -27,7 +25,6 @@ import { } from '../services/shine-blockchain-wallet-service.js?v=202605300007'; export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' }; -const SOLANA_PRIVATE_BASE58_MAX_LEN = 44; function nowRu() { return new Date().toLocaleString('ru-RU'); @@ -102,6 +99,32 @@ function concatBytes(...parts) { 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) { const out = new Uint8Array(8); let current = BigInt(value || 0); @@ -271,7 +294,7 @@ async function deriveSupportRandomWallet(extraText) { const keypair = solana.Keypair.fromSeed(seed); return { address: keypair.publicKey.toBase58(), - privateKey32Base58: solana.bs58.encode(seed), + privateKey32Base58: encodeBase58(seed), keypair, generatedAt: new Date().toLocaleString('ru-RU'), }; @@ -406,6 +429,14 @@ export function render({ navigate }) { 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() { activeModeToken += 1; clearArweaveSecretsInMemory(); @@ -433,7 +464,7 @@ export function render({ navigate }) { `; actions.querySelector('#support-buy')?.addEventListener('click', () => { - void renderSupportBuy(); + void renderSupportBuyIntro(); }); actions.querySelector('#support-queue')?.addEventListener('click', () => { void renderSupportQueue(); @@ -446,7 +477,7 @@ export function render({ navigate }) { setStatus('Выберите действие в разделе поддержки.'); } - async function renderSupportHelp(backTarget = renderSupportBuy) { + async function renderSupportHelp(backTarget = renderSupportBuyForm) { const modeToken = ++activeModeToken; clearArweaveSecretsInMemory(); content.innerHTML = ''; @@ -492,6 +523,7 @@ export function render({ navigate }) { saltInput.rows = 3; saltInput.placeholder = 'Можно оставить пустым или добавить любой текст как дополнительную примесь'; saltInput.spellcheck = false; + styleSupportInputField(saltInput); const timeLabel = document.createElement('p'); timeLabel.className = 'meta-muted'; @@ -507,6 +539,7 @@ export function render({ navigate }) { generatedPublicInput.type = 'text'; generatedPublicInput.readOnly = true; generatedPublicInput.placeholder = 'Появится после генерации'; + styleSupportInputField(generatedPublicInput); const generatedSecretLabel = document.createElement('label'); generatedSecretLabel.className = 'meta-muted'; @@ -519,6 +552,7 @@ export function render({ navigate }) { generatedSecretInput.readOnly = true; generatedSecretInput.placeholder = 'Появится после генерации'; generatedSecretInput.spellcheck = false; + styleSupportInputField(generatedSecretInput); const generatedAddressNote = document.createElement('p'); generatedAddressNote.className = 'meta-muted'; @@ -661,6 +695,7 @@ export function render({ navigate }) { queryInput.placeholder = 'Например: 12, 2-5 или 3 8'; queryInput.autocomplete = 'off'; queryInput.spellcheck = false; + styleSupportInputField(queryInput); const actions = document.createElement('div'); actions.className = 'row'; @@ -737,19 +772,89 @@ export function render({ navigate }) { setStatus('Просмотр билета готов. Введите номер в нужном формате.'); } - async function renderSupportBuy() { + async function renderSupportBuyIntro() { const modeToken = ++activeModeToken; clearArweaveSecretsInMemory(); content.innerHTML = ''; const backBtn = createModeBackButton(renderSupportHub); + const introCard = document.createElement('div'); + introCard.className = 'card stack'; + introCard.innerHTML = ` +

Купить билет

+

+ Сначала показываем условия и текущий лимит. После этого можно перейти к покупке и ввести сумму в долларах. + Оплата идет в SOL, а расчет строится по курсу USD/USDT на момент транзакции. +

+ `; + + const stateCard = document.createElement('div'); + stateCard.className = 'card stack'; + stateCard.innerHTML = `

Загрузка условий...

`; + + const actions = document.createElement('div'); + actions.className = 'stack'; + actions.innerHTML = ` + + + `; + + 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 = ` +
Коэффициент: ${formatPpmCoefText(currentCore.coef.coefPpm)}
+
Лимит текущего коэффициента: ${formatUsdCentsText(currentCore.coef.limitUsdCents)} USD
+
Уже куплено в очереди 1: ${formatUsdCentsText(queue.sumTotalUsdCents)} USD
+
Осталось купить по текущему коэффициенту: ${formatUsdCentsText(remainingUsdCents)} USD
+
Сколько билетов уже в очереди: ${queue.ticketsTotal.toString()}
+
Следующий билет: №${(queue.ticketsTotal + 1n).toString()}
+
Курс: 1 SOL = ${formatPythSolUsdText(currentCore.pyth)} USD
+
+ После заполнения лимита будет новый лимит, но уже с более низким коэффициентом. +
+ `; + nextBtn.disabled = false; + setStatus('Условия покупки загружены. Можно переходить к форме покупки.'); + } catch (error) { + if (modeToken !== activeModeToken) return; + stateCard.innerHTML = `

${String(error?.message || 'Не удалось загрузить состояние')}

`; + 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'); helpCard.className = 'card stack'; helpCard.innerHTML = ` -

Купить билет

+

Купить билет на сумму в долларах

- Вы покупаете только билет очереди 1. Сумма задается в USD, а оплата уходит в SOL по курсу USDT/SOL на момент транзакции. - Покупка подписывается вашим client key. Интерфейс проверяет допуск по курсу до 3 процентов и не дает выходить за текущий лимит по коэффициенту. + Здесь вводится сумма покупки и адрес получателя. Если адрес не указан, можно купить на тот же кошелёк, с которого идет оплата. + Покупка подписывается вашим client key и отклоняется, если курс уходит дальше допустимого порога.

`; @@ -770,6 +875,7 @@ export function render({ navigate }) { amountInput.value = '20'; amountInput.inputMode = 'decimal'; amountInput.autocomplete = 'off'; + styleSupportInputField(amountInput); const recipientWrap = document.createElement('div'); recipientWrap.className = 'stack'; @@ -779,6 +885,7 @@ export function render({ navigate }) { recipientInput.placeholder = 'Можно оставить пустым'; recipientInput.autocomplete = 'off'; recipientInput.spellcheck = false; + styleSupportInputField(recipientInput); const recipientLabel = document.createElement('label'); recipientLabel.className = 'meta-muted'; recipientLabel.setAttribute('for', 'support-buy-recipient'); @@ -830,6 +937,7 @@ export function render({ navigate }) { } else { recipientInput.disabled = false; } + styleSupportInputField(recipientInput); }; const updateQuote = () => { @@ -918,7 +1026,7 @@ export function render({ navigate }) { }); helpBtn?.addEventListener('click', () => { - void renderSupportHelp(renderSupportBuy); + void renderSupportHelp(renderSupportBuyForm); }); refreshBtn?.addEventListener('click', () => { @@ -1417,203 +1525,6 @@ export function render({ navigate }) { const sendBtn = actions.querySelector('#send-sol'); const topupBtn = actions.querySelector('#topup-sol'); - const generatedCard = document.createElement('div'); - generatedCard.className = 'card stack'; - generatedCard.innerHTML = ` -

Создание нового кошелька Solana

-

Введите приватный ключ Base58 (32 байта) или сгенерируйте случайный.

- `; - - 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 = ` - - - `; - - const copyGeneratedActions = document.createElement('div'); - copyGeneratedActions.className = 'row'; - copyGeneratedActions.innerHTML = ` - - - `; - - 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 () => { if (!walletAddress) { setStatus('Кошелёк не инициализирован.'); @@ -1689,7 +1600,7 @@ export function render({ navigate }) { window.location.assign(getTopupSiteUrl(walletAddress)); }); - content.append(backBtn, card, actions, generatedCard); + content.append(backBtn, card, actions); setStatus('Инициализация client.key...'); try {