Compare commits

...

584 Commits

Author SHA256 Message Date
AidarKC
5720f1cb50 Переименовать экран предоплаченного места 2026-06-29 08:45:30 +04:00
AidarKC
9324da5cb7 Разделить покупку билета на два экрана 2026-06-28 15:11:05 +04:00
AidarKC
408b0eeb39 Закрыть проверку remote AddBlock через homeserver 2026-06-28 14:49:57 +04:00
AidarKC
ed83b1f906 Переделать remote AddBlock на сборку блока на homeserver 2026-06-28 14:45:29 +04:00
AidarKC
3068c3e2b8 Добавить раздел поддержки проекта Сияние 2026-06-28 13:53:12 +04:00
AidarKC
93c6f247f7 Исправить ответный SendSignal на ESP32 2026-06-28 12:29:01 +04:00
AidarKC
05a9441493 Исправить подпись SendSignal в UI 2026-06-28 12:09:13 +04:00
AidarKC
aa02e92e4d Добавить SendSignal и remote AddBlock через homeserver 2026-06-28 11:20:51 +04:00
AidarKC
c397c28acb Удалить старый мусор из документации 2026-06-28 10:29:27 +04:00
AidarKC
c93cc6c522 Перенести backlog в TODO 2026-06-28 09:30:59 +04:00
AidarKC
0cdcc77606 Добавить TODO с планами на будущее 2026-06-26 17:55:05 +04:00
AidarKC
87eec7e5c9 Зафиксировать успешную синхронизацию на тестовом сервере 2026-06-26 17:46:34 +04:00
AidarKC
44a1ba01f3 Починить восстановление blockchain_state при full resync 2026-06-26 17:30:10 +04:00
AidarKC
d49661fa29 Исправить хэш коммита в changelog блокчейна 2026-06-26 17:06:05 +04:00
AidarKC
71fdee0cfd Вернуть crash-safe запись AddBlock через tmp_bch 2026-06-26 17:05:37 +04:00
AidarKC
1ced351ea2 Закрыть проверенные pending-фичи 2026-06-26 16:56:57 +04:00
AidarKC
c048347f2e Добавить startup recovery для resync цепочек 2026-06-26 15:51:52 +04:00
AidarKC
be4f76834a Добавить resync блокчейна при рассинхроне 2026-06-26 15:25:11 +04:00
AidarKC
23edad416c Добавить sync-профиль пользователя и обход Solana RPC 2026-06-25 18:46:47 +04:00
AidarKC
f3e4233285 Исправить загрузку внешнего application.properties 2026-06-25 18:04:04 +04:00
AidarKC
84e0f039cb Добавить периодический sync блокчейнов каждые 12 часов 2026-06-25 17:58:07 +04:00
AidarKC
1f8b20a7d1 Добавить ListBlockchainHeads для межсерверной сверки 2026-06-25 17:52:04 +04:00
AidarKC
f0e1ab3af8 Перевести тестовый контур на t.shineup.me 2026-06-25 13:44:22 +04:00
AidarKC
112ab4d5d5 Улучшить звуки входящих личных сообщений 2026-06-25 12:55:42 +04:00
AidarKC
8768e142e3 Исправить мобильный ввод в личном чате 2026-06-25 11:20:30 +04:00
AidarKC
827d2e9c3e Улучшить открытие личного чата 2026-06-25 10:55:17 +04:00
AidarKC
0f3c4a621d Улучшить UX личного чата на мобильных 2026-06-25 10:46:45 +04:00
AidarKC
e60475f351 Обновить синхронизацию серверов и экран сохранения ключей 2026-06-24 20:18:40 +04:00
AidarKC
0f63f7dae6 Добавить документацию по Figma 2026-06-24 16:30:16 +04:00
AidarKC
f9a15ab192 Обновить список каналов и кнопку сообщения 2026-06-24 14:59:08 +04:00
AidarKC
77f5759d60 Обновить UI кошелька и регистрацию 2026-06-24 13:48:07 +04:00
AidarKC
684f3237cf Исправить подключение и подпись в браузерном кошельке 2026-06-24 10:28:54 +04:00
AidarKC
23e61cc182 ESP32 регистрация и архив тестового удаления PDA 2026-06-23 20:31:05 +04:00
AidarKC
d2f45ff67a ESP32: приостановить reconnect на экранах регистрации 2026-06-23 19:19:18 +04:00
AidarKC
06e12e9103 ESP32: убрать ранние checkpoint при add homeserver 2026-06-23 19:12:58 +04:00
AidarKC
29dddeff4f ESP32: убрать временный автозапуск homeserver 2026-06-23 19:03:32 +04:00
AidarKC
017d568aea ESP32: автозапуск Add Homeserver для отладки 2026-06-23 18:56:33 +04:00
AidarKC
c91b52cfd2 ESP32: checkpoint перед чтением PDA homeserver 2026-06-23 18:46:41 +04:00
AidarKC
2bd38d8d78 ESP32: диагностический checkpoint для update homeserver 2026-06-23 18:39:58 +04:00
AidarKC
7d9db68d80 ESP32: NTP для update user_pda 2026-06-23 18:32:32 +04:00
AidarKC
4b94303d67 ESP32: server login и NTP для регистрации 2026-06-23 18:11:11 +04:00
AidarKC
08628704c7 UI: добавить просмотр любого PDA 2026-06-23 17:05:57 +04:00
AidarKC
f1c1132690 AuthChallenge: поддержать RecoveryKeyBlock 2026-06-23 17:04:47 +04:00
AidarKC
d2426c473c ESP32: убрать кэш генерации секрета 2026-06-23 14:42:02 +04:00
AidarKC
66986b804c Вынести дефолтный сервер в конфиг UI и оживить счетчик пароля 2026-06-23 14:11:09 +04:00
AidarKC
95daa230bb Обновить серверный UI под recovery key 2026-06-23 13:44:18 +04:00
AidarKC
365b22d778 ESP32: кэш последних генераций секрета 2026-06-23 12:06:24 +04:00
AidarKC
cf2b54464e Исправить update user_pda для homeserver на ESP32 2026-06-23 11:42:44 +04:00
AidarKC
4e60c1274a ESP32: ускорить и упростить secret screen 2026-06-23 11:08:19 +04:00
AidarKC
2f65e63fbe ESP32: новая derivation ключей 2026-06-23 10:51:03 +04:00
AidarKC
b461431197 Смена адреса shine_users и выкладка на test2 2026-06-23 10:40:52 +04:00
AidarKC
5c92b6a734 Миграция PDA на client.key 2026-06-22 21:57:09 +04:00
AidarKC
ba348dafb3 Add Wallet Standard registration for browser wallet 2026-06-22 01:35:26 +04:00
AidarKC
ce2d310e8c ESP32 wallet RPC, browser wallet provider, and side panel 2026-06-22 01:30:08 +04:00
AidarKC
475db28095 main: это именно та версия, которая уже стоит на тестовом продакшен сервере
Да, звучит немного удивительно, но именно так: люди уже могут регистрироваться, писать друг другу личные сообщения, звонить и пользоваться каналами.

При этом регистрация пока идёт через тестовые Solana/devnet, так что это всё ещё этап тестирования. И в UI ещё куча всего не доделана.

Но тем не менее всё равно.

Прикольно.
2026-06-21 13:14:02 +04:00
AidarKC
823a41c027 chore idea vcs mappings cleanup 2026-06-21 13:10:51 +04:00
AidarKC
2a834f1b14 fix ui dm chatid lowercase normalization 2026-06-21 12:27:41 +04:00
AidarKC
c8ffb6cf29 Настроить test2 как основной контур деплоя 2026-06-20 23:34:41 +04:00
AidarKC
ecc9efd434 UI: скрыть пароль при режиме 12 слов 2026-06-20 21:33:44 +04:00
AidarKC
dd35e56029 Добавить временную бесплатную загрузку аватаров в Arweave 2026-06-20 21:29:35 +04:00
AidarKC
d0e7998650 UI: обновить экраны входа 2026-06-20 20:15:40 +04:00
AidarKC
fec5e49304 UI: FAQ регистрации и режим пароля из 12 слов 2026-06-20 19:05:45 +04:00
AidarKC
3b12e14e71 Docs: добавить идею homeserver команд и обмена файлами 2026-06-20 17:21:47 +04:00
AidarKC
86eaf2139d UI: улучшить личные сообщения и поиск контактов 2026-06-20 17:19:32 +04:00
AidarKC
65fad993ad UI: вернуть старую вкладку Личные и починить аватары в Связях 2026-06-20 16:43:53 +04:00
AidarKC
55e6e477be merge(main): объединить esp-and-wallet и UI Pixel 2026-06-20 12:17:11 +04:00
ba5efcc152 Merge: UI «Связи» (финал) + редизайн «Личные» (чистый прод) в main
- Граф «Связи»: обновлён до финальной версии поверх PR #3 «pixel-связи»
  (орбы 12/13.06, единый PNG-оверлей орбов, мягкий край, вибрация выключена).
- «Личные»: редизайн списка как формы «Связей» — фото-аватары/инициалы,
  золотая галочка подтверждения у имени, значок-цепочка связи с попапом пути
  (Ты → посредники → цель) и переходом в профиль, граница карточки по типу
  связи, шапка «Shine». Данные пока мок-плейсхолдер (реальные relations/чаты —
  отдельная задача с бэкендом).
- Чистый прод: сняты обе демо-лаборатории (граф/ЛС), demo-чат, гость-обвязка
  ЛС и demo-avatars; экран «Личные» под логином.
- Сохранена работа агента в main (DM-ревизии/редактирование, wallet/pairing, esp32).
- VERSION: client 1.2.217 (server 1.2.204 без изменений).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:10:59 +03:00
26253564d5 chore: bump client 1.2.169
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:31:10 +03:00
92791c77a9 прод: убрать ЛС-лабу (demo-чат, гость-доступ, isDemo, demo-avatars); мок→плейсхолдер
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:31:09 +03:00
465792b2ab прод: убрать граф-лабу (network/lab.js, selftest.js, граф-мок в mock-data)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:31:09 +03:00
de269fd828 chore: bump client 1.2.168 + .gitignore (.claude, бэкап-ассеты)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:48:28 +03:00
8c91484f37 nav: вкладка «Личные» (лейбл)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:48:27 +03:00
6904ac8b7c ЛС: редизайн списка (фото-аватары, галочка/значок связи у имени, попап-цепочка→профиль) + demo-чат и lab-маршрут
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:48:27 +03:00
aea6bbcb0e ЛС: токены связей + резолвер визуала + семантический мок (connectedVia/login)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:48:26 +03:00
AidarKC
a788d8bcf5 Обновить pairing устройств и доработать ESP32 UI 2026-06-19 20:47:56 +04:00
AidarKC
cc074a941f Исправить маршрутизацию call push по sessionId 2026-06-19 19:18:16 +04:00
AidarKC
47574100f9 Исправить самообрыв звонка и обновить TURN 2026-06-19 18:25:47 +04:00
7ad74942e0 Связи: отключена вибрация в графе
haptic() сделан no-op — на экране «Связи» телефон не вибрирует ни на тапах по узлам,
ни на переходах (раскрытие/погружение/всплытие/пан). Вызовы haptic(...) оставлены, тело пустое.
Версия 1.2.167.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:03:32 +03:00
ac1cc04637 nav: неоновые PNG-иконки вкладок бара + единый вид
- toolbar.js: data-driven иконки (iconImg/glow/hero); все 5 вкладок → <img> неон-PNG;
  «Связи» помечена hero.
- components.css: единый размер (--tab-icon-size 27px), «Связи» крупнее ВИЗУАЛЬНО через
  transform: scale (без сдвига раскладки — иконки на одной линии); active/tap-состояния;
  у «Связи» убран лишний drop-shadow-ореол (светится сама PNG); глобально
  -webkit-tap-highlight-color: transparent (нет синего tap-квадрата нигде).
- assets: icon_lichnye/kanaly/svyazi/uvedomleniya/profil.png. Иконка «Уведомления»
  приведена к прозрачному фону (была без альфы) и обрезана до ~92% заполнения, как у других.
Версия 1.2.166.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:03:04 +03:00
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
b4480d89cf css: убрать закомментированный блок кластерной ауры (финально)
Решение по ауре финальное — не возвращаем. Удалён закомментированный «ТЕСТ»-блок
box-shadow по категориям; оставлено явное box-shadow:none для сияющих/фокуса.
Версия 1.2.165.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:13:17 +03:00
ff584ba5d1 orb: мягкий край фото + убран фон-градиент категории из-под стекла
- растушёвка края фото радиальной маской (--feather-full 62% / --feather-edge 78%):
  фото сливается со стеклом без жёсткого ободка; параметры для подкрутки.
- убран фон-градиент категории на .node-dot (просвечивал через прозрачный центр
  стекла → читался как цветная «обводка»): селектор поднят до (0,4,0), чтобы
  перебить правила категории. Цвет категории остаётся на линиях.
- кластерная аура (box-shadow по категории) отключена.
Не тронуто: кромка PNG, свечение сияющих/фокуса/common, линии.
Версия 1.2.164.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:55:09 +03: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
69f0fdf120 orb: единый PNG-оверлей на все узлы + ретайр вектора
- glass_overlay_faithful.png в assets; орбы = фото (низ, круглая маска 78%) +
  стеклянный PNG (верх, бокс 119% от node-dot, контакт линий от ORB_R).
- PNG-оверлей применён ко ВСЕМ полным орбам (центр + спутники); tier-3 точки без изменений.
- ретайр мёртвого векторного стекла: удалён buildGlassOrb (+orbSeq) и CSS .fg-orb-svg,
  снят остаточный border .node-dot (синее кольцо) у PNG-хостов.
Версия 1.2.163.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:21:56 +03:00
e3bebff618 anim (13.06): ускорение разлёта узлов — BLOOM_MS 900→550
Дети «выстреливают» из центра почти вдвое быстрее; easing и каскад (stagger)
прежние. Убирает ощущение тяжести/«подтупливания» при смене центра.
Версия 1.2.162.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:36:46 +03:00
f19f7b0ec4 Связи (13.06): орбы по референсу, линии по категории, постоянная вселенная
Орбы:
- материал «хрусталь»: чистое лицо (виньетка 0.5→0.22), диффузный блик окна
  вместо «капли» (+мягкий blur sf), стекло прозрачнее (тело 0.38→0.3),
  полупрозрачная преломляющая кромка (blur + opacity 0.25→0.2).
- размер +11.5% (node-dot 52→58px); единый ORB_R=29 как источник радиуса.
- убран значок * у общих узлов (логика is-common цела).

Линии:
- цвет по категории на ВСЕХ рёбрах; плазма только сияющим.
- общий узел наследует сияние исходного человека (не серый).
- контакт линий ровно на кромке сферы орба (ORB_R), без зазора, все уровни.

Навигация:
- констелляция (паутина 2-3 уровней) — постоянный режим; кнопка «Вселенная»
  убрана; Семья/Друзья/Сияющие остаются фильтрами. Чистка осиротевшего CSS.

Версия 1.2.161.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:31:46 +03: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
0b4374141e Связи (test 12.06): центр-орб крупнее (FOCUS_SCALE 1.78) + шире ореол центра (glowSpread 7)
Рычаг 1: glowSpread центра 4.5→7 (мягче/шире свечение), спутники без изменений. Рычаг 2: FOCUS_SCALE 1.5→1.78 (иерархия). Версия 1.2.160.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:53:13 +03:00
652ddc9d88 Связи (test 12.06): SVG-стеклянные орбы аватаров + цвет/свечение линий глубоких связей
Аватары → SVG GlassOrb (фото в стеклянной сфере, блик, rim, свечение). Линии глубоких связей (tier-2/3) — в цвете типа (друзья/семья/...), сияющие связи светятся (голубой ореол + ядро). Версия 1.2.159.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:10:06 +03: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
AidarKC
01d9553db4 merge(ui): влить PR #3 pixel-связи в main 2026-06-11 00:24:36 +04:00
AidarKC
578b526f96 Скорректировать main к базе 553a1f1 и UI из Pixel 2026-06-10 23:03:01 +04:00
AidarKC
e3061b46f9 Связи: финальный вид сияющих связей
Перенести финальный плазменный рендер связей из коммита Pixel 2559f1e6 в main.
2026-06-10 19:00:22 +04:00
2559f1e66b Связи: финальный вид сияющих связей — плазма (поле+трубка+ядро, screen-blend) + цвет связи у обычных
Промежуточные итерации линий схлопнуты в один коммит. Убран мёртвый фильтр fg-plasma-turb. Лаборатория (/network-view/lab) и автотесты сохранены. Версия 1.2.158.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:09:33 +03:00
AidarKC
9ca469a075 solana: усилить проверку Pyth oracle в shine_payments 2026-06-10 02:25:45 +04:00
519bce6b78 Связи (pixel-aquarium, 10.06): партия 3 (лаборатория) — общие связи + доступность
Вариант 2 (всё в лаборатории, реальный путь /network-view не трогаем).

- Общие связи: среди друзей человека один помечен как «общий» (он и твой друг тоже) — золотой ободок
  + ★ (CSS .fg-node.is-common). В лаб-генерации addDeepLevels подставляет узнаваемого друга Ивана.
- Доступность: визуально скрытый (sr-only) текстовый список графа .fg-a11y (центр + связи 1-го уровня)
  для скринридеров; обновляется в updateA11y при перестроении (role=region, aria-label).

Автопроверки расширены до 19 ассертов (добавлены «общие связи ★» и sr-only список) — прогон 19/19 PASS.
Бамп client.version → 1.2.150.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:53:49 +03:00
557ea96be0 Связи (pixel-aquarium, 10.06): партия 2 (UI-фишки) — поиск, хлебные крошки, бейджи, цветовые кластеры
Всё в лаборатории (вариант 2: реальный путь /network-view не трогаем).

- Поиск + телепорт: строка .fg-search; Enter → graph.findNode(имя) → камера летит к узлу
  (dive в «Вселенной», иначе перецентр).
- Хлебные крошки: .fg-breadcrumb «Иван › Нина › Ада» (движок шлёт onDiveChange(path), API getDivePath);
  клик по корню — полный сброс, по предку — навигация на его уровень.
- Бейдж числа связей: .fg-node-badge (degreeById → updateBadges; у центра — число связей 1-го уровня).
- Цветовые кластеры: мягкая аура узла по типу связи (CSS is-family/friend/business/contact).

Автопроверки расширены до 17 ассертов (добавлены поиск/крошки/бейдж) — прогон 17/17 PASS.
Фикс: TDZ breadcrumbEl (объявлен до createForceGraph, т.к. onDiveChange вызывается при монтировании).
Бамп client.version → 1.2.149.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:43:02 +03:00
9a49cc67f0 Связи (pixel-aquarium, 10.06): партия 1 (полиш) + автопроверки графа
Усиления (движок-полиш) с детерминированной самопроверкой:
- Веер детей — полукругом «наружу» (DEEP_FAN, по sibIndex от направления деда→родитель): не перекрывает
  нить-крошку и родителя; равномерное распределение.
- LOD с гистерезисом (LOD_ZOOM_UP=1.6 / DOWN=1.4) — точки 3-го уровня ↔ аватарки без «мигания» у порога.
- Двойной тап по пустому фону и сильный pinch-out на минимальном зуме = быстрый выход из погружения.
- Префетч аватарок детей при наведении/нырке (prefetchChildren) — лица в кэше до раскрытия.

Автопроверки (dev-only, ТОЛЬКО при ?fgtest):
- js/pages/network/selftest.js — 14 ассертов: камера-центровка, collision (нет слипания), полукруг,
  spotlight (путь 1.0 / фон 0.25 / сброс при переключении / 100% на выходе), LOD, возврат зума.
- Движок: read-only graph.debugState() + graph.pumpForTest() (синхронно докручивает кадры до покоя,
  не зависит от троттлинга rAF в фоне). Граф как window.__fg — тоже только при ?fgtest.
- Прогон: 14/14 PASS (offset 0px, мин.дистанция детей 89px, веер ±99°, LOD 4/4).

В обычной работе тест-хелперы не активны. Реальный путь /network-view не затронут. Бамп client → 1.2.148.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:25:57 +03:00
3012f0799b Связи (pixel-aquarium, 10.06): фикс физики раскрытия + камера-наезд + железный spotlight
Исправлены критические баги «аквариума» по разбору (видео):

1. Слипание узлов → адаптивный радиус орбиты (фикс collision). Дети раскладываются на кольце
   ringR = max(baseR + радиус_родителя, число_детей×13): не лезут на (увеличенный зумом) родитель
   и друг на друга. Проверено: мин. дистанция 125px, 0 наложений (было — все в одной точке).

2. Умный наезд камеры на КЛИК по любому узлу (раньше 1-й уровень раскрывался на месте). diveTo
   центрирует узел (offset ~0), zoom 1.7; узел и дети растут до единого видимого размера
   (HERO_VISUAL/baseScaleOf, DIVE_CHILD_VISUAL) — крупно и читаемо. Наведение остаётся лёгким превью.

3. Железный Spotlight (единый активный путь): diveTo гасит ВСЕ прежние pin/hover, затем раскрывает
   только путь к цели. Открыто → путь=1.0, остальное=0.25; переключение веток сбрасывает прежнюю;
   exitDive/тап по Ивану → ВСЕ узлы гарантированно 1.0 + камера отъезжает. (Проверено программно.)

Реальный путь /network-view не затронут (вся глубина под tier≥2/hasDeep). Бамп client.version → 1.2.147.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:12:27 +03:00
7a8852f64b Связи (pixel-aquarium): Умный фокус (Smart Zoom / «аквариум») — погружение в узел + LOD
Новая ветка для безопасного отката (от pixel-web). Режим «Вселенная», только лаборатория.

1. Гибрид клика: 1-й уровень → раскрытие ветки НА МЕСТЕ (как раньше); 2-й уровень+ → ПОГРУЖЕНИЕ.

2. Dive (умный наезд камеры, «аквариум», без перестройки графа):
   - diveTo(node): пинит весь путь (предки до Ивана), ставит diveTargetId + diveZoom=1.7;
     камера в tick плавно ЛЕТИТ и ЗУМИТ, центрируя узел (DIVE_FLY_K), узел ВЫРАСТАЕТ (×2.1 ~ герой).
   - Глубина (contextTargetOf → depthScale/depthBlur/spotCur, лерп): Иван и боковые ветки
     УМЕНЬШАЮТСЯ (root ×0.55) + уходят в BLUR 3px + тускнеют до 0.25 → задний план «аквариума».
   - Нить-крошка: путь Иван→…→узел (divePathSet/onPath) горит ярким «световодом» — виден путь назад.
   - Всплытие: повтор клика по цели → exitDive (камера/зум плавно к корню); клик по Ивану →
     collapseAll (полный сброс + всплытие).

3. Pinch-to-Zoom + LOD 3-го уровня: при zoom≥1.55 видимые точки 3-го уровня дорисовываются как
   читаемые аватарки (лицо+имя; updateLod/setNodeLod — пере-рендер DOM на пороге), при отдалении —
   обратно в светящиеся точки. Узлам tier-3 добавлены фото-заглушки (pravatar) и имена.

Глубина — фейк-3D через масштаб + CSS-blur (GPU), без WebGL. Реальный путь /network-view не затронут:
dive только tier≥2 (в реале их нет), depthScale/Blur нейтральны по умолчанию, updateLod выходит при !hasDeep.
Бамп client.version → 1.2.146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:32:16 +03:00
f92e6c3cf1 Связи (pixel-web): фикс багов паутины — видимый 2-й уровень, удалён «прицел», toggle+spotlight
Исправлены замечания по видео (режим «Вселенная», только лаборатория):

1. «Невидимые друзья» (узлы 2-го уровня). Раньше — тусклые пустые кружки с инициалом.
   Теперь tier-2 — полноценные аватарки: фото-лицо (pravatar) + имя, DEEP2_SCALE 0.5→0.62
   (≈radius 16px), DEEP2_OPACITY 0.4→0.85; читаемый ободок и подпись (CSS).

2. Мусорный «прицел» (пунктирное кольцо у центра). Полностью удалён из движка:
   элемент .fg-reticle, updateReticle/pulseReticle и все их вызовы, CSS .fg-reticle*.
   На экране только аватарки и линии связей.

3. Логика toggle + spotlight:
   - Повторный клик по раскрытому узлу теперь СВОРАЧИВАЕТ его (isOpen = pinned || expandP>0.5
     → сброс pinned+hovered) — работает, даже если ветка была раскрыта ховером.
   - Spotlight: при закреплённой ветке остальные тускнеют до 0.25 (узлы и линии), фокус и
     закреплённая/наведённая ветка — 100%; плавно через spotCur (lerp, см. spotTargetOf).
   - Клик по центру (Иван) — collapseAll + возврат всему графу 100% яркости.

Реальный путь /network-view не затронут (deep-код под tier≥2/hasDeep). Ветка экспериментальная.
Бамп client.version → 1.2.145.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:55:10 +03:00
72dc83daff Связи (pixel-web): этап 2 паутины — hover-превью + collision + zoom + камера-доводчик + синхро-пульс
Доработка режима «Интерактивная паутина» (только лаборатория, deep-режим «Вселенная»):

Взаимодействие (по запросу): наведение ≠ клик.
- Hover-превью: навёл мышь/палец на узел — его ветка ВРЕМЕННО выплывает; убрал — втягивается.
  (pointerover/out для мыши, pointerdown/up для пальца → onNodeHover → graph.setHover; флаг hovered).
- Фиксация кликом: тап/клик → graph.toggleExpand ставит pinned — ветка остаётся раскрытой и
  после ухода курсора; повторный тап снимает фиксацию. Эффект = pinned || hovered (expandTargetOf).

Этап 2 «Мегамасштаб»:
- Collision-расталкивание: раскрытая ветка усиливает отталкивание соседей 1-го уровня
  пропорционально expandP (EXPAND_REPULSION=2.4) — кластеры разъезжаются, не накладываясь.
- Свободный зум: колесо мыши (onWheel) + щипок двумя пальцами (activePointers/pinching),
  zoom 0.55–2.6 «к точке»; мир — CSS-scale, линии (SVG) пересчитываются в экранных координатах × zoom.
- Камера-доводчик: при фиксации ветки, если её веер упирается в край, камера мягко дотягивается
  (glideCameraTo → camTargetX/Y, lerp CAM_GLIDE_K в tick); любой жест отменяет доводчик.
- Синхро-пульс: сияющие/трековые «световоды» дышат толщиной/размытием 3.6с в такт ободку узла.

Реальный путь /network-view не затронут: deep-код под tier≥2/hasDeep, hover-колбэк даёт только
лаборатория. Ветка экспериментальная (отдельно от pixel-08.06/PR). Бамп client.version → 1.2.144.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:34:26 +03:00
AidarKC
5981d3f871 solana: привязать singleton pda в shine_payments 2026-06-09 23:10:20 +04:00
AidarKC
fb0c5ad3f8 audit: сохранить отчёт по Solana 2026-06-09 23:09:21 +04:00
04d9d588e8 Связи (pixel-web): режим «Интерактивная паутина» — раскрытие веток без смены центра
Этап 1 mind-map (только лаборатория, deep-режим «Вселенная»):
- Отмена прыжков в центр: тап по периферийному узлу больше НЕ перецентрирует — он остаётся
  на орбите, а из него раскрывается/сворачивается (toggle) его ветка дальних связей НА МЕСТЕ.
- Глобальный сброс: тап по корню (Иван) рекурсивно сворачивает все раскрытые ветки (collapseAll).
- Глубина скрыта по умолчанию; ветка плавно выплывает (expandP, ~400мс) и втягивается по повтору.
- Мерцающие звёзды 3-го уровня (CSS box-shadow/brightness, десинхрон по узлам) — «созвездие».
- Тактильный отклик navigator.vibrate(): клик при нажатии, серия импульсов на bloom-раскрытие,
  щелчок «гитарной струны» при сильном натяжении нитей свайпом.
- Движок: API toggleExpand/collapseAll; убрана press/hover-логика раскрытия (заменена тапом).

Ветка экспериментальная (отдельно от pixel-08.06/PR), бамп client.version → 1.2.143.
Ещё не сделано (следующие этапы): collision-расталкивание веток, камера-доводчик, zoom,
синхро-пульс линий к сияющим.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:04:48 +03:00
AidarKC
e5fe925023 solana: защитить economy config в shine_users 2026-06-09 23:04:03 +04:00
345a21a211 Связи (эксперимент pixel-web): база — микро-взаимодействия + глубина
Фиксирую накопленные черновики как точку отката перед режимом «Интерактивная паутина»:
- press/pan-bend (резиновые нити при свайпе, тактильное «вдавливание» узла);
- глубина 2-3 уровней (прототип «Вселенная», переключатель в лаборатории);
- прогрессивное раскрытие (глубина скрыта, выплывает по нажатию/наведению).

Ветка pixel-web — экспериментальная (отдельно от pixel-08.06/PR), чтобы можно было откатиться.
Бамп client.version → 1.2.142.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:51:48 +03:00
AidarKC
b83e8c3979 solana: удалить неиспользуемый модуль common 2026-06-09 22:48:59 +04:00
AidarKC
0744ac3163 fix(ui): починить прямые ссылки профиля и связей 2026-06-09 22:32:02 +04:00
3de992d251 Связи: вернуть лабораторию в ветку (для просмотра графа на моках)
Откат удаления из aed64e7: lab.js снова в репозитории, статический импорт в network-view.js,
строка lab.js убрана из .gitignore. Лаборатория /network-view/lab нужна для демонстрации/проверки
графа на мок-данных без бэкенда. Бамп client.version → 1.2.141.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:18 +03:00
369ef61cab Связи: лаборатория только локально (исключена из репозитория)
- shine-UI/js/pages/network/lab.js убран из git (git rm --cached) и добавлен в .gitignore:
  на сервере лаборатория не нужна (там реальные данные), локально файл остаётся и работает.
- network-view.js: статический импорт lab.js заменён на ДИНАМИЧЕСКИЙ с фолбэком — если файла
  нет (прод/сервер), реальный экран «Связи» не ломается, а заход на /network-view/lab уводит
  на обычный экран. Локально лаборатория грузится как прежде.
- Документация фичи отражает, что lab.js — локальный (в .gitignore).
- Бамп client.version → 1.2.140.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:18 +03:00
e6e96c4b0d Связи: чистка мёртвого кода в движке (lerp, неиспользуемые easing)
- Убран механизм lerpX/lerpY: координаты для отрисовки берутся из n.x/n.y, lerp нигде
  не читался кроме условия заморозки (lerpSettling). Удалены поля, advanceLerp(), EDGE_LERP
  и lerpSettling — граф засыпает чуть раньше (без визуальных изменений; проверено: frozen=true).
- Удалены неиспользуемые cubicBezier() и EASE_BLOOM (easing теперь делает CSS); easeOutCubic
  оставлен (нужен в stepTween для фолбэк-центрирования).
- Документация фичи актуализирована (убрана заметка про lerp как кандидата на чистку).
- Бамп client.version → 1.2.139.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:18 +03:00
dc96033cb1 Связи: двухслойные линии-световоды, живой фон и стеклянные фильтры
- Сияющие связи — двухслойный неоновый «световод»: размытый glow (4px, blur 2px,
  opacity 0.4) + тонкий чёткий core (1.5px, #e0f7fc). Объёмное OLED-свечение,
  линия остаётся изящной. Оба слоя растут синхронно (общий dashoffset).
- Обычные линии — тоньше (1.0–1.2px) и глубокий уход в прозрачность (0.42 → 0.07),
  чтобы матовые связи не спорили с сияющими.
- Живой фон-«небула»: глубокое размытое сине-голубое облако под центром, медленная
  пульсация радиуса/яркости + переливы индиго↔ультрамарин (hue-rotate, 7с).
- Стеклянные чипы фильтров (frosted glass): rgba(255,255,255,0.03) + backdrop blur(12px)
  + граница 0.5px solid rgba(255,255,255,0.1); активный подсвечен сине-голубым.
- Бамп client.version → 1.2.138; документация фичи обновлена.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:17 +03:00
9ee6bf4380 Связи: полировка карты связей (свечение, прорастание линий, CSS-фильтры)
- Линии: тонкие дуги Безье (градиент неон-центр → цвет роли); связь к «сияющему»
  монолитно светится статичной тенью drop-shadow (без бегущих импульсов).
- Прорастание новых линий из центра: stroke-dasharray/dashoffset синхронно с
  разлётом узла (кончик трекает аватарку); старые линии исчезают мгновенно.
- Ghost-слой: только аватарки (без линий), 1000мс — нет висящих «ошмётков».
- CSS-bloom разлёта на компоновщике (устойчив к троттлингу rAF; завершение по таймеру).
- Сияющие узлы: мягкая медленная пульсация 3.6с (многослойная box-shadow + SVG-ореол);
  тестовые фото-аватарки.
- Фильтры слоёв в лаборатории + фикс перехвата click сценой (stopPropagation на чипах);
  фейд скрываемых на месте (opacity 0 + scale 0.8, 300мс), фиксация без физики (ноль тряски).
- Бамп client.version → 1.2.137; обновлена документация фичи.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:17 +03:00
e0f0726e68 Связи: интерактивная карта связей (force-directed graph)
Переработка экрана «Связи» в интерактивный нод-граф с премиальными переходами.

Движок (js/pages/network/force-graph.js):
- diffing-переходы: общие узлы перелетают, новые расцветают каскадом, исчезнувшие — Ghost-слой (800мс, на месте);
- мягкая радиальная пружина + отталкивание (органичная орбита), упругий влёт фокуса;
- динамическая вязкость на старте (трение 0.92→0.82, отталкивание ослаблено) — мягкий разлёт без тряски;
- жёсткая заморозка (kill-switch) при затухании — нет «треска», экономия батареи;
- линии — SVG <path> Безье (изогнутые нити), прорастание; жесты pan с инерцией;
- хард-лимит DOM-аватарок (остальное — SVG-точки).

Интеграция и UX:
- adapter.js: getUserConnectionsGraph → модель движка (сервер не трогаем, read-only);
- фильтры (Все/Семья/Друзья/Сияющие), контекстное меню (node-menu.js), нижний сниппет, профиль;
- прицел в центре, дыхание фокуса, свечение сияющих;
- лаборатория network-view/lab на мок-данных (networkGraphUsers) для тестов без бэкенда.

Документация: shine-UI/Dev_Docs/features/interactive-network-graph.md.
Бамп client.version 1.2.135 -> 1.2.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:16 +03:00
AidarKC
105a56499d merge(ui): влить pixel-08.06 в main 2026-06-09 22:20:49 +04:00
AidarKC
2bd27cd73b Связи: вернуть лабораторию в ветку (для просмотра графа на моках)
Откат удаления из aed64e7: lab.js снова в репозитории, статический импорт в network-view.js,
строка lab.js убрана из .gitignore. Лаборатория /network-view/lab нужна для демонстрации/проверки
графа на мок-данных без бэкенда. Бамп client.version → 1.2.141.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:23:40 +03:00
AidarKC
aed64e76a7 Связи: лаборатория только локально (исключена из репозитория)
- shine-UI/js/pages/network/lab.js убран из git (git rm --cached) и добавлен в .gitignore:
  на сервере лаборатория не нужна (там реальные данные), локально файл остаётся и работает.
- network-view.js: статический импорт lab.js заменён на ДИНАМИЧЕСКИЙ с фолбэком — если файла
  нет (прод/сервер), реальный экран «Связи» не ломается, а заход на /network-view/lab уводит
  на обычный экран. Локально лаборатория грузится как прежде.
- Документация фичи отражает, что lab.js — локальный (в .gitignore).
- Бамп client.version → 1.2.140.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:12:48 +03:00
AidarKC
abfd073de8 Связи: чистка мёртвого кода в движке (lerp, неиспользуемые easing)
- Убран механизм lerpX/lerpY: координаты для отрисовки берутся из n.x/n.y, lerp нигде
  не читался кроме условия заморозки (lerpSettling). Удалены поля, advanceLerp(), EDGE_LERP
  и lerpSettling — граф засыпает чуть раньше (без визуальных изменений; проверено: frozen=true).
- Удалены неиспользуемые cubicBezier() и EASE_BLOOM (easing теперь делает CSS); easeOutCubic
  оставлен (нужен в stepTween для фолбэк-центрирования).
- Документация фичи актуализирована (убрана заметка про lerp как кандидата на чистку).
- Бамп client.version → 1.2.139.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:20:00 +03:00
AidarKC
41edd1423c Связи: двухслойные линии-световоды, живой фон и стеклянные фильтры
- Сияющие связи — двухслойный неоновый «световод»: размытый glow (4px, blur 2px,
  opacity 0.4) + тонкий чёткий core (1.5px, #e0f7fc). Объёмное OLED-свечение,
  линия остаётся изящной. Оба слоя растут синхронно (общий dashoffset).
- Обычные линии — тоньше (1.0–1.2px) и глубокий уход в прозрачность (0.42 → 0.07),
  чтобы матовые связи не спорили с сияющими.
- Живой фон-«небула»: глубокое размытое сине-голубое облако под центром, медленная
  пульсация радиуса/яркости + переливы индиго↔ультрамарин (hue-rotate, 7с).
- Стеклянные чипы фильтров (frosted glass): rgba(255,255,255,0.03) + backdrop blur(12px)
  + граница 0.5px solid rgba(255,255,255,0.1); активный подсвечен сине-голубым.
- Бамп client.version → 1.2.138; документация фичи обновлена.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:01:36 +03:00
AidarKC
471fde78c1 ESP32: ужесточить touch UX и обновить инструкции 2026-06-09 18:30:12 +04:00
AidarKC
3e4759a0c9 Связи: полировка карты связей (свечение, прорастание линий, CSS-фильтры)
- Линии: тонкие дуги Безье (градиент неон-центр → цвет роли); связь к «сияющему»
  монолитно светится статичной тенью drop-shadow (без бегущих импульсов).
- Прорастание новых линий из центра: stroke-dasharray/dashoffset синхронно с
  разлётом узла (кончик трекает аватарку); старые линии исчезают мгновенно.
- Ghost-слой: только аватарки (без линий), 1000мс — нет висящих «ошмётков».
- CSS-bloom разлёта на компоновщике (устойчив к троттлингу rAF; завершение по таймеру).
- Сияющие узлы: мягкая медленная пульсация 3.6с (многослойная box-shadow + SVG-ореол);
  тестовые фото-аватарки.
- Фильтры слоёв в лаборатории + фикс перехвата click сценой (stopPropagation на чипах);
  фейд скрываемых на месте (opacity 0 + scale 0.8, 300мс), фиксация без физики (ноль тряски).
- Бамп client.version → 1.2.137; обновлена документация фичи.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:06:45 +03:00
AidarKC
b5276890fb ESP32: обновить подпись и выровнять индикаторы HOME 2026-06-09 17:52:33 +04:00
AidarKC
1488bc3d6d ESP32: доработать HOME экран и поток секретов 2026-06-09 17:44:56 +04:00
AidarKC
f4e7210a40 ESP32: добавить NAV v8 с account и Wi-Fi reconnect 2026-06-09 16:14:24 +04:00
AidarKC
e385bb6bf9 ESP32: зафиксировать промежуточный NAV v6 UI прототип 2026-06-09 15:22:45 +04:00
AidarKC
f56e531384 Связи: интерактивная карта связей (force-directed graph)
Переработка экрана «Связи» в интерактивный нод-граф с премиальными переходами.

Движок (js/pages/network/force-graph.js):
- diffing-переходы: общие узлы перелетают, новые расцветают каскадом, исчезнувшие — Ghost-слой (800мс, на месте);
- мягкая радиальная пружина + отталкивание (органичная орбита), упругий влёт фокуса;
- динамическая вязкость на старте (трение 0.92→0.82, отталкивание ослаблено) — мягкий разлёт без тряски;
- жёсткая заморозка (kill-switch) при затухании — нет «треска», экономия батареи;
- линии — SVG <path> Безье (изогнутые нити), прорастание; жесты pan с инерцией;
- хард-лимит DOM-аватарок (остальное — SVG-точки).

Интеграция и UX:
- adapter.js: getUserConnectionsGraph → модель движка (сервер не трогаем, read-only);
- фильтры (Все/Семья/Друзья/Сияющие), контекстное меню (node-menu.js), нижний сниппет, профиль;
- прицел в центре, дыхание фокуса, свечение сияющих;
- лаборатория network-view/lab на мок-данных (networkGraphUsers) для тестов без бэкенда.

Документация: shine-UI/Dev_Docs/features/interactive-network-graph.md.
Бамп client.version 1.2.135 -> 1.2.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:43:56 +03:00
AidarKC
32606fe1c2 ESP32: зафиксировать рабочий LVGL nav prototype и тесты 2026-06-08 18:39:11 +04:00
AidarKC
a8734846a0 ESP32: добавить LVGL тесты и отметить рабочий hybrid touch test 2026-06-08 16:56:00 +04:00
AidarKC
ad0edf3c88 ESP32: зафиксировать тесты и нерабочий LVGL/UI вариант 2026-06-08 15:35:27 +04:00
AidarKC
66975862f7 ESP32: временно отключить PIN-вход в subserver UI 2026-06-08 12:29:47 +04:00
AidarKC
b9185e761b ESP32: добавить UI сабсервера, PIN-ввод пока не работает 2026-06-08 12:25:54 +04:00
AidarKC
885cf463a7 Уточнить финальный экран devnet topup 2026-06-07 11:26:09 +04:00
AidarKC
689f35fea2 Доработать клиентский UI Solana-регистрации 2026-06-07 11:19:58 +04:00
AidarKC
6bf5d1d5ed Исправить клиентскую Solana-регистрацию после ухода от Anchor 2026-06-07 10:58:06 +04:00
AidarKC
b0b8c7a445 Перевести shine_payments на новый program id и подтвердить e2e 2026-06-07 10:00:39 +04:00
AidarKC
d25c19cdaa Исправить чтение state в shine_payments и описать e2e тест 2026-06-06 17:12:26 +04:00
AidarKC
89d06d317b Переписать shine_payments и обновить тестовый UI с известным багом state 2026-06-06 16:58:57 +04:00
AidarKC
c5ec32f87a Обновить Telegram-бота, документацию и связанные доработки 2026-06-06 13:45:02 +04:00
AidarKC
ce5c348023 Убрать v2 из economy seed shine_users 2026-06-05 11:42:21 +04:00
AidarKC
832eea5889 Переписать shine_users и shine_login_guard на чистый Rust 2026-06-04 23:05:45 +04:00
AidarKC
60049442f1 Зафиксировать все текущие изменения проекта 2026-06-04 22:27:09 +04:00
AidarKC
624557ebfd Удалить старый путь документа формата PDA 2026-06-04 22:17:33 +04:00
AidarKC
6b0379bfdc Добавить спецификации Solana программ и вынести формат PDA 2026-06-04 22:17:17 +04:00
AidarKC
a9510a6d36 Обновить lazy-import Solana PDA под новый формат 2026-06-04 14:33:42 +04:00
AidarKC
59e4156bb9 Удалить obsolete server UI и подчистить ссылки 2026-06-04 14:25:59 +04:00
AidarKC
de9606519a Починить native Ed25519 update_user_pda без OOM 2026-06-04 13:47:47 +04:00
AidarKC
eeb115584d Добавить диагностику server PDA и баланс device (не проверено) 2026-06-03 16:12:40 +04:00
AidarKC
ee3721dfa4 Исправить DEVNET topup и автоподстановку пароля 2026-06-03 15:57:49 +04:00
AidarKC
239cc231ea Снимок состояния перед фиксом DEVNET topup и автоподстановки пароля 2026-06-03 15:56:17 +04:00
AidarKC
4bd4df7b09 Добавить переход в server UI и DEVNET topup 2026-06-03 15:21:55 +04:00
AidarKC
d12371b84f Перенести server UI в shine-UI и объединить PDA-модуль 2026-06-03 15:11:26 +04:00
AidarKC
c97b3e3ec3 Снимок состояния до переноса серверного UI 2026-06-03 14:49:03 +04:00
AidarKC
2c2aad1355 Убраны непроверенные готовые фичи и перенесён QR-план 2026-06-03 14:20:45 +04:00
AidarKC
9949935bcc Добавить обработку длинных voice/audio в агент-боте 2026-06-03 00:18:30 +04:00
AidarKC
35fc6ebf62 Сделана компактная кнопка показа ключей в регистрации 2026-06-02 16:43:38 +04:00
AidarKC
d2205648e6 Сделана компактная кнопка скрытия ключей в UI 2026-06-02 16:40:58 +04:00
AidarKC
68ed93dd24 Переведены ключи UI в base58 и обновлены deploy defaults 2026-06-02 16:34:37 +04:00
AidarKC
a06b76b800 Обновлён server UI и приватные ключи переведены в base58 2026-06-02 15:52:22 +04:00
AidarKC
67f882b9bc Добавлен контур server-backup для shineup.me и регламент обновляемых бэкапов 2026-06-01 13:05:54 +04:00
AidarKC
17dc4981c6 Поправить Solana-программу регистрации пользователей
Шаг 1 — Rust (users.rs)

- Убран server_key: Pubkey из UserMutableFields и UserRecord.

- Добавлены address_format_type: u8 и address_format_version: u8 в соответствующие структуры.

- Добавлена константа BLOCK_VERSION_1: u8 = 1.

- Обновлен write_server_profile_block: версия блока = 1, убраны 32 байта server_key, добавлены 2 байта формата адреса перед server_address.

- Обновлен deserialize_record_from_pda для BLOCK_TYPE_SERVER_PROFILE: ожидается BLOCK_VERSION_1, чтение server_key убрано, добавлено чтение type/version формата адреса.

- Обновлены конструкторы UserRecord под новые поля.

- Обновлена документация формата: shine-solana/shine/doc/SHiNE-user-format-v.1.0.md.

- Синхронизированы связанные изменения UI/доков и VERSION.properties (client 1.2.109, server 1.2.101).
2026-05-31 22:25:33 +04:00
AidarKC
0179b25d12 Исправить дефолты деплоя на shineup.me
- deployServer: remoteHost по умолчанию = shineup.me.

- deployServer: путь localJar по умолчанию = build/libs/shine-server.jar.

- deployUI: REMOTE_HOST по умолчанию = player@shineup.me.

- VERSION.properties: client 1.2.107, server 1.2.100.
2026-05-31 20:36:34 +04:00
AidarKC
e3c1cbf1c0 Обновить UI каналов, логаут DM и документацию
- Исправлена вкладка Каналы: стабильные режимы Все/Мои, корректные кнопки и навигация назад.

- Зафиксирована доработка по личным сообщениям: при logout очищается локальная база/кеш DM на устройстве.

- Обновлены AGENTS/CLAUDE и документация Personal_Messages.

- Обновлены версии в VERSION.properties (client 1.2.106, server 1.2.99).
2026-05-31 20:30:31 +04:00
AidarKC
5899bd2f77 Убрал long-press меню каналов и обновил deploy-проверку sudo 2026-05-31 19:30:36 +04:00
AidarKC
1b0e1cf1d4 refactor: перенос серверных модулей в папку SHiNE-server 2026-05-30 17:12:15 +04:00
AidarKC
134e877b7c chore: состояние проекта перед переносом сервера в SHiNE-server 2026-05-30 17:10:21 +04:00
AidarKC
b75ac46781 Кошелёк Сияния: фактический расход с сервера, доработка UI и topup через compute budget 2026-05-30 11:53:36 +04:00
AidarKC
edc94d3700 WIP: кошелек запускается, но есть ошибки 2026-05-30 11:24:59 +04:00
AidarKC
b13efa92fd Добавить адаптацию голосовых ответов бота 2026-05-30 00:16:39 +04:00
AidarKC
6f796c98f7 Уточнить голосовые ответы агента 2026-05-29 23:53:59 +04:00
AidarKC
3a5856c7f0 Добавить кошелек блокчейна и озвучивание агента 2026-05-29 23:48:44 +04:00
AidarKC
775b655aac Логин guard: корректный precheck, company приоритет, hp в trademarks; подробные ошибки UI 2026-05-27 22:15:54 +04:00
AidarKC
101fd2eaa4 Solana-first регистрация: lazy-import пользователя при входе, AddUser отключен, UI ожидание 15с 2026-05-27 18:38:45 +04:00
AidarKC
6f0bb01b61 Промежуточный коммит: состояние до нормальной Solana-first регистрации 2026-05-27 18:33:26 +04:00
AidarKC
b345900459 docs: убрать выполненную задачу про кнопку создания канала из Future_Features 2026-05-26 00:46:58 +03:00
AidarKC
f1cfe9b6aa UI: обновлена шапка каналов, закрыты pending-задачи и обновлены версии 2026-05-26 00:30:49 +03:00
AidarKC
8941582d54 chore: зафиксированы все текущие изменения проекта 2026-05-25 23:46:54 +03:00
AidarKC
8c5de781ea API: задокументирован rawBlockB64 в GetMessageThread и обновлены версии 2026-05-25 23:43:43 +03:00
AidarKC
baef264bd0 Обновить формат Solana user PDA 2026-05-24 19:41:13 +03:00
AidarKC
74df7e2645 Добавить документацию Solana PDA и ESP32-подпроект 2026-05-24 19:29:42 +03:00
AidarKC
56cd90a197 Отключить репосты и добавить Solana-модуль 2026-05-24 12:16:39 +03:00
AidarKC
abdce05136 Удалить Java-реализацию агента-кодера 2026-05-24 09:30:25 +03:00
AidarKC
35565845ca Добавить канальный режим агента-кодера 2026-05-24 09:25:25 +03:00
AidarKC
a83ec2c971 Обновить сервис агента-кодера 2026-05-24 09:21:50 +03:00
AidarKC
4b371e142d Документировать API и сервис агента-кодера 2026-05-24 08:04:44 +03:00
AidarKC
aa2644d812 fix: считать шаг линии через lineStep перед AddBlock 2026-05-21 16:57:31 +03:00
AidarKC
fd99250882 feat: добавить репосты сообщений в каналах и тредах 2026-05-21 16:16:26 +03:00
AidarKC
5344c42ceb chore: очистить Pending_Features и обновить версии 2026-05-21 15:03:28 +03:00
AidarKC
21413268f3 Добавил гостевой режим, единые shine-ссылки и пометку о нестабильности мнений 2026-05-20 16:14:59 +03:00
AidarKC
aa35d87885 Добавить opinion-связи и обновить UI связей в профиле 2026-05-20 13:13:50 +03:00
AidarKC
a53444b863 Перенести UI-деплой в /home/player/SHiNE/shine-ui 2026-05-19 22:10:19 +03:00
AidarKC
4b0031fb08 Исправить UI-деплой в /var/www и права rsync через sudo 2026-05-19 21:55:21 +03:00
AidarKC
62b8534769 Исправить устойчивость UI-деплоя и проверку Caddy 2026-05-19 21:50:05 +03:00
AidarKC
f3262c2d64 Исправить edit/delete сообщений, упростить вкладки каналов и улучшить автоскролл DM 2026-05-19 21:00:29 +03:00
AidarKC
7986184111 chore(deploy): новый прод-контур 93.170.12.154, единые deploy task и docs 2026-05-19 16:27:09 +03:00
AidarKC
9c35567389 fix(dm): Ctrl+Enter перенос, время в списке и выравнивание карточек 2026-05-19 16:13:44 +03:00
AidarKC
8325cbec84 UI: DM список метаданных и Enter/Ctrl+Enter в чате 2026-05-19 15:50:42 +03:00
AidarKC
c6d310184b UI: упростить профиль и обновить UX чатов/шапок 2026-05-19 15:34:46 +03:00
AidarKC
83892d5093 UI: поднята фиксированная шапка в канале и треде 2026-05-19 15:05:29 +03:00
AidarKC
1e1cdd9e76 UI: шапка channel owner/name и унификация карточек треда 2026-05-19 14:22:28 +03:00
AidarKC
3e62a2a01c UI: компактная тёмная плитка автора и корректный переход в профиль 2026-05-19 14:18:20 +03:00
AidarKC
90d10086d7 UI: карточка автора в канале, профиль user и назад по истории 2026-05-19 13:58:49 +03:00
AidarKC
db2d9a666b UI: переход на history-router без # и короткие ссылки тредов 2026-05-19 10:15:15 +03:00
AidarKC
3a0899bcfe feat(ui): короткий роут m для тредов и восстановление заголовка канала 2026-05-19 01:05:25 +03:00
AidarKC
d13c60fca1 fix(thread): открывать ответ через channel-thread-view с hash 2026-05-19 00:49:22 +03:00
AidarKC
580bd6fbeb feat(thread): переход в тред ответа и явная история сверху 2026-05-19 00:47:12 +03:00
AidarKC
49ebf1605a fix(ui-thread): корректные поля channel для GetChannelMessages 2026-05-19 00:42:00 +03:00
AidarKC
f1fbb35296 fix(ui): открыть вкладку каналов по умолчанию и исправить резолв owner/channel 2026-05-19 00:41:19 +03:00
AidarKC
b85643ca33 docs(blockchain): актуализировать MVP-формат и правила изменения протокола 2026-05-19 00:23:10 +03:00
AidarKC
c27da63a3e chore: зафиксированы оставшиеся локальные изменения 2026-05-19 00:07:49 +03:00
AidarKC
a332ddc828 UI: отключение вкладки чатов и исправление загрузки каналов 2026-05-19 00:04:34 +03:00
AidarKC
ab31ccf6d8 UI: каналы 1..32, публичный type=1 и актуальный prevLine перед записью 2026-05-14 17:58:16 +03:00
AidarKC
01b38952e5 UI: исправить каналы и добавить MCP-док по чтению/дозаписи 2026-05-14 17:35:54 +03:00
AidarKC
0fdb5b245c Откат мультисессии: возвращен один активный сеанс 2026-05-14 17:11:24 +03:00
AidarKC
94263a46bd UI: мультиаккаунты профиля и улучшенный поиск каналов 2026-05-14 16:28:17 +03:00
AidarKC
56a69ab683 UI: отправка UI-ошибок, персональный публичный чат, русские pending-файлы 2026-05-14 14:16:03 +03:00
AidarKC
e73e103ac4 UI: заглушки уведомлений и правило intake в AGENTS 2026-05-14 13:32:53 +03:00
AidarKC
b8b33696ec Docs: добавить правило push через GITEA_TOKEN 2026-05-14 12:49:10 +03:00
AidarKC
c0b0c99f53 UI: обновить thread/counters, вкладку Каналы и сценарий просмотра+подписки 2026-05-14 12:46:22 +03:00
AidarKC
a2954071bd Channels/CreateChannel: выровнена версия формата на v3 + legacy fallback 2026-05-13 02:47:43 +03:00
AidarKC
76e4a6cba0 UI/Channels: вкладки по тапу + CreateChannel fallback для legacy формата 2026-05-13 02:42:57 +03:00
AidarKC
b55fd1571e Auth/UI: Argon2id derivation для login/register + блок Расширенные 2026-05-13 02:10:42 +03:00
AidarKC
8de4e95c6a UI: голосовой ввод/STT, TTS через OpenAI, настройки инструментов + учёт недопроверенных фич 2026-05-13 02:01:51 +03:00
AidarKC
ddeaf82bfd CreateChannel: оставить единый актуальный формат, убрать legacy v2/v3 2026-05-13 01:22:07 +03:00
AidarKC
e95f65ac78 Каналы: типы 0/1/100/200, CreateChannel v3, state для chat200, новые API и деплой на prod 2026-05-13 01:17:23 +03:00
AidarKC
97d37a2eb6 12-05-2026
Перенёс репо
2026-05-12 23:26:27 +03:00
AidarKC
a23d090bc1 Канал root 0 переименован в news в API и документации 2026-05-08 20:10:02 +03:00
AidarKC
4956ba7352 Ужесточение имен каналов и удаление legacy USER_PARAM для описания 2026-05-08 19:06:58 +03:00
AidarKC
acdd6c928b Каналы: новый роутинг, поиск, вход-возврат, удаление просмотров и документация 2026-05-08 01:15:54 +03:00
AidarKC
6774c26ea1 Звонки: preflight сессии перед вызовом и retry; таймаут вынесен в настройки 2026-05-05 18:11:55 +03:00
AidarKC
ef0cd2cb7d deployServer: проверка sudo -n только для systemctl сервиса 2026-05-02 19:06:31 +03:00
AidarKC
e921b06826 Звонки: дольше показывать статус занято; deployServer проверяет sudo -n 2026-05-02 19:05:54 +03:00
AidarKC
c0c29b74ab Звонки: WebPush incoming/stop, actions и TTL; обновлена логика 2026-05-02 18:25:44 +03:00
AidarKC
310863faec Checkpoint: первая рабочая версия звонков, сигналинг будет переделан 2026-05-02 18:13:22 +03:00
AidarKC
b05da86197 Удалён AckIncomingMessage и обновлена документация доставки 2026-05-02 17:02:57 +03:00
AidarKC
c44d755ce0 Добавлен TODO по будущим доработкам доставки почты 2026-05-02 16:52:38 +03:00
AidarKC
b7e6cf7437 Почта v2: ReceiveOutcomingMessage без авторизации и атомарная вставка пары 2026-05-02 16:46:22 +03:00
AidarKC
e73328461e Звонки: выбор одной callee-сессии и авто-закрытие входящего на других устройствах 2026-05-01 19:26:32 +03:00
AidarKC
db93eace30 WebRTC: строгая обработка сигналов (ICE до PC в очередь, ANSWER только для outgoing) 2026-05-01 18:16:01 +03:00
AidarKC
c5dfa47903 WebRTC: очередь ранних ICE, игнор дублирующего ANSWER и деплой UI 1.2.30 2026-05-01 18:08:43 +03:00
AidarKC
d96985303d Звонки/UI: освобождение микрофона после звонка, детальнее failed и возврат в Настройки после принудительного обновления 2026-05-01 18:05:39 +03:00
AidarKC
27bd47dbe0 Звонки: фиксы session fallback/registry, аналитика ICE/TURN, авточек UI-версии и перенос кнопки разработчика 2026-05-01 17:42:51 +03:00
AidarKC
e3377a48b3 Звонки: расширенная диагностика + экран настроек разработчика + обновление TURN-конфига 2026-05-01 16:39:03 +03:00
AidarKC
a2ed41514d Деплой UI: обязательная подстановка client.version из VERSION.properties 2026-05-01 16:09:01 +03:00
AidarKC
78ee5a60fa UI: кнопка помощи при проблеме обновления клиента + инкремент версий 2026-05-01 15:36:04 +03:00
AidarKC
bff403ea04 CallDeliveryReport: универсальный формат type/value и расширенные отчёты по звонкам 2026-05-01 15:09:20 +03:00
AidarKC
3061bf3d1e Добавлен SHiNE-promo-solana-devnet в основной репозиторий без вложенного git 2026-05-01 13:58:59 +03:00
AidarKC
9b03273055 Промо devnet: вечный код 0000, сообщение после пополнения и деплой-задачи 2026-05-01 13:55:29 +03:00
AidarKC
bdcab5ee05 Регистрация: wallet.key работает и с пустым паролем 2026-04-27 02:20:02 +03:00
AidarKC
14cc3be620 Регистрация: проверка реального баланса wallet.key перед продолжением 2026-04-27 01:50:41 +03:00
AidarKC
50da3e868d Регистрация Solana: промо-topup URL с wallet(base58) и порог 0.01 SOL 2026-04-27 01:44:07 +03:00
AidarKC
2c68dedea2 UI: фикс шапки связей и правки профиля (работает) 2026-04-26 20:34:12 +03:00
AidarKC
da12521517 UI: обновлены профиль/связи, статусы отношений и фикс верхней панели (работает) 2026-04-26 19:13:08 +03:00
AidarKC
28bbdb8b7c style(ui): уплотнить профиль и растянуть экран связей 2026-04-26 18:57:26 +03:00
AidarKC
1e8e2915f9 feat(network): поиск, help, история центра и fullscreen PWA 2026-04-26 18:50:36 +03:00
AidarKC
2350745e61 feat(profile): разделить просмотр и редактирование профиля 2026-04-26 18:31:38 +03:00
AidarKC
1fec6c7b54 feat(relations): spouse 40/41 и новый UX вкладки Связи (проверено) 2026-04-26 18:24:30 +03:00
AidarKC
3e10407afd fix(auth): вход сразу по логину, регистрацию отправлять при любом балансе (проверено) 2026-04-26 18:13:37 +03:00
AidarKC
3d5b5b2214 fix(ui): восстановить показ аватаров и корректный fallback 2026-04-26 09:31:56 +03:00
AidarKC
c094af920e fix(ui): исправить наложение fallback-букв на аватарах 2026-04-26 09:24:27 +03:00
AidarKC
dafdae5276 Настройки: блок Для разработчиков и ручная загрузка аватара в Arweave 2026-04-26 02:46:18 +03:00
AidarKC
df7f38bd0a Аватары: убрать инициалы при наличии txId и усилить загрузку старых файлов 2026-04-26 02:39:21 +03:00
AidarKC
4c1aeeeac8 Аватары: общий компонент, кэш txId и avatar.ar в графе связей 2026-04-26 02:27:41 +03:00
AidarKC
667c5310bf Добавить аватар профиля через Arweave и мастер загрузки 2026-04-26 01:41:09 +03:00
AidarKC
126cf2f5c3 Добавить SAWD-v1 и Arweave-кошелек в UI 2026-04-26 01:19:46 +03:00
AidarKC
c8fa4a01a1 Добавить версионирование схемы БД и заблокировать прод-очистку БД 2026-04-26 00:32:10 +03:00
AidarKC
e764a713c4 chore(deploy): перевести UI-деплой на белый IP 194.87.0.247 2026-04-23 20:16:12 +03:00
AidarKC
93aef6e18b UI: исправить автообновление и хронологию диалогов; обновить деплой-цели 2026-04-23 18:04:14 +03:00
AidarKC
f213e9aa43 feat(update): server push-команда на массовое обновление UI и формат версий 2026-04-23 16:19:00 +03:00
AidarKC
630ba30c27 chore(version): bump client/server до 1.2.1 после merge каналов 2026-04-23 15:47:48 +03:00
AidarKC
b2d3061796 Merge remote-tracking branch 'origin/user/channels-ui-read-unread-views-2026-04-23' 2026-04-23 15:46:44 +03:00
AidarKC
ecb018b32c feat(version): единый файл версий и отображение client/server в настройках 2026-04-23 15:37:39 +03:00
AidarKC
e9f58ca004 fix(ui): чат метаданные сообщений и корректная раскладка backlog type2 2026-04-23 13:41:13 +03:00
DrygMira
c0dfa6c7ab Channels UI + read/unread + unique views + style polish 2026-04-23 13:36:01 +03:00
AidarKC
78d6124f2a feat(update): проверка версии UI через Ping без периодических опросов 2026-04-22 19:57:59 +03:00
AidarKC
1a8d1c70fd feat(ui): зелёная кнопка ответа и автообновление PWA 2026-04-22 19:49:32 +03:00
AidarKC
e0333a9c32 chore(docs): восстановить случайно удалённый README 2026-04-22 19:26:05 +03:00
AidarKC
9a3bc9e488 fix(call): корректный mute, статус прямое/TURN и реавторизация WS 2026-04-22 19:25:52 +03:00
AidarKC
a905822515 feat(call): серверная выдача ICE/TURN и подключение в WebRTC 2026-04-22 18:11:47 +03:00
AidarKC
d7c7bb3c23 fix(call): кнопки входящего и автопереподключение после обрыва 2026-04-22 17:33:57 +03:00
AidarKC
97a2bee81a feat(ui): старт с личных сообщений и бейдж непрочитанных 2026-04-22 17:23:20 +03:00
AidarKC
58bbf063ca fix(ui): обработка устаревшей сессии и корректировка индикаторов соединения 2026-04-22 17:04:16 +03:00
AidarKC
0159dd9074 feat(ui): индикатор connected в тулбаре и панель переподключения 2026-04-22 16:53:36 +03:00
AidarKC
29a07a9a8b feat(call-ui): полноценное окно звонка, статусы, звуки и тех-история вызовов 2026-04-22 15:46:45 +03:00
AidarKC
c824fb5e9b fix(debug-api): починить сборку после merge и унифицировать буфер debug-логов 2026-04-22 15:03:14 +03:00
AidarKC
6a9746c17a Merge branch 'main' of https://github.com/ai5590/SHiNE-server
# Conflicts:
#	shine-UI/js/app.js
2026-04-22 14:58:28 +03:00
ai5590
af0e9e51e0 Merge pull request #17 from ai5590/codex/add-temporary-debug-api-endpoints
Добавил времеенный энд поинт для возможности легко писать и тестировать звонки. Add temporary debug HTTP API and client debug reporting; UI WS reconnect and debug call tooling
2026-04-21 20:07:45 +03:00
ai5590
5713287c84 Добавлен флаг debug.tempApi.enabled для временных debug API 2026-04-21 20:06:51 +03:00
ai5590
bd0c3dba50 Добавлен временный debug API для автотеста WebRTC и runbook 2026-04-21 19:52:25 +03:00
AidarKC
e63c53a855 feat(ui): кошелек на device.key и проверки серверов (тоже пока не проверено) 2026-04-21 03:12:22 +03:00
AidarKC
8be56192cb Добавить автообновление UI и нижний статус соединения 2026-04-21 01:58:46 +03:00
AidarKC
d07602b0a9 Добавить диагностику PWA/Push и endpoint тестового push 2026-04-21 01:10:56 +03:00
AidarKC
185ba5b1d3 наверное работает 2026-04-21 01:04:05 +03:00
AidarKC
2d48ae7a16 Добавить деплой без clean/тестов и доработки PWA/сессии в UI 2026-04-20 20:33:20 +03:00
AidarKC
bec1d08757 Исправлен ReferenceError chatId в chat-view при отправке read-receipt 2026-04-20 12:53:46 +03:00
AidarKC
cc59bd18ee WIP: новая схема сообщений и push (не проверено) 2026-04-19 20:41:58 +03:00
AidarKC
f0b560ec06 Связи UI: сияние и бейдж официального
- Добавлен визуальный эффект сияния вокруг круга для аккаунтов с флагом 'Сияющий'.

- Добавлен бейдж 'ОФ' над узлом для официальных аккаунтов.
2026-04-17 23:56:11 +03:00
AidarKC
ba3ee4290f Добавлено тестовое заполнение и доработки входа
- Добавлено расширенное тестовое заполнение данных через Seed_TestDataPopulation и SeedDataPopulationHelper (включая базовую схему для пользователей 1, 2, 3, их связи и профили).

- Убраны лишние проверки из тестового заполнения: сид теперь только заполняет данные.

- Исправлен WsTestClient: корректная сборка фрагментированных WS-сообщений до полного JSON.

- На странице входа по логину добавлена подсказка про основные тестовые логины 1, 2, 3 (вход без пароля).
2026-04-17 22:26:37 +03:00
AidarKC
30fcde5744 Уточнён термин close friend и улучшен UX связей
Что сделано:\n- В UI возвращён термин «Близкий друг» без пометки про «друга».\n- На графе связей добавлено меню узла с двумя действиями: «Показать информацию» и «Показать связи» (перенос узла в центр).\n- В модалке добавления связи реализован автопоиск логинов: Enter или пауза 2 секунды, до 5 подсказок, выбор кликом.\n- Добавлены стили для меню узла и списка подсказок.\n- В коде добавлены явные пояснения и alias-константы close friend (без изменения кодов 10/11 и логики):\n  CONNECTION_CLOSE_FRIEND / CONNECTION_UNCLOSE_FRIEND.\n- Обработчики чтения/записи связей переключены на alias close friend для лучшей читаемости.
2026-04-17 21:18:03 +03:00
AidarKC
4a92a7fa22 Добавлены родственные связи, расширен граф связей и улучшен локальный запуск
Что добавлено:\n- Новые типы CONNECTION для родственников: parent/child/sibling (50/51, 52/53, 54/55) в blockchain/db слоях.\n- Обновлены проверки ConnectionBody и DB-триггер connections_state для корректной записи/удаления новых связей.\n- В профиле добавлен блок "Близкие родственники" с модальным выбором типа связи и логина; добавление через AddBlock для parent/child/sibling.\n- Расширен API GetUserConnectionsGraph: out/in списки для родителей/детей/сиблингов, агрегированные списки родственников с полом, список allUsers с метками официальный/сияющий.\n- Полностью обновлен UI страницы "Связи": новое позиционирование родственников вокруг центра, отдельный цвет родственных связей, линия для взаимных связей и стрелка для односторонних, корректная геометрия линий при ресайзе.\n- Добавлена Gradle-задача startLocalWithBuild для запуска локального стека после build; сохранена отдельная startLocal без полного build.
2026-04-17 21:01:53 +03:00
AidarKC
9b188d56e9 17-04-2026
Сделал красивыми кнопки для изменения параметров на вкладке профиль
2026-04-17 17:51:03 +03:00
AidarKC
c7bf8051b9 17-04-2026
Добавил вкладку пол
2026-04-17 17:39:04 +03:00
AidarKC
7591fbdace 17-04-2026
Сделал что бы все пораметры пользователя получаличь врезодним запросом ListUserParams, а не по отдельности кучей разных
2026-04-17 15:49:36 +03:00
AidarKC
5d4d451943 merge: codex/outline-call-request-workflow into main 2026-04-16 01:27:48 +03:00
AidarKC
828820b6e4 14-04-2026
Веб пуш работает. Дальше попробую звонки добавить.
2026-04-15 22:38:43 +03:00
ai5590
eaad476bf5 Add MVP call signaling API and browser call flow 2026-04-15 09:33:37 +03:00
AidarKC
0b7691bdea 14-04-2026
Веб пуш работает. Дальше попробую звонки добавить.
2026-04-15 00:50:25 +03:00
AidarKC
21fbc8ffa0 14-04-2026
Промежуточный комит версии в которой ну хоть какието тестовые уведомления приходят. Но пока ещё вебпуш не работает
2026-04-14 23:53:54 +03:00
AidarKC
24be1d0c1f merge: add-web-push into main
# Conflicts:
#	shine-UI/js/router.js
#	shine-UI/js/services/auth-service.js
#	shine-UI/js/state.js
2026-04-14 22:35:28 +03:00
AidarKC
2830f75f65 merge: drygmira/main into main 2026-04-14 22:05:18 +03:00
AidarKC
c4a3ff6f68 14-04-2026
То что дела ай и то во что надо влить изменнеия
2026-04-14 21:55:57 +03:00
AidarKC
cfc92beec0 14-04-2026
То что дела ай и то во что надо влить изменнеия
2026-04-14 21:51:16 +03:00
DrygMira
cdfc416d02 Finalize remaining Channels updates 2026-04-14 13:49:03 +03:00
DrygMira
e17a6765ec Fix channel/author subscription confirmation and follow trigger persistence 2026-04-14 13:47:59 +03:00
DrygMira
7bdb3118ae Channels UI surgical cleanup and create description save fix 2026-04-14 10:36:50 +03:00
DrygMira
126b4ba3a1 channels ux cleanup and create-flow recovery 2026-04-14 02:08:44 +03:00
DrygMira
07e57b8563 chore: merge remote main baseline 2026-04-13 23:01:27 +03:00
DrygMira
a9c69e5947 feat: finalize channels fixes and runtime stability 2026-04-13 23:00:36 +03:00
DrygMira
1340c8e9c6 Initial commit 2026-04-13 19:51:35 +03:00
ai5590
62e55dbaec feat(dm): implement signed direct messaging with web push fallback 2026-04-12 19:34:55 +03:00
AidarKC
1ee2a1cf62 12 -04-2026
Сделал отдельную ветку для ai
2026-04-12 18:30:31 +03:00
AidarKC
ad45e005f5 fix(ui): исправление мелкого бага деплоя
Исправлен скрипт deploy_shine-PWA.sh: версия сборки теперь проставляется в staged-копии и обновляется только __SHINE_BUILD_HASH__, чтобы не ломать index.html при деплое.
2026-04-07 21:29:48 +03:00
AidarKC
0c7d8fac02 07-04-2026
Сделал вкладку параметры пользователя РАБОТАЮЩЕЙ

Добавил
- Локальный запуск
- техническое задание 1 (доработать ui что бы RFYFKS работали)
2026-04-07 14:43:08 +03:00
AidarKC
0b7ad79032 ДОАВИЛ В ГРАДЛЕ ЛОКАЛЬНЫЙ ЗАПУСК 2026-04-07 14:25:49 +03:00
AidarKC
e2a9caa07d Внёс изменения что бы постоянно не обновляло версию каждого JS файла и не создавало кучу шума 2026-04-07 14:01:29 +03:00
AidarKC
619d2145f9 Закомитил промежуточную почти работающую версию ... 2026-04-07 13:57:32 +03:00
AidarKC
3016d25f73 Закомитил промежуточную почти работающую версию ... 2026-04-07 13:57:09 +03:00
ai5590
4deaedf79f Trim profile param flow to AddBlock-only write path 2026-04-07 02:18:32 +03:00
AidarKC
3a412fcd51 Merge branch 'codex/add-personal-data-to-blockchain-api'
# Conflicts:
#	shine-UI/js/pages/profile-view.js
#	shine-UI/js/services/auth-service.js
#	task/2.md
2026-04-07 01:11:54 +03:00
AidarKC
d9e61e7c5b добаил автозаполнение тестовых пользователей 2026-04-07 01:05:33 +03:00
ai5590
f3e4651bd5 Профиль: рабочие переключатели official/shine и подтверждение блокчейн-записи 2026-04-07 01:03:33 +03:00
AidarKC
9cbff47194 Добавил массовое тестовое заполнение через API: A1..A10 и дружеские связи
- Переименован тест в Seed_TestDataPopulation (не IT_07_*)\n- Тест создаёт пользователей A1..A10 через AddUser\n- Дружеские связи формируются через AddBlock (CONNECTION_FRIEND)\n- Добавлен контроль количества друзей (A1=5, A2=7, A3=3)\n- Тест включён в обязательный запуск всех IT и в suite\n- Обновлена TASKS-документация по тестовым логинам
2026-04-06 10:28:22 +03:00
ai5590
525627c972 Профиль: реальные UserParam данные в правой вкладке и обновление через сервер 2026-04-05 20:39:17 +03:00
AidarKC
6d8777da83 Merge remote-tracking branch 'origin/main' 2026-04-05 14:49:25 +03:00
ai5590
8fb428ef0d Merge pull request #11 from ai5590/codex/add-personal-data-to-blockchain-api-1vsw7s
Docs: clarify `sessionId` format, recommend USER_PARAM keys for profile, and note missing direct-session RPC
2026-04-05 14:48:40 +03:00
ai5590
1bfe742278 task: добавлен русский чеклист проверки API-доков по USER_PARAM и sessionId 2026-04-05 14:48:26 +03:00
AidarKC
76b1131d95 Merge remote-tracking branch 'origin/main' 2026-04-05 14:43:11 +03:00
ai5590
670e7e9743 Merge pull request #10 from ai5590/codex/add-personal-data-to-blockchain-api
docs: document profile user-param keys and session-target messaging gap
2026-04-05 12:40:11 +03:00
ai5590
ff911636c7 Merge pull request #8 from ai5590/codex/add-session-closing-notifications-and-pwa-support
Add PWA / FCM push support (frontend + server) with direct messages and delivery tracking
2026-04-05 12:39:49 +03:00
ai5590
61f4c1e115 docs: clarify user params profile keys and sessionId format 2026-04-05 12:39:21 +03:00
AidarKC
3ded1da707 Merge remote-tracking branch 'origin/codex/add-session-closing-notifications-and-pwa-support' 2026-04-05 12:20:33 +03:00
AidarKC
c9e4b8dfbf Промежуточная версия
в которой надо дорабоать

1. Исправить ошибки и сделать что бы работала вторая слева вкладка. ТОесть АПИ для сервера я сделал (пока они возвращают весь список сообщений целиком - всем большим списком сообщений в канал - для мвп это устраивает,и по этому только три АПИ функции добавилось)

  Там какието ошибки на клиенте ( я только сгенерил код - но гдето вылетает) по UI можешь исправлять переделывать - моешь оставить калечное как есть - мне пока не важно. Важно увидить что каналы и сообщения и публичная переписка в каналах блокчейна работает

2. потестировать и сделать корректное завершение сессии (там есть глюки при завершении сесии)
2026-04-05 12:14:17 +03:00
ai5590
c08826e848 Merge pull request #9 from ai5590/codex/add-session-closing-notifications-and-pwa-support-7qeofz
Add PWA/FCM push, direct messaging and connections (client + server)
2026-04-05 12:13:17 +03:00
ai5590
bc8f4a0582 Add handoff task document for PWA, chats, and connections 2026-04-05 12:13:05 +03:00
ai5590
ad323d17a2 Add handoff task document for PWA, chats, and connections 2026-04-05 12:12:59 +03:00
ai5590
09566fdfde Add close-friend flow on network tab with server API 2026-04-05 12:12:55 +03:00
ai5590
91ed444c90 Allow first DM to any user and show real login in profile 2026-04-05 12:12:46 +03:00
ai5590
32c046233b Add WS push events, PWA/FCM scaffolding, and direct messaging MVP 2026-04-04 18:10:25 +03:00
AidarKC
cf5460c5c7 Промежуточная версия
в которой надо дорабоать

1. Исправить ошибки и сделать что бы работала вторая слева вкладка. ТОесть АПИ для сервера я сделал (пока они возвращают весь список сообщений целиком - всем большим списком сообщений в канал - для мвп это устраивает,и по этому только три АПИ функции добавилось)

  Там какието ошибки на клиенте ( я только сгенерил код - но гдето вылетает) по UI можешь исправлять переделывать - моешь оставить калечное как есть - мне пока не важно. Важно увидить что каналы и сообщения и публичная переписка в каналах блокчейна работает

2. потестировать и сделать корректное завершение сессии (там есть глюки при завершении сесии)
2026-04-03 11:45:42 +03:00
ai5590
c25393e3b6 Задание которое надо доделать 2026-04-03 11:06:52 +03:00
AidarKC
8a83ac85d9 Промежуточная версия
в которой надо дорабоать

1. Исправить ошибки и сделать что бы работала вторая слева вкладка. ТОесть АПИ для сервера я сделал (пока они возвращают весь список сообщений целиком - всем большим списком сообщений в канал - для мвп это устраивает,и по этому только три АПИ функции добавилось)

  Там какието ошибки на клиенте ( я только сгенерил код - но гдето вылетает) по UI можешь исправлять переделывать - моешь оставить калечное как есть - мне пока не важно. Важно увидить что каналы и сообщения и публичная переписка в каналах блокчейна работает

2. потестировать и сделать корректное завершение сессии (там есть глюки при завершении сесии)
2026-04-03 11:04:59 +03:00
AidarKC
78e62997d1 Промежуточный комит для отдачи задания брату 2026-04-03 10:50:44 +03:00
ai5590
c0fba4af94 Add channels IT coverage, live UI loading, and runbook 2026-03-30 16:06:28 +03:00
ai5590
9723696b2c Start server-side channel read RPC handlers and simplify API spec 2026-03-30 14:32:15 +03:00
AidarKC
eb5593c7be 30 03 25
Сделал адекватное отображение ключей / и при регистрации ключи спрашивают какие сохранять

(что то работает что то сложно)
2026-03-30 03:11:09 +03:00
ai5590
089146a137 Merge pull request #5 from ai5590/codex/connect-ui-client-to-server-for-authentication-8njihj
Add AuthService, WS client and key-vault; implement session-based auth flow and update auth UI/pages
2026-03-30 02:25:59 +03:00
ai5590
d4c8201a88 Merge branch 'plus_ui' into codex/connect-ui-client-to-server-for-authentication-8njihj 2026-03-30 02:25:51 +03:00
ai5590
538ec8ec73 Auto-resume active session on app reload 2026-03-30 02:25:14 +03:00
ai5590
52fa631733 Merge pull request #4 from ai5590/codex/connect-ui-client-to-server-for-authentication-xqvn1u
Add AuthService + crypto/key-vault + WS client and integrate real auth/session flows into UI
2026-03-30 02:16:35 +03:00
ai5590
284e962910 Merge branch 'plus_ui' into codex/connect-ui-client-to-server-for-authentication-xqvn1u 2026-03-30 02:16:20 +03:00
ai5590
6ba7a54921 Restore registration step flow and create new session on login 2026-03-30 02:09:44 +03:00
ai5590
ecd059ced2 Merge pull request #3 from ai5590/codex/connect-ui-client-to-server-for-authentication
Implement authentication backend (AuthService, WS client, crypto, key vault) and integrate session flows in UI
2026-03-30 01:52:31 +03:00
ai5590
4f825e2a86 Derive root/device/blockchain keys from password SHA-256 2026-03-30 01:52:12 +03:00
AidarKC
1bf1c768dd 30 03 25
Удалил наверное устаревшую  документацию
2026-03-30 00:53:45 +03:00
AidarKC
b33fa4aeaa 30 03 25
Добавил сайт с UI прямо сюда
2026-03-30 00:43:49 +03:00
ai5590
889ce0d921 Merge pull request #2 from ai5590/codex/analyze-block-addition-functionality-and-create-api-docs
Документация: добавление API `AddBlock` и раздела Blockchain в Dev_Docs
2026-03-30 00:38:39 +03:00
ai5590
1c9841b4a6 Merge branch 'main' into codex/analyze-block-addition-functionality-and-create-api-docs 2026-03-30 00:36:29 +03:00
AidarKC
99cf000f24 30 03 25
Добавил АПИ функцию которая возвращает информацию о версии сервера и о том что он работает
2026-03-30 00:34:16 +03:00
ai5590
3d780a2605 Split blockchain block-format docs by block type 2026-03-28 11:11:16 +03:00
AidarKC
1aabcf4d80 27 03 25
Доделал API функции для авторификации и работы с сессиями сервер и документ для разработчиков по

Авторификациии и серверам

Всё работает
2026-03-27 22:06:19 +03:00
AidarKC
51de9779e3 27 03 25
Доделал сервер и документ для разработчиков по

Авторификациии и серверам

Всё работает
2026-03-27 16:29:19 +03:00
AidarKC
2f9cf2bff1 27 03 25
Добавил документ для разработчиков (про сессии но не закончил) и исправил мекую ошибку с несопостовлениеминдексов 2
2026-03-27 14:59:52 +03:00
AidarKC
6d3719ba71 27 03 25
Добавил документ для разработчиков (про сессии но не закончил) и исправил мекую ошибку с несопостовлениеминдексов
2026-03-27 14:44:01 +03:00
ai5590
dabda362e6 Merge pull request #1 from ai5590/codex/create-dev_docs-folder-with-documentation
Добавил документацию от кодекса
2026-03-26 15:29:22 +03:00
ai5590
b23ecdfdf2 Add Dev_Docs with protocol, blockchain, and API design analysis 2026-03-26 15:27:59 +03:00
AidarKC
18bf5d65d7 Initial commit 2026-03-18 22:28:13 +03:00
AidarKC
37c36ffdba 19 02 25
сделал единый формат протокола в случае ошибок (Наверное сделал удобнее)
2026-02-19 18:06:03 +03:00
AidarKC
c7440e2b5c 19 02 25
Лобавил пинг
2026-02-19 17:33:58 +03:00
AidarKC
6949fd8a2f 06 02 25
Сделал отдельную команду что бы всё на сервер заливать
2026-02-06 16:51:18 +03:00
AidarKC
ef72719502 06 02 25
Сделал отдельную команду что бы всё на сервер заливать
2026-02-06 16:49:58 +03:00
AidarKC
a647091a3f 06 02 25
Сделал отдельную команду что бы всё на сервер заливать
2026-02-06 16:47:28 +03:00
AidarKC
b5706d3ed5 29 01 25
Зделал всё только на база64 без всяких урл сафе

Вроде всё работает
тест весь проходит
2026-01-29 19:29:08 +03:00
AidarKC
0b2bee0a3d 29 01 25
Удалил старые черновики
тест весь проходит
2026-01-29 18:30:55 +03:00
AidarKC
4cee326a25 28 01 25
додела запрос связи с друзьями
тест весь проходит
2026-01-29 17:33:30 +03:00
AidarKC
bf4cecde05 28 01 25
додела запрос связи с друзьями
тест весь проходит
2026-01-29 17:28:50 +03:00
AidarKC
84bef3365e 28 01 25
доработки по запросу связи друг
тест пока не весь проходит
2026-01-29 17:26:54 +03:00
AidarKC
922c18db4b 28 01 25
Добавил запрос связей пользователя - друзей - для построения графа друзей

И тест добавил.

но тест пока не весь проходит
2026-01-28 22:31:20 +03:00
AidarKC
22fb35d1d4 28 01 25
Добавил запрос поиска пользователей по начаоу логина.
И тест добавил.

Все тесты проходят.
2026-01-28 21:23:01 +03:00
AidarKC
ebf7c9f18e 28 01 25
Добавил запрос проверить есть ли в системе такой пользователь и получить его данные.

И тесты добавил.

Все тесты проходят
2026-01-28 20:33:06 +03:00
AidarKC
43b0efb4d3 23 01 25
Ура прошли все тесты новой версии авторификации!!

Осталось что то доделать поправить с лишними закрытиями сервака
2026-01-23 22:10:14 +03:00
AidarKC
4430615117 23 01 25
Промежуточный комит.
Ужас как устал сегодня, узнал что и запросы у меня постоянно закрывают сессию. Надо переделать
2026-01-23 21:52:45 +03:00
AidarKC
e84c63c3d1 23 01 25
Сделал авторификацию новую через sessionKey

(Но пока тесты сессии падают)
2026-01-23 20:50:58 +03:00
AidarKC
580695b486 23 01 25
Сделал ещё более два поля в общем формате блоков блокчейна (перед самим блоком данных) и перед его цп

(все тесты проходят)
2026-01-23 17:49:13 +03:00
AidarKC
9f1ca37977 23 01 25
Сделал более понятный названия у интерфейса  BodyHasLine

(всё работает)
2026-01-23 13:05:29 +03:00
AidarKC
c1964adb58 22 01 25
перенёс класс у вдругую папку
Вроде всё работает и тесты проходят.

И блоки добавляются все что надо для MVP
2026-01-22 02:19:25 +03:00
AidarKC
ad5525d88b 22 01 25
увеличел количество тестовых запросов добавления блоков

И вроде всё работает и тесты проходят.

И блоки добавляются все что надо для MVP
2026-01-22 02:18:12 +03:00
AidarKC
98d478531b 22 01 25
добавил комент
Да вроде всё работает и тесты проходят.

И блоки добавляются все что надо для MVP
2026-01-22 02:09:28 +03:00
AidarKC
a2495afa44 22 01 25
Да вроде всё работает и тесты проходят.

И блоки добавляются все что надо для MVP
2026-01-22 01:59:49 +03:00
AidarKC
3f5f94a53f 22 01 25
Да вроде всё работает и тесты проходят.

И блоки добавляются все что надо для MVP
2026-01-22 01:57:02 +03:00
AidarKC
97840a45d6 22 01 25
Счас попробую новое просто добавить от гпт
Патч работает добавление линий - ситуация сложная

тест падает
2026-01-22 01:20:51 +03:00
AidarKC
69cd33479b 15 01 25
Потч работает добавление линий - ситуация сложная

тест падает
2026-01-21 18:37:05 +03:00
AidarKC
376d42cd79 15 01 25
Доделал типы сообщений посты в линии и едиты на них.ответы на них
И ответы в другие блокчейны

(Все тесты тесты проходят)
2026-01-15 18:55:03 +03:00
AidarKC
b69075cbac 15 01 25
Добавил мелких доп проверок

(Все тесты тесты проходят)
2026-01-15 15:24:25 +03:00
AidarKC
bbca821dcd 15 01 25
Исправил что бы в интерфейсе BodyHasTarget не требывалось хранить в блоках  BodyHasTarget
и в блоках коннекстин не зранилась поле тоЛогин в байты блока.

(Все тесты тесты проходят)
2026-01-15 15:13:29 +03:00
AidarKC
d9fe1f02b8 15 01 25
Исправил что бы в интерфейсе BodyHasTarget не требывалось хранить в блоках  BodyHasTarget
и в блоках коннекстин не зранилась поле тоЛогин в байты блока.

(Все тесты тесты проходят)
2026-01-15 15:09:45 +03:00
AidarKC
5fe41c7656 13 01 25
мелкие исправления. Убрал оставшиеся странные связи линии
2026-01-13 17:54:10 +03:00
AidarKC
9cf6fabe64 13 01 25
мелкие исправления 2
2026-01-13 17:46:23 +03:00
AidarKC
cd0352f904 13 01 25
мелкие исправления
2026-01-13 17:34:30 +03:00
AidarKC
fa30bd2a49 13 01 25
Перевёл блокчен на новый формат!
Все тесты проходят!!

Может в каких то деталях/мелочах ещё что то не так (не смотрел подробно), но в общем выглядит всё хорошо.

И главное работает!
2026-01-13 16:28:30 +03:00
AidarKC
e9e05c1192 13 01 25
Переписал код кучи классов перешёл на новый надеюсь теперь подходящий формат блоков

и тесты переделал.

Но пока остались баги и тесты не проходят (в частности пользователи не создаются - ошибка в бд)
2026-01-13 16:18:38 +03:00
AidarKC
b7025dde59 13 01 25
Запрос подписок, но это версия уже не актуальна тк дальше буду переделывать блоки под новый формат
2026-01-13 12:44:45 +03:00
AidarKC
973a632b85 09 01 25
На этом с форматом разобрались и отложили всё на праздничные выходные
2026-01-10 01:47:00 +03:00
AidarKC
9d0da4b39f 08 01 25
Список каналов возвращает - хотя сырое всё как то - но всё работает :)
2026-01-09 00:56:05 +03:00
AidarKC
aba86fc687 08 01 25
ещё помелочи
2026-01-09 00:03:40 +03:00
AidarKC
4c87207129 08 01 25
Помелоги поменял
2026-01-08 23:32:17 +03:00
AidarKC
7a167b470a 08 01 25
Сделал что бы при создании пользователя передавались три ключа пользователя. И имя блокчейна сделал через "-"
2026-01-08 15:02:01 +03:00
AidarKC
1ea5390771 08 01 25
Переименовал везде loginKey в solanaKey
2026-01-08 14:44:47 +03:00
AidarKC
a218f6586d 08 01 25
Навёл порядок в тестах. и доки дописал.
Всё красиво и работает!
2026-01-08 14:25:37 +03:00
AidarKC
4753b83831 08 01 25
Навёл порядок в тестах.
Всё красиво и работает!
2026-01-08 14:21:55 +03:00
AidarKC
a2626dfdd0 08 01 25
Добавил счётчик сколько раз изменялось сообщение
2026-01-08 14:13:44 +03:00
AidarKC
8e19486cf5 08 01 25
Тесты почти переделал
2026-01-08 14:12:16 +03:00
AidarKC
e2b89da2fa 08 01 25
Вынес константы, начал переделаывать тесты
2026-01-08 13:24:55 +03:00
AidarKC
f1af2bd4d4 07 01 25
вынес константы SHiNe
2026-01-08 00:02:43 +03:00
AidarKC
1c94bb25a6 07 01 25
сделал тест и он работает на то что бы изменить тект сообщения
2026-01-07 23:50:16 +03:00
AidarKC
06c77b1c1f 07 01 25
refactor: перевели хэши на BLOB и добавили поля block_hash / block_signature / edited_by_block_global_number

и главное добавили тип блока изменение сообщение и сслку на последнее изменение в табл блокс
2026-01-07 19:58:50 +03:00
AidarKC
8bcaa192c5 05 01 25
Добавил текст описание форматов блокчейна
2026-01-07 18:16:14 +03:00
AidarKC
8fd7f4676b 05 01 25
поменял все названия таблиц и полей в таблицах на стиль только маленькие буквы и разделение через "_"  . Все тесты проходят норм
2026-01-06 01:49:26 +03:00
AidarKC
93c007b2b9 05 01 25
добавил таблицу message_state и тригеры который считает в ней актуальное количество всех лайков и ответов на сообщения! (и всё рабоатет - тесты проходят)
2026-01-06 01:11:29 +03:00
AidarKC
eb922d918b 05 01 25
добавил таблицу connections_state и тригер который ведёт в ней актуальное состояние всех связей! (и всё рабоатет - тесты проходят)
2026-01-06 00:30:37 +03:00
AidarKC
7ba333bf6c 05 01 25
добавил новые типы связи - тоесть возможность добавлять убирать друга, контакт или подписку  (и тесты и всё работает)
2026-01-06 00:24:24 +03:00
AidarKC
94777c58c6 05 01 25
Документы: Описание БД и всех запросов актуализировал!
2026-01-05 17:17:39 +03:00
AidarKC
a6a5089379 05 01 25
Запрос для работы с параметрами пользователя работают!! И тесты на них проходят!!
2026-01-05 16:45:37 +03:00
AidarKC
eb122456ab 05 01 25
Дабавил и два запроса на получение параметров, но пока не работает
2026-01-05 16:42:32 +03:00
AidarKC
55d34e2a87 05 01 25
Дабавил и два запроса на получение параметров, но пока не проверял
2026-01-05 16:14:14 +03:00
AidarKC
bfffe44c4a 05 01 25
Дабавил соранение параметра пользователя. (бд и запрос на добавление), но пока не проверял
2026-01-05 16:11:54 +03:00
AidarKC
dd49c4de00 02 01 25
Сделал что бы в базу писался msgSubType и поля to (to login, toBlockchainName и т.д.)
2026-01-02 20:15:59 +03:00
AidarKC
eef760d776 02 01 25
улучшил скрипт склейки файлов (что бы он сразу в буфер кидал результат)
2026-01-02 19:52:53 +03:00
AidarKC
fa019bcb4f 02 01 25
ОГО доделал и тесты (теперь два пользователя добавляется и меж ними есть связи) вроде всё работает
2026-01-02 19:09:56 +03:00
AidarKC
432b574592 02 01 25
ОГО доделал и тесты (теперь два пользователя добавляется и меж ними есть связи) вроде всё работает
2026-01-02 19:09:17 +03:00
AidarKC
c3d20ba338 02 01 25
Доделал тесты и названия линий сделал в константы

Дальше делать:
Описание форматов.
Запросы клиент-сервер.
Промт на клиента.

---
Потом в сервак дописать
Синхронизацию серверов.
2026-01-02 18:52:19 +03:00
AidarKC
be7a3ab7a6 02 01 25
Добавил боди для параметров пользователя

Дальше делать:
Описание форматов.
Запросы клиент-сервер.
Промт на клиента.

---
Потом в сервак дописать
Синхронизацию серверов.
2026-01-02 18:44:35 +03:00
AidarKC
272d7ca1be 02 01 25
Добавил боди для связей

Дальше делать:
Описание форматов.
Запросы клиент-сервер.
Промт на клиента.

---
Потом в сервак дописать
Синхронизацию серверов.
2026-01-02 18:16:59 +03:00
AidarKC
05a4714fb1 02 01 25
Добавил боди для связей

Дальше делать:
Описание форматов.
Запросы клиент-сервер.
Промт на клиента.

---
Потом в сервак дописать
Синхронизацию серверов.
2026-01-02 17:18:13 +03:00
AidarKC
59e5df0dd3 02 01 25
Сделал тесты с ответами на сообщения

Дальше делать:
Описание форматов.
Запросы клиент-сервер.
Промт на клиента.

---
Потом в сервак дописать
Синхронизацию серверов.
2026-01-02 17:06:07 +03:00
AidarKC
771758c831 02 01 25
Переделал тест добавления блоков в новый формат ( стло удобнее)

Дальше делать:
Описание форматов.
Запросы клиент-сервер.
Промт на клиента.

---
Потом в сервак дописать
Синхронизацию серверов.
2026-01-02 16:54:13 +03:00
AidarKC
ca55bfca93 02 01 25
Добавил поле subType и исправил мелкие баги (все тесты работают)

Дальше делать:
Описание форматов.
Запросы клиент-сервер.
Промт на клиента.

---
Потом в сервак дописать
Синхронизацию серверов.
2026-01-02 16:42:15 +03:00
AidarKC
71f1a6179c 31 12 25
Стабильная версия сервера  0.2
Пакует блокченй в файл и бд, проверяет форматы.

Дальше делать:
Описание форматов.
Запросы клиент-сервер.
Промт на клиента.

---
Потом в сервак дописать
Синхронизацию серверов.
2025-12-31 21:50:16 +03:00
AidarKC
c13940216b 31 12 25
тест
2025-12-31 21:41:58 +03:00
AidarKC
f17d077f25 31 12 25
Сделал что бы запускалось. Поправил мелкие ошибки
2025-12-31 21:10:05 +03:00
AidarKC
62ea49d1fc 30 12 25
Ура работает всё под новую таблицу. И все тесты проходят!!
2025-12-30 13:03:03 +03:00
AidarKC
f653689112 30 12 25
Ура работает всё под новую таблицу 2
2025-12-30 12:55:21 +03:00
AidarKC
df03f3f4ba 30 12 25
Ну типо переделал Всё под короткую таблицу солана юзерс, но теперь не надо поправить баги
2025-12-30 12:41:06 +03:00
AidarKC
34e8640e78 30 12 25
Ну типо переделал Всё под короткую таблицу солана юзерс, но теперь не надо поправить баги
2025-12-30 12:39:55 +03:00
AidarKC
b6b50557a7 29 12 25
добавил toString для классов Body
2025-12-29 15:31:20 +03:00
AidarKC
08d90b6e8e 29 12 25
исправил микро баг
2025-12-29 15:10:14 +03:00
AidarKC
43a26007d6 29 12 25
Тесты работают - запускаются из специальной кнопки - возле билд.

Не совсем всё по стандарту но и это очень хорошо и удобно!
2025-12-29 15:03:42 +03:00
AidarKC
7fdc890a85 29 12 25
В общем тесты работают. Только запускать их надо из специального класса :)
2025-12-29 14:51:13 +03:00
AidarKC
ae3838ccf2 29 12 25
Ура линии работают тесты прошли! Третий тест в лог выводиться!
2025-12-29 14:14:53 +03:00
AidarKC
783b5b08e3 29 12 25
Ура линии работают тесты прошли! 2
2025-12-29 14:07:20 +03:00
AidarKC
015caec01c 29 12 25
Ура линии работают тесты прошли!
2025-12-29 13:53:22 +03:00
AidarKC
526e2d9cc4 28 12 25
Всё ещё не работает проверка линий.
Переделываю тесты понял что нетак в сервере. Дальше буду исправлять сервак.
2025-12-29 13:33:26 +03:00
AidarKC
3f374f48e1 28 12 25
Всё ещё не работает проверка линий.
Переделал первые два теста! Третий (АддБлокс) ещё не работает
2025-12-29 13:16:00 +03:00
AidarKC
795341dd8d 28 12 25
Всё ещё не работает проверка линий. Залил старые тесты какие есть - каке есть
2025-12-29 12:13:12 +03:00
AidarKC
c523816cdf 28 12 25
Вроде как сделал работу с линиями :) но ещё не тестил
2025-12-28 20:11:31 +03:00
AidarKC
b26e09904a 28 12 25
Старая версия. Вроде там тесты доделал что бы работали
2025-12-28 19:48:23 +03:00
AidarKC
1526392ca5 28 12 25
Старая версия. Вроде там тесты доделал что бы работали
2025-12-28 18:39:13 +03:00
AidarKC
809d897da6 26 12 25
Исправил баг что пир повторном подключении по одной сессии всё будет норм работать
2025-12-26 13:12:53 +03:00
AidarKC
eeb8ee9069 25 12 25
Сделал три теста на общий формат
2025-12-25 17:16:15 +03:00
AidarKC
25aa57dc5e 25 12 25
Добавил восстановление на случай застрявших темп файлов.  Оно работает!  И уведомление админа о критических ощибках, если файлы блокчейна поврежденны
2025-12-25 17:08:08 +03:00
AidarKC
6c2449f623 25 12 25
Добавил восстановление на случай застрявших темп файлов. Оно работает!
2025-12-25 16:42:40 +03:00
AidarKC
d8057807a3 25 12 25
Вроде заработало!!
2025-12-25 16:10:33 +03:00
AidarKC
e532401a75 25 12 25
Добавил логгер в настройки.2
2025-12-25 16:05:25 +03:00
AidarKC
f8cc12560e 25 12 25
Добавил логгер в настройки.2
2025-12-25 16:00:57 +03:00
AidarKC
c8ee9925a1 25 12 25
Добавил логгер в настройки.Омталось созранение стате в бд поправить
2025-12-25 14:53:08 +03:00
AidarKC
d460ea2952 25 12 25
Дорабатываю добавление блоков. Промежуточный комит2.Омталось созранение стате в бд поправить
2025-12-25 14:32:58 +03:00
AidarKC
e1b2c62231 25 12 25
Дорабатываю добавление блоков. Промежуточный комит
2025-12-25 14:15:26 +03:00
AidarKC
bead78b372 24 12 25
Дорабатываю добавление блоков.
2025-12-24 17:29:50 +03:00
AidarKC
4e14f300f9 24 12 25
Дорабатываю добавление блоков.
Убрал лишние старые классы
2025-12-24 17:11:29 +03:00
AidarKC
4759521176 24 12 25
Дорабатываю добавление блоков.
Улучшил класс записыватель в БД.
2025-12-24 17:01:10 +03:00
AidarKC
a309b6f3ef 24 12 25
Дорабатываю добавление блоков. Ура добавилось.
Объеденил в один Хэндлер и сделал атомарную запись в БД.
2025-12-24 16:42:26 +03:00
AidarKC
834cf98ef9 24 12 25
Дорабатываю добавление блоков. Ура добавилось. Осталось порядок навести и добавление файлов сделать и откат.
2025-12-24 14:55:30 +03:00
AidarKC
80ea016687 24 12 25
Дорабатываю добавление блоков. Промежуточный исправления (не работают)
2025-12-24 14:22:50 +03:00
AidarKC
5ecaf67bcb 24 12 25
Дорабатываю добавление блоков. Поставил todo что доделать
2025-12-24 14:08:40 +03:00
AidarKC
33635886e0 23 12 25
Дорабатываю добавление блоков! Вроде всё.  (но ещё не проверял и тестов нету)
2025-12-24 11:05:25 +03:00
AidarKC
bba4b7fb41 23 12 25
Дорабатываю добавление блоков! Вроде всё.  (но ещё не проверял и тестов нету)
2025-12-23 16:14:25 +03:00
AidarKC
26afcb892a 23 12 25
Дорабатываю добавление блоков! Вроде всё.Осталось ещё размер уточнить что без хэш и пподписи  2
2025-12-23 16:12:36 +03:00
AidarKC
9633e3528d 23 12 25
Дорабатываю добавление блоков! Вроде всё.Осталось ещё размер уточнить что без хэш и пподписи
2025-12-23 15:58:54 +03:00
AidarKC
62e4338e88 23 12 25
Дорабатываю добавление блоков!
2025-12-23 15:48:23 +03:00
AidarKC
d949895fec 23 12 25
Сессии работают пользователи добавляются. И тесты красиво выводият коменты. Старый класс теста больше не нужен
2025-12-23 14:55:18 +03:00
AidarKC
b5fa05a660 23 12 25
Сессии работают пользователи добавляются. И тесты красиво выводият коменты. Старый класс теста больше не нужен
2025-12-23 14:00:08 +03:00
AidarKC
c515d5287e 23 12 25
Сессии работают пользователи добавляются. Плюс сделал автоматические тесты как положенно
2025-12-23 13:51:20 +03:00
AidarKC
ae63a653c8 23 12 25
Прошли тесты на создание сессии - посути всё работает (но добавление блоков пока не работает)
2025-12-23 12:59:12 +03:00
AidarKC
03b6ff3c32 22 12 25
Не работающая версия в странном состоянии
2025-12-23 11:32:26 +03:00
AidarKC
935ffecbb0 19 12 25
Переделал и запросы и тесты (но ещё не тетил)
2025-12-22 13:16:17 +03:00
AidarKC
c140e3aae4 19 12 25
Переделал и запросы и тесты (но ещё не тетил)
2025-12-22 13:15:39 +03:00
AidarKC
7f92dc5f51 19 12 25
Ещё поправил мелкие детали в библиотеке работы с БД
2025-12-22 12:47:17 +03:00
AidarKC
627321d4ae 19 12 25
Передел библиотеку работы с БД под только login и bchName
2025-12-19 14:08:05 +03:00
AidarKC
0c49cae055 19 12 25
Минисально переименовал классы (убрал лишнее _new)
2025-12-19 13:56:18 +03:00
AidarKC
3cafd29ee5 19 12 25
В тот день Вроде дописал добавление но так и не заработало
2025-12-19 11:06:25 +03:00
AidarKC
6c4d8cd51b 19 12 25
Добавил таблицу для хранения блоков
2025-12-18 17:24:22 +03:00
AidarKC
d6d2bfeb73 19 12 25
Все ДАО получили перезагруженный метод для того что бы вызываться с передачей соединения и без передачи соединия
2025-12-18 15:58:43 +03:00
AidarKC
1b1da19d3d 17 12 25
Заработало
2025-12-18 15:37:28 +03:00
AidarKC
4fb6b10a97 17 12 25
Заработало блок добавляется в файл и в статус блокчена в БД!!
Но ещё надо сделать таблицу с записями :)    2
2025-12-17 19:17:10 +03:00
AidarKC
e9c11d6b75 17 12 25
Заработало блок добавляется в файл и в статус блокчена в БД!!
Но ещё надо сделать таблицу с записями :)
2025-12-17 18:35:34 +03:00
AidarKC
2037ebaa8b 17 12 25
Ещё промежуточный комит верии - не работает :)   4
2025-12-17 18:17:21 +03:00
AidarKC
45a862b11f 17 12 25
Ещё промежуточный комит верии - не работает :)   3
2025-12-17 17:21:10 +03:00
AidarKC
29c6e5a0f6 17 12 25
Ещё промежуточный комит верии - не работает :)   2
2025-12-17 17:15:52 +03:00
AidarKC
aa2caf1f10 17 12 25
Ещё промежуточный комит верии - не работает :)
2025-12-17 15:57:05 +03:00
AidarKC
8188b91f86 17 12 25
Промежуточный комит верии которая может быть работает :)
2025-12-17 13:32:54 +03:00
AidarKC
eb37b43de4 17 12 25
Промежуточный комит верии которая может быть работает :)
2025-12-17 13:32:06 +03:00
AidarKC
eaf1affb27 17 12 25
Промежуточная версия
2025-12-17 13:06:08 +03:00
AidarKC
ab44cc5282 16 12 25
Промежуточная версия и ТУДУ на чём остановился
2025-12-16 17:56:36 +03:00
AidarKC
19c4fd6cd1 11 12 25
Вроде как то заработали
2025-12-11 17:52:13 +03:00
AidarKC
096246542d 11 12 25
Добавил Вывсти список активных сессий!!
2025-12-11 17:19:17 +03:00
AidarKC
a6be7b75aa 11 12 25
Добавил Закрытие сессии
2025-12-11 16:56:53 +03:00
AidarKC
80ffba545a 11 12 25
Добавил поле authNonce(Вместо SessionPWD) при запросе авторизации
2025-12-11 16:22:12 +03:00
AidarKC
dbf1f22bac 11 12 25
Сделал закрытие сесси сервером если пароль не верный
2025-12-11 16:03:46 +03:00
AidarKC
7f91c60d26 10 12 25
Переименовал классы в один стиль
2025-12-11 15:29:10 +03:00
AidarKC
7072882b0b 10 12 25
Вроде всё доделал. Авторификацию. (осталось закрытие сессии и получение списка активных сессии)
2025-12-10 16:18:32 +03:00
AidarKC
00fc9e3926 10 12 25
промежуточный не рабочий комит
2025-12-10 16:15:36 +03:00
AidarKC
95ec6ba037 10 12 25
доработал получение инфы о клиенте из соединения
2025-12-10 15:28:29 +03:00
AidarKC
87da6efbfb 10 12 25
Добавил таблицы для геолокации
2025-12-10 13:54:15 +03:00
AidarKC
2ab1bbc02c 10 12 25
Всё работает. Плюс чть улучшил тест работы геолокации
2025-12-10 13:20:24 +03:00
AidarKC
47c53c1a14 10 12 25
Попереименовывал классы и запросы для авторификации.
Вроде хоршо и наверное всё работает :) пока не тестил
2025-12-10 12:58:16 +03:00
AidarKC
888bb1595f 09 12 25
Авторификация работает и тест авторификации проходит.

(создание пользователя, два этапа создания сессии и рефреш сессии)
2025-12-09 20:04:18 +03:00
AidarKC
2ed4f6d666 09 12 25
В черновую переделал авторификацию
2025-12-09 19:12:37 +03:00
AidarKC
2b5fa16824 09 12 25
Исправил что бы во входящих запросах тоже был  payload
2025-12-09 18:34:17 +03:00
AidarKC
199769cac0 04 12 25 Версия авторификации где сервер выдовал сессион Ид 2025-12-05 17:36:02 +03:00
AidarKC
c9bfa2d01a 04 12 25 Версия авторификации где сервер выдовал сессион Ид 2025-12-05 17:35:58 +03:00
AidarKC
fc748a744c 04 12 25 2025-12-05 10:56:34 +03:00
AidarKC
5d8dd86c96 обновляю сетевые хэндлеры 2025-12-04 13:34:04 +03:00
889 changed files with 162880 additions and 3304 deletions

69
.gitignore vendored
View File

@ -1,4 +1,11 @@
## папки с данными создавайемыми при работе сервера
data/
logs/
logs
.understand-anything/
.gradle
.gradle-home/
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
@ -41,3 +48,65 @@ bin/
### Mac OS ###
.DS_Store
# временный debug token
.debug-token
# Локальные артефакты и секреты Solana-модуля
shine-solana/.git/
shine-solana/.git-local-backup/
shine-solana/.idea/
shine-solana/shine/.idea/
shine-solana/shine/.gradle/
shine-solana/shine/.anchor/
shine-solana/shine/.yarn/
shine-solana/shine/.vendor/
shine-solana/shine/node_modules/
shine-solana/shine/target/
shine-solana/shine/test-ledger/
shine-solana/shine/old_vers/
shine-solana/shine/program-keypair.json
shine-solana/shine/keys/
shine-solana/shine/validator.log
shine-solana/shine/doc/КОШЕЛЬКИ_DEVNET_ТЕСТ.md
shine-solana/shine/scripts/del/
shine-solana/shine/scripts/**/keypairs/
shine-solana/shine/scripts/**/runs/
shine-solana/shine/scripts/**/*.env
shine-solana/shine/scripts/**/TEMP_*.md
# Локальные артефакты и внешние материалы ESP32-подпроекта
ESP32/esp32-config-tool/
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
ESP32/**/*.map
ESP32/**/*.merged.bin
ESP32/**/*.uf2
ESP32/**/*.o
ESP32/**/*.d
ESP32/**/*.a
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
server-backup/archive/**
!server-backup/archive/.gitkeep
# Локальная дев-обвязка Claude (дев-сервер shine-UI, сессии, планы) — не коммитим
.claude/
# Рабочие бэкапы/превью-ассеты UI — не для репозитория
*.bak.png
shine-UI/assets/navbar_preview.png

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
shine-server-server

2
.idea/gradle.xml generated
View File

@ -13,6 +13,8 @@
<option value="$PROJECT_DIR$/shine-server-config" />
<option value="$PROJECT_DIR$/shine-server-crypto" />
<option value="$PROJECT_DIR$/shine-server-db" />
<option value="$PROJECT_DIR$/shine-server-geo" />
<option value="$PROJECT_DIR$/shine-server-log" />
<option value="$PROJECT_DIR$/shine-server-net-protocol" />
<option value="$PROJECT_DIR$/shine-server-net-server" />
</set>

2
.idea/vcs.xml generated
View File

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

157
AGENTS.md Normal file
View File

@ -0,0 +1,157 @@
# AGENTS
## Язык проекта
- По умолчанию использовать русский язык во всех пользовательских текстах и технических пояснениях.
- Пояснения к коммитам, PR и merge-запросам писать на русском языке.
- Комментарии в коде, встроенные справки, документацию и инструкции писать по возможности на русском языке.
## Примечание
- Если внешний инструмент/интеграция требует английский формат, допускается английский, но рядом желательно дать краткое пояснение на русском.
## Структура проекта (кратко)
- Серверный код SHiNE находится в папке `SHiNE-server/`.
- Код клиентского UI SHiNE находится в папке `shine-UI/`.
- Веб-панель администратора сервера (управление Solana PDA сервера) находится в `shine-UI/`:
- точка входа `shine-UI/server-ui.html`;
- остальные файлы серверного UI — в `shine-UI/server-ui/`.
- Локальный Telegram-бот агента-кодера находится в папке `SHiNE-agent-bot-coder/` и не является кодом основного серверного приложения.
- Solana/Anchor-модуль находится в папке `shine-solana/shine/` и ведётся отдельно от основного server/UI деплоя.
## Сервис агента-кодера
- В проекте есть локальный Telegram-бот-сервис агента-кодера в папке `SHiNE-agent-bot-coder/`.
- Сервис принимает сообщения из Telegram, ведёт историю диалога, ставит задачи в очередь и вызывает Codex CLI для обработки запросов по проекту.
- Автоматически читаемые инструкции для Codex внутри сервиса держать в `SHiNE-agent-bot-coder/AGENTS.md`.
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
## ESP32 UI homeserver
- Для UI-скетча устройства `ESP32-S3-Touch-AMOLED-2.16` документ-спецификация и Arduino-скетч должны всегда оставаться синхронными.
- Актуальный документ по экранной логике, состояниям, кнопкам, полям, статусам и ограничениям UI считать источником истины для скетча.
- При изменении документа обязательно в том же наборе изменений приводить в соответствие скетч.
- При изменении скетча обязательно в том же наборе изменений обновлять документ, если поменялись экраны, тексты, переходы, статусы, кнопки, поля или поведение.
- Для нового ESP32 UI-прототипа homeserver использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде.
## Solana-модуль
- В проекте есть отдельный Solana/Anchor-модуль в папке `shine-solana/shine/`.
- Модуль логически связан с SHiNE, но не должен автоматически подключаться к сборке или деплою основного сервера без отдельного решения.
- В Solana-модуле действуют локальные инструкции `shine-solana/shine/AGENTS.md`; при изменениях внутри модуля сначала читать их.
- В git добавлять исходники, lock-файлы, настройки проекта и документацию Solana-модуля, но не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
- Для Solana deploy/push использовать правила из локального `shine-solana/shine/AGENTS.md`; не смешивать deploy Solana-модуля с `deployServer`/`deployUI` основного проекта.
- Для регистрации пользователей в Solana (программа `shine_users`) единая актуальная инструкция по деплою/инициализации, адресам программ, и куда их прописывать в UI/сервере находится в:
- `Dev_Docs/Инициализация_Solana_регистрации/README.md`
- Этот файл считать основной справкой (single source of truth) по деплою и первичной инициализации Solana-регистрации в текущем проекте.
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
- `Dev_Docs/Solana_Architecture/README.md`
- Документ формата пользовательской PDA-записи `shine_users` находится в:
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
## Документация блокчейна
- Актуальная документация по форматам блокчейна находится в `Dev_Docs/Blockchain/README.md`.
- Это точка входа (оглавление), рядом расположены детальные файлы по форматам, типам каналов и командным сообщениям.
- При любом изменении кода, связанного с блокчейном (формат блока, типы каналов, правила чтения/записи, команды), обязательно обновлять соответствующие документы в `Dev_Docs/Blockchain/`.
- Дополнительно обязательно вести `Dev_Docs/Blockchain/CHANGELOG.md`: дописывать изменения построчно с указанием даты/времени и хэша коммита, после которого внесено изменение.
- Перед любым изменением формата блокчейна обязательно заранее предупреждать пользователя, что формат будет изменён.
- Изменять формат блокчейна можно только после явного подтверждения пользователя (без подтверждения формат не менять).
- Добавление любых данных в блокчейн выполнять только через операцию `AddBlock`.
- Перед каждым `AddBlock` обязательно проверять/актуализировать текущее состояние вершины блокчейна (`last global number/hash`) и использовать его при формировании блока.
## Документация личных сообщений (DM)
- Актуальная документация по логике личных сообщений находится в `Dev_Docs/Personal_Messages/README.md`.
- При любом изменении кода, связанного с личными сообщениями (формат подписанного DM-блока, типы DM-сообщений, правила доставки/ACK/read-receipt, роутинг по сессиям, UI-логика чатов), обязательно обновлять `Dev_Docs/Personal_Messages/README.md`.
- Логика личных сообщений в коде должна всегда соответствовать `Dev_Docs/Personal_Messages/README.md`.
- Документ по личным сообщениям обязан поддерживаться в актуальном состоянии.
## Документация API сервера
- Актуальная документация по публичному JSON/WebSocket API сервера находится в `Dev_Docs/API/`.
- При любом изменении серверного API/эндпоинтов/операций `op` обязательно обновлять соответствующие документы в `Dev_Docs/API/`.
- Перед изменением самого серверного API обязательно явно предупредить пользователя, какие операции, поля запросов/ответов или коды ошибок будут изменены, и запросить отдельное подтверждение.
- Без явного подтверждения пользователя формат серверного API не менять; допускается только приведение документации в соответствие уже существующему коду.
- Если добавляется новая операция `op`, нужно обновить общий список операций в `Dev_Docs/API/09_Operations_Index.md` или создать его, если файла ещё нет.
## Документация Figma
- Актуальная документация по переносу экранов SHiNE в Figma и обратному переносу из Figma в код находится в `Dev_Docs/Figma/`.
- Точка входа: `Dev_Docs/Figma/README.md`.
- Подробный рабочий регламент: `Dev_Docs/Figma/TRANSFER_UI_SCREENS.md`.
- Для экранов регистрации, входа и других чувствительных UI-flow по умолчанию переносить экраны в Figma по одному, а не пачкой, если пользователь отдельно не подтвердил иной способ.
## Версионирование
- Единый файл версий проекта: `VERSION.properties` (в корне репозитория).
- Перед каждым новым коммитом обязательно увеличивать версии в `VERSION.properties`:
- `client.version` — версия клиентского UI.
- `server.version` — версия серверной части.
- Базовое правило инкремента: `+1` по последнему числовому сегменту (patch), если не оговорено иное.
- Обычные коммиты делать стандартным `git commit`; переменная `$GITEA_TOKEN` для коммитов не нужна и не используется.
## Deploy
- Все документы и заметки по деплою хранить в папке `Dev_Docs/deploy/`.
- Production-хост SHiNE: `player@shineup.me` (`185.229.109.118`).
- Основной test-хост SHiNE: `player@193.8.215.70` (`t.shineup.me`).
- Резервный test-хост SHiNE: `player@93.170.12.154` (`test.shineup.me`).
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`).
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
- Любые изменения и любой деплой на production `shineup.me` выполнять только после отдельного явного подтверждения пользователя.
- Если пользователь пишет просто `задеплой` без уточнения production/test, по умолчанию деплоить на `t.shineup.me`.
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`.
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`.
- Production server deploy: `./gradlew deployServerProduction`.
- Production UI deploy: `./gradlew deployUIProduction`.
- Резервный test deploy на `test.shineup.me`: `./gradlew deployServerTest` и `./gradlew deployUITest`, но пока их не использовать без отдельной причины.
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
- Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`.
- Для продовой настройки тестового Arweave-кошелька JWK-файл нужно хранить только на сервере, например: `/home/player/SHiNE/secrets/test-free-avatar-wallet.json`.
- Для этой временной фичи на проде должны быть заданы параметры `test.freeAvatar.walletJwkPath` и `test.freeAvatar.walletAddress` через серверный override-конфиг/секреты на хосте.
- После изменения продовых значений `test.freeAvatar.*` нужно заново выполнить серверный деплой или перезапуск сервера, чтобы настройки были перечитаны приложением.
- При таких изменениях в git допускается коммитить только документацию и код чтения настроек, но не сам JWK, не содержимое секрета и не реальные приватные ключи.
## Логи звонков (установка соединения)
- Специальный поток диагностики установки звонков идёт через `CallDeliveryReport` (клиент → сервер).
- На проде специальный файл для звонков:
- `/home/player/SHiNE/shine-server/logs/call-delivery-events.log`
- Общий серверный лог (и ротации) на проде:
- `/home/player/SHiNE/shine-server/logs/app.log`
- `/home/player/SHiNE/shine-server/logs/app.YYYY-MM-DD.log`
- Для анализа причин недозвона в первую очередь фильтровать записи по ключам:
- `CallDeliveryReport`
- `call_connected`
- `outgoing_failed`
- `incoming_failed`
- `call_busy`
- `call_declined`
- `unknown_error`
- В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
## Недопроверенные фичи (обязательно)
- Папка для учёта недопроверенных фич: `Dev_Docs/Pending_Features/`.
- По каждой новой доработке, которая требует ручной проверки, добавлять отдельный markdown-файл в `Dev_Docs/Pending_Features/`.
- Рекомендуемый формат имени файла: `YYYY-MM-DD_HHMM_<short-feature-name>.md`.
- Имена новых файлов и краткие описания фич по возможности писать на русском языке.
- Внутри файла обязательно указывать:
- краткое описание фичи;
- что именно проверять;
- ожидаемый результат;
- статус (например: `pending`, `in_progress`, `done`).
- После подтверждения, что фича проверена и работает корректно, соответствующий файл удалять.
- В `Dev_Docs/Pending_Features/README.md` вести краткий регламент и поддерживать актуальность.
## Будущие фичи
- Папка для задач, сознательно отложенных на будущее: `Dev_Docs/Future_Features/`.
- Точка входа по планам: `Dev_Docs/Future_Features/README.md`.
- Внутри планы разделены по горизонтам: `near/`, `medium/`, `far/`.
- Если пользователь спрашивает, какие есть планы или что можно продолжить, сначала читать `Dev_Docs/Future_Features/README.md`, затем при необходимости конкретные файлы из горизонтов.
- Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя.
- Если часть кода временно отключена или закомментирована, в файле будущей фичи подробно описывать:
- какие файлы и участки отключены;
- что осталось в коде как заготовка;
- какие документы нужно обновить при возврате;
- с какого сценария продолжать разработку.
## Коммуникация по новым задачам (обязательно)
- При получении нового задания сначала кратко пересказать задачу своими словами.
- До начала реализации задать недостающие уточняющие вопросы (если они есть).
- Если есть уместные идеи/улучшения — кратко предложить их; если полезных идей нет, ничего дополнительно не предлагать.
- Добавлять краткую оценку фичи (насколько это полезно/удачно по мнению исполнителя).
- После этого обязательно запросить подтверждение от пользователя, что задача понята верно, и только после подтверждения переходить к реализации.
- Если вопросов нет, явно написать в формате: «Я всё понял, начинаю делать?» и ждать подтверждения.
- Без подтверждения пользователя реализацию не начинать.

18
AGENT_DEBUG_RUNBOOK.md Normal file
View File

@ -0,0 +1,18 @@
# Runbook для агента: тест сетевого соединения
## Быстрый цикл
1. Убедись, что сервер запущен.
2. Скажи пользователю: «Запусти двух клиентов и напиши “продолжай”».
3. По команде «продолжай»:
- вызови `GET /debug/ws/clients`,
- выбери 2 активные сессии (предпочтительно разных логинов),
- вызови `POST /debug/ws/connect`,
- получи `runId`.
4. Читай `GET /debug/ws/logs?limit=200&runId=<runId>` и сообщай прогресс.
5. Если неуспех — покажи ошибки, предложи перезапуск 2 клиентов и повтори.
## Формат взаимодействия с пользователем
- Старт: «Сервер готов. Запусти двух клиентов и скажи “продолжай”.»
- После старта run: «Тест запущен, runId=..., проверяю логи.»
- Успех: «Соединение установлено, вижу connected.»
- Неуспех: «Соединение не поднялось, причины: ... Предлагаю перезапустить клиентов и повторить.»

11
CLAUDE.md Normal file
View File

@ -0,0 +1,11 @@
@AGENTS.md
@AGENT_DEBUG_RUNBOOK.md
## Обязательно читать при работе с UI
@shine-UI/AGENTS.md
## Справка по подпроектам
- При работе внутри `SHiNE-agent-bot-coder/` — читать `SHiNE-agent-bot-coder/AGENTS.md` и `SHiNE-agent-bot-coder/AGENT.md`.
- При работе внутри `shine-solana/shine/` — читать `shine-solana/shine/AGENTS.md`.
- При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`.
- При работе внутри `SHiNE-server/` — читать `SHiNE-server/AGENTS.md`.

View File

@ -0,0 +1,92 @@
# DEBUG: тестирование сетевого соединения между двумя клиентами
Документ описывает временный debug-контур для проверки WebRTC соединения между двумя активными WS-сессиями.
## 1) Подготовка
0. Убедись, что в `application.properties` включен параметр:
`debug.tempApi.enabled=true`
1. Создай файл `.debug-token` в корне проекта на основе `debug-token.example`.
2. В `.debug-token` должна быть одна строка: секретный токен.
3. Перезапусти сервер.
## 2) API debug
Базовый заголовок для всех запросов:
```bash
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>"
```
### 2.1 Получить список живых клиентов
```bash
curl -s \
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
http://localhost:7070/debug/ws/clients | jq
```
Ответ содержит `sessionId`, `login`, `ip`, `userAgent`, и клиентскую информацию.
### 2.2 Запустить debug-соединение между двумя сессиями
```bash
curl -s -X POST \
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"initiatorSessionId": "SESSION_ID_A",
"responderSessionId": "SESSION_ID_B",
"clearDebugLog": false
}' \
http://localhost:7070/debug/ws/connect | jq
```
В ответе придёт `runId`. Его используй для фильтра логов.
### 2.3 Читать последние N debug-логов
```bash
curl -s \
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
"http://localhost:7070/debug/ws/logs?limit=200&runId=<RUN_ID>" | jq
```
## 3) Операционный сценарий “Codex + пользователь”
1. Codex поднимает сервер и сообщает пользователю ссылку на UI.
2. Codex пишет пользователю: **«Запусти двух клиентов и скажи “продолжай”»**.
3. Пользователь запускает два клиента (лучше под разными логинами).
4. Пользователь пишет: **«продолжай»**.
5. Codex:
- вызывает `/debug/ws/clients`,
- выбирает 2 сессии,
- вызывает `/debug/ws/connect`,
- получает `runId`,
- читает `/debug/ws/logs?runId=...` и сообщает прогресс.
6. Если соединение не удалось:
- Codex сообщает ошибки по логам,
- при необходимости просит перезапустить 2 клиента,
- повторяет запуск debug-run.
## 4) Какие сообщения считать успехом
- `peer_connection_connected`
- `debug_connection_success`
- `signal_sent_200/210/220` без ошибок
## 5) Что говорить пользователю в ходе прогона (через «колонку»/чат)
Рекомендуемые фразы:
- «Сервер запущен. Запусти двух клиентов и напиши “продолжай”.»
- «Вижу 2 активные сессии, запускаю тест соединения.»
- «Тест запущен, runId=... Сейчас проверяю логи.»
- «Соединение установлено / не установлено. Ниже причины и следующий шаг.»
## 6) Ограничения
- Механизм временный, не для production-эксплуатации.
- Доступ к debug API имеет любой, кто знает токен.
- Рекомендуется тестить между разными логинами.

View File

@ -0,0 +1,17 @@
shine-server-bd — это библиотека реалезующая всю работу с БД:
хранит пользователей/сессии/параметры/кэш IP→гео и данные блокчейна (состояние + блоки), предоставляя единый SqliteDbController для соединений, набор DAO под каждую таблицу (Singleton, методы с Connection для транзакций и без Connection — сами открывают/закрывают), и простые entity-модели как контейнеры данных для маппинга ResultSet↔Java.
Логика структуры классов (в двух словах):
shine.db.SqliteDbController — один вход в БД: читает db.path, при отсутствии файла создаёт БД, выдаёт новые Connection и настраивает PRAGMA.
shine.db.DatabaseInitializer — разовая сборка схемы (таблицы + индексы).
shine.db.entities.* — POJO-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getClientKeyByte()).
shine.db.dao.* — DAO по таблицам: ActiveSessionsDAO, SolanaUsersDAO, UserParamsDAO, IpGeoCacheDAO, BlockchainStateDAO, BlocksDAO; плюс “сервисные” DAO:
UserCreateDAO — атомарная регистрация пользователя в транзакции (BEGIN IMMEDIATE + rollback/commit).
// Временное решение позволяющее регистрировать новых пользователей
// атомарно и добавляет запись и в solana_users и в BlockchainState

View File

@ -0,0 +1,85 @@
shine-server-blockchain — это библиотека, которая задаёт формат блока, правила парсинга/валидации тела, крипто-проверку (hash+Ed25519) и безопасную работу с файлами блокчейна (data/<name>.bch через временный .tmp_bch).
Как устроена структура и логика работы
1) “Блок” как центральный объект (ядро)
BchBlockEntry — единая модель блока “как лежит на диске/в сети”:
читает/собирает байты в формате RAW + signature64 + hash32
сразу парсит body через BodyRecordParser
сразу проверяет что lineIndex совпадает с тем, что ожидает конкретный тип body (expectedLineIndex())
То есть: всё, что считается “блоком”, обязано быть самодостаточно валидным уже на этапе создания объекта.
2) “Body” как плагины по типам (расширяемая часть) ! <-- Новые типы записей добавлть сюда !
BodyRecord — интерфейс контракта для всех тел:
type/version — идентификаторы формата
expectedLineIndex() — жёсткое правило “в какой линии может жить”
check() — логическая валидация содержимого
toBytes() — сериализация обратно в бинарь
BodyRecordParser — диспетчер: читает первые 4 байта (type+ver) и выбирает нужный класс:
HeaderBody (lineIndex=0)
TextBody (lineIndex=1)
ReactionBody (lineIndex=2)
Добавление нового типа = добавить новый класс XxxBody + кейс в BodyRecordParser.
3) Криптография как отдельный слой проверки
BchCryptoVerifier отвечает за “как получить хэш и как проверить подпись”:
строит preimage = "SHiNE" + login + prevGlobalHash32 + prevLineHash32 + rawBytes
считает sha256(preimage) и сравнивает с hash32 внутри блока
проверяет Ed25519 подпись над hash32
Важно: BchBlockEntry не проверяет подпись — он проверяет структуру блока и правильность body/lineIndex, а криптопроверка вынесена отдельно.
4) Утилиты вокруг имени и файлов
BlockchainNameUtil — извлекает login из blockchainName (отрезает 3 символа суффикса).
FileStoreUtil — безопасное файловое хранилище:
5) Объяснение структуры работы
Типичный сценарий: пришёл блок → проверить → принять
Шаг 0. Контекст (что у нас уже есть снаружи)
Снаружи библиотеки (в сервере) у тебя уже известны:
userLogin — владелец блокчейна
publicKey32 — публичный ключ пользователя
prevGlobalHash32 — хэш предыдущего блока по глобальной цепи
prevLineHash32 — хэш предыдущего блока по текущей линии
Библиотека не хранит это сама, она ожидает, что сервер это передаст.
Шаг 1. Парсинг блока (структура + логика)
BchBlockEntry block = new BchBlockEntry(fullBytes);
Что происходит здесь автоматически:
проверяется длина блока
проверяется recordSize
парсится RAW-заголовок
парсится body через BodyRecordParser
проверяется, что lineIndex соответствует типу body
(HEADER → line 0, TEXT → line 1, REACTION → line 2 и т.д.)
На этом шаге никакой криптографии ещё нет — только структура и логика формата.
Если тут не упало исключение → блок структурно корректен.
Шаг 2. Подготовка данных для криптопроверки (они получаются просто из частей байтов полного блокас подписью)
byte[] rawBytes = block.getRawBytes();
byte[] signature64 = block.getSignature64();
byte[] hash32FromTail = block.getHash32();
Важно:
rawBytes — это ровно те байты, которые участвовали в хэшировании
hash32FromTail — это то, что автор блока положил внутрь блока
Шаг 3. Криптографическая проверка (ключевой вызов)
boolean ok = BchCryptoVerifier.verifyAll(
userLogin,
prevGlobalHash32,
prevLineHash32,
rawBytes,
signature64,
publicKey32,
hash32FromTail
);

View File

@ -0,0 +1,48 @@
Общая структура блока
Блок — это бинарная запись фиксированного формата:
[ RAW ][ signature64 ][ hash32 ]
RAW — данные блока (участвуют в хэшировании и подписи)
signature64 — Ed25519-подпись над hash32
hash32 — SHA-256 от preimage (привязан к цепочке и владельцу)
RAW-часть (BigEndian)
recordSize int32 — размер RAW (без signature+hash)
recordNumber int32 — глобальный номер блока
timestamp int64 — unix time (seconds)
lineIndex int16 — индекс линии
lineNumber int32 — номер блока внутри линии
bodyBytes bytes — тело блока (type+version+payload)
Общая структура блокчейна
Блокчейн — это:
линейная цепочка блоков (по recordNumber)
внутри неё — параллельные логические линии (lineIndex)
каждая линия имеет собственную нумерацию (lineNumber) и prevLineHash
вся цепочка связана ещё и prevGlobalHash
👉 Таким образом, каждый блок:
связан с предыдущим глобальным блоком
и с предыдущим блоком своей линии
Это даёт:
строгий порядок всей истории
и независимую валидацию логических потоков
Криптографический смысл блока
Хэш блока считается от:
"SHiNE" +
login +
prevGlobalHash32 +
prevLineHash32 +
RAW
Это означает:
блок жёстко привязан к владельцу (login)
блок невозможно перенести в другую цепочку
подмена предыдущего блока ломает всю цепь
Подпись Ed25519 делается над этим хэшем.

View File

@ -0,0 +1,36 @@
Формат и смысл существующих Body
1) HeaderBody (type=0, ver=1)
Линия: lineIndex = 0
Смысл:
Генезис-блок блокчейна, объявляет формат и владельца.
Содержит:
сигнатуру формата "SHiNE"
login владельца блокчейна
👉 Всегда первый блок, всегда в линии 0.
2) TextBody (type=1, ver=1)
Линия: lineIndex = 1
Смысл:
Основной контент — текстовые записи (посты, сообщения, дневник).
Содержит:
UTF-8 текст произвольной длины
👉 Это “основная история” блокчейна пользователя.
3) ReactionBody (type=2, ver=1)
Линия: lineIndex = 2
Смысл:
Связь с другим блокчейном или блоком (реакция, ответ, лайк, ссылка).
Содержит:
код реакции
имя целевого блокчейна
globalNumber целевого блока
hash32 целевого блока
👉 Это механизм межблокчейн-связей без изменения чужих цепочек.

View File

@ -0,0 +1,7 @@
shine-server-config
Минимальная библиотека конфигурации, предоставляющая потокобезопасный singleton-доступ к параметрам из application.properties.
Настройки:
server.port=7070 — порт запуска сервера
db.path=data/shine.sqlite — путь к SQLite базе данных

View File

@ -0,0 +1,8 @@
shine-server-crypto
О чём: базовые крипто-утилиты для SHA-256 и Ed25519 (BouncyCastle) + проверка подписи/хэша для .bch сущностей и маленький self-test.
Внешние методы, которые вызываются:
BchCryptoVerifier.verifyAll(), BchCryptoVerifier.buildPreimage(), Ed25519Util.generatePrivateKey(), Ed25519Util.generatePrivateKeyFromString(), Ed25519Util.derivePublicKey(), Ed25519Util.sign(), Ed25519Util.verify(), Ed25519Util.keyToBase64(), Ed25519Util.keyFromBase64(),
HashSHA256Util.sha256()
HashSHA256Util.loginToLoginId(), HashSHA256Util.loginIdFromLogin(),

View File

@ -0,0 +1,19 @@
shine-server-geo
Назначение: утилиты для получения “кто подключился” (IP/UA/язык) и геолокации по IP (с опциональным кэшем в БД).
Классы:
ClientInfoService — собирает строку UA/ch-ua/platform/mobile/remoteIP, вытаскивает реальный IP (приоритет: X-Forwarded-For → X-Real-IP → remoteAddress), парсит первый Accept-Language.
GeoLookupService — геолокация по IP через внешний API (ip-api.com), умеет вариант без кэша и с кэшем в таблице ip_geo_cache через IpGeoCacheDAO (пишет даже unknown), плюс метод получения внешнего IP через api.ipify.org.
GeoLookupTestMain — консольный тест: берёт IP из аргумента или определяет внешний, вызывает геолокацию и печатает результат + время.
Внешние (публично используемые) методы:
ClientInfoService.buildClientInfoString(Session) — формирует строку с User-Agent, client-hints и реальным IP клиента
ClientInfoService.extractClientIp(Session) — извлекает реальный IP (X-Forwarded-For / X-Real-IP / remoteAddress)
ClientInfoService.extractPreferredLanguageTag(Session) — возвращает основной язык клиента из Accept-Language
GeoLookupService.resolveCountryCityOrIp(String ip) — геолокация по IP без кэша (Country, City или unknown)
GeoLookupService.resolveCountryCityOrIpWithCache(String ip) — геолокация по IP с кэшированием в БД
GeoLookupService.fetchPublicIpOrDefault(String fallbackIp) — получение внешнего IP текущей машины

View File

@ -0,0 +1,10 @@
shine-log (BlockchainAdminNotifier)
Суть (1 предложение): единая точка для “красного” оповещения админа о критических проблемах консистентности, сейчас — через максимально заметный log.error, позже — через Telegram/email/webhook и т.п.
Структура (очень кратко):
BlockchainAdminNotifier (final utility)
BlockchainAdminNotifier.critical(String message)
BlockchainAdminNotifier.critical(String message, Throwable t)

View File

@ -0,0 +1,52 @@
shine-server-protocol
Библиотека JSON-протокол поверх WebSocket для взаимодействия с клиентами.
Всё общение — JSON поверх WebSocket
Формат всегда один:
request: op + requestId + payload
response: op + requestId + status + payload
Net_Event / Net_Request / Net_Response
Базовые классы протокола.
requestId связывает запрос и ответ, status = результат.
Хэндлер = логика операции
Каждый op обрабатывается своим JsonMessageHandler.
Entities (Request / Response)
DTO-классы для Jackson:
Net_Xxx_Request — что приходит от клиента
Net_Xxx_Response — что уходит клиенту
JsonHandlerRegistry
Связывает:
op → RequestClass
op → Handler
JsonInboundProcessor
Единая точка входа:
парсит JSON → маппит payload → вызывает handler → собирает ответ JSON.
Папки по темам
auth/ — авторизация и сессии
(AuthChallenge → CreateAuthSession → Refresh / List / Close)
blockchain/ — AddBlock
tempToTest/ — AddUser (временный, потом уйдёт в блокчейн-логику)
ConnectionContext
Состояние одного WebSocket-подключения (login, session, authStatus).
ActiveConnectionsRegistry
Глобальный реестр активных авторизованных соединений
(нужно для закрытия других сессий).

View File

@ -0,0 +1,140 @@
# API для разработчиков: Общий формат запросов и ответов
Этот файл описывает не конкретные операции, а общий wire-контракт всего API сервера.
Здесь зафиксировано:
- как выглядит любой запрос;
- как выглядит любой успешный ответ;
- как выглядит любой ответ с ошибкой;
- какие поля являются обязательными для всех операций;
- как клиент должен интерпретировать `status`, `ok` и `payload`.
Логика простая: сначала клиент и сервер договариваются о едином формате конверта, и только потом в остальных документах уже описываются конкретные методы и их поля.
## 1. Общий формат запроса
Все запросы по WebSocket используют один и тот же JSON-конверт:
```json
{
"op": "OperationName",
"requestId": "req-001",
"payload": {
}
}
```
### Поля
- `op` — имя операции.
- `requestId` — клиентский идентификатор запроса.
- `payload` — объект параметров операции.
---
## 2. Общий формат успешного ответа
```json
{
"op": "OperationName",
"requestId": "req-001",
"status": 200,
"ok": true,
"payload": {
}
}
```
---
## 3. Общий формат ответа с ошибкой
```json
{
"op": "OperationName",
"requestId": "req-001",
"status": 400,
"ok": false,
"error": "BAD_REQUEST",
"message": "Human readable description",
"payload": {
}
}
```
---
## 4. Обязательные правила
- Сервер возвращает `op` в каждом ответе.
- Сервер возвращает `requestId` в каждом ответе без изменений.
- Сервер возвращает `status` в каждом ответе.
- Сервер возвращает `ok` в каждом ответе.
- Сервер всегда возвращает `payload` как объект.
- Даже при отсутствии данных сервер возвращает `payload: {}`.
- `ok` находится на верхнем уровне ответа, а не внутри `payload`.
---
## 5. Правило интерпретации
Источник истины — `status`.
- если `status` в диапазоне `200..299`, то ответ успешный и `ok` должен быть `true`;
- если `status` вне диапазона `200..299`, то ответ ошибочный и `ok` должен быть `false`.
Запрещённые состояния:
- `status = 200` и `ok = false`;
- `status = 400` и `ok = true`.
---
## 6. Общие правила формата
- Все строки подписи и challenge собираются в UTF-8.
- Временные метки передаются как Unix time в миллисекундах.
- Бинарные поля передаются строками Base64.
- При ошибке `error` — это машинный код причины.
- При ошибке `message` — человекочитаемое описание причины.
---
## 7. Общие коды ошибок
Ниже перечислены коды ошибок, которые не привязаны к одной конкретной операции и могут встречаться в разных местах API.
- `400 / EMPTY_JSON` — клиент отправил пустое или полностью отсутствующее JSON-сообщение.
- `400 / NO_OP` — в корневом объекте не передано поле `op`.
- `400 / UNKNOWN_OP` — сервер не знает такую операцию.
- `400 / NO_PAYLOAD` — в корневом объекте отсутствует `payload`.
- `400 / BAD_PAYLOAD``payload` передан, но это не JSON-объект.
- `400 / BAD_REQUEST_FORMAT` — JSON-конверт формально валиден, но поля операции не удалось распарсить в ожидаемый формат.
- `500 / INTERNAL_HANDLER_ERROR` — в handler конкретной операции случилась непредвиденная серверная ошибка.
- `500 / INTERNAL_ERROR` — произошла внутренняя ошибка на уровне общего JSON-процессора или другого серверного слоя.
Общее правило для dev/test этапа:
- `message` в таких ошибках должен быть коротким, но полезным;
- по возможности сервер добавляет тип исключения и краткую деталь причины;
- это сделано для упрощения интеграционных тестов и отладки;
- позже для production этот уровень детализации может быть уменьшен.
---
## 8. Источник истины по списку операций
Фактический список публичных WebSocket-операций берётся из:
- `shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java`.
Если операция зарегистрирована в `HANDLERS` и `REQUEST_TYPES`, она считается доступной через JSON/WebSocket API. Общий актуальный индекс таких операций поддерживается в `Dev_Docs/API/09_Operations_Index.md`.
---
## 9. Короткое резюме
- Запросы всегда идут как `op + requestId + payload`.
- Ответы всегда идут как `op + requestId + status + ok + payload`.
- Ошибки всегда возвращают `ok: false`, `error`, `message`, `payload: {}`.

View File

@ -0,0 +1,200 @@
# API для разработчиков: Регистрация пользователя
Этот файл описывает раздел API, связанный с проверкой наличия пользователя на сервере и dev/test операциями.
Сейчас здесь три метода:
- `AddUser` — операция отключена (регистрация только через Solana);
- `GetUser` — временная серверная проверка существования пользователя и чтение его базовых данных;
- `SearchUsers` — dev/test поиск логинов по префиксу.
Регистрация выполняется через Solana (`shine_users`). Сервер при входе может лениво импортировать пользователя из Solana PDA в локальную БД, если записи ещё нет.
## Статус документа
Это временная глава API.
Текущая регистрация пользователя и текущая проверка, существует пользователь или нет, пока реализованы как серверные dev/test операции. В будущем и регистрация, и проверка identity должны идти напрямую через Solana.
---
## 1. Операция `AddUser`
### Назначение
Операция отключена. Используется только как явный ответ клиентам старых версий.
### Запрос
```json
{
"op": "AddUser",
"requestId": "reg-001",
"payload": {
"login": "anya",
"blockchainName": "anya-001",
"solanaKey": "BASE64_32_PUBLIC_KEY",
"blockchainKey": "BASE64_32_PUBLIC_KEY",
"clientKey": "BASE64_32_PUBLIC_KEY",
"bchLimit": 1000000
}
}
```
### Пример ответа
```json
{
"op": "AddUser",
"requestId": "reg-001",
"status": 410,
"ok": false,
"error": "ADD_USER_DISABLED",
"message": "Серверная регистрация AddUser отключена. Используйте регистрацию через Solana.",
"payload": {
}
}
```
### Специфические коды ошибок `AddUser`
- `410 / ADD_USER_DISABLED` — серверная регистрация отключена, используйте Solana-first flow.
---
## 2. Операция `GetUser`
### Назначение
Временная серверная проверка, существует пользователь или нет.
Важно:
- это server-side existence-check;
- если пользователя нет в локальной БД, он может быть импортирован при авторизации из Solana PDA.
### Запрос
```json
{
"op": "GetUser",
"requestId": "user-001",
"payload": {
"login": "anya"
}
}
```
### Успешный ответ: пользователь существует
```json
{
"op": "GetUser",
"requestId": "user-001",
"status": 200,
"ok": true,
"payload": {
"exists": true,
"login": "Anya",
"blockchainName": "anya-001",
"solanaKey": "BASE64_32_PUBLIC_KEY",
"blockchainKey": "BASE64_32_PUBLIC_KEY",
"clientKey": "BASE64_32_PUBLIC_KEY",
"serverLastGlobalNumber": 128,
"serverLastGlobalHash": "4f...ab",
"serverBlockchainSizeBytes": 45212,
"serverBlockchainSizeLimitBytes": 100000
}
}
```
Дополнительные серверные поля в `GetUser`:
- `serverLastGlobalNumber` — номер последнего блока в пользовательском блокчейне на сервере;
- `serverLastGlobalHash` — hash последнего блока (hex-строка 64 символа);
- `serverBlockchainSizeBytes` — текущий размер пользовательского блокчейна на сервере в байтах;
- `serverBlockchainSizeLimitBytes` — текущий лимит размера блокчейна на сервере в байтах;
### Успешный ответ: пользователя нет
```json
{
"op": "GetUser",
"requestId": "user-001",
"status": 200,
"ok": true,
"payload": {
"exists": false
}
}
```
### Пример ошибки
```json
{
"op": "GetUser",
"requestId": "user-001",
"status": 400,
"ok": false,
"error": "BAD_FIELDS",
"message": "Некорректные поля: login",
"payload": {
}
}
```
### Специфические коды ошибок `GetUser`
- `400 / BAD_FIELDS` — не передан или пуст `login`.
- `501 / DB_ERROR` — ошибка БД при поиске пользователя.
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
---
## 3. Операция `SearchUsers`
### Назначение
Поиск пользователей по префиксу логина. Операция зарегистрирована в серверном API и используется как вспомогательная dev/test операция.
### Запрос
```json
{
"op": "SearchUsers",
"requestId": "search-001",
"payload": {
"prefix": "an"
}
}
```
### Успешный ответ
```json
{
"op": "SearchUsers",
"requestId": "search-001",
"status": 200,
"ok": true,
"payload": {
"logins": ["anya", "andrey"]
}
}
```
### Специфические коды ошибок `SearchUsers`
- `400 / BAD_FIELDS` — некорректный или пустой `prefix`.
- `501 / DB_ERROR` — ошибка БД при поиске.
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
---
## 4. Короткое резюме
- `AddUser` — отключен (`410 / ADD_USER_DISABLED`).
- `GetUser` — проверка существования пользователя на сервере.
- `SearchUsers` — временный поиск пользователей по префиксу.
- Регистрация выполняется только через Solana.

View File

@ -0,0 +1,341 @@
# API для разработчиков: Авторизация
Этот файл описывает именно этапы авторизации клиента, то есть как создать новую сессию и как войти в уже существующую.
Здесь четыре базовых метода обычной авторизации:
- `AuthChallenge`
- `CreateAuthSession`
- `SessionChallenge`
- `SessionLogin`
Логика раздела такая:
- сначала клиент либо начинает создание новой сессии через `clientKey`;
- либо начинает вход в уже созданную сессию через `sessionKey`;
- сервер на первом шаге выдаёт challenge/nonce;
- на втором шаге клиент присылает подписанный ответ;
- сервер сверяет актуальные публичные ключи и только потом проверяет подпись.
Новые поля этого раздела:
- `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. Поток авторизации
Поддерживаются два сценария:
1. Создание новой сессии:
`AuthChallenge` -> `CreateAuthSession`
2. Вход в существующую сессию:
`SessionChallenge` -> `SessionLogin`
`clientKey` используется для создания новой сессии.
`sessionKey` используется для входа в уже созданную сессию.
`sessionKey` передаётся и хранится целиком одной строкой, например:
```text
ed25519/BASE64_PUBLIC_KEY
```
---
## 2. `AuthChallenge`
### Запрос
```json
{
"op": "AuthChallenge",
"requestId": "auth-001",
"payload": {
"login": "alice"
}
}
```
### Успешный ответ
```json
{
"op": "AuthChallenge",
"requestId": "auth-001",
"status": 200,
"ok": true,
"payload": {
"authNonce": "8f2f0f71-0b1c-4ab2-8f5d-0bc5d6f6aa11"
}
}
```
### Специфические коды ошибок `AuthChallenge`
- `400 / EMPTY_LOGIN` — пустой `login`.
- `400 / ALREADY_AUTHED` — по текущему соединению уже выполнена авторизация.
- `422 / UNKNOWN_USER` — пользователь с таким `login` не найден.
- `501 / SOLANA_IMPORT_FAILED` — сервер не смог проверить/импортировать пользователя из Solana при lazy-import.
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера, если появится вне штатного сценария.
---
## 3. `CreateAuthSession`
### Запрос
```json
{
"op": "CreateAuthSession",
"requestId": "create-001",
"payload": {
"login": "alice",
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
"timeMs": 1774600000123,
"authNonce": "nonce",
"clientKey": "BASE64_DEVICE_PUBLIC_KEY",
"signatureB64": "BASE64_SIGNATURE",
"sessionType": 1,
"clientPlatform": "Web",
"clientInfo": "Android 15; Pixel 9"
}
}
```
### Строка для подписи
```text
AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
```
### Дополнительная проверка ключа
Перед проверкой подписи сервер должен:
1. взять актуальный `solana_users.client_key`;
2. сравнить его с `payload.clientKey`;
3. только потом проверять подпись.
Если `clientKey` не совпадает, сервер возвращает ошибку `DEVICE_KEY_NOT_ACTUAL`.
На будущее:
- для ротации `client_key` желательно добавить перепроверку через Solana.
### Успешный ответ
```json
{
"op": "CreateAuthSession",
"requestId": "create-001",
"status": 200,
"ok": true,
"payload": {
"sessionId": "sess_7c5e5c4b"
}
}
```
### Специфические коды ошибок `CreateAuthSession`
- `400 / NO_STEP1_CONTEXT` — для данного соединения не был корректно выполнен `AuthChallenge`.
- `400 / EMPTY_LOGIN` — пустой `login`.
- `400 / LOGIN_MISMATCH``login` не совпадает с тем, для кого был выдан `authNonce`.
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при повторном чтении пользователя.
- `422 / USER_NOT_FOUND` — пользователь не найден.
- `501 / NO_LOGIN`у пользователя на сервере не заполнен `login`.
- `400 / EMPTY_STORAGE_PWD` — пустой `storagePwd`.
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `clientKey` не поддерживается текущим сервером.
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `clientKey` или `signatureB64`.
- `400 / EMPTY_SIGNATURE` — пустая подпись.
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
- `400 / NO_DEVICE_KEY`у пользователя в БД отсутствует `clientKey`.
- `400 / EMPTY_AUTH_NONCE` — пустой `authNonce`.
- `400 / AUTH_NONCE_MISMATCH``authNonce` не соответствует значению из `AuthChallenge`.
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `clientKey`.
- `422 / DEVICE_KEY_NOT_ACTUAL``clientKey` не совпадает с актуальной версией на сервере.
- `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` — непредвиденная внутренняя ошибка сервера.
---
## 4. `SessionChallenge`
### Запрос
```json
{
"op": "SessionChallenge",
"requestId": "sch-001",
"payload": {
"sessionId": "sess_7c5e5c4b"
}
}
```
### Успешный ответ
```json
{
"op": "SessionChallenge",
"requestId": "sch-001",
"status": 200,
"ok": true,
"payload": {
"nonce": "0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66"
}
}
```
### Специфические коды ошибок `SessionChallenge`
- `400 / EMPTY_SESSION_ID` — пустой `sessionId`.
- `501 / DB_ERROR` — ошибка БД при чтении сессии.
- `422 / SESSION_NOT_FOUND` — сессия не найдена.
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
---
## 5. `SessionLogin`
### Запрос
```json
{
"op": "SessionLogin",
"requestId": "slogin-001",
"payload": {
"sessionId": "sess_7c5e5c4b",
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
"timeMs": 1774600010456,
"signatureB64": "BASE64_SIGNATURE",
"sessionType": 1,
"clientPlatform": "Web",
"clientInfo": "Android 15; Pixel 9"
}
}
```
### Строка для подписи
```text
SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
```
### Дополнительная проверка ключа
Перед проверкой подписи сервер должен:
1. взять `active_sessions.session_key`;
2. сравнить его с `payload.sessionKey`;
3. только потом проверять подпись.
Если ключ не совпадает, сервер возвращает ошибку `SESSION_KEY_NOT_ACTUAL`.
### Успешный ответ
```json
{
"op": "SessionLogin",
"requestId": "slogin-001",
"status": 200,
"ok": true,
"payload": {
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET"
}
}
```
### Специфические коды ошибок `SessionLogin`
- `400 / EMPTY_SESSION_ID` — пустой `sessionId`.
- `400 / NO_CHALLENGE` — перед `SessionLogin` не был успешно выполнен `SessionChallenge` либо nonce уже истёк.
- `400 / SESSION_ID_MISMATCH` — nonce был выдан для другого `sessionId`.
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
- `400 / EMPTY_SIGNATURE` — пустая подпись.
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
- `501 / DB_ERROR` — ошибка БД при чтении сессии.
- `422 / SESSION_NOT_FOUND` — сессия не найдена.
- `501 / NO_SESSION_KEY`у сессии отсутствует `session_key`.
- `422 / SESSION_KEY_NOT_ACTUAL` — переданный `sessionKey` не совпадает с актуальной версией на сервере.
- `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`
В этом потоке:
- новое устройство не владеет `clientKey` и не проходит обычный `CreateAuthSession`;
- пароль проверяется сервером только как фильтр;
- решение об одобрении принимает уже авторизованная доверенная сессия пользователя;
- сервер не расшифровывает `encryptedPayload` и не становится источником приватных ключей.
Точные форматы этих операций см. в `03_Session_Management_API.md` и в протокольном документе:
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
---
## 6. Пример ошибки
```json
{
"op": "SessionLogin",
"requestId": "slogin-001",
"status": 403,
"ok": false,
"error": "SESSION_KEY_NOT_ACTUAL",
"message": "session_key не соответствует актуальной версии",
"payload": {
}
}
```

View File

@ -0,0 +1,485 @@
# API для разработчиков: Управление сессиями
Этот файл описывает методы, которые используются уже после успешной авторизации пользователя в сессию.
Здесь два метода:
- `ListSessions` — получить список активных сессий пользователя;
- `CloseActiveSession` — закрыть одну из активных сессий.
Дополнительно в этом же слое управления сессиями появился сценарий pairing через доверенную уже авторизованную сессию пользователя:
- `GetTrustedDeviceLoginSettings`
- `UpsertTrustedDeviceLoginSettings`
- `ListTrustedDeviceLoginRequests`
- `ApproveTrustedDeviceLogin`
- `RejectTrustedDeviceLogin`
- `CancelTrustedDeviceLogin`
Анонимное новое устройство работает с двумя связанными операциями:
- `StartTrustedDeviceLogin`
- `GetTrustedDeviceLoginStatus`
Логика раздела такая:
- сначала пользователь проходит `SessionLogin`;
- после этого сервер считает соединение авторизованным;
- уже в этом состоянии клиент может читать список сессий и управлять ими.
То есть это не этап создания или входа в сессию, а этап последующего контроля уже существующих активных сессий.
## 1. `ListSessions`
Доступно только после успешного `SessionLogin`.
### Запрос
```json
{
"op": "ListSessions",
"requestId": "list-001",
"payload": {
}
}
```
### Успешный ответ
```json
{
"op": "ListSessions",
"requestId": "list-001",
"status": 200,
"ok": true,
"payload": {
"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",
"lastAuthenticatedAtMs": 1774600010500
}
]
}
}
```
### Специфические коды ошибок `ListSessions`
- `422 / NOT_AUTHENTICATED` — запрос доступен только после успешного `SessionLogin`.
- `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`
Доступно только после успешного `SessionLogin`.
### Запрос
```json
{
"op": "CloseActiveSession",
"requestId": "close-001",
"payload": {
"sessionId": "sess_7c5e5c4b"
}
}
```
### Успешный ответ
```json
{
"op": "CloseActiveSession",
"requestId": "close-001",
"status": 200,
"ok": true,
"payload": {
}
}
```
### Специфические коды ошибок `CloseActiveSession`
- `422 / NOT_AUTHENTICATED` — запрос доступен только после успешного `SessionLogin`.
- `400 / NO_SESSION_TO_CLOSE` — сервер не смог определить, какую сессию нужно закрыть.
- `501 / DB_ERROR` — ошибка БД при поиске сессии или её удалении.
- `422 / SESSION_NOT_FOUND` — целевая сессия не найдена.
- `422 / SESSION_OF_ANOTHER_USER` — нельзя закрывать сессию другого пользователя.
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
---
## 3. Пример ошибки
```json
{
"op": "CloseActiveSession",
"requestId": "close-001",
"status": 403,
"ok": false,
"error": "NOT_AUTHENTICATED",
"message": "Операция доступна только для авторизованных пользователей",
"payload": {
}
}
```
## 4. Формат `sessionId`
Текущее серверное значение `sessionId` генерируется как:
- случайные **32 байта** (`SecureRandom`),
- кодирование в **стандартный Base64 RFC 4648** (алфавит `A-Z a-z 0-9 + /`),
- **без padding** `=`.
Практически это строка длиной около **43 символов** (для 32 байт без `=`).
Пример реального формата:
```
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-заявку.
Поле `shortCode` теперь содержит `10` цифр. В UI его рекомендуется показывать как `5` пар, например: `49 20 70 91 23`.
TTL заявки фиксирован на сервере и сейчас всегда равен `300` секундам.
### Успешный ответ
```json
{
"op": "StartTrustedDeviceLogin",
"requestId": "esp-start-001",
"status": 200,
"ok": true,
"payload": {
"pairingId": "base64url",
"state": "created",
"shortCode": "4920709123",
"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": "4920709123",
"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": "4920709123",
"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

@ -0,0 +1,239 @@
# API для разработчиков: 04 — Запись и чтение блока блокчейна
Документ описывает **текущий рабочий формат** сетевых вызовов:
- `AddBlock` — запись любого блока в блокчейн пользователя;
- `GetBlockchainBlock` — публичное чтение одного конкретного блока по имени цепочки и номеру.
`GetBlockchainBlock` нужен в том числе для межсерверной синхронизации и для открытого чтения публичного блокчейна по одному блоку.
> Важный принцип: на уровне JSON API сейчас есть **один универсальный метод** записи — `AddBlock`.
> Конкретный смысл записи задаётся типом самого бинарного блока (`type/subType/version` в заголовке блока).
## 1. Что делает `AddBlock`
`AddBlock`:
- принимает имя блокчейна и base64 бинарного блока;
- проверяет непрерывность цепочки (`blockNumber`, `prevHash`);
- проверяет формат и подпись Ed25519;
- валидирует `body` по правилам типа блока;
- сохраняет блок и обновляет состояние цепочки.
## 2. JSON формат запроса
`op = "AddBlock"`.
```json
{
"op": "AddBlock",
"requestId": "req-1001",
"payload": {
"blockchainName": "alice-001",
"blockNumber": 12,
"prevBlockHash": "ab12...ff",
"blockBytesB64": "AAAB..."
}
}
```
Поля `payload`:
- `blockchainName` — обязательно, формат `login-NNN`.
- `blockNumber` — обязательно (временное legacy-поле для совместимости; должно совпасть с номером внутри бинарного блока).
- `prevBlockHash` — legacy-поле, сейчас сервер использует `prevHash` из бинарного блока и состояние цепочки.
- `blockBytesB64` — обязательно: **полный бинарный блок** (`preimage + sigMarker + signature`) в Base64.
## 3. Успешный ответ
```json
{
"op": "AddBlock",
"requestId": "req-1001",
"status": 200,
"ok": true,
"payload": {
"reasonCode": null,
"serverLastGlobalNumber": 12,
"serverLastGlobalHash": "9f0e...a1"
}
}
```
## 4. Ошибка (единый формат)
При ошибках сервер отдаёт `Net_Exception_Response` со стандартными полями и дополнительно с состоянием сервера для ресинка:
```json
{
"op": "AddBlock",
"requestId": "req-1001",
"status": 400,
"ok": false,
"error": "bad_prev_hash",
"message": "Некорректный prevHash (цепочка не совпадает)",
"payload": {
"serverLastGlobalNumber": 11,
"serverLastGlobalHash": "c3d4...98"
}
}
```
### Основные `reasonCode`
- `empty_blockchain_name`, `bad_blockchain_name`
- `blockchain_state_not_found`
- `bad_block_base64`, `bad_block_format`, `bad_block_body`
- `bad_block_number`, `req_global_mismatch`, `bad_prev_hash`
- `bad_signature`, `signature_verify_failed`
- `prev_line_block_not_found`, `bad_prev_line_hash`
- `limit_exceeded`
- `chain_resync_in_progress` — цепочка временно заблокирована полным resync
- `repost_disabled` — репосты временно отключены до будущей реализации
- `internal_error`
## 5. Какие блоки реально можно добавлять через `AddBlock`
Через `AddBlock` можно писать поддержанные форматы, кроме явно отключённых временных фич:
1. **TECH (type=0)**
- `HEADER_COMPAT (subType=0)`
- `TECH_CREATE_CHANNEL (subType=1)`
2. **TEXT (type=1)**
- `TEXT_POST (10)`
- `TEXT_EDIT_POST (11)`
- `TEXT_REPLY (20)`
- `TEXT_EDIT_REPLY (21)`
- `TEXT_REPOST (30)` — формат зарезервирован, но новые блоки временно отклоняются с `repost_disabled`
3. **REACTION (type=2)**
- `REACTION_LIKE (1)`
4. **CONNECTION (type=3)**
- `CONNECTION_FRIEND (10)`
- `CONNECTION_UNFRIEND (11)`
- `CONNECTION_CONTACT (20)`
- `CONNECTION_UNCONTACT (21)`
- `CONNECTION_FOLLOW (30)`
- `CONNECTION_UNFOLLOW (31)`
- `CONNECTION_SPOUSE (40)`
- `CONNECTION_UNSPOUSE (41)`
- `CONNECTION_PARENT (50)`
- `CONNECTION_UNPARENT (51)`
- `CONNECTION_CHILD (52)`
- `CONNECTION_UNCHILD (53)`
- `CONNECTION_SIBLING (54)`
- `CONNECTION_UNSIBLING (55)`
- `CONNECTION_KNOWN_PERSON (60)`
- `CONNECTION_UNKNOWN_PERSON (61)`
- `CONNECTION_SHINE_CONFIRMED (70)`
- `CONNECTION_SHINE_UNCONFIRMED (71)`
- `CONNECTION_SHINE_SEEN (74)`
- `CONNECTION_SHINE_UNSEEN (75)`
5. **USER_PARAM (type=4)**
- `USER_PARAM_TEXT_TEXT (1)`
## 6. Хватает ли функций сейчас
Коротко: **для записи событий в блокчейн — хватает**, для полноценного клиентского чтения — **пока не хватает**.
Что есть:
- единый надёжный write-путь `AddBlock`;
- есть `GetFriendsLists` и API по `UserParam`;
- есть унифицированные коды ошибок и поля для ресинхронизации.
Что пока ограничивает продукт:
- нет полноценного read API для каналов/постов/тредов;
- нет API списка подписок с серверными счётчиками непрочитанного;
- нет ленты событий (новые ответы/лайки/подписки) как отдельного RPC.
## 7. Рекомендации по клиенту при записи блоков
1. Перед отправкой держать локальный `lastNumber/lastHash`.
2. При `bad_prev_hash` или `bad_block_number`:
- взять `serverLastGlobalNumber/serverLastGlobalHash` из ошибки,
- пересобрать следующий блок на актуальной вершине.
3. Для edit-блоков всегда ссылаться на **оригинальный** блок, а не на предыдущий edit.
4. Для связей/подписок использовать target на **root** (HEADER или CREATE_CHANNEL), а не на произвольный пост.
## 8. USER_PARAM для «личных данных»
Да, на текущем API это можно добавить **без изменения серверного кода**:
- в `UserParam` поле `param` сейчас не ограничено фиксированным справочником;
- сервер хранит пары `param -> value` как строки (при наличии корректной подписи и `time_ms`);
- чтение уже есть через `GetUserParam` и `ListUserParams`.
Рекомендуемый стартовый набор ключей для профиля (MVP):
- `name`
- `last_name`
- `address_physical`
- `address_web`
- `phone`
Практическая рекомендация: заранее зафиксировать единый словарь ключей в клиенте/документации, чтобы избежать дублей вида `lastname` vs `last_name`, `site` vs `address_web` и т.д.
Ограничения, которые важно учесть:
- сейчас нет серверной ACL-политики чтения параметров (в MVP их может читать любой клиент, который знает `login`);
- нет валидации формата значений для конкретных ключей (телефон, URL и т.д. проверяются только на стороне клиента);
- нет отдельного индекса/поиска по этим полям — только точечное чтение и listing по `login`.
---
## 9. `GetBlockchainBlock`
### Назначение
Публичное чтение одного конкретного блока из цепочки.
Нужно для:
- открытого чтения блокчейна по одному блоку;
- межсерверной синхронизации;
- восстановления/докачки отсутствующего хвоста цепочки.
### JSON формат запроса
`op = "GetBlockchainBlock"`.
```json
{
"op": "GetBlockchainBlock",
"requestId": "req-2001",
"payload": {
"blockchainName": "alice-001",
"blockNumber": 12
}
}
```
Поля `payload`:
- `blockchainName` — обязательно, формат `login-NNN`.
- `blockNumber` — обязательно, номер блока в цепочке, `>= 0`.
### Успешный ответ
```json
{
"op": "GetBlockchainBlock",
"requestId": "req-2001",
"status": 200,
"ok": true,
"payload": {
"blockchainName": "alice-001",
"blockNumber": 12,
"blockHash": "9f0eaabbccddeeff00112233445566778899aabbccddeeff0011223344556677",
"blockBytesB64": "AAAB..."
}
}
```
### Ошибки
- `400 / BAD_FIELDS` — некорректные `blockchainName` или `blockNumber`.
- `404 / BLOCK_NOT_FOUND` — такого блока нет.
- `500 / INTERNAL_ERROR` — внутренняя ошибка сервера.

View File

@ -0,0 +1,615 @@
# API для разработчиков: Технические запросы
Этот файл описывает технические WebSocket-запросы, которые нужны для служебной работы клиента с сервером. Часть операций доступна без авторизации, часть требует успешной авторизованной сессии.
Сейчас здесь девять методов:
- `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения;
- `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети;
- `ListBlockchainHeads` — краткая сводка по всем локальным блокчейнам сервера для межсерверной синхронизации;
- `GetSyncUserProfile` — межсерверный профиль пользователя для создания локальной цепочки без Solana RPC;
- `SendSignal` — общий межсессионный технический сигнал в одну конкретную сессию или сразу во все активные сессии пользователя;
- `GetCallIceConfig` — выдача STUN/TURN конфигурации для звонков;
- `ClientErrorLog` — отправка клиентской ошибки в серверный лог;
- `ClientDebugLog` — отправка клиентского debug-события в серверный буфер;
- `CallDeliveryReport` — диагностический отчёт клиента о доставке/установке звонка.
Логика раздела такая:
- `Ping` нужен для регулярной проверки, что соединение всё ещё живо;
- `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла.
- `ListBlockchainHeads` нужен для сервер-сервер сверки: партнёр получает список heads по всем цепочкам, сравнивает его со своим состоянием и затем добирает недостающие блоки по диапазону.
- `GetSyncUserProfile` нужен для server-to-server режима, когда принимающий сервер хочет создать у себя локальные `solana_users + blockchain_state` без прямого обращения в Solana. Это используется как временный обход ограничений внешнего Solana RPC.
- `SendSignal` нужен для доверенных межсессионных команд одного пользователя. Первое практическое применение — `remote AddBlock via homeserver session`, но формат задуман как общий transport на вырост.
Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов.
## 1. `Ping`
### Назначение
Служебный keep-alive запрос.
Клиент может отправлять его периодически, чтобы:
- поддерживать активное WebSocket-соединение;
- понимать, что сервер отвечает;
- при необходимости получать текущее серверное время.
### Запрос
```json
{
"op": "Ping",
"requestId": "ping-001",
"payload": {
"ts": 1774700000123
}
}
```
Поле `ts` в запросе необязательно для логики сервера. Сервер его не валидирует и не использует для принятия решения.
### Успешный ответ
```json
{
"op": "Ping",
"requestId": "ping-001",
"status": 200,
"ok": true,
"payload": {
"ts": 1774700000456
}
}
```
### Специфические коды ошибок `Ping`
- У `Ping` нет специальных прикладных ошибок.
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
---
## 2. `GetServerInfo`
### Назначение
Запрос публичной информации о сервере.
Он нужен клиенту для выбора сервера в децентрализованной сети. По этому запросу клиент может:
- проверить, что сервер вообще доступен;
- показать URL и версию сервера;
- показать физический регион или адрес размещения;
- показать описание сервера;
- показать поле `origin` как комментарий о природе этого узла;
- показать дополнительную текстовую информацию.
Этот запрос доступен без авторизации.
### Источник данных
- `version` берётся из Gradle build и подставляется в `application.properties`;
- остальные поля читаются из настроек сервера;
- если значение в конфиге не задано, сервер возвращает пустую строку.
### Запрос
```json
{
"op": "GetServerInfo",
"requestId": "srv-001",
"payload": {
}
}
```
### Успешный ответ
```json
{
"op": "GetServerInfo",
"requestId": "srv-001",
"status": 200,
"ok": true,
"payload": {
"url": "wss://node.example.org/ws",
"version": "1.0",
"physicalRegion": "Грузия, Тбилиси",
"description": "Public community SHiNE node",
"origin": "Community-operated node",
"extraInfo": "IPv4 + IPv6; test federation enabled"
}
}
```
### Поля ответа
- `url` — публичный URL сервера.
- `version` — версия сервера из Gradle build.
- `physicalRegion` — физический регион или адрес размещения сервера.
- `description` — человекочитаемое описание сервера.
- `origin` — комментарий о том, какой это сервер.
- `extraInfo` — любая дополнительная информация о сервере.
### Специфические коды ошибок `GetServerInfo`
- У `GetServerInfo` нет специальных прикладных ошибок при штатной работе.
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
---
## 3. `ListBlockchainHeads`
### Назначение
Запрос краткой сводки по всем локальным блокчейнам сервера.
Нужен для межсерверной синхронизации. Партнёр может:
- получить список всех блокчейнов;
- сравнить `lastBlockNumber` и `lastBlockHash` со своими значениями;
- понять, какие цепочки нужно догонять;
- затем отдельно запросить недостающие блоки по диапазону.
Этот запрос доступен без авторизации.
### Запрос
```json
{
"op": "ListBlockchainHeads",
"requestId": "heads-001",
"payload": {}
}
```
### Успешный ответ
```json
{
"op": "ListBlockchainHeads",
"requestId": "heads-001",
"status": 200,
"ok": true,
"payload": {
"blockchains": [
{
"blockchainName": "alice_main",
"lastBlockNumber": 124,
"lastBlockHash": "aabbccdd00112233445566778899aabbccddeeff00112233445566778899aabb",
"fileSizeBytes": 58720
}
]
}
}
```
### Поля ответа
- `blockchains` — массив текущих heads всех цепочек сервера.
- `blockchainName` — имя блокчейна.
- `lastBlockNumber` — последний номер блока в этой цепочке.
- `lastBlockHash` — последний хэш блока в HEX-формате `64` символа.
- `fileSizeBytes` — текущий размер файла блокчейна в байтах.
### Специфические коды ошибок `ListBlockchainHeads`
- У `ListBlockchainHeads` нет специальных прикладных ошибок при штатной работе.
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
---
## 4. `GetSyncUserProfile`
### Назначение
Запрос минимального профиля пользователя для межсерверной синхронизации.
Нужен в сценарии, когда сервер во время periodic sync увидел чужой блокчейн, которого у него локально ещё нет. Вместо обращения в Solana PDA он может запросить у партнёра:
- `login`
- `blockchainName`
- `solanaKey`
- `blockchainKey`
- `clientKey`
- `blockchainSizeLimitBytes`
После этого принимающий сервер может локально создать записи в `solana_users` и `blockchain_state`, а затем уже докачивать блоки через `GetBlockchainBlock`.
Этот запрос доступен без авторизации и предназначен именно для server-to-server sync.
### Запрос
```json
{
"op": "GetSyncUserProfile",
"requestId": "sync-user-001",
"payload": {
"login": "alice"
}
}
```
### Успешный ответ: пользователь не найден
```json
{
"op": "GetSyncUserProfile",
"requestId": "sync-user-001",
"status": 200,
"ok": true,
"payload": {
"exists": false
}
}
```
### Успешный ответ: пользователь найден
```json
{
"op": "GetSyncUserProfile",
"requestId": "sync-user-001",
"status": 200,
"ok": true,
"payload": {
"exists": true,
"login": "alice",
"blockchainName": "alice-001",
"solanaKey": "BASE64_32",
"blockchainKey": "BASE64_32",
"clientKey": "BASE64_32",
"blockchainSizeLimitBytes": 100000
}
}
```
### Поля ответа
- `exists` — найден ли пользователь на сервере-партнёре.
- `login` — канонический login из БД сервера-партнёра.
- `blockchainName` — имя основной цепочки пользователя.
- `solanaKey` — публичный ключ логина.
- `blockchainKey` — публичный ключ блокчейна.
- `clientKey` — публичный клиентский ключ, который в текущей модели используется при создании локальной записи.
- `blockchainSizeLimitBytes` — лимит размера файла блокчейна, который будет записан в локальный `blockchain_state`.
### Специфические коды ошибок `GetSyncUserProfile`
- `400 / BAD_FIELDS` — пустой или некорректный `login`.
- `404 / BLOCKCHAIN_STATE_NOT_FOUND` — пользователь найден, но на сервере-партнёре отсутствует `blockchain_state` для его цепочки.
- При непредвиденной ошибке сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
---
## 5. `SendSignal`
Доступно только после успешной авторизации.
### Назначение
Общий межсессионный технический сигнал.
Этот метод нужен для случаев, когда одна активная сессия пользователя должна быстро передать служебную команду другой сессии того же пользователя или сразу всем его активным сессиям.
Первый целевой сценарий:
- `remote AddBlock via homeserver session`
То есть телефон без локального `blockchain.key` может:
- подготовить только сырой payload операции без текущей вершины цепочки;
- подписать сам `SendSignal` своим `session key`;
- дополнительно подписать его `client key`, чтобы homeserver/ESP32 точно видел, что запрос пришёл от доверенного клиента этого же логина;
- отправить запрос в выбранную `homeserver`-сессию;
- получить от неё ответ после настоящего `AddBlock`, который homeserver соберёт и подпишет уже сама.
### Режимы доставки
- `targetMode = "single_session"` — доставка в одну конкретную `targetSessionId`.
- `targetMode = "all_sessions"` — доставка во все активные сессии указанного логина.
### Важное правило подписи
Сам `SendSignal` не подписывает поле `data` отдельной вложенной подписью. Вместо этого сервер проверяет подписи по общему preimage сигнала, в который входит:
- `fromLogin`
- `fromSessionId`
- `toLogin`
- `targetMode`
- `targetSessionId`
- `signalType`
- `signalRequestId`
- `timeMs`
- `sha256(data)`
Поддерживаются две подписи:
- `sessionSignatureB64` — обязательная подпись текущей авторизованной `session key`;
- `clientSignatureB64` — необязательная подпись `client key`.
Для сценария `remote AddBlock via homeserver` текущая договорённость такая:
- запрос должен идти только своему же логину;
- запрос должен быть подписан и `session key`, и `client key`;
- в будущем для отдельных wallet-сценариев `clientSignatureB64` может быть пустой.
### Запрос в одну сессию
```json
{
"op": "SendSignal",
"requestId": "ws-req-001",
"payload": {
"toLogin": "alice",
"targetMode": "single_session",
"targetSessionId": "sess-hs-001",
"signalType": "remote_addblock_request",
"signalRequestId": "remote-addblock-001",
"data": "{\"operation\":\"remote_addblock_request\",\"signalRequestId\":\"remote-addblock-001\",\"blockchainName\":\"alice_main\",\"blockBodyB64\":\"...\"}",
"timeMs": 1774700000123,
"sessionSignatureB64": "BASE64_64",
"clientSignatureB64": "BASE64_64"
}
}
```
### Успешный ответ
```json
{
"op": "SendSignal",
"requestId": "ws-req-001",
"status": 200,
"ok": true,
"payload": {
"deliveredCount": 1,
"deliveredSessionIds": ["sess-hs-001"]
}
}
```
### Событие на принимающей стороне
```json
{
"op": "IncomingSignal",
"eventId": "evt-001",
"payload": {
"fromLogin": "alice",
"fromSessionId": "sess-phone-001",
"toLogin": "alice",
"targetMode": "single_session",
"targetSessionId": "sess-hs-001",
"signalType": "remote_addblock_request",
"signalRequestId": "remote-addblock-001",
"data": "{\"operation\":\"remote_addblock_request\",\"signalRequestId\":\"remote-addblock-001\",\"blockchainName\":\"alice_main\",\"blockBodyB64\":\"...\"}",
"timeMs": 1774700000123,
"sessionSignatureB64": "BASE64_64",
"clientSignatureB64": "BASE64_64",
"dataSha256B64": "BASE64_32"
}
}
```
### Специфика `remote AddBlock`
Для `remote_addblock_request` поле `data` теперь содержит:
- `blockchainName`
- `blockBodyB64`
Где `blockBodyB64` — это не финальный блок и не почти готовый preimage, а компактный бинарный контейнер:
- `msgType` (`u16`)
- `msgSubType` (`u16`)
- `msgVersion` (`u16`)
- `bodyBytes`
После этого homeserver сама:
- вызывает `GetUser(login)` и получает `serverLastGlobalNumber/serverLastGlobalHash`;
- вычисляет новый `blockNumber = last + 1`;
- подставляет актуальный `prevBlockHash`;
- ставит текущее время;
- досчитывает полный preimage;
- подписывает его своим `blockchain key`;
- и только потом делает настоящий `AddBlock`.
### Специфические коды ошибок `SendSignal`
- `422 / NOT_AUTHENTICATED` — требуется авторизация.
- `400 / BAD_FIELDS` — не хватает обязательных полей или нарушено правило `single_session/all_sessions`.
- `400 / BAD_TARGET_MODE` — передан неизвестный `targetMode`.
- `400 / TIME_SKEW``timeMs` отличается от серверного более чем на 30 секунд.
- `500 / NO_CLIENT_KEY` — для текущего пользователя не найден `client key`.
- `404 / USER_NOT_FOUND` — логин адресата не найден.
- `400 / BAD_DATA` — сервер не смог обработать `data`.
- `400 / BAD_SESSION_SIGNATURE` — некорректная подпись `session key`.
- `400 / BAD_CLIENT_SIGNATURE` — некорректная подпись `client key`.
- `404 / SESSION_NOT_FOUND` — при `single_session` целевая сессия не найдена или не онлайн.
- `404 / NO_TARGET_SESSIONS` — при `all_sessions` у пользователя сейчас нет активных онлайн-сессий.
- `404 / DELIVERY_FAILED` — сервер не смог отправить событие ни в одну из целевых сессий.
---
## 6. `GetCallIceConfig`
Доступно только после успешной авторизации.
### Запрос
```json
{
"op": "GetCallIceConfig",
"requestId": "ice-001",
"payload": {
}
}
```
### Успешный ответ
```json
{
"op": "GetCallIceConfig",
"requestId": "ice-001",
"status": 200,
"ok": true,
"payload": {
"stunUrls": ["stun:stun.example.org:3478"],
"turnUrls": ["turn:turn.example.org:3478?transport=udp"],
"turnUsername": "user",
"turnPassword": "password",
"turnServers": [
{
"id": "primary",
"urls": ["turn:turn.example.org:3478?transport=udp"],
"username": "user",
"password": "password"
}
],
"turnEnabled": true,
"generatedAtMs": 1774700000123,
"expiresAtMs": 1774700300123,
"ttlSec": 300
}
}
```
### Специфические коды ошибок `GetCallIceConfig`
- `422 / NOT_AUTHENTICATED` — требуется авторизация.
---
## 7. `ClientErrorLog`
### Запрос
```json
{
"op": "ClientErrorLog",
"requestId": "err-001",
"payload": {
"kind": "global_error",
"message": "TypeError: failed",
"stack": "...",
"sourceUrl": "https://shineup.me/app.js",
"lineNumber": 10,
"columnNumber": 20,
"route": "#/channel-view/own-0",
"href": "https://shineup.me/#/channel-view/own-0",
"userAgent": "...",
"clientTs": 1774700000123,
"requestOp": "GetChannelMessages",
"requestIdRef": "GetChannelMessages-123",
"contextJson": "{\"screen\":\"channels\"}"
}
}
```
### Успешный ответ
```json
{
"op": "ClientErrorLog",
"requestId": "err-001",
"status": 200,
"ok": true,
"payload": {
"serverTs": 1774700000456,
"accepted": true
}
}
```
### Специфические коды ошибок `ClientErrorLog`
- `400 / BAD_FIELDS` — обязательные поля ошибки не заполнены.
---
## 8. `ClientDebugLog`
### Запрос
```json
{
"op": "ClientDebugLog",
"requestId": "dbg-001",
"payload": {
"runId": "ui-run-1",
"level": "info",
"message": "opened channels tab",
"details": "{\"route\":\"#/channels\"}"
}
}
```
### Успешный ответ
```json
{
"op": "ClientDebugLog",
"requestId": "dbg-001",
"status": 200,
"ok": true,
"payload": {
"accepted": true,
"serverTs": 1774700000456
}
}
```
### Специфические коды ошибок `ClientDebugLog`
- `400 / BAD_FIELDS` — поле `message` не заполнено.
---
## 9. `CallDeliveryReport`
### Запрос
```json
{
"op": "CallDeliveryReport",
"requestId": "call-report-001",
"payload": {
"type": "outgoing_failed",
"value": "{\"reason\":\"ice_failed\",\"callId\":\"call-1\"}"
}
}
```
### Успешный ответ
```json
{
"op": "CallDeliveryReport",
"requestId": "call-report-001",
"status": 200,
"ok": true,
"payload": {
"serverTs": 1774700000456,
"accepted": true
}
}
```
### Специфические коды ошибок `CallDeliveryReport`
- `400 / BAD_FIELDS` — поле `type` не заполнено.
---
## 10. Короткое резюме
- `Ping` нужен для keep-alive и проверки, что WebSocket-соединение живо.
- `GetServerInfo` нужен для выбора сервера в сети и показа публичной информации об узле.
- `SendSignal` нужен для доверенных межсессионных сигналов одного пользователя, включая `remote AddBlock via homeserver session`.
- `GetCallIceConfig` нужен для WebRTC-звонков и требует авторизации.
- `ClientErrorLog`, `ClientDebugLog`, `CallDeliveryReport` используются для диагностики клиента и звонков.

View File

@ -0,0 +1,341 @@
# 06. Channels Read API
## Человеко-читаемое объяснение
Эти функции — это **чтение данных каналов** для UI:
1. `ListSubscriptionsFeed` — отдает данные для экрана списка каналов:
- ваши каналы (личный + созданные вами),
- каналы пользователей, на кого вы подписаны,
- отдельные каналы, на которые вы подписаны напрямую.
2. `GetChannelMessages` — отдает полную ленту одного канала (пока без курсоров, загружается сразу целиком),
включая версии сообщений, лайки и ответы.
3. `GetMessageThread` — отдает дерево обсуждения вокруг конкретного сообщения:
предки, фокус-сообщение, потомки.
4. `GetChannelsCounters` — отдает счетчики разделов каналов для пользователя.
5. `ListGroupChats200` — отдает список групповых чатов типа `200`.
6. `GetGroupDialog` — отдает сообщения конкретного группового чата типа `200`.
> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки.
---
## 1) ListSubscriptionsFeed
### Request
```json
{
"op": "ListSubscriptionsFeed",
"requestId": "req-1",
"payload": {
"login": "Alice",
"limit": 200
}
}
```
### Response (success)
```json
{
"op": "ListSubscriptionsFeed",
"requestId": "req-1",
"status": 200,
"ok": true,
"payload": {
"login": "Alice",
"ownedChannels": [
{
"channel": {
"ownerLogin": "Alice",
"ownerBlockchainName": "alice-001",
"channelName": "0",
"personal": true,
"channelRoot": { "blockNumber": 0, "blockHash": "..." }
},
"messagesCount": 120,
"lastMessage": {
"messageRef": { "blockNumber": 921, "blockHash": "..." },
"text": "последняя версия текста",
"createdAtMs": 1760000000000,
"authorLogin": "Alice",
"authorBlockchainName": "alice-001"
}
}
],
"followedUsersChannels": [
{
"channel": {
"ownerLogin": "Bob",
"ownerBlockchainName": "bob-001",
"channelName": "0",
"personal": true,
"channelRoot": { "blockNumber": 0, "blockHash": "..." }
},
"messagesCount": 540,
"lastMessage": {
"messageRef": { "blockNumber": 922, "blockHash": "..." },
"text": "последняя версия текста",
"createdAtMs": 1760000100000,
"authorLogin": "Bob",
"authorBlockchainName": "bob-001"
}
}
],
"followedChannels": [
{
"channel": {
"ownerLogin": "Carl",
"ownerBlockchainName": "carl-001",
"channelName": "market",
"personal": false,
"channelRoot": { "blockNumber": 456, "blockHash": "..." }
},
"messagesCount": 90,
"lastMessage": {
"messageRef": { "blockNumber": 1002, "blockHash": "..." },
"text": "актуальный текст",
"createdAtMs": 1760001000000,
"authorLogin": "Carl",
"authorBlockchainName": "carl-001"
}
}
]
}
}
```
---
## 2) GetChannelMessages
### Request
```json
{
"op": "GetChannelMessages",
"requestId": "req-2",
"payload": {
"channel": {
"ownerBlockchainName": "bob-001",
"channelRootBlockNumber": 123,
"channelRootBlockHash": "..."
},
"limit": 200,
"sort": "asc"
}
}
```
### Response (success)
```json
{
"op": "GetChannelMessages",
"requestId": "req-2",
"status": 200,
"ok": true,
"payload": {
"channel": {
"ownerLogin": "Bob",
"ownerBlockchainName": "bob-001",
"channelName": "news",
"channelRoot": { "blockNumber": 123, "blockHash": "..." }
},
"messages": [
{
"messageRef": { "blockNumber": 140, "blockHash": "..." },
"authorLogin": "Bob",
"authorBlockchainName": "bob-001",
"createdAtMs": 1760000000000,
"text": "текущая версия",
"likesCount": 12,
"repliesCount": 3,
"versionsTotal": 4,
"versions": [
{ "versionIndex": 1, "blockNumber": 140, "blockHash": "...", "text": "v1", "createdAtMs": 1760000000000 },
{ "versionIndex": 2, "blockNumber": 155, "blockHash": "...", "text": "v2", "createdAtMs": 1760001000000 },
{ "versionIndex": 3, "blockNumber": 170, "blockHash": "...", "text": "v3", "createdAtMs": 1760002000000 },
{ "versionIndex": 4, "blockNumber": 199, "blockHash": "...", "text": "v4", "createdAtMs": 1760003000000 }
]
}
]
}
}
```
---
## 3) GetMessageThread
### Request
```json
{
"op": "GetMessageThread",
"requestId": "req-3",
"payload": {
"message": {
"blockchainName": "bob-001",
"blockNumber": 333,
"blockHash": "..."
},
"depthUp": 20,
"depthDown": 2,
"limitChildrenPerNode": 50
}
}
```
### Response (success)
```json
{
"op": "GetMessageThread",
"requestId": "req-3",
"status": 200,
"ok": true,
"payload": {
"ancestors": [MessageNode],
"focus": MessageNode,
"descendants": [MessageNodeTree]
}
}
```
### MessageNode (дополнение)
- `MessageNode` расширяет формат сообщения из `GetChannelMessages` и дополнительно содержит:
- `channelInfo` — мета-информация о канале (если применимо);
- `rawBlockB64` — сырой `block_bytes` текущего блока в Base64.
- Поле `rawBlockB64` присутствует у узлов во всех частях ответа `GetMessageThread`: `focus`, `ancestors[]`, `descendants[]`.
- В `GetChannelMessages` поле `rawBlockB64` **не добавляется** (лента канала без сырого блока, чтобы не раздувать ответ).
---
## 4) GetChannelsCounters
### Request
```json
{
"op": "GetChannelsCounters",
"requestId": "req-4",
"payload": {
"login": "Alice"
}
}
```
### Response (success)
```json
{
"op": "GetChannelsCounters",
"requestId": "req-4",
"status": 200,
"ok": true,
"payload": {
"login": "Alice",
"feedCount": 12,
"dialogs100Count": 3,
"groupChats200Count": 4,
"myChannelsCount": 2
}
}
```
---
## 5) ListGroupChats200
### Request
```json
{
"op": "ListGroupChats200",
"requestId": "req-5",
"payload": {
"login": "Alice"
}
}
```
### Response (success)
```json
{
"op": "ListGroupChats200",
"requestId": "req-5",
"status": 200,
"ok": true,
"payload": {
"login": "Alice",
"chats": [
{
"ownerLogin": "Alice",
"ownerBlockchainName": "alice-001",
"channelRootBlockNumber": 123,
"channelRootBlockHash": "...",
"channelName": "team",
"chatTitle": "Team chat",
"membersCount": 3,
"updatedAtMs": 1760000000000
}
]
}
}
```
---
## 6) GetGroupDialog
### Request
```json
{
"op": "GetGroupDialog",
"requestId": "req-6",
"payload": {
"login": "Alice",
"group": {
"ownerBlockchainName": "alice-001",
"channelRootBlockNumber": 123
}
}
}
```
### Response (success)
```json
{
"op": "GetGroupDialog",
"requestId": "req-6",
"status": 200,
"ok": true,
"payload": {
"group": {
"ownerLogin": "Alice",
"ownerBlockchainName": "alice-001",
"channelRootBlockNumber": 123,
"channelName": "team",
"chatTitle": "Team chat"
},
"messages": [
{
"authorLogin": "Bob",
"authorBlockchainName": "bob-001",
"blockNumber": 140,
"blockHash": "...",
"createdAtMs": 1760000000000,
"text": "Привет"
}
]
}
}
```
---
## Reason codes
- `bad_fields`
- `user_not_found`
- `channel_not_found`
- `message_not_found`
- `limit_too_large`
- `channel_name_already_exists`
- `internal_error`

View File

@ -0,0 +1,145 @@
# 07. Channels Feature Runbook (человеческое описание + диагностика)
## 1) Что уже сделано простыми словами
Сейчас реализован полный минимальный контур для каналов:
1. **Серверные read API**:
- `ListSubscriptionsFeed` — экран списка каналов.
- `GetChannelMessages` — сообщения конкретного канала.
- `GetMessageThread` — дерево обсуждения для сообщения.
2. **UI вкладки Каналы**:
- при открытии пытается загрузить реальный feed с сервера;
- если сервер недоступен — fallback на мок-данные;
- группы каналов выводятся в нужном порядке;
- есть кнопка «Добавить канал», модалки подписки, переход в канал.
3. **Проверка уникальности имени канала на сервере**
- в `AddBlock` при `CreateChannelBody` добавлена проверка;
- при дубле возвращается `409 channel_name_already_exists`.
---
## 2) Что тестировать в первую очередь (быстрый чеклист)
### Базовый smoke
1. Авторизоваться в UI.
2. Открыть вкладку «Каналы».
3. Убедиться, что данные загрузились с сервера (или виден fallback-баннер).
4. Нажать любой канал — должен открыться экран канала с сообщениями.
### API smoke
1. Вызвать `ListSubscriptionsFeed`.
2. Для канала `ownedChannels[0]` вызвать `GetChannelMessages`.
3. Для первого `messages[0]` вызвать `GetMessageThread`.
### Ошибки
1. `ListSubscriptionsFeed` с пустым login -> `bad_fields`.
2. `GetChannelMessages` с битым channel payload -> `bad_fields`.
3. `GetMessageThread` с несуществующим block -> `message_not_found`.
4. `AddBlock(CreateChannel)` с уже существующим именем -> `channel_name_already_exists`.
---
## 3) Готовые JSON-запросы для ручной диагностики
## 3.1 ListSubscriptionsFeed
```json
{
"op": "ListSubscriptionsFeed",
"requestId": "debug-feed-1",
"payload": {
"login": "TestUser1",
"limit": 200
}
}
```
## 3.2 GetChannelMessages
```json
{
"op": "GetChannelMessages",
"requestId": "debug-ch-1",
"payload": {
"channel": {
"ownerBlockchainName": "TestUser1-001",
"channelRootBlockNumber": 0,
"channelRootBlockHash": ""
},
"limit": 200,
"sort": "asc"
}
}
```
## 3.3 GetMessageThread
```json
{
"op": "GetMessageThread",
"requestId": "debug-thread-1",
"payload": {
"message": {
"blockchainName": "TestUser1-001",
"blockNumber": 123,
"blockHash": "<hash-from-GetChannelMessages>"
},
"depthUp": 20,
"depthDown": 2,
"limitChildrenPerNode": 50
}
}
```
---
## 4) Что смотреть в ответах
### ListSubscriptionsFeed
- `payload.login` — канонический login.
- `ownedChannels / followedUsersChannels / followedChannels` — массивы.
- у каждой записи есть:
- `channel.channelRoot.blockNumber`,
- `messagesCount`,
- `lastMessage` (может быть null, если сообщений нет).
### GetChannelMessages
- `payload.channel` заполнен;
- `payload.messages[]` содержит:
- `likesCount`, `repliesCount`,
- `versionsTotal`, `versions[]`,
- `text` должен быть текущей (последней) версией.
### GetMessageThread
- `payload.ancestors[]`, `payload.focus`, `payload.descendants[]`.
- у узлов должны быть версии и счетчики.
- у каждого узла дополнительно может приходить `rawBlockB64` (Base64 сырого `block_bytes`).
### Важно по совместимости
- `rawBlockB64` добавлен только в `GetMessageThread`.
- `GetChannelMessages` не содержит `rawBlockB64` (без изменений формата ленты).
---
## 5) Частые проблемы и как быстро локализовать
1. **`status != 200`, code=bad_fields**
- проверить вложенность payload и обязательные поля.
2. **`message_not_found` в GetMessageThread**
- обычно передали blockNumber/hash не из `messageRef`.
3. **Пустой список сообщений в GetChannelMessages**
- проверить `ownerBlockchainName` и `channelRootBlockNumber`.
4. **`channel_name_already_exists` при AddBlock**
- это ожидаемо: в этой цепочке уже есть канал с таким именем.
---
## 6) Для будущей доработки
1. Добавить курсоры (пагинацию) для больших каналов.
2. Перевести «Подписаться»/«Добавить канал» в UI с демо-заглушек на реальные write RPC.
3. Добавить batch-агрегации для thread/versions (оптимизация).
4. Добавить полноценные интеграционные тесты на негативные кейсы и нагрузку.

View File

@ -0,0 +1,162 @@
# MCP: чтение и дозапись персонального публичного чата (type=100)
Документ для реализации MCP-инструмента, который:
- читает переписку между двумя логинами (`from`, `to`);
- добавляет новое сообщение от отправителя через серверный `AddBlock`.
Важно: речь про **персональные публичные** каналы (`channelTypeCode=100`), а не приватные DM.
## 1. Базовые предпосылки
1. У каждого пользователя свой блокчейн (`<login>-001`).
2. Персональный публичный чат хранится как канал типа `100`:
- у `A` канал с `channelName = B`;
- у `B` зеркальный канал с `channelName = A`.
3. Сообщения канала — `TEXT_POST` и `TEXT_REPOST` в линии `line_code = rootBlockNumber` канала.
4. Запись блока возможна только при валидной подписи blockchain-ключом владельца цепочки.
## 2. Что должен уметь MCP-инструмент
Минимальный набор операций:
1. `read_personal_public_dialog(fromLogin, toLogin, limitPerSide=200)`
2. `append_personal_public_message(fromLogin, toLogin, text)`
## 3. Алгоритм чтения переписки
### 3.1 Найти оба канала (прямой и зеркальный)
Для `fromLogin = A`, `toLogin = B`:
1. Запросить `ListSubscriptionsFeed` для `A` и найти owned-канал:
- `ownerLogin == A`
- `channelName == B`
- `channelTypeCode == 100`
2. Запросить `ListSubscriptionsFeed` для `B` и найти owned-канал:
- `ownerLogin == B`
- `channelName == A`
- `channelTypeCode == 100`
Если какой-то из каналов не найден — вернуть частичный результат + флаг отсутствия зеркала.
### 3.2 Вычитать сообщения из каналов
Для каждого найденного канала вызвать `GetChannelMessages`:
```json
{
"op": "GetChannelMessages",
"payload": {
"login": "<текущий-login-сессии>",
"channel": {
"ownerBlockchainName": "...",
"channelRootBlockNumber": 123,
"channelRootBlockHash": "..."
},
"limit": 200,
"sort": "asc"
}
}
```
### 3.3 Склеить в единый диалог
1. Объединить массивы сообщений из `A->B` и `B->A`.
2. Отсортировать по `createdAtMs`, при равенстве — по `messageRef.blockNumber`.
3. Вернуть структуру:
- `messages[]`
- `directChannelFound` / `reverseChannelFound`
- метаданные обоих каналов.
## 4. Алгоритм дозаписи сообщения
Цель: добавить сообщение **от имени `fromLogin`** в его канал `fromLogin -> toLogin`.
### 4.1 Найти канал отправителя
Через `ListSubscriptionsFeed(fromLogin)` найти owned-канал:
- `channelName == toLogin`
- `channelTypeCode == 100`
Если канал не найден — вернуть ошибку `channel_not_found`.
### 4.2 Отправить `AddBlock` с `TEXT_POST`
Использовать клиентский/серверный helper формирования `TEXT_POST` body:
- `lineCode = channelRootBlockNumber`;
- `prevLineNumber/prevLineHash` берутся из последнего сообщения линии;
- подпись — blockchain private key пользователя `fromLogin`.
Если у вас в MCP нет приватного ключа пользователя, дозапись невозможна.
## 5. Контракт MCP (рекомендуемый)
## `read_personal_public_dialog`
Вход:
```json
{
"fromLogin": "alice",
"toLogin": "bob",
"limitPerSide": 200
}
```
Выход:
```json
{
"ok": true,
"directChannelFound": true,
"reverseChannelFound": true,
"messages": [
{
"authorLogin": "alice",
"text": "Привет",
"createdAtMs": 1760000000000,
"channelSide": "alice->bob",
"messageRef": { "blockNumber": 11, "blockHash": "..." }
}
]
}
```
## `append_personal_public_message`
Вход:
```json
{
"fromLogin": "alice",
"toLogin": "bob",
"text": "Тест"
}
```
Выход:
```json
{
"ok": true,
"serverLastGlobalNumber": 321,
"serverLastGlobalHash": "..."
}
```
## 6. Ограничения и безопасность
1. Персональный канал типа `100` сейчас публичный по модели чтения (не E2E DM).
2. Нельзя дозаписать блок в чужой блокчейн без приватного ключа владельца (проверка подписи сервером).
3. Для прод-инструмента нужно:
- строгая авторизация MCP-вызовов;
- аудит, кто и от чьего имени запрашивал чтение/запись;
- лимиты/квоты на запись.
## 7. Мини-чеклист для реализации MCP
1. Реализовать helper поиска канала `findOwnedPersonalChannel(ownerLogin, peerLogin)`.
2. Реализовать чтение двух сторон и merge/sort.
3. Реализовать отправку `TEXT_POST` в найденный канал отправителя.
4. Добавить понятные ошибки:
- `user_not_found`
- `channel_not_found`
- `reverse_channel_not_found`
- `signature_required`
- `add_block_failed`

View File

@ -0,0 +1,73 @@
# API для разработчиков: индекс операций
Этот файл фиксирует полный список публичных JSON/WebSocket операций, зарегистрированных в коде сервера.
Источник истины на момент актуализации:
- `shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java`.
Если операция есть в `HANDLERS` и `REQUEST_TYPES`, клиент может отправлять её как `op` в общем JSON-конверте из `00_Common_API_Format.md`.
## Актуальные операции
| Операция | Раздел документации | Кратко |
| --- | --- | --- |
| `AddUser` | `01_User_Registration_API.md` | отключено (`410 / ADD_USER_DISABLED`) |
| `GetUser` | `01_User_Registration_API.md` | чтение/проверка пользователя + server-состояние его блокчейна |
| `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу |
| `TestGetFreeAvatarQuota` | `14_Test_Free_Avatar_Upload_API.md` | временный тестовый просмотр остатка бесплатных загрузок аватара |
| `TestUploadFreeAvatar` | `14_Test_Free_Avatar_Upload_API.md` | временная тестовая бесплатная загрузка маленького аватара в Arweave |
| `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии |
| `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` | добавление блока в блокчейн |
| `GetBlockchainBlock` | `04_Add_Block_to_Blockchain_API.md` | чтение одного блока блокчейна |
| `Ping` | `05_Technical_Requests_API.md` | keep-alive |
| `GetServerInfo` | `05_Technical_Requests_API.md` | публичная информация о сервере |
| `ListBlockchainHeads` | `05_Technical_Requests_API.md` | список heads всех локальных блокчейнов |
| `GetSyncUserProfile` | `05_Technical_Requests_API.md` | межсерверный профиль пользователя для синхронизации |
| `SendSignal` | `05_Technical_Requests_API.md` | общий межсессионный технический сигнал в одну или все сессии пользователя |
| `GetCallIceConfig` | `05_Technical_Requests_API.md` | STUN/TURN конфигурация звонков |
| `ClientErrorLog` | `05_Technical_Requests_API.md` | логирование клиентской ошибки |
| `ClientDebugLog` | `05_Technical_Requests_API.md` | клиентский debug-лог |
| `CallDeliveryReport` | `05_Technical_Requests_API.md` | диагностика доставки/установки звонков |
| `ListSubscriptionsFeed` | `06_Channels_Read_API.md` | лента каналов/подписок |
| `GetChannelMessages` | `06_Channels_Read_API.md` | сообщения канала |
| `GetMessageThread` | `06_Channels_Read_API.md` | тред сообщения |
| `GetChannelsCounters` | `06_Channels_Read_API.md` | счетчики разделов каналов |
| `ListGroupChats200` | `06_Channels_Read_API.md` | список групповых чатов типа `200` |
| `GetGroupDialog` | `06_Channels_Read_API.md` | сообщения группового чата типа `200` |
| `UpsertUserParam` | `10_User_Params_API.md` | запись параметра пользователя |
| `GetUserParam` | `10_User_Params_API.md` | чтение одного параметра пользователя |
| `ListUserParams` | `10_User_Params_API.md` | список параметров пользователя |
| `GetFriendsLists` | `11_Connections_API.md` | входящие/исходящие друзья |
| `ListContacts` | `11_Connections_API.md` | контакты текущего пользователя |
| `GetUserConnectionsGraph` | `11_Connections_API.md` | граф связей пользователя |
| `AddCloseFriend` | `11_Connections_API.md` | добавить близкого друга |
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
| `SendDirectMessage` | `12_Direct_Messages_Push_Calls_API.md` | отправка подписанного DM-пакета |
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | алиас `SendMessagePair` |
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
| `AckSessionDelivery` | `12_Direct_Messages_Push_Calls_API.md` | подтверждение доставки в сессию |
| `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку |
| `CallSignalToSession` | `12_Direct_Messages_Push_Calls_API.md` | сигнал звонка в конкретную сессию |
## Важные замечания
- `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

@ -0,0 +1,129 @@
# API для разработчиков: параметры пользователя
Документ описывает операции для записи и чтения пользовательских параметров.
Текущие операции:
- `UpsertUserParam`
- `GetUserParam`
- `ListUserParams`
## 1. `UpsertUserParam`
### Запрос
```json
{
"op": "UpsertUserParam",
"requestId": "param-upsert-001",
"payload": {
"login": "alice",
"param": "display_name",
"time_ms": 1774700000123,
"value": "Alice",
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
"signature": "BASE64_SIGNATURE"
}
}
```
### Успешный ответ
```json
{
"op": "UpsertUserParam",
"requestId": "param-upsert-001",
"status": 200,
"ok": true,
"payload": {
}
}
```
### Типовые ошибки
- `400 / BAD_FIELDS` — некорректные обязательные поля.
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
- `501 / DB_ERROR` — ошибка БД.
---
## 2. `GetUserParam`
### Запрос
```json
{
"op": "GetUserParam",
"requestId": "param-get-001",
"payload": {
"login": "alice",
"param": "display_name"
}
}
```
### Успешный ответ
```json
{
"op": "GetUserParam",
"requestId": "param-get-001",
"status": 200,
"ok": true,
"payload": {
"login": "alice",
"param": "display_name",
"time_ms": 1774700000123,
"value": "Alice",
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
"signature": "BASE64_SIGNATURE"
}
}
```
Если параметр не найден, сервер возвращает `404` с пустым `payload`; отдельный прикладной код ошибки текущий handler не задаёт.
---
## 3. `ListUserParams`
### Запрос
```json
{
"op": "ListUserParams",
"requestId": "param-list-001",
"payload": {
"login": "alice"
}
}
```
### Успешный ответ
```json
{
"op": "ListUserParams",
"requestId": "param-list-001",
"status": 200,
"ok": true,
"payload": {
"login": "alice",
"params": [
{
"login": "alice",
"param": "display_name",
"time_ms": 1774700000123,
"value": "Alice",
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
"signature": "BASE64_SIGNATURE"
}
]
}
}
```
## Примечание
Имена JSON-полей `time_ms` и `client_key` сейчас соответствуют Java-модели ответа/запроса и должны передаваться именно в таком виде.

View File

@ -0,0 +1,174 @@
# API для разработчиков: связи пользователей
Документ описывает операции чтения и записи пользовательских связей.
Текущие операции:
- `GetFriendsLists`
- `ListContacts`
- `GetUserConnectionsGraph`
- `AddCloseFriend`
## 1. `GetFriendsLists`
### Запрос
```json
{
"op": "GetFriendsLists",
"requestId": "friends-001",
"payload": {
"login": "alice"
}
}
```
### Успешный ответ
```json
{
"op": "GetFriendsLists",
"requestId": "friends-001",
"status": 200,
"ok": true,
"payload": {
"login": "Alice",
"out_friends": ["Bob"],
"in_friends": ["Kate"]
}
}
```
---
## 2. `ListContacts`
`ListContacts` использует текущую авторизованную сессию. В payload нет дополнительных полей.
### Запрос
```json
{
"op": "ListContacts",
"requestId": "contacts-001",
"payload": {
}
}
```
### Успешный ответ
```json
{
"op": "ListContacts",
"requestId": "contacts-001",
"status": 200,
"ok": true,
"payload": {
"login": "Alice",
"contacts": ["Bob", "Kate"]
}
}
```
---
## 3. `GetUserConnectionsGraph`
### Запрос
```json
{
"op": "GetUserConnectionsGraph",
"requestId": "graph-001",
"payload": {
"login": "alice"
}
}
```
### Успешный ответ
```json
{
"op": "GetUserConnectionsGraph",
"requestId": "graph-001",
"status": 200,
"ok": true,
"payload": {
"login": "Alice",
"outFriends": ["Bob"],
"inFriends": ["Kate"],
"outContacts": [],
"inContacts": [],
"outFollows": [],
"inFollows": [],
"outSpouses": [],
"inSpouses": [],
"outParents": [],
"inParents": [],
"outChildren": [],
"inChildren": [],
"outSiblings": [],
"inSiblings": [],
"outKnownPersons": [],
"inKnownPersons": [],
"outShineConfirmed": [],
"inShineConfirmed": [],
"outShineSeen": [],
"inShineSeen": [],
"parents": [],
"children": [],
"siblings": [],
"spouses": [],
"allUsers": [
{
"login": "Bob",
"official": false,
"shine": true,
"officialLabel": "",
"shineLabel": "shine",
"avatar": { "ar": "..." }
}
]
}
}
```
### Примечание
Поля `known_person`, `shine_confirmed`, `shine_seen` в UI считаются недопроверенной зоной проекта; при изменениях этой логики нужна ручная end-to-end проверка.
---
## 4. `AddCloseFriend`
`AddCloseFriend` использует текущую авторизованную сессию как источник `login`.
### Запрос
```json
{
"op": "AddCloseFriend",
"requestId": "close-friend-001",
"payload": {
"toLogin": "bob"
}
}
```
### Успешный ответ
```json
{
"op": "AddCloseFriend",
"requestId": "close-friend-001",
"status": 200,
"ok": true,
"payload": {
"login": "Alice",
"toLogin": "Bob",
"relation": "close_friend"
}
}
```

View File

@ -0,0 +1,190 @@
# API для разработчиков: DM, push и сигналы звонков
Документ описывает публичные операции, связанные с личными сообщениями, WebPush и сигналами звонков.
Подробная логика DM и бинарного формата:
- `Dev_Docs/Personal_Messages/README.md`
## 1. `UpsertPushToken`
Требует авторизации.
### Запрос
```json
{
"op": "UpsertPushToken",
"requestId": "push-upsert-001",
"payload": {
"sessionId": "SESSION_ID",
"endpoint": "https://push.example/...",
"p256dhKey": "BASE64",
"authKey": "BASE64",
"platform": "web",
"userAgent": "Mozilla/5.0 ..."
}
}
```
### Успешный ответ
```json
{
"op": "UpsertPushToken",
"requestId": "push-upsert-001",
"status": 200,
"ok": true,
"payload": {
"tokenId": "token-1",
"updatedAtMs": 1774700000123
}
}
```
## 2. `SendTestWebPush`
Требует авторизации.
### Запрос
```json
{
"op": "SendTestWebPush",
"requestId": "push-test-001",
"payload": {
"login": "alice",
"sessionId": "SESSION_ID",
"title": "Test",
"text": "Push body"
}
}
```
## 3. `SendMessagePair` и `ReceiveOutcomingMessage`
`ReceiveOutcomingMessage` — алиас `SendMessagePair`.
### Назначение
Передаёт пару signed DM-блоков:
- `incomingBlobB64` — блок `type=1` или `type=3`
- `outgoingBlobB64` — блок `type=2` или `type=4`
Для контентных сообщений `type=1/2` внутри base64 лежит бинарный формат `SHiNE_DM`.
### Запрос
```json
{
"op": "SendMessagePair",
"requestId": "dm-pair-001",
"payload": {
"incomingBlobB64": "BASE64_INCOMING_SIGNED_BLOCK",
"outgoingBlobB64": "BASE64_OUTGOING_SIGNED_BLOCK"
}
}
```
### Успешный ответ
```json
{
"op": "SendMessagePair",
"requestId": "dm-pair-001",
"status": 200,
"ok": true,
"payload": {
"baseKey": "from|to|time|nonce",
"incomingKey": "from|to|time|nonce|1",
"outgoingKey": "from|to|time|nonce|2",
"deliveredWsSessions": 1,
"deliveredWebPushSessions": 0
}
}
```
### Ошибки
- `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` — подпись блока не прошла проверку
## 4. `ReceiveIncomingMessage`
Принимает только один входящий signed DM-блок.
### Назначение
Используется там, где нужно принять только incoming-вариант сообщения.
### Запрос
```json
{
"op": "ReceiveIncomingMessage",
"requestId": "dm-in-001",
"payload": {
"incomingBlobB64": "BASE64_INCOMING_SIGNED_BLOCK"
}
}
```
## 5. `AckSessionDelivery`
Требует авторизации. Подтверждает доставку в текущую сессию.
### Запрос
```json
{
"op": "AckSessionDelivery",
"requestId": "ack-001",
"payload": {
"messageKey": "from|to|time|nonce|1"
}
}
```
## 6. Событие `SignedMessageArrived`
Сервер присылает его по WebSocket в активные сессии адресата.
### Payload события
```json
{
"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`.
## 8. `CallSignalToSession`
Требует авторизации. Шлёт сигнал звонка в конкретную сессию.
## 9. Замечания
- read-receipt `type=3/4` пока остаются в legacy-формате `SHiNE_dm2`
- контентные DM `type=1/2` используют `SHiNE_DM`
- сервер хранит только последнюю версию контентного сообщения по `messageKey`
- удаление сообщения реализуется новой ревизией с пустым телом и `attachmentsCount = 0`
- HTTP endpoints для DM-файлов сейчас отсутствуют

View File

@ -0,0 +1,190 @@
# API для разработчиков: HTTP debug endpoints
Этот файл описывает отдельный HTTP debug API сервера. Он не использует WebSocket-конверт `op/requestId/payload` и включается только настройкой:
- `debug.tempApi.enabled=true`
Источник истины:
- `src/main/java/server/debug/DebugApiConfigurator.java`
- `src/main/java/server/debug/*Servlet.java`
Если `.debug-token` отсутствует или пуст, endpoints возвращают `503 / DEBUG_DISABLED`.
## Авторизация
Для большинства debug endpoints используется Bearer token из `.debug-token`:
```http
Authorization: Bearer <token>
```
Для `POST /debug/ws/ui-reload-all` поддерживается заголовок:
```http
X-Debug-Token: <token>
```
При неверном токене сервер возвращает `401 / UNAUTHORIZED`.
## Формат успешного HTTP-ответа
```json
{
"ok": true,
"payload": {
}
}
```
## Формат HTTP-ошибки
```json
{
"ok": false,
"code": "BAD_JSON",
"message": "Тело запроса должно быть JSON"
}
```
## 1. `GET /debug/ws/clients`
Возвращает список активных WebSocket-клиентов.
### Успешный ответ
```json
{
"ok": true,
"payload": {
"count": 1,
"clients": [
{
"sessionId": "SESSION_ID",
"login": "alice",
"authStatus": 2,
"wsOpen": true,
"remoteAddress": "127.0.0.1",
"ip": "127.0.0.1",
"userAgent": "Mozilla/5.0 ...",
"clientInfoFromClient": "...",
"clientInfoFromRequest": "...",
"userLanguage": "ru",
"sessionCreatedAtMs": 1774700000123
}
]
}
}
```
## 2. `POST /debug/ws/connect`
Запускает debug-сценарий соединения двух активных WS-сессий и отправляет им события:
- `DebugConnectPrepareResponder`
- `DebugConnectStartInitiator`
### Запрос
```json
{
"initiatorSessionId": "SESSION_ID_1",
"responderSessionId": "SESSION_ID_2",
"clearDebugLog": true
}
```
### Успешный ответ
```json
{
"ok": true,
"payload": {
"runId": "dbg-...",
"callId": "debug-call-...",
"accepted": true,
"initiatorSessionId": "SESSION_ID_1",
"responderSessionId": "SESSION_ID_2",
"initiatorLogin": "alice",
"responderLogin": "bob",
"mode": "cross-login"
}
}
```
### Ошибки
- `400 / BAD_JSON` — тело запроса не JSON.
- `400 / BAD_FIELDS` — не заполнены sessionId или переданы одинаковые sessionId.
- `404 / INITIATOR_NOT_FOUND` — сессия инициатора не найдена или неактивна.
- `404 / RESPONDER_NOT_FOUND` — сессия получателя не найдена или неактивна.
## 3. `GET /debug/ws/logs`
Возвращает tail debug-логов из `DebugRunLogBuffer`.
### Query-параметры
- `limit` — количество записей.
- `runId` — фильтр по runId.
### Успешный ответ
```json
{
"ok": true,
"payload": {
"count": 1,
"limit": 100,
"runIdFilter": "ui-run-1",
"logs": [
{
"ts": 1774700000123,
"level": "info",
"runId": "ui-run-1",
"source": "debug-connect",
"sessionId": "SESSION_ID",
"login": "alice",
"message": "opened channels tab",
"details": "{}"
}
]
}
}
```
## 4. `POST /debug/ws/ui-reload-all`
Рассылает активным UI-сессиям debug-событие на reload.
### Запрос
```json
{
"reason": "manual_debug_api",
"reloadAfterMs": 700
}
```
Если `reason` не передан или пустой, сервер использует `manual_debug_api`. `reloadAfterMs` ограничивается диапазоном `100..15000`.
### Успешный ответ
```json
{
"ok": true,
"payload": {
"accepted": true,
"reason": "manual_debug_api",
"reloadAfterMs": 700,
"issuedAtMs": 1774700000123,
"totalConnections": 2,
"sentCount": 2,
"skippedCount": 0
}
}
```
### Ошибки
- `400 / BAD_JSON` — тело запроса не JSON.

View File

@ -0,0 +1,176 @@
# Временное Test API для бесплатной загрузки аватаров в Arweave
> Статус: **временное тестовое решение**.
> Все операции из этого файла начинаются с `Test...`, чтобы это было видно сразу и в коде, и в UI.
## Назначение
Этот временный API даёт пользователю ограниченную бесплатную загрузку маленьких аватаров в Arweave:
- загрузка идёт через **серверный Arweave-кошелёк**;
- лимит на пользователя: по умолчанию `3` загрузки за всё время;
- лимит хранится в SQLite-таблице `test_free_avatar_uploads`;
- если лимит исчерпан, сервер возвращает понятную ошибку;
- загружать можно только маленький итоговый файл аватара, по умолчанию до `128 KB`.
## Настройки сервера
В `application.properties`:
```properties
test.freeAvatar.enabled=true
test.freeAvatar.gateway=https://arweave.net
test.freeAvatar.limitPerUser=3
test.freeAvatar.maxBytes=131072
test.freeAvatar.walletAddress=
test.freeAvatar.walletJwkPath=
```
Пояснения:
- `test.freeAvatar.enabled` - включить или выключить временный API;
- `test.freeAvatar.gateway` - Arweave gateway для `price/tx/wallet`;
- `test.freeAvatar.limitPerUser` - пожизненный бесплатный лимит на пользователя;
- `test.freeAvatar.maxBytes` - максимальный размер итогового файла;
- `test.freeAvatar.walletAddress` - публичный адрес серверного Arweave-кошелька;
- `test.freeAvatar.walletJwkPath` - путь к приватному JWK-файлу серверного кошелька.
Важно:
- приватный JWK хранится вне кода;
- если `walletAddress` указан и не совпадает с адресом, вычисленным из JWK, сервер вернёт ошибку настройки.
## `TestGetFreeAvatarQuota`
Возвращает остаток бесплатных загрузок для текущего авторизованного пользователя.
### Запрос
```json
{
"op": "TestGetFreeAvatarQuota",
"requestId": "req-test-avatar-quota-1",
"payload": {}
}
```
### Успешный ответ
```json
{
"op": "TestGetFreeAvatarQuota",
"requestId": "req-test-avatar-quota-1",
"status": 200,
"ok": true,
"payload": {
"enabled": true,
"limit": 3,
"usedCount": 1,
"remainingCount": 2,
"maxBytes": 131072
}
}
```
### Поля ответа
- `enabled` - временный API сейчас включён на сервере или нет;
- `limit` - полный лимит бесплатных загрузок;
- `usedCount` - сколько уже израсходовано;
- `remainingCount` - сколько ещё осталось;
- `maxBytes` - максимальный размер итогового файла.
### Ошибки
- `422 NOT_AUTHENTICATED` - требуется авторизация.
## `TestUploadFreeAvatar`
Временная бесплатная загрузка маленькой аватарки в Arweave через серверный кошелёк.
### Правила
- операция требует авторизованную сессию;
- сервер использует текущий login из сессии;
- сервер принимает только:
- `image/jpeg`
- `image/png`
- `image/webp`
- размер итогового файла должен быть не больше `maxBytes` из квоты;
- если пользователь уже сделал `limit` бесплатных загрузок, операция запрещена.
### Запрос
`fileBytesBase64` - это обычный Base64 байт итогового подготовленного файла.
```json
{
"op": "TestUploadFreeAvatar",
"requestId": "req-test-avatar-upload-1",
"payload": {
"contentType": "image/webp",
"fileBytesBase64": "UklGRiQAAABXRUJQVlA4WAoAAAAQAAAA...",
"sha256Hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
}
```
### Успешный ответ
```json
{
"op": "TestUploadFreeAvatar",
"requestId": "req-test-avatar-upload-1",
"status": 200,
"ok": true,
"payload": {
"txId": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"sha256Hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"usedCount": 2,
"remainingCount": 1,
"limit": 3,
"gateway": "https://arweave.net"
}
}
```
### Поля ответа
- `txId` - Arweave Transaction ID загруженного файла;
- `sha256Hex` - SHA-256 загруженного файла;
- `usedCount` - сколько бесплатных загрузок уже израсходовано после этой операции;
- `remainingCount` - сколько бесплатных загрузок осталось;
- `limit` - общий лимит;
- `gateway` - gateway, через который сервер отправлял транзакцию.
### Ошибки
- `422 NOT_AUTHENTICATED` - требуется авторизация;
- `400 BAD_FIELDS` - не переданы `contentType` или `fileBytesBase64`;
- `400 BAD_BASE64` - `fileBytesBase64` не декодируется;
- `400 BAD_AVATAR_FILE` - файл не проходит ограничения сервера;
- `400 FREE_AVATAR_LIMIT_EXHAUSTED` - бесплатный лимит аватарок исчерпан;
- `501 FREE_AVATAR_TEMP_DISABLED` - временная функция выключена или сервер не настроен;
- `500 INTERNAL_ERROR` - внутренняя ошибка сервера.
## Как это используется в UI
На экране редактирования профиля в мастере смены аватара есть временный сценарий:
- `Залить аватар бесплатно`
UI:
1. вызывает `TestGetFreeAvatarQuota`;
2. показывает остаток лимита;
3. локально подготавливает уменьшенный файл аватара;
4. проверяет, что итоговый файл не превышает `maxBytes`;
5. вызывает `TestUploadFreeAvatar`;
6. после получения `txId` обычным путём записывает `avatar.ar` в профиль через `AddBlock`.
## Почему решение временное
- используется общий серверный Arweave-кошелёк;
- лимит хранится отдельной технической таблицей;
- операции имеют префикс `Test...`;
- сценарий нужен как переходный бесплатный путь для маленьких аватаров.

View File

@ -0,0 +1,25 @@
# Форматы блокчейнов и блоков (индекс раздела)
Этот раздел разбит на несколько файлов, чтобы формат добавляемых блоков было проще читать и сопровождать.
## Структура раздела
- `01_Common_Block_Format.md` — общий бинарный формат блока (Frame v0), правила подписи и проверки.
- `02_Blockchain_Kinds_and_Lines.md` — виды блокчейнов и логические линии внутри цепочки.
- `10_TECH_Blocks.md` — системные блоки (`type=0`).
- `11_TEXT_Blocks.md` — текстовые блоки (`type=1`).
- `12_REACTION_Blocks.md` — реакции (`type=2`).
- `13_CONNECTION_Blocks.md` — связи/подписки (`type=3`).
- `14_USER_PARAM_Blocks.md` — пользовательские параметры (`type=4`).
## Быстрая карта типов
- `type=0` — TECH: HEADER, CREATE_CHANNEL.
- `type=1` — TEXT: POST/EDIT_POST/REPLY/EDIT_REPLY/REPOST.
- `type=2` — REACTION: LIKE/UNLIKE.
- `type=3` — CONNECTION: FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING и обратные операции.
- `type=4` — USER_PARAM: key/value-параметры пользователя.
## Примечание
Если нужно добавить новый тип или подтип блока, сначала обновляйте профильный файл этого раздела, затем API-документацию в `Dev_Docs/API`.

View File

@ -0,0 +1,40 @@
# Типы каналов и CreateChannel
## 1. Формат `CreateChannelBody` (`msg_type=0`, `subType=1`, `version=1`)
Payload включает:
1. line-поля канала (`lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`);
2. `channelName`;
3. `channelDescription`;
4. `channelType` (`uint16`, 2 байта);
5. `channelTypeVersion` (`uint16`, 2 байта).
## 2. Типы каналов
- `0``stories` (root-канал пользователя).
- `1` — публичный канал.
- `100` — персональный канал.
- `200` — групповой/чатовый канал.
Версия типа (`channelTypeVersion`) сейчас используется со значением `1`.
Важно для MVP:
- `100` и `200` в формате поддерживаются, но в текущем UI не используются.
- В MVP рабочий UI-флоу — каналы `0` и `1`.
## 3. Имя root-канала
- Root-канал (`line_code = 0`) в API/чтении отображается как `stories`.
- Публикации в `stories` разрешены владельцу собственного блокчейна.
## 4. Уникальность имени канала
Проверка уникальности выполняется на сервере по ключу:
`owner_bch_name + channel_type_code + slug(channel_name)`
Это означает:
- одно и то же имя допустимо у одного владельца для разных типов (`1`, `100`, `200`);
- в рамках одного типа и одного владельца имя уникально.
## 5. Персональные каналы (`type=100`)
- Сервер не проверяет бизнес-валидность собеседника по имени канала.
- Проверка существования login для персонального канала выполняется на UI при создании.
- При чтении сервер пытается собрать парный поток `A->B` + `B->A` (если обратный канал существует).

View File

@ -0,0 +1,49 @@
# Общий формат добавляемого блока (Frame v0)
Этот файл описывает **единый бинарный формат** блока, который клиент отправляет через `AddBlock` в поле `blockBytesB64`.
## 1. Полная структура блока
Блок состоит из двух частей:
1. **PREIMAGE** (подписывается)
2. **TAIL** (маркер подписи + подпись)
### PREIMAGE
- `frameCode (uint16)`
- `prevHash32 (32 bytes)`
- `blockSize (int32)` — размер PREIMAGE
- `blockNumber (int32)`
- `timestamp (int64)`
- `type (uint16)`
- `subType (uint16)`
- `version (uint16)`
- `bodyBytes (N)`
### TAIL
- `sigMarker (uint16)`
- `signature64 (64 bytes, Ed25519)`
## 2. Что проверяет сервер при AddBlock
- `frameCode` должен быть `0x0000`.
- `sigMarker` должен быть `0x0100`.
- `blockNumber` должен идти строго по порядку (`last + 1`).
- `prevHash32` должен совпасть с вершиной цепочки на сервере.
- `body` должен пройти `check()` для конкретного типа.
- подпись должна валидироваться публичным ключом блокчейна.
## 3. Ограничения
- максимальный полный размер блока: до 4 MiB;
- timestamp не должен сильно уходить в будущее;
- `bodyBytes` парсится по `type/subType/version` из заголовка блока.
## 4. Почему это важно
Одинаковый общий формат позволяет:
- передавать разные виды записей через один RPC `AddBlock`;
- валидировать блоки единообразно;
- расширять типы `body`, не ломая каркас блока.

View File

@ -0,0 +1,47 @@
# Виды блокчейнов и логических линий
## 1. Именованный блокчейн
Базовый идентификатор цепочки пользователя:
- `blockchainName = <login>-<NNN>`
- пример: `alice-001`
Обычно это одна основная цепочка пользователя.
## 2. Логические линии внутри одной цепочки
Физически цепочка одна, но внутри есть независимые логические последовательности (линии), которые ведутся через поля:
- `lineCode`
- `prevLineNumber`
- `prevLineHash32`
- `thisLineNumber`
Линии используются для:
- TECH-событий;
- каналов с текстовыми постами;
- связей и подписок;
- пользовательских параметров.
## 3. Правила line-полей (фактическая серверная валидация)
Line-поля: `lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`.
- Line-поля разрешены только для `msg_type`: `0`, `1`, `3`, `4`.
- Если передано хотя бы одно line-поле, должны быть переданы все 4.
- `prevLineNumber/prevLineHash32` должны указывать на существующий блок этой же цепочки.
- Для первого шага после root (`prevLineNumber == lineCode`):
- `TEXT (msg_type=1)`: `thisLineNumber = 0`;
- `TECH/CONNECTION/USER_PARAM (0/3/4)`: `thisLineNumber = 1`.
- Для обычного шага:
- `TEXT`: `thisLineNumber` допускает `same` или `+1` от предыдущего блока линии;
- `TECH/CONNECTION/USER_PARAM`: строго `+1`.
## 4. Root-идея для каналов и подписок
Для ссылок вида follow/friend/contact принято ссылаться на корневые блоки:
- `HEADER` для базовой сущности пользователя/канала `0`;
- `CREATE_CHANNEL` для пользовательских каналов.
Так ссылки остаются стабильными, даже когда в канале появляются новые сообщения.

View File

@ -0,0 +1,33 @@
# Командные сообщения каналов
## 1. Общий префикс
Командные сообщения распознаются по префиксу:
`/.`
Пример:
- `/.desc Новый комментарий канала`
## 2. Поддерживаемые команды
### Для всех типов каналов (`0`, `1`, `100`, `200`)
- `/.desc <text>` — смена описания канала.
Примечание:
- Описание канала в чтении определяется последней командой `/.desc` в линии канала.
- Если `/.desc` не было, используется описание из `CreateChannel`.
### Дополнительно для `type=200`
- `/.add <login> <channelName>`
- `/.remove <login> <channelName>`
Формат аргументов фиксирован: через пробел.
## 3. Текущая модель применения
- Команды передаются как обычные `TEXT_POST` сообщения.
- Сервер уже применяет `/.desc` при вычислении актуального описания канала.
- Команды `/.add` и `/.remove` зарезервированы под расширенную модель участников `type=200` на уровне UI/агрегации.
## 4. Статус для MVP
- В текущем UI каналы `type=100` и `type=200` не используются.
- Соответственно, `/.add` и `/.remove` считаются запланированными и пока не участвуют в рабочем UI-сценарии.

View File

@ -0,0 +1,18 @@
# TECH блоки (`type=0`, `version=1`)
TECH-тип покрывает системные записи цепочки.
## Подтипы
1. `subType=0``HEADER_COMPAT`
- стартовый блок цепочки;
- payload: tag `SHiNE` + login владельца.
2. `subType=1``TECH_CREATE_CHANNEL`
- создание нового канала;
- хранит line-поля + `channelName` + `channelDescription` + `channelType` + `channelTypeVersion`.
## Назначение
- инициализация блокчейна;
- управление набором каналов пользователя.

View File

@ -0,0 +1,38 @@
# TEXT блоки (`type=1`, `version=1`)
TEXT-тип хранит сообщения и редактирования.
## Подтипы
1. `subType=10``TEXT_POST`
- пост в линии канала;
- содержит line-поля + текст.
2. `subType=11``TEXT_EDIT_POST`
- редактирование поста;
- line-поля + target на оригинальный POST + новый текст.
3. `subType=20``TEXT_REPLY`
- ответ на сообщение;
- target (`toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`) + текст.
4. `subType=21``TEXT_EDIT_REPLY`
- редактирование ответа;
- target на исходный REPLY + новый текст.
- допускается пустой `text` для логического удаления сообщения (без физического удаления блока).
5. `subType=30``TEXT_REPOST`
- репост сообщения в линию канала;
- содержит line-поля + target на оригинальное сообщение + текст комментария;
- на текущем этапе продуктовой логики репост не редактируется (версии не накапливаются);
- временно отключён для записи через `AddBlock` до будущей реализации репостов.
## Правило для edit
`EDIT_POST` и `EDIT_REPLY` должны ссылаться на **оригинальный** блок, а не на предыдущий edit.
## Пустой text в edit
- Для `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` допустим `textLen=0`.
- Такой edit трактуется как логическое удаление содержимого сообщения.
- Для удаления используется именно edit-блок; отдельного `DELETE`-подтипа нет.

View File

@ -0,0 +1,14 @@
# REACTION блоки (`type=2`, `version=1`)
## Подтипы
1. `subType=1``REACTION_LIKE`
- лайк на целевой блок;
- хранит target: `toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`.
2. `subType=2``REACTION_UNLIKE`
- снятие лайка с целевого блока;
- хранит target: `toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`.
## Назначение
- реакция на текстовые сообщения (и потенциально другие target-блоки, если это разрешено бизнес-логикой).

View File

@ -0,0 +1,29 @@
# CONNECTION блоки (`type=3`, `version=1`)
CONNECTION-тип описывает социальные связи и подписки.
## Подтипы
`10/11``close_friend / unclose_friend` (близкий друг)
`20/21``contact / uncontact` (контакт)
`30/31``follow / unfollow` (подписан)
`40/41``spouse / unspouse` (супруг/супруга)
`50/51``parent / unparent` (родитель)
`52/53``child / unchild` (ребёнок)
`54/55``sibling / unsibling` (брат/сестра)
`60/61``known_person / unknown_person` (знаю этого человека)
`70/71``shine_confirmed / shine_unconfirmed` (точно уверен, что сияющий)
`74/75``shine_seen / shine_unseen` (мало знаком, но видел сияющим)
## Общий формат payload
- line-поля (`lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`)
- target (`toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`)
## Правила target
- FRIEND/CONTACT обычно указывают на `HEADER` цели (`block 0`).
- FOLLOW указывает на root канала:
- `HEADER` для канала `0`;
- `CREATE_CHANNEL` для пользовательского канала.
- Для остальных типов связи (`SPOUSE/PARENT/CHILD/SIBLING`) используется тот же target-формат.

View File

@ -0,0 +1,14 @@
# USER_PARAM блоки (`type=4`, `version=1`)
## Подтипы
1. `subType=1``USER_PARAM_TEXT_TEXT`
- хранит line-поля + `paramKey` + `paramValue`.
## Назначение
- сохранение пользовательского состояния (настройки клиента, синк-метки, курсоры чтения и т.д.).
## Практика
Для сложных структур удобно хранить JSON-строку в `paramValue` с версией схемы.

View File

@ -0,0 +1,64 @@
# История изменений документации блокчейна
# 2026-06-26 17:45:18 +0400
- Базовый коммит-ориентир: `44a1ba0`.
- На `t.shineup.me` подтверждена рабочая схема startup sync и full-resync:
- после рестарта сервер добивает `BlockchainTmpRecovery` и `BlockchainResyncRecovery`;
- `aidartest-001` успешно подтягивается с `shineup.me`;
- итоговое локальное состояние по `aidartest-001` дошло до `last_block_number=13`.
- В `Dev_Docs/Blockchain/sync-between-servers.md` добавлен практический результат ручной проверки на тестовом сервере.
## 2026-06-26 17:03:22 +0400
- Базовый коммит-ориентир: `71fdee0`.
- Обычный `AddBlock` переведён на crash-safe схему через временный кандидат `<blockchainName>.tmp_bch`, sidecar `<blockchainName>.write_check` и marker `<blockchainName>.write_pending`.
- `BlockchainTmpRecoveryOnStartup` теперь разбирает marker-driven recovery для обычной записи блока:
- если marker есть, recovery либо завершает swap tmp -> main, либо удаляет мусор;
- если marker нет, временные артефакты считаются мусором и удаляются.
- В `Dev_Docs/Blockchain/sync-between-servers.md` добавлено описание обычного `AddBlock` recovery и разделение между `write_pending` и `resync_pending`.
## 2026-05-24 11:40:00 +0300
- Базовый коммит-ориентир: `abdce05`.
- `TEXT_REPOST (subType=30)` оставлен как зарезервированный формат, но новые блоки репоста временно отключены на уровне `AddBlock`.
- В `11_TEXT_Blocks.md` зафиксировано, что запись `TEXT_REPOST` временно не используется до будущей реализации.
- В `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md` добавлен код отказа `repost_disabled`.
## 2026-05-21 19:05:00 +0300
- Базовый коммит-ориентир: `5344c42`.
- Добавлен новый TEXT-подтип `TEXT_REPOST (subType=30)`:
- обновлён перечень типов в `11_TEXT_Blocks.md`;
- обновлена быстрая карта типов в `00_Blockchain_Formats_and_Block_Types.md`.
- Уточнено API-описание поддержанных подтипов в `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md`.
- В документе `Dev_Docs/API/08_MCP_Чтение_и_дозапись_персонального_публичногоата.md` зафиксировано, что чтение канала учитывает `TEXT_POST` и `TEXT_REPOST`.
## 2026-05-20 11:34:17 +0300
- Базовый коммит-ориентир: `a53444b`.
- В `13_CONNECTION_Blocks.md` добавлены новые CONNECTION подтипы:
- `60/61``known_person / unknown_person` (знаю этого человека);
- `70/71``shine_confirmed / shine_unconfirmed` (точно уверен, что сияющий);
- `74/75``shine_seen / shine_unseen` (мало знаком, но видел сияющим).
- Обновлён список CONNECTION-подтипов в `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md`.
## 2026-05-19 20:30:21 +0300
- Базовый коммит-ориентир: `7986184`.
- Уточнён документ `11_TEXT_Blocks.md`: для `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` зафиксировано, что `textLen=0` допустим и трактуется как логическое удаление сообщения.
- Явно закреплено, что отдельного `DELETE`-подтипа нет, удаление выполняется edit-блоком.
## 2026-05-19 00:22:46 +0300
- Базовый коммит-ориентир: `c27da63a3e65`.
- Актуализирован `README.md` как точка входа для MVP-документации по протоколу.
- В документации явно зафиксировано, что `channelType=100` и `channelType=200` присутствуют в формате, но пока не используются в UI.
- Актуализирован перечень REACTION-подтипов: добавлен `REACTION_UNLIKE (subType=2)`.
- Актуализирован перечень CONNECTION-подтипов: добавлены `SPOUSE/PARENT/CHILD/SIBLING` и обратные операции.
- В документ `02_Blockchain_Kinds_and_Lines.md` добавлены фактические серверные правила валидации line-полей.
- Обновлён корневой `AGENTS.md`: формат блокчейна менять только после явного подтверждения пользователя и с предварительным предупреждением.
## 2026-05-13 00:02:32 +0300
- Базовый коммит-ориентир: `f63f40f1eb2f`.
- Добавлен текущий формат `CreateChannelBody` с полями `channelType (2 байта)` и `channelTypeVersion (2 байта)`.
- Зафиксированы типы каналов: `0=stories`, `1=public`, `100=personal`, `200=group`.
- Серверная уникальность имени канала изменена на `owner + type + name(slug)`.
- Root-канал `0` переименован в `stories` на уровне API-чтения.
- Для персонального канала (`type=100`) включена сборка парного потока при чтении (`A->B` + `B->A`, если существует).
- Добавлена поддержка командного префикса `/.` и команды `/.desc` для актуализации описания канала при чтении.
- Зафиксированы команды `/.add` и `/.remove` для каналов `type=200` (зарезервировано под расширение участниками).
- В `AGENTS.md` добавлено обязательное правило актуализации документации в `Dev_Docs/Blockchain/`.

View File

@ -0,0 +1,34 @@
# Документация блокчейна SHiNE (MVP)
Этот каталог описывает только текущий рабочий формат протокола для MVP.
## Основные документы
1. [01_Common_Block_Format.md](./01_Common_Block_Format.md)
Единый бинарный формат блока (Frame v0), подпись, базовые проверки.
2. [02_Blockchain_Kinds_and_Lines.md](./02_Blockchain_Kinds_and_Lines.md)
Виды цепочек и правила line-полей.
3. [10_TECH_Blocks.md](./10_TECH_Blocks.md)
Системные блоки (`msg_type=0`).
4. [11_TEXT_Blocks.md](./11_TEXT_Blocks.md)
Текстовые блоки (`msg_type=1`).
5. [12_REACTION_Blocks.md](./12_REACTION_Blocks.md)
Реакции (`msg_type=2`).
6. [13_CONNECTION_Blocks.md](./13_CONNECTION_Blocks.md)
Социальные связи (`msg_type=3`).
7. [14_USER_PARAM_Blocks.md](./14_USER_PARAM_Blocks.md)
Параметры пользователя (`msg_type=4`).
8. [01_Channel_Types_and_CreateChannel.md](./01_Channel_Types_and_CreateChannel.md)
Типы каналов и формат `CreateChannelBody`.
9. [02_Channel_Commands.md](./02_Channel_Commands.md)
Команды в текстовых сообщениях каналов.
10. [CHANGELOG.md](./CHANGELOG.md)
Журнал изменений документации.
## Важные ограничения MVP
- Каналы `type=100` и `type=200` присутствуют в формате, но сейчас не используются в UI.
- Поддерживаемый рабочий сценарий UI на текущем этапе: `stories (type=0)` и `public (type=1)`.
## Обязательное сопровождение
- При любом изменении формата/правил блокчейна в коде документы этого каталога обновляются в том же наборе изменений.
- Обычный `AddBlock` сейчас пишет через `<blockchainName>.tmp_bch`, `<blockchainName>.write_check` и `<blockchainName>.write_pending`; эта схема и `BlockchainTmpRecoveryOnStartup` должны быть описаны в актуальной документации по синхронизации и recovery.
- Каждое обновление документов фиксируется в `CHANGELOG.md` с датой/временем и хэшем коммита-основания.

View File

@ -0,0 +1,256 @@
# Синхронизация блоков и DM между серверами SHiNE
Документ описывает архитектуру и протокол синхронизации данных между партнёрскими серверами SHiNE.
## 1. Зачем нужна синхронизация
Пользователи SHiNE могут быть «приписаны» к разным серверам.
Когда пользователь A (на сервере X) пишет пользователю B (на сервере Y):
1. Сервер X принимает сообщение;
2. Сервер X должен переслать DM-блок серверу Y;
3. Сервер Y сохраняет блок и доставляет в активные сессии пользователя B.
Аналогично, блоки пользовательского блокчейна (записи `AddBlock`) должны синхронизироваться,
чтобы любой партнёрский сервер мог отдать полную историю пользователя.
## 2. Список серверов синхронизации (`sync_servers`)
Каждый сервер регистрирует в своей Solana PDA список `sync_servers`
логины SHiNE-аккаунтов партнёрских серверов, с которыми он синхронизируется.
- Список хранится в блоке `ServerProfileBlock` внутри `user_pda` сервера.
- Адрес каждого партнёрского сервера читается из его PDA на Solana.
- Синхронизация двусторонняя: оба сервера должны иметь друг друга в `sync_servers`.
## 3. Что синхронизируется
### 3.1 Личные сообщения (DM)
- Все DM-блоки форматов типов `1/2` (текст) и `3/4` (read-receipt).
- Сервер-отправитель: при получении пары блоков от клиента перенаправляет их серверу получателя.
- Сервер-получатель: сохраняет блоки в `signed_messages_v2`, доставляет в активные сессии.
- Дедупликация по уникальному `message_key = from|to|timeMs|nonce|type`.
### 3.2 Блоки пользовательского блокчейна
- Все блоки `AddBlock` пользователей, зарегистрированных на сервере или синхронизирующихся через него.
- Синхронизируются в обе стороны между всеми партнёрами из `sync_servers`.
- Порядок блоков сохраняется (по глобальному номеру блока и хэшу).
- Дедупликация по глобальному номеру блока и хэшу.
## 4. Текущая реализованная схема
На текущем этапе сервер уже умеет базовую межсерверную синхронизацию пользовательских блокчейнов.
### 4.1 Что уже сделано
1. При старте сервер читает свой `server.SHiNE.login`.
2. По этому логину он загружает из Solana свою server PDA.
3. Из неё вытаскивает список `sync_servers`.
4. Для каждого логина партнёра сервер читает его PDA и сохраняет локально:
- `login`
- `server_address`
5. После этого:
- новые локальные `AddBlock` рассылаются партнёрам в фоне;
- при старте запускается periodic sync;
- periodic sync повторяется каждые `12` часов после старта.
### 4.2 Какие server-to-server API уже используются
- `ListBlockchainHeads` — список heads всех локальных цепочек партнёра;
- `GetBlockchainBlock` — чтение одного конкретного блока партнёра;
- `GetSyncUserProfile` — минимальный профиль пользователя для локального создания `solana_users + blockchain_state` без обращения в Solana RPC.
### 4.3 Как сейчас работает periodic sync
Для каждого сервера из локальной таблицы `sync_servers`:
1. запрашивается `ListBlockchainHeads`;
2. для каждой удалённой цепочки сравниваются:
- `lastBlockNumber`
- `lastBlockHash`
- локальное состояние;
3. если локальная цепочка слабее, сервер по одному блоку вызывает `GetBlockchainBlock`;
4. каждый скачанный блок локально применяется через существующий `AddBlock`;
5. если у сервера ещё нет локальной записи пользователя/цепочки, перед этим подготавливается локальный `solana_users + blockchain_state`.
6. если во время replay обнаруживается рассинхрон или на одинаковой высоте удалённая цепочка сильнее, запускается полный resync:
- цепочка помечается in-memory как `resync in progress`;
- создаётся marker-file в `data/`;
- в одной SQL-транзакции очищаются локальные данные цепочки и корректируются чужие счётчики;
- удаляются `.bch` и `.tmp_bch`;
- цепочка подтягивается заново с `0` через `GetBlockchainBlock`.
- обычный `AddBlock` на эту цепочку в этот момент возвращает `chain_resync_in_progress`.
### 4.4 Как именно работает full resync
Full resync запускается только тогда, когда:
- локальная chain отстаёт и обычная докачка хвоста упирается в `bad_prev_hash` или `bad_block_number`;
- либо высота цепочек одинаковая, но удалённая версия сильнее по правилу:
- `lastBlockNumber`;
- `fileSizeBytes`;
- `lastBlockHash`.
Порядок действий:
1. Ставится in-memory guard на `blockchainName`.
2. Создаётся marker-file `<blockchainName>.resync_pending`.
3. Обычный `AddBlock` на эту chain временно получает `chain_resync_in_progress`.
4. Вызывается атомарный SQL cleanup одной chain:
- уменьшаются чужие `likes_count` и `replies_count`;
- удаляются локальные derived-state записи этой chain;
- удаляются `blocks` и `blockchain_state` этой chain.
5. Удаляются файлы `<blockchainName>.bch` и `<blockchainName>.tmp_bch`.
6. Локальная chain создаётся заново через `GetSyncUserProfile` или через Solana import, если `sync.importUserProfileFromPartner.enabled=false`.
7. Chain replay-ится с `0` через `GetBlockchainBlock`.
8. Если всё прошло успешно, marker-file удаляется.
9. Если на любом шаге произошёл сбой, marker-file остаётся на диске, и сервер добивает эту chain при следующем старте.
Важно:
- full resync не делает умный rollback по одному блоку;
- full resync не трогает DM-таблицы и `solana_users`;
- висячие cross-chain ссылки считаются допустимым поведением системы.
### 4.5 Как работает обычный `AddBlock` и его recovery
Обычная запись блока теперь тоже идёт через временные артефакты:
1. собирается `<blockchainName>.tmp_bch` как полный кандидат на замену основного файла;
2. пишется маленький sidecar `<blockchainName>.write_check` с `blockNumber` и `blockHash`;
3. только после этого создаётся пустой marker `<blockchainName>.write_pending`;
4. выполняется SQL-транзакция;
5. после `commit` tmp атомарно ставится на место основного `.bch`;
6. marker и sidecar удаляются.
На старте `BlockchainTmpRecoveryOnStartup` смотрит именно на эту пару:
- если `write_pending` есть, recovery проверяет sidecar и БД, а затем либо завершает swap, либо чистит временные файлы;
- если `write_pending` нет, а `tmp_bch` или `write_check` остались, это мусор и он удаляется;
- `resync_pending` сюда не относится, это отдельный recovery-поток.
### 4.6 Startup recovery по marker-file
При старте сервер идёт в таком порядке:
1. `BlockchainTmpRecoveryOnStartup` для `*.write_pending` и orphan `*.tmp_bch` / `*.write_check`;
2. `BlockchainResyncRecoveryOnStartup` для `*.resync_pending`;
3. только потом поднимается обычный сервер и запускается `PeriodicBlockchainSyncService`.
Если marker-file существует:
- сервер не должен начинать обычную работу поверх этой chain;
- recovery снова выполняет cleanup и replay с нуля;
- если recovery не завершился, marker остаётся, и сервер не переходит к обычному режиму для этой chain.
### 4.7 Зачем понадобился `GetSyncUserProfile`
Изначально подготовка локальной цепочки делалась через Solana:
- из `blockchainName` извлекался `login`;
- сервер вызывал import пользователя из Solana PDA;
- по данным PDA локально создавались `solana_users + blockchain_state`.
На практике это упёрлось в ограничение внешнего Solana RPC: при чистом старте и массовой подтяжке чужих цепочек сервер мог получать `HTTP 429`.
Поэтому добавлен отдельный обходной режим:
- настройка `sync.importUserProfileFromPartner.enabled=true`
- в этом режиме сервер **не ходит в Solana RPC** для создания локальной цепочки во время sync;
- вместо этого он запрашивает у сервера-партнёра `GetSyncUserProfile` и создаёт локальную запись по данным партнёра.
- если локальный `solana_users` уже существует, sync восстанавливает только `blockchain_state` и не трогает identity-слой.
Это временная практическая заплатка, чтобы clean-start sync не зависел от rate limit внешнего Solana endpoint.
### 4.8 Что делает настройка `sync.importUserProfileFromPartner.enabled`
- `false` — стандартный режим, подготовка локального пользователя идёт через Solana PDA;
- `true` — sync-режим обхода Solana, локальный пользователь создаётся по server-to-server `GetSyncUserProfile`.
Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время periodic sync.
## 5. Целевой протокол следующего этапа
### 5.1 Межсерверное соединение
- Серверы устанавливают постоянное WebSocket-соединение друг с другом.
- Адрес партнёра определяется по `server_address` из его Solana PDA.
- Аутентификация: подпись Ed25519 корневым ключом сервера (`root_key` из PDA).
- При разрыве — переподключение с экспоненциальным backoff.
### 5.2 Доставка новых данных (push)
- При получении нового блока или DM сервер немедленно пушит его всем подключённым партнёрам.
- Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff.
### 5.3 Начальная синхронизация (backfill)
- При первом подключении к партнёру серверы обмениваются «курсорами» состояния:
последний глобальный номер блока, последний известный DM-ключ.
- Сервер с более полной историей досылает недостающее партнёру.
### 5.4 Разрешение конфликтов
- Блоки пользовательского блокчейна: порядок определяется глобальным номером блока.
Конфликтующие ветки (fork) разрешаются по правилам `AddBlock` (см. `Dev_Docs/Blockchain/README.md`).
- DM: конфликтов нет, `message_key` уникален.
## 6. Маршрутизация DM между серверами
При отправке DM от пользователя A к пользователю B:
1. Клиент A отправляет пару блоков на свой сервер X.
2. Сервер X определяет, на каком сервере зарегистрирован пользователь B.
- Сначала проверяет локально (если B зарегистрирован на X).
- Иначе читает PDA пользователя B из Solana и смотрит `access_servers`.
- Выбирает первый доступный сервер из `access_servers` и перенаправляет туда DM.
3. Сервер Y (из `access_servers` B) сохраняет и доставляет блоки.
Кэш адресов серверов: обновляется раз в сессию (при ошибке соединения).
## 7. Безопасность
- Все блоки подписаны ключами пользователя на клиенте — сервер не может подделать содержимое.
- Серверы не расшифровывают DM-контент (шифрование — задача следующего этапа).
- При синхронизации каждый блок проходит валидацию подписи на принимающем сервере.
## 8. Статус реализации
| Компонент | Статус |
|-----------|--------|
| Регистрация серверной PDA в Solana | ✅ Реализовано |
| Чтение `sync_servers` из PDA | ✅ Реализовано |
| Локальная таблица `sync_servers` | ✅ Реализовано |
| Публичный `ListBlockchainHeads` | ✅ Реализовано |
| Публичный `GetBlockchainBlock` | ✅ Реализовано |
| Публичный `GetSyncUserProfile` | ✅ Реализовано |
| Плановый blockchain sync при старте + каждые 12 часов | ✅ Реализовано |
| Обход Solana RPC через `sync.importUserProfileFromPartner.enabled` | ✅ Реализовано |
| Обычный `AddBlock` через `tmp_bch`/`write_check`/`write_pending` | ✅ Реализовано |
| Межсерверный постоянный WebSocket-канал | Нужна реализация |
| Push новых DM партнёрам | Нужна реализация |
| Push блоков блокчейна партнёрам | ✅ Реализована базовая one-shot версия |
| Periodic backfill отсутствующего хвоста | ✅ Реализовано |
| Разрешение рассинхрона / divergence | ✅ Реализована базовая full-resync схема во время periodic sync |
| Startup recovery по `*.resync_pending` marker-file | ✅ Реализовано |
| Маршрутизация DM через access_servers | Нужна реализация (заглушка) |
Текущая версия сервера уже умеет базовую синхронизацию блокчейнов между партнёрами.
Не реализованы ещё DM-sync и постоянные server-to-server соединения.
Следующие отдельные шаги после текущего этапа:
- отдельно проверить full-resync и startup-recovery на реальном тестовом прогоне после ручного удаления БД/файлов.
### 8.1 Практическая проверка на тестовом сервере
Проверка на `t.shineup.me` показала, что текущая схема действительно поднимает цепочку при старте:
- после рестарта сервер сначала проходит `BlockchainTmpRecovery`;
- затем обрабатывает `BlockchainResyncRecovery`;
- после этого сам догружает цепочку `aidartest-001` с `shineup.me`;
- итоговое состояние на тестовом сервере:
- `blockchain_state.last_block_number = 13`
- `blocks` по `aidartest-001` = `14` записей
Это подтверждает, что startup sync и full-resync flow работают в живом сценарии, а не только в коде.

33
Dev_Docs/Figma/README.md Normal file
View File

@ -0,0 +1,33 @@
# Figma
Эта папка хранит рабочие инструкции по переносу экранов SHiNE в Figma и по обратному переносу изменений из Figma в код.
## Что здесь лежит
- `README.md` — точка входа и краткий регламент.
- `TRANSFER_UI_SCREENS.md` — подробная инструкция по переносу экранов UI в Figma и обратно.
## Когда читать
Читать перед любыми задачами вида:
- перенести экран из `shine-UI` в Figma;
- собрать новый Figma-файл для экранов SHiNE;
- перенести изменения из Figma обратно в код;
- уточнить, каким способом переносить экраны: по одному или пачкой.
## Ключевое правило
Для экранов SHiNE безопасный рабочий способ на текущий момент:
- переносить экраны в Figma по одному;
- не пытаться сразу переносить длинный auth-flow пачкой;
- после каждого переноса визуально проверять результат в самой Figma;
- только после удачного одного экрана переходить к следующему.
## Про Miro
Отдельной папки `Miro` пока нет.
Причина:
- практики по Miro в проекте пока мало;
- устойчивого процесса ещё нет;
- как только появится стабильный сценарий работы с Miro, его нужно будет оформить аналогично Figma.

View File

@ -0,0 +1,224 @@
# Перенос экранов UI в Figma и обратно
## Зачем нужен этот документ
Этот документ фиксирует практический опыт, который уже был получен на переносе стартового экрана, экрана регистрации и дальнейших попытках.
Главная цель:
- чтобы агент не повторял неудачные попытки;
- чтобы переносы делались одинаково;
- чтобы изменения из Figma можно было уверенно переносить назад в `shine-UI`.
## Где находится основной UI
- основной клиентский UI: `shine-UI/`
- маршруты и список pre-auth экранов: `shine-UI/js/router.js`
- экраны: `shine-UI/js/pages/`
- общие стили: `shine-UI/styles/main.css`, `shine-UI/styles/layout.css`, `shine-UI/styles/components.css`
## Что считать успешным переносом в Figma
Успешный перенос экрана в Figma — это не просто фон и прямоугольники.
Нужно, чтобы:
- были видны все ключевые текстовые элементы;
- кнопки были перенесены как отдельные элементы;
- поля ввода были явно видны;
- экран был узнаваем визуально;
- пользователь мог вручную подправить макет в Figma;
- после правок можно было понять, что именно переносить обратно в код.
## Текущий рабочий способ
На текущем проекте лучший практический способ такой:
1. Переносить только один экран за раз.
2. Сначала читать конкретный `js/pages/<screen>.js`.
3. Затем читать связанные стили из `styles/components.css` и `styles/layout.css`.
4. После этого вручную собирать экран в Figma как отдельный frame с явными элементами.
5. Проверять в Figma, что не получился только фон без текста и контролов.
6. Только после успешной проверки переходить к следующему экрану.
## Почему нельзя переносить пачкой
Был получен негативный опыт:
- при переносе сразу многих экранов в Figma часть экранов отображалась как фон без нормальных надписей и элементов;
- длинные экраны с большим количеством текста и форм разваливались;
- автогенерация давала внешний вид, непригодный для ручной доработки.
Поэтому правило такое:
- auth-flow, регистрация, вход, onboarding — переносить по одному экрану;
- после каждого экрана ждать визуального подтверждения пользователя;
- не объединять 5-10 экранов в один проход без отдельного разрешения и без промежуточной проверки.
## Рекомендуемый порядок переноса в Figma
### Вперёд: код -> Figma
1. Определить точный экран.
2. Найти файл экрана в `shine-UI/js/pages/`.
3. Найти используемые CSS-классы через поиск по файлу экрана.
4. Вытащить:
- тексты;
- состав кнопок;
- поля ввода;
- карточки;
- блоки статуса;
- последовательность секций.
5. Если экран длинный, всё равно переносить его как один frame, но собирать блоками сверху вниз.
6. В Figma создавать отдельный экран рядом с уже существующими экранами, а не смешивать всё в одну кучу.
7. После создания экрана проверить метаданные/скриншот Figma, если инструмент это позволяет.
### Назад: Figma -> код
1. Снять актуальный скриншот изменённого Figma-экрана.
2. Получить метаданные узла, если это помогает понять структуру.
3. Сравнить Figma с текущим кодом экрана.
4. Переносить обратно в код только реальные изменения:
- порядок блоков;
- тексты;
- размеры/отступы;
- наличие или отсутствие карточек;
- подписи кнопок;
- видимость блоков.
5. Не придумывать новые UX-решения без отдельного подтверждения пользователя, если их нет в Figma.
6. После правок проверять экран локально или как минимум по коду и зависимостям.
## Что переносить вручную
Вручную, а не автогенерацией, нужно переносить:
- экраны регистрации;
- экраны входа;
- длинные формы;
- экраны с несколькими карточками;
- экраны с длинными объясняющими текстами;
- экраны, где важен порядок блоков.
Причина:
- именно они чаще всего ломаются при слишком автоматическом переносе.
## Какие ошибки уже были
### Ошибка 1. Перенос пачкой
Проблема:
- несколько экранов были добавлены сразу;
- пользователь увидел, что на экранах в Figma «какая-то ерунда».
Вывод:
- переносить по одному.
### Ошибка 2. Видно только фон
Проблема:
- frame создавался, фон и свечения были видны;
- тексты и элементы либо не появлялись, либо получались непригодными.
Вывод:
- при сложных экранах собирать элементы вручную и явно.
### Ошибка 3. Слишком вольная реконструкция
Проблема:
- экран формально был перенесён, но визуально не соответствовал ожиданию пользователя.
Вывод:
- для SHiNE важнее узнаваемый и редактируемый экран, чем «формально похожий» экран.
## Обязательные проверки после переноса в Figma
После каждого нового экрана агент должен проверить:
- виден ли заголовок;
- видны ли кнопки;
- видны ли поля ввода;
- не исчезли ли длинные тексты;
- не сломан ли порядок секций;
- не оказался ли на холсте только фон и пустые прямоугольники.
Если хотя бы один пункт не выполнен:
- не считать перенос завершённым;
- либо переделать экран сразу;
- либо остановиться и показать пользователю только после исправления.
## Правила для длинных экранов
Если экран длинный, например регистрация:
- высота frame может быть больше стандартной мобильной высоты;
- секции должны идти в правильном вертикальном порядке;
- отдельные карточки должны быть вынесены в отдельные блоки;
- тексты лучше упрощённо располагать вручную, чем терять их совсем.
## Правила для экрана регистрации
Экран `register-view` особенно чувствительный.
При переносе нужно отдельно учитывать:
- заголовок и стрелку назад;
- поля логина и пароля;
- переключатель режима 12 слов;
- сетку слов;
- строку статуса длины пароля;
- строку статуса проверки логина;
- кнопку проверки логина;
- отдельную карточку первого сервера;
- отдельную карточку FAQ;
- нижние кнопки `Назад` и `Далее`.
## Правила для экрана входа
Для экранов входа важно не смешивать:
- экран выбора способа входа;
- вход по логину/паролю;
- вход через другое устройство;
- вход по QR.
Каждый из них переносить отдельно.
## Что делать после правок пользователя в Figma
Если пользователь изменил экран в Figma:
1. Считать Figma источником визуальной правки.
2. Сначала понять, что именно изменено:
- тексты;
- порядок блоков;
- наличие блоков;
- размеры;
- отступы;
- логика flow.
3. Переносить эти изменения назад в код минимально необходимыми правками.
4. Если из Figma следует уже не только визуальная, но и UX-логическая правка, отдельно проверить, что она согласована пользователем.
## Когда нужно добавить заметку в Pending_Features
Если после изменения по Figma:
- поменялась логика flow;
- поменялась регистрация/вход;
- нужен реальный прогон на test2;
- затронута интеграция с Solana;
тогда нужно добавить файл в `Dev_Docs/Pending_Features/`.
## Что пока не оформлено для Miro
По Miro пока нет устойчивого процесса.
Из того, что уже понятно:
- пока не стоит обещать такой же отлаженный перенос, как для Figma;
- сначала нужно накопить хотя бы 2-3 реальных сценария работы;
- только после этого оформлять отдельную папку и регламент.
## Краткая памятка для агента
Если задача звучит как:
- «перенеси экран в Figma»;
- «добавь экран в Figma»;
- «я поправил экран в Figma, перенеси назад»;
то агент должен:
1. Прочитать этот документ.
2. Работать по одному экрану.
3. Не переносить auth-flow пачкой.
4. Проверять результат после каждого экрана.
5. При переносе обратно в код не гадать, а опираться на Figma-правки.

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** | `client.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(clientPub)`. См. §3. |
| homeserver | `homeserver.key:<имя>` | Ключ homeserver-устройства, по одному на каждый homeserver (различитель — имя). См. §4. |
Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`.
---
## 3. Solana-ключ
Отдельного «солана-ключа» нет. На Solana работают два ключа:
- **`client.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(clientPub)`.
Этим ключом оплачиваются и подписываются `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 оплачиваются с `client_key`», «root_key — не fee payer»).
Кратко про роли на Solana: `root.key` — это **главный (master) ключ**: им управляют PDA-записью
(`create/update`) и через это можно заменить все остальные ключи; `client.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(clientPub)` (~113).
- `shine-UI/js/pages/topup-view.js``clientWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.clientPair`.
Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `client.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, `ClientKeyBlock` §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. Нельзя сознательно оставлять код и этот документ в рассинхроне без отдельной явной договорённости.

176
Dev_Docs/Keys/README.md Normal file
View File

@ -0,0 +1,176 @@
# Ключи SHiNE
Этот документ описывает роли ключей в SHiNE и их связь с Solana, персональным блокчейном, личными сообщениями, сессиями и будущими аппаратными устройствами.
Документ является архитектурной справкой. Он не меняет текущие форматы API, DM-блоков или блокчейна сам по себе.
## Коротко
В SHiNE у пользователя есть несколько уровней ключей:
- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `client key`).
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
- `client key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
- `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере.
Главная идея: самые важные ключи можно держать на доверенном серверном или аппаратном устройстве, а обычные клиентские устройства получают только ключи, нужные для текущей работы.
## `root key`
`root key` - главный ключ пользователя.
Назначение:
- регистрация пользователя в Solana;
- создание и обновление пользовательской PDA-записи;
- вызов критически важных Solana-функций;
- изменение главных настроек пользователя;
- управление остальными ключами;
- подтверждение операций, которые должны иметь максимальный уровень доверия.
`root key` — это **главный (master) ключ** в следующем смысле: зная `root key`, можно управлять пользовательской PDA-записью в Solana (`create_user_pda` / `update_user_pda`) и тем самым **заменить все остальные ключи** пользователя (device, blockchain, homeserver). Поэтому компрометация `root key` равносильна компрометации всей личности пользователя.
Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `client key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3.
## `blockchain key`
`blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
Назначение:
- подпись записей в персональном блокчейне пользователя;
- подтверждение действий, которые должны попасть в SHiNE-блокчейн;
- разделение полномочий между главным Solana-ключом и ключом ежедневной записи.
У пользователя может быть несколько персональных блокчейнов или веток. При смене `blockchain key` фактически создаётся новая ветка записи:
- `username-001` - первая ветка;
- `username-002` - вторая ветка;
- `username-003` - третья ветка.
Рабочая логика по умолчанию должна использовать последнюю актуальную ветку. Старые ветки остаются читаемыми и показывают историю смены ключей.
## `client key`
`client key` - общий ключ, который знают доверенные устройства пользователя.
Назначение:
- повседневные входящие и исходящие личные сообщения;
- звонки и связанные с ними сообщения;
- self-messages, то есть внутренние сообщения пользователя самому себе;
- мелкие Solana-расходы на текущие операции;
- derivation Arweave-кошелька;
- оплата или подготовка добавления данных в Arweave-кошелек по отдельному протоколу.
Arweave-кошелёк должен выводиться из `client key` по протоколу:
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md`
Если пользователь теряет только `client key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными.
## `session key`
`session key` - уникальный ключ конкретной сессии или устройства.
Возможные форматы:
- `Ed25519` - предпочтительный современный вариант;
- `RSA` - legacy-вариант, полезный для устройств, где системное защищённое хранилище хорошо поддерживает RSA-ключи и не позволяет извлекать приватный ключ.
Назначение:
- авторизация сессии на сервере;
- привязка устройства к пользователю;
- подтверждение запросов от конкретной сессии;
- доступ к зашифрованному `client key` после успешной авторизации.
Одна и та же сессия может быть пригодна для подключения к нескольким серверам пользователя, если архитектура конкретного пользователя это допускает.
У сессии должны быть:
- имя сессии;
- тип сессии;
- публичная часть ключа;
- ссылка на пользователя;
- информация о сервере или серверах, которым эта сессия доверена.
Имя сессии может создаваться автоматически из названия устройства и короткого случайного идентификатора, например `Android-a1b2c3`, `Ubuntu-f47a90`. Пользователь может переименовать сессию.
## Типы сессий
Базовые типы:
- обычная пользовательская сессия;
- серверная сессия;
- аппаратная или доверенная сессия с доступом к расширенным ключам.
Обычное устройство обычно имеет:
- собственный `session key`;
- зашифрованный `client key`, который открывается после авторизации;
- доступ к DM, звонкам и обычным пользовательским операциям.
Доверенное серверное или аппаратное устройство может иметь:
- `root key`;
- `blockchain key`;
- `client key`;
- собственный `session key`.
Такая сессия может подписывать операции повышенной важности по запросам пользователя.
## Внутренние self-messages
Self-message - это сообщение пользователя самому себе.
Такие сообщения нужны, чтобы обычное устройство могло попросить доверенное устройство выполнить действие:
- подписать запись `blockchain key` и передать её в SHiNE-блокчейн;
- подписать изменение настройки через `root key`;
- обновить ключи;
- сохранить внутреннюю команду или настройку;
- отправить сообщение другому пользователю с сохранением копии себе;
- сохранить сообщение только себе.
Важно: self-message не является публичной командой сервера. Это пользовательская внутренняя команда, которую сервер или доверенное устройство обрабатывает в рамках прав конкретного пользователя.
## Шифрование входящих сообщений
Входящее сообщение может быть зашифровано:
- `client key`;
- `session key`;
- отдельным ключом конкретного чата;
- другим ключом, который уже известен клиенту.
В сообщении не должно быть лишнего раскрытия того, каким именно ключом оно зашифровано. Клиент пробует расшифровать сообщение доступными ключами по порядку. Если расшифровка не удалась, сообщение остаётся непонятным для этого устройства.
## Копии сообщений
Для отправки сообщений нужны несколько режимов:
- сообщение другому пользователю с исходящей копией себе;
- сообщение другому пользователю без локальной исходящей копии;
- сообщение только себе.
Это должно позволить строить обычные DM, внутренние команды, личные заметки и зашифрованные пользовательские чаты поверх одной общей модели сообщений.
## Связанные документы
- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`client.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 и движения средств.
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` - деплой и первичная инициализация Solana-регистрации.
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md` - derivation Arweave-кошелька из `client key`.
## Что нужно уточнить перед реализацией
- точный формат записи списка ключей в Solana PDA;
- как именно обозначать активную ветку персонального блокчейна;
- какие операции требуют `root key`, а какие достаточно подписывать `blockchain key`;
- формат self-message-команд;
- порядок перебора ключей при расшифровке входящих сообщений;
- правила ротации `client key` и восстановления доступа после потери устройства;
- какие типы серверных и аппаратных сессий нужны в первой реализации.

View File

@ -0,0 +1,38 @@
# Поддержать проект Сияние
Статус: `pending`
## Кратко
В `shine-UI/js/pages/wallet-view.js` добавлен новый раздел `Поддержать проект Сияние` с тремя входами:
1. купить билет;
2. посмотреть билет по номеру;
3. сгенерировать новую пару ключей.
## Что проверить
1. Открыть `Кошелёк`.
2. Перейти в `Поддержать проект Сияние`.
3. Проверить экран покупки:
- виден коэффициент;
- виден остаток лимита очереди 1;
- виден расчет в SOL;
- кнопка `Справка` открывает отдельный экран;
- покупка блокируется, если сумма больше остатка лимита.
4. Проверить экран просмотра:
- `12` ищется как билет очереди 1;
- `2-5` и `3 8` ищутся как билеты очередей 2 и 3;
- показываются статус, количество билетов до него и уже выплаченные значения.
5. Проверить генератор ключей:
- генерируется новая пара ключей;
- публичный и секретный ключи показываются;
- можно скопировать и скачать результат;
- дополнительный текст в поле необязателен.
## Ожидаемый результат
- Экран раздела поддержки открывается из `wallet-view`.
- Покупка билета выполняется по текущему курсу и с допуском 3%.
- По номеру билета показывается понятная сводка по очереди.
- Генерация ключей использует безопасный браузерный рандом и не требует сохранения секретного ключа.

View File

@ -0,0 +1,20 @@
# Недопроверенные фичи
Эта папка хранит список доработок, которые уже реализованы, но ещё не подтверждены ручной проверкой.
## Как использовать
1. При каждом коммите с новыми пользовательскими фичами (если нужна ручная проверка) добавить новый файл:
- формат: `YYYY-MM-DD_HHMM_<short-feature-name>.md`
- название `<short-feature-name>` и текст файла по возможности писать на русском языке
2. В файле указать:
- что сделано;
- как проверять;
- ожидаемый результат;
- текущий статус (`pending` / `in_progress` / `done`).
3. После подтверждения работоспособности — удалить файл фичи из этой папки.
## Важно
- `README.md` не удаляется.
- Количество недопроверенных фич = число файлов `*.md` в этой папке, кроме `README.md`.

View File

@ -0,0 +1,25 @@
# Регистрация: FAQ и режим пароля из 12 слов
- краткое описание:
- на экране регистрации добавлен блок частых вопросов с переходом на отдельный экран справки;
- добавлен альтернативный режим ввода пароля через 12 полей-слов в кошелёчном формате, которые склеиваются в одну строку без изменения API;
- такой же режим добавлен и на экран входа по логину и паролю.
- что проверять:
- на стартовом экране открыть `Зарегистрироваться`;
- убедиться, что внизу экрана есть кнопки FAQ;
- открыть несколько вопросов и проверить возврат обратно на регистрацию;
- включить галочку `Представить пароль в виде 12 слов`;
- убедиться, что появляется сетка с нумерованными полями в 3 колонки;
- ввести часть слов, перейти дальше и проверить, что шаг подтверждения и генерация ключей работают;
- выключить галочку и проверить, что пароль остаётся собранным в одном поле;
- открыть экран входа по паролю и повторить те же проверки для режима `12 слов`;
- пройти регистрацию до шага оплаты без ошибок интерфейса.
- ожидаемый результат:
- FAQ открывается отдельным экраном и содержит понятные ответы;
- режим `12 слов` не ломает регистрацию и вход и даёт тот же поток, что и обычный пароль;
- пароль не отправляется в новом формате, а продолжает использоваться как одна строка.
- статус:
- pending

View File

@ -0,0 +1,25 @@
# Временная бесплатная загрузка аватара в Arweave
- краткое описание фичи:
Добавлены два временных `Test...` API для бесплатной загрузки маленьких аватаров в Arweave через серверный кошелёк с лимитом `3` загрузки на пользователя. В UI мастера смены аватара добавлен пункт `Залить аватар бесплатно`.
- что именно проверять:
1. Пользователь с активной сессией открывает редактирование профиля.
2. По нажатию на аватар открывается мастер `Сменить аватар`.
3. В мастере есть пункт `Залить аватар бесплатно`.
4. До первой загрузки UI показывает остаток `3 из 3`.
5. Маленький JPEG/PNG/WebP после уменьшения до файла <= `128 KB` успешно уходит через `TestUploadFreeAvatar`.
6. После загрузки приходит `txId`, и аватар сохраняется в профиль как `avatar.ar`.
7. Остаток уменьшается: `2`, `1`, `0`.
8. На четвёртой попытке сервер отвечает понятной ошибкой про исчерпанный бесплатный лимит.
9. Если итоговый уменьшенный файл всё ещё > `128 KB`, UI не отправляет его и показывает понятную ошибку.
10. Если серверный Arweave JWK/path не настроен, UI получает понятную ошибку временной функции.
- ожидаемый результат:
- первые 3 маленькие аватарки загружаются через серверный Arweave-кошелёк;
- после каждой успешной загрузки `ava` в профиле указывает на новый `txId`;
- после исчерпания лимита дальнейшая бесплатная загрузка блокируется без записи в профиль;
- обычная загрузка через свой Arweave-кошелёк продолжает работать отдельно.
- статус:
pending

View File

@ -0,0 +1,28 @@
# Общий список каналов без stories
- Краткое описание:
вкладка `Каналы` переведена на единый список без разделения на "мои" и "подписки".
Название канала в списке теперь показывается как `login_владельцаазваниеанала`.
Служебный канал `stories` скрыт из списка каналов, поиска, подписки и связанных UI-сценариев.
- Что проверять:
1. Открыть вкладку `Каналы`.
2. Убедиться, что сразу показывается один общий список.
3. Проверить, что свои и чужие каналы отображаются вместе.
4. Проверить формат названий: `ownerLogin/channelName`.
5. Открыть свой канал и убедиться, что внутри сохраняется UI владельца.
6. Открыть чужой канал и убедиться, что внутри сохраняется UI подписчика.
7. Проверить, что `stories` не отображается:
- в общем списке;
- в поиске каналов;
- в подписке на канал;
- в списках выбора канала для репоста.
- Ожидаемый результат:
- вкладка `Каналы` больше не делится на два режима;
- все видимые каналы идут единым списком;
- `stories` нигде не виден и не предлагается пользователю;
- переход в канал сохраняет корректный UI в зависимости от владельца.
- Статус:
`pending`

View File

@ -0,0 +1,47 @@
# Crash-safe запись обычного `AddBlock` через `tmp_bch`
## Кратко
Обычный `AddBlock` переведён на схему:
1. сборка `<blockchainName>.tmp_bch`;
2. запись sidecar `<blockchainName>.write_check` с `blockNumber` и `blockHash`;
3. создание пустого marker `<blockchainName>.write_pending`;
4. SQL-транзакция;
5. атомарная подмена `tmp -> main`;
6. удаление временных файлов.
## Что проверить
1. Обычный `AddBlock` на свежей цепочке.
2. Падение до SQL-commit:
- должны остаться только временные файлы;
- на старте они должны быть удалены.
3. Падение после SQL-commit, но до `atomicReplaceBlockchainFile(...)`:
- на старте recovery должен довести swap до конца.
4. Падение после `atomicReplaceBlockchainFile(...)`, но до удаления marker/sidecar:
- на старте recovery должен просто подчистить хвост.
5. Сценарий без marker:
- `tmp_bch` / `write_check` считаются мусором и удаляются.
## Ожидаемый результат
- БД и файловая версия цепочки остаются согласованными.
- Повторный старт сервера не ломает chain и не требует ручной правки файлов.
- `BlockchainTmpRecoveryOnStartup` корректно обрабатывает и живые остатки, и мусор.
## Статус
`pending`
## Что уже сделано
- В коде есть `tmp_bch`, `write_check` и `write_pending`.
- `BlockchainWriter` пишет обычный `AddBlock` через временные артефакты.
- `BlockchainTmpRecoveryOnStartup` умеет добивать или чистить незавершённую запись.
## Что ещё перепроверить
- ручной crash-test на тестовом сервере;
- совместимость с уже существующими `resync_pending` marker-файлами;
- отсутствие ложных срабатываний на старых временных файлах.

View File

@ -0,0 +1,29 @@
# Проверка аварийных остановок на разных этапах
## Кратко
Нужно отдельно проверить, как сервер восстанавливается после внезапной остановки:
1. во время обычного `AddBlock` / `tmp_bch`-pipeline;
2. во время `full resync` цепочки;
3. во время startup recovery, если остановка произошла на предыдущем запуске;
4. при обычном апгрейде сервиса без явного crash-сценария.
## Что проверять
1. Остановка сервиса до `commit` БД.
2. Остановка сервиса после `commit`, но до замены `main.bch`.
3. Остановка сервиса во время `BlockchainResyncCleanupDAO`.
4. Остановка сервиса во время повторной загрузки цепочки по `GetBlockchainBlock`.
5. Поведение при обычном `systemctl restart`, когда сервер сам должен добить recovery.
## Ожидаемый результат
- после старта сервер либо дочищает временные артефакты, либо завершает незаконченный `resync`;
- не остаётся битых `.tmp_bch`, `.write_check`, `.write_pending`, `.resync_pending`;
- БД и файлы цепочки остаются согласованными;
- обычная работа сервера не стартует поверх незавершённого recovery.
## Статус
`pending`

View File

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

View File

@ -0,0 +1,214 @@
# Личные сообщения (DM)
## Текущее состояние
Сейчас в проекте реализованы:
- новый формат контентных личных сообщений `SHiNE_DM`;
- ревизии сообщений через `revisionTimeMs`;
- редактирование сообщения через повторную отправку той же логической пары;
- удаление сообщения через пустую ревизию;
- `upsert` последней версии сообщения на сервере.
Сейчас в проекте **не реализованы**:
- вложения в DM;
- upload/download файлов для DM;
- UI-кнопка прикрепления файла;
- серверное хранение файловых связей для DM.
Черновик будущих вложений вынесен отдельно:
- `Dev_Docs/Personal_Messages/Черновик_будущих_DM_вложений.md`
## Общая схема
Личное сообщение по-прежнему отправляется парой signed-блоков:
- `type=1` — входящий блок для получателя;
- `type=2` — исходящая копия для отправителя.
Read-receipt пока остаются в legacy-формате:
- `type=3` — входящее подтверждение прочтения;
- `type=4` — исходящая копия подтверждения.
Ключи сообщения:
- `baseKey = fromLogin|toLogin|timeMs|nonce`
- `messageKey = baseKey|messageType`
Логический идентификатор письма задаётся парой:
- `timeMs`
- `nonce`
Эти поля не меняются при редактировании или удалении. Меняется только:
- `revisionTimeMs`
- содержимое `encryptedBody`
Сервер хранит только последнюю версию записи для каждого `messageKey`.
## Формат контентного DM: `SHiNE_DM`
Префикс бинарного блока:
- `SHiNE_DM`
Поля идут в 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)
### Ограничения
- `attachmentsCount` сейчас всегда должен быть `0`
- `encryptedBodyLen` сейчас ограничен сервером до `16384` байт
- `revisionTimeMs` не может быть отрицательным
Если приходит `attachmentsCount != 0`, сервер отклоняет такой DM как:
- `ATTACHMENTS_DISABLED`
## Legacy read-receipt: `SHiNE_dm2`
Подтверждения прочтения `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`
## Редактирование
Редактирование делается новой отправкой той же логической пары сообщения:
- `timeMs` и `nonce` остаются теми же;
- `messageType` остаётся `1/2`;
- `revisionTimeMs` становится больше;
- `encryptedBody` содержит новую версию текста.
Если на сервер приходит более старая ревизия, она игнорируется.
Если приходит та же ревизия и тот же бинарный блок, сервер тоже её не применяет повторно.
## Удаление
Удаление личного сообщения делается как новая ревизия того же сообщения:
- `timeMs` и `nonce` остаются прежними;
- `revisionTimeMs` увеличивается;
- `attachmentsCount = 0`;
- `encryptedBodyLen = 0`;
- `encryptedBody` пустой.
В UI такое сообщение не показывается.
На сервере это не отдельный тип сообщения, а просто последняя пустая ревизия того же `messageKey`.
## Поведение сервера
Для контентных DM сервер:
1. принимает пару signed-блоков `type=1/2`;
2. валидирует формат, подпись и совпадение ключевых полей пары;
3. проверяет, что для обеих сторон пары совпадают:
- `fromLogin`
- `toLogin`
- `timeMs`
- `nonce`
- `revisionTimeMs`
- `encryptedBody`
4. делает `upsert` последней версии в `signed_messages_v2`;
5. сбрасывает pending-доставку по сессиям для новой ревизии;
6. рассылает актуальную версию адресатам через `SignedMessageArrived`.
История старых ревизий сейчас не хранится отдельно: в таблице остаётся только последняя версия по каждому `messageKey`.
## Хранение в БД
Основная таблица:
- `signed_messages_v2`
Для контентных DM в ней используются:
- `message_key`
- `base_key`
- `target_login`
- `from_login`
- `to_login`
- `time_ms`
- `nonce`
- `message_type`
- `revision_time_ms`
- `raw_block`
- `created_at_ms`
Отдельных таблиц файлов для DM сейчас нет.
## События и доставка
Запрос на отправку по WebSocket остаётся прежним:
- `SendMessagePair`
- `ReceiveOutcomingMessage` как алиас
Клиент отправляет:
- `incomingBlobB64`
- `outgoingBlobB64`
Событие в активные сессии:
- `SignedMessageArrived`
Если пришла новая ревизия того же сообщения, `messageKey` остаётся прежним, а внутри `blobB64` будет более новый `revisionTimeMs`.
Подтверждение доставки в сессию:
- `AckSessionDelivery`
WebPush и локальные уведомления сейчас работают так:
- для активной онлайн-сессии приоритет у доставки по WebSocket через `SignedMessageArrived`;
- если целевая сессия не онлайн по WebSocket, сервер может отправить WebPush с `kind=new_message`;
- если вкладка/приложение живы, но страница скрыта (`document.visibilityState !== visible`), UI дополнительно пытается показать системное уведомление через `service worker`;
- для активной видимой страницы UI проигрывает короткий локальный сигнал на каждое новое входящее DM, если браузер ранее разрешил аудио-контекст после пользовательского жеста;
- для скрытой, но живой страницы UI также делает `best effort` сигнал через `vibrate()` и более длинный локальный звук;
- эти локальные сигналы не гарантируются браузером: на мобильных устройствах они зависят от политики Chrome/Android/iOS.
## Правила UI
UI сейчас работает так:
- показывает только текст `encryptedBody`;
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
- не показывает удалённые сообщения;
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
- на видимом экране чата/приложения проигрывает короткий локальный звук на новое входящее DM;
- при входящем DM для скрытой, но ещё живой страницы пытается поднять системное уведомление через `service worker`;
- не показывает и не принимает вложения.
## Что обязательно помнить
- вложения в 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

@ -0,0 +1,482 @@
# Solana user_pda: итоговый целевой формат пользовательской записи
Документ описывает целевой формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `Dev_Docs/Blockchain/`.
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
## 1. Назначение user_pda
`user_pda` хранит публичное состояние пользователя в Solana:
- логин пользователя;
- неизменяемые параметры создания записи;
- публичный recovery-ключ пользователя;
- корневой публичный ключ пользователя;
- клиентский публичный ключ пользователя;
- данные одного или нескольких пользовательских блокчейнов SHiNE;
- серверные данные пользователя, если пользователь выступает сервером;
- серверы доступа пользователя;
- счетчики/лимиты;
- подпись записи.
На первом этапе поддерживается один пользовательский блокчейн SHiNE, но формат блока блокчейна сразу допускает повторение таких блоков в будущем.
## 2. Адрес PDA
Адрес пользовательской PDA вычисляется по логину:
- seed prefix: `user_login=`;
- второй seed: нормализованный логин в нижнем регистре;
- program id: программа `shine_users`.
Один логин соответствует одной `user_pda`.
## 2.1. Кто оплачивает create/update PDA
- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `client_key`.
- `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer.
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `client_key`.
## 3. Общие правила кодирования
- Числа кодируются в Little Endian.
- `u8`, `u16`, `u32`, `u64` имеют обычный фиксированный размер.
- Публичный ключ Solana/Ed25519: 32 байта.
- Ed25519-подпись: 64 байта.
- SHA-256/Solana hash: 32 байта.
- Строка переменной длины: `len: u8` + `bytes[len]` в UTF-8.
- Arweave `tx_id`: строка переменной длины. Ожидаемая практическая длина base64url tx id - 43 байта, но формат хранит длину явно.
- Все типизированные блоки после фиксированного заголовка начинаются с `block_type: u8` и `block_version: u8`.
- Отдельный `block_len` у типизированных блоков не хранится: блоки парсятся по известным полям, счетчикам и строкам с `len: u8`.
## 4. Верхний формат записи
Первые 9 полей фиксированы и идут строго в указанном порядке. Это общий заголовок записи.
| N | Поле | Тип | Размер | Правило |
|---|------|-----|--------|---------|
| 1 | `magic` | bytes | 5 | Всегда `SHiNE`. |
| 2 | `format_major` | `u8` | 1 | Для первого формата: `1`. |
| 3 | `format_minor` | `u8` | 1 | Для первой версии нового формата: `0`. |
| 4 | `record_len` | `u16` | 2 | Длина полезной записи от `magic` до `signature` включительно, без padding. |
| 5 | `created_at_ms` | `u64` | 8 | Время создания записи, Unix time в миллисекундах. Не меняется. |
| 6 | `updated_at_ms` | `u64` | 8 | Время последнего обновления записи. |
| 7 | `record_number` | `u32` | 4 | Номер версии записи пользователя. При создании `0`, при обновлении +1. |
| 8 | `prev_record_hash` | bytes | 32 | Хэш unsigned-части предыдущей записи. При создании 32 нулевых байта. |
| 9 | `login` | string | `1 + len` | Логин пользователя. Не меняется. |
После первых 9 полей идет набор типизированных блоков:
```text
UserPdaRecordV1
- fixed_header: поля 1..9
- blocks_count: u8
- blocks: TypedBlock[blocks_count]
- signature: [u8; 64]
- padding: bytes до размера PDA, если нужен
```
`blocks_count` входит в unsigned-часть записи и подписывается.
## 5. Типы блоков
Зарезервированные значения `block_type`:
| block_type | Блок | Назначение |
|------------|------|------------|
| `0` | `RecoveryKeyBlock` | Ключ восстановления пользователя. |
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
| `2` | `ClientKeyBlock` | Клиентский ключ пользователя. |
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и homeserver-ы. |
| `70` | `TrustedStateBlock` | Счетчик trusted-связей. |
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
Правила:
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
- обязательные блоки: `RecoveryKeyBlock`, `RootKeyBlock`, `ClientKeyBlock`, `BlockchainRegistryBlock`;
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`;
- каждый обязательный блок должен встречаться ровно один раз;
- порядок блоков в записи фиксируется для простоты проверки:
`RecoveryKey`, `RootKey`, `ClientKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`.
## 6. RecoveryKeyBlock
Recovery-ключ нужен для будущих сценариев восстановления и ротации остальных ключей. В текущей версии он только публикуется в записи и не меняется через обычный `update_user_pda`.
```text
RecoveryKeyBlock
- block_type: u8 = 0
- block_version: u8 = 0
- recovery_key: [u8; 32]
```
Правила:
- при создании задается публичный recovery-ключ пользователя;
- при обновлении `recovery_key` должен совпадать с предыдущей записью;
- приватный `recovery.key` в PDA не хранится;
- отдельная ротация recovery-ключа будет отдельным форматом/сценарием в будущем.
## 7. RootKeyBlock
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
```text
RootKeyBlock
- block_type: u8 = 1
- block_version: u8 = 0
- root_key: [u8; 32]
```
Правила:
- при создании задается корневой публичный ключ пользователя;
- при обновлении `root_key` должен совпадать с предыдущей записью;
- ротация root-key будет отдельным форматом/сценарием в будущем.
## 8. ClientKeyBlock
Смена `client_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один клиентский ключ пользователя.
```text
ClientKeyBlock
- block_type: u8 = 2
- block_version: u8 = 0
- client_key: [u8; 32]
```
Правила:
- при создании задается текущий клиентский публичный ключ пользователя;
- при обновлении `client_key` должен совпадать с предыдущей записью;
- история устройств и несколько клиентских ключей в этом формате не хранятся.
## 9. BlockchainRegistryBlock
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
```text
BlockchainRegistryBlock
- block_type: u8 = 3
- block_version: u8 = 0
- blockchain_count: u8
- blockchain_records: BlockchainRecord[blockchain_count]
```
Правила:
- на первом этапе `blockchain_count = 1`;
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
## 10. BlockchainRecord
```text
BlockchainRecord
- blockchain_type: u8
- blockchain_name: string
- blockchain_public_key: [u8; 32]
- paid_limit_bytes: u64
- used_bytes: u64
- last_block_number: u32
- last_block_hash: [u8; 32]
- last_block_signature: [u8; 64]
- arweave_present: u8
- arweave_tx_id: string, только если arweave_present = 1
```
`blockchain_type`:
| Значение | Смысл |
|----------|-------|
| `1` | Основной пользовательский SHiNE-блокчейн. |
Поля:
- `blockchain_name` - строковое имя пользовательского блокчейна, например `login-001`. На первом этапе для основного блокчейна пользователя используется имя вида `<login>-001`, потому что это первый блокчейн этого пользователя.
- `blockchain_public_key` - публичный ключ блокчейна пользователя.
- `paid_limit_bytes` - оплаченный лимит хранения/записей в байтах.
- `used_bytes` - сколько байт уже занято в пользовательском SHiNE-блокчейне.
- `last_block_number` - номер последнего известного блока пользовательского блокчейна.
- `last_block_hash` - хэш последнего известного блока.
- `last_block_signature` - подпись хэша специального сообщения о вершине блокчейна ключом `blockchain_public_key`.
- `arweave_present` - `0`, если ссылки нет; `1`, если ссылка есть.
- `arweave_tx_id` - Arweave transaction id, где лежит выгруженный пользовательский канал/состояние.
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
## 11. Правила обновления BlockchainRecord
При обновлении записи:
- `blockchain_type` для существующей записи не меняется;
- `blockchain_public_key` пока не ротируется автоматически; смена ключа требует отдельного согласованного сценария;
- `paid_limit_bytes` может только увеличиваться или оставаться прежним;
- при увеличении `paid_limit_bytes` пользователь платит комиссию в Solana по тарифам программы;
- `used_bytes` может только увеличиваться или оставаться прежним;
- `last_block_number` может только увеличиваться или оставаться прежним;
- `used_bytes <= paid_limit_bytes`;
- если `last_block_number` увеличился, то должны быть переданы новый `last_block_hash` и новая `last_block_signature`;
- `last_block_signature` проверяется через Ed25519-инструкцию Solana: подпись должна соответствовать хэшу сообщения `LastBlockState` и `blockchain_public_key`;
- в транзакции `create_user_pda` / `update_user_pda` две Ed25519-инструкции должны идти непосредственно перед вызовом `shine_users`: сначала подпись `root_key`, затем подпись `blockchain_public_key`;
- `arweave_tx_id` можно добавить или заменить на новый, если пользователь выгрузил более актуальное состояние в Arweave;
- уменьшать лимит, число блоков или занятый размер нельзя.
Сообщение `LastBlockState`, которое хэшируется и подписывается ключом `blockchain_public_key`:
```text
LastBlockState
- constant: bytes = "SHiNE_LAST_BLOCK"
- login: string
- blockchain_name: string
- last_block_number: u32
- last_block_hash: [u8; 32]
- used_bytes: u64
```
Алгоритм:
```text
message = SHA-256(LastBlockState bytes)
last_block_signature = Ed25519(blockchain_public_key, message)
```
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
## 12. ServerProfileBlock
Блок присутствует, если пользователь выступает сервером.
```text
ServerProfileBlock
- block_type: u8 = 30
- block_version: u8 = 0
- is_server: u8
- address_format_type: u8, только если is_server = 1
- address_format_version: u8, только если is_server = 1
- server_address: string, только если is_server = 1
- sync_servers_count: u8, только если is_server = 1
- sync_servers: string[sync_servers_count], только если is_server = 1
```
Правила:
- `is_server = 0` означает, что серверных данных нет;
- `is_server = 1` означает, что пользователь публикует серверный профиль;
- `address_format_type` — тип формата адреса сервера: `1` = URL-строка (например `https://shineup.me/ws`);
- `address_format_version` — версия формата адреса, сейчас `0`;
- `sync_servers_count` максимум `32`;
- `server_address` - строковый адрес сервера в соответствии с `address_format_type`;
- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
## 13. AccessServersBlock
Блок хранит серверы доступа/relay для пользователя.
```text
AccessServersBlock
- block_type: u8 = 40
- block_version: u8 = 0
- access_servers_count: u8
- access_servers: string[access_servers_count]
```
Правила:
- блок может отсутствовать, если серверы доступа не заданы;
- список может обновляться при изменении маршрутизации пользователя;
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
## 14. SessionsBlock
Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком.
```text
SessionsBlock
- block_type: u8 = 50
- block_version: u8 = 0
- sessions_mode: u8
- sessions_count: u8
- sessions: SessionRecord[sessions_count]
```
`sessions_mode`:
| Значение | Смысл |
|----------|-------|
| `1` | Можно использовать и сессии, зарегистрированные в PDA, и сессии, созданные вне PDA. |
| `10` | Зарезервировано на будущее: можно использовать только сессии, опубликованные в PDA. |
Сейчас рабочий режим по умолчанию: `sessions_mode = 1`. Серверная логика пока не реализует особое поведение для `10`; это задел под будущее расширение.
```text
SessionRecord
- session_type: u8
- session_version: u8
- session_name: string
- session_pub_key: [u8; 32]
```
`session_type`:
| Значение | Смысл |
|----------|-------|
| `1` | Обычная пользовательская сессия. |
| `50` | Кошелёк пользователя. |
| `100` | Homeserver пользователя. |
Правила:
- максимум `64` записей на пользователя;
- `session_name` не пустой, максимум `64` байта;
- `session_name` может содержать только символы `[A-Za-z0-9_]`;
- `session_version` сейчас должна быть равна `1`;
- внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`;
- на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически.
## 15. TrustedStateBlock
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
```text
TrustedStateBlock
- block_type: u8 = 70
- block_version: u8 = 0
- trusted_count: u8 = 0
```
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
## 16. Подпись user_pda
Подписывается не вся PDA целиком, а unsigned-часть записи:
- от `magic` до последнего байта последнего типизированного блока включительно;
- включая `record_len`, `blocks_count`, все заголовки блоков и тела блоков;
- без поля `signature`;
- без padding.
Алгоритм:
```text
message = hash(unsigned_record_bytes)
signature = Ed25519(root_key, message)
```
Solana-программа проверяет подпись через встроенную Ed25519-инструкцию. Подписантом должен быть `root_key` из `RootKeyBlock`.
Для `shine_users` эта инструкция должна стоять в транзакции сразу перед Ed25519-инструкцией `last_block_signature` и непосредственно перед самой `create/update`-инструкцией программы.
Смену формата подписи сейчас не трогаем.
## 17. Регистрация пользователя
При регистрации:
- PDA еще не должна существовать;
- логин проходит проверку формата и login guard;
- `record_number = 0`;
- `prev_record_hash = 0x00...00`;
- `created_at_ms = updated_at_ms`;
- обязательные блоки присутствуют;
- создается минимум один `BlockchainRecord`;
- новый `SessionsBlock` может присутствовать, но при обычной регистрации сейчас записывается пустой список с `sessions_mode = 1`;
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
- `used_bytes <= paid_limit_bytes`;
- пользователь платит регистрационную комиссию;
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
- вся unsigned-часть записи подписана `root_key`.
## 18. Обновление пользователя
При обновлении:
- PDA должна существовать;
- `login`, `created_at_ms`, `recovery_key`, `root_key`, `client_key` не меняются;
- `record_number = previous_record_number + 1`;
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
- `updated_at_ms` обновляется;
- unsigned-часть новой записи подписана `root_key`;
- лимиты блокчейнов могут только увеличиваться;
- занятый размер и номер последнего блока не могут уменьшаться;
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
## 19. Отличия от старого линейного формата
Старый формат после `login` хранил поля линейно:
- `root_key_status`;
- `root_key`;
- `blockchain_key_status`;
- `blockchain_key`;
- `client_key_status`;
- `client_key`;
- `chain_number`;
- `balance`;
- серверные поля;
- access-серверы;
- `trusted_count`;
- `reserved`;
- `signature`.
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
- recovery-ключ становится отдельным обязательным блоком;
- ключи становятся отдельными блоками;
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
- серверные данные и access-серверы отделяются от данных блокчейна;
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
## 20. Деривация ключей из master secret
Сама Solana-программа не вычисляет ключи из секрета и не хранит приватные ключи. Но текущая согласованная клиентская схема деривации для публичной версии формата фиксируется здесь как reference для UI/ESP32/внешних клиентов.
Базовая формула:
```text
seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8)
```
Где:
- `master_secret32` — 32-байтовый master secret пользователя;
- `suffix_utf8` — строка назначения ключа.
Согласованные suffix:
```text
"recovery.key"
"root.key"
"blockchain.key"
"client.key"
```
Соответствие:
```text
recovery.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "recovery.key")
root.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "root.key")
blockchain.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "blockchain.key")
client.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "client.key")
```
Далее каждая строка `seed` интерпретируется off-chain как `seed32` для отдельной пары Ed25519.
## 21. Что пока не входит в формат
Пока не проектируем:
- ротацию `recovery_key`;
- ротацию `root_key`;
- сложную ротацию `client_key`;
- ротацию `blockchain_public_key`;
- проверку содержимого Arweave transaction;
- хранение полной истории пользовательского блокчейна внутри Solana;
- подключение Solana-модуля к сборке/деплою основного сервера SHiNE.

View File

@ -0,0 +1,166 @@
# Архитектура Solana-программ SHiNE
Документ описывает рабочую архитектуру Solana-части SHiNE: три Anchor-программы, DAO, ключи управления, PDA-счета и движение денег.
Это архитектурная справка. Она не меняет код, формат PDA-записи пользователя, серверный API или формат блокчейна SHiNE.
Статус: актуализировано по коду `shine-solana/shine/programs/*` на 2026-05-25.
Связанные документы:
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` — single source of truth по деплою и первичной инициализации регистрации пользователей.
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` — точный формат `user_pda` для `shine_users`.
- `shine-solana/shine/doc/FUNDS_FLOW.md` — короткая справка по денежным потокам внутри Solana-модуля.
## Кратко
В Solana-модуле сейчас три основные программы:
1. `shine_login_guard` — проверяет логин и возвращает класс логина: обычный, premium или trademark.
2. `shine_users` — создает и обновляет пользовательскую PDA-запись, проверяет подписи и берет оплату за регистрацию/увеличение лимита.
3. `shine_payments` — принимает входящий поток средств в `inflow_vault`, ведет очереди тикетов, позволяет DAO выдавать лимиты менеджерам и выполняет выплаты.
DAO в текущем виде не является отдельной Anchor-программой SHiNE внутри `programs/`. Это управляющая модель поверх кошельков, governance-скриптов и authority-адресов. Для проектирования ее удобно считать отдельным управляющим блоком: DAO голосует, назначает управляющие ключи, управляет казной и вызывает защищенные методы второй и третьей программ.
## Общая схема
Редактируемая Mermaid-схема находится в [schemes/architecture.mmd](schemes/architecture.mmd).
Картинки:
- [schemes/architecture.svg](schemes/architecture.svg)
- [schemes/architecture.png](schemes/architecture.png)
## Программы и функции
| Блок | Папка/имя | Текущие функции из кода | Основной смысл |
| --- | --- | --- | --- |
| 1 | `shine_login_guard` | `classify_login` | Проверка логина перед регистрацией. |
| 2 | `shine_users` | `init_users_economy_config`, `update_users_economy_config`, `create_user_pda`, `update_user_pda` | Регистрация пользователя, обновление записи, экономика лимита. |
| 3 | `shine_payments` | `init`, `update_coef_limit`, `grant_manager_limits`, `buy_ticket`, `buy_ticket_usd`, `buy_ticket_sol`, `manager_add_ticket`, `step_payout`, `change_ticket_recipient` | Vault, билеты, очереди, выплаты, DAO-настройки, лимиты менеджеров. |
| DAO | governance/authority | Вызовы через governance и управляющие ключи | Управление правами, казной, настройками и будущими обновлениями программ. |
## Актуальные program id
Актуальные адреса заданы одновременно в `Anchor.toml`, `declare_id!` программ и `programs/common/src/deploy_config.rs`:
| Программа | Program ID |
| --- | --- |
| `shine_login_guard` | `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` |
| `shine_users` | `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` |
| `shine_payments` | `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW` |
Если эти адреса меняются, нужно синхронно обновить:
1. `shine-solana/shine/Anchor.toml`
2. `declare_id!` в `programs/*/src/lib.rs`
3. `programs/common/src/deploy_config.rs`
4. UI/серверные константы, перечисленные в `Dev_Docs/Инициализация_Solana_регистрации/README.md`
## Ключи и authority
Для удобного понимания на старте можно считать, что есть четыре группы ключей:
1. `key_1` / authority программы `shine_login_guard`.
- Сейчас программа только классифицирует логин.
- На первом этапе ее можно оставить под отдельным ключом.
- В будущем право обновления можно передать DAO.
2. `key_2` / authority программы `shine_users`.
- Отвечает за деплой/upgrade второй программы.
- Защищенное обновление economy-конфига в коде уже проверяет `DAO_AUTHORITY`.
- В целевой модели upgrade-authority второй программы нужно передать DAO.
3. `key_3` / authority программы `shine_payments`.
- Отвечает за деплой/upgrade третьей программы.
- Защищенные методы `update_coef_limit` и `grant_manager_limits` проверяют `dao_wallet` из `ConfigState`.
- В целевой модели upgrade-authority третьей программы нужно передать DAO.
4. DAO-ключи.
- Это управляющие кошельки/токены/realm governance.
- DAO может добавлять и отзывать управляющие ключи по голосованию.
- DAO-казна получает деньги от покупки тикетов и DAO-часть выплат из `inflow_vault`.
Адреса program id сейчас берутся из `programs/common/src/deploy_config.rs`. Для production/devnet можно подбирать vanity-адреса с понятным началом вроде `SHi...`, но это отдельная операция генерации ключей и деплоя.
## Счета и PDA
Постоянные PDA и счета:
1. `shine_users`
- `user_pda` — пользовательская запись по seed `login=<login>`, создается для каждого логина.
- `users_economy_config_pda` — общие параметры экономики регистрации и лимита.
2. `shine_payments`
- `config_pda` — хранит `dao_wallet` и адрес `inflow_vault`.
- `coef_limit_pda` — хранит коэффициент выплат, лимит очереди и награду вызывающему `step_payout`.
- `queues_pda` — агрегаты очередей выплат.
- `inflow_vault_pda` — PDA-вольт, куда `shine_users` переводит оплату регистрации и увеличения лимита.
- `ticket_pda` — отдельная PDA-запись тикета на каждую покупку/менеджерскую выдачу.
- `manager_allowance_pda` — PDA лимитов конкретного менеджера.
3. DAO
- `dao_wallet` / treasury — казна DAO.
- governance-аккаунты DAO — realm, governance, proposal/vote records и связанные аккаунты SPL Governance, если используется эта модель.
## Правило разделения с основным сервером
Solana-модуль лежит в основном репозитории как отдельная папка `shine-solana/shine/`, но не подключается автоматически к сборке или деплою основного сервера SHiNE. Команды `deployServer` и `deployUI` не должны деплоить Anchor-программы. Solana build/deploy выполняется отдельно из папки `shine-solana/shine/` по локальным правилам модуля.
## Движение денег
Основные потоки:
1. Регистрация пользователя через `shine_users::create_user_pda`.
- Платит `signer`.
- Деньги идут в `shine_payments::inflow_vault_pda`.
- Сумма состоит из регистрационной комиссии и оплаты дополнительного лимита.
2. Увеличение лимита через `shine_users::update_user_pda`.
- Платит `signer`.
- Деньги идут в тот же `inflow_vault_pda`.
- Сумма равна оплате дополнительного лимита.
3. Покупка тикета через `shine_payments::buy_ticket*`.
- Платит покупатель.
- Деньги сразу идут в `dao_wallet`.
- Одновременно создается тикет на выплату.
4. Выплата через `shine_payments::step_payout`.
- Вызвать может любой подписант.
- Деньги берутся из `inflow_vault_pda`.
- Часть идет получателю тикета.
- Часть идет в `dao_wallet`.
- Небольшая награда идет вызвавшему шаг выплат.
- Если очереди пустые, весь доступный остаток `inflow_vault_pda` переводится в DAO.
## Передача прав DAO
Минимальная целевая модель:
1. `shine_login_guard`
- Пока оставить на отдельном ключе `key_1`.
- Передачу DAO сделать позже, когда логика premium/trademark стабилизируется.
2. `shine_users`
- Economy-настройки уже должны обновляться DAO-authority.
- Upgrade-authority программы после проверки можно передать DAO.
3. `shine_payments`
- DAO уже управляет настройками выплат и лимитами менеджеров через `dao_wallet`.
- Upgrade-authority программы после проверки можно передать DAO.
4. DAO
- Управляет казной.
- Принимает решения голосованием.
- Добавляет/отзывает управляющие ключи.
- Вызывает защищенные методы второй и третьей программ.
- В будущем может принять управление первой программой.
## Детальные файлы
- [details/shine_login_guard.md](details/shine_login_guard.md)
- [details/shine_users.md](details/shine_users.md)
- [details/shine_payments.md](details/shine_payments.md)
- [details/shine_dao.md](details/shine_dao.md)
- [details/accounts_and_money_flow.md](details/accounts_and_money_flow.md)

View File

@ -0,0 +1,110 @@
# Счета, ключи и движение денег
## Кратко
В архитектуре есть три типа объектов:
1. Ключи программ и DAO.
2. PDA-счета состояния.
3. Денежные счета, через которые проходят SOL/lamports.
## Ключи
Минимальный набор для понимания:
1. `key_1` — deploy/upgrade authority `shine_login_guard`.
2. `key_2` — deploy/upgrade authority `shine_users`.
3. `key_3` — deploy/upgrade authority `shine_payments`.
4. `DAO_AUTHORITY` — адрес, который имеет право менять защищенные настройки.
5. `DAO_TREASURY_WALLET` / `dao_wallet` — казна DAO.
6. `manager_wallet` — кошелек менеджера, которому DAO выдает лимиты на создание тикетов.
7. `user root_key` — корневой ключ пользователя для подписи пользовательской записи.
8. `user client_key` — ключ устройства пользователя.
9. `server_key` — ключ сервера пользователя, если пользователь является сервером.
Текущие адреса из `programs/common/src/deploy_config.rs`:
| Роль | Адрес |
| --- | --- |
| `SHINE_LOGIN_GUARD_PROGRAM_ID` | `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` |
| `SHINE_USERS_PROGRAM_ID` | `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` |
| `SHINE_PAYMENTS_PROGRAM_ID` | `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW` |
| `DAO_AUTHORITY` | `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P` |
| `DAO_TREASURY_WALLET` | `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P` |
## Постоянные PDA
`shine_users`:
- `user_pda` — создается для каждого логина, seed `login=` + normalized login.
- `users_economy_config_pda` — один PDA с экономикой регистрации, seed `shine_users_economy_config`.
`shine_payments`:
- `config_pda` — один PDA конфига, seed `shine_payments_config`.
- `coef_limit_pda` — один PDA коэффициента/лимита/награды, seed `shine_payments_coef_limit`.
- `queues_pda` — один PDA агрегатов очередей, seed `shine_payments_queues`.
- `inflow_vault_pda` — один PDA-вольт входящих средств, seed `shine_payments_inflow_vault`.
- `ticket_pda` — много PDA, по одному на тикет, seed `shine_payments_q1_ticket` или `shine_payments_q2_ticket` + индекс.
- `manager_allowance_pda` — много PDA, по одному на менеджера, seed `shine_p_manager_allow` + адрес менеджера.
## Денежные потоки
### Регистрация
```text
user signer -> shine_users::create_user_pda -> shine_payments::inflow_vault_pda
```
Состав платежа:
- регистрационная комиссия;
- оплата `additional_limit`.
### Увеличение лимита
```text
user signer -> shine_users::update_user_pda -> shine_payments::inflow_vault_pda
```
Состав платежа:
- только оплата `additional_limit`.
### Покупка тикета
```text
buyer signer -> shine_payments::buy_ticket* -> dao_wallet
```
При этом создается `ticket_pda`, но деньги в `inflow_vault_pda` на этом шаге не идут.
### Выплата
```text
shine_payments::inflow_vault_pda -> ticket_recipient_wallet
shine_payments::inflow_vault_pda -> dao_wallet
shine_payments::inflow_vault_pda -> step_payout caller
```
Если очереди пустые:
```text
shine_payments::inflow_vault_pda -> dao_wallet
```
## Что нужно создать на старте
Минимально:
1. Три program id для `shine_login_guard`, `shine_users`, `shine_payments`.
2. Три upgrade-authority ключа или один временный deploy-ключ с четким планом передачи прав.
3. DAO authority/treasury.
4. `users_economy_config_pda`.
5. `shine_payments` PDA: `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
Динамически будут создаваться:
- `user_pda` на каждого пользователя;
- `ticket_pda` на каждый тикет;
- `manager_allowance_pda` на каждого менеджера.

View File

@ -0,0 +1,74 @@
# SHiNE DAO
## Кратко
DAO — управляющий слой Solana-части SHiNE. В текущем коде это не отдельная Anchor-программа в `programs/`, а модель управления через DAO-кошелек, DAO-authority, governance-скрипты и будущую передачу upgrade-authority программ.
## Что DAO должно уметь
1. Управлять казной.
- Принимать средства на `dao_wallet`.
- Выплачивать средства со счета DAO по решениям голосования.
2. Управлять настройками `shine_users`.
- Обновлять регистрационную комиссию.
- Обновлять цену шага лимита.
- Обновлять стартовый бонус лимита.
3. Управлять настройками `shine_payments`.
- Обновлять коэффициент выплат.
- Обновлять лимит очереди.
- Обновлять награду за вызов `step_payout`.
4. Управлять менеджерами.
- Выдавать менеджеру лимит на добавление тикетов.
- Отдельно учитывать лимиты Q1 и Q2.
5. Управлять правами программ.
- Принять upgrade-authority `shine_users`.
- Принять upgrade-authority `shine_payments`.
- Позже принять upgrade-authority `shine_login_guard`, если это потребуется.
6. Управлять ключами DAO.
- Добавлять управляющие ключи.
- Отзывать или сжигать управляющие ключи.
- Делать это через голосование, а не вручную одним админом.
7. Фиксировать решения.
- Делать заявления/решения через governance-механику.
- Привязывать важные изменения к proposal/vote/execute.
## Текущие адреса управления
В общем deploy-конфиге сейчас есть два важных адреса:
- `DAO_AUTHORITY` — используется `shine_users` для проверки права менять economy-конфиг.
- `DAO_TREASURY_WALLET` — используется `shine_payments` как `dao_wallet`.
Сейчас они могут совпадать. В целевой DAO-модели их лучше рассматривать как разные роли:
- authority/governance signer — кто имеет право исполнять управленческие инструкции;
- treasury wallet — счет, куда приходят деньги DAO.
## Передача прав
Рекомендуемый порядок:
1. Сначала стабилизировать и проверить `shine_users` и `shine_payments`.
2. Передать DAO право обновлять настройки, если оно еще не передано.
3. Передать DAO upgrade-authority второй и третьей программ.
4. Оставить `shine_login_guard` на отдельном ключе до стабилизации словарей и правил логинов.
5. После стабилизации решить отдельным голосованием, передавать ли первую программу DAO.
## Важное разделение
Есть два разных типа прав:
1. Право вызвать защищенную функцию программы.
- Например, `update_coef_limit` или `grant_manager_limits`.
- Проверяется внутри программы по `dao_wallet` или `DAO_AUTHORITY`.
2. Право обновить саму программу.
- Это upgrade-authority Solana ProgramData.
- Оно передается отдельной Solana-командой/DAO-транзакцией и не равно обычному PDA-счету.

View File

@ -0,0 +1,58 @@
# `shine_login_guard`
## Кратко
`shine_login_guard` — первая программа Solana-модуля SHiNE. Она проверяет логин перед регистрацией пользователя и возвращает класс логина.
Папка программы: `shine-solana/shine/programs/shine_login_guard/`.
## Текущая функция
1. `classify_login(login: String)`
- Нормализует логин.
- Проверяет длину и допустимые символы.
- Сравнивает части логина со словарями premium/trademark.
- Возвращает результат через `set_return_data`.
Классы результата:
- `0` — обычный логин, регистрацию можно продолжать.
- `1` — premium-логин.
- `2` — trademark-логин, нужна отдельная проверка/разрешение.
## Правила нормализации и классификации
Текущая логика из `programs/shine_login_guard/src/lib.rs`:
- пустой логин или логин длиннее 20 символов получает класс `premium`;
- `_` при нормализации удаляется;
- допустимы только ASCII-буквы и цифры, остальные символы дают класс `premium`;
- после удаления `_` результат приводится к нижнему регистру;
- логины длиной 7 символов или меньше считаются `premium`;
- логин разбивается максимум на 3 словарных фрагмента;
- если среди найденных фрагментов есть trademark-слово, результат `trademark`;
- если найдены только premium-слова, результат `premium`;
- если разбиение по словарям не найдено, результат `free`.
Словари собираются на этапе build из файлов:
- `programs/shine_login_guard/src/dictionaries/premium/*.txt`
- `programs/shine_login_guard/src/dictionaries/trademarks/*.txt`
## Роль в общей схеме
`shine_users::create_user_pda` вызывает `shine_login_guard` через CPI и продолжает регистрацию только если логин получил класс `0`.
## Ключи и управление
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_1`.
Текущая рекомендация:
- пока оставить `shine_login_guard` под отдельным ключом;
- не передавать ее DAO до стабилизации правил premium/trademark;
- позже можно передать upgrade-authority DAO, чтобы изменения словарей и правил проходили через голосование.
## Счета
Собственных постоянных PDA-счетов у программы сейчас нет. Для проверки нужен только подписант транзакции в `ClassifyLogin`.

View File

@ -0,0 +1,173 @@
# `shine_payments`
## Кратко
`shine_payments` — третья программа Solana-модуля SHiNE. Она отвечает за vault входящих средств, DAO-казну, покупку тикетов, менеджерские лимиты, очереди выплат и пошаговое исполнение выплат.
Папка программы: `shine-solana/shine/programs/shine_payments/`.
## Текущие функции
1. `init`
- Создает основные PDA: `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
- Записывает `dao_wallet` и стартовые параметры выплат.
2. `update_coef_limit`
- Обновляет коэффициент выплаты, лимит очереди и награду вызвавшему `step_payout`.
- Требует подпись DAO-кошелька из `ConfigState`.
3. `grant_manager_limits`
- DAO выдает менеджеру лимиты на создание тикетов в очередях Q1/Q2.
- Создает или обновляет `manager_allowance_pda`.
4. `buy_ticket`
- Покупка тикета с суммой в lamports, пересчетом через Pyth SOL/USD.
5. `buy_ticket_usd`
- Покупка тикета от USD-центов с защитой по максимальному платежу в lamports.
6. `buy_ticket_sol`
- Покупка тикета в lamports с проверкой минимального ожидаемого USD-эквивалента.
7. `manager_add_ticket`
- Менеджер создает тикет за счет выданного ему DAO-лимита.
8. `step_payout`
- Любой подписант может вызвать шаг выплат.
- Программа выплачивает следующий тикет, DAO-часть и награду вызывающему.
9. `change_ticket_recipient`
- Текущий получатель тикета может поменять адрес получателя, если тикет еще не следующий на выплату.
## Аргументы инструкций
`init` аргументов не принимает.
`update_coef_limit`:
- `coef_ppm: u64`
- `limit_usd_cents: u64`
- `call_reward_lamports: u64`
`grant_manager_limits`:
- `manager_wallet: Pubkey`
- `add_q1_usd_cents: u64`
- `add_q2_usd_cents: u64`
`buy_ticket`:
- `amount_lamports: u64`
- `recipient_wallet: Pubkey`
`buy_ticket_usd`:
- `amount_usd_cents: u64`
- `max_pay_lamports: u64`
- `recipient_wallet: Pubkey`
`buy_ticket_sol`:
- `amount_lamports: u64`
- `min_expected_usd_cents: u64`
- `recipient_wallet: Pubkey`
`manager_add_ticket`:
- `queue_id: u8` — только `1` или `2`
- `recipient_wallet: Pubkey`
- `payout_usd_cents: u64`
`change_ticket_recipient`:
- `new_recipient_wallet: Pubkey`
## Главные PDA
1. `config_pda`
- Seed: `shine_payments_config`.
- Хранит `dao_wallet` и `inflow_vault`.
- Размер PDA: `8 + 160` байт.
2. `coef_limit_pda`
- Seed: `shine_payments_coef_limit`.
- Хранит коэффициент выплат, лимит и награду `step_payout`.
- Размер PDA: `8 + 96` байт.
3. `queues_pda`
- Seed: `shine_payments_queues`.
- Хранит агрегаты очередей Q1/Q2.
- Размер PDA: `8 + 192` байт.
4. `inflow_vault_pda`
- Seed: `shine_payments_inflow_vault`.
- Принимает деньги от `shine_users`.
- Из него выполняются выплаты тикетам, DAO и вызывающему `step_payout`.
- Размер PDA: `8 + 32` байт.
5. `ticket_pda`
- Seed зависит от очереди и индекса тикета.
- Отдельная PDA-запись на каждый тикет.
- Q1 seed: `shine_payments_q1_ticket` + `ticket_index`.
- Q2 seed: `shine_payments_q2_ticket` + `ticket_index`.
- Размер PDA: `8 + 160` байт.
6. `manager_allowance_pda`
- Seed: `shine_p_manager_allow` + адрес менеджера.
- Хранит доступный лимит менеджера по Q1/Q2.
- Размер PDA: `8 + 128` байт.
## Текущие параметры
Параметры initial config из `programs/shine_payments/src/settings.rs`:
| Поле | Значение | Смысл |
| --- | --- | --- |
| `START_COEF_PPM` | `5_000_000` | коэффициент 5.0x в ppm-масштабе |
| `START_LIMIT_USD_CENTS` | `1_000_000` | стартовый лимит Q1: 10_000 USD |
| `START_CALL_REWARD_LAMPORTS` | `8_000_000` | награда вызвавшему `step_payout`, 0.008 SOL |
| `MAX_CALL_REWARD_LAMPORTS` | `10_000_000` | максимум награды, 0.01 SOL |
| `ORACLE_MAX_AGE_SECS` | `120` | максимальный возраст цены Pyth |
Для расчетов используется Pyth SOL/USD:
- feed id: `0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d`
- price update account: `7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE`
## Деньги
Входы:
- из `shine_users` в `inflow_vault_pda` при регистрации и увеличении лимита;
- от покупателя тикета сразу в `dao_wallet` при `buy_ticket*`.
Выходы:
- из `inflow_vault_pda` получателю тикета;
- из `inflow_vault_pda` в `dao_wallet`;
- из `inflow_vault_pda` вызвавшему `step_payout`;
- если очереди пустые, весь доступный остаток `inflow_vault_pda` переводится в DAO.
## Очереди и выплаты
Выплаты идут строго пошагово:
- если есть невыплаченные Q1-тикеты, `step_payout` берет следующий Q1;
- если Q1 пустая, берется следующий Q2;
- для Q1 DAO-часть равна сумме тикета в USD;
- для Q2 DAO-часть равна двойной сумме тикета в USD;
- перед выплатой суммы пересчитываются из USD-центов в lamports по Pyth SOL/USD;
- если в `inflow_vault_pda` не хватает средств на тикет, DAO-часть и награду вызвавшему, шаг отклоняется.
`change_ticket_recipient` запрещает менять получателя у тикета, который является следующим на выплату.
## Ключи и управление
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_3`.
Целевая модель:
- `update_coef_limit` вызывает DAO;
- `grant_manager_limits` вызывает DAO;
- upgrade-authority программы после проверки передается DAO;
- `step_payout` остается открытым для любого подписанта, чтобы выплаты не зависели от одного оператора.

View File

@ -0,0 +1,136 @@
# `shine_users`
## Кратко
`shine_users` — вторая программа Solana-модуля SHiNE. Она отвечает за создание и обновление пользовательской PDA-записи, проверку подписи записи, проверку логина через `shine_login_guard` и оплату регистрации/дополнительного лимита.
Папка программы: `shine-solana/shine/programs/shine_users/`.
## Текущие функции
1. `init_users_economy_config`
- Создает PDA с экономическими настройками пользователей.
- Записывает стартовую регистрационную комиссию, цену шага лимита и стартовый бонус лимита.
2. `update_users_economy_config`
- Обновляет экономические настройки.
- Требует подпись `DAO_AUTHORITY` из общего deploy-конфига.
3. `create_user_pda`
- Проверяет логин через `shine_login_guard`.
- Проверяет структуру полей пользователя.
- Проверяет подпись записи root-ключом пользователя.
- Создает `user_pda` по seed `login=<normalized_login>`.
- Переводит оплату регистрации и дополнительного лимита в `shine_payments::inflow_vault_pda`.
4. `update_user_pda`
- Проверяет неизменяемые поля пользователя.
- Проверяет `prev_hash`, новую подпись и новое состояние последнего блока.
- При необходимости расширяет PDA.
- Переводит оплату дополнительного лимита в `shine_payments::inflow_vault_pda`.
## Аргументы инструкций
`init_users_economy_config` аргументов не принимает.
`update_users_economy_config`:
- `registration_fee_lamports: u64`
- `lamports_per_limit_step: u64`
- `start_bonus_limit: u64`
`create_user_pda`:
- `login: String`
- `root_key: Pubkey`
- `created_at_ms: u64`
- `additional_limit: u64`
- `fields: UserMutableFields`
- `signature: Vec<u8>`
`update_user_pda`:
- `login: String`
- `root_key: Pubkey`
- `created_at_ms: u64`
- `updated_at_ms: u64`
- `version: u32`
- `prev_hash: Vec<u8>`
- `additional_limit: u64`
- `fields: UserMutableFields`
- `signature: Vec<u8>`
`UserMutableFields`:
- `client_key: Pubkey`
- `blockchain_public_key: Pubkey`
- `blockchain_name: String`
- `used_bytes: u64`
- `last_block_number: u32`
- `last_block_hash: Vec<u8>` — ровно 32 байта
- `last_block_signature: Vec<u8>` — ровно 64 байта
- `arweave_tx_id: String`
- `is_server: bool`
- `server_key: Pubkey`
- `server_address: String`
- `sync_servers: Vec<String>`
- `access_servers: Vec<String>`
- `trusted_count: u8`
## Главные PDA
1. `user_pda`
- PDA записи пользователя.
- Seed: `login=<normalized_login>`.
- Создается отдельно для каждого логина.
- Стартовый размер: `768` байт.
- При обновлении может расширяться через `realloc`, но один auto-realloc ограничен `10_000` байт.
2. `users_economy_config_pda`
- PDA с настройками экономики.
- Seed: `shine_users_economy_config`.
- Хранит регистрационную комиссию, цену шага лимита и стартовый бонус.
- Размер PDA: `8 + 96` байт.
## Текущие параметры экономики
Параметры initial config из `programs/shine_users/src/settings.rs`:
| Поле | Значение | Смысл |
| --- | --- | --- |
| `START_REGISTRATION_FEE_LAMPORTS` | `10_000_000` | стартовая комиссия регистрации, 0.01 SOL |
| `LIMIT_STEP` | `10_000` | шаг `additional_limit` |
| `START_LAMPORTS_PER_LIMIT_STEP` | `100_000` | 0.0001 SOL за один шаг лимита |
| `START_BONUS_LIMIT` | `100_000` | стартовый бесплатный лимит при регистрации |
`additional_limit` в create/update должен быть кратен `LIMIT_STEP`.
## Связь с другими программами
`shine_users` зависит от:
- `shine_login_guard` — для проверки логина при создании пользователя;
- `shine_payments` — для вычисления и проверки `inflow_vault_pda`, куда уходят платежи.
`create_user_pda` делает CPI-вызов `shine_login_guard::classify_login` и принимает только результат `0`. Premium/trademark логины сейчас отклоняются ошибками `PremiumLogin` или `TrademarkLoginRequiresReview`.
Подпись `user_pda` и подпись состояния последнего блока проверяются через встроенную Solana Ed25519-инструкцию, которая должна идти раньше инструкции `shine_users` в той же транзакции.
## Деньги
Деньги из `shine_users` идут только в `inflow_vault_pda` программы `shine_payments`.
Потоки:
- `create_user_pda`: регистрационная комиссия + оплата `additional_limit`;
- `update_user_pda`: оплата `additional_limit`, если она больше нуля.
## Ключи и управление
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_2`.
Целевая модель:
- economy-настройки меняет DAO-authority;
- upgrade-authority программы после проверки передается DAO;
- пользовательские операции `create_user_pda` и `update_user_pda` остаются доступными обычным пользователям при корректных подписях и оплате.

View File

@ -0,0 +1,54 @@
flowchart LR
U[Пользователь / signer]
B[Покупатель тикета]
M[Менеджер]
C[Любой caller step_payout]
LG[1. shine_login_guard<br/>classify_login]
USERS[2. shine_users<br/>create_user_pda / update_user_pda]
PAY[3. shine_payments<br/>vault / tickets / payouts]
DAO[SHiNE DAO<br/>governance / authority / treasury]
USERPDA[(user_pda<br/>по login)]
ECON[(users_economy_config_pda)]
CONFIG[(config_pda)]
COEF[(coef_limit_pda)]
QUEUES[(queues_pda)]
VAULT[(inflow_vault_pda)]
TICKET[(ticket_pda)]
ALLOW[(manager_allowance_pda)]
U -->|логин| USERS
USERS -->|CPI проверка| LG
USERS -->|создает/обновляет| USERPDA
USERS -->|читает экономику| ECON
U -->|регистрация / лимит| VAULT
DAO -->|update economy| USERS
DAO -->|update coef/limit| PAY
DAO -->|grant manager limits| PAY
DAO -->|создает/отзывает ключи| DAO
PAY --> CONFIG
PAY --> COEF
PAY --> QUEUES
PAY --> VAULT
PAY --> TICKET
PAY --> ALLOW
B -->|buy_ticket*| PAY
B -->|оплата покупки тикета| DAO
PAY -->|создает тикет| TICKET
M -->|manager_add_ticket| PAY
ALLOW -->|лимиты Q1/Q2| M
C -->|step_payout| PAY
VAULT -->|выплата тикета| U
VAULT -->|DAO-часть| DAO
VAULT -->|call reward| C
DAO -. upgrade authority после передачи .-> USERS
DAO -. upgrade authority после передачи .-> PAY
DAO -. позже возможно .-> LG

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

View File

@ -0,0 +1,139 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="900" viewBox="0 0 1400 900" role="img" aria-labelledby="title desc">
<title id="title">Архитектура Solana-программ SHiNE</title>
<desc id="desc">Схема трех программ, DAO, PDA-счетов и движения денег.</desc>
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#2f3a45"/>
</marker>
<marker id="moneyArrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#0a7f62"/>
</marker>
<style>
.bg { fill: #f7f8fa; }
.title { font: 700 30px Arial, sans-serif; fill: #1f2933; }
.subtitle { font: 400 16px Arial, sans-serif; fill: #52606d; }
.box { fill: #ffffff; stroke: #9aa5b1; stroke-width: 2; rx: 8; }
.program { fill: #e8f1ff; stroke: #3465a4; }
.dao { fill: #fff3d6; stroke: #b7791f; }
.pda { fill: #edf7ed; stroke: #2f855a; }
.actor { fill: #f3e8ff; stroke: #805ad5; }
.txt { font: 700 17px Arial, sans-serif; fill: #1f2933; }
.small { font: 400 13px Arial, sans-serif; fill: #3e4c59; }
.line { stroke: #2f3a45; stroke-width: 2.2; fill: none; marker-end: url(#arrow); }
.money { stroke: #0a7f62; stroke-width: 3; fill: none; marker-end: url(#moneyArrow); }
.dashed { stroke-dasharray: 8 7; }
.legend { font: 400 14px Arial, sans-serif; fill: #3e4c59; }
</style>
</defs>
<rect class="bg" x="0" y="0" width="1400" height="900"/>
<text class="title" x="52" y="54">SHiNE Solana: программы, DAO, счета и движение денег</text>
<text class="subtitle" x="52" y="82">Текущая модель: три Anchor-программы, DAO/authority как управляющий слой, inflow vault и DAO treasury.</text>
<rect class="box actor" x="52" y="150" width="210" height="78"/>
<text class="txt" x="72" y="181">Пользователь</text>
<text class="small" x="72" y="206">signer, root_key, client_key</text>
<rect class="box actor" x="52" y="310" width="210" height="78"/>
<text class="txt" x="72" y="341">Покупатель тикета</text>
<text class="small" x="72" y="366">buy_ticket*</text>
<rect class="box actor" x="52" y="470" width="210" height="78"/>
<text class="txt" x="72" y="501">Менеджер</text>
<text class="small" x="72" y="526">manager_add_ticket</text>
<rect class="box actor" x="52" y="630" width="210" height="78"/>
<text class="txt" x="72" y="661">Любой caller</text>
<text class="small" x="72" y="686">step_payout</text>
<rect class="box program" x="360" y="126" width="270" height="96"/>
<text class="txt" x="382" y="160">1. shine_login_guard</text>
<text class="small" x="382" y="186">classify_login</text>
<text class="small" x="382" y="205">free / premium / trademark</text>
<rect class="box program" x="360" y="286" width="270" height="112"/>
<text class="txt" x="382" y="320">2. shine_users</text>
<text class="small" x="382" y="346">create_user_pda</text>
<text class="small" x="382" y="365">update_user_pda</text>
<text class="small" x="382" y="384">economy config</text>
<rect class="box program" x="360" y="518" width="270" height="122"/>
<text class="txt" x="382" y="552">3. shine_payments</text>
<text class="small" x="382" y="578">vault, tickets, queues</text>
<text class="small" x="382" y="597">grant_manager_limits</text>
<text class="small" x="382" y="616">step_payout</text>
<rect class="box dao" x="776" y="126" width="270" height="122"/>
<text class="txt" x="798" y="160">SHiNE DAO</text>
<text class="small" x="798" y="186">governance / authority</text>
<text class="small" x="798" y="205">treasury dao_wallet</text>
<text class="small" x="798" y="224">ключи через голосование</text>
<rect class="box pda" x="776" y="306" width="270" height="84"/>
<text class="txt" x="798" y="340">shine_users PDA</text>
<text class="small" x="798" y="365">user_pda, economy_config</text>
<rect class="box pda" x="776" y="500" width="270" height="150"/>
<text class="txt" x="798" y="534">shine_payments PDA</text>
<text class="small" x="798" y="560">config_pda, coef_limit_pda</text>
<text class="small" x="798" y="579">queues_pda</text>
<text class="small" x="798" y="598">inflow_vault_pda</text>
<text class="small" x="798" y="617">ticket_pda, manager_allowance</text>
<rect class="box pda" x="1134" y="500" width="214" height="88"/>
<text class="txt" x="1156" y="534">inflow_vault</text>
<text class="small" x="1156" y="560">деньги регистрации</text>
<rect class="box dao" x="1134" y="170" width="214" height="88"/>
<text class="txt" x="1156" y="204">DAO treasury</text>
<text class="small" x="1156" y="230">dao_wallet</text>
<path class="line" d="M262 189 C300 189, 318 334, 360 334"/>
<text class="small" x="270" y="286">регистрация / update</text>
<path class="line" d="M360 314 C322 250, 320 176, 360 174"/>
<text class="small" x="330" y="250">CPI login</text>
<path class="line" d="M630 342 L776 342"/>
<text class="small" x="646" y="329">создает/обновляет</text>
<path class="money" d="M262 205 C438 432, 1010 390, 1134 530"/>
<text class="small" x="430" y="430">регистрация и лимит -> inflow_vault</text>
<path class="money" d="M262 349 C540 260, 870 244, 1134 214"/>
<text class="small" x="538" y="270">покупка тикета -> DAO treasury</text>
<path class="line" d="M262 509 L360 579"/>
<text class="small" x="276" y="540">создать тикет</text>
<path class="line" d="M630 579 L776 575"/>
<text class="small" x="648" y="562">PDA состояния</text>
<path class="line" d="M1046 575 L1134 548"/>
<path class="money" d="M1134 560 C970 700, 580 728, 262 669"/>
<text class="small" x="650" y="720">call reward caller</text>
<path class="money" d="M1134 536 C860 754, 426 238, 262 194"/>
<text class="small" x="632" y="760">выплата получателю тикета</text>
<path class="money" d="M1241 500 L1241 258"/>
<text class="small" x="1254" y="380">DAO-часть выплат</text>
<path class="line" d="M776 188 L630 342"/>
<text class="small" x="642" y="250">update economy</text>
<path class="line" d="M776 216 C690 290, 666 516, 630 558"/>
<text class="small" x="654" y="438">settings / managers</text>
<path class="line dashed" d="M910 248 C850 702, 620 720, 520 640"/>
<text class="small" x="690" y="690">upgrade-authority: users/payments; login_guard позже</text>
<rect class="box" x="52" y="808" width="1296" height="54"/>
<line x1="74" y1="835" x2="132" y2="835" class="line"/>
<text class="legend" x="146" y="840">логические вызовы и управление</text>
<line x1="374" y1="835" x2="432" y2="835" class="money"/>
<text class="legend" x="446" y="840">движение SOL/lamports</text>
<line x1="682" y1="835" x2="740" y2="835" class="line dashed"/>
<text class="legend" x="754" y="840">будущая передача upgrade-authority DAO</text>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

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 == client_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 == client_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`, `client_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

@ -0,0 +1,114 @@
Аудит безопасности Solana-программ SHiNE
Проверены три программы в shine-solana/shine/programs/:
- shine_login_guard (183 строки) — stateless-классификатор логинов
- shine_users (1035 строк) — реестр пользователей, PDA-записи, подписи, экономика лимитов
- shine_payments (1330 строк) — очереди тикетов, выплаты, оракул Pyth
Общая инженерная культура высокая: везде checked_*-арифметика, overflow-checks = true, ручная верификация ed25519 через sysvar инструкций, аккуратный bounds-checked парсинг. Но есть две критические дыры одного класса — отсутствие проверки адреса PDA, которые позволяют обойти всю экономику и украсть средства из вольта.
---
🔴 CRITICAL #1 — shine_users: economy-config PDA не валидируется → бесплатная регистрация и бесконечный лимит
В process_create_user_pda (строка 448) и process_update_user_pda (строка 525):
let economy = read_users_economy_config(users_economy_config_pda)?;
А read_users_economy_config (строки 633643) не проверяет ни адрес, ни владельца аккаунта — просто читает байты:
fn read_users_economy_config(pda: &AccountInfo) -> Result<...> {
let raw = read_pda_all(pda)?; // try_borrow_data, без проверок
require!(raw.len() >= 25, ...);
Ok(UsersEconomyConfigState { version: raw[0], registration_fee_lamports: ..., ... })
}
Сравните: в init/update_economy_config адрес проверяется через find_users_economy_config_pda (строки 382, 414), а в create/update — нет.
Эксплуатация. Атакующий создаёт любой свой аккаунт с 25 байтами произвольного содержимого и передаёт его как users_economy_config_pda:
- registration_fee_lamports = 0 → регистрация без оплаты;
- start_bonus_limit = u64::MAX → запись пользователя сразу получает гигантский paid_limit_bytes (бесплатная безлимитная квота хранилища/блокчейна);
- lamports_per_limit_step = 0 → бесплатное пополнение лимита на любую величину.
Комиссия (когда она ненулевая) уходит в правильный вольт — validate_inflow_vault это проверяет — но атакующему достаточно обнулить комиссию и накрутить лимит. Это полный обход экономической модели программы.
Фикс: в обеих функциях перед чтением добавить
require_keys_eq!(find_users_economy_config_pda(program_id).0, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress);
require!(users_economy_config_pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
---
🔴 CRITICAL #2 — shine_payments: singleton-PDA не привязаны к адресу → кража из вольта в step_payout
ensure_expected_pdas вызывается только в process_init (строка 519). Во всех остальных инструкциях config_pda, coef_limit_pda, queues_pda читаются через read_state, который проверяет только владельца (*pda.owner == id()), но не адрес:
fn read_state<T>(pda) -> ... {
require!(!is_uninitialized_account(pda), ...);
require_keys_eq!(*pda.owner, id(), ...); // только owner, адрес НЕ проверяется
...
}
Программа владеет аккаунтами нескольких типов (config, coef_limit, queues, vault, tickets, manager_allowances), и часть их содержимого атакующий контролирует напрямую. В TicketState поле recipient_wallet (32 байта по смещению 11) — полностью произвольные байты из BuyTicketArgs, это не обязан быть валидный ключ.
Эксплуатация (конкретная, практически реализуемая). В process_step_payout (строки 783846) coef_limit_pda не проверяется по адресу. Раскладка CoefLimitState при декодировании: version=byte0, coef_ppm=[1..9], limit=[9..17], call_reward_lamports=[17..25]. Байты [17..25] тикета попадают на recipient_wallet[6..14] — их атакующий задаёт сам при покупке тикета. То есть:
1. Атакующий покупает тикет с recipient_wallet, чьи байты 6..14 кодируют огромный call_reward_lamports.
2. В step_payout подставляет этот тикет как coef_limit_pda.
3. transfer_from_vault(inflow_vault_pda, signer, coef_limit.call_reward_lamports) (строка 840) переводит подписанту (атакующему) почти весь доступный баланс вольта, который должен был накопиться для DAO.
version тикета = 1, decode значение версии не проверяет, поэтому подстановка проходит.
Подстановка config_pda для обхода DAO-авторизации в update_coef_limit/grant_manager_limits теоретически тоже возможна, но непрактична: там нужный dao_wallet пересекается со структурными полями тикета, и подбор потребовал бы грайндинга ~80 бит ключа. А вот путь через coef_limit/step_payout — реальная кража.
Фикс: во всех инструкциях, принимающих singleton-PDA, проверять адрес, например вызывать ensure_expected_pdas-подобную проверку (require_keys_eq!(find_single_pda(program_id, SEED).0, *pda.key, …)) для config/coef_limit/queues/inflow_vault, а для queues_pda в change_ticket_recipient — тоже.
---
🟠 MEDIUM — shine_payments: слабая валидация оракула Pyth
read_sol_usd_price / parse_pyth_price_update_v2 (строки 10381075):
1. Не проверяется владелец аккаунта цены (что он принадлежит Pyth receiver). Спасает только то, что адрес жёстко закреплён константой PYTH_SOL_USD_ACCOUNT. Это делает подмену невозможной, но защита держится на одном инварианте.
2. feed_id не проверяется. Константа PYTH_SOL_USD_FEED_ID объявлена в settings.rs, но в коде нигде не используется — программа доверяет, что в закреплённом аккаунте лежит именно SOL/USD.
3. Фиксированные смещения (73/89/93) предполагают VerificationLevel::Full. Borsh сериализует enum переменной длиной: Partial{num_signatures} занимает 2 байта вместо 1, что сдвигает все поля на 1 байт и приведёт к чтению мусорной цены. Уровень верификации не проверяется.
4. Confidence (conf) игнорируется — нет защиты от широкого ценового интервала.
Проверка возраста цены (ORACLE_MAX_AGE_SECS = 120) есть и сделана корректно. Рекомендация: проверять владельца аккаунта, сверять feed_id с константой и валидировать verification_level == Full (или парсить через официальный pyth_solana_receiver_sdk, который уже завендорен в .vendor/).
---
🟡 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.
---
✅ Что проверено и сделано корректно
- Верификация подписей ed25519 в shine_users (строки 885922): строго пинятся относительные индексы инструкций (1/2), требуется num_signatures == 1, все три ix-index == u16::MAX (данные внутри самой ed25519-инструкции — нельзя указать на чужую инструкцию), и сверяются pubkey/signature/message. Сделано грамотно.
- Цепочка версий записи (version == record_number+1, prev_hash == hash(old)) — корректная защита от replay (строки 535536).
- Авторизация обновления записи завязана на ed25519-подпись root_key, а не на подписанта-плательщика — случайный аккаунт обновить чужую запись не может.
- Монотонность used_bytes/last_block_number и used_bytes <= paid_limit_bytes (строки 979986).
- inflow_vault валидируется по derive из программы payments (строки 988993).
- transfer_from_vault сохраняет рент-экземпт (вычитает minimum_balance через available_vault_lamports).
- init обеих программ безопасен к front-run: значения берутся из констант, а не от вызывающего; повторная инициализация заблокирована проверкой «uninitialized».
- Арифметика: overflow-checks = true в release-профиле + повсеместные checked_add/sub/mul. Парсинг везде с проверкой границ.
- manager_allowance PDA — единственный из payments, чей адрес проверяется корректно во всех путях (строки 645646, 739740).
- shine_login_guard — stateless, без аккаунтов и средств; рисков безопасности не несёт.
---
Приоритет действий
1. Critical #1 — добавить проверку адреса+владельца economy-config в create/update shine_users. Тривиальный фикс, помощник find_users_economy_config_pda уже есть.
2. Critical #2 — добавить проверку адресов всех singleton-PDA во все инструкции shine_payments (минимум coef_limit_pda/config_pda/queues_pda в step_payout и change_ticket_recipient).
3. Medium — ужесточить парсинг Pyth (owner + feed_id + verification_level), либо перейти на завендоренный SDK.
4. Low — учесть griefing-DoS на предсказуемых адресах тикетов.
Обе критические находки относятся к одному классу (Solana «missing ownership/address check» — самая частая категория эксплойтов), их стоит закрыть до любого деплоя в mainnet. Изменений в код я не вносил — это только анализ; готов подготовить патчи на оба критических пункта, если подтвердите.

104
Dev_Docs/deploy/README.md Normal file
View File

@ -0,0 +1,104 @@
# Деплой SHiNE (шаблон)
Этот раздел хранит актуальные инструкции по деплою.
## Базовый сервер
- SSH: `player@shineup.me`
- Домен: `shineup.me`
- Базовый путь: `/home/player`
Для всех рабочих инструкций и скриптов использовать доменное имя `shineup.me`, а не фиксированный IP:
- актуальный IP должен браться через DNS-резолв на момент подключения;
- ручное дублирование IP в документации и deploy-скриптах не поддерживать.
## Контуры деплоя
- Production:
- SSH: `player@shineup.me`
- Домен: `shineup.me`
- IP: `185.229.109.118`
- Main test:
- SSH: `player@193.8.215.70`
- Домен: `t.shineup.me`
- IP: `193.8.215.70`
- Reserve test:
- SSH: `player@93.170.12.154`
- Домен: `test.shineup.me`
- IP: `93.170.12.154`
## Локальные команды
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`
- Production server deploy: `./gradlew deployServerProduction`
- Production UI deploy: `./gradlew deployUIProduction`
- Reserve test server deploy: `./gradlew deployServerTest`
- Reserve test UI deploy: `./gradlew deployUITest`
- Локальный запуск: `./gradlew startLocal`
## Политика подтверждений
- `shineup.me` — production.
- Любые изменения на `shineup.me`, включая deploy сервера, deploy UI, конфиги, перезапуски и миграции, делать только после отдельного явного подтверждения пользователя.
- Если пользователь пишет просто `задеплой` без уточнения, по умолчанию это означает deploy на `t.shineup.me`.
## Main test deploy (`t.shineup.me`)
- Это основной сервер для тестов.
- `deployServer` и `deployUI` по умолчанию направлены именно сюда.
- Серверный deploy не запускает JUnit/IT-тесты на удалённом сервере.
- `deployServer` / `deployServerTest2` делают:
- сборку fat-jar локально;
- синхронизацию `data/` и `shine.sqlite` с production `shineup.me`;
- перенос `application.properties` с production с поправкой `server.ui.indexPath` на `/home/player/SHiNE/shine-ui/index.html`;
- установку `systemd` unit на `193.8.215.70`;
- перезапуск `shine-server.service`;
- установку/проверку Caddy для `t.shineup.me`.
- `deployUI` / `deployUITest2` публикуют UI в `/home/player/SHiNE/shine-ui` на `193.8.215.70`.
## Reserve test deploy (`test.shineup.me`)
- `test.shineup.me` считается резервным тестовым сервером.
- Его настройки и адрес не менять без отдельной задачи.
- Задачи `deployServerTest` и `deployUITest` считаются резервными и требуют отдельной причины.
## UI-деплой и Caddy (обязательно)
- Целевая директория UI-деплоя: `/home/player/SHiNE/shine-ui`.
- `Caddyfile` на сервере должен смотреть в ту же директорию через `root * /home/player/SHiNE/shine-ui`.
- В `deploy_shine-PWA.sh` добавлена проверка: скрипт ищет блок `shineup.me { ... }` (или значение `EXPECTED_CADDY_SITE`) и проверяет `root` внутри этого блока.
- Если `root` внутри целевого блока не совпадает, деплой прерывается с ошибкой.
- Для ручного обхода проверки (только осознанно): `ALLOW_CADDY_MISMATCH=1 ./gradlew deployUI`.
- При необходимости можно явно переопределить путь деплоя:
- `REMOTE_UI_DIR=/нужный/путь ./gradlew deployUI`
- `EXPECTED_CADDY_UI_ROOT=/нужный/путь ./gradlew deployUI`
- `EXPECTED_CADDY_SITE=example.com ./gradlew deployUI`
## Временные тестовые сайты Solana tickets
- Для HTML UI программы `shine_payments` используется отдельный временный тестовый сайт.
- Основной каталог публикации:
- `/home/player/sites/test-solana-tickets.shineup.me`
- Рабочие домены:
- `https://test-solana-tickets.shineup.me`
- `https://test-solana-tickets.shiningpeople.ru`
- Назначение:
- ручная проверка сценариев покупки билетов;
- проверка DAO-инструментов и лимитов менеджеров;
- проверка ручного добавления билетов и `step_payout`.
- Эти сайты не считать основным UI SHiNE; это отдельная тестовая публикация под Solana-часть.
### Важно для локального UI (history-router / Ctrl+F5)
- Локальный UI **обязательно** поднимать только через `./gradlew startLocal`.
- Эта задача запускает `scripts/local_spa_server.py`, который делает SPA fallback: любой неизвестный путь (`/m/...`, `/channel/...`) возвращает `index.html`.
- Это обязательно для корректной работы `Ctrl+F5` на внутренних роутов без `404`.
- Рабочий URL выводится задачей в консоль в формате: `http://localhost:<WEB_PORT>/?localWsPort=<WS_PORT>`.
## Обязательные правила
1. Перед серверным деплоем проверить локально.
2. При нестандартном деплое (другой хост, другая структура, ручные шаги) обязательно уточнить у пользователя, нужно ли обновить этот шаблон.
3. Если деплой-процесс изменился, этот файл и файлы в `servers/` обновлять в том же коммите.

View File

@ -0,0 +1,36 @@
# Локальный деплой SHiNE-agent-bot-coder (systemd, пользователь ai)
## Где находится сервис
- Папка сервиса: `SHiNE-agent-bot-coder/`
- Systemd unit: `SHiNE-agent-bot-coder/scripts/systemd/shine-agent-bot-coder.service`
- Скрипт установки: `SHiNE-agent-bot-coder/scripts/systemd/install-local-systemd.sh`
## Предусловия
1. Заполнен `.env` на основе `.env.example`.
2. Доступен рабочий Codex CLI:
- `/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl`
3. На машине установлен `systemd --user`.
## Установка
Из корня репозитория:
```bash
bash SHiNE-agent-bot-coder/scripts/systemd/install-local-systemd.sh
```
Скрипт:
1. проверяет наличие `python3`;
2. копирует unit в `~/.config/systemd/user/`;
3. делает `systemctl --user daemon-reload`;
4. включает автозапуск и стартует сервис.
## Проверка
```bash
systemctl --user status shine-agent-bot-coder --no-pager
journalctl --user -u shine-agent-bot-coder -f
```
## Перезапуск после изменений
```bash
systemctl --user restart shine-agent-bot-coder
```

View File

@ -0,0 +1,43 @@
# Сервер `193.8.215.70` — основной test (`t.shineup.me`)
- Пользователь: `player`
- Домен: `t.shineup.me`
- Логин сервера: `tshineupme`
- Каталог SHiNE: `/home/player/SHiNE`
- UI публикация для Caddy: `/home/player/SHiNE/shine-ui`
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
- Данные: `/home/player/SHiNE/shine-server/data/`
- `shine.sqlite`
- `*.bch`
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
## Сервисы
- `shine-server.service` (systemd)
- `caddy.service` (systemd)
## Статус
- Это основной сервер для тестов SHiNE.
- Default deploy по умолчанию должен идти сюда.
- Источник данных для тестовой БД: production `shineup.me`.
## Caddy
- Конфиг: `/etc/caddy/Caddyfile`
- Сайты:
- `t.shineup.me`
- `agent.shiningpeople.ru`
- Для `t.shineup.me`:
- `root * /home/player/SHiNE/shine-ui`
- `try_files {path} /index.html`
- `reverse_proxy /ws* -> 127.0.0.1:7070`
## Deploy
- Default server deploy:
- `./gradlew deployServer`
- `./gradlew deployServerTest2`
- Default UI deploy:
- `./gradlew deployUI`
- `./gradlew deployUITest2`

View File

@ -0,0 +1,40 @@
# Сервер `93.170.12.154` — резервный test (`test.shineup.me`)
- Пользователь: `player`
- Каталог SHiNE: `/home/player/SHiNE`
- Домен: `test.shineup.me`
- Роль: резервный тестовый сервер
- UI публикация для Caddy: `/home/player/SHiNE/shine-ui`
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
- Данные: `/home/player/SHiNE/shine-server/data/`
- `shine.sqlite`
- `*.bch`
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
## Сервисы
- `shine-server.service` (systemd)
- `caddy.service` (systemd)
## Статус
- Резервный тестовый сервер для SHiNE.
- Источник данных для тестовой БД: production `shineup.me`.
- Пока не использовать для обычного deploy.
- Основной прод-сервер: `shineup.me` (`185.229.109.118`).
## Caddy
- Конфиг: `/etc/caddy/Caddyfile`
- Настройки:
- `no-store/no-cache` заголовки;
- `try_files {path} /index.html` (SPA fallback);
- `reverse_proxy /ws* -> 127.0.0.1:7070`;
- целевой сайт: `test.shineup.me`.
## Deploy
- Резервные задачи:
- `./gradlew deployServerTest`
- `./gradlew deployUITest`
- Эти задачи пока не использовать без отдельной причины.

View File

@ -0,0 +1,47 @@
# Сервер `shineup.me` — основной
- SSH: `player@shineup.me`
- Определение IP: через DNS-резолв домена `shineup.me` на момент подключения
- Пользователь: `player`
- Базовый путь: `/home/player`
- Каталог SHiNE: `/home/player/SHiNE`
- UI публикация: `/home/player/SHiNE/shine-ui`
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
- Данные: `/home/player/SHiNE/shine-server/data/`
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
## Сервисы
- `shine-server.service` (systemd)
- `caddy.service` (systemd)
## Caddy
- Активный конфиг (через systemd `ExecStart`): `/home/player/SHiNE/caddy/Caddyfile`
- Для UI:
- `root * /home/player/SHiNE/shine-ui`
- `try_files {path} /index.html` (SPA fallback)
- no-cache заголовки
- `reverse_proxy /ws* -> 127.0.0.1:7070`
## Дополнительно
- Для отдельной админки `shine_payments` используется каталог:
- `/home/player/sites/test-solana-tickets.shineup.me`
- Эта публикация используется как временный тестовый сайт для сценариев покупки билетов и выплат `shine_payments`.
- Домены этой публикации:
- `https://test-solana-tickets.shineup.me`
- `https://test-solana-tickets.shiningpeople.ru`
- Для всех deploy-скриптов и инструкций использовать именно `player@shineup.me`, без жёсткой фиксации IP.
## Правило изменений
- `shineup.me` — production.
- Любые изменения на этом сервере делать только после отдельного явного подтверждения пользователя.
## Deploy
- Production deploy-задачи:
- `./gradlew deployServerProduction`
- `./gradlew deployUIProduction`
- Default deploy-задачи `./gradlew deployServer` и `./gradlew deployUI` сюда больше не относятся.

View File

@ -0,0 +1,125 @@
# Деплой и инициализация Solana-регистрации (две обязательные программы)
## Коротко
Для рабочей регистрации пользователя нужны **обе** программы:
1. `shine_users` — хранение и обновление `user_pda`, economy-конфиг, логика регистрации.
2. `shine_login_guard` — проверка/классификация логина (CPI из `shine_users`).
Если задеплоена только одна из них — регистрация неработоспособна.
## Актуальные адреса (devnet)
- `shine_users` (регистрация пользователей):
`3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ`
- `shine_login_guard`:
`3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo`
- `shine_payments`:
`c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`
## Подтверждение деплоя
- Сеть: `https://api.devnet.solana.com`
- `shine_users`:
- `Program ID`: `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ`
- TX deploy: `5VzfpSirFCRqPUZfvAt3eADY9KnowW79PKZ1pCQAa2DJGiztj4dUYYXrSQNmWEhPVu6mPSDfcuHzFyEVmoKLa9DM`
- `shine_login_guard`:
- `Program ID`: `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo`
- TX deploy: `5iptngPYrLLjPE3Xby24zyNW3edVUnBNLBx785vjojMoq5JNLFNQvLNAm3jNYHbpf2B36qtbpTNzcvUNyRDqm1Mf`
## Порядок деплоя (devnet)
1. Убедиться, что CLI смотрит в devnet и у кошелька есть SOL.
2. Собрать и задеплоить `shine_login_guard`.
3. Собрать и задеплоить `shine_users`.
4. Проверить, что адреса совпадают между:
- `Anchor.toml`
- `declare_id!` в `programs/*/src/lib.rs`
- UI/серверными константами.
5. Выполнить `init_users_economy_config` (один раз на программу `shine_users`).
Пример команд:
```bash
cd shine-solana/shine
solana config get
solana balance
anchor build -p shine_login_guard
anchor deploy -p shine_login_guard
anchor build -p shine_users
anchor deploy -p shine_users
```
## Куда вписаны адреса в проекте
### UI
- Общие Solana-константы:
- `shine-UI/js/solana-programs.js`
- Страница инициализации:
- `shine-UI/js/pages/solana-users-init-view.js`
- Переход на страницу:
- `shine-UI/js/pages/developer-settings-view.js`
### Browser plugin wallet
- Резолвер PDA и проверка адресов:
- `SHiNE-browser-plugin-wallet/js/lib/shine-server-resolver.js`
- Проверка текущего wallet по PDA:
- `SHiNE-browser-plugin-wallet/background.js`
- Отображение состояния в popup:
- `SHiNE-browser-plugin-wallet/popup.js`
### Сервер
- Серверные константы Solana:
- `shine-server-config/src/main/java/utils/config/SolanaProgramsConfig.java`
### Solana / Anchor
- `shine-solana/shine/Anchor.toml`
- `shine-solana/shine/programs/shine_users/src/lib.rs` (`declare_id!`)
- `shine-solana/shine/programs/shine_login_guard/src/lib.rs` (`declare_id!`)
- `shine-solana/shine/programs/shine_payments/src/lib.rs` (`declare_id!`)
### Где ещё нужно синхронизировать адреса после нового deploy
- UI-константы и все потребители в `shine-UI/js/*`
- browser-plugin-wallet
- серверный `SolanaProgramsConfig.java`
- Anchor-конфиг и `declare_id!` в Solana-модуле
- документы:
- `Dev_Docs/Solana/user_pda/README.md`
- `shine-solana/shine/doc/programs/shine_users.md`
- `shine-solana/shine/doc/devnet_keys_and_deploy.md`
## Как запустить инициализацию economy PDA
1. Открыть UI.
2. Перейти: `Профиль -> Настройки -> Настройки разработчика -> Solana: init регистрации`.
3. Подключить кошелёк (Phantom, devnet).
4. Нажать `Запустить init_users_economy_config`.
5. Дождаться статуса `Успешно`.
Страница сама вычисляет PDA `users_economy_config` по seed:
- seed: `shine_users_economy_config`
- program: `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ`
## Кто оплачивает create/update user_pda
- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `clientKey`.
- В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера.
- `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию.
- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `clientKey`.
## Важно
- `init_users_economy_config` выполняется один раз на программу.
Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение).
- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `clientKey`, а содержимое записи подписывает `rootKey`.
- `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина.
Несовпадение адреса приведёт к ошибке регистрации.

View File

@ -0,0 +1,118 @@
# ESP Pairing и режимы подключения
Этот документ фиксирует актуальные режимы входа/подключения в SHiNE. Он нужен как отдельная точка входа по сценариям подключения, чтобы не смешивать обычную авторизацию и серверный pairing через доверенное уже авторизованное устройство пользователя.
## 1. Текущие режимы
### 1. Создание новой сессии через `clientKey`
Поток:
`AuthChallenge -> CreateAuthSession`
Смысл:
- новое устройство уже владеет приватным `clientKey`;
- сервер проверяет подпись `clientKey`;
- создаётся обычная активная сессия пользователя;
- этот поток остаётся без изменений.
### 2. Повторный вход в существующую сессию через `sessionKey`
Поток:
`SessionChallenge -> SessionLogin`
Смысл:
- устройство уже владеет приватным `sessionKey`;
- сервер проверяет подпись `sessionKey`;
- соединение снова входит в существующую сессию;
- этот поток тоже остаётся без изменений.
## 2. Добавление сессии через доверенное устройство пользователя
Новый поток не заменяет обычный логин, а живёт рядом с ним.
Цель:
- новое устройство знает `login`, а `pairing password` используется только если он включён на доверённом устройстве;
- сервер использует пароль только как фильтр от мусора;
- реальное доверие даёт любая уже онлайн доверенная сессия пользователя;
- сервер не выдаёт приватные ключи сам от себя.
Текущий поток:
1. Любая доверенная сессия пользователя создаёт на сервере pairing-настройку:
`UpsertEspPairingSettings`
2. Новое устройство создаёт pending-заявку:
`StartEspPairing`
3. Онлайн доверенная сессия видит список активных заявок:
`ListEspPairingRequests`
4. Доверенная сессия либо подтверждает заявку:
`ApproveEspPairing`
5. Либо отклоняет:
`RejectEspPairing`
6. Новое устройство читает результат:
`GetEspPairingStatus`
## 3. Что именно делает сервер
- хранит включённость pairing и optional `passwordHash` в формате `sha256$<hex>`;
- хранит pairing-заявки всех статусов, но в список активных для доверённого устройства отдаёт только pending `created`;
- рассчитывает короткий код `shortCode` из `10` цифр;
- рассчитывает длинный `fingerprintB58` из `SHA-256` заявки;
- уведомляет онлайн доверенные сессии событием `IncomingEspPairingRequest`, если такие сессии подключены;
- хранит переданный `encryptedPayload` как непрозрачную строку и не анализирует его содержимое.
## 4. Чего сервер в этом режиме не делает
- не передаёт приватный `clientKey`;
- не расшифровывает `encryptedPayload`;
- не проверяет криптографию содержимого payload;
- не делает клиентский UI;
- не навязывает конкретную схему `Ed25519 -> X25519` в коде сервера.
Это намеренно: сервер остаётся безопасным каркасом маршрутизации и состояния, а 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) )>
```
## 8. Связанный документ по внешнему кошельку
Для отдельного RPC-взаимодействия между браузерным wallet-расширением и ESP32 см. документ:
- [Формат_взаимодействия_внешнегоошелька_и_ESP32.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Dev_Docs/Протоколы/Формат_взаимодействия_внешнегоошелька_и_ESP32.md)

View File

@ -0,0 +1,104 @@
# SHiNE Arweave Wallet Derivation v1
Сокращение: **SAWD-v1**.
## Назначение
Из 32-байтного `clientKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address.
## Вход
- `clientKey32`: ровно 32 байта.
- Если исходный `client.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8.
- Если используется Solana keypair JSON на 64 байта, используются только `bytes[0..31]`.
## Выход
```json
{
"derivation": "SAWD-v1",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"n": "...",
"d": "...",
"p": "...",
"q": "...",
"dp": "...",
"dq": "...",
"qi": "..."
},
"owner": "...",
"address": "..."
}
```
Где:
- `owner = jwk.n`
- `address = base64url_no_padding(SHA-256(unsigned_big_endian_bytes(n)))`
## Константы
- `DERIVATION_NAME = "SAWD-v1"`
- `MASTER_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/MASTER"`
- `STREAM_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/STREAM"`
- `MR_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/MILLER-RABIN"`
- `RSA_BITS = 4096`
- `PRIME_BITS = 2048`
- `PUBLIC_EXPONENT = 65537`
- `MILLER_RABIN_ROUNDS = 64`
- `SMALL_PRIME_LIMIT = 10000`
## Алгоритм
1. Проверить `clientKey32.length == 32`.
2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = clientKey32)`.
3. Реализовать `deriveBytes(label, length)`:
- `output = empty`
- `counter = 0`
- while `output.length < length`:
- `block = HMAC-SHA256(key = masterSeed32, message = UTF8(STREAM_LABEL) || UTF8("/") || UTF8(label) || UTF8("/") || uint64_be(counter))`
- `output = output || block`
- `counter++`
- вернуть первые `length` байт.
4. Для `p` и `q`:
- `raw = deriveBytes(label + "/" + index, 256)`
- `candidate = unsigned_big_endian_integer(raw)`
- `candidate = candidate OR 2^2047`
- `candidate = candidate OR 1`
- Проверить:
- `bitLength(candidate) == 2048`
- `candidate odd`
- не делится на малые простые `<= 10000`
- `gcd(candidate - 1, 65537) == 1`
- проходит Miller-Rabin `64 rounds`
5. Базы Miller-Rabin детерминированные:
- `baseBytes = HMAC-SHA256(key = masterSeed32, message = UTF8(MR_LABEL) || UTF8("/") || UTF8(label) || UTF8("/") || uint64_be(index) || UTF8("/") || uint32_be(round))`
- `a = 2 + (unsigned_big_endian_integer(baseBytes) mod (candidate - 3))`
6. `p = derivePrime("p")`, `q = derivePrime("q")`.
7. Если `p == q`, продолжить поиск `q`.
8. Если `p > q`, поменять местами. В SAWD-v1 всегда `p < q`.
9. `n = p * q`
10. `e = 65537`
11. `lambda = lcm(p - 1, q - 1)`
12. `d = modular_inverse(e, lambda)`
13. `dp = d mod (p - 1)`
14. `dq = d mod (q - 1)`
15. `qi = modular_inverse(q, p)`
16. Сформировать JWK:
- `kty = "RSA"`
- `e = "AQAB"`
- `n,d,p,q,dp,dq,qi = base64url unsigned big-endian integer without padding`
17. `owner = jwk.n`
18. `address = base64url_no_padding(SHA-256(unsigned_big_endian_bytes(n)))`
## Запрещено
- `crypto.generateKeyPair`
- `WebCrypto generateKey`
- `KeyPairGenerator`
- `SecureRandom(seed)`
- `Math.random`
- системный `random`
- ArDrive CLI
- Turbo
- внешний API для генерации ключа
- сохранение приватного JWK
## Версионирование стандарта
Если меняется любая константа или шаг алгоритма — это уже **SAWD-v2**.
Пользователи, созданные на SAWD-v1, должны продолжать восстанавливаться через SAWD-v1.

View File

@ -0,0 +1,311 @@
# Формат взаимодействия внешнего кошелька и ESP32
Этот документ фиксирует актуальный формат взаимодействия между внешним браузерным wallet-расширением SHiNE и устройством `ESP32-S3-Touch-AMOLED-2.16`.
Документ описывает:
- как расширение получает текущий активный публичный ключ кошелька с ESP32;
- как расширение отправляет на ESP32 запрос подписи транзакции;
- что именно считается активным кошельком на ESP32;
- какие проверки и UI-реакции ожидаются в браузерном расширении и на устройстве;
- какие ограничения действуют в текущей версии протокола.
## 1. Общая идея
Устройство ESP32 хранит `master secret` пользователя и локально умеет выводить несколько кошельков из одного секрета.
На устройстве в UI пользователь выбирает текущий активный кошелёк:
- `client.key`
- `root.key`
- `custom`
Для `custom` используется derivation:
```text
sha256(base64(secret32) + "|wallet." + customName)
```
Браузерное расширение не указывает ESP32, какой кошелёк нужно вернуть в первом запросе. Оно просто спрашивает:
```text
какой кошелёк сейчас активен на устройстве
```
ESP32 возвращает:
- тип текущего активного кошелька;
- его публичный ключ `Base58`.
## 2. Транспорт и маршрут
Текущий формат использует уже существующую `wallet-session` браузерного расширения.
Схема маршрута:
`browser extension -> SHiNE server -> homeserver session on ESP32 -> SHiNE server -> browser extension`
В текущем формате:
- отдельная цифровая подпись payload не добавляется;
- отдельное E2E-шифрование для wallet RPC не добавляется;
- используется существующая авторизованная `wallet-session`, транспорт `WSS` и server-side маршрут через уже существующую операцию `CallSignalToSession`.
## 3. Запрос текущего публичного ключа кошелька
### 3.1. Назначение
Операция нужна, чтобы браузерное расширение могло узнать, какой кошелёк сейчас выбран на ESP32, и показать его пользователю перед дальнейшими действиями.
### 3.2. Формат запроса
```json
{
"v": 1,
"operation": "get_wallet_public_key",
"requestId": "1718998123456-482193",
"timeMs": 1718998123456
}
```
### 3.3. Поля запроса
- `v` — версия формата wallet RPC. Для текущего варианта: `1`.
- `operation` — строка операции. Для текущего запроса: `get_wallet_public_key`.
- `requestId` — идентификатор запроса, уникальный в пределах сеанса расширения. Рекомендуемый формат:
`timeMs-random`.
- `timeMs` — локальное время отправителя в миллисекундах.
### 3.4. Поведение ESP32
При получении такого запроса ESP32:
1. смотрит, какой кошелёк сейчас выбран в локальном UI;
2. вычисляет или берёт уже подготовленный публичный ключ именно этого активного кошелька;
3. возвращает тип кошелька и его `publicKeyBase58`.
Запрос не содержит:
- `walletSelector`;
- `customName`;
- `targetSessionName`.
Они намеренно не входят в текущий формат этого запроса.
## 4. Формат ответа
```json
{
"v": 1,
"op": "get_wallet_public_key_result",
"requestId": "1718998123456-482193",
"ok": true,
"wallet": {
"type": "custom",
"publicKeyBase58": "...."
},
"timeMs": 1718998123999
}
```
## 5. Поля ответа
- `v` — версия формата ответа. Сейчас `1`.
- `op` — строка результата операции. Сейчас `get_wallet_public_key_result`.
- `requestId` — должен совпадать с `requestId` исходного запроса.
- `ok` — признак успешного результата.
- `wallet.type` — тип активного кошелька:
- `client.key`
- `root.key`
- `custom`
- `wallet.publicKeyBase58` — публичный ключ активного кошелька в `Base58`.
- `timeMs` — время формирования ответа на стороне ESP32 в миллисекундах.
## 6. Ошибки текущего формата
Минимальный формат ошибки допускается таким:
```json
{
"v": 1,
"op": "get_wallet_public_key_result",
"requestId": "1718998123456-482193",
"ok": false,
"error": "wallet_unavailable",
"timeMs": 1718998123999
}
```
Рекомендуемые коды ошибок:
- `wallet_unavailable` — на устройстве нельзя получить текущий кошелёк;
- `secret_not_configured` — на устройстве ещё нет корректно сохранённого секрета;
- `wallet_type_unknown` — выбранный локальный тип кошелька не распознан;
- `internal_error` — прочая локальная ошибка устройства.
## 7. Правила для браузерного расширения
После ответа `ok=true` расширение должно:
1. показать пользователю тип кошелька;
2. показать полный `publicKeyBase58`;
3. дать кнопку копирования ключа в буфер;
4. сохранить этот ключ как текущий ключ устройства для следующей операции подписи.
### 7.1. Проверка через PDA Solana
Расширение уже знает публичные ключи пользователя из Solana PDA. Поэтому оно может дополнительно проверить ответ ESP32:
- если `wallet.type = client.key`, то `publicKeyBase58` должен совпасть с `clientKey`, прочитанным из PDA;
- если `wallet.type = root.key`, то `publicKeyBase58` должен совпасть с `rootKey`, прочитанным из PDA;
- если `wallet.type = custom`, такой проверки по PDA пока нет.
При несовпадении для `client.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA.
## 8. Ожидаемое поведение UI расширения
### 8.1. Общий вид popup
Popup браузерного расширения должен быть узким и вытянутым по вертикали.
### 8.2. Состояние без подключения
Если `wallet-session` ещё не подключена:
- показывается кнопка `Подключить`;
- по нажатию открывается экран подключения, близкий по смыслу к сценарию `Войти через другое устройство`;
- пользователь вводит логин устройства и получает код подключения.
### 8.3. Состояние после подключения
Если `wallet-session` уже подключена:
- показывается статус `Подключено`;
- остаётся выбор homeserver;
- появляется кнопка запроса текущего кошелька;
- появляется кнопка `Отключить`.
### 8.4. Подключение кошелька с сайта
Когда сайт просит подключить кошелёк через расширение, расширение должно вести себя как обычный wallet extension:
1. показать пользователю подтверждение подключения;
2. показать, какой именно кошелёк будет подключён;
3. после подтверждения пользователя завершить подключение;
4. если пользователь отказался, не подключать кошелёк к сайту.
## 9. Запрос подписи транзакции
### 9.1. Назначение
Операция нужна, чтобы браузерное расширение могло запросить у ESP32 подпись Solana-транзакции текущим активным кошельком.
Расширение передаёт:
- публичный ключ, которым ожидается подпись;
- сериализованную транзакцию;
- комментарий, который должен быть показан на экране ESP32.
ESP32:
1. показывает пользователю запрос подтверждения;
2. показывает комментарий к подписи;
3. после нажатия `APPROVE` или `REJECT` возвращает ответ в расширение.
### 9.2. Формат запроса
```json
{
"v": 1,
"operation": "sign_transaction",
"requestId": "1718998123456-482193",
"timeMs": 1718998123456,
"publicKeyBase58": "....",
"transactionBase64": "....",
"comment": "Site https://example.com requested transaction signature"
}
```
### 9.3. Поля запроса
- `v` — версия wallet RPC. Сейчас `1`.
- `operation` — строка операции: `sign_transaction`.
- `requestId` — идентификатор запроса.
- `timeMs` — время отправки на стороне расширения.
- `publicKeyBase58` — публичный ключ, от которого ожидается подпись.
- `transactionBase64` — сериализованная Solana transaction в `base64`.
- `comment` — короткое текстовое описание, которое ESP32 показывает пользователю при запросе подписи.
### 9.4. Поведение ESP32
При получении такого запроса ESP32:
1. сравнивает `publicKeyBase58` с публичным ключом текущего активного выбранного кошелька;
2. если ключ не совпадает, сразу возвращает ошибку `wallet_mismatch`;
3. если ключ совпадает, показывает отдельный экран подтверждения подписи;
4. на экране показывает:
- каким кошельком будет выполнена подпись;
- комментарий `comment`;
- кнопки `APPROVE` и `REJECT`;
5. если пользователь подтверждает подпись, ESP32 подписывает транзакцию и возвращает результат;
6. если пользователь отклоняет, ESP32 возвращает `rejected_by_user`.
## 10. Формат ответа на подпись
```json
{
"v": 1,
"op": "sign_transaction_result",
"requestId": "1718998123456-482193",
"ok": true,
"publicKeyBase58": "....",
"signatureBase58": "....",
"signedTransactionBase64": "....",
"timeMs": 1718998123999
}
```
Если пользователь отклонил запрос:
```json
{
"v": 1,
"op": "sign_transaction_result",
"requestId": "1718998123456-482193",
"ok": false,
"error": "rejected_by_user",
"timeMs": 1718998123999
}
```
Рекомендуемые ошибки для `sign_transaction`:
- `rejected_by_user`
- `wallet_unavailable`
- `wallet_mismatch`
- `transaction_base64_invalid`
- `transaction_sign_failed`
- `bad_request`
## 11. Подключение кошелька с сайта
При вызове сайта `connect wallet` расширение должно вести себя как обычный wallet extension:
1. запросить подтверждение у пользователя в браузере;
2. получить текущий публичный ключ с ESP32;
3. вернуть сайту `publicKey` текущего активного кошелька.
Для `signTransaction` расширение:
1. получает транзакцию от сайта;
2. пересылает её на ESP32 через `sign_transaction`;
3. ждёт решение пользователя на устройстве;
4. возвращает браузеру уже подписанную транзакцию.
## 12. Ограничения текущей версии
- запрос возвращает только текущий активный кошелёк, а не список всех кошельков;
- выбор типа кошелька делается только на самом ESP32;
- отдельная цифровая подпись ответа пока не используется;
- отдельное E2E-шифрование wallet RPC пока не используется;
- `custom`-кошельки пока не сверяются с PDA.

28
ESP32/AGENTS.md Normal file
View File

@ -0,0 +1,28 @@
# AGENTS for ESP32
## Язык UI
- Для ESP32-скетчей и экранного UI использовать английский язык.
- Русский текст на экране ESP32 пока не поддерживается корректно: шрифтовой путь для кириллицы не считается рабочим.
- Если меняется UI-скетч, все пользовательские строки на экране должны оставаться английскими, пока ограничение не снято отдельной задачей.
## Синхронизация со спецификацией
- При изменении экранов, кнопок, переходов, статусов или текстов обязательно обновлять соответствующую спецификацию в `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/`.
## Сборка ESP32
- Основной способ проверки и прошивки скетчей для `ESP32-S3-Touch-AMOLED-2.16` - `main-device/burn.sh`.
- Не собирать эти скетчи напрямую через `arduino-cli compile` без `burn.sh`, потому что скрипт добавляет нужные локальные библиотеки и конфиги из `official-demo/examples/Arduino-v3.3.5/libraries`.
- Если сборка падает по `lv_conf.h` или `TouchDrvCSTXXX.hpp`, сначала проверять именно `burn.sh` и его `--library` пути, а не считать, что файл пропал из репозитория.
## Диагностика ESP32
- Последнюю сохранённую ошибку или диагностическую запись читать с устройства через USB serial monitor на `115200`.
- Базовая команда:
`arduino-cli monitor -p /dev/ttyACM0 --config baudrate=115200`
- После подключения отправлять одну из команд:
`last_error`, `last_diag` или `reg_diag`
- Для очистки сохранённой диагностики использовать:
`clear_error` или `clear_diag`
- При падениях в ветках регистрации и обновления PDA сначала читать именно `last_error`: запись хранится в NVS и может пережить перезагрузку устройства.

View File

@ -0,0 +1,119 @@
# ESP32-S3-Touch-AMOLED-2.16 Codex Guide
Этот файл переносится в другие проекты как готовая инструкция для Codex по этой плате.
## 1) Что это за плата
- Модель: `Waveshare ESP32-S3-Touch-AMOLED-2.16`
- MCU: `ESP32-S3` (flash 16MB, PSRAM 8MB)
- Экран: AMOLED, физически 480x480, углы скруглены (часть крайних пикселей может быть невидима)
- Touch: CST92xx
- IMU: QMI8658
- Аудио:
- DAC/вывод (динамик): ES8311
- ADC/вход (микрофоны): ES7210
## 2) Что уже установлено в этой среде
- Ubuntu
- `arduino-cli 1.4.0`
- `esp32:esp32` core `3.3.5`
- `esptool` из `~/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool`
- USB порт платы: обычно `/dev/ttyACM0`
Проверка:
```bash
arduino-cli version
arduino-cli core list
arduino-cli board list
ls -l /dev/ttyACM0
```
## 3) Структура подпроекта (эталон)
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
- `original-firmware/` — backup/restore заводской прошивки
- `main-device/` — прошивки и `burn.sh`
- `reference/` — заметки и ссылки
## 4) Бэкап перед любыми экспериментами
```bash
cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
./backup_factory.sh
```
Ожидаемый результат:
- `factory-full-16mb.bin`
- `factory-full-16mb.bin.sha256`
Восстановление:
```bash
./restore_factory_backup.sh
```
## 5) Деплой (прошивка) — стандарт
Главный скрипт:
```bash
cd ESP32-S3-Touch-AMOLED-2.16/main-device
./burn.sh <mode>
```
Режимы:
- `hello` — базовый экран
- `widgets` — экран+touch+IMU (официальный пример)
- `audio` — тест аудио тракта
- `simple` — кастомный интеграционный тест (экран, touch, запись/воспроизведение, VU, tilt)
## 6) Как писать код под эту плату (важно)
1. **Экран**
- Рабочее разрешение использовать `480x480`.
- Не рисовать критичный текст/кнопки впритык к краю; держать safe margin (`~20px+`) из-за скругленных углов.
- Не делать полный `fillScreen` в каждом loop: только частичные обновления (`fillRect`/локальные перерисовки), иначе мерцание.
2. **Touch**
- Настройка CST:
- `setMaxCoordinates(480, 480)`
- `setSwapXY(true)`
- `setMirrorXY(true, false)`
- Обрабатывать touch по IRQ + `getPoint`.
- После смещения UI обязательно пересчитывать hitbox кнопок.
3. **Аудио**
- Для динамика инициализировать `ES8311`.
- Для микрофона обязательно инициализировать `ES7210`; без этого запись может быть пустой.
- Для отладки записи показывать VU/peak на экране во время `RECORD`.
- Для быстрой проверки тракта всегда держать кнопку `BEEP` (тон), чтобы отделить проблему динамика от проблемы микрофона.
4. **IMU**
- QMI8658 обновлять с ограниченной частотой (например 80150 мс для UI-строки), чтобы не шуметь перерисовками.
5. **Стабильность UI**
- Статика: рисуется один раз в setup.
- Динамика: отдельная зона, перерисовывать только по изменению данных.
## 7) Рекомендуемый workflow для Codex
1. Проверить порт и инструменты.
2. Если новая плата/первый запуск — сделать backup flash.
3. Собрать и залить `simple`.
4. Пройти ручной чек:
- экран отображает текст без обрезки,
- touch срабатывает по кнопкам,
- `BEEP` слышно,
- VU двигается во время записи,
- `PLAY` воспроизводит записанное,
- `Tilt` меняется при повороте.
5. Только после этого усложнять приложение.
## 8) Ссылки
- Product page: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
- Docs: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16

View File

@ -0,0 +1,119 @@
# ESP32-S3-Touch-AMOLED-2.16 Codex Guide
Этот файл переносится в другие проекты как готовая инструкция для Codex по этой плате.
## 1) Что это за плата
- Модель: `Waveshare ESP32-S3-Touch-AMOLED-2.16`
- MCU: `ESP32-S3` (flash 16MB, PSRAM 8MB)
- Экран: AMOLED, физически 480x480, углы скруглены (часть крайних пикселей может быть невидима)
- Touch: CST92xx
- IMU: QMI8658
- Аудио:
- DAC/вывод (динамик): ES8311
- ADC/вход (микрофоны): ES7210
## 2) Что уже установлено в этой среде
- Ubuntu
- `arduino-cli 1.4.0`
- `esp32:esp32` core `3.3.5`
- `esptool` из `~/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool`
- USB порт платы: обычно `/dev/ttyACM0`
Проверка:
```bash
arduino-cli version
arduino-cli core list
arduino-cli board list
ls -l /dev/ttyACM0
```
## 3) Структура подпроекта (эталон)
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
- `original-firmware/` — backup/restore заводской прошивки
- `main-device/` — прошивки и `burn.sh`
- `reference/` — заметки и ссылки
## 4) Бэкап перед любыми экспериментами
```bash
cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
./backup_factory.sh
```
Ожидаемый результат:
- `factory-full-16mb.bin`
- `factory-full-16mb.bin.sha256`
Восстановление:
```bash
./restore_factory_backup.sh
```
## 5) Деплой (прошивка) — стандарт
Главный скрипт:
```bash
cd ESP32-S3-Touch-AMOLED-2.16/main-device
./burn.sh <mode>
```
Режимы:
- `hello` — базовый экран
- `widgets` — экран+touch+IMU (официальный пример)
- `audio` — тест аудио тракта
- `simple` — кастомный интеграционный тест (экран, touch, запись/воспроизведение, VU, tilt)
## 6) Как писать код под эту плату (важно)
1. **Экран**
- Рабочее разрешение использовать `480x480`.
- Не рисовать критичный текст/кнопки впритык к краю; держать safe margin (`~20px+`) из-за скругленных углов.
- Не делать полный `fillScreen` в каждом loop: только частичные обновления (`fillRect`/локальные перерисовки), иначе мерцание.
2. **Touch**
- Настройка CST:
- `setMaxCoordinates(480, 480)`
- `setSwapXY(true)`
- `setMirrorXY(true, false)`
- Обрабатывать touch по IRQ + `getPoint`.
- После смещения UI обязательно пересчитывать hitbox кнопок.
3. **Аудио**
- Для динамика инициализировать `ES8311`.
- Для микрофона обязательно инициализировать `ES7210`; без этого запись может быть пустой.
- Для отладки записи показывать VU/peak на экране во время `RECORD`.
- Для быстрой проверки тракта всегда держать кнопку `BEEP` (тон), чтобы отделить проблему динамика от проблемы микрофона.
4. **IMU**
- QMI8658 обновлять с ограниченной частотой (например 80150 мс для UI-строки), чтобы не шуметь перерисовками.
5. **Стабильность UI**
- Статика: рисуется один раз в setup.
- Динамика: отдельная зона, перерисовывать только по изменению данных.
## 7) Рекомендуемый workflow для Codex
1. Проверить порт и инструменты.
2. Если новая плата/первый запуск — сделать backup flash.
3. Собрать и залить `simple`.
4. Пройти ручной чек:
- экран отображает текст без обрезки,
- touch срабатывает по кнопкам,
- `BEEP` слышно,
- VU двигается во время записи,
- `PLAY` воспроизводит записанное,
- `Tilt` меняется при повороте.
5. Только после этого усложнять приложение.
## 8) Ссылки
- Product page: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
- Docs: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16

View File

@ -0,0 +1,28 @@
# ESP32-S3-Touch-AMOLED-2.16
Подпроект для Waveshare `ESP32-S3-Touch-AMOLED-2.16`.
Структура:
- `official-demo/` — официальный репозиторий примеров Waveshare
- `original-firmware/` — резервная копия заводской прошивки
- `main-device/` — скрипты быстрой проверки устройства и основной скетч `shine_homeserver_main/`
- `reference/` — локальные заметки по документации и железу
- `main-device/shine_homeserver_main/` — основной рабочий скетч ESP32-проекта `SHiNE`
Примечание по git:
- `official-demo/` держать как локальный внешний checkout из `https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16`, в основной git его не добавлять.
- `original-firmware/*.bin` — локальный дамп конкретной платы, в git не добавлять.
- `.arduino-build/` и готовые `.bin/.elf/.map` — сборочные артефакты, в git не добавлять.
Быстрый старт:
1. Сделать backup текущей прошивки:
- `cd original-firmware && ./backup_factory.sh`
2. Залить тест экрана/тача:
- `cd ../main-device && ./burn.sh widgets`
3. Залить тест динамика:
- `cd ../main-device && ./burn.sh audio`
4. Залить основной UI:
- `cd ../main-device && ./burn.sh shine-homeserver-main`

View File

@ -0,0 +1,52 @@
# Main Device
Основной скетч homeserver и старые тестовые скетчи для быстрой проверки платы.
`burn.sh` теперь:
- сам пытается найти USB-порт ESP32;
- сначала делает быструю инкрементальную сборку;
- если быстрая сборка не удалась, автоматически повторяет полную `clean`-сборку.
Для режимов `widgets`, `audio` и `hello` рядом должен лежать локальный checkout `official-demo/` из официального репозитория Waveshare. В основной git он не добавляется, потому что это большой внешний набор примеров, библиотек, прошивок и артефактов.
Режимы:
- `widgets` — экран + touch + IMU (пример `05_LVGL_Widgets`)
- `audio` — динамик/аудио-кодек (пример `07_ES8311`)
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости)
- `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` — тест геометрии и нижних рядов кнопок
- `lvgl-basic-test` — минимальный экран на `LVGL` с текстом и кнопками
- `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_homeserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
- `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы
- `lvgl-nav-minimal-test` — старое имя основного скетча, теперь ведёт на `shine_homeserver_main/` для совместимости
Запуск:
- `./burn.sh widgets`
- `./burn.sh audio`
- `./burn.sh hello`
- `./burn.sh simple`
- `./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`
- `./burn.sh lvgl-basic-test`
- `./burn.sh lvgl-interaction-test`
- `./burn.sh lvgl-touch-debug-test`
- `./burn.sh lvgl-official-based-test`
- `./burn.sh lvgl-subserver-touch-test`
- `./burn.sh lvgl-russian-font-test`
- `./burn.sh lvgl-nav-minimal-test`
- `./flash_shine_homeserver_main.sh` - автоматически находит USB-порт и заливает `shine_homeserver_main`

View File

@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// BLAKE2b state
struct B2State {
uint64_t h[8], t[2], f[2];
uint8_t buf[128];
size_t buflen, outlen;
};

View File

@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BOARD_DIR="$(cd "${ROOT_DIR}/.." && pwd)"
DEMO_BASE="${BOARD_DIR}/official-demo/examples/Arduino-v3.3.5"
MODE="${1:-widgets}"
PORT="${PORT:-}"
FQBN="${FQBN:-esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc,UploadSpeed=921600,CPUFreq=240,FlashMode=dio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,PSRAM=opi}"
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/.arduino-build/build-${MODE}}"
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/.arduino-build/out-${MODE}}"
detect_port() {
local detected
detected="$(arduino-cli board list 2>/dev/null | awk '/\/dev\/tty(ACM|USB)/ {print $1; exit}')"
if [[ -n "${detected}" ]]; then
echo "${detected}"
return 0
fi
for candidate in /dev/ttyACM* /dev/ttyUSB*; do
if [[ -e "${candidate}" ]]; then
echo "${candidate}"
return 0
fi
done
return 1
}
case "${MODE}" in
hello) SKETCH_DIR="${DEMO_BASE}/examples/01_HelloWorld" ;;
widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;;
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;;
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" ;;
lvgl-basic-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_basic_test" ;;
lvgl-interaction-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_interaction_test" ;;
lvgl-touch-debug-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_touch_debug_test" ;;
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}/shine_homeserver_main" ;;
*)
echo "Unknown mode: ${MODE}" >&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
if [[ -z "${PORT}" ]]; then
if ! PORT="$(detect_port)"; then
echo "Failed to auto-detect ESP32 port. Set PORT=/dev/ttyACM0 ./burn.sh ${MODE}" >&2
exit 3
fi
fi
echo "== Mode: ${MODE}"
echo "== Sketch: ${SKETCH_DIR}"
echo "== Port: ${PORT}"
echo "== FQBN: ${FQBN}"
mkdir -p "${BUILD_DIR}" "${OUT_DIR}"
compile_args=(
--fqbn "${FQBN}"
--build-path "${BUILD_DIR}"
--output-dir "${OUT_DIR}"
--library "${DEMO_BASE}/libraries/GFX_Library_for_Arduino"
--library "${DEMO_BASE}/libraries/SensorLib"
--library "${DEMO_BASE}/libraries/XPowersLib"
--library "${DEMO_BASE}/libraries/lvgl"
--library "${DEMO_BASE}/libraries/Mylibrary"
"${SKETCH_DIR}"
)
echo "== Compile: fast incremental build"
if ! arduino-cli compile "${compile_args[@]}"; then
echo "== Compile: fast build failed, retrying clean build"
arduino-cli compile --clean "${compile_args[@]}"
fi
arduino-cli upload \
-p "${PORT}" \
--fqbn "${FQBN}" \
--input-dir "${OUT_DIR}" \
"${SKETCH_DIR}"
echo
echo "== Done."

View File

@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
detect_port_from_arduino_cli() {
local line
while IFS= read -r line; do
[[ -z "${line}" ]] && continue
[[ "${line}" == Port* ]] && continue
if [[ "${line}" == /dev/* ]]; then
awk '{print $1}' <<<"${line}"
return 0
fi
done < <(arduino-cli board list 2>/dev/null || true)
return 1
}
detect_port_from_dev() {
local candidates=()
local path
for path in /dev/ttyACM* /dev/ttyUSB*; do
[[ -e "${path}" ]] || continue
candidates+=("${path}")
done
if [[ "${#candidates[@]}" -eq 1 ]]; then
printf '%s\n' "${candidates[0]}"
return 0
fi
return 1
}
PORT="${PORT:-}"
if [[ -z "${PORT}" ]]; then
PORT="$(detect_port_from_arduino_cli || true)"
fi
if [[ -z "${PORT}" ]]; then
PORT="$(detect_port_from_dev || true)"
fi
if [[ -z "${PORT}" ]]; then
echo "Не удалось автоматически найти USB-порт ESP32." >&2
echo "Подключите плату и проверьте 'arduino-cli board list'." >&2
echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_homeserver_main.sh" >&2
exit 1
fi
echo "== Найден порт: ${PORT}"
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,591 @@
#include "shine_secret_generation.h"
#include <SD_MMC.h>
#include <mbedtls/sha256.h>
#include <mbedtls/base64.h>
#include <string.h>
#include <stdint.h>
#include <driver/gpio.h>
#define PIN_SD_CLK 2
#define PIN_SD_CMD 1
#define PIN_SD_D0 3
#define SD_MEM_FILE "/argon2.bin"
#define A2_T 2u
#define A2_M 65536u
#define A2_P 1u
#define A2_DKLEN 32u
#define A2_SYNC 4u
#define A2_SEG (A2_M / A2_SYNC)
#define A2_VERSION 0x13u
#define A2_TYPE 2u
#define A2_BLKSZ 1024u
#define TOTAL_FILLS (A2_T * A2_M - 2u)
struct B2State {
uint64_t h[8], t[2], f[2];
uint8_t buf[128];
size_t buflen, outlen;
};
static bool gSdReady = false;
static bool gRunning = false;
static bool gDone = false;
static bool gError = false;
static char gMessage[96] = {};
static uint8_t gSecret[32] = {};
static char gSecretB58[64] = {};
static uint32_t gDoneBlocks = 0;
static uint32_t gStartMs = 0;
static uint32_t gCurPass = 0;
static uint32_t gCurBlock = 2;
static bool gInitDone = false;
static File gSdFile;
static uint8_t *gBufPrev = nullptr;
static uint8_t *gBufRef = nullptr;
static uint8_t *gBufOut = nullptr;
static uint8_t *gBufZero = nullptr;
static uint8_t *gBufAddr = nullptr;
static uint32_t *gJ1Seg = nullptr;
static uint8_t gH0[64] = {};
static B2State gB2S;
#define ROTR64(x,n) (((x)>>(n))|((x)<<(64-(n))))
static const uint64_t B2IV[8] = {
0x6A09E667F3BCC908ULL,0xBB67AE8584CAA73BULL,
0x3C6EF372FE94F82BULL,0xA54FF53A5F1D36F1ULL,
0x510E527FADE682D1ULL,0x9B05688C2B3E6C1FULL,
0x1F83D9ABFB41BD6BULL,0x5BE0CD19137E2179ULL
};
static const uint8_t B2SIGMA[12][16] = {
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3},
{11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4},
{7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8},
{9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13},
{2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9},
{12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11},
{13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10},
{6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5},
{10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0},
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3}
};
static const char B58_ALPHA[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
static void setMessage(const char *message) {
snprintf(gMessage, sizeof(gMessage), "%s", message ? message : "");
}
static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32);
static void finishSecretFromBytes(const uint8_t secret32[32], const char *message) {
memcpy(gSecret, secret32, 32);
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
gDone = true;
gRunning = false;
gError = false;
gInitDone = false;
gDoneBlocks = TOTAL_FILLS;
setMessage(message ? message : "Secret generated");
}
static void b2_compress(B2State *S, const uint8_t *blk) {
uint64_t m[16], v[16];
for (int i = 0; i < 16; i++) m[i] = ((const uint64_t *)blk)[i];
for (int i = 0; i < 8; i++) v[i] = S->h[i];
v[8]=B2IV[0];v[9]=B2IV[1];v[10]=B2IV[2];v[11]=B2IV[3];
v[12]=B2IV[4]^S->t[0];v[13]=B2IV[5]^S->t[1];
v[14]=B2IV[6]^S->f[0];v[15]=B2IV[7]^S->f[1];
#define BG(r,i,a,b,c,d) \
v[a]+=v[b]+m[B2SIGMA[r][2*(i)]]; v[d]=ROTR64(v[d]^v[a],32); \
v[c]+=v[d]; v[b]=ROTR64(v[b]^v[c],24); \
v[a]+=v[b]+m[B2SIGMA[r][2*(i)+1]]; v[d]=ROTR64(v[d]^v[a],16); \
v[c]+=v[d]; v[b]=ROTR64(v[b]^v[c],63)
for (int r = 0; r < 12; r++) {
BG(r,0,0,4,8,12);BG(r,1,1,5,9,13);BG(r,2,2,6,10,14);BG(r,3,3,7,11,15);
BG(r,4,0,5,10,15);BG(r,5,1,6,11,12);BG(r,6,2,7,8,13);BG(r,7,3,4,9,14);
}
#undef BG
for (int i = 0; i < 8; i++) S->h[i] ^= v[i] ^ v[i + 8];
}
static void b2_init(B2State *S, size_t outlen) {
memset(S, 0, sizeof(*S));
memcpy(S->h, B2IV, 64);
S->h[0] ^= 0x01010000ULL | outlen;
S->outlen = outlen;
}
static void b2_update(B2State *S, const uint8_t *in, size_t inlen) {
while (inlen > 0) {
size_t fill = 128 - S->buflen;
size_t use = inlen < fill ? inlen : fill;
memcpy(S->buf + S->buflen, in, use);
S->buflen += use;
in += use;
inlen -= use;
if (S->buflen == 128 && inlen > 0) {
S->t[0] += 128;
if (S->t[0] < 128) S->t[1]++;
b2_compress(S, S->buf);
S->buflen = 0;
}
}
}
static void b2_final(B2State *S, uint8_t *digest) {
S->f[0] = ~0ULL;
S->t[0] += S->buflen;
if (S->t[0] < S->buflen) S->t[1]++;
memset(S->buf + S->buflen, 0, 128 - S->buflen);
b2_compress(S, S->buf);
uint8_t tmp[64];
for (int i = 0; i < 8; i++) {
uint64_t w = S->h[i];
for (int j = 0; j < 8; j++) tmp[i * 8 + j] = (uint8_t)(w >> (j * 8));
}
memcpy(digest, tmp, S->outlen);
}
static void b2hash(const uint8_t *in, size_t inlen, uint8_t *digest, size_t outlen) {
b2_init(&gB2S, outlen);
b2_update(&gB2S, in, inlen);
b2_final(&gB2S, digest);
}
static void b2long(const uint8_t *in, size_t inlen, uint8_t *digest, uint32_t outlen) {
uint8_t lenle[4] = {(uint8_t)outlen, (uint8_t)(outlen >> 8), (uint8_t)(outlen >> 16), (uint8_t)(outlen >> 24)};
if (outlen <= 64) {
b2_init(&gB2S, outlen);
b2_update(&gB2S, lenle, 4);
b2_update(&gB2S, in, inlen);
b2_final(&gB2S, digest);
return;
}
uint8_t tmp[64];
b2_init(&gB2S, 64);
b2_update(&gB2S, lenle, 4);
b2_update(&gB2S, in, inlen);
b2_final(&gB2S, tmp);
memcpy(digest, tmp, 32);
digest += 32;
uint32_t rem = outlen - 32;
while (rem > 64) {
b2hash(tmp, 64, tmp, 64);
memcpy(digest, tmp, 32);
digest += 32;
rem -= 32;
}
b2hash(tmp, 64, tmp, rem);
memcpy(digest, tmp, rem);
}
#define A2G(a,b,c,d) do{ \
(a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
(d)=ROTR64((d)^(a),32); \
(c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
(b)=ROTR64((b)^(c),24); \
(a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
(d)=ROTR64((d)^(a),16); \
(c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
(b)=ROTR64((b)^(c),63); \
}while(0)
#define A2P(v) do{ \
A2G((v)[0],(v)[4],(v)[8],(v)[12]);A2G((v)[1],(v)[5],(v)[9],(v)[13]); \
A2G((v)[2],(v)[6],(v)[10],(v)[14]);A2G((v)[3],(v)[7],(v)[11],(v)[15]); \
A2G((v)[0],(v)[5],(v)[10],(v)[15]);A2G((v)[1],(v)[6],(v)[11],(v)[12]); \
A2G((v)[2],(v)[7],(v)[8],(v)[13]);A2G((v)[3],(v)[4],(v)[9],(v)[14]); \
}while(0)
static void fillBlock(uint32_t prevIdx, uint32_t refIdx, uint32_t outIdx, bool xorMode) {
gSdFile.seek((uint64_t)prevIdx * A2_BLKSZ); gSdFile.read(gBufPrev, A2_BLKSZ);
gSdFile.seek((uint64_t)refIdx * A2_BLKSZ); gSdFile.read(gBufRef, A2_BLKSZ);
uint64_t *R = (uint64_t *)gBufOut, *P = (uint64_t *)gBufPrev, *F = (uint64_t *)gBufRef;
for (int i = 0; i < 128; i++) R[i] = P[i] ^ F[i];
uint64_t *Q = (uint64_t *)gBufRef; memcpy(Q, R, A2_BLKSZ);
for (int j = 0; j < 8; j++) A2P(&Q[16 * j]);
uint64_t row[16];
for (int i = 0; i < 8; i++) {
for (int k = 0; k < 8; k++) { row[2 * k] = Q[2 * i + 16 * k]; row[2 * k + 1] = Q[2 * i + 16 * k + 1]; }
A2P(row);
for (int k = 0; k < 8; k++) { Q[2 * i + 16 * k] = row[2 * k]; Q[2 * i + 16 * k + 1] = row[2 * k + 1]; }
}
for (int i = 0; i < 128; i++) R[i] = Q[i] ^ R[i];
if (xorMode) {
gSdFile.seek((uint64_t)outIdx * A2_BLKSZ); gSdFile.read(gBufPrev, A2_BLKSZ);
uint64_t *O = (uint64_t *)gBufPrev; for (int i = 0; i < 128; i++) R[i] ^= O[i];
}
gSdFile.seek((uint64_t)outIdx * A2_BLKSZ); gSdFile.write(gBufOut, A2_BLKSZ);
}
static void generateAddresses(uint32_t pass, uint32_t slice) {
memset(gBufZero, 0, A2_BLKSZ);
uint8_t inputBlk[A2_BLKSZ]; memset(inputBlk, 0, A2_BLKSZ);
uint64_t *iv = (uint64_t *)inputBlk;
iv[0]=pass;iv[1]=0;iv[2]=slice;iv[3]=A2_M;iv[4]=A2_T;iv[5]=A2_TYPE;
uint32_t count = 0;
for (uint32_t b = 0; b < A2_SEG; b += 128) {
iv[6] = ++count;
{
uint64_t *R=(uint64_t*)gBufAddr,*Z=(uint64_t*)gBufZero,*I=iv;
for(int i=0;i<128;i++)R[i]=Z[i]^I[i];
uint64_t *Q=R,row[16];
for(int j=0;j<8;j++)A2P(&Q[16*j]);
for(int i=0;i<8;i++){
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
A2P(row);
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
}
for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^I[i]);
}
{
uint64_t *R=(uint64_t*)gBufPrev,*Z=(uint64_t*)gBufZero,*A=(uint64_t*)gBufAddr;
for(int i=0;i<128;i++)R[i]=Z[i]^A[i];
uint64_t *Q=R,row[16];
for(int j=0;j<8;j++)A2P(&Q[16*j]);
for(int i=0;i<8;i++){
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
A2P(row);
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
}
for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^A[i]);
memcpy(gBufAddr,gBufPrev,A2_BLKSZ);
}
uint64_t *addr=(uint64_t*)gBufAddr;
uint32_t cnt=(b+128<=A2_SEG)?128:(A2_SEG-b);
for(uint32_t j=0;j<cnt;j++)gJ1Seg[b+j]=(uint32_t)(addr[j]&0xFFFFFFFFULL);
}
}
static uint32_t indexAlpha(uint32_t pass, uint32_t slice, uint32_t posInSlice, uint32_t J1) {
uint32_t refAreaSize;
if (pass == 0) {
if (slice == 0) {
uint32_t pos = posInSlice;
refAreaSize = (pos < 2) ? 0 : pos - 1;
} else {
refAreaSize = slice * A2_SEG + posInSlice - 1;
}
} else {
refAreaSize = A2_M - A2_SEG + posInSlice - 1;
}
if (refAreaSize == 0) refAreaSize = 1;
uint64_t relPos = (uint64_t)J1 * J1 >> 32;
relPos = refAreaSize - 1 - ((uint64_t)refAreaSize * relPos >> 32);
uint32_t startPos = 0;
if (pass != 0 && slice != A2_SYNC - 1) startPos = (slice + 1) * A2_SEG;
return (startPos + relPos) % A2_M;
}
static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32) {
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0);
mbedtls_sha256_update(&ctx, in, len);
mbedtls_sha256_finish(&ctx, out32);
mbedtls_sha256_free(&ctx);
}
static void trimInPlace(char *text) {
if (!text) return;
size_t len = strlen(text);
size_t start = 0;
while (start < len && (text[start] == ' ' || text[start] == '\t' || text[start] == '\n' || text[start] == '\r')) start++;
size_t end = len;
while (end > start && (text[end - 1] == ' ' || text[end - 1] == '\t' || text[end - 1] == '\n' || text[end - 1] == '\r')) end--;
if (start > 0 && end > start) memmove(text, text + start, end - start);
if (end <= start) { text[0] = '\0'; return; }
text[end - start] = '\0';
}
static void lowercaseAsciiInPlace(char *text) {
if (!text) return;
for (int i = 0; text[i]; i++) if (text[i] >= 'A' && text[i] <= 'Z') text[i] += 32;
}
static void normalizeLoginInPlace(char *text) {
trimInPlace(text);
lowercaseAsciiInPlace(text);
}
static void bytesToBase64Std(const uint8_t *data, size_t len, char *out, size_t outSz) {
size_t b64len = 0;
if (outSz == 0) return;
if (mbedtls_base64_encode((uint8_t*)out, outSz, &b64len, data, len) != 0) {
out[0] = '\0';
return;
}
if (b64len >= outSz) b64len = outSz - 1;
out[b64len] = '\0';
}
static void deriveLegacyMasterSecret(const char *password, uint8_t *out32) {
uint8_t baseHash[32];
sha256calc((const uint8_t*)password, strlen(password), baseHash);
char baseB64[64];
bytesToBase64Std(baseHash, 32, baseB64, sizeof(baseB64));
char material[96];
snprintf(material, sizeof(material), "%smaster.secret", baseB64);
sha256calc((const uint8_t*)material, strlen(material), out32);
}
void shineSecretBase58Encode(const uint8_t *data, size_t len, char *out, size_t outSz) {
int zeros = 0;
while (zeros < (int)len && data[zeros] == 0) zeros++;
uint8_t tmp[128] = {};
int tmpLen = 0;
for (int i = zeros; i < (int)len; i++) {
int carry = data[i];
for (int j = 0; j < tmpLen; j++) { carry += 256 * tmp[j]; tmp[j] = carry % 58; carry /= 58; }
while (carry) { tmp[tmpLen++] = carry % 58; carry /= 58; }
}
int n = 0;
for (int i = 0; i < zeros && n < (int)outSz - 1; i++) out[n++] = '1';
for (int i = tmpLen - 1; i >= 0 && n < (int)outSz - 1; i--) out[n++] = B58_ALPHA[(int)tmp[i]];
out[n] = '\0';
}
bool shineSecretBase58Decode(const char *input, uint8_t *out, size_t *outLen, size_t maxOutLen, String &error) {
error = "";
if (!input || !*input) {
error = "secret too short";
return false;
}
size_t inLen = strlen(input);
uint8_t tmp[256] = {};
size_t tmpLen = 0;
size_t zeros = 0;
while (zeros < inLen && input[zeros] == '1') zeros++;
for (size_t i = zeros; i < inLen; ++i) {
const char *pos = strchr(B58_ALPHA, input[i]);
if (!pos) {
error = "invalid base58";
return false;
}
int carry = (int)(pos - B58_ALPHA);
for (size_t j = 0; j < tmpLen; ++j) {
carry += 58 * tmp[j];
tmp[j] = carry & 0xFF;
carry >>= 8;
}
while (carry > 0) {
tmp[tmpLen++] = carry & 0xFF;
carry >>= 8;
if (tmpLen >= sizeof(tmp)) {
error = "secret too long";
return false;
}
}
}
size_t decodedLen = zeros + tmpLen;
if (decodedLen > maxOutLen) {
error = "secret too long";
return false;
}
memset(out, 0, maxOutLen);
size_t offset = 0;
for (size_t i = 0; i < zeros; ++i) out[offset++] = 0;
for (size_t i = 0; i < tmpLen; ++i) out[offset + i] = tmp[tmpLen - 1 - i];
*outLen = decodedLen;
return true;
}
static void argon2Init(const char *login, const char *password) {
char saltSrc[256];
snprintf(saltSrc, sizeof(saltSrc), "shine-auth-v2|login=%s|suffix=master.secret", login);
uint8_t saltHash[32]; sha256calc((uint8_t*)saltSrc, strlen(saltSrc), saltHash);
uint8_t salt[16]; memcpy(salt, saltHash, 16);
char passBytes[128];
snprintf(passBytes, sizeof(passBytes), "%s\n%s", login, password);
uint32_t passLen = strlen(passBytes), saltLen = 16;
b2_init(&gB2S, 64);
uint8_t le4[4];
#define B2LE32(val) do{uint32_t _v=(val);le4[0]=_v;le4[1]=_v>>8;le4[2]=_v>>16;le4[3]=_v>>24;b2_update(&gB2S,le4,4);}while(0)
B2LE32(A2_P);B2LE32(A2_DKLEN);B2LE32(A2_M);B2LE32(A2_T);
B2LE32(A2_VERSION);B2LE32(A2_TYPE);
B2LE32(passLen);b2_update(&gB2S,(const uint8_t*)passBytes,passLen);
B2LE32(saltLen);b2_update(&gB2S,salt,saltLen);
B2LE32(0);B2LE32(0);
#undef B2LE32
b2_final(&gB2S, gH0);
uint8_t input[72]; memcpy(input, gH0, 64);
for (uint32_t i = 0; i < 2; i++) {
input[64]=i;input[65]=0;input[66]=0;input[67]=0;
input[68]=0;input[69]=0;input[70]=0;input[71]=0;
b2long(input, 72, gBufOut, A2_BLKSZ);
gSdFile.seek((uint64_t)i * A2_BLKSZ); gSdFile.write(gBufOut, A2_BLKSZ);
}
gCurPass = 0;
gCurBlock = 2;
gDoneBlocks = 0;
gStartMs = millis();
gInitDone = true;
generateAddresses(0, 0);
}
bool shineSecretInitSd(String &error) {
error = "";
if (gSdReady) return true;
gpio_reset_pin(GPIO_NUM_2);
pinMode(PIN_SD_CMD, INPUT_PULLUP);
pinMode(PIN_SD_D0, INPUT_PULLUP);
delay(20);
SD_MMC.setPins(PIN_SD_CLK, PIN_SD_CMD, PIN_SD_D0);
gSdReady = SD_MMC.begin("/sdcard", true);
if (!gSdReady) {
error = "SD card error";
return false;
}
if (!gBufPrev) {
gBufPrev=(uint8_t*)ps_malloc(A2_BLKSZ);
gBufRef=(uint8_t*)ps_malloc(A2_BLKSZ);
gBufOut=(uint8_t*)ps_malloc(A2_BLKSZ);
gBufZero=(uint8_t*)ps_calloc(1, A2_BLKSZ);
gBufAddr=(uint8_t*)ps_malloc(A2_BLKSZ);
gJ1Seg=(uint32_t*)ps_malloc(A2_SEG * sizeof(uint32_t));
}
if (!gBufPrev || !gBufRef || !gBufOut || !gBufZero || !gBufAddr || !gJ1Seg) {
error = "PSRAM alloc failed";
return false;
}
return true;
}
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error) {
error = "";
if (!normalizedLogin || !*normalizedLogin) {
error = "login not set";
return false;
}
char loginBuf[64];
snprintf(loginBuf, sizeof(loginBuf), "%s", normalizedLogin);
normalizeLoginInPlace(loginBuf);
if (!loginBuf[0]) {
error = "login not set";
return false;
}
if (!shineSecretInitSd(error)) return false;
if (gSdFile) gSdFile.close();
SD_MMC.remove(SD_MEM_FILE);
gSdFile = SD_MMC.open(SD_MEM_FILE, "w+");
if (!gSdFile) {
error = "SD open failed";
return false;
}
memset(gSecret, 0, sizeof(gSecret));
gSecretB58[0] = '\0';
gDone = false;
gError = false;
gRunning = true;
gInitDone = false;
setMessage("Generating secret...");
if (!password || !password[0]) {
deriveLegacyMasterSecret(password ? password : "", gSecret);
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
gDone = true;
gRunning = false;
setMessage("Secret generated");
gSdFile.close();
SD_MMC.remove(SD_MEM_FILE);
return true;
}
argon2Init(loginBuf, password);
return true;
}
bool shineSecretStep(uint32_t blocksPerTick) {
if (!gRunning || !gInitDone) return gDone;
bool done = false;
for (uint32_t i = 0; i < blocksPerTick && !done; i++) {
if (gCurPass >= A2_T) {
done = true;
break;
}
uint32_t blk = gCurBlock;
uint32_t slice = blk / A2_SEG;
uint32_t posInSlice = blk - slice * A2_SEG;
bool xorMode = (gCurPass > 0);
uint32_t J1;
bool useArgon2i = (gCurPass == 0 && slice < 2);
if (useArgon2i) {
J1 = gJ1Seg[posInSlice];
} else {
uint32_t prevIdx = (blk == 0) ? A2_M - 1 : blk - 1;
gSdFile.seek((uint64_t)prevIdx * A2_BLKSZ);
gSdFile.read(gBufRef, 8);
J1 = ((uint32_t*)gBufRef)[0];
}
uint32_t prevIdx = (blk == 0) ? A2_M - 1 : blk - 1;
uint32_t refIdx = indexAlpha(gCurPass, slice, posInSlice, J1);
fillBlock(prevIdx, refIdx, blk, xorMode);
gDoneBlocks++;
gCurBlock++;
if (gCurBlock >= A2_M) {
gCurBlock = 0;
gCurPass++;
} else {
uint32_t newSlice = gCurBlock / A2_SEG;
if (newSlice != slice && gCurPass == 0 && newSlice < 2) generateAddresses(gCurPass, newSlice);
}
done = (gCurPass >= A2_T);
}
if (done) {
gSdFile.seek((uint64_t)(A2_M - 1) * A2_BLKSZ);
gSdFile.read(gBufOut, A2_BLKSZ);
b2long(gBufOut, A2_BLKSZ, gSecret, A2_DKLEN);
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
gDone = true;
gRunning = false;
setMessage("Secret generated");
gSdFile.close();
SD_MMC.remove(SD_MEM_FILE);
}
return gDone;
}
void shineSecretAbort() {
gRunning = false;
gDone = false;
gError = false;
gInitDone = false;
gDoneBlocks = 0;
if (gSdFile) gSdFile.close();
if (gSdReady) SD_MMC.remove(SD_MEM_FILE);
setMessage("Generation cancelled");
}
ShineSecretGenerationStatus shineSecretStatus() {
ShineSecretGenerationStatus status = {};
status.running = gRunning;
status.done = gDone;
status.error = gError;
status.doneBlocks = gDoneBlocks;
status.totalBlocks = TOTAL_FILLS;
status.elapsedSec = gStartMs == 0 ? 0 : (millis() - gStartMs) / 1000;
snprintf(status.message, sizeof(status.message), "%s", gMessage);
snprintf(status.secretBase58, sizeof(status.secretBase58), "%s", gSecretB58);
return status;
}
const uint8_t *shineSecretBytes() {
return gSecret;
}
const char *shineSecretBase58() {
return gSecretB58;
}

View File

@ -0,0 +1,26 @@
#pragma once
#include <Arduino.h>
#include <stdint.h>
#include <stddef.h>
struct ShineSecretGenerationStatus {
bool running;
bool done;
bool error;
uint32_t doneBlocks;
uint32_t totalBlocks;
uint32_t elapsedSec;
char message[96];
char secretBase58[64];
};
bool shineSecretInitSd(String &error);
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error);
bool shineSecretStep(uint32_t blocksPerTick = 4);
void shineSecretAbort();
ShineSecretGenerationStatus shineSecretStatus();
const uint8_t *shineSecretBytes();
const char *shineSecretBase58();
void shineSecretBase58Encode(const uint8_t *data, size_t len, char *out, size_t outSz);
bool shineSecretBase58Decode(const char *input, uint8_t *out, size_t *outLen, size_t maxOutLen, String &error);

View File

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

View File

@ -0,0 +1 @@
#include "../../official-demo/examples/Arduino-v3.3.5/libraries/lvgl/src/extra/libs/qrcode/qrcodegen.c"

View File

@ -0,0 +1,136 @@
/*
* ESPRESSIF MIT License
*
* Copyright (c) 2018 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
*
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
* it is free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
#ifndef _AUDIO_HAL_H_
#define _AUDIO_HAL_H_
#define AUDIO_HAL_VOL_DEFAULT 60
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Select media hal codec mode
*/
typedef enum {
AUDIO_HAL_CODEC_MODE_ENCODE = 1, /*!< select adc */
AUDIO_HAL_CODEC_MODE_DECODE, /*!< select dac */
AUDIO_HAL_CODEC_MODE_BOTH, /*!< select both adc and dac */
AUDIO_HAL_CODEC_MODE_LINE_IN, /*!< set adc channel */
} audio_hal_codec_mode_t;
/**
* @brief Select adc channel for input mic signal
*/
typedef enum {
AUDIO_HAL_ADC_INPUT_LINE1 = 0x00, /*!< mic input to adc channel 1 */
AUDIO_HAL_ADC_INPUT_LINE2, /*!< mic input to adc channel 2 */
AUDIO_HAL_ADC_INPUT_ALL, /*!< mic input to both channels of adc */
AUDIO_HAL_ADC_INPUT_DIFFERENCE, /*!< mic input to adc difference channel */
} audio_hal_adc_input_t;
/**
* @brief Select channel for dac output
*/
typedef enum {
AUDIO_HAL_DAC_OUTPUT_LINE1 = 0x00, /*!< dac output signal to channel 1 */
AUDIO_HAL_DAC_OUTPUT_LINE2, /*!< dac output signal to channel 2 */
AUDIO_HAL_DAC_OUTPUT_ALL, /*!< dac output signal to both channels */
} audio_hal_dac_output_t;
/**
* @brief Select operating mode i.e. start or stop for audio codec chip
*/
typedef enum {
AUDIO_HAL_CTRL_STOP = 0x00, /*!< set stop mode */
AUDIO_HAL_CTRL_START = 0x01, /*!< set start mode */
} audio_hal_ctrl_t;
/**
* @brief Select I2S interface operating mode i.e. master or slave for audio codec chip
*/
typedef enum {
AUDIO_HAL_MODE_SLAVE = 0x00, /*!< set slave mode */
AUDIO_HAL_MODE_MASTER = 0x01, /*!< set master mode */
} audio_hal_iface_mode_t;
/**
* @brief Select I2S interface samples per second
*/
typedef enum {
AUDIO_HAL_08K_SAMPLES, /*!< set to 8k samples per second */
AUDIO_HAL_11K_SAMPLES, /*!< set to 11.025k samples per second */
AUDIO_HAL_16K_SAMPLES, /*!< set to 16k samples in per second */
AUDIO_HAL_22K_SAMPLES, /*!< set to 22.050k samples per second */
AUDIO_HAL_24K_SAMPLES, /*!< set to 24k samples in per second */
AUDIO_HAL_32K_SAMPLES, /*!< set to 32k samples in per second */
AUDIO_HAL_44K_SAMPLES, /*!< set to 44.1k samples per second */
AUDIO_HAL_48K_SAMPLES, /*!< set to 48k samples per second */
} audio_hal_iface_samples_t;
/**
* @brief Select I2S interface number of bits per sample
*/
typedef enum {
AUDIO_HAL_BIT_LENGTH_16BITS = 1, /*!< set 16 bits per sample */
AUDIO_HAL_BIT_LENGTH_24BITS, /*!< set 24 bits per sample */
AUDIO_HAL_BIT_LENGTH_32BITS, /*!< set 32 bits per sample */
} audio_hal_iface_bits_t;
/**
* @brief Select I2S interface format for audio codec chip
*/
typedef enum {
AUDIO_HAL_I2S_NORMAL = 0, /*!< set normal I2S format */
AUDIO_HAL_I2S_LEFT, /*!< set all left format */
AUDIO_HAL_I2S_RIGHT, /*!< set all right format */
AUDIO_HAL_I2S_DSP, /*!< set dsp/pcm format */
} audio_hal_iface_format_t;
/**
* @brief I2s interface configuration for audio codec chip
*/
typedef struct {
audio_hal_iface_mode_t mode; /*!< audio codec chip mode */
audio_hal_iface_format_t fmt; /*!< I2S interface format */
audio_hal_iface_samples_t samples; /*!< I2S interface samples per second */
audio_hal_iface_bits_t bits; /*!< i2s interface number of bits per sample */
} audio_hal_codec_i2s_iface_t;
/**
* @brief Configure media hal for initialization of audio codec chip
*/
typedef struct {
audio_hal_adc_input_t adc_input; /*!< set adc channel */
audio_hal_dac_output_t dac_output; /*!< set dac channel */
audio_hal_codec_mode_t codec_mode; /*!< select codec mode: adc, dac or both */
audio_hal_codec_i2s_iface_t i2s_iface; /*!< set I2S interface configuration */
} audio_hal_codec_config_t;
#ifdef __cplusplus
}
#endif
#endif //__AUDIO_HAL_H__

View File

@ -0,0 +1,549 @@
/*
* ESPRESSIF MIT License
*
* Copyright (c) 2021 <ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD>
*
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
* it is free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
#ifdef ESP32
#include <Wire.h>
#include <string.h>
#include "esp_log.h"
#include "es7210.h"
#define I2S_DSP_MODE_A 0
#define MCLK_DIV_FRE 256
#define ES7210_MCLK_SOURCE FROM_CLOCK_DOUBLE_PIN /* In master mode, 0 : MCLK from pad 1 : MCLK from clock doubler */
#define FROM_PAD_PIN 0
#define FROM_CLOCK_DOUBLE_PIN 1
static TwoWire *es7210wire;
static es7210_gain_value_t gain;
/*
* Clock coefficient structer
*/
struct _coeff_div_es7210 {
uint32_t mclk; /* mclk frequency */
uint32_t lrck; /* lrck */
uint8_t ss_ds;
uint8_t adc_div; /* adcclk divider */
uint8_t dll; /* dll_bypass */
uint8_t doubler; /* doubler enable */
uint8_t osr; /* adc osr */
uint8_t mclk_src; /* select mclk source */
uint32_t lrck_h; /* The high 4 bits of lrck */
uint32_t lrck_l; /* The low 8 bits of lrck */
};
static const char *TAG = "ES7210";
static es7210_input_mics_t mic_select = (es7210_input_mics_t)(ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 | ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4);
/* Codec hifi mclk clock divider coefficients
* MEMBER REG
* mclk: 0x03
* lrck: standard
* ss_ds: --
* adc_div: 0x02
* dll: 0x06
* doubler: 0x02
* osr: 0x07
* mclk_src: 0x03
* lrckh: 0x04
* lrckl: 0x05
*/
static const struct _coeff_div_es7210 coeff_div[] = {
//mclk lrck ss_ds adc_div dll doubler osr mclk_src lrckh lrckl
/* 8k */
{12288000, 8000 , 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00},
{16384000, 8000 , 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00},
{19200000, 8000 , 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60},
{4096000, 8000 , 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
/* 11.025k */
{11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
/* 12k */
{12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
{19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40},
/* 16k */
{4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
{19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80},
{16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
{12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00},
/* 22.05k */
{11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
/* 24k */
{12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
{19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20},
/* 32k */
{12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80},
{16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
{19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58},
/* 44.1k */
{11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
/* 48k */
{12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
{19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90},
/* 64k */
{16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
{19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c},
/* 88.2k */
{11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
/* 96k */
{12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
{19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8},
};
static esp_err_t es7210_write_reg(uint8_t reg_addr, uint8_t data)
{
es7210wire->beginTransmission(ES7210_ADDR);
es7210wire->write(reg_addr);
es7210wire->write(data);
return es7210wire->endTransmission();
}
static esp_err_t es7210_update_reg_bit(uint8_t reg_addr, uint8_t update_bits, uint8_t data)
{
uint8_t regv;
regv = es7210_read_reg(reg_addr);
regv = (regv & (~update_bits)) | (update_bits & data);
return es7210_write_reg(reg_addr, regv);
}
static int get_coeff(uint32_t mclk, uint32_t lrck)
{
for (int i = 0; i < (sizeof(coeff_div) / sizeof(coeff_div[0])); i++) {
if (coeff_div[i].lrck == lrck && coeff_div[i].mclk == mclk)
return i;
}
return -1;
}
int8_t get_es7210_mclk_src(void)
{
return ES7210_MCLK_SOURCE;
}
int es7210_read_reg(uint8_t reg_addr)
{
uint8_t data;
es7210wire->beginTransmission(ES7210_ADDR);
es7210wire->write(reg_addr);
es7210wire->endTransmission(false);
es7210wire->requestFrom(ES7210_ADDR, (size_t)1);
data = es7210wire->read();
return (int)data;
}
esp_err_t es7210_config_sample(audio_hal_iface_samples_t sample)
{
uint8_t regv;
int coeff;
int sample_fre = 0;
int mclk_fre = 0;
esp_err_t ret = ESP_OK;
switch (sample) {
case AUDIO_HAL_08K_SAMPLES:
sample_fre = 8000;
break;
case AUDIO_HAL_11K_SAMPLES:
sample_fre = 11025;
break;
case AUDIO_HAL_16K_SAMPLES:
sample_fre = 16000;
break;
case AUDIO_HAL_22K_SAMPLES:
sample_fre = 22050;
break;
case AUDIO_HAL_24K_SAMPLES:
sample_fre = 24000;
break;
case AUDIO_HAL_32K_SAMPLES:
sample_fre = 32000;
break;
case AUDIO_HAL_44K_SAMPLES:
sample_fre = 44100;
break;
case AUDIO_HAL_48K_SAMPLES:
sample_fre = 48000;
break;
default:
ESP_LOGE(TAG, "Unable to configure sample rate %dHz", sample_fre);
break;
}
mclk_fre = sample_fre * MCLK_DIV_FRE;
coeff = get_coeff(mclk_fre, sample_fre);
if (coeff < 0) {
ESP_LOGE(TAG, "Unable to configure sample rate %dHz with %dHz MCLK", sample_fre, mclk_fre);
return ESP_FAIL;
}
/* Set clock parammeters */
if (coeff >= 0) {
/* Set adc_div & doubler & dll */
regv = es7210_read_reg(ES7210_MAINCLK_REG02) & 0x00;
regv |= coeff_div[coeff].adc_div;
regv |= coeff_div[coeff].doubler << 6;
regv |= coeff_div[coeff].dll << 7;
ret |= es7210_write_reg(ES7210_MAINCLK_REG02, regv);
/* Set osr */
regv = coeff_div[coeff].osr;
ret |= es7210_write_reg(ES7210_OSR_REG07, regv);
/* Set lrck */
regv = coeff_div[coeff].lrck_h;
ret |= es7210_write_reg(ES7210_LRCK_DIVH_REG04, regv);
regv = coeff_div[coeff].lrck_l;
ret |= es7210_write_reg(ES7210_LRCK_DIVL_REG05, regv);
}
return ret;
}
esp_err_t es7210_mic_select(es7210_input_mics_t mic)
{
esp_err_t ret = ESP_OK;
mic_select = mic;
if (mic_select & (ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 | ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4)) {
for (int i = 0; i < 4; i++) {
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00);
}
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0xff);
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0xff);
if (mic_select & ES7210_INPUT_MIC1) {
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC1");
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00);
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0x00);
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x10, 0x10);
}
if (mic_select & ES7210_INPUT_MIC2) {
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC2");
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00);
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0x00);
ret |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x10, 0x10);
}
if (mic_select & ES7210_INPUT_MIC3) {
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC3");
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x15, 0x00);
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0x00);
ret |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x10, 0x10);
}
if (mic_select & ES7210_INPUT_MIC4) {
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC4");
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x15, 0x00);
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0x00);
ret |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x10, 0x10);
}
} else {
ESP_LOGE(TAG, "Microphone selection error");
return ESP_FAIL;
}
return ret;
}
esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg)
{
esp_err_t ret = ESP_OK;
es7210wire = tw;
ret |= es7210_write_reg(ES7210_RESET_REG00, 0xff);
ret |= es7210_write_reg(ES7210_RESET_REG00, 0x41);
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, 0x1f);
ret |= es7210_write_reg(ES7210_TIME_CONTROL0_REG09, 0x30); /* Set chip state cycle */
ret |= es7210_write_reg(ES7210_TIME_CONTROL1_REG0A, 0x30); /* Set power on state cycle */
// ret |= es7210_write_reg(ES7210_ADC12_HPF2_REG23, 0x2a); /* Quick setup */
// ret |= es7210_write_reg(ES7210_ADC12_HPF1_REG22, 0x0a);
// ret |= es7210_write_reg(ES7210_ADC34_HPF2_REG20, 0x0a);
// ret |= es7210_write_reg(ES7210_ADC34_HPF1_REG21, 0x2a);
/* Set master/slave audio interface */
audio_hal_codec_i2s_iface_t *i2s_cfg = & (codec_cfg->i2s_iface);
switch (i2s_cfg->mode) {
case AUDIO_HAL_MODE_MASTER: /* MASTER MODE */
ESP_LOGI(TAG, "ES7210 in Master mode");
// ret |= es7210_update_reg_bit(ES7210_MODE_CONFIG_REG08, 0x01, 0x01);
ret |= es7210_write_reg(ES7210_MODE_CONFIG_REG08, 0x20);
/* Select clock source for internal mclk */
switch (get_es7210_mclk_src()) {
case FROM_PAD_PIN:
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x00);
break;
case FROM_CLOCK_DOUBLE_PIN:
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x80);
break;
default:
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x00);
break;
}
break;
case AUDIO_HAL_MODE_SLAVE: /* SLAVE MODE */
ESP_LOGI(TAG, "ES7210 in Slave mode");
break;
default:
break;
}
ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0xC3); /* Select power off analog, vdda = 3.3V, close vx20ff, VMID select 5KΩ start */
ret |= es7210_write_reg(ES7210_MIC12_BIAS_REG41, 0x70); /* Select 2.87v */
ret |= es7210_write_reg(ES7210_MIC34_BIAS_REG42, 0x70); /* Select 2.87v */
ret |= es7210_write_reg(ES7210_OSR_REG07, 0x20);
ret |= es7210_write_reg(ES7210_MAINCLK_REG02, 0xc1); /* Set the frequency division coefficient and use dll except clock doubler, and need to set 0xc1 to clear the state */
ret |= es7210_config_sample(i2s_cfg->samples);
ret |= es7210_mic_select(mic_select);
ret |= es7210_adc_set_gain_all(GAIN_0DB);
return ESP_OK;
}
esp_err_t es7210_adc_deinit()
{
return ESP_OK;
}
esp_err_t es7210_config_fmt(audio_hal_iface_format_t fmt)
{
esp_err_t ret = ESP_OK;
uint8_t adc_iface = 0;
adc_iface = es7210_read_reg(ES7210_SDP_INTERFACE1_REG11);
adc_iface &= 0xfc;
switch (fmt) {
case AUDIO_HAL_I2S_NORMAL:
ESP_LOGD(TAG, "ES7210 in I2S Format");
adc_iface |= 0x00;
break;
case AUDIO_HAL_I2S_LEFT:
case AUDIO_HAL_I2S_RIGHT:
ESP_LOGD(TAG, "ES7210 in LJ Format");
adc_iface |= 0x01;
break;
case AUDIO_HAL_I2S_DSP:
if (I2S_DSP_MODE_A) {
ESP_LOGD(TAG, "ES7210 in DSP-A Format");
adc_iface |= 0x03;
} else {
ESP_LOGD(TAG, "ES7210 in DSP-B Format");
adc_iface |= 0x13;
}
break;
default:
adc_iface &= 0xfc;
break;
}
ret |= es7210_write_reg(ES7210_SDP_INTERFACE1_REG11, adc_iface);
/* Force ADC1/2 output to SDOUT1 and ADC3/4 output to SDOUT2 */
ret |= es7210_write_reg(ES7210_SDP_INTERFACE2_REG12, 0x00);
return ret;
}
esp_err_t es7210_set_bits(audio_hal_iface_bits_t bits)
{
esp_err_t ret = ESP_OK;
uint8_t adc_iface = 0;
adc_iface = es7210_read_reg(ES7210_SDP_INTERFACE1_REG11);
adc_iface &= 0x1f;
switch (bits) {
case AUDIO_HAL_BIT_LENGTH_16BITS:
adc_iface |= 0x60;
break;
case AUDIO_HAL_BIT_LENGTH_24BITS:
adc_iface |= 0x00;
break;
case AUDIO_HAL_BIT_LENGTH_32BITS:
adc_iface |= 0x80;
break;
default:
adc_iface |= 0x60;
break;
}
ret |= es7210_write_reg(ES7210_SDP_INTERFACE1_REG11, adc_iface);
return ret;
}
esp_err_t es7210_adc_config_i2s(audio_hal_codec_mode_t mode, audio_hal_codec_i2s_iface_t *iface)
{
esp_err_t ret = ESP_OK;
ret |= es7210_set_bits(iface->bits);
ret |= es7210_config_fmt(iface->fmt);
ret |= es7210_config_sample(iface->samples);
return ret;
}
esp_err_t es7210_start(uint8_t clock_reg_value)
{
esp_err_t ret = ESP_OK;
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, clock_reg_value);
ret |= es7210_write_reg(ES7210_POWER_DOWN_REG06, 0x00);
// ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0x40);
ret |= es7210_write_reg(ES7210_MIC1_POWER_REG47, 0x00);
ret |= es7210_write_reg(ES7210_MIC2_POWER_REG48, 0x00);
ret |= es7210_write_reg(ES7210_MIC3_POWER_REG49, 0x00);
ret |= es7210_write_reg(ES7210_MIC4_POWER_REG4A, 0x00);
ret |= es7210_mic_select(mic_select);
return ret;
}
esp_err_t es7210_stop(void)
{
esp_err_t ret = ESP_OK;
ret |= es7210_write_reg(ES7210_MIC1_POWER_REG47, 0xff);
ret |= es7210_write_reg(ES7210_MIC2_POWER_REG48, 0xff);
ret |= es7210_write_reg(ES7210_MIC3_POWER_REG49, 0xff);
ret |= es7210_write_reg(ES7210_MIC4_POWER_REG4A, 0xff);
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B,0xff);
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0xff);
// ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0xc0);
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, 0x7f);
ret |= es7210_write_reg(ES7210_POWER_DOWN_REG06, 0x07);
return ret;
}
esp_err_t es7210_adc_ctrl_state(audio_hal_codec_mode_t mode, audio_hal_ctrl_t ctrl_state)
{
static uint8_t regv;
esp_err_t ret = ESP_OK;
// ESP_LOGW(TAG, "ES7210 only supports ADC mode");
ret = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
if ((ret != 0x7f) && (ret != 0xff)) {
regv = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
}
if (ctrl_state == AUDIO_HAL_CTRL_START) {
ESP_LOGI(TAG, "The ES7210_CLOCK_OFF_REG01 value before stop is %x",regv);
ret |= es7210_start(regv);
} else {
ESP_LOGW(TAG, "The codec is about to stop");
regv = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
ret |= es7210_stop();
}
return ESP_OK;
}
esp_err_t es7210_adc_set_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t gain)
{
esp_err_t ret_val = ESP_OK;
if (gain < GAIN_0DB) {
gain = GAIN_0DB;
}
if (gain > GAIN_37_5DB) {
gain = GAIN_37_5DB;
}
if (mic_mask & ES7210_INPUT_MIC1) {
ret_val |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x0f, gain);
}
if (mic_mask & ES7210_INPUT_MIC2) {
ret_val |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x0f, gain);
}
if (mic_mask & ES7210_INPUT_MIC3) {
ret_val |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x0f, gain);
}
if (mic_mask & ES7210_INPUT_MIC4) {
ret_val |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x0f, gain);
}
return ret_val;
}
esp_err_t es7210_adc_set_gain_all(es7210_gain_value_t gain)
{
esp_err_t ret = ESP_OK;
uint32_t max_gain_vaule = 14;
if (gain < 0) {
gain = (es7210_gain_value_t) 0;
} else if (gain > max_gain_vaule) {
gain = (es7210_gain_value_t) max_gain_vaule;
}
ESP_LOGD(TAG, "SET: gain:%d", gain);
if (mic_select & ES7210_INPUT_MIC1) {
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x0f, gain);
}
if (mic_select & ES7210_INPUT_MIC2) {
ret |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x0f, gain);
}
if (mic_select & ES7210_INPUT_MIC3) {
ret |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x0f, gain);
}
if (mic_select & ES7210_INPUT_MIC4) {
ret |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x0f, gain);
}
return ret;
}
esp_err_t es7210_adc_get_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t *gain)
{
int regv = 0;
uint8_t gain_value;
if (mic_mask & ES7210_INPUT_MIC1) {
regv = es7210_read_reg(ES7210_MIC1_GAIN_REG43);
} else if (mic_mask & ES7210_INPUT_MIC2) {
regv = es7210_read_reg(ES7210_MIC2_GAIN_REG44);
} else if (mic_mask & ES7210_INPUT_MIC3) {
regv = es7210_read_reg(ES7210_MIC3_GAIN_REG45);
} else if (mic_mask & ES7210_INPUT_MIC4) {
regv = es7210_read_reg(ES7210_MIC4_GAIN_REG46);
} else {
ESP_LOGE(TAG, "No MIC selected");
return ESP_FAIL;
}
if (regv == ESP_FAIL) {
return regv;
}
gain_value = (regv & 0x0f); /* Retain the last four bits for gain */
*gain = (es7210_gain_value_t) gain_value;
ESP_LOGI(TAG, "GET: gain_value:%d", gain_value);
return ESP_OK;
}
esp_err_t es7210_adc_set_volume(int volume)
{
esp_err_t ret = ESP_OK;
ESP_LOGD(TAG, "ADC can adjust gain");
return ret;
}
esp_err_t es7210_set_mute(bool enable)
{
ESP_LOGD(TAG, "ES7210 SetMute :%d", enable);
return ESP_OK;
}
void es7210_read_all(void)
{
for (int i = 0; i <= 0x4E; i++) {
uint8_t reg = es7210_read_reg(i);
ets_printf("REG:%02x, %02x\n", reg, i);
}
}
#endif

View File

@ -0,0 +1,260 @@
/*
* ESPRESSIF MIT License
*
* Copyright (c) 2021 <ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD>
*
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
* it is free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
#ifndef _ES7210_H
#define _ES7210_H
#include "audio_hal.h"
#include <Wire.h>
typedef enum {
ES7210_AD1_AD0_00 = 0x40,
ES7210_AD1_AD0_01 = 0x41,
ES7210_AD1_AD0_10 = 0x42,
ES7210_AD1_AD0_11 = 0x43,
} es7210_address_t;
/* ES7210 address*/
#define ES7210_ADDR ES7210_AD1_AD0_00
#ifdef __cplusplus
extern "C" {
#endif
#define ES7210_RESET_REG00 0x00 /* Reset control */
#define ES7210_CLOCK_OFF_REG01 0x01 /* Used to turn off the ADC clock */
#define ES7210_MAINCLK_REG02 0x02 /* Set ADC clock frequency division */
#define ES7210_MASTER_CLK_REG03 0x03 /* MCLK source $ SCLK division */
#define ES7210_LRCK_DIVH_REG04 0x04 /* lrck_divh */
#define ES7210_LRCK_DIVL_REG05 0x05 /* lrck_divl */
#define ES7210_POWER_DOWN_REG06 0x06 /* power down */
#define ES7210_OSR_REG07 0x07
#define ES7210_MODE_CONFIG_REG08 0x08 /* Set master/slave & channels */
#define ES7210_TIME_CONTROL0_REG09 0x09 /* Set Chip intial state period*/
#define ES7210_TIME_CONTROL1_REG0A 0x0A /* Set Power up state period */
#define ES7210_SDP_INTERFACE1_REG11 0x11 /* Set sample & fmt */
#define ES7210_SDP_INTERFACE2_REG12 0x12 /* Pins state */
#define ES7210_ADC_AUTOMUTE_REG13 0x13 /* Set mute */
#define ES7210_ADC34_MUTERANGE_REG14 0x14 /* Set mute range */
#define ES7210_ADC34_HPF2_REG20 0x20 /* HPF */
#define ES7210_ADC34_HPF1_REG21 0x21
#define ES7210_ADC12_HPF1_REG22 0x22
#define ES7210_ADC12_HPF2_REG23 0x23
#define ES7210_ANALOG_REG40 0x40 /* ANALOG Power */
#define ES7210_MIC12_BIAS_REG41 0x41
#define ES7210_MIC34_BIAS_REG42 0x42
#define ES7210_MIC1_GAIN_REG43 0x43
#define ES7210_MIC2_GAIN_REG44 0x44
#define ES7210_MIC3_GAIN_REG45 0x45
#define ES7210_MIC4_GAIN_REG46 0x46
#define ES7210_MIC1_POWER_REG47 0x47
#define ES7210_MIC2_POWER_REG48 0x48
#define ES7210_MIC3_POWER_REG49 0x49
#define ES7210_MIC4_POWER_REG4A 0x4A
#define ES7210_MIC12_POWER_REG4B 0x4B /* MICBias & ADC & PGA Power */
#define ES7210_MIC34_POWER_REG4C 0x4C
typedef enum {
ES7210_INPUT_MIC1 = 0x01,
ES7210_INPUT_MIC2 = 0x02,
ES7210_INPUT_MIC3 = 0x04,
ES7210_INPUT_MIC4 = 0x08
} es7210_input_mics_t;
typedef enum gain_value{
GAIN_0DB = 0,
GAIN_3DB,
GAIN_6DB,
GAIN_9DB,
GAIN_12DB,
GAIN_15DB,
GAIN_18DB,
GAIN_21DB,
GAIN_24DB,
GAIN_27DB,
GAIN_30DB,
GAIN_33DB,
GAIN_34_5DB,
GAIN_36DB,
GAIN_37_5DB,
} es7210_gain_value_t;
/*
* @brief Initialize ES7210 ADC chip
*
* @param[in] codec_cfg: configuration of ES7210
*
* @return
* - ESP_OK
* - ESP_FAIL
*/
esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg);
/**
* @brief Deinitialize ES7210 ADC chip
*
* @return
* - ESP_OK
* - ESP_FAIL
*/
esp_err_t es7210_adc_deinit();
/**
* @brief Configure ES7210 ADC mode and I2S interface
*
* @param[in] mode: codec mode
* @param[in] iface: I2S config
*
* @return
* - ESP_FAIL Parameter error
* - ESP_OK Success
*/
esp_err_t es7210_adc_config_i2s(audio_hal_codec_mode_t mode, audio_hal_codec_i2s_iface_t *iface);
/**
* @brief Control ES7210 ADC chip
*
* @param[in] mode: codec mode
* @param[in] ctrl_state: start or stop progress
*
* @return
* - ESP_FAIL Parameter error
* - ESP_OK Success
*/
esp_err_t es7210_adc_ctrl_state(audio_hal_codec_mode_t mode, audio_hal_ctrl_t ctrl_state);
/**
* @brief Set gain of given mask
*
* @param[in] mic_mask Mask of MIC channel
*
* @param[in] gain: gain
*
* gain : value
* GAIN_0DB : 1
* GAIN_3DB : 2
* GAIN_6DB : 3
* ·
* ·
* ·
* GAIN_30DB : 10
* GAIN_33DB : 11
* GAIN_34_5DB : 12
* GAIN_36DB : 13
* GAIN_37_5DB : 14
*
* @return
* - ESP_OK
* - ESP_FAIL
*/
esp_err_t es7210_adc_set_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t gain);
/**
* @brief Set gain (Note: the enabled microphone sets the same gain)
*
* @param[in] gain: gain
*
* gain : value
* GAIN_0DB : 1
* GAIN_3DB : 2
* GAIN_6DB : 3
* ·
* ·
* ·
* GAIN_30DB : 10
* GAIN_33DB : 11
* GAIN_34_5DB : 12
* GAIN_36DB : 13
* GAIN_37_5DB : 14
*
* @return
* - ESP_OK
* - ESP_FAIL
*/
esp_err_t es7210_adc_set_gain_all(es7210_gain_value_t gain);
/**
* @brief Get MIC gain
*
* @param mic_mask Selected MIC
* @param gain Pointer to `es7210_gain_value_t`
* @return
* - ESP_OK
* - ESP_FAIL
*/
esp_err_t es7210_adc_get_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t *gain);
/**
* @brief Set volume
*
* @param[in] volume: volume
*
* @return
* - ESP_OK
*/
esp_err_t es7210_adc_set_volume(int volume);
/**
* @brief Set ES7210 ADC mute status
*
* @return
* - ESP_FAIL
* - ESP_OK
*/
esp_err_t es7210_set_mute(bool enable);
/**
* @brief Select ES7210 mic
*
* @param[in] mic: mics
*
* @return
* - ESP_FAIL
* - ESP_OK
*/
esp_err_t es7210_mic_select(es7210_input_mics_t mic);
/**
* @brief Read regs of ES7210
*
* @param[in] reg_addr: reg_addr
*
* @return
* - ESP_FAIL
* - ESP_OK
*/
int es7210_read_reg(uint8_t reg_addr);
/**
* @brief Read all regs of ES7210
*/
void es7210_read_all(void);
#ifdef __cplusplus
}
#endif
#endif /* _ES7210_H_ */

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