From c27da63a3e653c3016bd65184921164037776f84acc5ec2e09e661be29bffe70 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 19 May 2026 00:07:49 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20=D0=B7=D0=B0=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D1=88=D0=B8=D0=B5=D1=81=D1=8F=20=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOC/api/PWA_FCM_SETUP.md | 52 ----- ...7_уведомления-заглушки-и-правило-intake.md | 22 -- ...6_2123_solana-генерация-кошелька-base58.md | 36 ++++ .../2026-05-14_1236_thread-действия-и-счетчики.md | 0 VERSION.properties | 4 +- shine-UI/js/pages/wallet-view.js | 202 +++++++++++++++++- shine-UI/js/services/solana-wallet-service.js | 29 +++ 7 files changed, 268 insertions(+), 77 deletions(-) delete mode 100644 DOC/api/PWA_FCM_SETUP.md delete mode 100644 Dev_Docs/Pending_Features/2026-05-14_1327_уведомления-заглушки-и-правило-intake.md create mode 100644 Dev_Docs/Pending_Features/2026-05-16_2123_solana-генерация-кошелька-base58.md rename Dev_Docs/Pending_Features/{ => Сделаные}/2026-05-14_1236_thread-действия-и-счетчики.md (100%) diff --git a/DOC/api/PWA_FCM_SETUP.md b/DOC/api/PWA_FCM_SETUP.md deleted file mode 100644 index 3c0848f..0000000 --- a/DOC/api/PWA_FCM_SETUP.md +++ /dev/null @@ -1,52 +0,0 @@ -# Настройка PWA + FCM для веб-клиента (Chrome/Edge/Firefox/Safari iOS) - -## 1) Что нужно создать в Firebase -1. Создать проект Firebase. -2. Включить Cloud Messaging. -3. Создать Web App и получить конфиг: - - apiKey - - authDomain - - projectId - - messagingSenderId - - appId -4. В Cloud Messaging -> Web Push certificates сгенерировать VAPID key. -5. Для серверной отправки взять **Server key** (legacy) или настроить HTTP v1 (service account). - -## 2) Куда вставить токены в клиенте -Файл: `shine-UI/index.html` и `shine-UI/firebase-messaging-sw.js`. - -Заполнить: -- `window.__SHINE_FIREBASE_CONFIG__` -- `window.__SHINE_FIREBASE_VAPID_KEY__` -- `FIREBASE_CONFIG` (в service worker) - -## 3) Куда вставить серверный ключ FCM -Файл: `src/main/resources/application.properties` - -Добавить: -``` -fcm.server.key=YOUR_FCM_SERVER_KEY -``` - -## 4) PWA требования -1. Открывать сайт только по HTTPS (или localhost). -2. Разрешить уведомления в браузере. -3. Убедиться, что `manifest.webmanifest` доступен. -4. Убедиться, что `firebase-messaging-sw.js` зарегистрирован. - -## 5) Safari / iPhone (iOS) -- Нужен iOS 16.4+. -- Пользователь должен добавить сайт на Home Screen. -- После запуска PWA с Home Screen дать разрешение на уведомления. -- Без Home Screen web push в Safari iOS не работает. - -## 6) Проверка -1. Логин в приложении. -2. Клиент вызывает `UpsertPushToken` и отправляет FCM токен на сервер. -3. Вызов `SendDirectMessage` пользователю без активной WS доставки. -4. Сервер шлет push через FCM. - -## 7) Поддержка разных браузеров -- Chrome/Edge/Opera/Android Browser: FCM web push поддерживается нативно. -- Firefox: поддержка web push есть, но тестировать отдельно (поведение токенов отличается). -- Safari macOS/iOS: web push есть, но требуется PWA режим и Apple-ограничения. diff --git a/Dev_Docs/Pending_Features/2026-05-14_1327_уведомления-заглушки-и-правило-intake.md b/Dev_Docs/Pending_Features/2026-05-14_1327_уведомления-заглушки-и-правило-intake.md deleted file mode 100644 index 4a2fbfa..0000000 --- a/Dev_Docs/Pending_Features/2026-05-14_1327_уведомления-заглушки-и-правило-intake.md +++ /dev/null @@ -1,22 +0,0 @@ -# Уведомления: продуктовые заглушки + правило intake в AGENTS - -- краткое описание фичи: - - На вкладке `Уведомления` удалены демонстрационные карточки из разделов `Ответы` и `События`. - - В каждом табе добавлена отдельная продуктовая заглушка: - - `Ответы`: про ответы и комментарии на сообщения в публичных каналах; - - `События`: про подписки, добавления, лайки и прочие действия. - - В обоих табах добавлено явное сообщение, что раздел находится в разработке. - - В `AGENTS.md` добавлен обязательный блок: при новом задании сначала пересказ, вопросы, при необходимости идеи, оценка фичи и обязательное подтверждение перед началом реализации. - -- что именно проверять: - - Открыть `Уведомления` и проверить, что в `Ответы` отображается только заглушка (без примеров карточек). - - Переключить на `События` и проверить отдельную заглушку с текстом про события. - - Убедиться, что в обоих табах присутствует сообщение о разработке и будущем добавлении функционала. - - Проверить наличие нового блока `Коммуникация по новым задачам (обязательно)` в `AGENTS.md`. - -- ожидаемый результат: - - Вкладка уведомлений содержит только две заглушки по табам и не показывает тестовые данные. - - Правило работы с новыми задачами зафиксировано в `AGENTS.md`. - -- статус: - - pending diff --git a/Dev_Docs/Pending_Features/2026-05-16_2123_solana-генерация-кошелька-base58.md b/Dev_Docs/Pending_Features/2026-05-16_2123_solana-генерация-кошелька-base58.md new file mode 100644 index 0000000..76f9490 --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-05-16_2123_solana-генерация-кошелька-base58.md @@ -0,0 +1,36 @@ +## Краткое описание + +На экране `Кошелёк -> Solana кошелёк` добавлен блок создания нового Solana-кошелька: +- генерация случайного кошелька; +- генерация публичного ключа из введённого приватного ключа Base58 (32 байта). + +Добавлены: +- валидация формата Base58; +- проверка точной длины приватного ключа (ровно 32 байта после декодирования); +- запрет ввода слишком длинного значения (`maxlength=44`); +- статус `Подходит` для валидного ввода; +- нередактируемое поле публичного ключа с возможностью копирования. + +## Что проверять + +1. Открыть `Кошелёк -> Solana кошелёк`. +2. В блоке создания кошелька нажать `Сгенерировать случайный кошелёк`. +3. Проверить, что появились: + - приватный ключ Base58; + - публичный ключ Base58 (в нередактируемом поле). +4. Нажать `Копировать приватный` и `Копировать публичный` — убедиться, что значения копируются. +5. Ввести невалидный приватный ключ (символы не из Base58) — увидеть ошибку формата. +6. Ввести слишком короткий ключ — увидеть сообщение, что значение слишком короткое. +7. Ввести валидный Base58-ключ на 32 байта — увидеть статус `Подходит`. +8. Нажать `Сгенерировать из приватного ключа` — публичный ключ должен сгенерироваться. +9. Проверить, что в поле ввода приватного ключа нельзя вставить/ввести более 44 символов. + +## Ожидаемый результат + +- Оба сценария генерации работают стабильно. +- Для невалидного ввода показываются корректные сообщения. +- Поле публичного ключа не редактируется, но значение можно скопировать. + +## Статус + +`pending` diff --git a/Dev_Docs/Pending_Features/2026-05-14_1236_thread-действия-и-счетчики.md b/Dev_Docs/Pending_Features/Сделаные/2026-05-14_1236_thread-действия-и-счетчики.md similarity index 100% rename from Dev_Docs/Pending_Features/2026-05-14_1236_thread-действия-и-счетчики.md rename to Dev_Docs/Pending_Features/Сделаные/2026-05-14_1236_thread-действия-и-счетчики.md diff --git a/VERSION.properties b/VERSION.properties index d06aa2e..93367e4 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.58 -server.version=1.2.52 +client.version=1.2.59 +server.version=1.2.53 diff --git a/shine-UI/js/pages/wallet-view.js b/shine-UI/js/pages/wallet-view.js index f8fc00e..fcb4e29 100644 --- a/shine-UI/js/pages/wallet-view.js +++ b/shine-UI/js/pages/wallet-view.js @@ -1,6 +1,8 @@ import { renderHeader } from '../components/header.js'; import { state } from '../state.js'; import { + createRandomSolanaWallet, + createSolanaWalletFromPrivateBase58, formatSol, getBalanceSol, getTopupSiteUrl, @@ -17,6 +19,7 @@ import { } from '../services/arweave-wallet-service.js'; export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' }; +const SOLANA_PRIVATE_BASE58_MAX_LEN = 44; function nowRu() { return new Date().toLocaleString('ru-RU'); @@ -165,6 +168,203 @@ 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('Кошелёк не инициализирован.'); @@ -265,7 +465,7 @@ export function render({ navigate }) { } }); - content.append(backBtn, card, actions); + content.append(backBtn, card, actions, generatedCard); setStatus('Инициализация wallet.key...'); try { diff --git a/shine-UI/js/services/solana-wallet-service.js b/shine-UI/js/services/solana-wallet-service.js index f8731fd..8b5540d 100644 --- a/shine-UI/js/services/solana-wallet-service.js +++ b/shine-UI/js/services/solana-wallet-service.js @@ -6,6 +6,7 @@ const DEFAULT_SOLANA_ENDPOINT = 'https://api.devnet.solana.com'; const TOPUP_SITE_URL = 'https://shine-promo-solana-devnet.shineup.me/'; let solanaLibPromise = null; +const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]+$/; function normalizeEndpoint(url) { const raw = String(url || '').trim(); @@ -37,6 +38,34 @@ export async function deriveWalletFromPassword(password) { }; } +export async function createRandomSolanaWallet() { + const solana = await loadSolanaLib(); + const keypair = solana.Keypair.generate(); + const privateKey32Base58 = solana.bs58.encode(keypair.secretKey.slice(0, 32)); + return { + address: keypair.publicKey.toBase58(), + privateKey32Base58, + keypair, + }; +} + +export async function createSolanaWalletFromPrivateBase58(privateKey32Base58) { + const solana = await loadSolanaLib(); + const clean = String(privateKey32Base58 || '').trim(); + if (!clean) throw new Error('Введите приватный ключ'); + if (!BASE58_RE.test(clean)) throw new Error('Разрешены только символы Base58'); + const privateBytes = solana.bs58.decode(clean); + if (privateBytes.length !== 32) { + throw new Error('Приватный ключ должен быть ровно 32 байта в Base58'); + } + const keypair = solana.Keypair.fromSeed(privateBytes); + return { + address: keypair.publicKey.toBase58(), + privateKey32Base58: clean, + keypair, + }; +} + export async function getWalletFromStoredDeviceKey({ login, storagePwd }) { const cleanLogin = String(login || '').trim(); const cleanPwd = String(storagePwd || '').trim();