From 3068c3e2b8e91e67cfeb1e8ed45442d685736be59d3a9c5fb4bf7399b7bb7ffa Mon Sep 17 00:00:00 2001
From: AidarKC
Date: Sun, 28 Jun 2026 13:53:12 +0400
Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?=
=?UTF-8?q?=D1=8C=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20=D0=BF=D0=BE?=
=?UTF-8?q?=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B8=20=D0=BF=D1=80=D0=BE?=
=?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0=20=D0=A1=D0=B8=D1=8F=D0=BD=D0=B8=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...026-06-28_1930_поддержка_проекта_сияние.md | 38 +
VERSION.properties | 4 +-
shine-UI/js/app.js | 2 +-
shine-UI/js/pages/wallet-view.js | 951 +++++++++++++++++-
4 files changed, 991 insertions(+), 4 deletions(-)
create mode 100644 Dev_Docs/Pending_Features/2026-06-28_1930_поддержка_проекта_сияние.md
diff --git a/Dev_Docs/Pending_Features/2026-06-28_1930_поддержка_проекта_сияние.md b/Dev_Docs/Pending_Features/2026-06-28_1930_поддержка_проекта_сияние.md
new file mode 100644
index 0000000..bbe28be
--- /dev/null
+++ b/Dev_Docs/Pending_Features/2026-06-28_1930_поддержка_проекта_сияние.md
@@ -0,0 +1,38 @@
+# Поддержать проект Сияние
+
+Статус: `pending`
+
+## Кратко
+
+В `shine-UI/js/pages/wallet-view.js` добавлен новый раздел `Поддержать проект Сияние` с тремя входами:
+
+1. купить билет;
+2. посмотреть билет по номеру;
+3. сгенерировать новую пару ключей.
+
+## Что проверить
+
+1. Открыть `Кошелёк`.
+2. Перейти в `Поддержать проект Сияние`.
+3. Проверить экран покупки:
+ - виден коэффициент;
+ - виден остаток лимита очереди 1;
+ - виден расчет в SOL;
+ - кнопка `Справка` открывает отдельный экран;
+ - покупка блокируется, если сумма больше остатка лимита.
+4. Проверить экран просмотра:
+ - `12` ищется как билет очереди 1;
+ - `2-5` и `3 8` ищутся как билеты очередей 2 и 3;
+ - показываются статус, количество билетов до него и уже выплаченные значения.
+5. Проверить генератор ключей:
+ - генерируется новая пара ключей;
+ - публичный и секретный ключи показываются;
+ - можно скопировать и скачать результат;
+ - дополнительный текст в поле необязателен.
+
+## Ожидаемый результат
+
+- Экран раздела поддержки открывается из `wallet-view`.
+- Покупка билета выполняется по текущему курсу и с допуском 3%.
+- По номеру билета показывается понятная сводка по очереди.
+- Генерация ключей использует безопасный браузерный рандом и не требует сохранения секретного ключа.
diff --git a/VERSION.properties b/VERSION.properties
index c42b1b3..f2b62bc 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.285
-server.version=1.2.265
+client.version=1.2.286
+server.version=1.2.266
diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js
index 9b39f73..03c97c8 100644
--- a/shine-UI/js/app.js
+++ b/shine-UI/js/app.js
@@ -50,7 +50,7 @@ import * as keyStorageView from './pages/key-storage-view.js';
import * as profileView from './pages/profile-view.js';
import * as profileEditView from './pages/profile-edit-view.js';
-import * as walletView from './pages/wallet-view.js?v=202605300007';
+import * as walletView from './pages/wallet-view.js?v=202606281930';
import * as settingsView from './pages/settings-view.js';
import * as developerSettingsView from './pages/developer-settings-view.js';
import * as serverSettingsView from './pages/server-settings-view.js?v=202606161240';
diff --git a/shine-UI/js/pages/wallet-view.js b/shine-UI/js/pages/wallet-view.js
index bfe647c..95dda6b 100644
--- a/shine-UI/js/pages/wallet-view.js
+++ b/shine-UI/js/pages/wallet-view.js
@@ -17,6 +17,7 @@ import {
transferAr,
} from '../services/arweave-wallet-service.js';
import { loadEncryptedUserSecrets } from '../services/key-vault.js';
+import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
import {
calcLimitTopupPriceLamports,
getLimitStepBytes,
@@ -65,6 +66,312 @@ function sessionArgsOrThrow() {
return { login, storagePwd };
}
+const SHINE_PAYMENTS_PROGRAM_ID = 'c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW';
+const SHINE_PAYMENTS_ORACLE_ACCOUNT = '7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE';
+const SHINE_PAYMENTS_SEEDS = {
+ config: 'shine_payments_config',
+ coef: 'shine_payments_coef_limit',
+ queues: 'shine_payments_queues',
+ inflow: 'shine_payments_inflow_vault',
+ q1: 'shine_payments_q1_ticket',
+ q2: 'shine_payments_q2_ticket',
+ q3: 'shine_payments_q3_ticket',
+};
+const COEF_SCALE_PPM = 1_000_000n;
+const LAMPORTS_PER_SOL = 1_000_000_000n;
+const SUPPORT_TICKET_HELP_TEXT = [
+ 'Билет привязан к очереди 1 и к адресу получателя, который вы укажете при покупке.',
+ 'Оплата считается в USD, а списание идет в SOL по курсу USDT/SOL, который проверяется в момент транзакции.',
+ 'Если курс уезжает слишком далеко, покупка отклоняется. Для интерфейса заложен допуск 3%.',
+ 'Когда текущий лимит очереди заполнится, откроется новый лимит с более низким коэффициентом.',
+ 'Номер билета нужно сохранить отдельно: именно по нему удобно отслеживать, когда он дойдет до выплаты.',
+].join('\n');
+
+function utf8Bytes(text) {
+ return new TextEncoder().encode(String(text || ''));
+}
+
+function concatBytes(...parts) {
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
+ const out = new Uint8Array(total);
+ let offset = 0;
+ parts.forEach((part) => {
+ out.set(part, offset);
+ offset += part.length;
+ });
+ return out;
+}
+
+function u64ToBytes(value) {
+ const out = new Uint8Array(8);
+ let current = BigInt(value || 0);
+ for (let i = 0; i < 8; i += 1) {
+ out[i] = Number(current & 0xffn);
+ current >>= 8n;
+ }
+ return out;
+}
+
+function readU64(data, offset) {
+ let value = 0n;
+ for (let i = 0; i < 8; i += 1) {
+ value |= BigInt(data[offset + i]) << (8n * BigInt(i));
+ }
+ return value;
+}
+
+function readI64(data, offset) {
+ let value = readU64(data, offset);
+ if (value > 0x7fffffffffffffffn) {
+ value -= 0x10000000000000000n;
+ }
+ return value;
+}
+
+function readI32(data, offset) {
+ let value = Number(readU64(data, offset) & 0xffffffffn);
+ if (value > 0x7fffffff) value -= 0x100000000;
+ return value;
+}
+
+function trimZeros(value) {
+ return String(value || '')
+ .replace(/(\.\d*?[1-9])0+$/u, '$1')
+ .replace(/\.0+$/u, '')
+ .replace(/\.$/u, '');
+}
+
+function formatUsdCentsText(cents) {
+ const value = Number(cents || 0n) / 100;
+ return value.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
+}
+
+function formatPpmCoefText(coefPpm) {
+ const value = Number(coefPpm || 0n) / Number(COEF_SCALE_PPM);
+ return `${trimZeros(value.toFixed(6))}x`;
+}
+
+function formatLamportsSolText(lamports, digits = 9) {
+ const value = Number(lamports || 0n) / Number(LAMPORTS_PER_SOL);
+ return value.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: digits });
+}
+
+function formatPythSolUsdText(pyth) {
+ const value = Number(pyth?.priceNum || 0n) / Number(pyth?.priceDen || 1n) / 100;
+ return trimZeros(value.toFixed(6));
+}
+
+function formatQueuePrefix(queueId) {
+ if (queueId === 2) return 'Q2';
+ if (queueId === 3) return 'Q3';
+ return 'Q1';
+}
+
+function parsePaymentsCoef(data) {
+ let offset = 0;
+ const version = data[offset];
+ offset += 1;
+ const coefPpm = readU64(data, offset); offset += 8;
+ const limitUsdCents = readU64(data, offset); offset += 8;
+ const callRewardLamports = readU64(data, offset);
+ return { version, coefPpm, limitUsdCents, callRewardLamports };
+}
+
+function parsePaymentsQueues(data) {
+ let offset = 0;
+ const version = data[offset];
+ offset += 1;
+ const q1TicketsTotal = readU64(data, offset); offset += 8;
+ const q1TicketsPaid = readU64(data, offset); offset += 8;
+ const q1SumTotalUsdCents = readU64(data, offset); offset += 8;
+ const q1SumPaidUsdCents = readU64(data, offset); offset += 8;
+ const q2TicketsTotal = readU64(data, offset); offset += 8;
+ const q2TicketsPaid = readU64(data, offset); offset += 8;
+ const q2SumTotalUsdCents = readU64(data, offset); offset += 8;
+ const q2SumPaidUsdCents = readU64(data, offset); offset += 8;
+ const q3TicketsTotal = readU64(data, offset); offset += 8;
+ const q3TicketsPaid = readU64(data, offset); offset += 8;
+ const q3SumTotalUsdCents = readU64(data, offset); offset += 8;
+ const q3SumPaidUsdCents = readU64(data, offset);
+ return {
+ version,
+ q1TicketsTotal,
+ q1TicketsPaid,
+ q1SumTotalUsdCents,
+ q1SumPaidUsdCents,
+ q2TicketsTotal,
+ q2TicketsPaid,
+ q2SumTotalUsdCents,
+ q2SumPaidUsdCents,
+ q3TicketsTotal,
+ q3TicketsPaid,
+ q3SumTotalUsdCents,
+ q3SumPaidUsdCents,
+ };
+}
+
+function parsePaymentsTicket(data) {
+ let offset = 0;
+ const version = data[offset];
+ offset += 1;
+ const queueId = data[offset];
+ offset += 1;
+ const index = readU64(data, offset);
+ offset += 8;
+ const isPaid = Boolean(data[offset]);
+ offset += 1;
+ const solana = window.solanaWeb3;
+ const recipientWallet = new solana.PublicKey(data.slice(offset, offset + 32)).toBase58();
+ offset += 32;
+ const payoutUsdCents = readU64(data, offset);
+ offset += 8;
+ const debtBeforeUsdCents = readU64(data, offset);
+ return {
+ version,
+ queueId,
+ index,
+ isPaid,
+ recipientWallet,
+ payoutUsdCents,
+ debtBeforeUsdCents,
+ };
+}
+
+function parseSupportTicketInput(rawValue) {
+ const clean = String(rawValue || '').trim();
+ if (!clean) throw new Error('Введите номер билета.');
+ if (/^\d+$/.test(clean)) {
+ return { queueId: 1, index: BigInt(clean) };
+ }
+ const match = clean.match(/^([123])(?:\s*[-_:\/#]+\s*|\s+)(\d+)$/);
+ if (!match) {
+ throw new Error('Формат: для Q1 просто номер, для Q2/Q3 - например 2-15 или 3 8.');
+ }
+ return {
+ queueId: Number(match[1]),
+ index: BigInt(match[2]),
+ };
+}
+
+async function deriveSupportRandomWallet(extraText) {
+ const solana = await loadSolanaWeb3();
+ if (!window.crypto?.getRandomValues || !window.crypto?.subtle) {
+ throw new Error('Этот браузер не поддерживает безопасную генерацию ключей.');
+ }
+ const entropy = new Uint8Array(32);
+ window.crypto.getRandomValues(entropy);
+ const payload = concatBytes(
+ entropy,
+ utf8Bytes(extraText || ''),
+ utf8Bytes(new Date().toISOString()),
+ utf8Bytes(String(Date.now())),
+ utf8Bytes(String(performance?.now?.() || 0)),
+ );
+ const seed = new Uint8Array(await window.crypto.subtle.digest('SHA-256', payload));
+ const keypair = solana.Keypair.fromSeed(seed);
+ return {
+ address: keypair.publicKey.toBase58(),
+ privateKey32Base58: solana.bs58.encode(seed),
+ keypair,
+ generatedAt: new Date().toLocaleString('ru-RU'),
+ };
+}
+
+async function loadSupportPaymentsCore(endpoint) {
+ const solana = await loadSolanaWeb3();
+ const rpc = String(endpoint || '').trim();
+ const connection = new solana.Connection(rpc, 'confirmed');
+ const programId = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
+ const oracleAccount = new solana.PublicKey(SHINE_PAYMENTS_ORACLE_ACCOUNT);
+ const [configPda] = solana.PublicKey.findProgramAddressSync([utf8Bytes(SHINE_PAYMENTS_SEEDS.config)], programId);
+ const [coefPda] = solana.PublicKey.findProgramAddressSync([utf8Bytes(SHINE_PAYMENTS_SEEDS.coef)], programId);
+ const [queuesPda] = solana.PublicKey.findProgramAddressSync([utf8Bytes(SHINE_PAYMENTS_SEEDS.queues)], programId);
+ const [inflowPda] = solana.PublicKey.findProgramAddressSync([utf8Bytes(SHINE_PAYMENTS_SEEDS.inflow)], programId);
+ const [oracleAi, configAi, coefAi, queuesAi] = await Promise.all([
+ connection.getAccountInfo(oracleAccount, 'confirmed'),
+ connection.getAccountInfo(configPda, 'confirmed'),
+ connection.getAccountInfo(coefPda, 'confirmed'),
+ connection.getAccountInfo(queuesPda, 'confirmed'),
+ ]);
+ if (!oracleAi) throw new Error('Не найден аккаунт оракула SOL/USD.');
+ if (!configAi || !coefAi || !queuesAi) throw new Error('PDA программы оплаты ещё не инициализированы.');
+
+ const parsePrice = (data) => {
+ const price = readI64(data, 73);
+ const exponent = readI32(data, 89);
+ const publishTime = readI64(data, 93);
+ if (price <= 0n) throw new Error('Оракул вернул некорректную цену.');
+ let num = price * 100n;
+ let den = 1n;
+ if (exponent >= 0) {
+ num *= 10n ** BigInt(exponent);
+ } else {
+ den *= 10n ** BigInt(-exponent);
+ }
+ return { priceNum: num, priceDen: den, publishTime };
+ };
+
+ let configOffset = 0;
+ const configVersion = configAi.data[configOffset];
+ configOffset += 1;
+ const daoWallet = new solana.PublicKey(configAi.data.slice(configOffset, configOffset + 32)).toBase58();
+ configOffset += 32;
+ const inflowVault = new solana.PublicKey(configAi.data.slice(configOffset, configOffset + 32)).toBase58();
+
+ return {
+ connection,
+ programId,
+ oracleAccount,
+ configPda,
+ coefPda,
+ queuesPda,
+ inflowPda,
+ config: { version: configVersion, daoWallet, inflowVault },
+ coef: parsePaymentsCoef(coefAi.data),
+ queues: parsePaymentsQueues(queuesAi.data),
+ pyth: parsePrice(oracleAi.data),
+ };
+}
+
+function queueStateView(queues, queueId) {
+ if (queueId === 2) {
+ return {
+ ticketsTotal: queues.q2TicketsTotal,
+ ticketsPaid: queues.q2TicketsPaid,
+ sumTotalUsdCents: queues.q2SumTotalUsdCents,
+ sumPaidUsdCents: queues.q2SumPaidUsdCents,
+ };
+ }
+ if (queueId === 3) {
+ return {
+ ticketsTotal: queues.q3TicketsTotal,
+ ticketsPaid: queues.q3TicketsPaid,
+ sumTotalUsdCents: queues.q3SumTotalUsdCents,
+ sumPaidUsdCents: queues.q3SumPaidUsdCents,
+ };
+ }
+ return {
+ ticketsTotal: queues.q1TicketsTotal,
+ ticketsPaid: queues.q1TicketsPaid,
+ sumTotalUsdCents: queues.q1SumTotalUsdCents,
+ sumPaidUsdCents: queues.q1SumPaidUsdCents,
+ };
+}
+
+function queueSeedFor(queueId) {
+ if (queueId === 2) return SHINE_PAYMENTS_SEEDS.q2;
+ if (queueId === 3) return SHINE_PAYMENTS_SEEDS.q3;
+ return SHINE_PAYMENTS_SEEDS.q1;
+}
+
+function ticketPdaFor(programId, queueId, index) {
+ const solana = window.solanaWeb3;
+ return solana.PublicKey.findProgramAddressSync(
+ [utf8Bytes(queueSeedFor(queueId)), u64ToBytes(index)],
+ programId,
+ );
+}
+
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
@@ -99,6 +406,640 @@ export function render({ navigate }) {
arweaveWalletCtx = null;
}
+ function renderSupportHub() {
+ activeModeToken += 1;
+ clearArweaveSecretsInMemory();
+ content.innerHTML = '';
+
+ const backBtn = createModeBackButton(renderWalletChoice);
+
+ const intro = document.createElement('div');
+ intro.className = 'card stack';
+ intro.innerHTML = `
+
Поддержать проект Сияние
+
+ Здесь можно купить билет, посмотреть очередь и увидеть, как устроена покупка.
+ Оплата идет в SOL, а сумма считается по курсу USD/USDT на момент транзакции.
+ Номер билета и секретный ключ кошелька нужно сохранить отдельно.
+
+ Генерация использует безопасный рандом браузера, а ваш дополнительный текст и текущее время добавляются как примесь.
+ Вводить текст необязательно: даже без него ключи остаются случайными.
+
+ Для Q1 вводите просто номер билета. Для Q2 и Q3 укажите номер в формате 2-15 или 3 8.
+ Экран покажет, сколько билетов перед ним уже стоит, сколько уже выплачено и кому принадлежит билет.
+
+ Вы покупаете только билет очереди 1. Сумма задается в USD, а оплата уходит в SOL по курсу USDT/SOL на момент транзакции.
+ Покупка подписывается вашим client key. Интерфейс проверяет допуск по курсу до 3 процентов и не дает выходить за текущий лимит по коэффициенту.
+
В очереди перед вами уже куплено: ${formatUsdCentsText(queue.sumTotalUsdCents)} USD
+
Осталось до лимита текущего коэффициента: ${formatUsdCentsText(remainingUsdCents)} USD
+
Следующий билет: №${(queue.ticketsTotal + 1n).toString()}
+
Примерная цена: ${amountSol} SOL
+
Максимум при допуске 3%: ${maxPaySol} SOL
+
${canBuy ? 'Покупка укладывается в текущий лимит.' : 'Сумма сейчас не укладывается в текущий лимит или введена некорректно.'}
+
+ Если текущий лимит заполнится, откроется новый лимит с более низким коэффициентом.
+ Покупка идет в SOL, но считается по курсу USD/USDT на момент транзакции.
+