Compare commits

..

57 Commits

Author SHA256 Message Date
722d055e2d Merge esp32-2-by-tbilisi into main
Merge ветки esp32-2-by-tbilisi в main: TrustedDeviceLogin, редактирование и удаление личных сообщений, browser wallet extension, аудит Solana smart contracts, удаление устаревшего SHiNE-promo-solana-devnet и закрытых pending features.
2026-06-18 12:21:37 +00:00
AidarKC
a9a55da8e0 Удалить закрытые записи pending features 2026-06-18 16:11:36 +04:00
AidarKC
f2b23ace8b Удалить устаревший SHiNE-promo-solana-devnet 2026-06-18 15:57:55 +04:00
AidarKC
1f2048e270 Перестроить структуру codex-agent-VPS для VPS-пакета 2026-06-18 15:29:09 +04:00
AidarKC
b16a23243e Добавить переносимый шаблон codex-agent-VPS 2026-06-18 15:18:07 +04:00
AidarKC
653f1268a6 Проверено: DM-ревизии подтверждены, pending убран 2026-06-18 14:34:37 +04:00
AidarKC
56db6d0add TrustedDeviceLogin API и настройки входа через устройство
Что сделано:\n- публичный API сценария входа через доверенное устройство переведён на TrustedDeviceLogin\n- добавлен GetTrustedDeviceLoginSettings\n- отсутствие записи настроек на сервере теперь трактуется как enabled=true и hasPassword=false\n- ttlSeconds убран из клиентского API, TTL заявки фиксирован на сервере: 300 секунд\n- в shine-UI добавлен отдельный экран настроек входа через устройство и статус на основном экране\n- browser wallet переведён на новые TrustedDeviceLogin операции\n- в wallet добавлен выбор rootKey/deviceKey для будущего запроса подписи\n- документация API обновлена\n\nЧто ещё не проверено вручную end-to-end:\n- полный сценарий UI/plugin после этого деплоя не прогонялся руками до конца\n- сам signaling подписи в wallet всё ещё не реализован
2026-06-18 14:19:31 +04:00
AidarKC
cf2152dcfc НЕ ПРОВЕРЕНО: UI редактирования и удаления личных сообщений 2026-06-18 13:04:06 +04:00
AidarKC
a95bd245cf НЕ ПРОВЕРЕНО: откат DM-вложений, оставлены ревизии и удаление 2026-06-18 12:24:14 +04:00
AidarKC
92fd315505 НЕ ПРОВЕРЕНО: DM-вложения, upload файлов и ревизии личных сообщений 2026-06-18 11:46:58 +04:00
AidarKC
2225c2d173 Wallet plugin: офлайн wallet-session и выбор homeserver\n\nСделано:\n- wallet plugin сохраняет PDA-профиль и остаётся офлайн до действия;\n- добавлен каркас выбора ключа подписи и homeserver-устройства;\n- добавлен ручной refresh trusted devices через ListSessions;\n- на регистрации показан первый сервер SHiNE и его адрес;\n- обновлены pending notes для ручной проверки.\n\nЕщё не проверено / не доделано:\n- end-to-end ручная проверка plugin после этих правок не завершена;\n- signaling запроса подписи и ответ подписи ещё не реализованы;\n- локальный browser plugin нужно отдельно reload в Chrome/Opera. 2026-06-18 11:04:34 +04:00
AidarKC
f8a76bcd7f Автоопределение SHiNE-сервера по логину через PDA 2026-06-16 16:32:33 +04:00
AidarKC
3efa8bb7ee Wallet-session pairing и browser plugin wallet, оплаты пока не работают 2026-06-16 16:23:08 +04:00
AidarKC
5c155ef503 UI: нормальное закрытие сессий и сортировка устройств 2026-06-16 10:36:43 +04:00
AidarKC
41d199e24a Показывать ошибки pairing-пароля в отдельном окне 2026-06-15 15:31:19 +04:00
AidarKC
e1f2b54de3 Сузить диалог изменения pairing-пароля 2026-06-15 15:23:19 +04:00
AidarKC
d6c5757dfa Переделать UI дополнительного pairing-пароля 2026-06-15 13:35:05 +04:00
AidarKC
9a489801c5 Доработать UX и отмену pairing по коду 2026-06-15 13:13:16 +04:00
AidarKC
9fcdcd087b Убрать QR-заглушку и очищать код после reject 2026-06-15 02:37:26 +04:00
AidarKC
af1304022e Исправить синтаксис экрана pairing 2026-06-15 02:30:17 +04:00
AidarKC
7972676eb8 Исправить pairing без пароля и убрать фантомные заявки 2026-06-15 02:21:21 +04:00
AidarKC
bef205aec7 Разрешить pairing без доп пароля 2026-06-15 00:54:56 +04:00
AidarKC
49fdbbf7ae Исправить переход со старта на экран выбора входа 2026-06-14 21:33:43 +04:00
AidarKC
dd69a52273 Форсировать обновление UI модулей входа 2026-06-14 21:13:22 +04:00
AidarKC
c681b4d684 Добавить UI pairing по коду и обновить документацию агента 2026-06-14 20:39:05 +04:00
AidarKC
b166013707 Бот: починить resume-вызов Codex CLI 2026-06-14 20:30:17 +04:00
AidarKC
3e04727022 Добавить ESP pairing через доверенные сессии 2026-06-14 18:21:23 +04:00
AidarKC
5d13112b00 ESP32: уменьшить рамку wallet QR 2026-06-14 11:22:11 +04:00
AidarKC
373f88086e ESP32: подправить вертикальный ритм wallet QR 2026-06-14 11:16:49 +04:00
AidarKC
05492306c0 ESP32: смягчить SHiNE reconnect при плохом сервере 2026-06-14 11:01:47 +04:00
AidarKC
423d490939 ESP32: доработать home экран и wallet QR 2026-06-14 10:50:31 +04:00
AidarKC
7edc0ba901 ESP32: зафиксировать LVGL qrcode конфиг 2026-06-14 10:28:42 +04:00
AidarKC
0ebb71daf1 ESP32: добавить реальный wallet QR через LVGL 2026-06-14 10:27:10 +04:00
AidarKC
4b15cabd4f ESP32: добавить быстрый QR-экран кошелька 2026-06-13 23:20:35 +04:00
AidarKC
be4a2d135a Проект: включить ESP32-wallet в основной репозиторий 2026-06-13 23:01:57 +04:00
AidarKC
ca4cfd9d8d UI: выровнять online-флаг с ответом ListSessions 2026-06-13 15:50:47 +04:00
AidarKC
96d292074b API: добавить online-флаг для ListSessions 2026-06-13 15:49:34 +04:00
AidarKC
0536a018c6 ESP32: починить JSON auth для homeserver sessionType 2026-06-13 15:22:19 +04:00
AidarKC
81d1b84a7d ESP32: отправлять homeserver sessionType в SHiNE auth 2026-06-13 15:08:53 +04:00
AidarKC
61c21b245e UI: явно показать тип сеанса в списке устройств 2026-06-13 15:05:41 +04:00
AidarKC
919387f581 API сессий: добавить sessionType и clientPlatform 2026-06-13 14:15:42 +04:00
AidarKC
3b8ea70d3c ESP32: добавить диагностику подключения SHiNE и починить WS handshake 2026-06-13 13:09:32 +04:00
AidarKC
477ab3b580 ESP32: починить добавление homeserver и вернуть автопрогон 2026-06-13 12:53:40 +04:00
AidarKC
a1da814030 ESP32: добавить flow обновления homeserver в user PDA 2026-06-13 09:07:49 +04:00
AidarKC
19fd5611b2 ESP32: добавить автотест регистрации и исправить signed server profile 2026-06-13 08:41:25 +04:00
AidarKC
556004a557 ESP32: исправить off-curve проверку для user PDA 2026-06-13 08:20:12 +04:00
AidarKC
fba6d6bba0 ESP32: исправить derivation user_pda для Solana 2026-06-13 07:36:45 +04:00
AidarKC
04252e006b ESP32: сохранять полный текст ошибки регистрации 2026-06-13 00:24:42 +04:00
AidarKC
436e1f0c53 ESP32: добавить USB-диагностику регистрации Solana 2026-06-13 00:01:57 +04:00
AidarKC
21030b1d51 ESP32: исправить base64 сериализацию Solana транзакции 2026-06-12 23:48:38 +04:00
AidarKC
b583a86ade ESP32: исправить ABI регистрации и подробные ошибки RPC 2026-06-12 23:43:42 +04:00
AidarKC
3262ec9b4a ESP32: перевести UI регистрации на английский 2026-06-12 23:35:05 +04:00
AidarKC
0c9afea67a ESP32: экран подтверждения регистрации 2026-06-12 23:02:07 +04:00
AidarKC
b83543d018 ESP32: регистрация по кнопке и зазор между кнопками 2026-06-12 22:24:21 +04:00
AidarKC
d4a0185507 Перенёс основной ESP32-скетч в main-device 2026-06-12 22:02:08 +04:00
AidarKC
42dcf6970d homeserver: рендейм subserver→homeserver, документ деривации ключей, запрет пустого пароля
Основное (наша работа в этой сессии):
- Переименование «subserver» → «homeserver» по всему проекту: основной ESP32-скетч
  (папка shine_subserver_ui → shine_homeserver_ui, .ino, flash-скрипт, режим burn.sh
  homeserver-ui), скетч lvgl_nav_minimal_test (ключ homeserver.key:<имя>), spec-доки
  reference/*, формат PDA (терминология session_type=100 «Homeserver пользователя»),
  константа SESSION_TYPE_HOMESERVER в JS и Rust (значение 100 не менялось, формат не затронут),
  pending/future доки, AGENTS.md, DAO-док. Сохранены отдельный lvgl_subserver_touch_test и
  историческая пометка о рендейме в DERIVATION.md.
- Новый источник истины по деривации ключей: Dev_Docs/Keys/DERIVATION.md (Argon2id-секрет из
  пароля, формула Ed25519(SHA-256(base64(secret)|suffix)), суффиксы root/bch/dev/homeserver.key,
  Solana-ключ = dev.key). Уточнены роли root (главный/master) и dev (пополняемый кошелёк) в
  Dev_Docs/Keys/README.md.
- UI: убран легаси-путь пустого пароля (derivePasswordSeed и др.), deriveMasterSecretFromPassword
  бросает ошибку на пустом пароле, register-view блокирует пустой пароль; экран пополнения
  переведён на канонический device-адрес из preGeneratedKeyBundle (удалён расходящийся
  deriveWalletFromPassword).

Включены также параллельные правки Solana-аудита №3 (были в рабочем дереве, переплетены в lib.rs):
- shine_users: defense-in-depth «строгий список аккаунтов» (require!(it.next().is_none()))
  в init/update economy config и create/update user PDA, плюс описание в doc/programs/shine_users.md;
- Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:16:12 +04:00
AidarKC
cf6a2830c8 solana: закрыть griefing создания PDA и заморозку выплат, добавить аудит №2
shine_payments + shine_users:
- create_pda_account переведён на «создание поверх предзаполненного»
  (allocate+assign+добор ренты), чтобы подсев лампортов на детерминированный
  адрес PDA (тикет/логин) не блокировал создание — закрыт LOW из аудита №1;
  в shine_payments is_uninitialized_account перестала зависеть от баланса.

shine_payments (HIGH из аудита №2):
- запрещён recipient == inflow_vault в buy_ticket*, manager_add_ticket и
  change_ticket_recipient; добавлена защита по умолчанию в transfer_from_vault
  (require vault.key != recipient.key). Это убирает алиасинг аккаунта в
  step_payout, который навсегда замораживал очередь выплат и средства вольта.

Документация и учёт:
- doc/programs/shine_payments.md §3.4, §10.1; doc/programs/shine_users.md §3.3;
- Dev_Docs/audit: добавлен аудит №2, обе закрытые находки помечены ИСПРАВЛЕНО;
- Dev_Docs/Pending_Features: две записи на ручную e2e-проверку на devnet;
- VERSION.properties: client 1.2.161, server 1.2.150.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 04:10:31 +04:00
241 changed files with 34680 additions and 3517 deletions

10
.gitignore vendored
View File

@ -78,8 +78,18 @@ shine-solana/shine/scripts/**/TEMP_*.md
# Локальные артефакты и внешние материалы ESP32-подпроекта
ESP32/**/.git/
ESP32/**/.idea/
ESP32-wallet/.idea/
ESP32/**/.arduino-build/
ESP32/**/official-demo/
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/**
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/**
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/**
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/**
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/lv_conf.h
ESP32/**/original-firmware/*.bin
ESP32/**/original-firmware/*.bin.sha256
ESP32/**/*.elf

View File

@ -24,12 +24,12 @@
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
## ESP32 UI сабсервера
## ESP32 UI homeserver
- Для UI-скетча устройства `ESP32-S3-Touch-AMOLED-2.16` документ-спецификация и Arduino-скетч должны всегда оставаться синхронными.
- Актуальный документ по экранной логике, состояниям, кнопкам, полям, статусам и ограничениям UI считать источником истины для скетча.
- При изменении документа обязательно в том же наборе изменений приводить в соответствие скетч.
- При изменении скетча обязательно в том же наборе изменений обновлять документ, если поменялись экраны, тексты, переходы, статусы, кнопки, поля или поведение.
- Для нового ESP32 UI-прототипа сабсервера использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде.
- Для нового ESP32 UI-прототипа homeserver использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде.
## Solana-модуль
- В проекте есть отдельный Solana/Anchor-модуль в папке `shine-solana/shine/`.

View File

@ -155,11 +155,11 @@
- это обязательный шаг перед переходом от "собрали" к "доверяем".
### 3. Устройство на ESP32 как сабсервер с ключами
### 3. Устройство на ESP32 как homeserver с ключами
Что сделать:
- дописать прошивку, чтобы устройство могло выступать сабсервером с ключами;
- дописать прошивку, чтобы устройство могло выступать homeserver с ключами;
- дать ему возможность регистрироваться и подключаться к серверу;
- определить, какие операции устройство подписывает и где хранит ключевой материал.

View File

@ -2,7 +2,7 @@
Этот файл описывает именно этапы авторизации клиента, то есть как создать новую сессию и как войти в уже существующую.
Здесь четыре метода:
Здесь четыре базовых метода обычной авторизации:
- `AuthChallenge`
- `CreateAuthSession`
@ -17,8 +17,35 @@
- на втором шаге клиент присылает подписанный ответ;
- сервер сверяет актуальные публичные ключи и только потом проверяет подпись.
Новые поля этого раздела:
- `sessionType` — числовой код типа сессии;
- `clientPlatform` — свободная строка платформы клиента.
Текущие поддерживаемые коды `sessionType`:
- `1` — обычный клиент;
- `50` — кошелёк;
- `100` — homeserver.
Правило проверки `sessionType`:
1. если в `Solana PDA` нет записи для `sessionKey`, сервер принимает `sessionType`, присланный клиентом;
2. если запись в `PDA` есть, `sessionType` в запросе должен совпадать с `session_type` из `PDA`;
3. при несовпадении сервер возвращает `460 / SESSION_TYPE_MISMATCH`.
Ниже в документе сначала описан сценарий, а потом зафиксированы точные форматы запросов и ответов.
Отдельно появился новый серверный сценарий pairing через доверенный homeserver/ESP. Он не заменяет обычный вход и описан в:
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
Кратко:
- `AuthChallenge/CreateAuthSession` и `SessionChallenge/SessionLogin` остаются каноническими потоками обычной авторизации;
- pairing через ESP идёт отдельными `op` и только подготавливает безопасное добавление новой сессии;
- решение об одобрении pairing принимает любая уже авторизованная доверенная сессия пользователя.
## 1. Поток авторизации
Поддерживаются два сценария:
@ -94,6 +121,8 @@ ed25519/BASE64_PUBLIC_KEY
"authNonce": "nonce",
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
"signatureB64": "BASE64_SIGNATURE",
"sessionType": 1,
"clientPlatform": "Web",
"clientInfo": "Android 15; Pixel 9"
}
}
@ -153,6 +182,8 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`.
- `422 / DEVICE_KEY_NOT_ACTUAL``deviceKey` не совпадает с актуальной версией на сервере.
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
- `460 / SESSION_TYPE_MISMATCH``sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
- `501 / DB_ERROR_SESSION_CREATE` — ошибка БД при создании записи активной сессии.
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
@ -208,6 +239,8 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
"timeMs": 1774600010456,
"signatureB64": "BASE64_SIGNATURE",
"sessionType": 1,
"clientPlatform": "Web",
"clientInfo": "Android 15; Pixel 9"
}
}
@ -258,12 +291,40 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` не поддерживается текущим сервером.
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey` или `signatureB64`.
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
- `460 / SESSION_TYPE_MISMATCH``sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при чтении пользователя для этой сессии.
- `422 / USER_NOT_FOUND_FOR_SESSION` — пользователь, которому принадлежит сессия, не найден.
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
---
## 6. Pairing через homeserver/ESP
Новые `op`, относящиеся к этому сценарию:
- `GetTrustedDeviceLoginSettings`
- `UpsertTrustedDeviceLoginSettings`
- `StartTrustedDeviceLogin`
- `ListTrustedDeviceLoginRequests`
- `ApproveTrustedDeviceLogin`
- `RejectTrustedDeviceLogin`
- `CancelTrustedDeviceLogin`
- `GetTrustedDeviceLoginStatus`
В этом потоке:
- новое устройство не владеет `deviceKey` и не проходит обычный `CreateAuthSession`;
- пароль проверяется сервером только как фильтр;
- решение об одобрении принимает уже авторизованная доверенная сессия пользователя;
- сервер не расшифровывает `encryptedPayload` и не становится источником приватных ключей.
Точные форматы этих операций см. в `03_Session_Management_API.md` и в протокольном документе:
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
---
## 6. Пример ошибки
```json

View File

@ -7,6 +7,20 @@
- `ListSessions` — получить список активных сессий пользователя;
- `CloseActiveSession` — закрыть одну из активных сессий.
Дополнительно в этом же слое управления сессиями появился сценарий pairing через доверенную уже авторизованную сессию пользователя:
- `GetTrustedDeviceLoginSettings`
- `UpsertTrustedDeviceLoginSettings`
- `ListTrustedDeviceLoginRequests`
- `ApproveTrustedDeviceLogin`
- `RejectTrustedDeviceLogin`
- `CancelTrustedDeviceLogin`
Анонимное новое устройство работает с двумя связанными операциями:
- `StartTrustedDeviceLogin`
- `GetTrustedDeviceLoginStatus`
Логика раздела такая:
- сначала пользователь проходит `SessionLogin`;
@ -42,6 +56,9 @@
"sessions": [
{
"sessionId": "sess_7c5e5c4b",
"sessionType": 1,
"clientPlatform": "Web",
"onlineOnThisServer": true,
"clientInfoFromClient": "Android 15; Pixel 9",
"clientInfoFromRequest": "UA=Java-http-client/17.0.18; remote=127.0.0.1",
"geo": "RU/Moscow",
@ -58,6 +75,20 @@
- `501 / DB_ERROR_LIST_SESSIONS` — ошибка БД при чтении списка активных сессий.
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
### Поля одной сессии в `ListSessions`
- `sessionId` — идентификатор активной сессии;
- `sessionType` — числовой код типа сессии:
- `1` — клиент;
- `50` — кошелёк;
- `100` — homeserver;
- `clientPlatform` — строка платформы, как её прислал клиент;
- `onlineOnThisServer``true`, если эта сессия сейчас держит живое WebSocket-подключение именно к данному серверу;
- `clientInfoFromClient` — краткая строка клиента;
- `clientInfoFromRequest` — строка, собранная сервером из запроса;
- `geo` — страна/город или fallback-строка;
- `lastAuthenticatedAtMs` — время последней успешной авторизации этой сессии.
---
## 2. `CloseActiveSession`
@ -134,3 +165,320 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M
Важно: это **не человеко-читаемое имя**, а непрозрачный идентификатор.
Нужно передавать его как есть, без нормализации регистра и без URL-экранирования внутри JSON.
---
## 5. TrustedDeviceLogin через доверенную сессию
Этот блок относится к сценарию добавления новой сессии через доверенное устройство пользователя.
### 5.1. `GetTrustedDeviceLoginSettings`
Доступно для любой уже авторизованной доверенной сессии пользователя.
### Запрос
```json
{
"op": "GetTrustedDeviceLoginSettings",
"requestId": "trusted-login-get-001",
"payload": {
}
}
```
### Успешный ответ
```json
{
"op": "GetTrustedDeviceLoginSettings",
"requestId": "trusted-login-get-001",
"status": 200,
"ok": true,
"payload": {
"enabled": true,
"hasPassword": false
}
}
```
Если отдельной записи настроек на сервере ещё нет, сервер считает состояние по умолчанию таким:
- `enabled = true`
- `hasPassword = false`
### Ошибки
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
### 5.2. `UpsertTrustedDeviceLoginSettings`
Доступно для любой уже авторизованной доверенной сессии пользователя.
### Запрос
```json
{
"op": "UpsertTrustedDeviceLoginSettings",
"requestId": "esp-set-001",
"payload": {
"enabled": true,
"passwordHash": "sha256$0123abcd..."
}
}
```
Если вход через доверенное устройство должен работать **без доп. пароля**, клиент включает его с пустым `passwordHash`.
Если `enabled = false`, сервер автоматически удаляет пароль и запрещает вход через другое устройство.
Формат непустого `passwordHash`:
```text
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
```
### Успешный ответ
```json
{
"op": "UpsertTrustedDeviceLoginSettings",
"requestId": "esp-set-001",
"status": 200,
"ok": true,
"payload": {
"enabled": true,
"hasPassword": true
}
}
```
### Ошибки
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
### 5.3. `StartTrustedDeviceLogin`
Эта операция доступна без уже существующей пользовательской сессии.
### Запрос
```json
{
"op": "StartTrustedDeviceLogin",
"requestId": "esp-start-001",
"payload": {
"login": "alice",
"passwordHash": "sha256$0123abcd...",
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY",
"requesterSessionType": 1,
"requesterClientPlatform": "Android",
"payloadType": 1
}
}
```
Если на доверённом устройстве вход включён **без доп. пароля**, новое устройство может отправить пустой `passwordHash`.
Поле `trustedSessionOnline` показывает, что у пользователя сейчас есть хотя бы одна онлайн доверенная сессия, способная принять pairing-заявку.
TTL заявки фиксирован на сервере и сейчас всегда равен `300` секундам.
### Успешный ответ
```json
{
"op": "StartTrustedDeviceLogin",
"requestId": "esp-start-001",
"status": 200,
"ok": true,
"payload": {
"pairingId": "base64url",
"state": "created",
"shortCode": "4920709",
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
"expiresAtMs": 1781441990538,
"trustedSessionOnline": true
}
}
```
### Ошибки
- `400 / EMPTY_LOGIN`
- `400 / EMPTY_REQUESTER_SESSION_KEY`
- `400 / BAD_REQUESTER_SESSION_KEY`
- `400 / BAD_SESSION_TYPE`
- `400 / BAD_PAYLOAD_TYPE`
- `422 / PAIRING_NOT_AVAILABLE`
- `422 / PAIRING_PASSWORD_INVALID` — pairing-пароль не подходит. Та же ошибка возвращается и если новое устройство ввело пароль, а у пользователя режим pairing включён без пароля.
- `422 / PAIRING_NO_TRUSTED_SESSION_ONLINE` — сейчас нет ни одной онлайн доверённой сессии пользователя, поэтому код не создаётся.
- `429 / PAIRING_RATE_LIMITED`
### 5.4. `ListTrustedDeviceLoginRequests`
Доступно для любой уже авторизованной доверенной сессии пользователя.
Возвращает только реально активные pending-заявки со `state = created`. Уже `approved` и `rejected` заявки в этот список больше не попадают.
### Успешный ответ
```json
{
"op": "ListTrustedDeviceLoginRequests",
"requestId": "esp-list-001",
"status": 200,
"ok": true,
"payload": {
"requests": [
{
"pairingId": "base64url",
"state": "created",
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY",
"requesterSessionType": 1,
"requesterClientPlatform": "Android",
"payloadType": 1,
"shortCode": "4920709",
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
"createdAtMs": 1781441810538,
"expiresAtMs": 1781441990538,
"deliveredToHomeserver": true
}
]
}
}
```
### Ошибки
- `463 / PAIRING_REQUIRES_AUTH_SESSION`
### 5.5. `ApproveTrustedDeviceLogin`
Доступно для любой уже авторизованной доверенной сессии пользователя.
### Запрос
```json
{
"op": "ApproveTrustedDeviceLogin",
"requestId": "esp-approve-001",
"payload": {
"pairingId": "base64url",
"encryptedPayload": "BASE64_OR_OTHER_OPAQUE_PAYLOAD"
}
}
```
### Успешный ответ
```json
{
"op": "ApproveTrustedDeviceLogin",
"requestId": "esp-approve-001",
"status": 200,
"ok": true,
"payload": {
"pairingId": "base64url",
"state": "approved"
}
}
```
### Ошибки
- `400 / EMPTY_PAIRING_ID`
- `400 / EMPTY_ENCRYPTED_PAYLOAD`
- `404 / PAIRING_NOT_FOUND`
- `422 / PAIRING_OF_ANOTHER_USER`
- `422 / PAIRING_NOT_PENDING`
- `422 / PAIRING_EXPIRED`
- `463 / PAIRING_REQUIRES_AUTH_SESSION`
### 5.6. `RejectTrustedDeviceLogin`
Доступно для любой уже авторизованной доверенной сессии пользователя. Похоже на approve, но переводит заявку в `state=rejected`.
### 5.7. `GetTrustedDeviceLoginStatus`
Операция для нового устройства.
### Запрос
```json
{
"op": "GetTrustedDeviceLoginStatus",
"requestId": "esp-status-001",
"payload": {
"pairingId": "base64url"
}
}
```
### Успешный ответ после approve
```json
{
"op": "GetTrustedDeviceLoginStatus",
"requestId": "esp-status-001",
"status": 200,
"ok": true,
"payload": {
"pairingId": "base64url",
"state": "approved",
"shortCode": "4920709",
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
"payloadType": 1,
"encryptedPayload": "AQIDBA==",
"expiresAtMs": 1781441990538
}
}
```
### Возможные `state`
- `created`
- `approved`
- `rejected`
- `canceled`
- `expired`
### 5.8. `CancelTrustedDeviceLogin`
Операция для нового устройства, которое уже создало pairing-заявку и хочет принудительно снять ожидание до истечения TTL.
### Запрос
```json
{
"op": "CancelTrustedDeviceLogin",
"requestId": "esp-cancel-001",
"payload": {
"pairingId": "base64url",
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY"
}
}
```
### Успешный ответ
```json
{
"op": "CancelTrustedDeviceLogin",
"requestId": "esp-cancel-001",
"status": 200,
"ok": true,
"payload": {
"pairingId": "base64url",
"state": "canceled"
}
}
```
### Ошибки
- `400 / EMPTY_PAIRING_ID`
- `400 / EMPTY_REQUESTER_SESSION_KEY`
- `400 / BAD_REQUESTER_SESSION_KEY`
- `404 / PAIRING_NOT_FOUND`
- `422 / PAIRING_OF_ANOTHER_REQUESTER`
- `422 / PAIRING_NOT_PENDING`

View File

@ -19,6 +19,14 @@
| `CreateAuthSession` | `02_Authentication_API.md` | создание новой авторизованной сессии |
| `SessionChallenge` | `02_Authentication_API.md` | challenge для входа в существующую сессию |
| `SessionLogin` | `02_Authentication_API.md` | вход в существующую сессию |
| `GetTrustedDeviceLoginSettings` | `03_Session_Management_API.md` | чтение текущего режима входа через доверенное устройство |
| `UpsertTrustedDeviceLoginSettings` | `03_Session_Management_API.md` | включение/обновление pairing-настроек доверенной сессией |
| `StartTrustedDeviceLogin` | `03_Session_Management_API.md` | создание pairing-заявки для нового устройства |
| `ListTrustedDeviceLoginRequests` | `03_Session_Management_API.md` | список активных pairing-заявок для доверенной сессии |
| `ApproveTrustedDeviceLogin` | `03_Session_Management_API.md` | подтверждение pairing-заявки доверенной сессией |
| `RejectTrustedDeviceLogin` | `03_Session_Management_API.md` | отклонение pairing-заявки доверенной сессией |
| `CancelTrustedDeviceLogin` | `03_Session_Management_API.md` | отмена pairing-заявки со стороны нового ожидающего устройства |
| `GetTrustedDeviceLoginStatus` | `03_Session_Management_API.md` | чтение статуса и результата pairing-заявки |
| `ListSessions` | `03_Session_Management_API.md` | список активных сессий |
| `CloseActiveSession` | `03_Session_Management_API.md` | закрытие активной сессии |
| `AddBlock` | `04_Add_Block_to_Blockchain_API.md` | добавление блока в блокчейн |
@ -54,5 +62,6 @@
## Важные замечания
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.

View File

@ -1,8 +1,10 @@
# API для разработчиков: DM, push и сигналы звонков
Документ описывает WebSocket-операции для подписанных личных сообщений, WebPush и realtime-сигналов звонков.
Документ описывает публичные операции, связанные с личными сообщениями, WebPush и сигналами звонков.
Логика личных сообщений дополнительно описана в `Dev_Docs/Personal_Messages/README.md`; этот файл фиксирует именно публичные `op`, поля запросов и поля ответов.
Подробная логика DM и бинарного формата:
- `Dev_Docs/Personal_Messages/README.md`
## 1. `UpsertPushToken`
@ -40,11 +42,9 @@
}
```
---
## 2. `SendTestWebPush`
Требует авторизации. Если `login` передан, он должен совпадать с логином текущей сессии.
Требует авторизации.
### Запрос
@ -61,65 +61,18 @@
}
```
### Успешный ответ
## 3. `SendMessagePair` и `ReceiveOutcomingMessage`
```json
{
"op": "SendTestWebPush",
"requestId": "push-test-001",
"status": 200,
"ok": true,
"payload": {
"targetLogin": "alice",
"attemptedSessions": 1,
"sessionsWithPushConfig": 1,
"delivered": 1,
"failed": 0,
"sentAtMs": 1774700000123
}
}
```
`ReceiveOutcomingMessage` — алиас `SendMessagePair`.
---
### Назначение
## 3. `SendDirectMessage`
Передаёт пару signed DM-блоков:
Отправляет один подписанный DM-пакет.
- `incomingBlobB64` — блок `type=1` или `type=3`
- `outgoingBlobB64` — блок `type=2` или `type=4`
### Запрос
```json
{
"op": "SendDirectMessage",
"requestId": "dm-001",
"payload": {
"blobB64": "BASE64_SIGNED_DM_PACKET"
}
}
```
### Успешный ответ
```json
{
"op": "SendDirectMessage",
"requestId": "dm-001",
"status": 200,
"ok": true,
"payload": {
"messageId": "dm-1",
"deliveredWsSessions": 1,
"deliveredWebPushSessions": 0,
"sessionNotFound": false
}
}
```
---
## 4. `SendMessagePair` и `ReceiveOutcomingMessage`
`ReceiveOutcomingMessage` сейчас является алиасом `SendMessagePair` и использует тот же request/handler.
Для контентных сообщений `type=1/2` внутри base64 лежит бинарный формат `SHiNE_DM`.
### Запрос
@ -143,20 +96,31 @@
"status": 200,
"ok": true,
"payload": {
"baseKey": "base-key",
"incomingKey": "incoming-key",
"outgoingKey": "outgoing-key",
"baseKey": "from|to|time|nonce",
"incomingKey": "from|to|time|nonce|1",
"outgoingKey": "from|to|time|nonce|2",
"deliveredWsSessions": 1,
"deliveredWebPushSessions": 0
}
}
```
---
### Ошибки
## 5. `ReceiveIncomingMessage`
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
- `400 / BAD_CONTENT_FORMAT` — для контентного сообщения пришёл не `SHiNE_DM`
- `400 / ATTACHMENTS_DISABLED` — в `SHiNE_DM` пришёл `attachmentsCount != 0`
- `404 / USER_NOT_FOUND` — один из логинов не найден
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку
Принимает входящий подписанный DM-блок.
## 4. `ReceiveIncomingMessage`
Принимает только один входящий signed DM-блок.
### Назначение
Используется там, где нужно принять только incoming-вариант сообщения.
### Запрос
@ -170,28 +134,9 @@
}
```
### Успешный ответ
## 5. `AckSessionDelivery`
```json
{
"op": "ReceiveIncomingMessage",
"requestId": "dm-in-001",
"status": 200,
"ok": true,
"payload": {
"messageKey": "incoming-key",
"baseKey": "base-key",
"deliveredWsSessions": 1,
"deliveredWebPushSessions": 0
}
}
```
---
## 6. `AckSessionDelivery`
Требует авторизации. Подтверждает доставку сообщения в текущую сессию.
Требует авторизации. Подтверждает доставку в текущую сессию.
### Запрос
@ -200,107 +145,46 @@
"op": "AckSessionDelivery",
"requestId": "ack-001",
"payload": {
"messageKey": "incoming-key"
"messageKey": "from|to|time|nonce|1"
}
}
```
### Успешный ответ
## 6. Событие `SignedMessageArrived`
Сервер присылает его по WebSocket в активные сессии адресата.
### Payload события
```json
{
"op": "AckSessionDelivery",
"requestId": "ack-001",
"status": 200,
"ok": true,
"payload": {
"messageKey": "incoming-key"
}
"messageKey": "from|to|time|nonce|1",
"baseKey": "from|to|time|nonce",
"fromLogin": "alice",
"toLogin": "bob",
"targetLogin": "bob",
"messageType": 1,
"timeMs": 1774700000123,
"nonce": 123456789,
"blobB64": "BASE64_SIGNED_BLOCK",
"backlog": false
}
```
---
Если это новая ревизия того же письма, `messageKey` остаётся тем же, а `revisionTimeMs` меняется внутри бинарного блока.
## 7. `CallInviteBroadcast`
Требует авторизации. Отправляет приглашение к звонку на активные сессии пользователя `toLogin`.
### Запрос
```json
{
"op": "CallInviteBroadcast",
"requestId": "call-invite-001",
"payload": {
"toLogin": "bob",
"callId": "call-1",
"type": 100
}
}
```
### Успешный ответ
```json
{
"op": "CallInviteBroadcast",
"requestId": "call-invite-001",
"status": 200,
"ok": true,
"payload": {
"callId": "call-1",
"deliveredWsSessions": 1,
"deliveredFcmSessions": 0,
"deliveredWebPushSessions": 0
}
}
```
---
Требует авторизации. Шлёт приглашение к звонку в активные сессии `toLogin`.
## 8. `CallSignalToSession`
Требует авторизации. Отправляет сигнал звонка в конкретную сессию получателя.
Требует авторизации. Шлёт сигнал звонка в конкретную сессию.
### Запрос
## 9. Замечания
```json
{
"op": "CallSignalToSession",
"requestId": "call-signal-001",
"payload": {
"toLogin": "bob",
"targetSessionId": "SESSION_ID",
"callId": "call-1",
"type": 101,
"data": "{\"sdp\":\"...\"}"
}
}
```
### Успешный ответ
```json
{
"op": "CallSignalToSession",
"requestId": "call-signal-001",
"status": 200,
"ok": true,
"payload": {
"delivered": true
}
}
```
Если целевая сессия не найдена или доставка не удалась, сервер может вернуть `404`.
## Типовые ошибки
- `422 / NOT_AUTHENTICATED` — требуется авторизация.
- `400 / BAD_FIELDS` — не заполнены обязательные поля.
- `404 / USER_NOT_FOUND` — пользователь не найден.
- `404 / SESSION_NOT_FOUND` — сессия не найдена.
- `422 / BAD_SIGNATURE` — подпись DM не прошла проверку.
- `422 / BAD_DEVICE_KEY` — некорректный device key отправителя.
- `422 / BAD_TIME_WINDOW` — время подписанного сообщения вне допустимого окна.
- `422 / REPLAY` — повторное сообщение заблокировано.
- read-receipt `type=3/4` пока остаются в legacy-формате `SHiNE_dm2`
- контентные DM `type=1/2` используют `SHiNE_DM`
- сервер хранит только последнюю версию контентного сообщения по `messageKey`
- удаление сообщения реализуется новой ревизией с пустым телом и `attachmentsCount = 0`
- HTTP endpoints для DM-файлов сейчас отсутствуют

View File

@ -37,7 +37,7 @@
- `medium/2026-05-25_1106_shine_balance_wallet.md` - кошелёк и пополнение баланса сияния через блокчейн.
- `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений.
- `medium/2026-06-03_подключениеругих_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать.
- `medium/2026-06-02_сессионные_саб_серверы_в_pda.md` - несколько саб-серверов пользователя как типизированные сессии в PDA с версией записи.
- `medium/2026-06-02_сессионные_homeserver_в_pda.md` - несколько homeserver-ов пользователя как типизированные сессии в PDA с версией записи.
### DAO-запуск

View File

@ -1,4 +1,4 @@
# Сессионные саб-серверы в PDA пользователя
# Сессионные homeserver-ы в PDA пользователя
- Статус:
`future`
@ -10,15 +10,15 @@
после завершения первого этапа по пользовательским сессиям
- Основание:
Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних саб-серверов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли.
Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних homeserver-ов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли.
## Зачем нужна фича
У одного пользователя может быть несколько доверенных внутренних саб-серверов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели.
У одного пользователя может быть несколько доверенных внутренних homeserver-ов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели.
Это нужно, чтобы:
- хранить несколько саб-серверов у одного пользователя одновременно;
- хранить несколько homeserver-ов у одного пользователя одновременно;
- различать обычные клиентские сессии и серверные сессии по явному типу;
- дать расширяемый формат записи с версией;
- использовать единый подход для DM, звонков и внутренних команд между сессиями.
@ -35,18 +35,18 @@
Предварительные значения:
- тип `1` - обычная пользовательская сессия;
- тип `100` - саб-сервер пользователя;
- тип `100` - homeserver пользователя;
- версия `1` - первая рабочая версия формата записи сессии.
На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`.
Важно: саб-серверов у одного пользователя может быть несколько.
Важно: homeserver-ов у одного пользователя может быть несколько.
## Архитектурный принцип
Внутренний протокол взаимодействия должен оставаться транспортным.
То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки саб-сервера, а должен:
То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки homeserver-а, а должен:
- доставлять сообщения между сессиями;
- доставлять сигналы звонков между сессиями;
@ -60,7 +60,7 @@
- Вызов звонка уже рассылается по нескольким активным сессиям пользователя.
- Сигналы звонка уже адресуются конкретной сессии, а stop-сигналы дублируются на остальные сессии того же пользователя.
Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол саб-сервера".
Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол homeserver-а".
## Что нужно сделать при возврате к задаче
@ -77,7 +77,7 @@
- правила удаления и обновления записи;
- правила ротации `sessionPubKey`.
6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`.
7. Определить, какие внутренние сообщения саб-сервера останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
7. Определить, какие внутренние сообщения homeserver-а останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов.
9. После реализации обязательно обновить документацию.
@ -101,5 +101,5 @@
Продолжать после завершения первой части:
1. описать минимальный формат записи пользовательской сессии;
2. отдельно решить, живут ли саб-серверы в том же списке, что и обычные сессии;
2. отдельно решить, живут ли homeserver-ы в том же списке, что и обычные сессии;
3. затем уже проектировать операции регистрации, обновления и отключения таких сессий.

154
Dev_Docs/Keys/DERIVATION.md Normal file
View File

@ -0,0 +1,154 @@
# Деривация секрета и ключей SHiNE (формулы)
> **Статус: ИСТОЧНИК ИСТИНЫ (single source of truth) по конкретной деривации.**
> Этот файл описывает, как из пароля получается секрет и как из секрета выводятся
> все ключи (root, blockchain, device/Solana, homeserver) — формулами, байт-в-байт.
> Если в коде меняется деривация (формула секрета, параметры Argon2id, соль, формула
> ключа, разделитель `|`, набор/имена суффиксов, формат homeserver-ключа, связь
> dev-ключ ↔ Solana-адрес) — **в том же изменении обязательно править этот документ**.
> Роли и назначение ключей описаны отдельно в `Dev_Docs/Keys/README.md` (архитектура).
> Здесь — только механика. Документ намеренно краткий.
---
## 1. Секрет (masterSecret)
`masterSecret` — 32 байта. Два источника:
**А. Из пароля пользователя (основной путь, UI).**
```
login = trim(lowercase(login))
salt = SHA-256("shine-auth-v2|login=" + login + "|suffix=master.secret")[0..16) // первые 16 байт
material = utf8(login + "\n" + password)
masterSecret(32) = Argon2id(material, salt, t=2, m=65536 KiB, p=1, dkLen=32)
```
- Параметры Argon2id фиксированы: `t=2`, `m=65536` (64 МиБ), `p=1`, `dkLen=32`.
- Логин входит и в соль, и в начало `material` (склейка через `\n`).
- Пустой пароль **запрещён**: легаси-fallback без Argon2 удалён, `deriveMasterSecretFromPassword` бросает ошибку на пустом пароле, а форма регистрации в UI блокирует пустой пароль (`register-view.js`).
**Б. Случайный (прошивка ESP32, новый аккаунт без пароля).**
```
masterSecret(32) = 32 случайных байта (esp_random) // хранится на устройстве как base58
```
Дальше деривация ключей одинакова независимо от источника секрета.
---
## 2. Производные ключи
Все ключи выводятся из `masterSecret` по **одной формуле**, отличается только суффикс:
```
material = base64_std(masterSecret) + "|" + <суффикс>
seed(32) = SHA-256(material)
(pub, priv) = Ed25519_keypair_from_seed(seed)
```
- `base64_std` — стандартный base64 (не url-safe).
- Разделитель — символ `|`.
- Суффиксы значимы байт-в-байт (регистр и точки важны).
| Ключ | Суффикс | Назначение (кратко) |
|------|---------|---------------------|
| root | `root.key` | Личность. Подписывает unsigned-часть PDA-записи (`RootKeyBlock`). |
| blockchain | `bch.key` | Подписывает `LastBlockState` персонального блокчейна (`blockchain_public_key`). |
| device / **Solana** | `dev.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(devicePub)`. См. §3. |
| homeserver | `homeserver.key:<имя>` | Ключ homeserver-устройства, по одному на каждый homeserver (различитель — имя). См. §4. |
Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`.
---
## 3. Solana-ключ
Отдельного «солана-ключа» нет. На Solana работают два ключа:
- **`dev.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(devicePub)`.
Этим ключом оплачиваются и подписываются `create_user_pda` / `update_user_pda`.
Пополнять SOL нужно именно на этот адрес.
- **`root.key` — авторитет записи**, подписывает unsigned-часть PDA через Ed25519-инструкцию, но **не** является fee payer.
Соответствует формату PDA `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` §2.1
(«create/update оплачиваются с `device_key`», «root_key — не fee payer»).
Кратко про роли на Solana: `root.key` — это **главный (master) ключ**: им управляют PDA-записью
(`create/update`) и через это можно заменить все остальные ключи; `dev.key` — это **пополняемый
кошелёк и плательщик комиссий**. Полное описание ролей — `Dev_Docs/Keys/README.md`.
---
## 4. Ключи homeserver
У пользователя может быть несколько homeserver-ов. Каждый имеет **своё имя** и **свой приватный ключ**,
выведенный из секрета по той же формуле с именованным суффиксом:
```
suffix = "homeserver.key:" + <имя homeserver> // имя по умолчанию: "homeserver1"
material = base64_std(masterSecret) + "|" + suffix
seed(32) = SHA-256(material)
(pub, priv) = Ed25519_keypair_from_seed(seed)
```
Пример для двух homeserver-ов:
```
homeserver.key:home-a -> ключ A
homeserver.key:home-b -> ключ B
```
Публичный ключ homeserver-а публикуется в `SessionsBlock` пользовательской PDA как
`session_pub_key` с `session_type = 100`, имя — в `session_name` (формат PDA §13).
> Это переименование прежней схемы `subserver.key:<имя>``homeserver.key:<имя>`.
> Термин «саб-сервер» по проекту заменяется на «homeserver».
---
## 5. Где это в коде
### Деривация секрета и ключей (UI, каноническая)
- `shine-UI/js/services/crypto-utils.js`
- секрет из пароля: `makeArgon2Salt`, `deriveMasterSecretArgon2id`, `deriveMasterSecretFromPassword` (~129218);
- ключ из секрета: `deriveEd25519FromMasterSecret` (~220).
- `shine-UI/js/services/auth-service.js` — набор root/bch/dev из `masterSecret` (~732758).
- `shine-UI/server-ui/js/server-ui-shared.js` — те же root/bch/dev для серверного UI (~147160).
### Solana-ключ / адрес кошелька (UI)
- `shine-UI/js/pages/registration-payment-view.js``deriveUserWalletAddress`: адрес = `base58(devicePub)` (~113).
- `shine-UI/js/pages/topup-view.js``deviceWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.devicePair`.
Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `dev.key`, мимо `masterSecret`) удалён.
### Деривация ключей (прошивка ESP32)
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino`
- основной скетч ESP32-проекта `SHiNE`; `deriveKeysFromMasterSecret` (~782), `restoreDerivedKeysFromSecret` (~806), `deriveFreshSecretAndWallet` (~829);
- регистрация/подпись Solana: `registerHomeserverOnSolana` (~1182), `signMessageEd25519` (~1147).
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_ui/shine_homeserver_ui.ino`
- старый тестовый вариант; оставлен как legacy-скетч для сравнения и диагностики.
### Формат PDA (куда попадают ключи)
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
`RootKeyBlock` §6, `DeviceKeyBlock` §7, `blockchain_public_key` §9, `SessionsBlock`/`session_type=100` §13, оплата §2.1.
### Сервер (тестовый seed)
- `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) —
выводит ключи как `Ed25519(SHA-256(base64(SHA-256(password)) + suffix))`, **без** Argon2 и **без** разделителя `|`.
Это **не баг**, а точное повторение легаси-пути UI `derivePasswordSeed` (для пустого пароля), у которого тоже нет `|`.
С современным путём `masterSecret`-bundle (Argon2 + `base64(secret)|suffix`) он **не совпадает** by design.
Если потребуется, чтобы seed совпадал с реальными клиентами на Argon2 — нужно отдельно портировать
Argon2id+masterSecret в Java (на сервере Argon2 сейчас нет). Простое добавление `|` было бы **неверным**:
сломало бы совпадение с легаси-путём и всё равно не дало бы совпадения с Argon2-путём.
---
## 6. Правило синхронизации (обязательно)
1. Этот документ — источник истины по деривации секрета и ключей.
2. Любое изменение кода, затрагивающее формулу секрета, параметры Argon2id, соль, формулу ключа,
разделитель `|`, набор/имена суффиксов, формат homeserver-ключа или связь dev-ключ ↔ Solana-адрес —
**обязательно** отражать здесь в том же изменении.
3. Пункты, помеченные ⚠️, — это долг к устранению, а не норма.
4. Нельзя сознательно оставлять код и этот документ в рассинхроне без отдельной явной договорённости.

View File

@ -8,7 +8,7 @@
В SHiNE у пользователя есть несколько уровней ключей:
- `root key` - главный корневой ключ пользователя, он же основной Solana-ключ.
- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `device key`).
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
- `device key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
- `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере.
@ -28,9 +28,9 @@
- управление остальными ключами;
- подтверждение операций, которые должны иметь максимальный уровень доверия.
В текущей модели `root key` совпадает по смыслу с главным Solana-ключом пользователя.
`root key` — это **главный (master) ключ** в следующем смысле: зная `root key`, можно управлять пользовательской PDA-записью в Solana (`create_user_pda` / `update_user_pda`) и тем самым **заменить все остальные ключи** пользователя (device, blockchain, homeserver). Поэтому компрометация `root key` равносильна компрометации всей личности пользователя.
На `root key` могут храниться значимые средства, если пользователь сознательно выбирает такую модель. Для мелких текущих расходов предпочтительнее использовать `device key`.
Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `device key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3.
## `blockchain key`
@ -158,6 +158,7 @@ Self-message - это сообщение пользователя самому
## Связанные документы
- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`dev.key`/`homeserver.key:<имя>`, Solana-ключ, ссылки на код).
- `Dev_Docs/Personal_Messages/README.md` - текущая документация личных сообщений.
- `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна.
- `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств.

View File

@ -1,43 +0,0 @@
# Кошелёк: лимит/закрепление блокчейна Сияния
- статус: `pending`
## Кратко что сделано
- На экране `Кошелёк -> Блокчейн Сияния` добавлены 2 слоя данных:
- фактическое состояние цепочки на сервере (`кол-во блоков`, `размер`, `крайний блок`, `hash`, `размер крайнего блока`);
- закреплённое состояние в Solana PDA (`лимит`, `использовано`, `остаток`, `крайний блок`, `hash`).
- Добавлены действия:
- `Закрепить в Solana` — обновляет PDA до текущего состояния серверной цепочки;
- `Увеличить лимит` — увеличивает `paid_limit_bytes` в PDA с учётом цены из economy PDA.
- Если `rootKey`/`blockchainKey` не сохранены локально, экран запрашивает пароль, восстанавливает ключи через стандартную derivation-логику и предлагает сохранить их в зашифрованный контейнер.
## Что проверять вручную
1. Открыть `Кошелёк -> Блокчейн Сияния` под авторизованным пользователем.
2. Проверить, что в блоке "Фактическое состояние на сервере" отображаются:
- число блоков;
- размер цепочки;
- номер/хэш крайнего блока;
- размер крайнего блока.
3. Проверить, что в блоке "Закреплено в Solana" отображаются:
- лимит;
- израсходовано;
- остаток;
- номер/хэш крайнего закреплённого блока.
4. Нажать `Закрепить в Solana` и убедиться, что:
- приходит успешная транзакция;
- после обновления Solana-показатели подтягиваются до серверных (или максимально близко по актуальному состоянию).
5. Нажать `Увеличить лимит`, ввести значение кратное шагу, подтвердить списание и проверить:
- лимит увеличился;
- отображение цены/списания соответствует economy PDA.
6. Повторить пункты 4-5 в сценарии, когда `rootKey`/`blockchainKey` не сохранены, и проверить:
- появляется запрос пароля;
- после ввода пароля операции выполняются;
- предложение сохранить ключи показывается.
## Ожидаемый результат
- Экран корректно разделяет "фактическое состояние на сервере" и "закреплённое в Solana".
- Обе операции (`Закрепить в Solana`, `Увеличить лимит`) выполняются без ошибок при валидных данных.
- Восстановление ключей через пароль работает, а без нужных ключей операция не выполняется молча.

View File

@ -1,29 +0,0 @@
# Озвучивание ответов агента
## Что сделано
В локальный Telegram-бот-сервис агента-кодера добавлены персональные настройки озвучивания финальных ответов:
- `/voice_on` включает озвучивание для текущего Telegram-пользователя;
- `/voice_off` выключает озвучивание для текущего Telegram-пользователя;
- `/voice_status` показывает текущее состояние;
- если озвучивание включено, после текстового финального ответа сервис генерирует voice-файл через OpenAI TTS и отправляет его в Telegram;
- длинные ответы делятся на несколько фрагментов озвучки.
## Что проверять
1. Перезапустить `shine-agent-bot-coder`.
2. Отправить `/voice_status` и убедиться, что по умолчанию озвучивание выключено.
3. Отправить `/voice_on`.
4. Дать простую задачу агенту и проверить, что пришёл полный текстовый ответ и voice-файл с тем же ответом.
5. Отправить `/voice_off`.
6. Дать ещё одну простую задачу и проверить, что приходит только текст.
7. При возможности проверить второго whitelist-пользователя: его настройка должна быть независимой.
## Ожидаемый результат
Настройка хранится персонально по username и сохраняется после перезапуска сервиса. При включённой настройке Telegram получает текстовый ответ и дополнительное voice-сообщение с озвучкой. При выключенной настройке поведение остаётся прежним.
## Статус
pending

View File

@ -1,19 +0,0 @@
# Голосовая адаптация ответов Telegram-бота
## Краткое описание
Добавлены персональные настройки голосовых ответов и адаптации текста перед озвучкой. Если голосовые ответы включены, сервис перед TTS может отдельно прогонять финальный текст через OpenAI-модель и отправлять более короткую голосовую версию в исходный чат, личный чат пользователя и общий чат `@shine_writing`, если эти чаты доступны и отличаются.
## Что проверить
- Команды `/voice_on`, `/voice_off`, `/voice_status` для конкретного пользователя.
- Команды `/voice_rewrite_on`, `/voice_rewrite_off`, `/voice_rewrite_status` для конкретного пользователя.
- Команда `/status` показывает очередь, голосовые ответы и адаптацию текста перед озвучкой.
- При включённых голосовых ответах после задачи приходит текстовый ответ и voice-ответ.
- При включённой адаптации voice-ответ короче и без длинных технических строк.
- При задаче из личного чата voice дополнительно появляется в общем чате `@shine_writing`.
- При задаче из общего чата voice дополнительно появляется в личном чате пользователя, если сервис уже знает его личный chat_id.
## Ожидаемый результат
Текстовый ответ остаётся полным. Голосовая версия приходит отдельно, звучит короче и естественнее, а персональные настройки одного пользователя не меняют поведение других пользователей.
## Статус
pending

View File

@ -1,24 +0,0 @@
# Эксперимент Understand Anything
## Краткое описание
Добавлена изолированная лаборатория для проверки `Lum1104/Understand-Anything` без подключения к сборке, деплою и рабочему коду SHiNE.
## Что проверять
- Установить Node.js 22+ и pnpm 10+.
- Запустить `./tools/understand-anything-lab/install_codex_skills.sh`.
- Перезапустить Codex-сессию.
- Выполнить `/understand --language ru` в корне проекта.
- После генерации выполнить `/understand-dashboard` и проверить, что граф открывается и помогает ориентироваться по серверным, UI, Solana и агентским папкам.
## Ожидаемый результат
- В проекте появляется локальная папка `.understand-anything/` с графом знаний.
- Dashboard открывается и показывает интерактивный граф проекта.
- Основные процессы сборки и деплоя SHiNE не меняются.
## Статус
`pending`

View File

@ -1,24 +0,0 @@
# Центр задач Telegram-агента
## Краткое описание
Добавлена первая версия центра задач и предложений внутри `SHiNE-agent-bot-coder`.
Бот хранит задачи и предложения в JSON-файле данных сервиса, умеет показывать список через `/tasks`, создавать задачи для игроков по фразе Айдара, принимать предложения игроков по префиксу `предложение:`, менять статусы и добавлять короткие напоминания после ответов.
## Что проверять
- Айдар пишет `/tasks` и видит текущий список задач и предложений без уже закрытого предложения от Димы.
- Айдар пишет `поставь задачу Милане: проверить описание SHiNE` и задача появляется в списке Миланы.
- Милана пишет `/tasks` и видит назначенную задачу.
- Игрок пишет `предложение: ...`, после чего предложение появляется у Айдара.
- Айдар меняет статус фразами вида `одобрить TC-XXXX`, `доработать TC-XXXX`, `закрыть TC-XXXX`, где `TC-XXXX` - ID существующей задачи или предложения.
- После обычного ответа бота Айдару или игроку появляется короткое напоминание, если у пользователя есть активные задачи.
## Ожидаемый результат
Задачи и предложения сохраняются между перезапусками сервиса, статусы меняются корректно, напоминания не мешают основному ответу Codex.
## Статус
pending

View File

@ -1,31 +0,0 @@
# Рестарты и voice-настройки Telegram-агента
## Краткое описание
Добавлена первая версия безопасного рестарта Telegram-агента:
- `/restart` и `/restart_service` ставят отложенный рестарт после текущей задачи и до взятия следующей;
- `/restart_hard`, `/restart_now`, `/restart_force` выполняют жёсткий рестарт сразу;
- команды рестарта доступны только Айдару;
- voice-ответы включены по умолчанию для новых пользователей;
- адаптация текста перед озвучкой стала ближе к исходному ответу и не должна менять смысл;
- скрыты отдельные команды статуса voice-функций из справки, состояние показывается через `/status`.
## Что проверить
1. Отправить `/restart` во время активной задачи игрока или Айдара.
2. Убедиться, что активная задача завершается, после чего сервис перезапускается до следующей задачи.
3. Отправить `/restart_hard` и убедиться, что сервис перезапускается сразу.
4. Проверить, что игрок не может выполнить команды рестарта.
5. Проверить `/status`: он показывает очередь и состояния голосовых функций.
6. Проверить нового пользователя: voice-ответы должны быть включены по умолчанию.
7. Проверить текстовый запрос пользователя с включённым voice: после текстового ответа должен прийти voice-файл.
8. Проверить, что адаптированная озвучка не превращается в другой ответ, а только убирает длинные технические строки.
## Ожидаемый результат
Сервис можно обновлять без потери текущей задачи через отложенный рестарт. Жёсткий рестарт остаётся аварийной командой Айдара. Voice-ответы работают для текстовых и голосовых запросов, а голосовая версия остаётся близкой к текстовой.
## Статус
pending

View File

@ -1,26 +0,0 @@
# Кнопки вкладки «Каналы»
## Что сделано
Доработана верхняя панель вкладки «Каналы»:
- при открытии нижней кнопкой «Каналы» показывается режим «Все каналы»;
- в режиме «Все каналы» справа доступны кнопка «Мои каналы» и иконка поиска канала;
- в режиме «Мои каналы» доступен переход обратно во «Все каналы», а справа показывается плюсик создания канала.
## Что проверить
1. Открыть вкладку «Каналы» через нижнюю навигацию.
2. Убедиться, что открыт режим «Все каналы», а плюсик создания канала не отображается.
3. Нажать иконку поиска в режиме «Все каналы».
4. Убедиться, что открывается текущий сценарий поиска каналов.
5. Нажать «Мои каналы».
6. Убедиться, что справа появился плюсик создания канала.
7. Нажать «Все каналы» или стрелку назад и проверить возврат к режиму «Все каналы».
## Ожидаемый результат
Кнопки верхней панели соответствуют активному режиму: поиск в «Все каналы», создание только в «Мои каналы».
## Статус
pending

View File

@ -1,13 +0,0 @@
# Длинные voice/audio в Telegram-боте агента
- краткое описание фичи:
Бот теперь умеет обрабатывать длинные voice/audio аккуратнее: учитывает лимит Telegram Bot API на скачивание слишком больших файлов, поддерживает альтернативный `TELEGRAM_API_BASE_URL` для локального `telegram-bot-api`, локально пережимает длинное аудио через `ffmpeg`, режет на куски и отправляет их в OpenAI transcription последовательно.
- что именно проверять:
1. Короткий `voice` по-прежнему распознаётся без заметной задержки.
2. Длинный `audio/voice`, который помещается в скачивание Telegram, успешно пережимается, режется на части и даёт цельную расшифровку.
3. Очень большой файл через обычный `https://api.telegram.org` даёт понятное сообщение про лимит Telegram.
4. После переключения на локальный `telegram-bot-api` такой же большой файл начинает скачиваться и распознаваться.
- ожидаемый результат:
Бот не падает на длинных аудио, даёт либо расшифровку, либо понятное объяснение, какой именно лимит мешает и что нужно включить.
- статус:
pending

View File

@ -1,14 +0,0 @@
# Диагностика больших voice/audio в Telegram-боте
- краткое описание фичи:
- Бот при большом voice/audio больше не отказывается заранее по метаданным Telegram. Теперь он сначала сообщает, что пробует скачать файл, затем отдельно сообщает об успешном скачивании и только после этого переходит к подготовке аудио и распознаванию через OpenAI.
- что именно проверять:
- Отправить в бота большой `voice` или `audio`, который раньше попадал под ранний отказ.
- Проверить, что сначала приходит сообщение о попытке скачать большой файл.
- Проверить два сценария:
- скачивание удалось: бот пишет об успешной загрузке и продолжает распознавание;
- скачивание не удалось: бот пишет именно о неудачном скачивании из Telegram, без ложной привязки к ошибке OpenAI.
- ожидаемый результат:
- Пользователь видит понятную поэтапную диагностику: попытка скачивания, результат скачивания и только потом следующий этап обработки.
- статус:
- pending

View File

@ -1,24 +0,0 @@
# Перенос server UI в shine-UI
- краткое описание фичи:
Веб-панель управления серверной Solana PDA перенесена в `shine-UI/` как отдельные страницы.
Новая точка входа: `shine-UI/server-ui.html`.
Общая логика работы с PDA вынесена в единый модуль `shine-UI/js/services/shine-user-pda-service.js`.
- что именно проверять:
1. Открытие `shine-UI/server-ui.html` и переходы на страницы создания и обновления PDA.
2. Генерацию ключей из логина и пароля на странице создания.
3. Ручной ввод base58-ключей и регистрацию серверного PDA.
4. Загрузку существующей серверной PDA на странице обновления.
5. Обновление `server_address` и `sync_servers` только по `root + device` без blockchain-ключа.
6. Корректное чтение нового формата `ServerProfileBlock` через общий PDA-модуль.
7. То, что актуальной точкой входа остаётся `shine-UI/server-ui.html`.
- ожидаемый результат:
1. Новые страницы открываются без JS-ошибок.
2. Создание серверной PDA проходит через общий модуль и пишет актуальный формат.
3. Обновление серверной PDA переиспользует существующую подпись LastBlockState и не требует blockchain-ключ.
4. Клиентский UI не ломается после перевода общего PDA-слоя на новый формат.
- статус:
pending

View File

@ -1,20 +0,0 @@
# Кнопка настройки сервера и DEVNET topup
- краткое описание фичи:
На экране `entry-settings-view` добавлена кнопка `Настроить свой сервер`, открывающая `server-ui.html` в новой вкладке.
На страницах серверного UI добавлена кнопка открытия `devnet-topup-view` в новой вкладке с автоматической передачей `wallet` из device-адреса.
- что именно проверять:
1. На странице настроек входа есть кнопка `Настроить свой сервер`.
2. Кнопка открывает `shine-UI/server-ui.html` в новой вкладке.
3. На страницах `create-server-pda.html` и `update-server-pda.html` есть кнопка `Открыть пополнение DEVNET`.
4. Если device public key заполнен, новая вкладка открывает `devnet-topup-view?wallet=...` с правильным адресом.
5. Если device-адрес не введён, серверный UI показывает понятную ошибку и не открывает пустую ссылку.
- ожидаемый результат:
1. Переход в серверный UI с клиентской страницы настроек работает.
2. Пополнение devnet из серверного UI открывается сразу на нужный адрес.
3. Основной клиентский UI и серверные страницы не получают JS-ошибок при загрузке.
- статус:
pending

View File

@ -1,15 +0,0 @@
# Фикс DEVNET topup и автоподстановки пароля
- статус: pending
- кратко: исправлена ширина экрана `devnet-topup-view` после успешного пополнения и отключена нежелательная автоподстановка пароля в server UI и на экранах входа/регистрации.
## Что проверять
- Открыть страницу пополнения DEVNET, выполнить пополнение и убедиться, что после появления `Signature` экран не расширяется по ширине.
- Проверить, что кнопки на странице пополнения остаются аккуратными и не разъезжаются.
- Открыть `server-ui/update-server-pda.html`, загрузить PDA и убедиться, что поле пароля остаётся пустым.
- Проверить обычные экраны входа и регистрации: поле пароля не должно самопроизвольно заполняться длинной строкой.
## Ожидаемый результат
- Длинная transaction signature переносится по строкам внутри прежней ширины экрана.
- Кнопки сохраняют компактный mobile-first layout.
- Поля пароля пустые, пока пользователь сам ничего не вводил.

View File

@ -1,17 +0,0 @@
# Диагностика ключей server PDA и баланс device
- статус: pending
- кратко: на странице обновления server PDA добавлена сверка ожидаемых ключей с уже загруженной PDA, предупреждение о неверном пароле, кнопка показа баланса device-аккаунта и уточнение, что create/update оплачиваются с deviceKey.
## Что проверять
- На `update-server-pda.html` загрузить существующую PDA и убедиться, что видны ожидаемые `root/blockchain/device` public key.
- Ввести правильный пароль и нажать `Сгенерировать`: должно появиться сообщение, что ключи совпадают.
- Ввести неверный пароль и нажать `Сгенерировать`: должно появиться сообщение, что ключи не совпали и пароль, вероятно, неверный.
- На `create-server-pda.html` и `update-server-pda.html` нажать `Показать / обновить баланс device` и убедиться, что баланс читается по текущему `devPub`.
- Повторить `update_user_pda` после увеличения `heap frame` и проверить, ушла ли ошибка `memory allocation failed`.
## Ожидаемый результат
- Пользователь видит, какие именно public key должны получиться для загруженной PDA.
- Ошибка неправильного пароля выявляется до отправки транзакции.
- Баланс device-кошелька читается прямо со страницы.
- Если проблема `OOM` была только в размере heap frame/compute budget клиента, `update_user_pda` начинает проходить.

View File

@ -1,15 +0,0 @@
# Lazy-import Solana PDA: актуальный формат
- Краткое описание:
Серверный Java lazy-import пользователя из `shine_users` обновлён под актуальный формат `user_pda`. Убран RPC-фильтр по размеру PDA, добавлен разбор нового `ServerProfileBlock` (`block_type = 30`) без сохранения server-only полей в `solana_users`.
- Что проверять:
1. Взять логин пользователя, который существует в Solana PDA, но отсутствует в локальной таблице `solana_users`.
2. Выполнить вход этим логином через сервер.
3. Убедиться, что lazy-import подтянул пользователя из Solana.
4. Убедиться, что запись в `solana_users` создана с полями `login`, `blockchain_name`, `solana_key`, `blockchain_key`, `device_key`.
5. Убедиться, что отсутствие/наличие server-полей в PDA не ломает импорт.
- Ожидаемый результат:
1. Пользователь успешно находится и импортируется из Solana PDA независимо от фактического размера PDA.
2. Новый `ServerProfileBlock` не ломает парсер.
3. В БД не появляются лишние server-only поля.
- Статус: `pending`

View File

@ -1,33 +0,0 @@
# Pure Rust `shine_users` и `shine_login_guard`
Статус: `pending`
## Что сделано
- `shine_login_guard` переписан без Anchor на чистый Rust/Solana SDK.
- `shine_users` переписан без Anchor на чистый Rust/Solana SDK.
- Для `shine_users` введён новый instruction ABI без Anchor discriminator'ов.
- Для `shine_users` используются новые seed'ы:
- `user_login=` для `user_pda`
- `shine_users_economy_config` для economy PDA
- Формат блоков PDA синхронизирован:
- `SessionsBlock = 50`
- `TrustedStateBlock = 70`
- UI JS-модуль и Java lazy-import обновлены под новые seeds/ABI/коды блоков.
## Что проверить руками
1. В обычном UI выполнить регистрацию нового пользователя в Solana.
2. Проверить, что после регистрации читается новая `user_pda`.
3. В server UI выполнить создание server PDA.
4. В server UI выполнить update server PDA.
5. Проверить, что после update растёт `record_number`.
6. Проверить, что lazy-import на сервере читает новый формат PDA без ошибок.
7. Проверить, что старые Anchor discriminator'ы больше нигде не требуются.
## Ожидаемый результат
- Регистрация и update работают на новых чисто-rust программах.
- UI не использует старый Anchor ABI.
- Серверный Java parser читает новый формат PDA.
- Ошибок `out of memory` и anchor-specific падений больше нет.

View File

@ -1,25 +0,0 @@
# ESP32 Argon2/UI совместимость и экран результата
- краткое описание фичи:
выравнивание derivation на `ESP32` с текущим `UI` по нормализации логина, совместимости `master secret`/`root.key`/`bch.key`/`dev.key`, а также правки экрана результата и progress bar.
- что именно проверять:
1. На `UI` и `ESP32` ввести один и тот же логин в разном регистре, например `Anya24`, и один и тот же непустой пароль.
2. Убедиться, что после нормализации логина на `ESP32` и `UI` получаются одинаковые:
`master secret`, `root`, `blockchain`, `device` в `Base58`.
3. Проверить режим пустого пароля:
`UI` и `ESP32` должны выдать одинаковые ключи в legacy-режиме.
4. Проверить, что пустой логин на `ESP32` не запускает расчёт и показывает сообщение об ошибке.
5. Проверить progress bar:
при непустом пароле полоса должна быть видна и двигаться.
6. Проверить экран результата:
сначала `Login`, затем `Password`, затем `Master secret` и ключи;
свайп вверх/вниз должен прокручивать длинный результат без артефактов.
- ожидаемый результат:
`ESP32` и `UI` считают одинаковый `master secret` и одинаковые ключи для одинаковых входных данных;
progress bar виден;
экран результата читаемый и корректно прокручивается.
- статус:
pending

View File

@ -1,17 +0,0 @@
## Краткое описание
В `SHiNE-agent-bot-coder` для личных чатов добавлен режим одного редактируемого статусного сообщения. Бот принимает запрос, обновляет это сообщение по этапам обработки и в конце превращает его в финальный текстовый ответ. При длинном ответе допускается ещё одно дополнительное текстовое сообщение с продолжением. Голосовой ответ остаётся отдельным сообщением.
## Что проверять
1. Отправить в личный чат короткий текстовый запрос и убедиться, что бот не шлёт цепочку промежуточных сообщений, а редактирует одно сообщение до финального ответа.
2. Отправить в личный чат `voice` или `audio` и убедиться, что в том же сообщении последовательно видны этапы распознавания и выполнения.
3. Проверить длинный ответ, который не помещается в один Telegram message: должно получиться не больше двух текстовых сообщений.
4. Проверить, что `voice`-ответ приходит отдельным новым сообщением после текстового.
5. Проверить, что в `@shine_writing` по-прежнему логируются только итоговые `вопрос -> ответ`, без промежуточных статусов.
## Ожидаемый результат
- В личке основная переписка стала чище: промежуточные этапы живут в одном редактируемом сообщении.
- При длинном ответе бот не разбрасывает ответ на много сообщений.
- Канал `@shine_writing` работает по старой схеме без лишнего шума.
## Статус
`pending`

View File

@ -1,24 +0,0 @@
## Краткое описание
В локальный Telegram-бот `SHiNE-agent-bot-coder` добавлена команда `/settings`, которая сразу показывает текущие персональные настройки пользователя и список доступных команд для их изменения. В `/help` оставлена только ссылка на `/settings` без перечисления самих команд настроек. Также добавлен переключатель режима ответа в личке: один редактируемый статус или отдельные сообщения по этапам.
## Что проверять
1. Отправить `/help` и убедиться, что в справке есть `/settings`, но нет списка команд `/voice_*` и `/single_message_*`.
2. Отправить `/settings` и проверить, что бот показывает текущие значения:
- озвучивание финальных ответов;
- адаптацию текста перед озвучкой;
- режим одного редактируемого сообщения в личке.
3. По очереди переключить:
- `/voice_on` и `/voice_off`;
- `/voice_rewrite_on` и `/voice_rewrite_off`;
- `/single_message_on` и `/single_message_off`.
4. После каждого переключения снова вызвать `/settings` и убедиться, что статус изменился и сохранился.
5. При `/single_message_on` отправить обычный запрос в личку и проверить, что бот ведёт его через одно редактируемое сообщение.
6. При `/single_message_off` отправить обычный запрос в личку и проверить, что бот снова шлёт отдельные сообщения по этапам и отдельный финальный ответ.
## Ожидаемый результат
- `/settings` стал основной точкой входа для пользовательских настроек.
- `/help` стал короче и не дублирует список команд настроек.
- Режим ответа в личке реально переключается персонально для пользователя и сохраняется после перезапуска сервиса.
## Статус
`pending`

View File

@ -1,210 +0,0 @@
# Shine Payments: e2e после переписи без Anchor и добавления Q3
## Краткое описание
Нужно вручную и через вспомогательные CLI-проверки подтвердить, что программа `shine_payments` после:
- переписи на чистый `solana_program`;
- отказа от `programs/common`;
- добавления очереди `Q3`;
- обновления HTML UI;
корректно работает на devnet с новым `program id`.
Отличие от финального боевого сценария:
- вместо DAO-механики используется обычный кошелёк `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P`, которому даны права DAO на изменение коэффициента и выдачу лимитов менеджеру.
## Что именно проверять
### 1. Подготовка окружения
Проверить и зафиксировать:
- новый keypair программы `shine_payments`;
- новый `program id`;
- обновление `program id` в HTML UI и связанных настройках;
- наличие deploy authority, которой можно закрыть старый buffer/programdata, если это технически доступно;
- адреса тестовых кошельков:
- DAO/базовый кошелёк;
- менеджер;
- покупатель 1;
- покупатель 2;
- получатели выплат.
### 2. Очистка/смена старой программы
Проверить один из сценариев:
- если возможно, закрыть старый `program buffer/programdata` текущими ключами;
- если закрытие невозможно или нецелесообразно, зафиксировать это и продолжить с новым `program id`.
Отдельно проверить, что старые PDA предыдущей версии не используются новой программой.
### 3. Деплой и init новой программы
Проверить:
- `cargo build-sbf` проходит;
- новая программа деплоится на devnet;
- `init` выполняется один раз на пустых PDA;
- после `init` читаются:
- `config`;
- `coef_limit`;
- `queues`;
- `inflow_vault`.
Сразу после `init` запросить состояние очередей и зафиксировать, что:
- `Q1`, `Q2`, `Q3` пустые;
- `tickets_total = 0`;
- `tickets_paid = 0`;
- все суммы равны `0`.
### 4. Проверка покупки билета
На минимальных суммах проверить:
1. покупку через `buy_ticket_usd`;
2. покупку через `buy_ticket_sol`;
3. при необходимости ещё один вызов базового `buy_ticket`.
После каждой покупки:
- запросить состояние `Q1`;
- убедиться, что создался следующий ticket;
- проверить рост:
- `q1_tickets_total`;
- `q1_sum_total_usd_cents`;
- убедиться, что деньги покупки ушли в `dao_wallet`, а не в `inflow_vault`.
### 5. Проверка DAO-управления
Проверить:
1. изменение коэффициента через `update_coef_limit`;
2. повторный запрос `coef_limit` и подтверждение нового значения;
3. выдачу менеджеру прав через `grant_manager_limits`:
- отдельно под `Q1`;
- отдельно под `Q2`;
- отдельно под `Q3`.
После выдачи лимитов:
- считать `manager_allowance_pda`;
- убедиться, что лимиты записаны отдельно по трём очередям.
### 6. Проверка manager_add_ticket
На минимальных суммах создать менеджерские тикеты:
1. один ticket в `Q1`;
2. один ticket в `Q2`;
3. один ticket в `Q3`.
После каждого добавления:
- запросить состояние очередей;
- проверить рост счётчиков и сумм именно у нужной очереди;
- проверить уменьшение соответствующего manager allowance.
### 7. Проверка приоритета очередей
Подтвердить очередность `step_payout`:
1. сначала выплачивается `Q1`;
2. затем `Q2`;
3. затем `Q3`.
Для этого:
- между шагами регулярно читать `queues`;
- фиксировать, какой именно ticket был следующим к выплате;
- убедиться, что при наличии pending в `Q1` программа не уходит в `Q2` или `Q3`.
### 8. Проверка частичных выплат
Перед выплатами пополнять `inflow_vault` только минимально достаточными суммами.
Нужно проверить:
1. частичную серию выплат, когда часть тикетов ещё остаётся pending;
2. дополнительную покупку билета в промежутке между выплатами;
3. повторную проверку приоритета после появления нового билета в `Q1`.
После каждого `step_payout`:
- запрашивать состояние очередей;
- проверять:
- рост `tickets_paid`;
- рост `sum_paid_usd_cents`;
- `is_paid = true` у погашенного ticket;
- правильный DAO multiplier:
- `Q1 -> 1x`;
- `Q2 -> 2x`;
- `Q3 -> 3x`.
### 9. Проверка финального добора
После частичных выплат:
- купить ещё один билет;
- допополнить `inflow_vault`;
- выполнить оставшиеся `step_payout` до полного погашения всех трёх очередей.
В конце:
- все pending ticket должны отсутствовать;
- все суммы paid должны совпасть с total по каждой очереди;
- если вызвать `step_payout` на пустых очередях, доступный остаток `inflow_vault` должен уйти в `dao_wallet`.
### 10. Финальный возврат лампортов
После завершения теста вернуть все доступные остатки, которые можно вернуть текущими полномочиями, на базовый кошелёк:
- `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P`
Отдельно зафиксировать:
- что именно удалось вернуть;
- что именно нельзя вернуть без специальной инструкции закрытия или без deploy authority.
## Ожидаемый результат
- `buy_ticket_usd` и `buy_ticket_sol` создают ticket без ошибок чтения state;
- `Q3` работает наравне с `Q2`, но с третьим приоритетом;
- DAO может менять коэффициент и выдавать лимиты;
- менеджер может создавать билеты во все три очереди;
- `step_payout` соблюдает порядок `Q1 -> Q2 -> Q3`;
- DAO-множитель на выплатах равен `1x/2x/3x` для `Q1/Q2/Q3`;
- HTML UI и on-chain программа используют один и тот же актуальный `program id`;
- остатки средств после теста по максимуму возвращены на базовый DAO-кошелёк.
## Статус
- `done`
## Итог выполнения
- новый `shine_payments` задеплоен в devnet с `program id`:
- `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`
- старый `shine_payments`:
- `m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR`
- закрыт, лампорты возвращены на базовый DAO-кошелёк
- HTML UI переведён на новый `program id`
- подтверждены:
- `init`
- `buy_ticket_usd`
- `buy_ticket_sol`
- `grant_manager_limits`
- `manager_add_ticket` для `Q1/Q2/Q3`
- `change_ticket_recipient`
- `update_coef_limit`
- `step_payout` по порядку `Q1 -> Q2 -> Q3`
- повторный возврат приоритета в `Q1` после новой покупки
- итоговые агрегаты очередей:
- `Q1 total=4, paid=4, sum_total=780, sum_paid=780`
- `Q2 total=1, paid=1, sum_total=60, sum_paid=60`
- `Q3 total=1, paid=1, sum_total=70, sum_paid=70`
- временные тестовые кошельки собраны обратно в базовый DAO-кошелёк
- в `inflow_vault` остался только rent-минимум PDA

View File

@ -1,30 +0,0 @@
# Клиентская Solana-регистрация после ухода от Anchor
## Краткое описание
Исправлен рассинхрон обычного клиентского UI с no-Anchor ABI программ:
- `shine_login_guard`
- `shine_users`
Исправлены клиентские вызовы:
1. Solana-предпроверка логина в обычном UI.
2. `init_users_economy_config` в обычном UI.
## Что проверять
1. На странице регистрации проверка свободного логина не выдаёт `InvalidInstructionData`.
2. Для свободного обычного логина отображается корректный статус без fallback-предупреждения про недоступную Solana-предпроверку.
3. Регистрация пользователя через обычный UI проходит до конца.
4. Страница `Solana: init регистрации` в обычном UI отправляет корректную транзакцию и не падает из-за старого Anchor discriminator.
## Ожидаемый результат
1. `shine_login_guard` принимает клиентский precheck.
2. `init_users_economy_config` из обычного UI совместим с текущей программой `shine_users`.
3. Обычный клиентский UI ведёт себя так же, как серверный UI, там где используется общий no-Anchor путь.
## Статус
- `pending`

View File

@ -1,26 +0,0 @@
# ESP32 UI-прототип сабсервера SHiNE
- краткое описание фичи:
для `Waveshare ESP32-S3-Touch-AMOLED-2.16` добавлен новый интерактивный UI-скетч сабсервера `SHiNE` с хранением данных в `NVS`, настройками `Wi-Fi`, настройками серверов, кошельком, экраном `QR/URI`, живой Solana-регистрацией и экраном входящих запросов. Логика PIN в коде сохранена, но вход по PIN во временной сборке отключён, чтобы не блокировать проверку остальных экранов. В текущей версии `Wi-Fi` подключается реально, адреса `API/RPC/WS` проверяются реально, баланс кошелька читается из `Solana RPC`, а регистрация отправляет `create_user_pda` в `shine_users`.
- что именно проверять:
1. Прошить режим `subserver-ui` и дождаться открытия главного экрана без PIN.
2. Проверить, что текст в заголовках, кнопках и статусах отображается читаемо; в текущей временной сборке допускается ASCII-транслитерация русского текста.
3. Открыть `Настройки` и убедиться, что показывается пометка о временно отключённом входе по PIN.
4. Открыть `Подключение -> Wi-Fi`, ввести `SSID` и пароль, нажать `Проверить`, дождаться реального подключения, затем перезагрузить устройство и проверить, что значения сохранились.
5. Открыть `Подключение -> Серверы`, проверить или изменить `API/RPC/WS`, нажать `Проверить` и убедиться, что показываются реальные статусы доступности, затем перезагрузить устройство и проверить сохранение значений.
6. Открыть `Аккаунт`, ввести логин, имя сабсервера и нажать `Сгенерировать`; проверить, что появились секрет и адрес кошелька, а после перезагрузки они не исчезают.
7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка.
8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению.
9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной.
10. Выполнить регистрацию и убедиться, что статус меняется на `Сабсервер активен`, онлайн-статус становится активным, а на экране появляются краткие отпечатки `PDA/TX`.
11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана и соответствует `device`-адресу устройства.
12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус.
13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте.
14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются.
- ожидаемый результат:
новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации сабсервера.
- статус:
pending

View File

@ -1,13 +0,0 @@
# ESP32 авто-прошивка shine_subserver_ui
- краткое описание фичи:
добавлен исполняемый скрипт `flash_shine_subserver_ui.sh`, который автоматически ищет USB-порт `ESP32` и запускает заливку прошивки `shine_subserver_ui` без ручного указания `PORT`.
- что именно проверять:
1. Подключить плату `ESP32` по USB.
2. Перейти в папку `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/`.
3. Запустить `./flash_shine_subserver_ui.sh`.
4. Убедиться, что скрипт сам показывает найденный порт и успешно запускает compile/upload.
- ожидаемый результат:
скрипт без ручного ввода порта находит `ESP32`, печатает найденный `/dev/ttyACM*` или `/dev/ttyUSB*` и заливает `shine_subserver_ui`.
- статус:
pending

View File

@ -1,14 +0,0 @@
# ESP32 тест рендера текста
- краткое описание фичи:
добавлен отдельный диагностический скетч `text_render_test`, который показывает один экран с несколькими вариантами вывода текста: встроенный шрифт `Arduino_GFX`, `U8g2` ASCII, `U8g2` кириллица и кнопки с подписями. Скрипт нужен для изоляции проблемы, когда на экране видны только цветные кнопки и блоки, но не видно ни одной буквы.
- что именно проверять:
1. Прошить режим `text-test`.
2. Проверить, виден ли заголовок `TEXT TEST 123`.
3. Проверить, видны ли строки `A`, `B`, `C`, `D`.
4. Проверить, видны ли подписи на трёх нижних кнопках: `BTN 1`, `abc123`, `Русский`.
5. Сравнить, какой из способов вывода реально отображается, а какой нет.
- ожидаемый результат:
хотя бы один вариант вывода текста становится видим на экране, что позволяет локализовать проблему до конкретного шрифта или способа рендера.
- статус:
pending

View File

@ -1,12 +0,0 @@
# ESP32 PIN-клавиатура: подписи кнопок
- краткое описание фичи:
в UI-скетче `shine_subserver_ui` изменена отрисовка подписей кнопок. Вместо малого шрифта теперь используется более стабильный шрифт с явным центрированием текста внутри кнопок, чтобы на экране ввода PIN и других экранах не пропадали цифры и надписи.
- что именно проверять:
1. Включить устройство и дождаться экрана ввода PIN.
2. Убедиться, что на всех серых кнопках видны цифры `0-9`, `Отмена` и `OK`.
3. Открыть другие экраны с кнопками (`Главный экран`, `Wi-Fi`, `Серверы`, `Настройки`) и убедиться, что подписи отображаются и не уезжают за границы кнопок.
- ожидаемый результат:
подписи кнопок стабильно видны сразу после старта, текст визуально центрирован, пустых серых кнопок без цифр и названий нет.
- статус:
pending

View File

@ -1,13 +0,0 @@
# ESP32 папка тестовых скетчей
- краткое описание фичи:
добавлена отдельная папка `test_sketches/` с изолированными диагностическими скетчами для экрана `ESP32-S3-Touch-AMOLED-2.16`: тест рендера текста через `Arduino_GFX`, тест геометрии кнопок и минимальный тест `LVGL`.
- что именно проверять:
1. Запустить `./burn.sh gfx-text-test` и убедиться, что прошивается тест текста из новой папки.
2. Запустить `./burn.sh gfx-layout-test` и проверить нижние ряды кнопок.
3. Запустить `./burn.sh lvgl-basic-test` и проверить, что `LVGL` показывает текст и кнопки.
4. Убедиться, что новая папка не мешает сборке `subserver-ui`.
- ожидаемый результат:
тестовые скетчи лежат отдельно от основного UI, шьются отдельными режимами и позволяют быстро проверять разные гипотезы по экрану без правок в `shine_subserver_ui`.
- статус:
pending

View File

@ -1,14 +0,0 @@
# ESP32 LVGL interaction test
- краткое описание фичи:
добавлен отдельный скетч `lvgl_interaction_test` на `LVGL`: экран с 9 кнопками, touch-вводом и нижней статусной строкой. При нажатии на кнопку на экране и в `Serial` показывается, какая именно кнопка нажата и сколько нажатий уже было.
- что именно проверять:
1. Прошить режим `lvgl-interaction-test`.
2. Убедиться, что виден заголовок, подзаголовок, 9 кнопок и нижняя статусная панель.
3. Поочерёдно нажать разные кнопки.
4. Проверить, что нижняя строка меняется на `Pressed: <button> (#N)`.
5. Проверить, что touch устойчиво работает по всей сетке кнопок.
- ожидаемый результат:
`LVGL` стабильно рисует плотный экран с множеством кнопок, а нажатия корректно обрабатываются и визуально подтверждаются без глюков позиционирования.
- статус:
pending

View File

@ -1,14 +0,0 @@
# ESP32 LVGL touch debug test
- краткое описание фичи:
добавлен отдельный диагностический скетч `lvgl_touch_debug_test`, который одновременно показывает сырые координаты touch, маркер точки касания и одну большую кнопку `LVGL`. Он нужен, чтобы отделить проблему raw-touch от проблемы доставки событий в `LVGL`.
- что именно проверять:
1. Прошить режим `lvgl-touch-debug-test`.
2. Коснуться экрана в разных местах.
3. Проверить, меняется ли текст `RAW pressed` и координаты `x/y`.
4. Проверить, появляется ли розовый маркер точки касания.
5. Проверить, срабатывает ли большая кнопка `Tap Here` и меняется ли строка `LVGL button clicked`.
- ожидаемый результат:
становится ясно, работает ли сам touch-драйвер, правильно ли приходят координаты и доходит ли нажатие до кнопки `LVGL`.
- статус:
pending

View File

@ -1,13 +0,0 @@
# ESP32 LVGL official based test
- краткое описание фичи:
добавлен отдельный скетч `lvgl_official_based_test`, который строится на максимально близкой к официальному `05_LVGL_Widgets` инициализации `display + touch + LVGL`, но вместо официального demo рисует наш компактный экран с кнопками и статусом нажатия.
- что именно проверять:
1. Прошить режим `lvgl-official-based-test`.
2. Убедиться, что экран отображается без артефактов по краям.
3. Нажать разные кнопки и проверить, меняется ли нижняя строка `Pressed: ...`.
4. Проверить, идут ли координаты touch в `Serial`.
- ожидаемый результат:
если официальный каркас инициализации действительно является рабочей базой, то на этом тесте должны заработать и touch, и кнопки, и исчезнуть визуальные артефакты, которые были в наших самодельных `LVGL`-тестах.
- статус:
pending

View File

@ -1,12 +0,0 @@
# LVGL Russian font test
- Краткое описание: тест кастомного `LVGL`-шрифта с кириллицей на базе рабочего `LVGL + subserver touch` контура.
- Что проверять:
- на экране видны русские заголовки и подписи без транслита;
- отображаются буквы `Ё/ё`;
- видны кнопки `Статус`, `Подключение`, `Кошелёк`, `Запросы`, `Настройки`, `Регистрация`, `Разрешить`, `Отклонить`, `Назад`;
- длинная кнопка `Проверка переноса русского текста` отображается читаемо;
- строка `Нажато:` меняется при клике;
- строка `Касание:` меняется при касании.
- Ожидаемый результат: кириллица стабильно отображается на `LVGL`-экране и не ломает touch.
- Статус: pending

View File

@ -1,104 +0,0 @@
# ESP32 nav minimal test
- Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста.
- Что проверять:
- стартует экран `HOME`;
- на `HOME` видны реальное значение сабсервера или `subserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка баланса, строка `SHiNE: ...`, кнопка `SETTINGS` уменьшенной ширины у правого края и нижняя подпись `SHiNE subserver (v.0.18)`;
- справа от строки логина виден индикатор статуса Solana-аккаунта:
- зелёный, если ключи совпали;
- красный, если mismatch;
- белый контур, если пользователь не найден;
- если статус не зелёный, рядом выводится краткое текстовое пояснение;
- строка Wi-Fi на `HOME` корректно показывает одно из состояний:
- `Wi-Fi (not configured) not configured`
- `Wi-Fi (<saved_ssid>) disconnected`
- `Wi-Fi (<current_ssid>) connected`
- строка `SHiNE:` корректно показывает одно из состояний:
- `connected`
- `account not configured`
- `unavailable`
- пока открыт `HOME`, статус сам обновляется без перехода на другие экраны;
- баланс обновляется кнопкой по нажатию;
- если логин зарегистрирован и секрет/сабсервер заданы, устройство:
- читает `user_pda` через Solana RPC;
- сверяет `root`, `blockchain`, `device` и `subserver` session type `100`;
- поднимает WebSocket-сессию с сервером SHiNE;
- шлёт `Ping` раз в минуту;
- кнопка `SETTINGS` открывает `SETTINGS_MENU`;
- свайп влево на `HOME` открывает `SETTINGS_MENU`;
- если пользователь не найден в Solana PDA, слева снизу появляется `REGISTER ACCOUNT`;
- `REGISTER ACCOUNT` открывает экран-заглушку;
- в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`;
- обе видимые карточки меню одного цвета;
- свайп вверх показывает `Server` и `Account`;
- свайп вниз возвращает `Wi-Fi` и `Server`;
- свайп вправо из `SETTINGS_MENU` возвращает на `HOME`;
- нажатие `Wi-Fi` открывает `WIFI_SCREEN`;
- `SELECT NETWORK` запускает скан;
- после скана показывается список доступных SSID;
- выбор SSID открывает общий экран редактирования текста для пароля;
- если для этого SSID пароль уже сохранялся раньше, он автоматически подставляется в редактор;
- если затем ввести пароль для другого SSID, пароль первой сети не теряется;
- одновременно хранится до `8` паролей для разных SSID;
- на этом экране видно старое значение, курсор стоит в конце;
- две верхние служебные строки над полем ввода отсутствуют;
- при вводе пароля Wi-Fi текст показывается открыто, без точек;
- большая клавиатура реально видна на экране и занимает большую часть высоты;
- буквы разбиты на 2 страницы;
- режим символов тоже разбит на 2 страницы;
- на правой странице кнопки стоят в ровных вертикальных колонках;
- свайп влево/вправо на экране ввода переключает страницы клавиатуры;
- при этом свайп страниц клавиатуры срабатывает только из нижней клавиатурной зоны, а не из верхней части экрана;
- при переключении `ABC/123` и `SHIFT` уже введённый текст не пропадает;
- при свайпе между левой и правой половиной клавиатуры уже введённый текст тоже не пропадает, в том числе для цифр, символов и заглавных букв;
- визуальный курсор в поле ввода не показывается;
- новые символы всегда дописываются только в конец строки;
- основные 3 ряда клавиш и нижний служебный ряд стали выше;
- внизу остаётся отдельная тёмная полоса с версией `SHiNE subserver (v.0.18)`, а рамка клавиатурного блока заканчивается выше неё;
- одно непрерывное касание вызывает не более одного действия кнопки;
- скольжение пальцем по клавиатуре не нажимает подряд несколько клавиш;
- медленный свайп по экрану не должен превращаться в случайное нажатие кнопки;
- `ABC/123`, `SHIFT`, `DEL`, `SAVE`, `CANCEL` работают;
- при успехе SSID и пароль сохраняются, а `HOME` показывает `Wi-Fi connected`;
- если после подключения ко второй сети снова выбрать первую, её старый пароль уже подставлен и достаточно нажать `SAVE`;
- при ошибке показывается `Connection failed`;
- `CLEAR SAVED WI-FI` очищает сохранённые настройки;
- если сеть была ранее успешно сохранена, после потери связи устройство автоматически пытается переподключиться;
- первые повторные попытки идут раз в `10` секунд, а после долгого отсутствия связи интервал увеличивается до `30` секунд;
- нажатие `Server` открывает `SERVER_SCREEN`;
- в `SERVER_SCREEN` видны и редактируются два значения:
- `https://api.devnet.solana.com`
- `https://shineup.me`
- нажатие `SOLANA RPC` открывает общий экран редактирования;
- нажатие `SHINE SERVER` открывает общий экран редактирования;
- после `SAVE` новые адреса сохраняются в NVS;
- нажатие `Account` открывает `ACCOUNT_SCREEN`;
- `ACCOUNT_SCREEN` показывает 3 кнопки:
- `Login (<value|not set>)`
- `Subserver (<value|not set>)`
- `Secret (<*****|not set>)`
- `Login` открывает общий экран редактирования и сохраняется в NVS;
- `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`;
- `USE SUBSERVER1` возвращает стандартное значение `subserver1`;
- `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS;
- `Secret` теперь открывает меню секрета с показом секрета, ручным вводом и генерацией;
- в `SHOW SECRET` показывается прокручиваемый список всех ключей:
- `Secret (base58)`
- `Root key (base58)`
- `Root key priv (base58)`
- `Blockchain key (base58)`
- `Blockchain key priv (base58)`
- `Device key (base58)`
- `Device key priv (base58)`
- `Subserver key (base58)`
- `Subserver key priv (base58)`
- значения ключей показываются полными строками увеличенным шрифтом;
- при смене `login` сохранённый секрет сбрасывается в `not set`;
- во время генерации секрета есть `CANCEL` и подтверждение остановки;
- при отмене генерации старый секрет, если он был, не должен теряться;
- свайп вправо из внутренних экранов возвращает в `SETTINGS_MENU`;
- свайп вправо из `ACCOUNT_SUBSERVER_SCREEN` и `ACCOUNT_SECRET_SCREEN` возвращает в `ACCOUNT_SCREEN`;
- если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`.
- Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32.
- Дополнительно ожидается: `HOME` уже показывает реальный Solana/WS-статус сабсервера, а отсутствие пользователя в Solana заметно сразу без перехода в настройки.
- Статус: pending

View File

@ -1,16 +0,0 @@
# Deeplink ссылки профиля и связей
- краткое описание:
Исправлена загрузка UI по прямым ссылкам вида `https://shineup.me/shine.<login>` и `https://shineup.me/shine.<login>/links` через добавление корневого `<base href="/">` в основной `index.html`.
- что проверять:
1. Открыть прямую ссылку на профиль в новой вкладке: `https://shineup.me/shine.<login>`.
2. Открыть прямую ссылку на связи в новой вкладке: `https://shineup.me/shine.<login>/links`.
3. Повторить оба сценария в состоянии гостя.
4. Повторить оба сценария в состоянии, когда в браузере залогинен другой пользователь.
- ожидаемый результат:
1. Страница загружается напрямую без поломки ассетов и без ухода на неверный экран.
2. Открывается профиль/связи именно пользователя из URL.
3. Для гостя экран открывается в read-only режиме.
4. Для залогиненного другого пользователя URL не подменяется на текущую сессию.
- статус:
pending

View File

@ -0,0 +1,18 @@
# AGENTS
## Документация DM в этой папке
- Основной актуальный документ по личным сообщениям:
- `README.md`
- Его считать единственным источником истины по текущей реализованной логике DM.
## Черновик будущих вложений
- Файл ерновик_будущих_DM_вложений.md` не является актуальной спецификацией.
- В нём описан только ранний черновик того, как когда-то планировались:
- формат вложений в DM;
- внешние и внутренние поля вложения;
- предполагаемая механика загрузки файлов.
- Эта схема не была реализована в таком виде и может существенно измениться в будущем.
- Любые решения по текущему коду, протоколу и UI нельзя принимать по этому черновику.
- Если есть расхождение между `README.md` и черновиком вложений, верным всегда считается `README.md`.

View File

@ -1,269 +1,203 @@
# Личные сообщения (DM): как это устроено
# Личные сообщения (DM)
## Коротко (для быстрого понимания)
## Текущее состояние
Личные сообщения в SHiNE сейчас работают как пара **подписанных клиентом блоков** в формате `SHiNE_dm2`:
Сейчас в проекте реализованы:
- тип `1` — входящее сообщение для собеседника;
- тип `2` — исходящая копия того же сообщения для автора.
- новый формат контентных личных сообщений `SHiNE_DM`;
- ревизии сообщений через `revisionTimeMs`;
- редактирование сообщения через повторную отправку той же логической пары;
- удаление сообщения через пустую ревизию;
- `upsert` последней версии сообщения на сервере.
Оба блока отправляются вместе одной операцией (`SendMessagePair` / `ReceiveOutcomingMessage`) и либо сохраняются оба, либо не сохраняются вовсе.
Дальше сервер доставляет их по активным сессиям целевого логина событием `SignedMessageArrived`, а клиент подтверждает доставку на конкретную сессию через `AckSessionDelivery`.
Сейчас в проекте **не реализованы**:
Подтверждение прочтения также идёт парой блоков:
- вложения в DM;
- upload/download файлов для DM;
- UI-кнопка прикрепления файла;
- серверное хранение файловых связей для DM.
- тип `3` — «прочитано» для исходящего сообщения автора;
- тип `4` — зеркальная копия для второй стороны.
Черновик будущих вложений вынесен отдельно:
UI чата строится на этих типах: текстовые сообщения (1/2), read-receipt (3/4), непрочитанные, галочки и история.
- `Dev_Docs/Personal_Messages/Черновик_будущих_DM_вложений.md`
---
## Общая схема
## Подробно
Личное сообщение по-прежнему отправляется парой signed-блоков:
## 1) Общая схема потока
- `type=1` — входящий блок для получателя;
- `type=2` — исходящая копия для отправителя.
1. Клиент формирует текст сообщения и строит **2 подписанных блока** (`type=1` и `type=2`) с одинаковыми `fromLogin/toLogin/timeMs/nonce`.
2. Клиент отправляет оба блока в одном RPC: `SendMessagePair` (алиас: `ReceiveOutcomingMessage`).
3. Сервер:
- парсит оба блока;
- валидирует пару;
- проверяет существование `from/to` пользователей и подписи;
- атомарно сохраняет пару в `signed_messages_v2`.
4. Сервер доставляет блоки в активные сессии целевого логина событием `SignedMessageArrived`.
5. Клиент, получив событие, кладёт сообщение в локальный чат и отправляет `AckSessionDelivery(messageKey)`.
6. При открытии чата клиент отправляет read-receipt (пара `type=3/4`) для непрочитанных входящих.
Read-receipt пока остаются в legacy-формате:
## 2) Формат signed DM-блока (`SHiNE_dm2`)
- `type=3` — входящее подтверждение прочтения;
- `type=4` — исходящая копия подтверждения.
Префикс: `SHiNE_dm2` (ASCII).
Ключи сообщения:
Далее поля (big-endian):
1. `toLoginLen` (`u8`) + `toLogin` (ASCII, 1..60);
2. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, 1..60);
3. `timeMs` (`u64`);
4. `nonce` (`u32`);
5. `messageType` (`u16`);
6. `payloadLen` (`u16`);
7. `payloadBytes` (`1..4096`);
8. `signature` (`64 bytes`, Ed25519).
Ограничения:
- полный пакет: до `8192` байт;
- `messageType` сейчас допустим только `1..4`.
## 3) Типы DM-сообщений
- `1` (`TYPE_INCOMING_TEXT`) — входящий текст для получателя.
- `2` (`TYPE_OUTGOING_COPY`) — исходящая копия в истории автора.
- `3` (`TYPE_READ_INCOMING`) — read-receipt (входящий тип для пары квитанции).
- `4` (`TYPE_READ_OUTGOING_COPY`) — зеркальная копия read-receipt.
Правило пары:
- первый блок должен быть нечётным (`1` или `3`);
- второй должен быть ровно `+1` (`2` или `4`);
- ключевые поля пары совпадают: `toLogin/fromLogin/timeMs/nonce`.
## 4) Ключи сообщений
- `baseKey = from|to|timeMs|nonce`
- `baseKey = fromLogin|toLogin|timeMs|nonce`
- `messageKey = baseKey|messageType`
Эти ключи используются:
Логический идентификатор письма задаётся парой:
- для дедупликации;
- для связи read-receipt с исходным сообщением;
- для ACK доставки по сессии.
- `timeMs`
- `nonce`
## 5) RPC и события
Эти поля не меняются при редактировании или удалении. Меняется только:
## `SendMessagePair` (алиас `ReceiveOutcomingMessage`)
- `revisionTimeMs`
- содержимое `encryptedBody`
Запрос:
Сервер хранит только последнюю версию записи для каждого `messageKey`.
```json
{
"op": "SendMessagePair",
"requestId": "req-1",
"payload": {
"incomingBlobB64": "<base64 signed block type 1 or 3>",
"outgoingBlobB64": "<base64 signed block type 2 or 4>"
}
}
```
## Формат контентного DM: `SHiNE_DM`
Успешный ответ:
Префикс бинарного блока:
```json
{
"op": "SendMessagePair",
"requestId": "req-1",
"status": 200,
"ok": true,
"payload": {
"baseKey": "from|to|time|nonce",
"incomingKey": "from|to|time|nonce|1",
"outgoingKey": "from|to|time|nonce|2",
"deliveredWsSessions": 2,
"deliveredWebPushSessions": 1
}
}
```
- `SHiNE_DM`
## `SignedMessageArrived` (server event)
Поля идут в big-endian порядке:
Событие в сессию получателя содержит:
1. `formatVersionMajor` (`u8`) = `1`
2. `formatVersionMinor` (`u8`) = `0`
3. `toLoginLen` (`u8`) + `toLogin` (ASCII, `1..60`)
4. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, `1..60`)
5. `timeMs` (`u64`)
6. `nonce` (`u32`)
7. `messageType` (`u16`) — только `1` или `2`
8. `revisionTimeMs` (`u64`)
9. `attachmentsCount` (`u8`)
10. `encryptedBodyLen` (`u32`)
11. `encryptedBody` (`bytes`)
12. `signature` (`64 bytes`, Ed25519)
- `messageKey`, `baseKey`;
- `fromLogin`, `toLogin`, `targetLogin`;
- `messageType`, `timeMs`, `nonce`;
- `blobB64`;
- `backlog` (признак догрузки из очереди).
### Ограничения
## `AckSessionDelivery`
- `attachmentsCount` сейчас всегда должен быть `0`
- `encryptedBodyLen` сейчас ограничен сервером до `16384` байт
- `revisionTimeMs` не может быть отрицательным
Запрос:
Если приходит `attachmentsCount != 0`, сервер отклоняет такой DM как:
```json
{
"op": "AckSessionDelivery",
"requestId": "ack-1",
"payload": {
"messageKey": "from|to|time|nonce|1"
}
}
```
- `ATTACHMENTS_DISABLED`
Ответ: `status=200`, echo `messageKey`.
## Legacy read-receipt: `SHiNE_dm2`
## 6) Хранение на сервере (SQLite)
Подтверждения прочтения `type=3/4` пока используют старый контейнер `SHiNE_dm2`:
Основные таблицы:
1. `toLoginLen` (`u8`) + `toLogin`
2. `fromLoginLen` (`u8`) + `fromLogin`
3. `timeMs` (`u64`)
4. `nonce` (`u32`)
5. `messageType` (`u16`) — `3` или `4`
6. `payloadLen` (`u16`)
7. `payloadBytes`
8. `signature`
1. `signed_messages_v2` — сами DM-блоки типов `1/2/3/4`:
- `message_key` (PK),
- `base_key`,
- `target_login`,
- `from_login`, `to_login`,
- `time_ms`, `nonce`, `message_type`,
- `raw_block`,
- `source_api`, `origin_session_id`,
- `receipt_ref_base_key`, `receipt_ref_type`.
2. `signed_message_session_delivery` — доставка по сессиям:
- составной PK `(message_key, session_id)`,
- `delivered` (0/1),
- `delivered_at_ms`, `created_at_ms`.
## Редактирование
Примечание: историческая таблица `signed_direct_messages_history` в БД присутствует как legacy-слой, но текущий рабочий поток DM v2 опирается на `signed_messages_v2` + `signed_message_session_delivery`.
Редактирование делается новой отправкой той же логической пары сообщения:
## 7) Доставка и backlog
- `timeMs` и `nonce` остаются теми же;
- `messageType` остаётся `1/2`;
- `revisionTimeMs` становится больше;
- `encryptedBody` содержит новую версию текста.
- При сохранении пары сервер пытается сразу доставить в онлайн-сессии.
- Для офлайн/недоступных сессий остаётся pending-запись доставки в таблице `signed_message_session_delivery`.
- При подключении сессии сервер автоматически вызывает `dispatchPendingForSession`:
- для новой сессии регистрирует все существующие сообщения адресата как «недоставленные»;
- отправляет **все** pending через WebSocket событием `SignedMessageArrived(backlog=true)`;
- лимита на количество сообщений нет — передаётся вся история без ограничений.
- Клиент дедублирует входящие через `knownMessageKeys`: если `messageKey` уже есть локально — игнорирует.
- После получения клиент отправляет `AckSessionDelivery`, чтобы отметить `delivered=1` в таблице доставки.
Если на сервер приходит более старая ревизия, она игнорируется.
## 8) Read-receipt логика
Если приходит та же ревизия и тот же бинарный блок, сервер тоже её не применяет повторно.
Когда клиент открывает чат:
## Удаление
1. ищет входящие `messageType=1` без `readReceiptSent`;
2. для каждого отправляет read-receipt как пару `type=3/4`;
3. после успешной отправки помечает `readReceiptSent`.
Удаление личного сообщения делается как новая ревизия того же сообщения:
Сервер для read-receipt хранит ссылку на исходное сообщение:
- `timeMs` и `nonce` остаются прежними;
- `revisionTimeMs` увеличивается;
- `attachmentsCount = 0`;
- `encryptedBodyLen = 0`;
- `encryptedBody` пустой.
- `receipt_ref_base_key`;
- `receipt_ref_type`.
В UI такое сообщение не показывается.
Есть уникальность, чтобы не плодить дубликаты receipt на один и тот же `baseKey` для одного `target_login`.
На сервере это не отдельный тип сообщения, а просто последняя пустая ревизия того же `messageKey`.
## 9) Логика UI-клиента
## Поведение сервера
### Хранилище сообщений
Для контентных DM сервер:
- In-memory: `state.chats[chatId]` — массив сообщений по каждому диалогу.
- Персистентно: IndexedDB база `shine-ui-messages-v1`, object store `messages`, ключ `messageKey`.
- `chatId` для `type=1``fromLogin`, для `type=2``toLogin`.
1. принимает пару signed-блоков `type=1/2`;
2. валидирует формат, подпись и совпадение ключевых полей пары;
3. проверяет, что для обеих сторон пары совпадают:
- `fromLogin`
- `toLogin`
- `timeMs`
- `nonce`
- `revisionTimeMs`
- `encryptedBody`
4. делает `upsert` последней версии в `signed_messages_v2`;
5. сбрасывает pending-доставку по сессиям для новой ревизии;
6. рассылает актуальную версию адресатам через `SignedMessageArrived`.
### Жизненный цикл при старте/подключении
История старых ревизий сейчас не хранится отдельно: в таблице остаётся только последняя версия по каждому `messageKey`.
1. `hydrateMessagesFromStore()` — читает все сообщения из IndexedDB в `state.chats` (до WebSocket-соединения).
2. После установки WebSocket-сессии сервер присылает backlog (`SignedMessageArrived(backlog=true)`) для всех недоставленных сообщений.
3. Клиент дедублирует через `knownMessageKeys` — уже имеющиеся в IndexedDB игнорируются.
4. Новые сообщения в реальном времени приходят теми же WebSocket-событиями.
## Хранение в БД
### Очистка при выходе и смене пользователя
Основная таблица:
- При любом логауте (`terminateCurrentSession`) IndexedDB с сообщениями **удаляется полностью**.
- При входе нового пользователя через QR — IndexedDB удаляется явно до вызова `terminateCurrentSession`.
- При входе нового пользователя через логин/пароль — IndexedDB удаляется в `registration-keys-view.js` прямо перед `authorizeSession()`.
- Это гарантирует: при любом способе входа старые сообщения предыдущего пользователя не попадут к следующему.
- `signed_messages_v2`
### UI-поведение
Для контентных DM в ней используются:
- непрочитанные считаются по `from='in' && unread=true`;
- доставка/прочтение исходящих:
- `firstTick` — сообщение принято сервером,
- `secondTick` — пришло подтверждение прочтения;
- при открытии диалога UI автопрокручивает ленту в самый низ;
- после отправки нового сообщения UI сразу прокручивает ленту вниз.
- `message_key`
- `base_key`
- `target_login`
- `from_login`
- `to_login`
- `time_ms`
- `nonce`
- `message_type`
- `revision_time_ms`
- `raw_block`
- `created_at_ms`
## 10) Синхронизация личных сообщений между серверами
Отдельных таблиц файлов для DM сейчас нет.
Когда пользователи зарегистрированы на разных серверах SHiNE, серверы должны синхронизировать DM между собой.
## События и доставка
### Общий принцип
Запрос на отправку по WebSocket остаётся прежним:
- Сервер A получает DM-блок, адресованный пользователю на сервере B.
- Сервер A пересылает этот блок серверу B (межсерверный relay).
- Сервер B сохраняет блок и доставляет его в активные сессии получателя.
- Серверы, между которыми идёт синхронизация, задаются списком `sync_servers` в PDA пользователя-сервера.
- `SendMessagePair`
- `ReceiveOutcomingMessage` как алиас
### Что синхронизируется
Клиент отправляет:
- Все DM-блоки типов `1/2` (текстовые сообщения) и `3/4` (read-receipt).
- Синхронизация двусторонняя: оба сервера должны уметь принимать и пересылать блоки.
- `incomingBlobB64`
- `outgoingBlobB64`
### Идемпотентность
Событие в активные сессии:
- Блоки имеют уникальный `message_key` (`from|to|timeMs|nonce|type`).
- Повторная доставка одного и того же блока безопасна — дедупликация происходит по `message_key`.
- `SignedMessageArrived`
### Статус реализации
Если пришла новая ревизия того же сообщения, `messageKey` остаётся прежним, а внутри `blobB64` будет более новый `revisionTimeMs`.
Межсерверная синхронизация DM **пока не реализована**. Текущая версия работает только в рамках одного сервера. Это задача для следующего этапа.
Подтверждение доставки в сессию:
---
- `AckSessionDelivery`
## 11) Инварианты (обязательно соблюдать при доработках)
## Правила UI
1. Пара блоков (1/2 или 3/4) должна оставаться атомарной.
2. `messageKey`/`baseKey` формат должен быть совместим с текущей логикой дедупликации и receipt.
3. Доставка должна оставаться **по сессиям** с явным `AckSessionDelivery`.
4. Read-receipt не должен отправляться многократно на один и тот же `baseKey`.
5. Любые изменения DM-логики в коде должны сразу отражаться в этом документе.
UI сейчас работает так:
## 12) Ключевые файлы реализации
- показывает только текст `encryptedBody`;
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
- не показывает удалённые сообщения;
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
- не показывает и не принимает вложения.
- UI:
- `shine-UI/js/services/auth-service.js`
- `shine-UI/js/app.js`
- `shine-UI/js/state.js`
- `shine-UI/js/pages/chat-view.js`
- Сервер:
- `shine-server-net-protocol/.../messages/SignedMessageBlock.java`
- `shine-server-net-protocol/.../messages/SignedMessagesCore.java`
- `shine-server-net-protocol/.../messages/Net_SendMessagePair_Handler.java`
- `shine-server-net-protocol/.../messages/SignedMessagesRealtime.java`
- `shine-server-net-protocol/.../messages/Net_AckSessionDelivery_Handler.java`
- БД:
- `shine-server-db/src/main/java/shine/db/DatabaseInitializer.java`
- `shine-server-db/src/main/java/shine/db/dao/SignedMessagesV2DAO.java`
## Что обязательно помнить
- вложения в DM сейчас отключены на уровне протокола и UI;
- любые старые описания `/f/...`, `/upload` и файловых таблиц для DM больше не актуальны;
- если позже вложения вернутся, их формат и серверная логика могут быть другими.

View File

@ -0,0 +1,73 @@
# Черновик будущих вложений в DM
## Важно
Этот документ описывает только ранний черновик идеи.
Сейчас в проекте **нет** поддержки вложений в личных сообщениях:
- в реализованном формате `SHiNE_DM` поле `attachmentsCount` пока всегда должно быть `0`;
- UI не показывает кнопку прикрепления файлов;
- сервер не принимает upload файлов для DM;
- сервер не раздаёт специальные DM-файлы по отдельным endpoints;
- сервер не хранит отдельные файловые связи для личных сообщений.
Этот документ нужен только для того, чтобы рядом с актуальной документацией было явно видно:
- какие идеи обсуждались;
- что это **не реализовано**;
- что формат, хранение и способ загрузки потом могут сильно измениться.
## Что обсуждалось
Рассматривался такой общий подход:
- у контентного DM есть внешний список вложений;
- во внешнем формате лежат только технические данные;
- человекочитаемые данные о файле живут внутри зашифрованного тела сообщения;
- один и тот же blob-файл теоретически мог бы переиспользоваться в нескольких сообщениях.
Черновой вариант внешнего списка:
- `attachmentsCount`
- далее для каждого вложения:
- `encFileHashSHA256` (`32 bytes`)
- `encFileSize` (`u64`)
Черновой вариант внутреннего маркера в тексте:
```text
<<file:file-format(1.0):type|fileName|origSize|origHashB64u|encHashB64u|encSize|keyB64u|nonceB64u>>
```
Где обсуждались поля:
- `type`
- `fileName`
- `origSize`
- `origHashB64u`
- `encHashB64u`
- `encSize`
- `keyB64u`
- `nonceB64u`
## Что может измениться
В будущем могут измениться любые части идеи:
- сам бинарный формат;
- способ привязки файлов к сообщению;
- момент загрузки файла относительно отправки сообщения;
- серверное хранение blob-файлов;
- права доступа к скачиванию;
- способ рендера вложения в UI.
Именно поэтому этот файл не надо воспринимать как актуальную спецификацию.
## Источник истины на сейчас
Актуальное состояние личных сообщений описано только в:
- `Dev_Docs/Personal_Messages/README.md`
Если между этим черновиком и основным README есть расхождение, верным считается `README.md`.

View File

@ -90,7 +90,7 @@ UserPdaRecordV1
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. |
| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и homeserver-ы. |
| `70` | `TrustedStateBlock` | Счетчик trusted-связей. |
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
@ -309,7 +309,7 @@ SessionRecord
| Значение | Смысл |
|----------|-------|
| `1` | Обычная пользовательская сессия. |
| `100` | Саб-сервер пользователя. |
| `100` | Homeserver пользователя. |
Правила:

View File

@ -0,0 +1,177 @@
# Аудит безопасности Solana-программ SHiNE — выпуск 2 (11.06.2026)
Повторный независимый аудит после исправления всех 4 находок первого отчёта
(`Solana-audit-by-Claude-File5-9июня2026.md`). Код перечитан целиком:
- `shine_login_guard` (183 строки) — stateless-классификатор логинов;
- `shine_users` (1069 строк) — реестр пользователей, PDA-записи, подписи, экономика лимитов;
- `shine_payments` (1381 строка) — очереди тикетов, выплаты из вольта, оракул Pyth.
Перебраны классы атак: подмена аккаунтов/PDA, авторизация и подписи, арифметика и
переполнения, валидация оракула, экономика, реентранси, griefing/DoS, **алиасинг
аккаунтов (передача одного аккаунта в несколько слотов инструкции)**.
## Статус прошлых находок (все закрыты)
- 🔴 Critical #1 (economy-config PDA в `shine_users`) — закрыто: `validate_users_economy_config_pda` проверяет и адрес, и `owner == program_id`, и вызывается перед чтением и в create, и в update.
- 🔴 Critical #2 (singleton-PDA в `shine_payments`) — закрыто: `validate_singleton_state_pda` проверяет точный адрес + `owner == id()` во всех инструкциях (`update_coef_limit`, `grant_manager_limits`, `buy_ticket*`, `manager_add_ticket`, `step_payout`, `change_ticket_recipient`).
- 🟠 Medium (валидация Pyth) — закрыто: пин адреса аккаунта `PYTH_SOL_USD_ACCOUNT`, проверка `owner == pyth_receiver`, разбор официальным `PriceUpdateV2`, `get_price_no_older_than` с проверкой `feed_id`, проверка возраста и доверительного интервала (`ORACLE_MAX_CONFIDENCE_PPM`).
- 🟡 Low (griefing на предсказуемых адресах) — закрыто: `create_pda_account` в обеих программах переведён на «создание поверх предзаполненного» (allocate + assign + добор ренты).
---
## 🔴 HIGH (НОВОЕ) — `shine_payments`: тикет с `recipient_wallet == inflow_vault` навсегда замораживает все выплаты — ✅ ИСПРАВЛЕНО (11.06.2026)
Закрыто: равенство `recipient == inflow_vault` запрещено во всех точках задания
получателя — `buy_ticket_by_purchase_usd` (через `config.inflow_vault`),
`process_manager_add_ticket` и `process_change_ticket_recipient` (через
`find_single_pda(INFLOW_VAULT_SEED)`). Дополнительно в `transfer_from_vault` добавлена
защита по умолчанию `require!(vault.key != recipient.key)`. Документация —
`doc/programs/shine_payments.md` §10.1. Историческое описание находки ниже.
### Где
`transfer_from_vault` (строки 12581268) переводит лампорты из вольта прямой
манипуляцией балансами (вольт — PDA без приватного ключа, обычный system-перевод
невозможен):
```rust
fn transfer_from_vault(vault: &AccountInfo, recipient: &AccountInfo, amount: u64) -> ProgramResult {
if amount == 0 { return Ok(()); }
let mut vault_lamports = vault.try_borrow_mut_lamports()?; // займ #1
let mut recipient_lamports = recipient.try_borrow_mut_lamports()?; // займ #2
...
}
```
В `step_payout` (строка 849) получатель — это `ticket.recipient_wallet`:
```rust
transfer_from_vault(inflow_vault_pda, ticket_recipient_wallet, ticket_lamports)?;
```
А `recipient_wallet` нигде не валидируется при создании тикета:
`buy_ticket*` (строки 696/711/725 → 1031), `manager_add_ticket` (строка 765),
`change_ticket_recipient` (строка 900) — берут его «как есть» из аргументов.
### Суть атаки (алиасинг аккаунта)
В Solana, если один и тот же аккаунт передан в инструкцию в нескольких слотах,
рантайм отдаёт для всех слотов **один и тот же** `RefCell` (механизм дублей).
Поэтому если `ticket.recipient_wallet` равен адресу `inflow_vault` PDA, то в
`step_payout` аккаунт вольта попадает и в слот `inflow_vault_pda`, и в слот
`ticket_recipient_wallet`. Тогда внутри `transfer_from_vault`:
- `vault.try_borrow_mut_lamports()` — берёт mutable-займ (успех);
- `recipient.try_borrow_mut_lamports()` — это **тот же** аккаунт → второй
mutable-займ → `Err(AccountBorrowFailed)``?` возвращает ошибку → инструкция
падает.
### Почему это «заморозка всего», а не один тикет
Выплаты идут строго по возрастанию индекса. `step_payout` всегда обслуживает
сначала очередь Q1 (если в ней есть pending), затем Q2, затем Q3, и в каждой —
ровно «следующий неоплаченный» тикет (`paid + 1`). Тикет с `recipient == vault`:
- не может быть оплачен (`step_payout` всегда падает на нём);
- не может быть пропущен (нет механизма «skip»);
- блокирует все тикеты после него в своей очереди;
- если он в Q1 — блокирует обслуживание Q2 и Q3 (до них очередь не доходит);
- лампорты вольта (накопленные регистрационные комиссии) перестают выплачиваться
и не уходят в DAO (слив в DAO происходит только когда `pending == 0` по всем
очередям, а это состояние недостижимо).
### Эксплуатация (тривиальная, перестановочная)
Q1 — публичная очередь (`buy_ticket` доступен любому). Атакующий покупает **один**
дешёвый тикет Q1, указав `recipient_wallet = <адрес inflow_vault PDA>`. Адрес вольта
детерминирован и публичен (`find_single_pda(INFLOW_VAULT_SEED)`). С этого момента вся
подсистема выплат и средства вольта заморожены за стоимость одного тикета + ренты.
Дополнительно: даже при защите на этапе покупки остаётся вектор через
`change_ticket_recipient` (строка 900) — владелец любого своего неоплаченного тикета
может выставить `new_recipient_wallet = vault` позже.
### Класс и серьёзность
Класс: «account aliasing / duplicate-account mutable borrow» + отсутствие
валидации адреса получателя. Прямой кражи средств нет, но это перманентный
отказ в обслуживании (availability) с блокировкой средств вольта, триггер —
копеечный и доступен анонимно. Оценка: **HIGH**.
### Рекомендуемый фикс
Запретить `recipient`, равный адресу вольта, во всех точках, где он задаётся, чтобы
тикет с таким получателем вообще не мог появиться:
1. в `buy_ticket_by_purchase_usd``require!(recipient_wallet != config.inflow_vault, …)`
(config уже прочитан);
2. в `process_manager_add_ticket` — сверять с `find_single_pda(INFLOW_VAULT_SEED).0`;
3. в `process_change_ticket_recipient` — то же для `new_recipient_wallet`.
Дополнительно (defense-in-depth) — в `transfer_from_vault` явно
`require!(vault.key != recipient.key, …)` с понятной ошибкой, чтобы любой будущий
вызов был защищён от алиасинга. Этого `require` недостаточно как единственной меры
(тикет всё равно застрял бы), поэтому основная защита — на входе.
---
## 🟡 LOW / INFO — наблюдения без прямой эксплуатации
### L1. `change_ticket_recipient` и `buy_ticket` не проверяют получателя на «опасные» адреса
Связано с HIGH выше; после фикса основной проблемы стоит заодно зафиксировать
правило «получатель не должен совпадать с системными PDA программы».
### L2. Гонка за логином (first-come) в `shine_users`
Адрес `user_pda` выводится из логина. После закрытия griefing-подсева остаётся
обычное состязание: увидев в мемпуле регистрацию `alice`, атакующий может
зарегистрировать `alice` со своим `root_key` первым. On-chain это решается только
commit-reveal; для текущей модели — приемлемый риск, отметить как известный.
### L3. `step_payout` без slippage-параметра
Выплата считается по текущей цене оракула без верхней границы лампортов. Цена
ограничена возрастом (120с) и доверительным интервалом (10%), аккаунт оракула
запинен — манипуляция маловероятна, но при резком движении цены SOL объём выплаты
в лампортах плавает. Риск низкий; при желании добавить верхнюю границу на шаг.
### L4. Экономическая устойчивость вольта (дизайн, не баг)
Деньги за покупку тикетов (`buy_ticket`) уходят на `dao_wallet`, а выплаты в
`step_payout` идут из `inflow_vault`, который наполняется **регистрационными
комиссиями** `shine_users`. Если поток регистраций меньше обязательств по выплатам,
вольт истощается и выплаты останавливаются (без потери средств, но с остановкой
сервиса). Это свойство экономической модели — стоит явно держать в уме и
мониторить баланс вольта/обязательств.
### L5. Заполнение Q1 до лимита как мягкий DoS
`buy_ticket` блокируется при `q1_sum_total >= limit_usd_cents`. Атакующий может
наполнить Q1 своими тикетами и приостановить покупки. Дорого (тратит SOL в DAO и
ренту) и его же тикеты потом оплачиваются из вольта, поэтому это скорее
экономический, а не дешёвый griefing. Риск низкий.
---
## ✅ Проверено и подтверждено как корректное
- **Подмена singleton-PDA** невозможна: везде сверяется точный адрес и владелец.
- **Авторизация**: `update_coef_limit`/`grant_manager_limits` требуют `signer == config.dao_wallet`; `manager_add_ticket``signer == allowance.manager_wallet`; `change_ticket_recipient``signer == ticket.recipient_wallet`; обновление economy-config — `signer == DAO_AUTHORITY`.
- **Ed25519 в `shine_users`**: строгие относительные индексы (1/2), `num_signatures == 1`, все три `ix_index == u16::MAX` (данные внутри самой ed25519-инструкции), сверка pubkey/signature/message по хэшу. Подмена и указание на чужую инструкцию исключены.
- **Цепочка версий записи** (`version == record_number+1`, `prev_hash == hash(old)`) — корректная защита от replay; сигнатура записи завязана на `root_key`, а не на плательщика.
- **Монотонность** `used_bytes`/`last_block_number` и `used_bytes <= paid_limit_bytes`.
- **Арифметика**: повсеместные `checked_*`, `overflow-checks = true`, расчёты оракула в `u128` с `u64::try_from` на сужении.
- **Оракул Pyth**: пин аккаунта + owner + feed_id + возраст + confidence через официальный SDK.
- **Рент-экземпт вольта** сохраняется: `available_vault_lamports` вычитает `minimum_balance`, а суммарная проверка `available >= needed` гарантирует, что после выплат вольт не опустится ниже ренты.
- **Двойная оплата тикета** исключена: `is_paid` + инкремент `*_tickets_paid`, следующий шаг адресует следующий индекс.
- **Реентранси отсутствует**: CPI только в System Program (transfer/allocate/assign) и в stateless `shine_login_guard` (с проверкой возвращённого `program_id`); обратных вызовов в наши программы нет.
- **create_pda_account (новый)**: устойчив к подсеву лампортов; атакующий не может ни выделить данные, ни сменить владельца PDA (нет ключа/seeds), поэтому ветка allocate+assign безопасна.
- **shine_login_guard**: stateless, без аккаунтов и средств; DFS-классификация ограничена (`MAX_WORDS_PER_LOGIN = 3`, длина ≤ 20) — без compute-DoS.
---
## Приоритет действий
1. **HIGH** — запретить `recipient == inflow_vault` в `buy_ticket*`, `manager_add_ticket`,
`change_ticket_recipient`; добавить `require!(vault.key != recipient.key)` в
`transfer_from_vault` как защиту по умолчанию. Закрыть до mainnet.
2. **LOW** — зафиксировать правило «получатель ≠ системные PDA» (L1), оценить
добавление верхней границы выплаты на шаг (L3).
3. **INFO** — формально задокументировать экономику вольта (L4) и known-issue
гонки за логином (L5/L2).
Изменений в код в рамках этого аудита не вносил — это анализ. Готов подготовить патч
по пункту 1, если подтвердите.

View File

@ -0,0 +1,134 @@
# Аудит безопасности Solana-программ SHiNE — выпуск 3 (12.06.2026)
Тематический аудит с фокусом на **полноту проверок входных аккаунтов**
(signer / owner / каноничный PDA-адрес / system-program / sysvar инструкций /
аккаунт оракула) — отвечает на вопрос «точно ли хватает всех проверок входных
аккаунтов». Код перечитан целиком после исправлений аудита №2
(`Solana-audit-2-by-Claude-11июня2026.md`):
- `shine_login_guard` (183 строки) — stateless-классификатор логинов, аккаунтами не пользуется;
- `shine_users` (1068 строк) — реестр пользователей, PDA-записи, ed25519-подписи, экономика лимитов;
- `shine_payments` (1398 строк) — очереди тикетов, выплаты из вольта, оракул Pyth.
Это ручная (не-Anchor `#[derive(Accounts)]`) реализация на `solana_program`, поэтому
каждая проверка аккаунта выполняется явно в коде handler-а. Перебраны: подмена
аккаунтов/PDA, подмена владельца, bump-seed атаки, отсутствие signer/authority,
подмена system-program и sysvar, подмена аккаунта оракула, неинициализированные/
повторно инициализируемые PDA, «лишние» аккаунты.
## Итоговый вердикт
**Проверок входных аккаунтов достаточно во всех трёх программах.** По каждому
handler присутствуют все требуемые классы проверок; грубых дыр (подмена PDA на
чужой аккаунт, отсутствие owner/signer-проверки, использование пользовательского
bump, подмена аккаунта оракула) не найдено. Все Critical/HIGH из аудитов №1 и №2
закрыты и в этом проходе подтверждены в коде. Новых эксплуатируемых пробелов в
валидации аккаунтов нет; есть несколько LOW/INFO-замечаний «by design».
## Статус прошлых находок (подтверждено в коде на 12.06.2026)
- 🔴 Critical #1 (economy-config PDA, `shine_users`) — закрыто: `validate_users_economy_config_pda` (адрес + `owner == program_id`) вызывается и в create, и в update перед чтением.
- 🔴 Critical #2 (singleton-PDA, `shine_payments`) — закрыто: `validate_singleton_state_pda` (адрес + `owner == id()`) во всех инструкциях.
- 🟠 Medium (валидация Pyth) — закрыто: пин адреса `PYTH_SOL_USD_ACCOUNT`, `owner == pyth_receiver`, `PriceUpdateV2`, `feed_id`, возраст, доверительный интервал.
- 🟡 Low (griefing на предсказуемых адресах) — закрыто: `create_pda_account` создаёт «поверх предзаполненного» в обеих программах.
- 🔴 HIGH аудита №2 (`recipient_wallet == inflow_vault` замораживает выплаты) — закрыто: запрет `recipient == inflow_vault` в `buy_ticket_by_purchase_usd` (стр. 1026), `process_manager_add_ticket` (стр. 747), `process_change_ticket_recipient` (стр. 878) + защита по умолчанию `require!(vault.key != recipient.key)` в `transfer_from_vault` (стр. 1278).
---
## Матрица проверок входных аккаунтов
### shine_users
| Инструкция | signer | owner PDA | адрес/seed PDA | system | sysvar / подпись | прочее |
|---|---|---|---|---|---|---|
| `init_users_economy_config` | ✓ | `owner == system` + `data_is_empty` (анти-reinit) | деривация + сверка | ✓ | — | значения из `settings`, не из ввода |
| `update_users_economy_config` | ✓ + `signer == DAO_AUTHORITY` | `owner == program_id` | деривация + сверка | — | — | `lamports_per_limit_step > 0` |
| `create_user_pda` | ✓ + `signer == device_key` | user_pda `owner == system` + empty; econ_config `owner == program_id` | user_pda, econ_config, inflow_vault, login_guard — все сверены | ✓ | ed25519 (record sig idx 2, last_block idx 1) | `inflow_vault` сверен с PDA `shine_payments`; login_guard сверен дважды |
| `update_user_pda` | ✓ + `signer == device_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью |
### shine_payments
| Инструкция | signer | owner / валидация PDA | адрес PDA | system | прочее |
|---|---|---|---|---|---|
| `init` | ✓ payer | все 4 PDA `is_uninitialized` | деривация + сверка | ✓ | `dao_wallet` из `settings`, нет лишних аккаунтов |
| `update_coef_limit` | ✓ + `signer == config.dao_wallet` | config/coef `owner == id()` | деривация + сверка | — | границы coef/limit/reward; нет лишних аккаунтов |
| `grant_manager_limits` | ✓ + `signer == config.dao_wallet` | config `owner == id()`; allowance create/read | allowance из `manager_wallet` | ✓ | `state.manager_wallet == args.manager_wallet` |
| `buy_ticket` / `_usd` / `_sol` | ✓ | config/coef/queues `owner == id()` | ticket деривация + сверка + `is_uninitialized` | ✓ | oracle (key+owner+возраст+confidence), `dao_wallet == config.dao_wallet`, `recipient != inflow_vault`, slippage |
| `manager_add_ticket` | ✓ | allowance/queues `owner == id()` | allowance из `signer`; ticket деривация + сверка + uninit | ✓ | `allowance.manager_wallet == signer`, `queue_id ∈ {1,2,3}`, `recipient != inflow_vault` |
| `step_payout` | ✓ | все singleton-PDA `owner == id()` | ticket деривация + сверка | — | `dao_wallet == config.dao_wallet`, `inflow == config.inflow_vault`, ticket `queue/index/!is_paid/recipient`, oracle |
| `change_ticket_recipient` | ✓ + `signer == ticket.recipient_wallet` | queues + ticket `owner == id()` (через `read_state`) | ticket деривация из своих `queue_id/index` + сверка | — | `!is_paid`, запрет менять «следующий к выплате», `recipient != inflow_vault` |
### shine_login_guard
Аккаунты не используются (`_accounts`); программа stateless, средствами не владеет.
Защита со стороны вызова реализована в `shine_users`: сверяется и адрес вызываемой
программы (`login_guard_program.key == SHINE_LOGIN_GUARD_PROGRAM_ID`), и `program_id`
в `get_return_data`. Подмена/подделка ответа исключены. Отдельных проверок входных
аккаунтов внутри программы не требуется.
---
## 🟡 LOW / INFO — наблюдения без прямой эксплуатации
### L1. Permissionless `init` в обеих программах
`shine_payments::init` и `shine_users::init_users_economy_config` может вызвать кто
угодно первым. Практического эксплойта нет: все значения (включая `dao_wallet` и
`DAO_AUTHORITY`) берутся из констант `settings`, а не из ввода, повторная
инициализация заблокирована проверками `is_uninitialized` / `data_is_empty`. Риск
низкий; при желании привязать init к ожидаемому деплой-кошельку. Совпадает с моделью
«первый init = деплой».
### L2. В `shine_users` нет явной проверки «лишних аккаунтов» — ✅ ИСПРАВЛЕНО (12.06.2026)
`shine_payments` в каждом handler делает `require!(account_iter.next().is_none())`.
В `shine_users` такой проверки не было — лишние аккаунты в конце списка просто
игнорировались (читается строго нужное количество через `next_account_info`). Это
безвредно (на безопасность не влияло), но для симметрии и явности добавлено.
Класс: гигиена, не уязвимость.
Закрыто: во все 4 инструкции `shine_users` (`init_users_economy_config`,
`update_users_economy_config`, `create_user_pda`, `update_user_pda`) после чтения
фиксированного набора аккаунтов добавлено `require!(it.next().is_none(),
ShineUsersError::InvalidInstruction)`. Документация — `doc/programs/shine_users.md` §3.4.
### L3. Гонка за логином (first-come) в `shine_users` — known issue
Адрес `user_pda` детерминирован из логина; после закрытия griefing-подсева остаётся
обычное состязание за регистрацию (front-run в мемпуле). On-chain решается только
commit-reveal; для текущей модели — приемлемый риск, ранее зафиксирован в аудите №2
(L2). К проверкам аккаунтов не относится.
### L4. Экономическая устойчивость вольта (дизайн, не баг)
Деньги за покупку тикетов уходят на `dao_wallet`, а выплаты `step_payout` идут из
`inflow_vault`, наполняемого регистрационными комиссиями `shine_users` (коэффициент
по умолчанию `START_COEF_PPM = 5x`). При недостаточном притоке регистраций вольт
истощается и выплаты останавливаются (без потери средств). Это свойство
экономической модели «очередь/билеты», а не дефект валидации аккаунтов — отмечено
для полноты (ранее L4 в аудите №2). Мониторить баланс вольта vs обязательств.
---
## ✅ Проверено и подтверждено как корректное (по входным аккаунтам)
- **Подмена PDA** невозможна нигде: всюду пара «деривация `find_program_address` + сверка полного адреса». Пользовательский bump не принимается, `create_program_address` с внешним bump не используется — bump-seed атаки исключены.
- **Проверка владельца** при каждом чтении PDA: `read_state` и `validate_singleton_state_pda` (`shine_payments`) требуют `owner == id()`; `validate_users_economy_config_pda` и проверка `user_pda.owner == program_id` (`shine_users`) — перед десериализацией данных.
- **Создаваемые PDA**: проверка `is_uninitialized` / `owner == system && data_is_empty` исключает повторную инициализацию и перезапись чужого аккаунта.
- **signer / authority**: все handler начинают с обязательного `is_signer`; привилегированные операции дополнительно сверяют ключ с авторитетом (`config.dao_wallet`, `DAO_AUTHORITY`, `allowance.manager_wallet`, `ticket.recipient_wallet`, `device_key`).
- **system-program** сверяется с `system_program::ID` там, где идёт создание аккаунта/перевод; **sysvar инструкций** сверяется с `sysvar::instructions::id()` перед ed25519-интроспекцией.
- **Аккаунт оракула**: пин адреса `PYTH_SOL_USD_ACCOUNT` + `owner == pyth_receiver` + `feed_id` + возраст (120 с) + доверительный интервал (10%).
- **Ed25519 в `shine_users`**: относительные индексы 1/2, `num_signatures == 1`, все три `ix_index == u16::MAX` (offset-данные внутри самой ed25519-инструкции), сверка `program_id == ed25519_program` и pubkey/signature/message по хэшу — указать на чужую инструкцию нельзя.
- **Алиасинг аккаунтов**: `recipient != inflow_vault` запрещён на входе во всех точках задания получателя + `vault.key != recipient.key` в `transfer_from_vault`.
- **`inflow_vault` в `shine_users`** сверяется с PDA, выведенным из `SHINE_PAYMENTS_PROGRAM_ID` и `SHINE_PAYMENTS_INFLOW_VAULT_SEED` — комиссия не может уйти на чужой адрес.
- **Реентранси** отсутствует: CPI только в System Program и в stateless `shine_login_guard` (с проверкой возвращённого `program_id`); обратных вызовов в наши программы нет.
---
## Приоритет действий
1. **LOW** — ✅ выполнено 12.06.2026: добавлено `require!(it.next().is_none(), …)` во
все инструкции `shine_users` для симметрии с `shine_payments` (L2).
2. **INFO** — зафиксировать в эксплуатационной документации known-issue гонки за
логином (L3) и экономику вольта (L4); рассмотреть привязку `init` к ожидаемому
деплой-кошельку (L1).
Критичных и высоких находок по полноте проверок входных аккаунтов в этом проходе
нет. Единственная LOW-правка (L2) применена в рамках этого же изменения; код
`shine_users` собирается успешно (`cargo build -p shine_users`).

View File

@ -73,7 +73,19 @@ read_sol_usd_price / parse_pyth_price_update_v2 (строки 10381075):
Проверка возраста цены (ORACLE_MAX_AGE_SECS = 120) есть и сделана корректно. Рекомендация: проверять владельца аккаунта, сверять feed_id с константой и валидировать verification_level == Full (или парсить через официальный pyth_solana_receiver_sdk, который уже завендорен в .vendor/).
---
🟡 LOW — DoS через предсказуемые адреса тикетов
🟡 LOW — DoS через предсказуемые адреса тикетов — ✅ ИСПРАВЛЕНО (11.06.2026)
Закрыто: `create_pda_account` в `shine_payments` и `shine_users` переведён на паттерн
«создание поверх предзаполненного» (allocate + assign + добор ренты вместо строгого
`system_instruction::create_account`). «Подсев» лампортов на заранее известный адрес
тикета или пользовательской записи больше не блокирует создание PDA. Проверка
`is_uninitialized_account` в payments перестала зависеть от нулевого баланса. Тот же фикс
закрывает аналогичный сквоттинг логинов в `shine_users` (адрес выводится из логина).
Подробности — в `doc/programs/shine_payments.md` §3.4 и `doc/programs/shine_users.md` §3.3.
Историческое описание находки ниже.
is_uninitialized_account (строка 1195) считает аккаунт неинициализированным только если lamports() == 0. Адреса тикетов детерминированы (queue_seed + index), а индекс последователен и предсказуем. Любой может заранее перевести немного лампортов на адрес следующего тикета — тогда create_pda_account упадёт (PdaAlreadyExists / ошибка create_account), заблокировав покупку/добавление тикета. Это griefing-DoS, не кража. Митигировать можно паттерном «create поверх предзаполненного» (allocate + assign + добор ренты) вместо system_instruction::create_account.

View File

@ -0,0 +1,112 @@
# ESP Pairing и режимы подключения
Этот документ фиксирует текущие и новые режимы входа/подключения в SHiNE без клиентской UI-реализации. Он нужен как отдельная точка входа по сценариям подключения, чтобы не смешивать обычную авторизацию и серверный pairing через доверенное уже авторизованное устройство пользователя.
## 1. Текущие режимы
### 1. Создание новой сессии через `deviceKey`
Поток:
`AuthChallenge -> CreateAuthSession`
Смысл:
- новое устройство уже владеет приватным `deviceKey`;
- сервер проверяет подпись `deviceKey`;
- создаётся обычная активная сессия пользователя;
- этот поток остаётся без изменений.
### 2. Повторный вход в существующую сессию через `sessionKey`
Поток:
`SessionChallenge -> SessionLogin`
Смысл:
- устройство уже владеет приватным `sessionKey`;
- сервер проверяет подпись `sessionKey`;
- соединение снова входит в существующую сессию;
- этот поток тоже остаётся без изменений.
## 2. Новый режим: добавление сессии через доверенное устройство пользователя
Новый поток не заменяет обычный логин, а живёт рядом с ним.
Цель:
- новое устройство знает `login`, а `pairing password` используется только если он включён на доверённом устройстве;
- сервер использует пароль только как фильтр от мусора;
- реальное доверие даёт любая уже онлайн доверенная сессия пользователя;
- сервер не выдаёт приватные ключи сам от себя.
Поток версии `v1`:
1. Любая доверенная сессия пользователя создаёт на сервере pairing-настройку:
`UpsertEspPairingSettings`
2. Новое устройство создаёт pending-заявку:
`StartEspPairing`
3. Онлайн доверенная сессия видит список активных заявок:
`ListEspPairingRequests`
4. Доверенная сессия либо подтверждает заявку:
`ApproveEspPairing`
5. Либо отклоняет:
`RejectEspPairing`
6. Новое устройство читает результат:
`GetEspPairingStatus`
## 3. Что именно делает сервер
- хранит включённость pairing и optional `passwordHash` в формате `sha256$<hex>`;
- хранит pairing-заявки всех статусов, но в список активных для доверённого устройства отдаёт только pending `created`;
- рассчитывает короткий код `shortCode` из `7` цифр;
- рассчитывает длинный `fingerprintB58` из `SHA-256` заявки;
- уведомляет онлайн доверенные сессии событием `IncomingEspPairingRequest`, если такие сессии подключены;
- хранит переданный `encryptedPayload` как непрозрачную строку и не анализирует его содержимое.
## 4. Чего сервер в этой версии не делает
- не передаёт приватный `deviceKey`;
- не расшифровывает `encryptedPayload`;
- не проверяет криптографию содержимого payload;
- не делает клиентский UI;
- не навязывает конкретную схему `Ed25519 -> X25519` в коде сервера.
Это намеренно: серверная версия `v1` подготавливает безопасный каркас маршрутизации и состояния, а настоящая E2E-логика упаковки ключей будет жить на клиентах и ESP-устройствах.
## 5. Роли и ограничения
- любая уже авторизованная доверенная сессия пользователя может вызывать:
- `UpsertEspPairingSettings`
- `ListEspPairingRequests`
- `ApproveEspPairing`
- `RejectEspPairing`
- новое устройство может вызвать `StartEspPairing` и `GetEspPairingStatus` без уже существующей авторизованной сессии;
- `payloadType` поддерживается в вариантах:
- `1` — минимальный пакет
- `2` — расширенный пакет
- `3` — полный пакет
Сервер не интерпретирует эти три типа глубже, а только фиксирует их в состоянии заявки.
## 6. Статусы pairing-заявки
- `created` — заявка создана и ждёт решения доверенной сессии;
- `approved` — доверенная сессия подтвердила и приложила `encryptedPayload`;
- `rejected` — доверенная сессия отклонила заявку;
- `expired` — TTL заявки истёк до подтверждения.
## 7. Практический смысл
Эта схема даёт нужное разделение доверия:
- пароль на сервере, если он включён, только отсеивает лишних;
- онлайн доверенная сессия решает, добавлять ли новую сессию;
- сервер остаётся маршрутизатором и хранилищем состояния, а не владельцем секретов.
Текущий формат pairing-пароля:
```text
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
```

View File

@ -34,7 +34,7 @@ ls -l /dev/ttyACM0
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
- `original-firmware/` — backup/restore заводской прошивки
- `test-device/` — прошивки и `burn.sh`
- `main-device/` — прошивки и `burn.sh`
- `reference/` — заметки и ссылки
## 4) Бэкап перед любыми экспериментами
@ -59,7 +59,7 @@ cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
Главный скрипт:
```bash
cd ESP32-S3-Touch-AMOLED-2.16/test-device
cd ESP32-S3-Touch-AMOLED-2.16/main-device
./burn.sh <mode>
```

View File

@ -34,7 +34,7 @@ ls -l /dev/ttyACM0
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
- `original-firmware/` — backup/restore заводской прошивки
- `test-device/` — прошивки и `burn.sh`
- `main-device/` — прошивки и `burn.sh`
- `reference/` — заметки и ссылки
## 4) Бэкап перед любыми экспериментами
@ -59,7 +59,7 @@ cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
Главный скрипт:
```bash
cd ESP32-S3-Touch-AMOLED-2.16/test-device
cd ESP32-S3-Touch-AMOLED-2.16/main-device
./burn.sh <mode>
```

View File

@ -6,8 +6,9 @@
- `official-demo/` — официальный репозиторий примеров Waveshare
- `original-firmware/` — резервная копия заводской прошивки
- `test-device/` — скрипты быстрой проверки устройства
- `main-device/` — скрипты быстрой проверки устройства и основной скетч `shine_homeserver_main/`
- `reference/` — локальные заметки по документации и железу
- `main-device/shine_homeserver_main/` — основной рабочий скетч ESP32-проекта `SHiNE`
Примечание по git:
@ -20,6 +21,8 @@
1. Сделать backup текущей прошивки:
- `cd original-firmware && ./backup_factory.sh`
2. Залить тест экрана/тача:
- `cd ../test-device && ./burn.sh widgets`
- `cd ../main-device && ./burn.sh widgets`
3. Залить тест динамика:
- `cd ../test-device && ./burn.sh audio`
- `cd ../main-device && ./burn.sh audio`
4. Залить основной UI:
- `cd ../main-device && ./burn.sh shine-homeserver-main`

View File

@ -1,6 +1,6 @@
# Test Device
# Main Device
Скрипт заливает официальные Arduino-примеры для быстрой проверки платы.
Основной скетч homeserver и старые тестовые скетчи для быстрой проверки платы.
`burn.sh` теперь:
- сам пытается найти USB-порт ESP32;
- сначала делает быструю инкрементальную сборку;
@ -14,7 +14,10 @@
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости)
- `subserver-ui` — основной UI-прототип сабсервера SHiNE: NVS, PIN, Wi-Fi, серверы, кошелёк, QR, запросы
- `homeserver-ui` — совместимый алиас, указывает на `shine_homeserver_main/`
- `shine-homeserver-main` — основной скетч проекта `SHiNE` для ESP32, текущая рабочая версия UI
- `shine-homeserver-ui-main` — старое имя основного скетча, оставлено как совместимый алиас
- `legacy-homeserver-ui` — старый UI-прототип `shine_homeserver_ui/`, оставлен как тестовый и не является основным
- `text-test` — диагностический экран рендера текста: default font, U8g2 ASCII, U8g2 кириллица, кнопки с подписями
- `gfx-text-test` — тот же тест рендера текста, но уже внутри новой папки `test_sketches/`
- `gfx-layout-test` — тест геометрии и нижних рядов кнопок
@ -22,9 +25,9 @@
- `lvgl-interaction-test` — экран на `LVGL` с большим числом кнопок и сообщением о нажатой кнопке
- `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL`
- `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации
- `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_subserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
- `lvgl-subserver-touch-test`старый гибридный тест: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из старого `shine_homeserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
- `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы
- `lvgl-nav-minimal-test`новый минимальный UI-каркас сабсервера: `HOME`, `SETTINGS_MENU`, `Wi-Fi`, `Server`, `Account`, свайпы, крупные кнопки и реальная настройка Wi-Fi с сохранением в NVS
- `lvgl-nav-minimal-test`старое имя основного скетча, теперь ведёт на `shine_homeserver_main/` для совместимости
Запуск:
@ -32,7 +35,10 @@
- `./burn.sh audio`
- `./burn.sh hello`
- `./burn.sh simple`
- `./burn.sh subserver-ui`
- `./burn.sh homeserver-ui`
- `./burn.sh shine-homeserver-main`
- `./burn.sh shine-homeserver-ui-main`
- `./burn.sh legacy-homeserver-ui`
- `./burn.sh text-test`
- `./burn.sh gfx-text-test`
- `./burn.sh gfx-layout-test`
@ -43,4 +49,4 @@
- `./burn.sh lvgl-subserver-touch-test`
- `./burn.sh lvgl-russian-font-test`
- `./burn.sh lvgl-nav-minimal-test`
- `./flash_shine_subserver_ui.sh` - автоматически находит USB-порт и заливает `shine_subserver_ui`
- `./flash_shine_homeserver_main.sh` - автоматически находит USB-порт и заливает `shine_homeserver_main`

View File

@ -34,7 +34,10 @@ case "${MODE}" in
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;;
subserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_subserver_ui" ;;
homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
shine-homeserver-main) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
shine-homeserver-ui-main) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
legacy-homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_ui" ;;
text-test) SKETCH_DIR="${ROOT_DIR}/text_render_test" ;;
gfx-text-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_text_render_test" ;;
gfx-layout-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_button_layout_test" ;;
@ -44,10 +47,10 @@ case "${MODE}" in
lvgl-official-based-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_official_based_test" ;;
lvgl-subserver-touch-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_subserver_touch_test" ;;
lvgl-russian-font-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_russian_font_test" ;;
lvgl-nav-minimal-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_nav_minimal_test" ;;
lvgl-nav-minimal-test) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
*)
echo "Unknown mode: ${MODE}" >&2
echo "Use one of: hello, widgets, audio, simple, argon2, subserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test, lvgl-nav-minimal-test" >&2
echo "Use one of: hello, widgets, audio, simple, argon2, homeserver-ui, shine-homeserver-main, shine-homeserver-ui-main, legacy-homeserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test" >&2
exit 2
;;
esac

View File

@ -43,9 +43,9 @@ fi
if [[ -z "${PORT}" ]]; then
echo "Не удалось автоматически найти USB-порт ESP32." >&2
echo "Подключите плату и проверьте 'arduino-cli board list'." >&2
echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_subserver_ui.sh" >&2
echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_homeserver_main.sh" >&2
exit 1
fi
echo "== Найден порт: ${PORT}"
PORT="${PORT}" "${ROOT_DIR}/burn.sh" subserver-ui
PORT="${PORT}" "${ROOT_DIR}/burn.sh" shine-homeserver-main

View File

@ -0,0 +1,14 @@
# SHiNE Homeserver UI Main
Это основной рабочий скетч ESP32-проекта `SHiNE`.
Текущая каноническая точка запуска:
- `./burn.sh shine-homeserver-main`
- `./burn.sh homeserver-ui`
Историческое имя этого скетча:
- `lvgl-nav-minimal-test`
Прежние тестовые варианты для этой платы остаются в `main-device/test_sketches/` и должны восприниматься как старые диагностические сборки, а не как основной UI.

View File

@ -0,0 +1,6 @@
# SHiNE Homeserver UI Legacy
Это старый тестовый вариант UI для ESP32-платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Не использовать как основной скетч проекта.
Основной рабочий скетч сейчас лежит в `../shine_homeserver_main/`.

View File

@ -97,7 +97,7 @@ enum ActionId {
ACT_VERIFY_SERVERS,
ACT_SET_TEST_SERVERS,
ACT_EDIT_LOGIN,
ACT_EDIT_SUBSERVER,
ACT_EDIT_HOMESERVER,
ACT_GENERATE_SECRET,
ACT_CLEAR_ACCOUNT,
ACT_SHOW_QR,
@ -137,7 +137,7 @@ enum EditTarget {
EDIT_SSID,
EDIT_WIFI_PASSWORD,
EDIT_LOGIN,
EDIT_SUBSERVER,
EDIT_HOMESERVER,
EDIT_API,
EDIT_RPC,
EDIT_WS,
@ -174,7 +174,7 @@ struct AppData {
String wifiSsid;
String wifiPassword;
String login;
String subserverName;
String homeserverName;
String secret;
String walletAddress;
String userPdaAddress;
@ -551,7 +551,7 @@ static bool canRegister() {
static String registrationSummary() {
if (gData.registered) {
return "Сабсервер активен";
return "Homeserver активен";
}
if (!gData.wifiReady) {
return "Нужен Wi-Fi";
@ -1179,7 +1179,7 @@ static bool awaitTransactionConfirmation(const String &signatureB58, String &mes
return false;
}
static bool registerSubserverOnSolana(String &messageOut) {
static bool registerHomeserverOnSolana(String &messageOut) {
messageOut = "";
if (!gDerivedKeys.ready) {
if (!restoreDerivedKeysFromSecret()) {
@ -1656,7 +1656,7 @@ static bool refreshWalletBalance(String &messageOut) {
static void seedRequests() {
gRequests[0].type = "Вход в сессию";
gRequests[0].actor = "Chrome / aidarkc";
gRequests[0].details = "Клиент просит подключиться к сабсерверу и открыть сессию без ввода пароля.";
gRequests[0].details = "Клиент просит подключиться к homeserverу и открыть сессию без ввода пароля.";
gRequests[0].status = "Ожидает";
gRequests[1].type = "Подпись сообщения";
@ -1670,7 +1670,7 @@ static void loadDefaults() {
gData.wifiSsid = "";
gData.wifiPassword = "";
gData.login = "";
gData.subserverName = "subserver1";
gData.homeserverName = "homeserver1";
gData.secret = "";
gData.walletAddress = "";
gData.userPdaAddress = "";
@ -1692,7 +1692,7 @@ static void saveData() {
gPrefs.putString("wifi_ssid", gData.wifiSsid);
gPrefs.putString("wifi_pass", gData.wifiPassword);
gPrefs.putString("login", gData.login);
gPrefs.putString("subserver", gData.subserverName);
gPrefs.putString("homeserver", gData.homeserverName);
gPrefs.putString("secret", gData.secret);
gPrefs.putString("wallet", gData.walletAddress);
gPrefs.putString("user_pda", gData.userPdaAddress);
@ -1714,7 +1714,7 @@ static void loadData() {
gData.wifiSsid = gPrefs.getString("wifi_ssid", gData.wifiSsid);
gData.wifiPassword = gPrefs.getString("wifi_pass", gData.wifiPassword);
gData.login = gPrefs.getString("login", gData.login);
gData.subserverName = gPrefs.getString("subserver", gData.subserverName);
gData.homeserverName = gPrefs.getString("homeserver", gData.homeserverName);
gData.secret = gPrefs.getString("secret", gData.secret);
gData.walletAddress = gPrefs.getString("wallet", gData.walletAddress);
gData.userPdaAddress = gPrefs.getString("user_pda", gData.userPdaAddress);
@ -1758,8 +1758,8 @@ static void generateSecretAndWallet() {
gData.registrationSignature = "";
gData.registered = false;
gData.online = false;
if (gData.subserverName.length() == 0) {
gData.subserverName = "subserver1";
if (gData.homeserverName.length() == 0) {
gData.homeserverName = "homeserver1";
}
saveData();
}
@ -1815,7 +1815,7 @@ static String editTargetLabel() {
case EDIT_SSID: return "SSID";
case EDIT_WIFI_PASSWORD: return "Пароль Wi-Fi";
case EDIT_LOGIN: return "Логин";
case EDIT_SUBSERVER: return "Имя сабсервера";
case EDIT_HOMESERVER: return "Имя homeserver";
case EDIT_API: return "API URL";
case EDIT_RPC: return "RPC URL";
case EDIT_WS: return "WS URL";
@ -1846,7 +1846,7 @@ static void drawHomeScreen() {
drawPanel(20, 92, 440, 98, C_PANEL, C_BORDER, 16);
drawText(36, 122, registrationSummary(), canRegister() || gData.registered ? C_ACCENT : C_WARN, (const uint8_t *)FONT_HEAD);
drawText(36, 152, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT, (const uint8_t *)FONT_BODY);
drawText(36, 174, "Сабсервер: " + gData.subserverName, C_MUTE, (const uint8_t *)FONT_BODY);
drawText(36, 174, "Homeserver: " + gData.homeserverName, C_MUTE, (const uint8_t *)FONT_BODY);
drawPanel(20, 204, 210, 82, C_CARD, C_BORDER, 12);
drawText(34, 232, "Wi-Fi", C_TEXT, (const uint8_t *)FONT_BODY);
@ -1871,7 +1871,7 @@ static void drawStatusScreen() {
drawTopBar("Статус");
drawPanel(20, 92, 440, 286, C_PANEL, C_BORDER, 16);
drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT);
drawText(36, 148, "Сабсервер: " + gData.subserverName, C_TEXT);
drawText(36, 148, "Homeserver: " + gData.homeserverName, C_TEXT);
drawText(36, 174, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN);
drawText(36, 200, "Отпечаток: " + (gData.secretReady ? shortenValue(gData.secret) : "-"), C_MUTE, (const uint8_t *)FONT_SMALL);
drawText(36, 226, "Wi-Fi: " + boolText(gData.wifiReady, "готов", "не готов"), gData.wifiReady ? C_ACCENT : C_WARN);
@ -1947,13 +1947,13 @@ static void drawAccountScreen() {
drawTopBar("Аккаунт");
drawPanel(20, 92, 440, 188, C_PANEL, C_BORDER, 16);
drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT);
drawText(36, 152, "Сабсервер: " + gData.subserverName, C_TEXT);
drawText(36, 152, "Homeserver: " + gData.homeserverName, C_TEXT);
drawText(36, 182, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN);
drawText(36, 212, "Кошелёк: " + (gData.walletAddress.length() ? shortenValue(gData.walletAddress, 10, 8) : "не создан"), C_MUTE, (const uint8_t *)FONT_SMALL);
drawText(36, 236, "Регистрация: " + boolText(gData.registered, "выполнена", "не выполнена"), gData.registered ? C_ACCENT : C_WARN);
drawText(36, 260, "PDA: " + registrationDetailsShort(), C_MUTE, (const uint8_t *)FONT_SMALL);
addButton(20, 300, 212, 48, ACT_EDIT_LOGIN, "Изменить логин");
addButton(248, 300, 212, 48, ACT_EDIT_SUBSERVER, "Имя сабсервера");
addButton(248, 300, 212, 48, ACT_EDIT_HOMESERVER, "Имя homeserver");
addButton(20, 360, 212, 48, ACT_GENERATE_SECRET, "Сгенерировать", true, C_OK);
addButton(248, 360, 212, 48, ACT_CLEAR_ACCOUNT, "Очистить", true, C_BUTTON2);
addButton(20, 420, 440, 36, ACT_BACK, "Назад");
@ -2193,9 +2193,9 @@ static void applyEditValue() {
gData.registrationSignature = "";
gNotice = "Логин сохранён";
break;
case EDIT_SUBSERVER:
gData.subserverName = value.length() ? value : "subserver1";
gNotice = "Имя сабсервера сохранено";
case EDIT_HOMESERVER:
gData.homeserverName = value.length() ? value : "homeserver1";
gNotice = "Имя homeserver сохранено";
break;
case EDIT_API:
gData.apiUrl = value;
@ -2351,7 +2351,7 @@ static void handleAction(ActionId action) {
}
if (action == ACT_CONFIRM_YES) {
if (gConfirmTarget == CONFIRM_REGISTER) {
registerSubserverOnSolana(gNotice);
registerHomeserverOnSolana(gNotice);
} else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
gData.secret = "";
gData.walletAddress = "";
@ -2445,7 +2445,7 @@ static void handleAction(ActionId action) {
gNeedRedraw = true;
break;
case ACT_EDIT_LOGIN: openEdit(EDIT_LOGIN, gData.login, false); break;
case ACT_EDIT_SUBSERVER: openEdit(EDIT_SUBSERVER, gData.subserverName, false); break;
case ACT_EDIT_HOMESERVER: openEdit(EDIT_HOMESERVER, gData.homeserverName, false); break;
case ACT_GENERATE_SECRET:
generateSecretAndWallet();
gNotice = gData.secretReady ? "Секрет сгенерирован, device-кошелёк выведен из него" : "Не удалось сгенерировать секрет";

View File

@ -1,8 +1,9 @@
# Test Sketches
Набор отдельных диагностических скетчей для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Набор старых отдельных диагностических скетчей для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Скетчи в этой папке нужны для быстрой проверки конкретных гипотез без влияния на основной `shine_subserver_ui`.
Скетчи в этой папке нужны для быстрой проверки конкретных гипотез и не являются основным UI проекта.
Основной скетч сейчас лежит в `main-device/shine_homeserver_main/`.
## Список
@ -12,9 +13,9 @@
- `lvgl_interaction_test/` - расширенный тест `LVGL` с 9 кнопками, touch-вводом и статусом нажатия
- `lvgl_touch_debug_test/` - диагностика touch: сырые координаты, точка касания и одна большая кнопка `LVGL`
- `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации
- `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве
- `lvgl_subserver_touch_test/` - старый гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из старого `shine_homeserver_ui`; подтверждён на реальном устройстве
- `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch
- `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch`, расширенный настройкой Wi-Fi и сохранением в NVS
- `lvgl_nav_minimal_test/` - старое тестовое имя, этот скетч перенесён в `shine_homeserver_main/` и теперь является основным
## Запуск
@ -28,4 +29,3 @@
- `./burn.sh lvgl-official-based-test`
- `./burn.sh lvgl-subserver-touch-test`
- `./burn.sh lvgl-russian-font-test`
- `./burn.sh lvgl-nav-minimal-test`

View File

@ -4,7 +4,7 @@
#include <Arduino_GFX_Library.h>
#include <TouchDrvCSTXXX.hpp>
// Подтверждено на устройстве: LVGL-рендер работает вместе с touch-путём из shine_subserver_ui.
// Подтверждено на устройстве: LVGL-рендер работает вместе с touch-путём из shine_homeserver_ui.
#define PIN_LCD_CS 12
#define PIN_LCD_SCLK 38
@ -146,7 +146,7 @@ static void createUi() {
lv_obj_align(gVersionLabel, LV_ALIGN_TOP_MID, 0, 12);
lv_obj_t *subtitle = lv_label_create(screen);
lv_label_set_text(subtitle, "Touch path comes from shine_subserver_ui. Tap buttons and watch status.");
lv_label_set_text(subtitle, "Touch path comes from shine_homeserver_ui. Tap buttons and watch status.");
lv_obj_set_width(subtitle, 436);
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0);

View File

@ -0,0 +1,784 @@
/**
* @file lv_conf.h
* Configuration file for v8.4.0
*/
/*
* Copy this file as `lv_conf.h`
* 1. simply next to the `lvgl` folder
* 2. or any other places and
* - define `LV_CONF_INCLUDE_SIMPLE`
* - add the path as include path
*/
/* clang-format off */
#if 1 /*Set it to "1" to enable content*/
#ifndef LV_CONF_H
#define LV_CONF_H
#include <stdint.h>
/*====================
COLOR SETTINGS
*====================*/
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 0
/*Enable features to draw on transparent background.
*It's required if opa, and transform_* style properties are used.
*Can be also used if the UI is above another layer, e.g. an OSD menu or video player.*/
#define LV_COLOR_SCREEN_TRANSP 0
/* Adjust color mix functions rounding. GPUs might calculate color mix (blending) differently.
* 0: round down, 64: round up from x.75, 128: round up from half, 192: round up from x.25, 254: round up */
#define LV_COLOR_MIX_ROUND_OFS 0
/*Images pixels with this color will not be drawn if they are chroma keyed)*/
#define LV_COLOR_CHROMA_KEY lv_color_hex(0x00ff00) /*pure green*/
/*=========================
MEMORY SETTINGS
*=========================*/
/*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/
#define LV_MEM_CUSTOM 0
#if LV_MEM_CUSTOM == 0
/*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/
#define LV_MEM_SIZE (48U * 1024U) /*[bytes]*/
/*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/
#define LV_MEM_ADR 0 /*0: unused*/
/*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/
#if LV_MEM_ADR == 0
#undef LV_MEM_POOL_INCLUDE
#undef LV_MEM_POOL_ALLOC
#endif
#else /*LV_MEM_CUSTOM*/
#define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /*Header for the dynamic memory function*/
#define LV_MEM_CUSTOM_ALLOC malloc
#define LV_MEM_CUSTOM_FREE free
#define LV_MEM_CUSTOM_REALLOC realloc
#endif /*LV_MEM_CUSTOM*/
/*Number of the intermediate memory buffer used during rendering and other internal processing mechanisms.
*You will see an error log message if there wasn't enough buffers. */
#define LV_MEM_BUF_MAX_NUM 16
/*Use the standard `memcpy` and `memset` instead of LVGL's own functions. (Might or might not be faster).*/
#define LV_MEMCPY_MEMSET_STD 0
/*====================
HAL SETTINGS
*====================*/
/*Default display refresh period. LVG will redraw changed areas with this period time*/
#define LV_DISP_DEF_REFR_PERIOD 10 /*[ms]*/
/*Input device read period in milliseconds*/
#define LV_INDEV_DEF_READ_PERIOD 10 /*[ms]*/
/*Use a custom tick source that tells the elapsed time in milliseconds.
*It removes the need to manually update the tick with `lv_tick_inc()`)*/
#define LV_TICK_CUSTOM 0
#if LV_TICK_CUSTOM
#define LV_TICK_CUSTOM_INCLUDE "Arduino.h" /*Header for the system time function*/
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (millis()) /*Expression evaluating to current system time in ms*/
/*If using lvgl as ESP32 component*/
// #define LV_TICK_CUSTOM_INCLUDE "esp_timer.h"
// #define LV_TICK_CUSTOM_SYS_TIME_EXPR ((esp_timer_get_time() / 1000LL))
#endif /*LV_TICK_CUSTOM*/
/*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings.
*(Not so important, you can adjust it to modify default sizes and spaces)*/
#define LV_DPI_DEF 130 /*[px/inch]*/
/*=======================
* FEATURE CONFIGURATION
*=======================*/
/*-------------
* Drawing
*-----------*/
/*Enable complex draw engine.
*Required to draw shadow, gradient, rounded corners, circles, arc, skew lines, image transformations or any masks*/
#define LV_DRAW_COMPLEX 1
#if LV_DRAW_COMPLEX != 0
/*Allow buffering some shadow calculation.
*LV_SHADOW_CACHE_SIZE is the max. shadow size to buffer, where shadow size is `shadow_width + radius`
*Caching has LV_SHADOW_CACHE_SIZE^2 RAM cost*/
#define LV_SHADOW_CACHE_SIZE 0
/* Set number of maximally cached circle data.
* The circumference of 1/4 circle are saved for anti-aliasing
* radius * 4 bytes are used per circle (the most often used radiuses are saved)
* 0: to disable caching */
#define LV_CIRCLE_CACHE_SIZE 4
#endif /*LV_DRAW_COMPLEX*/
/**
* "Simple layers" are used when a widget has `style_opa < 255` to buffer the widget into a layer
* and blend it as an image with the given opacity.
* Note that `bg_opa`, `text_opa` etc don't require buffering into layer)
* The widget can be buffered in smaller chunks to avoid using large buffers.
*
* - LV_LAYER_SIMPLE_BUF_SIZE: [bytes] the optimal target buffer size. LVGL will try to allocate it
* - LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE: [bytes] used if `LV_LAYER_SIMPLE_BUF_SIZE` couldn't be allocated.
*
* Both buffer sizes are in bytes.
* "Transformed layers" (where transform_angle/zoom properties are used) use larger buffers
* and can't be drawn in chunks. So these settings affects only widgets with opacity.
*/
#define LV_LAYER_SIMPLE_BUF_SIZE (24 * 1024)
#define LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE (3 * 1024)
/*Default image cache size. Image caching keeps the images opened.
*If only the built-in image formats are used there is no real advantage of caching. (I.e. if no new image decoder is added)
*With complex image decoders (e.g. PNG or JPG) caching can save the continuous open/decode of images.
*However the opened images might consume additional RAM.
*0: to disable caching*/
#define LV_IMG_CACHE_DEF_SIZE 0
/*Number of stops allowed per gradient. Increase this to allow more stops.
*This adds (sizeof(lv_color_t) + 1) bytes per additional stop*/
#define LV_GRADIENT_MAX_STOPS 2
/*Default gradient buffer size.
*When LVGL calculates the gradient "maps" it can save them into a cache to avoid calculating them again.
*LV_GRAD_CACHE_DEF_SIZE sets the size of this cache in bytes.
*If the cache is too small the map will be allocated only while it's required for the drawing.
*0 mean no caching.*/
#define LV_GRAD_CACHE_DEF_SIZE 0
/*Allow dithering the gradients (to achieve visual smooth color gradients on limited color depth display)
*LV_DITHER_GRADIENT implies allocating one or two more lines of the object's rendering surface
*The increase in memory consumption is (32 bits * object width) plus 24 bits * object width if using error diffusion */
#define LV_DITHER_GRADIENT 0
#if LV_DITHER_GRADIENT
/*Add support for error diffusion dithering.
*Error diffusion dithering gets a much better visual result, but implies more CPU consumption and memory when drawing.
*The increase in memory consumption is (24 bits * object's width)*/
#define LV_DITHER_ERROR_DIFFUSION 0
#endif
/*Maximum buffer size to allocate for rotation.
*Only used if software rotation is enabled in the display driver.*/
#define LV_DISP_ROT_MAX_BUF (10*1024)
/*-------------
* GPU
*-----------*/
/*Use Arm's 2D acceleration library Arm-2D */
#define LV_USE_GPU_ARM2D 0
/*Use STM32's DMA2D (aka Chrom Art) GPU*/
#define LV_USE_GPU_STM32_DMA2D 0
#if LV_USE_GPU_STM32_DMA2D
/*Must be defined to include path of CMSIS header of target processor
e.g. "stm32f7xx.h" or "stm32f4xx.h"*/
#define LV_GPU_DMA2D_CMSIS_INCLUDE
#endif
/*Enable RA6M3 G2D GPU*/
#define LV_USE_GPU_RA6M3_G2D 0
#if LV_USE_GPU_RA6M3_G2D
/*include path of target processor
e.g. "hal_data.h"*/
#define LV_GPU_RA6M3_G2D_INCLUDE "hal_data.h"
#endif
/*Use SWM341's DMA2D GPU*/
#define LV_USE_GPU_SWM341_DMA2D 0
#if LV_USE_GPU_SWM341_DMA2D
#define LV_GPU_SWM341_DMA2D_INCLUDE "SWM341.h"
#endif
/*Use NXP's PXP GPU iMX RTxxx platforms*/
#define LV_USE_GPU_NXP_PXP 0
#if LV_USE_GPU_NXP_PXP
/*1: Add default bare metal and FreeRTOS interrupt handling routines for PXP (lv_gpu_nxp_pxp_osa.c)
* and call lv_gpu_nxp_pxp_init() automatically during lv_init(). Note that symbol SDK_OS_FREE_RTOS
* has to be defined in order to use FreeRTOS OSA, otherwise bare-metal implementation is selected.
*0: lv_gpu_nxp_pxp_init() has to be called manually before lv_init()
*/
#define LV_USE_GPU_NXP_PXP_AUTO_INIT 0
#endif
/*Use NXP's VG-Lite GPU iMX RTxxx platforms*/
#define LV_USE_GPU_NXP_VG_LITE 0
/*Use SDL renderer API*/
#define LV_USE_GPU_SDL 0
#if LV_USE_GPU_SDL
#define LV_GPU_SDL_INCLUDE_PATH <SDL2/SDL.h>
/*Texture cache size, 8MB by default*/
#define LV_GPU_SDL_LRU_SIZE (1024 * 1024 * 8)
/*Custom blend mode for mask drawing, disable if you need to link with older SDL2 lib*/
#define LV_GPU_SDL_CUSTOM_BLEND_MODE (SDL_VERSION_ATLEAST(2, 0, 6))
#endif
/*-------------
* Logging
*-----------*/
/*Enable the log module*/
#define LV_USE_LOG 0
#if LV_USE_LOG
/*How important log should be added:
*LV_LOG_LEVEL_TRACE A lot of logs to give detailed information
*LV_LOG_LEVEL_INFO Log important events
*LV_LOG_LEVEL_WARN Log if something unwanted happened but didn't cause a problem
*LV_LOG_LEVEL_ERROR Only critical issue, when the system may fail
*LV_LOG_LEVEL_USER Only logs added by the user
*LV_LOG_LEVEL_NONE Do not log anything*/
#define LV_LOG_LEVEL LV_LOG_LEVEL_WARN
/*1: Print the log with 'printf';
*0: User need to register a callback with `lv_log_register_print_cb()`*/
#define LV_LOG_PRINTF 0
/*Enable/disable LV_LOG_TRACE in modules that produces a huge number of logs*/
#define LV_LOG_TRACE_MEM 1
#define LV_LOG_TRACE_TIMER 1
#define LV_LOG_TRACE_INDEV 1
#define LV_LOG_TRACE_DISP_REFR 1
#define LV_LOG_TRACE_EVENT 1
#define LV_LOG_TRACE_OBJ_CREATE 1
#define LV_LOG_TRACE_LAYOUT 1
#define LV_LOG_TRACE_ANIM 1
#endif /*LV_USE_LOG*/
/*-------------
* Asserts
*-----------*/
/*Enable asserts if an operation is failed or an invalid data is found.
*If LV_USE_LOG is enabled an error message will be printed on failure*/
#define LV_USE_ASSERT_NULL 1 /*Check if the parameter is NULL. (Very fast, recommended)*/
#define LV_USE_ASSERT_MALLOC 1 /*Checks is the memory is successfully allocated or no. (Very fast, recommended)*/
#define LV_USE_ASSERT_STYLE 0 /*Check if the styles are properly initialized. (Very fast, recommended)*/
#define LV_USE_ASSERT_MEM_INTEGRITY 0 /*Check the integrity of `lv_mem` after critical operations. (Slow)*/
#define LV_USE_ASSERT_OBJ 0 /*Check the object's type and existence (e.g. not deleted). (Slow)*/
/*Add a custom handler when assert happens e.g. to restart the MCU*/
#define LV_ASSERT_HANDLER_INCLUDE <stdint.h>
#define LV_ASSERT_HANDLER while(1); /*Halt by default*/
/*-------------
* Others
*-----------*/
/*1: Show CPU usage and FPS count*/
#define LV_USE_PERF_MONITOR 0
#if LV_USE_PERF_MONITOR
#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
#endif
/*1: Show the used memory and the memory fragmentation
* Requires LV_MEM_CUSTOM = 0*/
#define LV_USE_MEM_MONITOR 0
#if LV_USE_MEM_MONITOR
#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
#endif
/*1: Draw random colored rectangles over the redrawn areas*/
#define LV_USE_REFR_DEBUG 0
/*Change the built in (v)snprintf functions*/
#define LV_SPRINTF_CUSTOM 0
#if LV_SPRINTF_CUSTOM
#define LV_SPRINTF_INCLUDE <stdio.h>
#define lv_snprintf snprintf
#define lv_vsnprintf vsnprintf
#else /*LV_SPRINTF_CUSTOM*/
#define LV_SPRINTF_USE_FLOAT 0
#endif /*LV_SPRINTF_CUSTOM*/
#define LV_USE_USER_DATA 1
/*Garbage Collector settings
*Used if lvgl is bound to higher level language and the memory is managed by that language*/
#define LV_ENABLE_GC 0
#if LV_ENABLE_GC != 0
#define LV_GC_INCLUDE "gc.h" /*Include Garbage Collector related things*/
#endif /*LV_ENABLE_GC*/
/*=====================
* COMPILER SETTINGS
*====================*/
/*For big endian systems set to 1*/
#define LV_BIG_ENDIAN_SYSTEM 0
/*Define a custom attribute to `lv_tick_inc` function*/
#define LV_ATTRIBUTE_TICK_INC
/*Define a custom attribute to `lv_timer_handler` function*/
#define LV_ATTRIBUTE_TIMER_HANDLER
/*Define a custom attribute to `lv_disp_flush_ready` function*/
#define LV_ATTRIBUTE_FLUSH_READY
/*Required alignment size for buffers*/
#define LV_ATTRIBUTE_MEM_ALIGN_SIZE 1
/*Will be added where memories needs to be aligned (with -Os data might not be aligned to boundary by default).
* E.g. __attribute__((aligned(4)))*/
#define LV_ATTRIBUTE_MEM_ALIGN
/*Attribute to mark large constant arrays for example font's bitmaps*/
#define LV_ATTRIBUTE_LARGE_CONST
/*Compiler prefix for a big array declaration in RAM*/
#define LV_ATTRIBUTE_LARGE_RAM_ARRAY
/*Place performance critical functions into a faster memory (e.g RAM)*/
#define LV_ATTRIBUTE_FAST_MEM
/*Prefix variables that are used in GPU accelerated operations, often these need to be placed in RAM sections that are DMA accessible*/
#define LV_ATTRIBUTE_DMA
/*Export integer constant to binding. This macro is used with constants in the form of LV_<CONST> that
*should also appear on LVGL binding API such as Micropython.*/
#define LV_EXPORT_CONST_INT(int_value) struct _silence_gcc_warning /*The default value just prevents GCC warning*/
/*Extend the default -32k..32k coordinate range to -4M..4M by using int32_t for coordinates instead of int16_t*/
#define LV_USE_LARGE_COORD 0
/*==================
* FONT USAGE
*===================*/
/*Montserrat fonts with ASCII range and some symbols using bpp = 4
*https://fonts.google.com/specimen/Montserrat*/
#define LV_FONT_MONTSERRAT_8 1
#define LV_FONT_MONTSERRAT_10 1
#define LV_FONT_MONTSERRAT_12 1
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 1
#define LV_FONT_MONTSERRAT_18 1
#define LV_FONT_MONTSERRAT_20 1
#define LV_FONT_MONTSERRAT_22 1
#define LV_FONT_MONTSERRAT_24 1
#define LV_FONT_MONTSERRAT_26 1
#define LV_FONT_MONTSERRAT_28 1
#define LV_FONT_MONTSERRAT_30 1
#define LV_FONT_MONTSERRAT_32 1
#define LV_FONT_MONTSERRAT_34 1
#define LV_FONT_MONTSERRAT_36 1
#define LV_FONT_MONTSERRAT_38 1
#define LV_FONT_MONTSERRAT_40 1
#define LV_FONT_MONTSERRAT_42 1
#define LV_FONT_MONTSERRAT_44 1
#define LV_FONT_MONTSERRAT_46 1
#define LV_FONT_MONTSERRAT_48 1
/*Demonstrate special features*/
#define LV_FONT_MONTSERRAT_12_SUBPX 0
#define LV_FONT_MONTSERRAT_28_COMPRESSED 0 /*bpp = 3*/
#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /*Hebrew, Arabic, Persian letters and all their forms*/
#define LV_FONT_SIMSUN_16_CJK 0 /*1000 most common CJK radicals*/
/*Pixel perfect monospace fonts*/
#define LV_FONT_UNSCII_8 0
#define LV_FONT_UNSCII_16 0
/*Optionally declare custom fonts here.
*You can use these fonts as default font too and they will be available globally.
*E.g. #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) LV_FONT_DECLARE(my_font_2)*/
#define LV_FONT_CUSTOM_DECLARE
/*Always set a default font*/
#define LV_FONT_DEFAULT &lv_font_montserrat_14
/*Enable handling large font and/or fonts with a lot of characters.
*The limit depends on the font size, font face and bpp.
*Compiler error will be triggered if a font needs it.*/
#define LV_FONT_FMT_TXT_LARGE 0
/*Enables/disables support for compressed fonts.*/
#define LV_USE_FONT_COMPRESSED 0
/*Enable subpixel rendering*/
#define LV_USE_FONT_SUBPX 0
#if LV_USE_FONT_SUBPX
/*Set the pixel order of the display. Physical order of RGB channels. Doesn't matter with "normal" fonts.*/
#define LV_FONT_SUBPX_BGR 0 /*0: RGB; 1:BGR order*/
#endif
/*Enable drawing placeholders when glyph dsc is not found*/
#define LV_USE_FONT_PLACEHOLDER 1
/*=================
* TEXT SETTINGS
*=================*/
/**
* Select a character encoding for strings.
* Your IDE or editor should have the same character encoding
* - LV_TXT_ENC_UTF8
* - LV_TXT_ENC_ASCII
*/
#define LV_TXT_ENC LV_TXT_ENC_UTF8
/*Can break (wrap) texts on these chars*/
#define LV_TXT_BREAK_CHARS " ,.;:-_"
/*If a word is at least this long, will break wherever "prettiest"
*To disable, set to a value <= 0*/
#define LV_TXT_LINE_BREAK_LONG_LEN 0
/*Minimum number of characters in a long word to put on a line before a break.
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
#define LV_TXT_LINE_BREAK_LONG_PRE_MIN_LEN 3
/*Minimum number of characters in a long word to put on a line after a break.
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
#define LV_TXT_LINE_BREAK_LONG_POST_MIN_LEN 3
/*The control character to use for signalling text recoloring.*/
#define LV_TXT_COLOR_CMD "#"
/*Support bidirectional texts. Allows mixing Left-to-Right and Right-to-Left texts.
*The direction will be processed according to the Unicode Bidirectional Algorithm:
*https://www.w3.org/International/articles/inline-bidi-markup/uba-basics*/
#define LV_USE_BIDI 0
#if LV_USE_BIDI
/*Set the default direction. Supported values:
*`LV_BASE_DIR_LTR` Left-to-Right
*`LV_BASE_DIR_RTL` Right-to-Left
*`LV_BASE_DIR_AUTO` detect texts base direction*/
#define LV_BIDI_BASE_DIR_DEF LV_BASE_DIR_AUTO
#endif
/*Enable Arabic/Persian processing
*In these languages characters should be replaced with an other form based on their position in the text*/
#define LV_USE_ARABIC_PERSIAN_CHARS 0
/*==================
* WIDGET USAGE
*================*/
/*Documentation of the widgets: https://docs.lvgl.io/latest/en/html/widgets/index.html*/
#define LV_USE_ARC 1
#define LV_USE_BAR 1
#define LV_USE_BTN 1
#define LV_USE_BTNMATRIX 1
#define LV_USE_CANVAS 1
#define LV_USE_CHECKBOX 1
#define LV_USE_DROPDOWN 1 /*Requires: lv_label*/
#define LV_USE_IMG 1 /*Requires: lv_label*/
#define LV_USE_LABEL 1
#if LV_USE_LABEL
#define LV_LABEL_TEXT_SELECTION 1 /*Enable selecting text of the label*/
#define LV_LABEL_LONG_TXT_HINT 1 /*Store some extra info in labels to speed up drawing of very long texts*/
#endif
#define LV_USE_LINE 1
#define LV_USE_ROLLER 1 /*Requires: lv_label*/
#if LV_USE_ROLLER
#define LV_ROLLER_INF_PAGES 7 /*Number of extra "pages" when the roller is infinite*/
#endif
#define LV_USE_SLIDER 1 /*Requires: lv_bar*/
#define LV_USE_SWITCH 1
#define LV_USE_TEXTAREA 1 /*Requires: lv_label*/
#if LV_USE_TEXTAREA != 0
#define LV_TEXTAREA_DEF_PWD_SHOW_TIME 1500 /*ms*/
#endif
#define LV_USE_TABLE 1
/*==================
* EXTRA COMPONENTS
*==================*/
/*-----------
* Widgets
*----------*/
#define LV_USE_ANIMIMG 1
#define LV_USE_CALENDAR 1
#if LV_USE_CALENDAR
#define LV_CALENDAR_WEEK_STARTS_MONDAY 0
#if LV_CALENDAR_WEEK_STARTS_MONDAY
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
#else
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}
#endif
#define LV_CALENDAR_DEFAULT_MONTH_NAMES {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
#define LV_USE_CALENDAR_HEADER_ARROW 1
#define LV_USE_CALENDAR_HEADER_DROPDOWN 1
#endif /*LV_USE_CALENDAR*/
#define LV_USE_CHART 1
#define LV_USE_COLORWHEEL 1
#define LV_USE_IMGBTN 1
#define LV_USE_KEYBOARD 1
#define LV_USE_LED 1
#define LV_USE_LIST 1
#define LV_USE_MENU 1
#define LV_USE_METER 1
#define LV_USE_MSGBOX 1
#define LV_USE_SPAN 1
#if LV_USE_SPAN
/*A line text can contain maximum num of span descriptor */
#define LV_SPAN_SNIPPET_STACK_SIZE 64
#endif
#define LV_USE_SPINBOX 1
#define LV_USE_SPINNER 1
#define LV_USE_TABVIEW 1
#define LV_USE_TILEVIEW 1
#define LV_USE_WIN 1
/*-----------
* Themes
*----------*/
/*A simple, impressive and very complete theme*/
#define LV_USE_THEME_DEFAULT 1
#if LV_USE_THEME_DEFAULT
/*0: Light mode; 1: Dark mode*/
#define LV_THEME_DEFAULT_DARK 0
/*1: Enable grow on press*/
#define LV_THEME_DEFAULT_GROW 1
/*Default transition time in [ms]*/
#define LV_THEME_DEFAULT_TRANSITION_TIME 80
#endif /*LV_USE_THEME_DEFAULT*/
/*A very simple theme that is a good starting point for a custom theme*/
#define LV_USE_THEME_BASIC 1
/*A theme designed for monochrome displays*/
#define LV_USE_THEME_MONO 1
/*-----------
* Layouts
*----------*/
/*A layout similar to Flexbox in CSS.*/
#define LV_USE_FLEX 1
/*A layout similar to Grid in CSS.*/
#define LV_USE_GRID 1
/*---------------------
* 3rd party libraries
*--------------------*/
/*File system interfaces for common APIs */
/*API for fopen, fread, etc*/
#define LV_USE_FS_STDIO 0
#if LV_USE_FS_STDIO
#define LV_FS_STDIO_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_STDIO_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_STDIO_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for open, read, etc*/
#define LV_USE_FS_POSIX 0
#if LV_USE_FS_POSIX
#define LV_FS_POSIX_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_POSIX_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_POSIX_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for CreateFile, ReadFile, etc*/
#define LV_USE_FS_WIN32 0
#if LV_USE_FS_WIN32
#define LV_FS_WIN32_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_WIN32_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_WIN32_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for FATFS (needs to be added separately). Uses f_open, f_read, etc*/
#define LV_USE_FS_FATFS 0
#if LV_USE_FS_FATFS
#define LV_FS_FATFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_FATFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for LittleFS (library needs to be added separately). Uses lfs_file_open, lfs_file_read, etc*/
#define LV_USE_FS_LITTLEFS 0
#if LV_USE_FS_LITTLEFS
#define LV_FS_LITTLEFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_LITTLEFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*PNG decoder library*/
#define LV_USE_PNG 0
/*BMP decoder library*/
#define LV_USE_BMP 0
/* JPG + split JPG decoder library.
* Split JPG is a custom format optimized for embedded systems. */
#define LV_USE_SJPG 0
/*GIF decoder library*/
#define LV_USE_GIF 0
/*QR code library*/
#define LV_USE_QRCODE 1
/*FreeType library*/
#define LV_USE_FREETYPE 0
#if LV_USE_FREETYPE
/*Memory used by FreeType to cache characters [bytes] (-1: no caching)*/
#define LV_FREETYPE_CACHE_SIZE (16 * 1024)
#if LV_FREETYPE_CACHE_SIZE >= 0
/* 1: bitmap cache use the sbit cache, 0:bitmap cache use the image cache. */
/* sbit cache:it is much more memory efficient for small bitmaps(font size < 256) */
/* if font size >= 256, must be configured as image cache */
#define LV_FREETYPE_SBIT_CACHE 0
/* Maximum number of opened FT_Face/FT_Size objects managed by this cache instance. */
/* (0:use system defaults) */
#define LV_FREETYPE_CACHE_FT_FACES 0
#define LV_FREETYPE_CACHE_FT_SIZES 0
#endif
#endif
/*Tiny TTF library*/
#define LV_USE_TINY_TTF 0
#if LV_USE_TINY_TTF
/*Load TTF data from files*/
#define LV_TINY_TTF_FILE_SUPPORT 0
#endif
/*Rlottie library*/
#define LV_USE_RLOTTIE 0
/*FFmpeg library for image decoding and playing videos
*Supports all major image formats so do not enable other image decoder with it*/
#define LV_USE_FFMPEG 0
#if LV_USE_FFMPEG
/*Dump input information to stderr*/
#define LV_FFMPEG_DUMP_FORMAT 0
#endif
/*-----------
* Others
*----------*/
/*1: Enable API to take snapshot for object*/
#define LV_USE_SNAPSHOT 0
/*1: Enable Monkey test*/
#define LV_USE_MONKEY 0
/*1: Enable grid navigation*/
#define LV_USE_GRIDNAV 0
/*1: Enable lv_obj fragment*/
#define LV_USE_FRAGMENT 0
/*1: Support using images as font in label or span widgets */
#define LV_USE_IMGFONT 0
/*1: Enable a published subscriber based messaging system */
#define LV_USE_MSG 0
/*1: Enable Pinyin input method*/
/*Requires: lv_keyboard*/
#define LV_USE_IME_PINYIN 0
#if LV_USE_IME_PINYIN
/*1: Use default thesaurus*/
/*If you do not use the default thesaurus, be sure to use `lv_ime_pinyin` after setting the thesauruss*/
#define LV_IME_PINYIN_USE_DEFAULT_DICT 1
/*Set the maximum number of candidate panels that can be displayed*/
/*This needs to be adjusted according to the size of the screen*/
#define LV_IME_PINYIN_CAND_TEXT_NUM 6
/*Use 9 key input(k9)*/
#define LV_IME_PINYIN_USE_K9_MODE 1
#if LV_IME_PINYIN_USE_K9_MODE == 1
#define LV_IME_PINYIN_K9_CAND_TEXT_NUM 3
#endif // LV_IME_PINYIN_USE_K9_MODE
#endif
/*==================
* EXAMPLES
*==================*/
/*Enable the examples to be built with the library*/
#define LV_BUILD_EXAMPLES 1
/*===================
* DEMO USAGE
====================*/
/*Show some widget. It might be required to increase `LV_MEM_SIZE` */
#define LV_USE_DEMO_WIDGETS 1
#if LV_USE_DEMO_WIDGETS
#define LV_DEMO_WIDGETS_SLIDESHOW 1
#endif
/*Demonstrate the usage of encoder and keyboard*/
#define LV_USE_DEMO_KEYPAD_AND_ENCODER 0
/*Benchmark your system*/
#define LV_USE_DEMO_BENCHMARK 1
#if LV_USE_DEMO_BENCHMARK
/*Use RGB565A8 images with 16 bit color depth instead of ARGB8565*/
#define LV_DEMO_BENCHMARK_RGB565A8 0
#endif
/*Stress test for LVGL*/
#define LV_USE_DEMO_STRESS 1
/*Music player demo*/
#define LV_USE_DEMO_MUSIC 1
#if LV_USE_DEMO_MUSIC
#define LV_DEMO_MUSIC_SQUARE 0
#define LV_DEMO_MUSIC_LANDSCAPE 0
#define LV_DEMO_MUSIC_ROUND 0
#define LV_DEMO_MUSIC_LARGE 0
#define LV_DEMO_MUSIC_AUTO_PLAY 0
#endif
/*--END OF LV_CONF_H--*/
#endif /*LV_CONF_H*/
#endif /*End of "Content enable"*/

View File

@ -1,6 +1,7 @@
# SHiNE ESP32 Subserver UI Nav Minimal Spec
# SHiNE ESP32 Homeserver UI Nav Minimal Spec
Минимальный навигационный прототип для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Легаси-спецификация старого навигационного теста для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Актуальный основной скетч теперь находится в `../main-device/shine_homeserver_main/`; этот документ оставлен только как историческая справка по старому тестовому UI.
## Цель
@ -23,7 +24,7 @@
- `WIFI_SCREEN`
- `SERVER_SCREEN`
- `ACCOUNT_SCREEN`
- `ACCOUNT_SUBSERVER_SCREEN`
- `ACCOUNT_HOMESERVER_SCREEN`
- `ACCOUNT_SECRET_SCREEN`
- `SECRET_SHOW_SCREEN`
- `SECRET_GENERATE_*`
@ -33,7 +34,7 @@
## HOME
Показывает:
- сверху слева значение сабсервера или `subserver not set`;
- сверху слева значение homeserver или `homeserver not set`;
- ниже значение логина или `login not set`;
- справа от строки логина индикатор статуса Solana-аккаунта:
- зелёный — все ключи совпадают;
@ -51,7 +52,7 @@
- строка `SHiNE: <server> connected/account not configured/unavailable`;
- при отсутствии пользователя в Solana PDA слева снизу появляется кнопка `REGISTER ACCOUNT`;
- снизу кнопку `SETTINGS`, уменьшенную примерно до половины ширины экрана и сдвинутую к правому краю.
- внизу на тёмной полосе подпись `SHiNE subserver (v.0.18)`.
- внизу на тёмной полосе подпись `SHiNE homeserver (v.0.18)`.
Строка Wi-Fi на `HOME`:
- `Wi-Fi (not configured) not configured`
@ -63,13 +64,16 @@
- кнопка `SETTINGS` -> `SETTINGS_MENU`;
- свайп влево -> `SETTINGS_MENU`.
Примечание:
- поведение `REGISTER ACCOUNT -> REGISTER_ACCOUNT_PLACEHOLDER` относится к старой тестовой версии и не является актуальным для основного скетча.
Фоновая логика:
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
- при наличии `login + secret + subserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
- сравниваются `root key`, `blockchain key`, `device key` и `subserver` session-запись типа `100`;
- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
- сравниваются `root key`, `blockchain key`, `device key` и `homeserver` session-запись типа `100`;
- для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE:
- авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`;
- session key = публичный `subserver key`;
- session key = публичный `homeserver key`;
- подтверждение создания сессии подписывается `device key`;
- heartbeat выполняется `Ping` раз в минуту.
@ -164,26 +168,26 @@
- заголовок `ACCOUNT`;
- статусное сообщение;
- кнопку `Login (<value|not set>)`;
- кнопку `Subserver (<value|not set>)`;
- кнопку `Homeserver (<value|not set>)`;
- кнопку `Secret (<*****|not set>)`.
Переходы:
- свайп вправо -> `SETTINGS_MENU`
- `Login` -> `TEXT_EDIT_SCREEN`
- `Subserver` -> `ACCOUNT_SUBSERVER_SCREEN`
- `Homeserver` -> `ACCOUNT_HOMESERVER_SCREEN`
- `Secret` -> `ACCOUNT_SECRET_SCREEN`
## ACCOUNT_SUBSERVER_SCREEN
## ACCOUNT_HOMESERVER_SCREEN
Показывает:
- текущий `subserver`;
- рекомендацию оставить `subserver1`, если устройство одно;
- кнопку `USE SUBSERVER1`;
- текущий `homeserver`;
- рекомендацию оставить `homeserver1`, если устройство одно;
- кнопку `USE HOMESERVER1`;
- кнопку `EDIT MANUALLY`;
- кнопку `BACK`.
Переходы:
- `USE SUBSERVER1` -> сохраняет `subserver1` и возвращает в `ACCOUNT_SCREEN`
- `USE HOMESERVER1` -> сохраняет `homeserver1` и возвращает в `ACCOUNT_SCREEN`
- `EDIT MANUALLY` -> `TEXT_EDIT_SCREEN`
- свайп вправо -> `ACCOUNT_SCREEN`
@ -212,8 +216,8 @@
- `Blockchain key priv (base58)`;
- `Device key (base58)`;
- `Device key priv (base58)`;
- `Subserver key (base58)`;
- `Subserver key priv (base58)`;
- `Homeserver key (base58)`;
- `Homeserver key priv (base58)`;
- для каждого поля показывается формула derivation;
- значения ключей показываются полными строками увеличенным шрифтом;
- кнопку `BACK`.
@ -293,7 +297,7 @@
Используется `Preferences` (NVS памяти ESP32):
- `login`
- `subserver`
- `homeserver`
- `secret_set`
## Детали клавиатуры
@ -312,7 +316,7 @@
- `DEL`
- `SAVE`
- `CANCEL`
- ниже рамки клавиатурного блока остаётся отдельная тёмная полоса с версией `SHiNE subserver (v.0.18)`.
- ниже рамки клавиатурного блока остаётся отдельная тёмная полоса с версией `SHiNE homeserver (v.0.18)`.
## Жесты
@ -329,7 +333,7 @@
- `WIFI_SCREEN`: свайп вправо -> `SETTINGS_MENU`
- `SERVER_SCREEN`: свайп вправо -> `SETTINGS_MENU`
- `ACCOUNT_SCREEN`: свайп вправо -> `SETTINGS_MENU`
- `ACCOUNT_SUBSERVER_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN`
- `ACCOUNT_HOMESERVER_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN`
- `ACCOUNT_SECRET_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN`
- `TEXT_EDIT_SCREEN`: свайп влево/вправо -> переключение страниц клавиатуры
- переключение страниц клавиатуры срабатывает только если свайп начался в зоне самой клавиатуры, а не по всему экрану редактора

View File

@ -1,8 +1,9 @@
# SHiNE ESP32 Subserver UI Spec
# SHiNE ESP32 Homeserver UI Spec
## Назначение
Этот документ описывает актуальный UI-прототип сабсервера `SHiNE` для платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Этот документ описывает актуальный UI-прототип homeserver `SHiNE` для платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Актуальный основной Arduino-скетч лежит в `../main-device/shine_homeserver_main/`.
Документ является источником истины для Arduino-скетча:
@ -19,16 +20,17 @@
- локальный UI на тач-экране;
- хранение настроек и секретов во внутренней памяти `ESP32` через `NVS`;
- русский текст в логике интерфейса; в текущей временной сборке отображение на экране идёт через ASCII-транслитерацию, потому что `U8g2`-шрифты на устройстве временно не рисуются;
- экран пополнения с реальным `solana:` URI и рисованием QR-кода;
- экран пополнения с реальным `solana:` URI и рисованием QR-кода через `LVGL`;
- реальное подключение к `Wi-Fi` по сохранённым `SSID/паролю`;
- реальная проверка доступности `API`, `RPC` и `WS`-адресов;
- реальное чтение баланса кошелька из `Solana RPC`;
- проверка обязательных условий перед регистрацией;
- живая on-chain регистрация серверного `user_pda` в `shine_users` через `device key` устройства;
- живая on-chain регистрация серверного `user_pda` в `shine_users` по нажатию кнопки на главном экране;
- прототип входящих запросов с подтверждением и отклонением;
- PIN-блокировка (в текущей временной сборке вход по PIN отключён, устройство открывает `HOME` сразу после старта);
- базовые настройки, статус и главный экран;
- сохранение `PDA` и `tx signature` после успешной регистрации.
- создание и возобновление серверной сессии `SHiNE` через WebSocket с `sessionType = 100` и `clientPlatform = "ESP32"`.
Что пока считается именно прототипом, а не финальной интеграцией:
@ -37,13 +39,13 @@
## Основная идея устройства
Устройство работает как отдельный сабсервер:
Устройство работает как отдельный homeserver:
- хранит секрет на самом устройстве;
- позволяет ввести логин, секрет и имя сабсервера;
- позволяет ввести логин, секрет и имя homeserver;
- показывает адрес кошелька устройства;
- позволяет пополнить баланс перед регистрацией;
- после выполнения условий даёт зарегистрировать устройство как сабсервер;
- после выполнения условий даёт зарегистрировать устройство как homeserver;
- после регистрации может принимать входящие запросы на вход и на подпись.
`SD`-карта не нужна для постоянного хранения секрета в этом прототипе.
@ -57,7 +59,7 @@
- `Wi-Fi SSID`;
- `Wi-Fi password`;
- `login`;
- `session/subserver name`;
- `session/homeserver name`;
- `master secret`;
- `wallet address`;
- `user pda address`;
@ -69,6 +71,16 @@
- флаги:
`wifiReady`, `serversReady`, `secretReady`, `registered`, `online`.
## Правило серверной сессии SHiNE
При подключении к серверу `SHiNE` устройство должно авторизовываться как homeserver-сеанс:
- `sessionType = 100`
- `clientPlatform = "ESP32"`
- `clientInfo = "ESP32 homeserver"`
Это относится и к `CreateAuthSession`, и к `SessionLogin`.
## Правила готовности к регистрации
Кнопка регистрации доступна только если одновременно выполнены условия:
@ -101,6 +113,8 @@
13. `PIN_EDIT`
14. `TEXT_INPUT`
15. `CONFIRM`
16. `REGISTER_ACCOUNT_CONFIRM`
17. `REGISTER_ACCOUNT_RESULT`
## Общие правила интерфейса
@ -142,11 +156,25 @@
- крупный статус регистрации;
- имя логина;
- имя сабсервера;
- короткий статус Wi-Fi;
- короткий статус сервера;
- имя homeserver;
- строку `Wi-Fi: <SSID> connected|disconnected`;
- строку `SHiNE: <server address> connected|unavailable`;
- короткий статус баланса.
Особенности верхнего блока:
- зелёный/контурный статусный кружок аккаунта расположен слева от строки логина;
- блок `STATUS` поднят выше относительно предыдущей версии;
- если состояние хорошее, слово `connected` в строках `Wi-Fi` и `SHiNE` показывается зелёным.
В зоне баланса:
- основная кнопка показа/обновления баланса занимает примерно 80% строки;
- текст на кнопке баланса выровнен левее центра;
- справа от неё стоит отдельная кнопка `QR`;
- после старта устройства баланс пытается загрузиться автоматически, если уже есть секрет и `Wi-Fi`;
- нажатие на кнопку `QR` открывает экран `WALLET_QR`.
Нижние кнопки:
- `Статус`
@ -158,18 +186,113 @@
Дополнительная большая кнопка:
- `Зарегистрировать`
- `REGISTER ACCOUNT`
- либо жёлтая `ADD HOMESERVER`
- либо жёлтая `FIX HOMESERVER PASSWORD`
Если регистрация уже сделана:
- вместо призыва к регистрации показывается статус `Сабсервер активен`.
- если пользователь создан, но в `PDA` ещё нет сессии текущего homeserver, показывается жёлтая кнопка `ADD HOMESERVER`;
- если в `PDA` есть homeserver с тем же именем, но с другим ключом, показывается жёлтая кнопка `FIX HOMESERVER PASSWORD`;
- если и пользователь, и homeserver-сессия уже корректны, вместо призыва к регистрации показывается статус `Homeserver активен`.
- две нижние кнопки внизу экрана не прилегают вплотную друг к другу, между ними есть небольшой зазор.
## Экран REGISTER_ACCOUNT_CONFIRM
Показывает предварительную проверку перед отправкой транзакции регистрации.
Отображается:
- повторный `login`;
- сообщение о том, что логин свободен или уже занят;
- баланс кошелька с проверкой порога `0.020 SOL`;
- имя homeserver;
- пометка, если используется стандартное значение `homeserver1`;
- отдельное предупреждение, если `Wi-Fi` не подключён.
Кнопки:
- `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`
- `BACK`
Поведение:
- если `Wi-Fi` не подключён, на экране прямо показывается уведомление об этом и кнопка регистрации недоступна;
- если `login` уже занят в `shine_users`, регистрация недоступна;
- если баланс меньше `0.020 SOL`, на экране показывается соответствующая ошибка;
- если все проверки пройдены, кнопка регистрации активна и запускает on-chain регистрацию.
## Экран REGISTER_ACCOUNT_RESULT
Показывает результат отправки регистрации.
Отображается:
- текст успеха или ошибки;
- подробное сообщение;
- при успехе краткий `user_pda`;
- при успехе краткий `tx signature`.
Кнопки:
- `BACK HOME`
- `ACCOUNT`
Поведение:
- после успешной регистрации данные `user_pda` и `tx signature` сохраняются в `NVS`;
- при ошибке на экране показывается причина отказа;
- если ошибку вернул `sendTransaction`, экран старается показать не только общий текст, но и детали `RPC`/preflight/simulate-логов.
## Экран HOMESERVER_PDA_CONFIRM
Показывает, что именно не так с homeserver-секцией уже существующей пользовательской `PDA`.
Отображается:
- причина (`homeserver` отсутствует в `PDA` или ключ не совпадает с локальным секретом);
- `login`;
- имя `homeserver`;
- короткое пояснение, что именно будет сделано.
Кнопки:
- `ADD HOMESERVER` или `FIX HOMESERVER PASSWORD`
- `BACK`
Поведение:
- если `Wi-Fi` не подключён, действие недоступно;
- экран не создаёт нового пользователя, а запускает `update_user_pda`;
- при `ADD HOMESERVER` в блок `sessions` добавляется запись `session_type=100`;
- при `FIX HOMESERVER PASSWORD` обновляется публичный ключ уже существующей записи `homeserver`.
## Экран HOMESERVER_PDA_RESULT
Показывает результат обновления `sessions` в пользовательской `PDA`.
Отображается:
- успех или ошибка;
- короткое сообщение;
- при успехе краткий `tx signature`.
Кнопки:
- `BACK HOME`
- `ACCOUNT`
Поведение:
- при ошибке текст ошибки сохраняется в ту же USB/NVS-диагностику, что и регистрация;
- после успешного обновления выполняется повторная проверка `PDA`, и основной экран должен перейти в состояние `ok`.
## Экран STATUS
Показывает сводку:
- логин;
- сабсервер;
- homeserver;
- есть ли секрет;
- зарегистрировано ли устройство;
- подключён ли Wi-Fi;
@ -256,7 +379,7 @@
Показывает:
- логин;
- имя сабсервера;
- имя homeserver;
- статус секрета;
- короткий отпечаток секрета;
- статус регистрации;
@ -266,7 +389,7 @@
- `Изменить логин`
- `Секрет`
- `Имя сабсервера`
- `Имя homeserver`
- `Сгенерировать`
- `Очистить`
- `Назад`
@ -276,6 +399,7 @@
- `Сгенерировать` создаёт новый `master secret` и пересчитывает из него `device`-кошелёк;
- `Очистить` удаляет секрет, адрес кошелька, `PDA`, `tx`, регистрацию и онлайн-статус;
- логин приводится к нижнему регистру и trim.
- после успешной регистрации на экране сохраняются и отображаются краткие отпечатки `PDA` и `TX`.
## Экран WALLET
@ -304,19 +428,27 @@
## Экран WALLET_QR
Экран показывает:
- крупный реальный `QR` для строки `solana:<wallet_address>`;
- снизу крупный текст самого адреса кошелька.
Поведение:
- отдельная текстовая подсказка возврата не показывается;
- возврат на главный экран выполняется обычным тапом по экрану.
Показывает:
- QR-код для строки вида:
`solana:<wallet>?amount=0.20&label=SHiNE%20Register`;
- адрес кошелька;
- сумму;
- текст URI.
`solana:<wallet>`;
- мелкую подпись с полным адресом кошелька под QR.
Кнопки:
Поведение:
- `Назад`
QR должен быть сканируемым, а не декоративным.
- QR должен быть сканируемым, а не декоративным;
- адрес кошелька берётся из `device key`, вычисленного из сохранённого `master secret`;
- нажатие в любую точку экрана возвращает пользователя на `HOME`.
## Экран REQUESTS
@ -394,7 +526,7 @@ QR должен быть сканируемым, а не декоративны
- `SSID`
- `Пароль Wi-Fi`
- `Логин`
- `Имя сабсервера`
- `Имя homeserver`
- `API URL`
- `RPC URL`
- `WS URL`
@ -432,13 +564,16 @@ QR должен быть сканируемым, а не декоративны
5. проверить или задать серверные адреса;
6. открыть `Аккаунт`;
7. ввести логин;
8. задать имя сабсервера;
8. задать имя homeserver;
9. сгенерировать секрет;
10. открыть `Кошелёк`;
11. при необходимости пополнить баланс;
12. вернуться на `HOME`;
13. нажать `Зарегистрировать`;
14. после подтверждения увидеть статус `Сабсервер активен`.
13. нажать `REGISTER ACCOUNT`;
14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1` и при необходимости сообщение о неподключённом `Wi-Fi`;
15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`;
16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку;
17. после успешной регистрации увидеть статус `Homeserver активен`.
Примечание:

View File

@ -27,6 +27,7 @@
- Сервис ведёт состояние активной задачи и текущего файла истории, а после рестарта продолжает незавершённую обработку с учётом сохранённого состояния.
- Истории диалогов хранятся в JSONL по каждому разрешённому username отдельно: `data/history/<username>/`.
- Архив истории после `/new`: `data/history/<username>/archive/`.
- После `/new` для этого же пользователя должен сбрасываться и контекст продолжения Codex-сессии; следующий запрос запускается как новая сессия, не через resume.
- Для просмотра истории игрока открывать файлы в его папке истории по username.
- Дедупликация входящих Telegram update нужна, чтобы одно сообщение не попало в обработку повторно.
- Если Codex молчит во время активной задачи 2 минуты подряд, сервис отправляет аварийный статус с общим временем работы задачи; при дальнейшем молчании повторяет статус каждые 2 минуты.

View File

@ -89,7 +89,7 @@ python3 SHiNE-agent-bot-coder/py_bot_service.py --selftest-codex "Ответь
- `/queue` — список задач в очереди.
- `/stop` — остановить текущую задачу.
- `/cancel <id|all>` — удалить задачу по id/префиксу или очистить очередь.
- `/new` — архивировать текущую историю и начать новый диалог.
- `/new` — архивировать текущую историю, сбросить продолжение Codex-сессии для этого пользователя и начать новый диалог.
- `/voice_on` — включить озвучивание финальных ответов для текущего пользователя.
- `/voice_off` — выключить озвучивание финальных ответов для текущего пользователя.
- `/voice_rewrite_on` — включить адаптацию текста перед озвучкой.

View File

@ -656,13 +656,33 @@ class ShinePyBotService:
self.state["current_history_file"] = str(history_file)
self._persist_state()
def _current_history_file_for_user(self, username: str) -> Path:
def _user_session_state(self, username: str) -> dict[str, Any]:
uname = normalize_username(username) or self.cfg.allowed_username
self._ensure_user_session(uname)
sessions = self.state.get("user_sessions") or {}
session = sessions.get(uname) or {}
session = sessions.get(uname)
if not isinstance(session, dict):
session = {}
sessions[uname] = session
return session
def _current_history_file_for_user(self, username: str) -> Path:
session = self._user_session_state(username)
return Path(session["current_history_file"])
def _codex_thread_id_for_user(self, username: str) -> str:
thread_id = (self._user_session_state(username).get("codex_thread_id") or "").strip()
return thread_id
def _set_codex_thread_id_for_user(self, username: str, thread_id: str) -> None:
session = self._user_session_state(username)
normalized = (thread_id or "").strip()
if normalized:
session["codex_thread_id"] = normalized
else:
session.pop("codex_thread_id", None)
self._persist_state()
def _create_new_history_file(self, reason: str, username: str) -> Path:
ts = dt.datetime.now().strftime("%Y-%m-%d_%H%M%S")
rnd = "".join(random.choices(string.hexdigits.lower(), k=8))
@ -690,7 +710,12 @@ class ShinePyBotService:
if not isinstance(sessions, dict):
sessions = {}
self.state["user_sessions"] = sessions
previous = sessions.get(uname) if isinstance(sessions.get(uname), dict) else {}
sessions[uname] = {"current_history_file": str(new_file)}
if reason != "command_new" and isinstance(previous, dict):
thread_id = (previous.get("codex_thread_id") or "").strip()
if thread_id:
sessions[uname]["codex_thread_id"] = thread_id
if uname == self.cfg.allowed_username:
self.state["current_history_file"] = str(new_file)
self._persist_state()
@ -926,7 +951,7 @@ class ShinePyBotService:
text = (
f"Привет, {player_name}.\n"
"Можно задавать вопросы по проекту, просить анализ, идеи и подготовку готового ТЗ.\n"
"Команда /new начинает новую сессию и архивирует текущую историю."
"Команда /new начинает новую Codex-сессию и архивирует текущую историю."
)
reminder = self._task_center_counts_text(uname)
if reminder:
@ -1449,7 +1474,7 @@ class ShinePyBotService:
"/tasks — список ваших задач и предложений",
"/stop — остановить текущую задачу",
"/cancel <id|all> — удалить задачу по id (префикс) или все",
"/new — архивировать историю и начать новую",
"/new — архивировать историю и начать новую Codex-сессию",
"/help — эта справка",
]
if is_owner:
@ -1680,9 +1705,31 @@ class ShinePyBotService:
)
def _run_codex(self, prompt: str, job: dict[str, Any]) -> str:
username = job.get("username") or self.cfg.allowed_username
thread_id = self._codex_thread_id_for_user(username)
try:
return self._run_codex_once(prompt, job, thread_id=thread_id)
except RuntimeError as e:
if not thread_id or not self._is_missing_codex_session_error(str(e)):
raise
self._set_codex_thread_id_for_user(username, "")
self._append_history(
Path(job["history_file"]),
"system_event",
{
"event": "codex_thread_reset",
"reason": "missing_session",
"username": normalize_username(username),
"oldThreadId": thread_id,
},
)
return self._run_codex_once(prompt, job, thread_id="")
def _run_codex_once(self, prompt: str, job: dict[str, Any], *, thread_id: str) -> str:
output_lines: list[str] = []
job_id = str(job["id"])
job_num = job.get("num", "?")
username = job.get("username") or self.cfg.allowed_username
with tempfile.NamedTemporaryFile(prefix="shine-codex-last-message-", suffix=".txt", delete=False) as tmp:
output_file = Path(tmp.name)
@ -1693,9 +1740,12 @@ class ShinePyBotService:
"--json",
"-C", str(self.cfg.codex_workdir),
"-o", str(output_file),
prompt,
]
print(f"[py-bot] codex exec start job={job_id[:8]}", flush=True)
if thread_id:
cmd.extend(["resume", thread_id])
cmd.append(prompt)
mode = f"resume {thread_id}" if thread_id else "new"
print(f"[py-bot] codex exec start job={job_id[:8]} mode={mode}", flush=True)
process = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
@ -1714,10 +1764,14 @@ class ShinePyBotService:
last_user_note_at = 0.0
codex_started_at = time.time()
last_job_message_at = codex_started_at
seen_thread_id = ""
def on_line(line: str) -> None:
nonlocal last_user_note, last_user_note_at, last_job_message_at
nonlocal last_user_note, last_user_note_at, last_job_message_at, seen_thread_id
output_lines.append(line)
current_thread_id = self._extract_codex_thread_id(line)
if current_thread_id:
seen_thread_id = current_thread_id
note = self._extract_codex_user_note(line)
now = time.time()
if note and note != last_user_note and now - last_user_note_at > 8:
@ -1770,6 +1824,9 @@ class ShinePyBotService:
tail = "\n".join(output_lines[-40:])
raise RuntimeError(f"Codex exited with code {return_code}. Output tail:\n{tail}")
if seen_thread_id and seen_thread_id != thread_id:
self._set_codex_thread_id_for_user(username, seen_thread_id)
if output_file.exists():
answer = output_file.read_text(encoding="utf-8").strip()
try:
@ -2829,6 +2886,35 @@ class ShinePyBotService:
return line
return ""
@staticmethod
def _extract_codex_thread_id(line: str) -> str:
s = (line or "").strip()
if not s.startswith("{"):
return ""
try:
obj = json.loads(s)
except Exception:
return ""
if obj.get("type") != "thread.started":
return ""
thread_id = (obj.get("thread_id") or "").strip()
return thread_id
@staticmethod
def _is_missing_codex_session_error(text: str) -> bool:
lowered = (text or "").lower()
markers = [
"session not found",
"conversation not found",
"thread not found",
"no session found",
"invalid session",
"unknown session",
"no conversation found",
"unknown thread",
]
return any(marker in lowered for marker in markers)
@staticmethod
def _format_duration(seconds: int) -> str:
seconds = max(0, seconds)

44
SHiNE-browser-plugin-wallet/.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
.gradle
build/
node_modules/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

10
SHiNE-browser-plugin-wallet/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
SHiNE-browser-plugin-wallet/.idea/.name generated Normal file
View File

@ -0,0 +1 @@
ESP-wallet

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
<option name="myGradleHome" value="" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,40 @@
# SHiNE Browser Plugin Wallet
Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.
## Что уже умеет
- создать `wallet-session` через `StartTrustedDeviceLogin`;
- показать код подключения;
- дождаться подтверждения на доверенном устройстве;
- принять `session-only` payload без передачи `deviceKey/rootKey/blockchainKey`;
- сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin;
- восстанавливать session через `SessionChallenge -> SessionLogin`;
- держать wallet-state в `background service worker`, а popup использовать как UI.
- принимать не адрес сервера, а логин серверного аккаунта SHiNE и находить точный `https://...` / `wss://...` адрес через его PDA.
## Как загрузить локально
1. Открой `chrome://extensions/`
2. Включи `Developer mode`
3. Нажми `Load unpacked`
4. Выбери папку `SHiNE-browser-plugin-wallet/`
## Ограничения текущего этапа
- plugin пока не держит постоянный фоновый WS-канал после закрытия popup, но хранит wallet-state в `background`;
- на этом этапе реализован только `session-only login`;
- запросы на подпись будут следующим этапом.
- pairing-пароль, если он используется, должен генерироваться в формате `sha256$<hex>` от строки `shine-pairing|loginLower|password`.
## Сборка crypto bundle
Для обычной загрузки plugin это не нужно: bundled crypto-файл уже лежит в репозитории.
Если понадобится пересобрать локальный crypto bundle:
```bash
npm install
npx esbuild js/lib/vendor/noble-ed25519-entry.js --bundle --format=esm --platform=browser --outfile=js/lib/vendor/noble-ed25519-bundle.js
npx esbuild js/lib/vendor/solana-publickey-entry.js --bundle --format=esm --platform=browser --outfile=js/lib/vendor/solana-publickey-bundle.js
```

View File

@ -0,0 +1,567 @@
import { createRequesterPairingMaterial, decryptPairingPayloadFromEnvelope, deriveEspPairingPasswordHash } from './js/lib/device-pairing.js';
import { loadPluginSettings, loadSessionMaterial, savePluginSettings, saveSessionMaterial, clearSessionMaterial } from './js/lib/session-store.js';
import { ShineApiClient } from './js/lib/shine-api.js';
import {
DEFAULT_SHINE_SERVER_LOGIN,
buildHttpBase,
readWalletProfileByLogin,
resolveShineServerByUserLogin,
} from './js/lib/shine-server-resolver.js';
const state = {
api: null,
settings: {
serverLogin: DEFAULT_SHINE_SERVER_LOGIN,
serverHttp: buildHttpBase('shineup.me'),
serverUrl: 'wss://shineup.me/ws',
login: '',
},
requesterMaterial: null,
pairingId: '',
expiresAtMs: 0,
shortCode: '',
trustedSessionOnline: false,
pollTimer: 0,
activeSession: null,
connectionOnline: false,
walletProfile: null,
signing: {
selectedKeyId: 'device',
selectedDeviceName: '',
devicesResolvedAtMs: 0,
},
statusText: '',
statusKind: 'info',
};
function setStatus(message = '', kind = 'info') {
state.statusText = String(message || '');
state.statusKind = kind === 'error' ? 'error' : 'info';
}
function stopPoll() {
if (state.pollTimer) {
clearTimeout(state.pollTimer);
state.pollTimer = 0;
}
}
function clearPairingState() {
stopPoll();
state.requesterMaterial = null;
state.pairingId = '';
state.expiresAtMs = 0;
state.shortCode = '';
state.trustedSessionOnline = false;
}
function ensureApi(serverUrl = state.settings.serverUrl) {
const normalized = String(serverUrl || '').trim() || 'wss://shineup.me/ws';
if (!state.api || state.api.serverUrl !== normalized) {
state.api?.close();
state.api = new ShineApiClient(normalized);
}
return state.api;
}
async function loadStateFromStorage() {
const settings = await loadPluginSettings();
state.settings = {
serverLogin: String(settings?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(),
serverHttp: String(settings?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim() || buildHttpBase('shineup.me'),
serverUrl: String(settings?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
login: String(settings?.login || '').trim(),
};
state.activeSession = await loadSessionMaterial();
state.walletProfile = state.activeSession?.walletProfile || null;
state.signing = {
selectedKeyId: String(state.activeSession?.selectedKeyId || 'device'),
selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''),
devicesResolvedAtMs: Number(state.activeSession?.devicesResolvedAtMs || 0),
};
}
async function persistSettings(nextSettings = {}) {
state.settings = {
...state.settings,
...nextSettings,
};
await savePluginSettings(state.settings);
return state.settings;
}
async function resolveServerForLogin(login) {
const cleanLogin = String(login || state.settings.login || '').trim();
if (!cleanLogin) {
state.settings = {
...state.settings,
login: '',
serverLogin: '',
};
await savePluginSettings(state.settings);
return { ok: true, resolved: false };
}
const resolved = await resolveShineServerByUserLogin(cleanLogin);
state.settings = {
...state.settings,
login: cleanLogin,
serverLogin: resolved.serverLogin,
serverHttp: resolved.serverHttp,
serverUrl: resolved.serverUrl,
};
await savePluginSettings(state.settings);
return { ok: true, resolved: true, ...resolved };
}
async function saveActiveSessionRecord() {
if (!state.activeSession) return;
const nextRecord = {
...state.activeSession,
walletProfile: state.walletProfile,
selectedKeyId: state.signing.selectedKeyId,
selectedDeviceName: state.signing.selectedDeviceName,
devicesResolvedAtMs: state.signing.devicesResolvedAtMs,
};
state.activeSession = nextRecord;
await saveSessionMaterial(nextRecord);
}
function shortKey(value = '', size = 10) {
const raw = String(value || '').trim();
return raw ? raw.slice(0, size) : '';
}
function extractErrorCode(message = '') {
const match = String(message || '').match(/\(([A-Z0-9_]+)\)\s*$/i);
return match ? String(match[1]).toUpperCase() : '';
}
function toWalletErrorMessage(error, fallback = 'Не удалось выполнить операцию кошелька.') {
const raw = String(error?.message || '').trim();
const code = String(error?.code || extractErrorCode(raw) || '').toUpperCase();
if (code === 'PAIRING_NOT_AVAILABLE') {
return 'Для этого логина ещё не включено подключение по коду. На доверенном устройстве откройте «Подключить по коду» и нажмите «Включить подключение по коду» или задайте дополнительный пароль.';
}
if (code === 'PAIRING_NO_TRUSTED_SESSION_ONLINE') {
return 'Сейчас нет ни одной онлайн доверенной сессии этого пользователя. Откройте SHiNE на другом уже подключённом устройстве и держите его в сети.';
}
if (code === 'PAIRING_PASSWORD_INVALID') {
return 'Дополнительный пароль подключения не подходит.';
}
return raw || fallback;
}
function buildSigningKeyOptions(walletProfile) {
const rootKey = String(walletProfile?.publicKeys?.rootKeyBase58 || '').trim();
const deviceKey = String(walletProfile?.publicKeys?.deviceKeyBase58 || '').trim();
const options = [];
if (rootKey) {
options.push({
id: 'root',
label: `rootKey (ed25519, ${shortKey(rootKey)})`,
keyType: 'ed25519',
publicKeyBase58: rootKey,
});
}
if (deviceKey) {
options.push({
id: 'device',
label: `deviceKey (ed25519, ${shortKey(deviceKey)})`,
keyType: 'ed25519',
publicKeyBase58: deviceKey,
});
}
return options;
}
function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = []) {
const published = Array.isArray(publishedHomeservers) ? publishedHomeservers : [];
const homeserverSessions = Array.isArray(serverSessions)
? serverSessions.filter((item) => Number(item?.sessionType || 0) === 100)
: [];
const onlineHomeservers = homeserverSessions.filter((item) => !!item?.onlineOnThisServer);
return published.map((item) => {
let onlineState = 'unknown';
if (published.length === 1) {
onlineState = onlineHomeservers.length > 0 ? 'online' : 'offline';
} else if (onlineHomeservers.length === 0) {
onlineState = 'offline';
} else if (onlineHomeservers.length === published.length) {
onlineState = 'online';
}
return {
...item,
onlineState,
onlineLabel: onlineState === 'online' ? 'online' : onlineState === 'offline' ? 'offline' : 'unknown',
};
});
}
async function hydrateWalletProfile(login) {
const cleanLogin = String(login || state.activeSession?.login || state.settings.login || '').trim();
if (!cleanLogin) throw new Error('Нет логина для чтения PDA кошелька.');
const profile = await readWalletProfileByLogin(cleanLogin);
const signingKeyOptions = buildSigningKeyOptions(profile);
const selectedKeyId = signingKeyOptions.some((item) => item.id === state.signing.selectedKeyId)
? state.signing.selectedKeyId
: (signingKeyOptions[0]?.id || '');
const selectedDeviceName = state.signing.selectedDeviceName
|| String(profile?.homeserverSessions?.[0]?.sessionName || '');
state.walletProfile = {
...profile,
signingKeyOptions,
homeserverSessions: Array.isArray(profile.homeserverSessions) ? profile.homeserverSessions.map((item) => ({
...item,
onlineState: 'unknown',
onlineLabel: 'unknown',
})) : [],
};
state.signing = {
...state.signing,
selectedKeyId,
selectedDeviceName,
};
await saveActiveSessionRecord();
return state.walletProfile;
}
async function resumeActiveSession({ keepConnected = false } = {}) {
const sessionRecord = await loadSessionMaterial();
state.activeSession = sessionRecord;
if (!sessionRecord) {
state.connectionOnline = false;
setStatus('Wallet-session ещё не подключена.', 'info');
return { ok: true, connected: false };
}
try {
await persistSettings({
serverLogin: String(sessionRecord?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(),
serverHttp: String(sessionRecord?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim(),
serverUrl: String(sessionRecord?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim(),
login: String(sessionRecord?.login || state.settings.login || '').trim(),
});
const resumed = await ensureApi().resumeSession(sessionRecord);
state.connectionOnline = !!keepConnected;
if (!keepConnected) {
ensureApi().close();
state.api = null;
setStatus(`Wallet-session сохранена для @${resumed.login}. Подключение будет открываться только по действию.`, 'info');
} else {
setStatus(`Wallet-session активна для @${resumed.login}.`, 'info');
}
return { ok: true, connected: true, login: resumed.login, sessionId: resumed.sessionId };
} catch (error) {
state.connectionOnline = false;
setStatus(error.message || 'Не удалось восстановить wallet-session.', 'error');
return { ok: false, connected: false, error: state.statusText };
}
}
async function attachApprovedSession(payload) {
if (String(payload?.type || '') !== 'shine-esp-session-attach') {
throw new Error('Доверенное устройство вернуло неподдерживаемый payload. Для plugin нужен session-only approve.');
}
const login = String(payload?.login || state.settings.login || '').trim();
const approvedSession = payload?.session || {};
const sessionRecord = {
login,
sessionId: String(approvedSession?.sessionId || '').trim(),
sessionKey: state.requesterMaterial?.sessionKey || '',
sessionPrivPkcs8: state.requesterMaterial?.sessionPrivPkcs8 || '',
sessionType: Number(approvedSession?.sessionType || 50) || 50,
serverLogin: state.settings.serverLogin,
serverHttp: state.settings.serverHttp,
serverUrl: state.settings.serverUrl,
};
if (!sessionRecord.login || !sessionRecord.sessionId || !sessionRecord.sessionKey || !sessionRecord.sessionPrivPkcs8) {
throw new Error('Получен неполный session-only payload');
}
await clearSessionMaterial();
state.activeSession = sessionRecord;
await hydrateWalletProfile(login);
await saveActiveSessionRecord();
await persistSettings({
login: sessionRecord.login,
serverLogin: sessionRecord.serverLogin,
serverHttp: sessionRecord.serverHttp,
serverUrl: sessionRecord.serverUrl,
});
state.connectionOnline = false;
}
async function pollPairingStatus() {
if (!state.pairingId || !state.requesterMaterial) return;
try {
const payload = await ensureApi().getTrustedDeviceLoginStatus(state.pairingId);
const stateValue = String(payload?.state || '');
if (stateValue === 'created') {
state.pollTimer = setTimeout(() => {
void pollPairingStatus();
}, 2200);
return;
}
if (stateValue === 'approved') {
const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial);
await attachApprovedSession(decoded);
clearPairingState();
setStatus('Wallet-session создана и сохранена. Кошелёк остаётся офлайн до запроса подписи.', 'info');
return;
}
if (stateValue === 'rejected') {
clearPairingState();
setStatus('Заявка отклонена на доверенном устройстве.', 'error');
return;
}
if (stateValue === 'expired' || stateValue === 'canceled') {
clearPairingState();
setStatus('Ожидание подключения завершено.', 'error');
return;
}
state.pollTimer = setTimeout(() => {
void pollPairingStatus();
}, 2200);
} catch (error) {
clearPairingState();
setStatus(error.message || 'Не удалось проверить pairing-статус.', 'error');
}
}
async function startPairing({ login, usePassword, password }) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) {
throw new Error('Введите логин.');
}
await persistSettings({ login: cleanLogin });
await resolveServerForLogin(cleanLogin);
clearPairingState();
setStatus('Проверяем пользователя и создаём wallet-session заявку...', 'info');
const api = ensureApi();
const user = await api.getUser(cleanLogin);
if (user?.exists !== true) {
throw new Error('Пользователь не найден.');
}
state.requesterMaterial = await createRequesterPairingMaterial();
const passwordHash = usePassword
? await deriveEspPairingPasswordHash(cleanLogin, String(password || ''))
: '';
const payload = await api.startTrustedDeviceLogin({
login: cleanLogin,
passwordHash,
requesterSessionKey: state.requesterMaterial.sessionKey,
payloadType: 1,
});
state.pairingId = String(payload?.pairingId || '').trim();
state.expiresAtMs = Number(payload?.expiresAtMs || 0);
state.shortCode = String(payload?.shortCode || '0000000');
state.trustedSessionOnline = !!payload?.trustedSessionOnline;
if (!state.pairingId) {
throw new Error('Сервер не вернул pairingId.');
}
state.pollTimer = setTimeout(() => {
void pollPairingStatus();
}, 1800);
setStatus('Wallet-session заявка создана. Ожидаем подтверждение на доверенном устройстве.', 'info');
return {
pairingId: state.pairingId,
shortCode: String(payload?.shortCode || '0000000'),
expiresAtMs: state.expiresAtMs,
trustedSessionOnline: !!payload?.trustedSessionOnline,
};
}
async function cancelPairing() {
if (!state.pairingId || !state.requesterMaterial?.sessionKey) {
clearPairingState();
return { ok: true };
}
await ensureApi().cancelTrustedDeviceLogin(state.pairingId, state.requesterMaterial.sessionKey);
clearPairingState();
setStatus('Ожидание подключения отменено.', 'info');
return { ok: true };
}
async function disconnectSession() {
ensureApi().close();
state.api = null;
await clearSessionMaterial();
state.activeSession = null;
state.connectionOnline = false;
state.walletProfile = null;
state.signing = {
selectedKeyId: 'device',
selectedDeviceName: '',
devicesResolvedAtMs: 0,
};
setStatus('Сохранённая wallet-session удалена из plugin.', 'info');
return { ok: true };
}
async function refreshWalletDevices() {
if (!state.activeSession?.login) {
throw new Error('Сначала подключите wallet-session.');
}
await hydrateWalletProfile(state.activeSession.login);
const resumed = await resumeActiveSession({ keepConnected: true });
if (!resumed.ok) {
throw new Error(resumed.error || 'Не удалось открыть wallet-session.');
}
try {
const sessions = await ensureApi().listSessions();
state.walletProfile = {
...state.walletProfile,
homeserverSessions: mergeHomeserverStatuses(state.walletProfile?.homeserverSessions, sessions),
};
state.signing.devicesResolvedAtMs = Date.now();
if (!state.signing.selectedDeviceName && state.walletProfile.homeserverSessions[0]?.sessionName) {
state.signing.selectedDeviceName = state.walletProfile.homeserverSessions[0].sessionName;
}
await saveActiveSessionRecord();
setStatus('Список доверенных homeserver-устройств обновлён.', 'info');
return {
ok: true,
devices: state.walletProfile.homeserverSessions,
};
} finally {
ensureApi().close();
state.api = null;
state.connectionOnline = false;
}
}
async function updateSigningSelection({ selectedKeyId, selectedDeviceName } = {}) {
state.signing = {
...state.signing,
selectedKeyId: String(selectedKeyId || state.signing.selectedKeyId || ''),
selectedDeviceName: String(selectedDeviceName || state.signing.selectedDeviceName || ''),
};
await saveActiveSessionRecord();
return { ok: true };
}
async function prepareSignSignal() {
if (!state.activeSession?.login) {
throw new Error('Сначала подключите wallet-session.');
}
if (!state.signing.selectedKeyId) {
throw new Error('Не выбран ключ подписи.');
}
if (!state.signing.selectedDeviceName) {
throw new Error('Не выбрано устройство homeserver.');
}
const selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName);
if (!selectedDevice) {
throw new Error('Выбранное устройство не найдено в PDA аккаунта.');
}
setStatus(
`Каркас готов: запрос подписи должен идти через ${selectedDevice.sessionName}. Сам signaling подписи ещё не доделан.`,
'info',
);
return {
ok: true,
pending: true,
};
}
function snapshot() {
return {
settings: { ...state.settings },
pairing: {
active: !!state.pairingId,
pairingId: state.pairingId,
expiresAtMs: state.expiresAtMs,
shortCode: state.shortCode,
trustedSessionOnline: state.trustedSessionOnline,
},
session: state.activeSession ? { ...state.activeSession } : null,
connectionOnline: state.connectionOnline,
walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
signing: { ...state.signing },
status: {
text: state.statusText,
kind: state.statusKind,
},
};
}
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
(async () => {
const type = String(message?.type || '');
if (type === 'wallet:getState') {
await loadStateFromStorage();
sendResponse({ ok: true, state: snapshot() });
return;
}
if (type === 'wallet:saveSettings') {
await persistSettings(message?.payload || {});
sendResponse({ ok: true, state: snapshot() });
return;
}
if (type === 'wallet:resolveServerInfo') {
const result = await resolveServerForLogin(String(message?.payload?.login || '').trim());
sendResponse({ ok: true, result, state: snapshot() });
return;
}
if (type === 'wallet:startPairing') {
const result = await startPairing(message?.payload || {});
sendResponse({ ok: true, result, state: snapshot() });
return;
}
if (type === 'wallet:cancelPairing') {
const result = await cancelPairing();
sendResponse({ ok: true, result, state: snapshot() });
return;
}
if (type === 'wallet:resumeSession') {
const result = await resumeActiveSession();
sendResponse({ ok: true, result, state: snapshot() });
return;
}
if (type === 'wallet:refreshWalletDevices') {
const result = await refreshWalletDevices();
sendResponse({ ok: true, result, state: snapshot() });
return;
}
if (type === 'wallet:updateSigningSelection') {
const result = await updateSigningSelection(message?.payload || {});
sendResponse({ ok: true, result, state: snapshot() });
return;
}
if (type === 'wallet:prepareSignSignal') {
const result = await prepareSignSignal();
sendResponse({ ok: true, result, state: snapshot() });
return;
}
if (type === 'wallet:disconnectSession') {
const result = await disconnectSession();
sendResponse({ ok: true, result, state: snapshot() });
return;
}
sendResponse({ ok: false, error: 'UNKNOWN_MESSAGE' });
})().catch((error) => {
const message = toWalletErrorMessage(error, 'Unknown error');
setStatus(message, 'error');
sendResponse({ ok: false, error: message, state: snapshot() });
});
return true;
});
void loadStateFromStorage().then(async () => {
if (state.activeSession?.login) {
await hydrateWalletProfile(state.activeSession.login).catch(() => {});
setStatus(`Wallet-session сохранена для @${state.activeSession.login}. Подключение будет открываться только по действию.`, 'info');
}
}).catch((error) => {
setStatus(error?.message || 'Не удалось инициализировать wallet plugin.', 'error');
});

View File

@ -0,0 +1,20 @@
plugins {
id 'java'
}
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation platform('org.junit:junit-bom:6.0.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}

Some files were not shown because too many files have changed in this diff Show More