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 {