SHA256
Compare commits
229
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
8f32e82d14 | ||
|
|
4d80ab639e | ||
|
|
95bbd2852e | ||
|
|
4b0a934e51 | ||
|
|
ee5d86003b | ||
|
|
bc11b60147 | ||
|
|
801a4697ea | ||
|
|
628a970e6f | ||
|
|
566ea942dc | ||
|
|
e047c2d54e | ||
|
|
3fdd3d22e2 | ||
|
|
08e0fa9206 | ||
|
|
ed9e6ce676 | ||
|
|
a7bf07130e | ||
|
|
7c31662698 | ||
|
|
15df06a0bc | ||
|
|
47b5f8db7b | ||
|
|
0e763987aa | ||
|
|
37e8ea27d0 | ||
|
|
67d63256c2 | ||
|
|
ee185cf20a | ||
|
|
3552e05e4c | ||
|
|
735bb55b78 | ||
|
|
60f8a50a97 | ||
|
|
ffd01814b0 | ||
|
|
c511910b8f | ||
|
|
5c38f8f0d8 | ||
|
|
d512442325 | ||
|
|
43f54c90d2 | ||
|
|
57a99b478a | ||
|
|
3f402fbde7 | ||
|
|
98140c1f71 | ||
|
|
f00de85e38 | ||
|
|
b686b12c6a | ||
|
|
e4b903b13c | ||
|
|
72f576a70e | ||
|
|
c4f2c7be63 | ||
|
|
75b75fd223 | ||
|
|
391b18a5ac | ||
|
|
8de0f8e87c | ||
|
|
adb0b76559 | ||
|
|
b6df25c02c | ||
|
|
620186db1c | ||
|
|
21fac8046e | ||
|
|
de7c35f587 | ||
|
|
a23c979045 | ||
|
|
13da7287b1 | ||
|
|
e8398df9fc | ||
|
|
8303682bd7 | ||
|
|
8125d1a124 | ||
|
|
110ff5e12e | ||
|
|
24afcba20a | ||
|
|
d5f41d12e8 | ||
|
|
d640dd66e2 | ||
|
|
05bd9f57d6 | ||
|
|
60ab476ec7 | ||
|
|
bcae285ab3 | ||
|
|
cd67176b41 | ||
|
|
98eba54621 | ||
|
|
c530627078 | ||
|
|
f961947e1e | ||
|
|
e9e6628b21 | ||
|
|
c7684d6fe1 | ||
|
|
7d475ab403 | ||
|
|
143adcbbe4 | ||
|
|
ca8b6a33ba | ||
|
|
2f73cdb0f9 | ||
|
|
d1e06d9ef1 | ||
|
|
0ed78e2202 | ||
|
|
64818c586b | ||
|
|
578f5cad4a | ||
|
|
d2ff277b16 | ||
|
|
df4f9b3f8e | ||
|
|
be7f53a9ae | ||
|
|
6ed91a105e | ||
|
|
0db3c3af5a | ||
|
|
23748504e6 | ||
|
|
f74fecddd8 | ||
|
|
618a30c2ab | ||
|
|
b5116474c7 | ||
|
|
91e7239866 | ||
|
|
1dddb5fb3c | ||
|
|
8ddf592ffc | ||
|
|
59a1117ed3 | ||
|
|
3bb6fa1e59 | ||
|
|
5af98ac911 | ||
|
|
f197196c21 | ||
|
|
ce5595bc16 | ||
|
|
1406111f22 | ||
|
|
bfdada6792 | ||
|
|
5326ad85d8 | ||
|
|
f9dd245481 | ||
|
|
20677a9090 | ||
|
|
d54cb7507a | ||
|
|
d6b313b582 | ||
|
|
f31cdb413b | ||
|
|
69faf55b2b | ||
|
|
4d9e42205f | ||
|
|
5df69d73c7 | ||
|
|
4704f8485b | ||
|
|
809c216a5c | ||
|
|
415a40e436 | ||
|
|
d233f1d73f | ||
|
|
9a7019a2ce | ||
|
|
07b1623c5e | ||
|
|
681437acb1 | ||
|
|
a183a586cb | ||
|
|
af62b51ef5 | ||
|
|
f640451257 | ||
|
|
a56252c361 | ||
|
|
38141ea5c7 | ||
|
|
d24d1c178b | ||
|
|
93673e5786 | ||
|
|
2f3b1571e5 | ||
|
|
65703f0fc4 | ||
|
|
4e674af4ed | ||
|
|
f9c2d7c63a | ||
|
|
968d85d738 | ||
|
|
d230b61c0b | ||
|
|
59047a8d0e | ||
|
|
81493ac4be | ||
|
|
cf33f9a3bd | ||
|
|
d2f65b169c | ||
|
|
fa1f7358b7 | ||
|
|
8208bb0b9d | ||
|
|
daa516babe | ||
|
|
6c2a9836eb | ||
|
|
bd3e6a56ca | ||
|
|
ea2ee1711c | ||
|
|
2890cdd457 | ||
|
|
e4dfb43b5e | ||
|
|
febdbc059e | ||
|
|
3926d561c0 | ||
|
|
d6bc883520 | ||
|
|
b2d20671cd | ||
|
|
a72e2e7014 | ||
|
|
cf6ca1d96b | ||
|
|
9d1e49949a | ||
|
|
ea3b34e11e | ||
|
|
09dc55db0e | ||
|
|
85133db483 | ||
|
|
6fc7d75e17 | ||
|
|
be321813fa | ||
|
|
d2c0ecdf62 | ||
|
|
266a74ef79 | ||
|
|
18d27c0146 | ||
|
|
24cca1f1c6 | ||
|
|
8c4997ee2e | ||
|
|
118be418b5 | ||
|
|
2a4c2c64c9 | ||
|
|
172f007e1e | ||
|
|
4cb98f7ca0 | ||
|
|
af12d7b954 | ||
|
|
c9cfb394d7 | ||
|
|
0d2d7b6dc5 | ||
|
|
076b43e542 | ||
|
|
cc1f78106d | ||
|
|
5a5e9c01ce | ||
|
|
0fb1147eb7 | ||
|
|
2b23aa7b95 | ||
|
|
bb92cb6b2b | ||
|
|
0730837ad7 | ||
|
|
98a2631590 | ||
|
|
faada9dcdb | ||
|
|
1b7a2a8f7c | ||
|
|
9c588bd9b5 | ||
|
|
31563fe9ed | ||
|
|
81bef1e1cc | ||
|
|
0240db59ee | ||
|
|
ab4dab34aa | ||
|
|
63f13a4c29 | ||
|
|
0a416a2f5c | ||
|
|
b0887648f7 | ||
|
|
5720f1cb50 | ||
|
|
9324da5cb7 | ||
|
|
408b0eeb39 | ||
|
|
ed83b1f906 | ||
|
|
3068c3e2b8 | ||
|
|
93c6f247f7 | ||
|
|
05a9441493 | ||
|
|
aa02e92e4d | ||
|
|
c397c28acb | ||
|
|
c93cc6c522 | ||
|
|
0cdcc77606 | ||
|
|
87eec7e5c9 | ||
|
|
44a1ba01f3 | ||
|
|
d49661fa29 | ||
|
|
71fdee0cfd | ||
|
|
1ced351ea2 | ||
|
|
c048347f2e | ||
|
|
be4f76834a | ||
|
|
23edad416c | ||
|
|
f3e4233285 | ||
|
|
84e0f039cb | ||
|
|
1f8b20a7d1 | ||
|
|
f0e1ab3af8 | ||
|
|
112ab4d5d5 | ||
|
|
8768e142e3 | ||
|
|
827d2e9c3e | ||
|
|
0f3c4a621d | ||
|
|
e60475f351 | ||
|
|
0f63f7dae6 | ||
|
|
127c561a41 | ||
|
|
f9a15ab192 | ||
|
|
77f5759d60 | ||
|
|
684f3237cf | ||
|
|
23e61cc182 | ||
|
|
d2f45ff67a | ||
|
|
06e12e9103 | ||
|
|
29dddeff4f | ||
|
|
017d568aea | ||
|
|
c91b52cfd2 | ||
|
|
2bd38d8d78 | ||
|
|
7d9db68d80 | ||
|
|
4b94303d67 | ||
|
|
08628704c7 | ||
|
|
f1c1132690 | ||
|
|
d2426c473c | ||
|
|
66986b804c | ||
|
|
95daa230bb | ||
|
|
365b22d778 | ||
|
|
cf2b54464e | ||
|
|
4e60c1274a | ||
|
|
2f65e63fbe | ||
|
|
b461431197 | ||
|
|
5c92b6a734 | ||
|
|
ba348dafb3 | ||
|
|
ce2d310e8c | ||
|
|
475db28095 |
+6
-3
@@ -13,6 +13,7 @@ build/
|
|||||||
.kotlin
|
.kotlin
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
|
.idea/
|
||||||
.idea/modules.xml
|
.idea/modules.xml
|
||||||
.idea/jarRepositories.xml
|
.idea/jarRepositories.xml
|
||||||
.idea/compiler.xml
|
.idea/compiler.xml
|
||||||
@@ -102,10 +103,12 @@ ESP32/**/*.d
|
|||||||
ESP32/**/*.a
|
ESP32/**/*.a
|
||||||
|
|
||||||
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
||||||
server-backup/archive/**
|
deploy/backup/archive/**
|
||||||
!server-backup/archive/.gitkeep
|
!deploy/backup/archive/.gitkeep
|
||||||
|
|
||||||
# Локальная дев-обвязка Claude (дев-сервер shine-UI, сессии, планы) — не коммитим
|
# Локальная дев-обвязка AI-агентов (сессии, планы, настройки) — не коммитим
|
||||||
|
.agents/
|
||||||
|
.codex/
|
||||||
.claude/
|
.claude/
|
||||||
# Рабочие бэкапы/превью-ассеты UI — не для репозитория
|
# Рабочие бэкапы/превью-ассеты UI — не для репозитория
|
||||||
*.bak.png
|
*.bak.png
|
||||||
|
|||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
Generated
-1
@@ -1 +0,0 @@
|
|||||||
shine-server-server
|
|
||||||
Generated
-10
@@ -1,10 +0,0 @@
|
|||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" build-on-make="true" name="server:jar">
|
|
||||||
<output-path>$PROJECT_DIR$/out/artifacts/server_jar</output-path>
|
|
||||||
<root id="archive" name="server.jar">
|
|
||||||
<element id="directory" name="META-INF">
|
|
||||||
<element id="file-copy" path="$PROJECT_DIR$/META-INF/MANIFEST.MF" />
|
|
||||||
</element>
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
||||||
Generated
-25
@@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
|
||||||
<component name="GradleSettings">
|
|
||||||
<option name="linkedExternalProjectsSettings">
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
|
||||||
<option name="gradleHome" value="" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$" />
|
|
||||||
<option value="$PROJECT_DIR$/shine-server-blockchain" />
|
|
||||||
<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>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-10
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (2)" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -14,14 +14,15 @@
|
|||||||
- Веб-панель администратора сервера (управление Solana PDA сервера) находится в `shine-UI/`:
|
- Веб-панель администратора сервера (управление Solana PDA сервера) находится в `shine-UI/`:
|
||||||
- точка входа `shine-UI/server-ui.html`;
|
- точка входа `shine-UI/server-ui.html`;
|
||||||
- остальные файлы серверного UI — в `shine-UI/server-ui/`.
|
- остальные файлы серверного UI — в `shine-UI/server-ui/`.
|
||||||
- Локальный Telegram-бот агента-кодера находится в папке `SHiNE-agent-bot-coder/` и не является кодом основного серверного приложения.
|
- Локальный Telegram-бот агента-кодера живёт рядом с репозиторием продукта, обычно в `../SHiNE-agent-bot-coder/`, и не входит в публичный код основного приложения.
|
||||||
- Solana/Anchor-модуль находится в папке `shine-solana/shine/` и ведётся отдельно от основного server/UI деплоя.
|
- Solana/Anchor-модуль находится в папке `shine-solana/shine/` и ведётся отдельно от основного server/UI деплоя.
|
||||||
|
|
||||||
## Сервис агента-кодера
|
## Сервис агента-кодера
|
||||||
- В проекте есть локальный Telegram-бот-сервис агента-кодера в папке `SHiNE-agent-bot-coder/`.
|
- Локальный Telegram-бот-сервис агента-кодера находится вне этого git-репозитория, обычно в `../SHiNE-agent-bot-coder/`.
|
||||||
- Сервис принимает сообщения из Telegram, ведёт историю диалога, ставит задачи в очередь и вызывает Codex CLI для обработки запросов по проекту.
|
- Сервис принимает сообщения из Telegram, ведёт историю диалога, ставит задачи в очередь и вызывает Codex CLI для обработки запросов по проекту.
|
||||||
- Автоматически читаемые инструкции для Codex внутри сервиса держать в `SHiNE-agent-bot-coder/AGENTS.md`.
|
- Рабочая папка Codex для сервиса должна указывать на этот продуктовый репозиторий: `CODEX_WORKDIR=/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-product`.
|
||||||
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
|
- Автоматически читаемые инструкции для Codex внутри сервиса держать в `../SHiNE-agent-bot-coder/AGENTS.md`.
|
||||||
|
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `../SHiNE-agent-bot-coder/AGENT.md`.
|
||||||
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
|
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
|
||||||
|
|
||||||
## ESP32 UI homeserver
|
## ESP32 UI homeserver
|
||||||
@@ -36,66 +37,67 @@
|
|||||||
- Модуль логически связан с SHiNE, но не должен автоматически подключаться к сборке или деплою основного сервера без отдельного решения.
|
- Модуль логически связан с SHiNE, но не должен автоматически подключаться к сборке или деплою основного сервера без отдельного решения.
|
||||||
- В Solana-модуле действуют локальные инструкции `shine-solana/shine/AGENTS.md`; при изменениях внутри модуля сначала читать их.
|
- В Solana-модуле действуют локальные инструкции `shine-solana/shine/AGENTS.md`; при изменениях внутри модуля сначала читать их.
|
||||||
- В git добавлять исходники, lock-файлы, настройки проекта и документацию Solana-модуля, но не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
|
- В 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 deploy/push использовать правила из локального `shine-solana/shine/AGENTS.md`; не смешивать deploy Solana-модуля со скриптами основного server/UI deploy из `deploy/scripts/`.
|
||||||
- Для регистрации пользователей в Solana (программа `shine_users`) единая актуальная инструкция по деплою/инициализации, адресам программ, и куда их прописывать в UI/сервере находится в:
|
- Для регистрации пользователей в Solana (программа `shine_users`) единая актуальная инструкция по деплою/инициализации, адресам программ, и куда их прописывать в UI/сервере находится в:
|
||||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
- `docs/Инициализация_Solana_регистрации/README.md`
|
||||||
- Этот файл считать основной справкой (single source of truth) по деплою и первичной инициализации Solana-регистрации в текущем проекте.
|
- Этот файл считать основной справкой (single source of truth) по деплою и первичной инициализации Solana-регистрации в текущем проекте.
|
||||||
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
|
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
|
||||||
- `Dev_Docs/Solana_Architecture/README.md`
|
- `docs/Solana_Architecture/README.md`
|
||||||
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
||||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||||
|
- Актуальная документация по серверному модулю синхронизации Solana users находится в:
|
||||||
|
- `docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md`
|
||||||
|
- При любом изменении логики серверной синхронизации `shine_users`, её таблиц PostgreSQL, checkpoint-механизма, startup/lifecycle или deploy-настроек обязательно обновлять:
|
||||||
|
- `docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md`
|
||||||
|
- `deploy/SOLANA_USERS_SYNC_SERVER_SETUP.md`
|
||||||
|
|
||||||
## Документация блокчейна
|
## Документация блокчейна
|
||||||
- Актуальная документация по форматам блокчейна находится в `Dev_Docs/Blockchain/README.md`.
|
- Актуальная документация по форматам блокчейна находится в `docs/Blockchain/README.md`.
|
||||||
- Это точка входа (оглавление), рядом расположены детальные файлы по форматам, типам каналов и командным сообщениям.
|
- Это точка входа (оглавление), рядом расположены детальные файлы по форматам, типам каналов и командным сообщениям.
|
||||||
- При любом изменении кода, связанного с блокчейном (формат блока, типы каналов, правила чтения/записи, команды), обязательно обновлять соответствующие документы в `Dev_Docs/Blockchain/`.
|
- При любом изменении кода, связанного с блокчейном (формат блока, типы каналов, правила чтения/записи, команды), обязательно обновлять соответствующие документы в `docs/Blockchain/`.
|
||||||
- Дополнительно обязательно вести `Dev_Docs/Blockchain/CHANGELOG.md`: дописывать изменения построчно с указанием даты/времени и хэша коммита, после которого внесено изменение.
|
- Дополнительно обязательно вести `docs/Blockchain/CHANGELOG.md`: дописывать изменения построчно с указанием даты/времени и хэша коммита, после которого внесено изменение.
|
||||||
- Перед любым изменением формата блокчейна обязательно заранее предупреждать пользователя, что формат будет изменён.
|
- Перед любым изменением формата блокчейна обязательно заранее предупреждать пользователя, что формат будет изменён.
|
||||||
- Изменять формат блокчейна можно только после явного подтверждения пользователя (без подтверждения формат не менять).
|
- Изменять формат блокчейна можно только после явного подтверждения пользователя (без подтверждения формат не менять).
|
||||||
- Добавление любых данных в блокчейн выполнять только через операцию `AddBlock`.
|
- Добавление любых данных в блокчейн выполнять только через операцию `AddBlock`.
|
||||||
- Перед каждым `AddBlock` обязательно проверять/актуализировать текущее состояние вершины блокчейна (`last global number/hash`) и использовать его при формировании блока.
|
- Перед каждым `AddBlock` обязательно проверять/актуализировать текущее состояние вершины блокчейна (`last global number/hash`) и использовать его при формировании блока.
|
||||||
|
|
||||||
## Документация личных сообщений (DM)
|
## Документация личных сообщений (DM)
|
||||||
- Актуальная документация по логике личных сообщений находится в `Dev_Docs/Personal_Messages/README.md`.
|
- Актуальная документация по логике личных сообщений находится в `docs/Personal_Messages/Протокол_DM_v1.md`.
|
||||||
- При любом изменении кода, связанного с личными сообщениями (формат подписанного DM-блока, типы DM-сообщений, правила доставки/ACK/read-receipt, роутинг по сессиям, UI-логика чатов), обязательно обновлять `Dev_Docs/Personal_Messages/README.md`.
|
- Точный байтовый формат DM находится в `docs/Personal_Messages/Формат_DM_v1.md`.
|
||||||
- Логика личных сообщений в коде должна всегда соответствовать `Dev_Docs/Personal_Messages/README.md`.
|
- При любом изменении кода, связанного с личными сообщениями (формат подписанного DM-блока, типы DM-сообщений, правила доставки/ACK/read-receipt, роутинг по сессиям, UI-логика чатов), обязательно обновлять оба документа:
|
||||||
|
- `docs/Personal_Messages/Протокол_DM_v1.md`
|
||||||
|
- `docs/Personal_Messages/Формат_DM_v1.md`
|
||||||
|
- Логика личных сообщений в коде должна всегда соответствовать этим документам.
|
||||||
- Документ по личным сообщениям обязан поддерживаться в актуальном состоянии.
|
- Документ по личным сообщениям обязан поддерживаться в актуальном состоянии.
|
||||||
|
|
||||||
## Документация API сервера
|
## Документация API сервера
|
||||||
- Актуальная документация по публичному JSON/WebSocket API сервера находится в `Dev_Docs/API/`.
|
- Актуальная документация по публичному JSON/WebSocket API сервера находится в `docs/API/`.
|
||||||
- При любом изменении серверного API/эндпоинтов/операций `op` обязательно обновлять соответствующие документы в `Dev_Docs/API/`.
|
- При любом изменении серверного API/эндпоинтов/операций `op` обязательно обновлять соответствующие документы в `docs/API/`.
|
||||||
- Перед изменением самого серверного API обязательно явно предупредить пользователя, какие операции, поля запросов/ответов или коды ошибок будут изменены, и запросить отдельное подтверждение.
|
- Перед изменением самого серверного API обязательно явно предупредить пользователя, какие операции, поля запросов/ответов или коды ошибок будут изменены, и запросить отдельное подтверждение.
|
||||||
- Без явного подтверждения пользователя формат серверного API не менять; допускается только приведение документации в соответствие уже существующему коду.
|
- Без явного подтверждения пользователя формат серверного API не менять; допускается только приведение документации в соответствие уже существующему коду.
|
||||||
- Если добавляется новая операция `op`, нужно обновить общий список операций в `Dev_Docs/API/09_Operations_Index.md` или создать его, если файла ещё нет.
|
- Если добавляется новая операция `op`, нужно обновить общий список операций в `docs/API/09_Operations_Index.md` или создать его, если файла ещё нет.
|
||||||
|
|
||||||
## Известная проблема (временная пометка)
|
## Документация Figma
|
||||||
- Мнения по связям пользователя (`known_person`, `shine_confirmed`, `shine_seen`) в UI могут отображаться нестабильно.
|
- Актуальная документация по переносу экранов SHiNE в Figma и обратному переносу из Figma в код находится в `docs/Figma/`.
|
||||||
- Требуется отдельная доработка и финальная проверка end-to-end: запись мнения в блокчейн → обновление `connections_state` → ответ `GetUserConnectionsGraph` → отображение в UI.
|
- Точка входа: `docs/Figma/README.md`.
|
||||||
- До фикса считать эту часть функционала незавершённой и обязательно перепроверять вручную после каждого деплоя.
|
- Подробный рабочий регламент: `docs/Figma/TRANSFER_UI_SCREENS.md`.
|
||||||
|
- Для экранов регистрации, входа и других чувствительных UI-flow по умолчанию переносить экраны в Figma по одному, а не пачкой, если пользователь отдельно не подтвердил иной способ.
|
||||||
|
|
||||||
## Версионирование
|
## Версионирование
|
||||||
- Единый файл версий проекта: `VERSION.properties` (в корне репозитория).
|
- Все правила по коммитам, merge в `main`, `git push` и обновлению `VERSION.properties` находятся в `COMMIT_AND_VERSION_RULES.md`.
|
||||||
- Перед каждым новым коммитом обязательно увеличивать версии в `VERSION.properties`:
|
- Этот файл считать единым источником истины по правилам версионирования и коммитов для данного репозитория.
|
||||||
- `client.version` — версия клиентского UI.
|
|
||||||
- `server.version` — версия серверной части.
|
|
||||||
- Базовое правило инкремента: `+1` по последнему числовому сегменту (patch), если не оговорено иное.
|
|
||||||
- Обычные коммиты делать стандартным `git commit`; переменная `$GITEA_TOKEN` для коммитов не нужна и не используется.
|
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
- Все документы и заметки по деплою хранить в папке `Dev_Docs/deploy/`.
|
- Все документы, инструкции, backup-правила и скрипты деплоя хранить в папке `deploy/`.
|
||||||
- Production-хост SHiNE: `player@shineup.me` (`185.229.109.118`).
|
- Подробные правила для агента по деплою находятся в `deploy/AGENTS.md`; перед любым deploy читать этот файл.
|
||||||
- Основной test-хост SHiNE: `player@193.8.215.70` (`test2.shineup.me`).
|
- Production-серверы SHiNE: `shineup.me` и `server2.shineup.me`.
|
||||||
- Резервный test-хост SHiNE: `player@93.170.12.154` (`test.shineup.me`).
|
- Тестовые/devnet серверы SHiNE: `t1.shineup.me`, `t2.shineup.me`, `t3.shineup.me`, `t4.shineup.me`.
|
||||||
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`).
|
- В deploy-документах и скриптах использовать домены, а не IP.
|
||||||
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
|
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
|
||||||
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
|
- Любые изменения и любой деплой на production (`shineup.me` и `server2.shineup.me`) выполнять только после отдельного явного подтверждения пользователя.
|
||||||
- Любые изменения и любой деплой на production `shineup.me` выполнять только после отдельного явного подтверждения пользователя.
|
- Перед production deploy обязательно проверить/обновить бэкап в `deploy/backup/archive/`.
|
||||||
- Если пользователь пишет просто `задеплой` без уточнения production/test, по умолчанию деплоить на `test2.shineup.me`.
|
- Если пользователь пишет просто `задеплой` без уточнения production/test, уточнить целевой контур; не выбирать production автоматически.
|
||||||
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`.
|
- Deploy выполнять shell-скриптами из `deploy/scripts/`; Gradle deploy-задачи не использовать.
|
||||||
- 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`).
|
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
|
||||||
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
|
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
|
||||||
- Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`.
|
- Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`.
|
||||||
@@ -121,30 +123,19 @@
|
|||||||
- `unknown_error`
|
- `unknown_error`
|
||||||
- В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
|
- В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
|
||||||
|
|
||||||
## Недопроверенные фичи (обязательно)
|
## Будущие фичи / TODO
|
||||||
- Папка для учёта недопроверенных фич: `Dev_Docs/Pending_Features/`.
|
- Папка для задач, сознательно отложенных на будущее: `TODO/`.
|
||||||
- По каждой новой доработке, которая требует ручной проверки, добавлять отдельный markdown-файл в `Dev_Docs/Pending_Features/`.
|
- Точка входа по планам: `TODO/README.md`.
|
||||||
- Рекомендуемый формат имени файла: `YYYY-MM-DD_HHMM_<short-feature-name>.md`.
|
- Внутри планы разделены по горизонтам: `near/`, `medium/`, `far/` и тематическим подпапкам.
|
||||||
- Имена новых файлов и краткие описания фич по возможности писать на русском языке.
|
- Если пользователь спрашивает, какие есть планы или что можно продолжить, сначала читать `TODO/README.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`, затем при необходимости конкретные файлы из горизонтов.
|
|
||||||
- Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя.
|
- Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя.
|
||||||
- Если часть кода временно отключена или закомментирована, в файле будущей фичи подробно описывать:
|
- Старую папку `docs/Future_Features/` считать выведенной из использования и больше не использовать для новых записей.
|
||||||
|
- Если часть кода временно отключена или закомментирована, либо удалена как временная заглушка, в TODO-файле подробно описывать:
|
||||||
- какие файлы и участки отключены;
|
- какие файлы и участки отключены;
|
||||||
- что осталось в коде как заготовка;
|
- что осталось в коде как заготовка;
|
||||||
- какие документы нужно обновить при возврате;
|
- какие документы нужно обновить при возврате;
|
||||||
- с какого сценария продолжать разработку.
|
- с какого сценария продолжать разработку;
|
||||||
|
- из какого коммита брать последнюю полную реализацию.
|
||||||
|
|
||||||
## Коммуникация по новым задачам (обязательно)
|
## Коммуникация по новым задачам (обязательно)
|
||||||
- При получении нового задания сначала кратко пересказать задачу своими словами.
|
- При получении нового задания сначала кратко пересказать задачу своими словами.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
@shine-UI/AGENTS.md
|
@shine-UI/AGENTS.md
|
||||||
|
|
||||||
## Справка по подпроектам
|
## Справка по подпроектам
|
||||||
- При работе внутри `SHiNE-agent-bot-coder/` — читать `SHiNE-agent-bot-coder/AGENTS.md` и `SHiNE-agent-bot-coder/AGENT.md`.
|
- При работе с локальным агентом-кодером — читать внешние файлы `../SHiNE-agent-bot-coder/AGENTS.md` и `../SHiNE-agent-bot-coder/AGENT.md`.
|
||||||
- При работе внутри `shine-solana/shine/` — читать `shine-solana/shine/AGENTS.md`.
|
- При работе внутри `shine-solana/shine/` — читать `shine-solana/shine/AGENTS.md`.
|
||||||
- При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`.
|
- При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`.
|
||||||
- При работе внутри `SHiNE-server/` — читать `SHiNE-server/AGENTS.md`.
|
- При работе внутри `SHiNE-server/` — читать `SHiNE-server/AGENTS.md`.
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Правила коммитов, merge и версионирования
|
||||||
|
|
||||||
|
Этот файл является единым источником правил для:
|
||||||
|
- коммитов;
|
||||||
|
- merge в `main`;
|
||||||
|
- изменения версий в `VERSION.properties`;
|
||||||
|
- `git push` из этого репозитория.
|
||||||
|
|
||||||
|
## Язык
|
||||||
|
|
||||||
|
- Пояснения к коммитам, PR и merge-запросам писать на русском языке.
|
||||||
|
|
||||||
|
## Где хранится версия
|
||||||
|
|
||||||
|
- Единый файл версий проекта: `VERSION.properties` в корне репозитория.
|
||||||
|
- Основные поля:
|
||||||
|
- `client.version` — версия клиентского UI.
|
||||||
|
- `server.version` — версия серверной части.
|
||||||
|
|
||||||
|
## Базовое правило для обычных коммитов
|
||||||
|
|
||||||
|
- Перед каждым новым коммитом обязательно обновлять версии в `VERSION.properties`.
|
||||||
|
- Если менялся только UI, увеличивать только `client.version`.
|
||||||
|
- Если менялся только сервер, увеличивать только `server.version`.
|
||||||
|
- Если менялись и UI, и сервер, увеличивать обе версии.
|
||||||
|
- Для обычных коммитов вне `main` использовать стандартный patch-инкремент: `+1` к последнему числовому сегменту.
|
||||||
|
- Пример: `1.2.346` → `1.2.347`.
|
||||||
|
|
||||||
|
## Правило для `main`
|
||||||
|
|
||||||
|
- Ветка `main` предназначена только для стабильных версий.
|
||||||
|
- По умолчанию в `main` нужно не коммитить напрямую, а мержить готовые изменения из рабочей ветки.
|
||||||
|
- Если пользователь просит сделать прямой коммит в `main`, нужно отдельно и явно предупредить, что это обход обычного стабильного процесса, и обязательно переспросить подтверждение.
|
||||||
|
|
||||||
|
## Версионирование при merge или прямом коммите в `main`
|
||||||
|
|
||||||
|
- Для попадания изменений в `main` действует отдельная схема инкремента.
|
||||||
|
- Нужно увеличивать вторую цифру версии и обнулять третью.
|
||||||
|
- Пример: `1.2.346` → `1.3.0`.
|
||||||
|
- Если в наборе изменений менялся только UI, обновлять только `client.version`.
|
||||||
|
- Если в наборе изменений менялся только сервер, обновлять только `server.version`.
|
||||||
|
- Если соответствующая часть не менялась, её версию не трогать.
|
||||||
|
|
||||||
|
## Правила для git commit и git push
|
||||||
|
|
||||||
|
- Обычные коммиты делать стандартным `git commit`; токен для локального коммита не нужен и не используется.
|
||||||
|
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
# DAO_запуск
|
|
||||||
|
|
||||||
Рабочий документ по тому, что ещё нужно сделать для первого запуска DAO-сценария SHiNE.
|
|
||||||
|
|
||||||
Логика документа:
|
|
||||||
|
|
||||||
- `этап1` — то, без чего нельзя считать сценарий первого запуска собранным даже в тестовом виде;
|
|
||||||
- `этап2` — то, что полезно и, вероятно, потребуется дальше, но это можно делать после старта `этап1` или параллельно без блокировки первого результата.
|
|
||||||
|
|
||||||
Базовая среда первого прохода:
|
|
||||||
|
|
||||||
- сеть `Solana devnet`;
|
|
||||||
- модель синхронизации: `server-to-server`;
|
|
||||||
- `Solana + Arweave` используются как якорь и архив;
|
|
||||||
- DAO понимается как стандартный governance/smart-contract контур, который управляет отдельными программами SHiNE, приносящими деньги.
|
|
||||||
|
|
||||||
## Краткий вывод
|
|
||||||
|
|
||||||
Для первого запуска DAO в тестовом виде текущего списка в целом хватает, но только если понимать запуск как:
|
|
||||||
|
|
||||||
- можно развернуть и проверить базовый DAO-контур;
|
|
||||||
- можно зарегистрировать пользователей и ключевые сущности;
|
|
||||||
- можно провести тестовую покупку билета через smart contract;
|
|
||||||
- можно завести тестовый денежный поток в программы, управляемые DAO;
|
|
||||||
- можно проверить опорную межсерверную синхронизацию и фиксацию состояния в архивный слой.
|
|
||||||
|
|
||||||
Если же под "запуском" понимать уже полностью устойчивую production-схему с ротацией ключей, восстановлением любого сервера из архива, железными устройствами подписи и полным циклом администрирования, то текущий список нужно будет ещё расширять.
|
|
||||||
|
|
||||||
## Этап1
|
|
||||||
|
|
||||||
Цель этапа: собрать минимально жизнеспособный DAO-сценарий в `devnet`, который можно пройти руками от регистрации до базовой экономики и проверки архитектуры.
|
|
||||||
|
|
||||||
### 1. Переписать и стабилизировать регистрацию пользователей без Anchor
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- довести `shine_users` в чистом Rust/Solana SDK до рабочего и проверенного состояния;
|
|
||||||
- убедиться, что `shine_login_guard` и связанный сценарий регистрации совместимы с новым ABI;
|
|
||||||
- проверить создание и чтение `user_pda`;
|
|
||||||
- проверить update пользовательской записи и связанные экономические параметры;
|
|
||||||
- синхронизировать сервер, UI и lazy-import с новым форматом и seed'ами.
|
|
||||||
|
|
||||||
Почему это в `этап1`:
|
|
||||||
|
|
||||||
- без стабильной пользовательской регистрации дальше нельзя строить ни DAO-сценарий, ни привязку устройств, ни платёжные сценарии.
|
|
||||||
|
|
||||||
### 2. Проверить полный сценарий регистрации и базовой Solana-интеграции
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- руками прогнать регистрацию нового пользователя;
|
|
||||||
- руками прогнать создание и update server PDA там, где это требуется текущему сценарию;
|
|
||||||
- убедиться, что сервер читает новые PDA без anchor-зависимостей и без старых discriminator'ов;
|
|
||||||
- зафиксировать, какие именно части сценария уже подтверждены руками, а какие ещё нет.
|
|
||||||
|
|
||||||
Почему это в `этап1`:
|
|
||||||
|
|
||||||
- сейчас в проекте уже есть признаки перехода на pure Rust, но без ручной проверки это нельзя считать завершённым.
|
|
||||||
|
|
||||||
### 3. Создать стандартный DAO smart contract / governance-контур
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- определить и реализовать стандартный DAO-контур, который будет управлять программами SHiNE;
|
|
||||||
- зафиксировать, какие права сразу передаются DAO, а какие временно остаются на отдельных ключах;
|
|
||||||
- подготовить тестовую DAO-структуру в `devnet`.
|
|
||||||
|
|
||||||
Минимум для первого запуска:
|
|
||||||
|
|
||||||
- DAO существует как управляемая сущность;
|
|
||||||
- DAO может владеть или контролировать ключевые права управления денежными программами;
|
|
||||||
- есть понятный путь, как DAO влияет на доходные программы SHiNE.
|
|
||||||
|
|
||||||
Почему это в `этап1`:
|
|
||||||
|
|
||||||
- без этого "DAO-запуск" будет только запуском отдельных Solana-программ, но не запуском управляемой DAO-системы.
|
|
||||||
|
|
||||||
### 4. Доработать смарт-контракт выплат с третьей очередью
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- добавить в `shine_payments` третью очередь, о которой уже принято решение;
|
|
||||||
- проверить совместимость с текущей моделью тикетов, выплат и DAO-управления;
|
|
||||||
- убедиться, что логика очередей соответствует ожидаемой экономике проекта.
|
|
||||||
|
|
||||||
Почему это в `этап1`:
|
|
||||||
|
|
||||||
- по текущей постановке это нужно именно для сценария регистрации DAO и дальнейшей экономики.
|
|
||||||
|
|
||||||
### 5. Сделать UI для покупки билетов и просмотра очереди
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- добавить UI-сценарий покупки билетов через smart contract;
|
|
||||||
- показать пользователю, сколько перед ним человек в очереди;
|
|
||||||
- убедиться, что UI отражает актуальное состояние контрактной логики, а не локальные предположения.
|
|
||||||
|
|
||||||
Почему это в `этап1`:
|
|
||||||
|
|
||||||
- покупка билетов у тебя обозначена как часть DAO-сценария, а не как побочная функция;
|
|
||||||
- без UI можно тестировать контракт вручную, но нельзя считать сценарий запуска достаточно собранным для нормальной проверки.
|
|
||||||
|
|
||||||
### 6. Реализовать базовую синхронизацию серверов
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- сделать обмен состоянием между серверами по модели `server-to-server`;
|
|
||||||
- определить минимальный набор данных, который обязан синхронизироваться;
|
|
||||||
- предусмотреть фиксацию синхронизированного состояния в `Arweave`, а `Solana` использовать как якорь и ссылочный слой;
|
|
||||||
- описать, какой сервер считается источником истины в спорных случаях или как решается конфликт.
|
|
||||||
|
|
||||||
Почему это в `этап1`:
|
|
||||||
|
|
||||||
- без межсерверной синхронизации трудно обосновать архитектуру сети как воспроизводимую и переносимую;
|
|
||||||
- это напрямую связано с идеей, что любой сможет поднять свой сервер.
|
|
||||||
|
|
||||||
### 7. Подготовить базовый сценарий архивирования и восстановления
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- описать и частично реализовать схему: серверы синхронизируются между собой, архив состояния уходит в `Arweave`, ссылка/якорь фиксируется через `Solana`;
|
|
||||||
- определить минимальный сценарий восстановления блоков или состояния из архивного слоя;
|
|
||||||
- подтвердить, что новый сервер может получить достаточно данных для старта.
|
|
||||||
|
|
||||||
Почему это в `этап1`:
|
|
||||||
|
|
||||||
- это один из ключевых признаков независимой и воспроизводимой DAO-инфраструктуры.
|
|
||||||
|
|
||||||
## Этап2
|
|
||||||
|
|
||||||
Цель этапа: усилить безопасность, автономность и удобство системы после того, как минимальный DAO-сценарий уже запустился и проверен в `devnet`.
|
|
||||||
|
|
||||||
### 1. Смена ключей цифровой подписи
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- продумать и реализовать смену `root key`, `device key`, `blockchain key`;
|
|
||||||
- описать ограничения, кто и в каком сценарии может менять каждый тип ключа;
|
|
||||||
- продумать, как не потерять доступ и как обновлять доверие к новым ключам.
|
|
||||||
|
|
||||||
Почему это в `этап2`:
|
|
||||||
|
|
||||||
- для production это очень важно;
|
|
||||||
- для первого тестового запуска можно временно использовать фиксированный набор ключей.
|
|
||||||
|
|
||||||
### 2. Полная повторная перепроверка всех сценариев
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- повторно прогнать регистрацию, DAO, выплаты, билеты, синхронизацию и архивирование после стабилизации `этап1`;
|
|
||||||
- оформить итоговый чек-лист ручной проверки;
|
|
||||||
- отдельно проверить пограничные сценарии и восстановление после ошибок.
|
|
||||||
|
|
||||||
Почему это в `этап2`:
|
|
||||||
|
|
||||||
- это обязательный шаг перед переходом от "собрали" к "доверяем".
|
|
||||||
|
|
||||||
### 3. Устройство на ESP32 как homeserver с ключами
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- дописать прошивку, чтобы устройство могло выступать homeserver с ключами;
|
|
||||||
- дать ему возможность регистрироваться и подключаться к серверу;
|
|
||||||
- определить, какие операции устройство подписывает и где хранит ключевой материал.
|
|
||||||
|
|
||||||
Почему это в `этап2`:
|
|
||||||
|
|
||||||
- это очень сильное развитие архитектуры, но оно не должно блокировать первый DAO-запуск.
|
|
||||||
|
|
||||||
### 4. Логин и подпись через коробочки / устройства
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- реализовать сценарий входа через устройство или хотя бы сценарий подписи сообщений и ключей через устройство;
|
|
||||||
- определить, как это встраивается в регистрацию DAO и подтверждение действий;
|
|
||||||
- проверить, можно ли через это безопасно регистрировать DAO или подписывать критичные команды.
|
|
||||||
|
|
||||||
Почему это в `этап2`:
|
|
||||||
|
|
||||||
- это следующий уровень безопасности и UX, но не минимальный блокер первого старта.
|
|
||||||
|
|
||||||
### 5. Создание тестового DAO с использованием устройств подписи
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- после готовности устройств собрать тестовый DAO-сценарий уже с аппаратным участием;
|
|
||||||
- проверить, где устройство достаточно, а где всё ещё нужен обычный кошелёк или управляющий ключ.
|
|
||||||
|
|
||||||
Почему это в `этап2`:
|
|
||||||
|
|
||||||
- это проверка усиленной модели, а не базового старта.
|
|
||||||
|
|
||||||
### 6. Расписание синхронизации серверов
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- определить периодичность и правила фоновой синхронизации;
|
|
||||||
- продумать ручной и автоматический режим;
|
|
||||||
- решить, как часто публиковать архивные снимки и якоря.
|
|
||||||
|
|
||||||
Почему это в `этап2`:
|
|
||||||
|
|
||||||
- сначала важнее добиться самой работающей синхронизации, а потом уже делать её регулярной и автономной.
|
|
||||||
|
|
||||||
### 7. Полное восстановление блоков из Solana/Arweave
|
|
||||||
|
|
||||||
Что сделать:
|
|
||||||
|
|
||||||
- довести процедуру восстановления до сценария "любой может поднять свой сервер";
|
|
||||||
- определить минимальный bootstrap-набор;
|
|
||||||
- проверить восстановление на чистом окружении.
|
|
||||||
|
|
||||||
Почему это в `этап2`:
|
|
||||||
|
|
||||||
- для концепции сети это критично, но как полноценная задача обычно идёт после появления базового архива и первичной синхронизации.
|
|
||||||
|
|
||||||
## Что блокирует первый запуск сильнее всего
|
|
||||||
|
|
||||||
Если расставить приоритет внутри `этап1`, то самый жёсткий порядок сейчас выглядит так:
|
|
||||||
|
|
||||||
1. pure Rust регистрация пользователей и ручная проверка сценария;
|
|
||||||
2. DAO/gov-контур и его права управления;
|
|
||||||
3. доработка выплат с третьей очередью;
|
|
||||||
4. покупка билетов через smart contract и UI-проверка очереди;
|
|
||||||
5. межсерверная синхронизация;
|
|
||||||
6. архивирование в `Arweave` с якорем в `Solana`;
|
|
||||||
7. минимальное восстановление состояния новым сервером.
|
|
||||||
|
|
||||||
## Что уже частично похоже на готовое
|
|
||||||
|
|
||||||
По текущим документам и следам в проекте уже видно, что:
|
|
||||||
|
|
||||||
- переход `shine_users` и `shine_login_guard` на pure Rust уже начат и в значительной степени сделан;
|
|
||||||
- архитектура DAO, `shine_users` и `shine_payments` уже описана;
|
|
||||||
- часть Solana-структуры и PDA-форматов уже формализована;
|
|
||||||
- тема ESP32 уже отдельно присутствует в проекте как направление.
|
|
||||||
|
|
||||||
Это хорошо, потому что документ получается не "с нуля", а как сборка того, что уже назрело в коде и планах.
|
|
||||||
|
|
||||||
## Вопросы, которые всё ещё стоит уточнить
|
|
||||||
|
|
||||||
1. Какой именно стандарт DAO планируется использовать в первом проходе: готовый governance-стек Solana или собственная минимальная обвязка вокруг управляющих кошельков?
|
|
||||||
2. Третья очередь в `shine_payments` уже точно определена по смыслу, или пока есть только решение "она нужна", но без финальной экономики?
|
|
||||||
3. Что именно считается единицей синхронизации между серверами: блоки SHiNE, агрегированные снапшоты, PDA-состояния, или смесь этих вариантов?
|
|
||||||
4. Нужен ли для `этап1` уже полноценный автоматический recovery нового сервера, или достаточно доказать это в полу-ручном сценарии?
|
|
||||||
5. Покупка билетов должна в первом проходе работать только через web/UI, или также нужен отдельный сценарий из серверного UI или скриптов?
|
|
||||||
|
|
||||||
## Рекомендуемый следующий практический шаг
|
|
||||||
|
|
||||||
Если идти без распыления, то следующим рабочим фокусом стоит считать:
|
|
||||||
|
|
||||||
1. закрыть ручную проверку pure Rust регистрации;
|
|
||||||
2. после этого формализовать минимальный DAO-контур;
|
|
||||||
3. затем переходить к третьей очереди выплат и к UI покупки билетов;
|
|
||||||
4. после этого делать синхронизацию, архив и восстановление.
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# 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 имеет любой, кто знает токен.
|
|
||||||
- Рекомендуется тестить между разными логинами.
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
0. ПЕРЕДЕЛАТЬ ВСЁ НА НОВЫЙ ФОРМАТ!!
|
|
||||||
|
|
||||||
ВЫНЕСТИ ЭТИ ТРИ ВЕЩИ В ОБЩИЙ ПАРСЕР
|
|
||||||
* [2] type - тип соощения
|
|
||||||
* [2] Sиbtype - субтип сообщения
|
|
||||||
* [2] version - версия формата соощения
|
|
||||||
|
|
||||||
А ОСТАЛЬНОЕ В РЕАЛИЗАЦИЮ
|
|
||||||
|
|
||||||
|
|
||||||
ПЕРЕДЕЛАЕМ БД
|
|
||||||
|
|
||||||
1. СДЕЛАЕМ ЛИНИЮ ТОЛЬКО ДЛЯ ТЕХ ТИПОВ КОМУ НАДО (ЛАЙКАМ И ОТВЕТАМ НЕ НАДО)
|
|
||||||
(НОМЕР СООБЩЕНИЯ В ЛИНИИ ХРАНИТЬ В БЛОКАХ ВРОДЕ И НЕ НАДО ТЕМ БОЛЕЕ ЕГО ПОТОМ ПЕРЕПРОВЕРЯТЬ ВСЁ РАВНО)
|
|
||||||
А МОЖЕТ И НАДО ТК КАК ПО ОДНОМУ БЛОКУ ( ИЛИ ЧАСТИ БЛОКОВ ПОНЯТЬ КАКАЯ ЭТО ЧАСТЬ ПЕРЕПИСКИ - ВЕДЬ ГЛОБАЛ НОМЕР ВООБЩЕ НЕ ПОКАЗАТЕЛЬ)
|
|
||||||
|
|
||||||
В БД ПОМЕЧАТЬ ЧТО БЛОК ИЗ ЭТОЙ ЛИНИИ (ДЛЯ БЫСТРОГО ПОИСКА)
|
|
||||||
|
|
||||||
А УНИКАЛЬНЫЙ НОМЕР ЛИНИИ ЭТО ПО СУТИ НОМЕР СООБЩЕНИЯ СОЗДАВШЕГО ЛИНИЮ КАНАЛ (НУ И ФОРМАТ СООБЩЕНИЯ НАЧАЛА ЛИНИИ - КАНАЛА)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
3. СООТВЕТСТВЕННО удалить НАПИСАТЬ/ПЕРОВЕРИТЬ НОРМАЛЬНЫЙ SubscriptionsDAO - ТК СТАРЫЙ РАБОТАЛ НО НА ДРУГОМ ФОРМАТЕ И ТИПО КРИВО
|
|
||||||
|
|
||||||
и дальше:
|
|
||||||
ЗДЕЛАТЬ ТРИ ЗАПРОСА:
|
|
||||||
СПИСОК КАНАЛОВ НА КОГО ПОДПИСАН И ПО СКОЛЬКО СООБЩЕНИЙ И ПОСЛДНИЙ ТЕКСТ
|
|
||||||
ДОДЕЛАТЬ И СВЯЗ ПОДПИСАН УЖЕ НЕ ТОЛЬКО НА ЧЕЛА НО И НА КАНАЛ. (И ПОЛУЧАЕТСЯ ЕСТЬ ОБЩИЙ КАНАЛЛ ПОСТОВ (НО НЕКОТОРЫЕ ПОСТЫ В НИКУДА-
|
|
||||||
А НЕКОТОРЫЕ ПОСТЫ ОБЪЯВЛЕНИЕ КАНАЛА
|
|
||||||
|
|
||||||
СПИСОК СООБЩЕНИЙ В КАНАЛЕ
|
|
||||||
|
|
||||||
ОПСИСАНИЕ ОДНОГО СОООБЩЕНИЯ (С ИСТОРИЕЙ ДО НАЧАЛА ВЕТКИ И СО ВСЕМИ ОТВЕТАМИ НА НЕГО)
|
|
||||||
|
|
||||||
(НУ И В БУДУЩЕМ четвёртый ИСТОРИЮ сообщения ПО ЕДИТУ)
|
|
||||||
|
|
||||||
|
|
||||||
И ПОМЯТКА
|
|
||||||
ВСЕГДА СЧИТАЕМ ПО ПОСЛЕДНЕМУ БЛОКЧЕЙНУ ДОСТУПНОМУ ПОЛЬЗОВАТЕЛЮ
|
|
||||||
ХОТЯ ССЫЛКА ПО НОМЕРУ БЛОКЧЕЙНА КУДА ДОБАВИЛИ
|
|
||||||
|
|
||||||
ЛАЙКИ И ОТВЕТЫ ПИШЕМ НА НОМЕР СООБЩЕНИЯ ЕДИТА
|
|
||||||
(СЧИТАЕМ ТРИГЕРОМ И НА ОРИГИНАЛЬНЫЙ СУМАРНОЕ И ОТДЕЛЬНО НА НЕГО, И НА КАЖДЫЙ ЕДИТ ОТДЕЛЬНО)
|
|
||||||
|
|
||||||
ОТВЕТЫ ПОКАЗЫВАЕМ ВСЕ ВРАЗ
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
Сделать возможность убрать свой лайк. (пока не надо а сложность что надо больше проверок) - хотя можно и без проверки, просто за двойной лайк или за снятие двойное лайка. Будет двойное проникновение :)) тому кто изменил код клиента и убрал проверку на клиенте - и блокчейн заблокируется и всё.
|
|
||||||
поэтому просто на каждую реакцию добавиться убрать эту ракцию .
|
|
||||||
- это просто
|
|
||||||
|
|
||||||
сделатьпотом что бы в солану_юзерс хранилось имя текущего блокчейна пользователя. Что бы потом можно было грузить именно актуальный ТО ЕСТЬ потом можно будет менять блокченый!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
сделать сессион пасворд тоже ключём подписи устройства!!
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
Перечень библиотек и их краткое описание
|
|
||||||
|
|
||||||
shine-server-log
|
|
||||||
Статический “сиренный” метод для максимально заметного критического лога администратору
|
|
||||||
|
|
||||||
shine-server-config
|
|
||||||
Минимальный конфиг-лоадер, который один раз читает application.properties и даёт доступ к параметрам.
|
|
||||||
|
|
||||||
shine-server-geo
|
|
||||||
Утилиты, которые вытаскивают IP/язык/UA из Jetty WebSocket и (опционально) резолвят гео по IP с кэшем в БД.
|
|
||||||
|
|
||||||
shine-server-crypto
|
|
||||||
Базовые крипто-утилиты для SHA-256 и Ed25519 (BouncyCastle) + проверка подписи/хэша для .bch сущностей и маленький self-test.
|
|
||||||
|
|
||||||
shine-server-bd
|
|
||||||
Библиотека реалезующая всю работу с БД:
|
|
||||||
|
|
||||||
shine-server-blockchain
|
|
||||||
Библиотека, которая задаёт единый бинарный формат блоков (RAW+signature+hash), парсит/валидирует “тело” блока по type/version, и проверяет целостность/подпись цепочки через SHA-256 + Ed25519 с привязкой к login и предыдущим хэшам.
|
|
||||||
|
|
||||||
shine-server-protocol
|
|
||||||
Библиотека JSON-протокол поверх WebSocket для взаимодействия с клиентами.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getDeviceKeyByte()).
|
|
||||||
shine.db.dao.* — DAO по таблицам: ActiveSessionsDAO, SolanaUsersDAO, UserParamsDAO, IpGeoCacheDAO, BlockchainStateDAO, BlocksDAO; плюс “сервисные” DAO:
|
|
||||||
|
|
||||||
UserCreateDAO — атомарная регистрация пользователя в транзакции (BEGIN IMMEDIATE + rollback/commit).
|
|
||||||
// Временное решение позволяющее регистрировать новых пользователей
|
|
||||||
// атомарно и добавляет запись и в solana_users и в BlockchainState
|
|
||||||
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
SHiNE — структура БД (актуальная версия)
|
|
||||||
|
|
||||||
Перечень таблиц и назначение
|
|
||||||
|
|
||||||
solana_users
|
|
||||||
Справочник пользователей: логин + ключ устройства + (опционально) Solana-ключ.
|
|
||||||
Базовая таблица, используется как FK почти везде.
|
|
||||||
|
|
||||||
active_sessions
|
|
||||||
Активные сессии авторизации/работы клиента: секреты, тайминги, WebPush-данные, IP и информация о клиенте.
|
|
||||||
|
|
||||||
users_params
|
|
||||||
Хранилище актуальных параметров пользователя.
|
|
||||||
Для каждой пары (login, param) хранится только самая новая версия по time_ms.
|
|
||||||
|
|
||||||
ip_geo_cache
|
|
||||||
Кеш геолокации по IP для снижения нагрузки на внешние сервисы.
|
|
||||||
|
|
||||||
blockchain_state
|
|
||||||
Агрегированное состояние блокчейна по blockchain_name:
|
|
||||||
лимиты, текущий размер, последний глобальный блок и состояние линий 0..7.
|
|
||||||
|
|
||||||
blocks
|
|
||||||
Журнал всех блоков и сообщений.
|
|
||||||
Содержит историю событий: тексты, реакции, ответы, связи.
|
|
||||||
PRIMARY KEY намеренно отсутствует.
|
|
||||||
|
|
||||||
connections_state ⭐
|
|
||||||
Актуальное состояние связей между пользователями
|
|
||||||
(друг / контакт / подписка).
|
|
||||||
Обновляется автоматически на основе событий из blocks.
|
|
||||||
|
|
||||||
message_stats ⭐
|
|
||||||
Агрегированные счётчики лайков и ответов на конкретные сообщения.
|
|
||||||
Поддерживается триггерами из blocks.
|
|
||||||
|
|
||||||
Таблицы подробно
|
|
||||||
|
|
||||||
solana_users
|
|
||||||
login — TEXT PK — уникальный логин пользователя
|
|
||||||
device_key — TEXT NOT NULL — публичный ключ устройства (Base64(32) / HEX(64))
|
|
||||||
solana_key — TEXT NULL — публичный ключ Solana-аккаунта
|
|
||||||
|
|
||||||
active_sessions
|
|
||||||
session_id — TEXT PK — идентификатор сессии
|
|
||||||
login — TEXT NOT NULL, FK → solana_users(login)
|
|
||||||
session_pwd — TEXT NOT NULL — секрет сессии
|
|
||||||
storage_pwd — TEXT NOT NULL — секрет storage
|
|
||||||
session_created_at_ms — INTEGER NOT NULL
|
|
||||||
last_authirificated_at_ms — INTEGER NOT NULL
|
|
||||||
push_endpoint — TEXT NULL
|
|
||||||
push_p256dh_key — TEXT NULL
|
|
||||||
push_auth_key — TEXT NULL
|
|
||||||
client_ip — TEXT NULL
|
|
||||||
client_info_from_client — TEXT NULL
|
|
||||||
client_info_from_request — TEXT NULL
|
|
||||||
user_language — TEXT NULL
|
|
||||||
|
|
||||||
users_params
|
|
||||||
login — TEXT NOT NULL, FK → solana_users(login)
|
|
||||||
param — TEXT NOT NULL
|
|
||||||
time_ms — INTEGER NOT NULL
|
|
||||||
value — TEXT NOT NULL
|
|
||||||
device_key — TEXT NULL
|
|
||||||
signature — TEXT NULL
|
|
||||||
|
|
||||||
Ограничение:
|
|
||||||
UNIQUE(login, param)
|
|
||||||
|
|
||||||
Логика:
|
|
||||||
обновление принимается только если excluded.time_ms > users_params.time_ms
|
|
||||||
|
|
||||||
ip_geo_cache
|
|
||||||
ip — TEXT PK
|
|
||||||
geo — TEXT NULL
|
|
||||||
updated_at_ms — INTEGER NOT NULL
|
|
||||||
|
|
||||||
blockchain_state
|
|
||||||
blockchain_name — TEXT PK
|
|
||||||
login — TEXT NOT NULL, FK → solana_users(login)
|
|
||||||
blockchain_key — TEXT NOT NULL
|
|
||||||
size_limit — INTEGER NOT NULL
|
|
||||||
file_size_bytes — INTEGER NOT NULL
|
|
||||||
last_global_number — INTEGER NOT NULL (-1 = genesis)
|
|
||||||
last_global_hash — TEXT NOT NULL
|
|
||||||
updated_at_ms — INTEGER NOT NULL
|
|
||||||
|
|
||||||
Линии 0..7:
|
|
||||||
для каждой линии:
|
|
||||||
lineX_last_number
|
|
||||||
lineX_last_hash
|
|
||||||
|
|
||||||
blocks
|
|
||||||
login — TEXT NOT NULL
|
|
||||||
bch_name — TEXT NOT NULL
|
|
||||||
block_global_number — INTEGER NOT NULL
|
|
||||||
block_global_pre_hash — TEXT NOT NULL
|
|
||||||
block_line_index — INTEGER NOT NULL
|
|
||||||
block_line_number — INTEGER NOT NULL
|
|
||||||
block_line_pre_hash — TEXT NOT NULL
|
|
||||||
msg_type — INTEGER NOT NULL
|
|
||||||
msg_sub_type — INTEGER NOT NULL
|
|
||||||
block_bytes — BLOB NULL
|
|
||||||
|
|
||||||
Ссылка на другой блок (nullable):
|
|
||||||
to_login
|
|
||||||
to_bch_name
|
|
||||||
to_block_global_number
|
|
||||||
to_block_hash
|
|
||||||
|
|
||||||
connections_state ⭐
|
|
||||||
Текущее агрегированное состояние связей.
|
|
||||||
|
|
||||||
login — TEXT NOT NULL
|
|
||||||
rel_type — INTEGER NOT NULL
|
|
||||||
10 = FRIEND
|
|
||||||
20 = CONTACT
|
|
||||||
30 = FOLLOW
|
|
||||||
to_login — TEXT NOT NULL
|
|
||||||
to_bch_name — TEXT NOT NULL
|
|
||||||
to_block_global_number — INTEGER NULL
|
|
||||||
to_block_hash — TEXT NULL
|
|
||||||
|
|
||||||
Ограничение:
|
|
||||||
UNIQUE(login, rel_type, to_login)
|
|
||||||
|
|
||||||
message_stats ⭐
|
|
||||||
Счётчики активности по целевому сообщению.
|
|
||||||
|
|
||||||
to_login — TEXT NOT NULL
|
|
||||||
to_bch_name — TEXT NOT NULL
|
|
||||||
to_block_global_number — INTEGER NOT NULL
|
|
||||||
to_block_hash — TEXT NOT NULL
|
|
||||||
likes_count — INTEGER NOT NULL DEFAULT 0
|
|
||||||
replies_count — INTEGER NOT NULL DEFAULT 0
|
|
||||||
|
|
||||||
UNIQUE:
|
|
||||||
(to_login, to_bch_name, to_block_global_number, to_block_hash)
|
|
||||||
|
|
||||||
Триггеры БД (полная логика)
|
|
||||||
|
|
||||||
3.1 Связи пользователей
|
|
||||||
trg_blocks_connection_state_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
|
|
||||||
Условие:
|
|
||||||
msg_type = 3 (connection)
|
|
||||||
|
|
||||||
Добавление / обновление связи
|
|
||||||
msg_sub_type IN (10,20,30)
|
|
||||||
выполняется UPSERT в connections_state
|
|
||||||
|
|
||||||
Удаление связи
|
|
||||||
msg_sub_type IN (11,21,31)
|
|
||||||
удаляется соответствующая связь:
|
|
||||||
11 → 10
|
|
||||||
21 → 20
|
|
||||||
31 → 30
|
|
||||||
|
|
||||||
Итог:
|
|
||||||
blocks — журнал событий
|
|
||||||
connections_state — всегда актуальное состояние
|
|
||||||
|
|
||||||
3.2 Подсчёт лайков ⭐
|
|
||||||
trg_blocks_message_stats_like_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
|
|
||||||
Условие:
|
|
||||||
msg_type = 2 (reaction)
|
|
||||||
msg_sub_type = 1 (like)
|
|
||||||
|
|
||||||
Действие:
|
|
||||||
определяется цель по to_bch_name, to_block_global_number, to_block_hash
|
|
||||||
to_login вычисляется как
|
|
||||||
substr(to_bch_name, 1, length(to_bch_name) - 3)
|
|
||||||
выполняется UPSERT в message_stats
|
|
||||||
likes_count += 1
|
|
||||||
|
|
||||||
3.3 Подсчёт ответов ⭐
|
|
||||||
trg_blocks_message_stats_reply_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
|
|
||||||
Условие:
|
|
||||||
msg_type = 1 (text)
|
|
||||||
msg_sub_type = 2 (reply)
|
|
||||||
|
|
||||||
Действие:
|
|
||||||
цель определяется аналогично лайкам
|
|
||||||
выполняется UPSERT в message_stats
|
|
||||||
replies_count += 1
|
|
||||||
|
|
||||||
Индексы (смысл)
|
|
||||||
|
|
||||||
idx_solana_users_login — поиск пользователя
|
|
||||||
idx_active_sessions_login — сессии пользователя
|
|
||||||
idx_users_params_login — параметры пользователя
|
|
||||||
idx_ip_geo_cache_updated_at — чистка кеша
|
|
||||||
idx_blockchain_state_login — блокчейны пользователя
|
|
||||||
idx_blockchain_state_updated_at — обслуживание
|
|
||||||
idx_blocks_chain_global — чтение цепочки
|
|
||||||
idx_blocks_to_target — реакции / ответы
|
|
||||||
idx_message_stats_target — быстрый доступ к счётчикам
|
|
||||||
|
|
||||||
Итоговая модель мышления
|
|
||||||
|
|
||||||
blocks — неизменяемый журнал событий
|
|
||||||
connections_state — проекция связей
|
|
||||||
message_stats — проекция активности
|
|
||||||
всё вычисляется детерминированно через триггеры
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# Протокол звонков (MVP)
|
|
||||||
|
|
||||||
Версия: browser-to-browser, runtime-only signaling.
|
|
||||||
|
|
||||||
## Цели
|
|
||||||
- Технические сообщения звонка не сохраняются в БД direct_messages.
|
|
||||||
- Первый INVITE рассылается всем активным сессиям получателя и дублируется web push.
|
|
||||||
- Последующие сигналы идут только в конкретную sessionId и не дублируются в push.
|
|
||||||
|
|
||||||
## Операции API
|
|
||||||
|
|
||||||
### 1) CallInviteBroadcast
|
|
||||||
Отправляет общий вызов пользователю.
|
|
||||||
|
|
||||||
Запрос payload:
|
|
||||||
- `toLogin: string`
|
|
||||||
- `callId: string`
|
|
||||||
- `type: 100` (INVITE)
|
|
||||||
|
|
||||||
Поведение сервера:
|
|
||||||
- Рассылает `IncomingCallInvite` во все активные WS-сессии `toLogin`.
|
|
||||||
- В payload события передаёт:
|
|
||||||
- `fromLogin`
|
|
||||||
- `fromSessionId` (session инициатора)
|
|
||||||
- `toLogin`
|
|
||||||
- `callId`
|
|
||||||
- `type=100`
|
|
||||||
- `timeMs`
|
|
||||||
- Отправляет web push уведомление о входящем вызове.
|
|
||||||
|
|
||||||
Ответ payload:
|
|
||||||
- `callId`
|
|
||||||
- `deliveredWsSessions`
|
|
||||||
- `deliveredFcmSessions`
|
|
||||||
|
|
||||||
### 2) CallSignalToSession
|
|
||||||
Отправляет технический сигнал в конкретную сессию.
|
|
||||||
|
|
||||||
Запрос payload:
|
|
||||||
- `toLogin: string`
|
|
||||||
- `targetSessionId: string`
|
|
||||||
- `callId: string`
|
|
||||||
- `type: int`
|
|
||||||
- `data: string` (для SDP/ICE/служебных строк)
|
|
||||||
|
|
||||||
Поведение сервера:
|
|
||||||
- Ищет только `targetSessionId`.
|
|
||||||
- Проверяет, что сессия принадлежит `toLogin`.
|
|
||||||
- Отправляет `IncomingCallSignal` только в эту сессию.
|
|
||||||
- В БД ничего не сохраняет.
|
|
||||||
- Push не отправляет.
|
|
||||||
|
|
||||||
Ответ payload:
|
|
||||||
- `delivered: boolean`
|
|
||||||
|
|
||||||
## Коды type
|
|
||||||
- `100` INVITE
|
|
||||||
- `110` RINGING
|
|
||||||
- `120` ACCEPT
|
|
||||||
- `130` DECLINE_BUSY
|
|
||||||
- `140` TIMEOUT
|
|
||||||
- `150` HANGUP
|
|
||||||
- `200` OFFER
|
|
||||||
- `210` ANSWER
|
|
||||||
- `220` ICE
|
|
||||||
|
|
||||||
## Правила UI/логики
|
|
||||||
- Если уже есть активный звонок и пришел новый INVITE -> автоответ `DECLINE_BUSY` без UI.
|
|
||||||
- После ACCEPT `callId` остаётся во всех OFFER/ANSWER/ICE сообщениях до конца звонка.
|
|
||||||
- При параллельных звонках A<->B допускается детерминированное правило, кто создаёт OFFER.
|
|
||||||
|
|
||||||
## Тайминги MVP
|
|
||||||
- Ожидание подтверждения/реакции после INVITE: до 5с (у инициатора).
|
|
||||||
- Ожидание принятия у входящего звонка: 20с.
|
|
||||||
- Общий лимит ожидания до соединения: 22с.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
Дальше делать:
|
|
||||||
Описание форматов.
|
|
||||||
Запросы клиент-сервер.
|
|
||||||
Промт на клиента.
|
|
||||||
|
|
||||||
---
|
|
||||||
Потом в сервак дописать синхронизацию серверов.
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# SHiNE Deployment Servers Inventory
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
This folder contains all deployment-related notes and server records for SHiNE.
|
|
||||||
|
|
||||||
## Legacy Production Server
|
|
||||||
- Name: `VPS-02` (legacy)
|
|
||||||
- Access: `root@194.87.0.247`
|
|
||||||
- Current role: old production server
|
|
||||||
- Confirmed services:
|
|
||||||
- `coturn` is installed and active (`systemd: active/running`)
|
|
||||||
- `caddy` is installed (reported by project context; verify version on host if needed)
|
|
||||||
- TURN configuration observed on host:
|
|
||||||
- `listening-port=3478`
|
|
||||||
- `external-ip=194.87.0.247`
|
|
||||||
- `relay-ip=194.87.0.247`
|
|
||||||
- auth mode: `use-auth-secret` + `static-auth-secret`
|
|
||||||
- SHiNE deployment note:
|
|
||||||
- This host is used as current/legacy runtime for SHiNE.
|
|
||||||
- Gradle-based deployment is used in this project (see repository deploy tasks and scripts).
|
|
||||||
|
|
||||||
## Target Production Server (Migration)
|
|
||||||
- Name: `VPS-05` (new)
|
|
||||||
- Access: `root@45.136.124.227`
|
|
||||||
- Planned role: new primary production server for gradual migration
|
|
||||||
- Baseline setup done:
|
|
||||||
- `ripgrep` installed
|
|
||||||
- user `player` created
|
|
||||||
- user `player` added to `sudo` group
|
|
||||||
- deployment directory created: `/home/player/SHiNE`
|
|
||||||
- Rule:
|
|
||||||
- All SHiNE-related runtime files and deployments on VPS-05 should be placed under `/home/player/SHiNE`.
|
|
||||||
|
|
||||||
## Additional TURN Node
|
|
||||||
- Name: `promo-node-93`
|
|
||||||
- Access: `ubuntu@93.170.12.154` (and `player` user for SHiNE operations)
|
|
||||||
- Role: additional TURN node for SHiNE calls
|
|
||||||
- TURN setup:
|
|
||||||
- `coturn` installed and active
|
|
||||||
- `listening-port=3478`
|
|
||||||
- `tls-listening-port=5349`
|
|
||||||
- `use-auth-secret` + shared `static-auth-secret`
|
|
||||||
- relay UDP port range: `49152-50152`
|
|
||||||
- Runtime files:
|
|
||||||
- `/etc/turnserver.conf`
|
|
||||||
- `/home/player/SHiNE/coturn/turnserver.conf`
|
|
||||||
- Cleanup done:
|
|
||||||
- Disabled old reverse SSH tunnel (`reverse-ssh.service`) that exposed `0.0.0.0:1200 -> localhost:22` to `194.87.0.247`.
|
|
||||||
|
|
||||||
## Next Migration Steps (recommended)
|
|
||||||
1. Install and configure runtime dependencies (JDK, Caddy, DB, TURN if required).
|
|
||||||
2. Mirror SHiNE deployment process from VPS-02 using existing Gradle deployment flow.
|
|
||||||
3. Move traffic gradually and validate logs/metrics before final cutover.
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
# API для разработчиков: 04 — Добавление блока в блокчейн (AddBlock)
|
|
||||||
|
|
||||||
Документ описывает **текущий рабочий формат** сетевого вызова `AddBlock`, который используется для записи **любого** блока в блокчейн пользователя.
|
|
||||||
|
|
||||||
> Важный принцип: на уровне 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`
|
|
||||||
- `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`.
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
# API для разработчиков: Технические запросы
|
|
||||||
|
|
||||||
Этот файл описывает технические WebSocket-запросы, которые нужны для служебной работы клиента с сервером. Часть операций доступна без авторизации, часть требует успешной авторизованной сессии.
|
|
||||||
|
|
||||||
Сейчас здесь шесть методов:
|
|
||||||
|
|
||||||
- `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения;
|
|
||||||
- `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети;
|
|
||||||
- `GetCallIceConfig` — выдача STUN/TURN конфигурации для звонков;
|
|
||||||
- `ClientErrorLog` — отправка клиентской ошибки в серверный лог;
|
|
||||||
- `ClientDebugLog` — отправка клиентского debug-события в серверный буфер;
|
|
||||||
- `CallDeliveryReport` — диагностический отчёт клиента о доставке/установке звонка.
|
|
||||||
|
|
||||||
Логика раздела такая:
|
|
||||||
|
|
||||||
- `Ping` нужен для регулярной проверки, что соединение всё ещё живо;
|
|
||||||
- `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла.
|
|
||||||
|
|
||||||
Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов.
|
|
||||||
|
|
||||||
## 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. `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` — требуется авторизация.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. `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` — обязательные поля ошибки не заполнены.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. `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` не заполнено.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. `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` не заполнено.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Короткое резюме
|
|
||||||
|
|
||||||
- `Ping` нужен для keep-alive и проверки, что WebSocket-соединение живо.
|
|
||||||
- `GetServerInfo` нужен для выбора сервера в сети и показа публичной информации об узле.
|
|
||||||
- `GetCallIceConfig` нужен для WebRTC-звонков и требует авторизации.
|
|
||||||
- `ClientErrorLog`, `ClientDebugLog`, `CallDeliveryReport` используются для диагностики клиента и звонков.
|
|
||||||
|
|
||||||
|
|
||||||
## 8. Прямое техническое сообщение в конкретную сессию
|
|
||||||
|
|
||||||
На текущий момент в публичном JSON API этого документа **нет отдельного RPC** для отправки произвольного технического сообщения в конкретную сессию пользователя (по `sessionId`).
|
|
||||||
|
|
||||||
Что уже есть в системе:
|
|
||||||
|
|
||||||
- сервер хранит `sessionId` активной сессии;
|
|
||||||
- есть `ListSessions`, чтобы клиент получил список sessionId своего пользователя;
|
|
||||||
- у сервера есть внутренний реестр активных WS-подключений по `sessionId`.
|
|
||||||
|
|
||||||
Чего не хватает для полноценной фичи «direct tech message by sessionId»:
|
|
||||||
|
|
||||||
1. отдельная API-операция (например, `SendSessionTechMessage`);
|
|
||||||
2. правило авторизации (кто имеет право писать в чужую/свою сессию);
|
|
||||||
3. унифицированный формат payload и события доставки;
|
|
||||||
4. коды ошибок (`SESSION_OFFLINE`, `SESSION_NOT_FOUND`, `FORBIDDEN` и т.п.).
|
|
||||||
|
|
||||||
Итог: как инфраструктурная база это почти готово, но нужен отдельный RPC-слой и политика доступа.
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
# 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-файлов сейчас отсутствуют
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Командные сообщения каналов
|
|
||||||
|
|
||||||
## 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-сценарии.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# 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`-подтипа нет.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# История изменений документации блокчейна
|
|
||||||
|
|
||||||
## 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/`.
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Документация блокчейна 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)`.
|
|
||||||
|
|
||||||
## Обязательное сопровождение
|
|
||||||
- При любом изменении формата/правил блокчейна в коде документы этого каталога обновляются в том же наборе изменений.
|
|
||||||
- Каждое обновление документов фиксируется в `CHANGELOG.md` с датой/временем и хэшем коммита-основания.
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# Синхронизация блоков и 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 Межсерверное соединение
|
|
||||||
|
|
||||||
- Серверы устанавливают постоянное WebSocket-соединение друг с другом.
|
|
||||||
- Адрес партнёра определяется по `server_address` из его Solana PDA.
|
|
||||||
- Аутентификация: подпись Ed25519 корневым ключом сервера (`root_key` из PDA).
|
|
||||||
- При разрыве — переподключение с экспоненциальным backoff.
|
|
||||||
|
|
||||||
### 4.2 Доставка новых данных (push)
|
|
||||||
|
|
||||||
- При получении нового блока или DM сервер немедленно пушит его всем подключённым партнёрам.
|
|
||||||
- Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff.
|
|
||||||
|
|
||||||
### 4.3 Начальная синхронизация (backfill)
|
|
||||||
|
|
||||||
- При первом подключении к партнёру серверы обмениваются «курсорами» состояния:
|
|
||||||
последний глобальный номер блока, последний известный DM-ключ.
|
|
||||||
- Сервер с более полной историей досылает недостающее партнёру.
|
|
||||||
|
|
||||||
### 4.4 Разрешение конфликтов
|
|
||||||
|
|
||||||
- Блоки пользовательского блокчейна: порядок определяется глобальным номером блока.
|
|
||||||
Конфликтующие ветки (fork) разрешаются по правилам `AddBlock` (см. `Dev_Docs/Blockchain/README.md`).
|
|
||||||
- DM: конфликтов нет, `message_key` уникален.
|
|
||||||
|
|
||||||
## 5. Маршрутизация 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) сохраняет и доставляет блоки.
|
|
||||||
|
|
||||||
Кэш адресов серверов: обновляется раз в сессию (при ошибке соединения).
|
|
||||||
|
|
||||||
## 6. Безопасность
|
|
||||||
|
|
||||||
- Все блоки подписаны ключами пользователя на клиенте — сервер не может подделать содержимое.
|
|
||||||
- Серверы не расшифровывают DM-контент (шифрование — задача следующего этапа).
|
|
||||||
- При синхронизации каждый блок проходит валидацию подписи на принимающем сервере.
|
|
||||||
|
|
||||||
## 7. Статус реализации
|
|
||||||
|
|
||||||
| Компонент | Статус |
|
|
||||||
|-----------|--------|
|
|
||||||
| Регистрация серверной PDA в Solana | ✅ Реализовано |
|
|
||||||
| Чтение `sync_servers` из PDA | Нужна реализация |
|
|
||||||
| Межсерверный WebSocket-канал | Нужна реализация |
|
|
||||||
| Push новых DM партнёрам | Нужна реализация |
|
|
||||||
| Push блоков блокчейна партнёрам | Нужна реализация |
|
|
||||||
| Backfill при первом подключении | Нужна реализация |
|
|
||||||
| Маршрутизация DM через access_servers | Нужна реализация (заглушка) |
|
|
||||||
|
|
||||||
Текущая версия сервера работает без межсерверной синхронизации.
|
|
||||||
Синхронизация — задача следующего этапа разработки.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Будущие фичи
|
|
||||||
|
|
||||||
Эта папка хранит задачи, которые сознательно отложены и сейчас не должны попадать в активную разработку или ручную проверку без отдельной команды пользователя.
|
|
||||||
|
|
||||||
## Горизонты планирования
|
|
||||||
|
|
||||||
- `near/` - ближайшие планы: задачи, к которым можно вернуться сегодня или завтра.
|
|
||||||
- `medium/` - среднесрочные планы: задачи на ближайшие недели или 1-2 месяца.
|
|
||||||
- `far/` - дальнее будущее: идеи без понятного срока возврата.
|
|
||||||
|
|
||||||
Если пользователь спрашивает, какие есть планы, агент должен смотреть эти три папки и кратко перечислять задачи по горизонтам.
|
|
||||||
|
|
||||||
## Как использовать
|
|
||||||
|
|
||||||
1. Каждая будущая фича описывается отдельным markdown-файлом в одном из горизонтов.
|
|
||||||
2. В файле нужно фиксировать:
|
|
||||||
- зачем нужна фича;
|
|
||||||
- к какому сроку или горизонту она относится;
|
|
||||||
- что нужно сделать;
|
|
||||||
- какие вопросы нужно уточнить перед реализацией;
|
|
||||||
- что уже было сделано в коде, если фича частично реализована;
|
|
||||||
- что временно отключено или закомментировано, если применимо;
|
|
||||||
- какие документы нужно обновить при возврате к задаче;
|
|
||||||
- с какого места продолжать разработку.
|
|
||||||
3. Агент не должен начинать реализацию файлов из этой папки без явной просьбы пользователя.
|
|
||||||
|
|
||||||
## Текущие планы
|
|
||||||
|
|
||||||
### Ближайшие
|
|
||||||
|
|
||||||
- `near/2026-05-25_1106_telegram_agent_players.md` - разрешённые пользователи Telegram для агента, отдельные папки игроков, персональные истории и публикация краткого вопроса/ответа в общий канал.
|
|
||||||
- `near/2026-05-25_1106_wallet_topup_solana_arweave.md` - пополнение Solana и Arweave через внешний сервис покупки с подсказкой и копированием адреса.
|
|
||||||
|
|
||||||
### Среднесрочные
|
|
||||||
|
|
||||||
- `medium/2026-05-24_1140_репосты_в_каналах_и_тредах.md` - репосты в каналах и тредах.
|
|
||||||
- `medium/2026-05-25_1106_shine_balance_wallet.md` - кошелёк и пополнение баланса сияния через блокчейн.
|
|
||||||
- `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений.
|
|
||||||
- `medium/2026-06-03_подключение_других_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать.
|
|
||||||
- `medium/2026-06-02_сессионные_homeserver_в_pda.md` - несколько homeserver-ов пользователя как типизированные сессии в PDA с версией записи.
|
|
||||||
|
|
||||||
### DAO-запуск
|
|
||||||
|
|
||||||
- `dao_запуск/2026-06-05_esp32_hardware_wallet_device_session.md` - ESP32 как аппаратный кошелёк: постоянная device-сессия на сервере, подтверждение операций на экране, делегированные сессии для браузера/телефона.
|
|
||||||
|
|
||||||
### Дальнее будущее
|
|
||||||
|
|
||||||
- `far/2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md` - технические команды для homeserver через SHiNE/WebRTC DataChannel и обмен файлами по чанкам с адресацией по `SHA-256`.
|
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
# ESP32 как аппаратный кошелёк (device-сессия)
|
|
||||||
|
|
||||||
## Суть фичи
|
|
||||||
|
|
||||||
ESP32 становится аппаратным HSM (hardware security module): хранит ключи, постоянно подключён к SHiNE-серверу как device-сессия, подтверждает операции нажатием на экране. Другие устройства (браузер, телефон) взаимодействуют с ESP32 через сервер — без прямого соединения.
|
|
||||||
|
|
||||||
## Два ключевых сценария
|
|
||||||
|
|
||||||
### Сценарий 1 — Создание делегированной сессии
|
|
||||||
1. Браузер/телефон → сервер: «хочу делегированную сессию от имени пользователя X»
|
|
||||||
2. Сервер → ESP32 (device-сессия): «запрос на одобрение»
|
|
||||||
3. Пользователь нажимает «Да» на сенсорном экране ESP32
|
|
||||||
4. ESP32 → сервер: одобрено → сервер создаёт делегированную сессию для браузера
|
|
||||||
|
|
||||||
### Сценарий 2 — Подпись транзакции / блока
|
|
||||||
1. Браузер (через делегированную сессию) → сервер → ESP32: «подпиши вот это»
|
|
||||||
2. ESP32 показывает запрос на экране, пользователь подтверждает
|
|
||||||
3. ESP32 подписывает нужным ключом → ответ через сервер → браузер
|
|
||||||
|
|
||||||
## Что нужно сделать
|
|
||||||
|
|
||||||
### ESP32 (основная работа)
|
|
||||||
- [ ] Инициализация WiFi (SSID/пароль в NVS)
|
|
||||||
- [ ] WebSocket-клиент (`WebSocketsClient`) — постоянное соединение с сервером
|
|
||||||
- [ ] Авторизация на сервере: `AuthChallenge` → `CreateAuthSession` через `deviceKey` (уже есть в NVS), сохранить `sessionId` в NVS
|
|
||||||
- [ ] Обработчик входящих WebSocket-событий: JSON-парсинг, диспетчер по типу
|
|
||||||
- [ ] Новые UI-экраны: «Разрешить сессию?» и «Подписать?» с кнопками Да/Нет
|
|
||||||
- [ ] Расширенное хранилище ключей в NVS (произвольные именованные ключи сверх базовых трёх)
|
|
||||||
- [ ] Переподключение при разрыве (reconnect loop)
|
|
||||||
|
|
||||||
### Сервер (минимальные изменения)
|
|
||||||
- [ ] Добавить поле `sessionType` (`USER` / `DEVICE`) в таблицу `active_sessions`
|
|
||||||
- [ ] Новая операция `DeviceApprovalRequest` — браузер запрашивает одобрение у device-сессии
|
|
||||||
- [ ] Новая операция `DeviceApprovalResponse` — ESP32 отвечает (одобрено/отклонено)
|
|
||||||
- [ ] Новые операции `SignRequest` / `SignResponse` — запрос подписи и ответ
|
|
||||||
- [ ] Роутинг: при получении запроса найти device-сессию через `ActiveConnectionsRegistry.getByLogin(login)` + фильтр по `sessionType=DEVICE`, переслать туда
|
|
||||||
|
|
||||||
### Клиент (отдельный этап)
|
|
||||||
- [ ] Браузерное расширение или UI: создание делегированной сессии, отправка `SignRequest`
|
|
||||||
|
|
||||||
## Что уже готово (переиспользуем)
|
|
||||||
|
|
||||||
- **Роутинг сообщений** — `SendDirectMessage` с `TARGET_ONE_SESSION` и `CallSignalToSession` уже умеют точечно доставлять в конкретный `sessionId`. Механизм готов, нужно добавить только новые op-коды поверх него.
|
|
||||||
- **Ed25519 на ESP32** — библиотека `<Ed25519.h>` уже используется в скетче. Подписи работают.
|
|
||||||
- **NVS** — уже хранит логин, мастер-секрет, 3 пары ключей. Расширяется легко.
|
|
||||||
- **`ActiveConnectionsRegistry`** — поиск по `login` и `sessionId` уже есть на сервере.
|
|
||||||
- **Аутентификация** — схема `AuthChallenge` → `CreateAuthSession` через Ed25519 уже полностью реализована.
|
|
||||||
|
|
||||||
## Оценка сложности
|
|
||||||
|
|
||||||
| Компонент | Сложность |
|
|
||||||
|---|---|
|
|
||||||
| ESP32: WiFi + WebSocket-клиент + авторизация | Средняя |
|
|
||||||
| ESP32: обработчик входящих + UI подтверждений | Средняя |
|
|
||||||
| Сервер: флаг sessionType + 4 новых op-а + роутинг | Низкая–средняя |
|
|
||||||
| Браузерное расширение | Высокая (отдельный этап) |
|
|
||||||
|
|
||||||
**Итого фазы ESP32 + сервер: ~1–1.5 недели.**
|
|
||||||
|
|
||||||
## С чего начинать
|
|
||||||
|
|
||||||
1. Серверная часть проще и быстрее — начать с добавления `sessionType` и `DeviceApprovalRequest/Response`.
|
|
||||||
2. Затем ESP32: WiFi → WebSocket → авторизация → обработчик входящих → UI.
|
|
||||||
3. Браузерное расширение — отдельная итерация после того как ESP32 + сервер работают.
|
|
||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
# Homeserver: технические команды и передача файлов через SHiNE/WebRTC
|
|
||||||
|
|
||||||
## Зачем нужна фича
|
|
||||||
|
|
||||||
Идея на дальнее будущее: дать возможность обращаться к homeserver не только как к участнику сети SHiNE, но и как к удалённой технической точке управления.
|
|
||||||
|
|
||||||
Цели:
|
|
||||||
- отправлять на homeserver технические команды в текстовом виде;
|
|
||||||
- получать текстовый ответ на команду;
|
|
||||||
- при наличии WebRTC DataChannel передавать части файлов в обе стороны;
|
|
||||||
- хранить полученные файлы на SD-карте homeserver;
|
|
||||||
- использовать единый механизм доставки как через сервер SHiNE, так и напрямую через DataChannel.
|
|
||||||
|
|
||||||
## Горизонт
|
|
||||||
|
|
||||||
`far` - идея без ближайшего срока реализации. Сейчас приоритет ниже, чем запуск и стабилизация основного проекта.
|
|
||||||
|
|
||||||
## Что именно имеется в виду
|
|
||||||
|
|
||||||
### 1. Единая модель технической команды
|
|
||||||
|
|
||||||
Техническая команда должна иметь единый смысл независимо от транспорта доставки:
|
|
||||||
- через любой доступный сервер SHiNE;
|
|
||||||
- через уже установленный WebRTC DataChannel.
|
|
||||||
|
|
||||||
Если конкретный транспорт недоступен, ответ по нему может не прийти. Это считается нормальным поведением протокола.
|
|
||||||
|
|
||||||
### 2. Команда как короткоживущий подписанный сигнал
|
|
||||||
|
|
||||||
У команды должны быть:
|
|
||||||
- `commandId`;
|
|
||||||
- временная метка;
|
|
||||||
- TTL около 10 секунд;
|
|
||||||
- криптографическая подпись.
|
|
||||||
|
|
||||||
Смысл такой:
|
|
||||||
- если команда быстро дошла, homeserver подтверждает принятие;
|
|
||||||
- если не дошла вовремя, команда считается протухшей;
|
|
||||||
- отправитель может безопасно послать повтор;
|
|
||||||
- при повторе homeserver отвечает либо `команда принята`, либо `уже выполнено ранее`.
|
|
||||||
|
|
||||||
Это даёт дедупликацию и безопасный resend без повторного выполнения действия.
|
|
||||||
|
|
||||||
### 3. Текстовые технические команды
|
|
||||||
|
|
||||||
Базовый сценарий похож на короткий удалённый shell-протокол, но на уровне строго ограниченных команд:
|
|
||||||
- отправил строку-команду;
|
|
||||||
- получил строку-ответ.
|
|
||||||
|
|
||||||
Команды не обязаны исполнять произвольный shell. Предпочтительная модель - белый список операций с контролируемым форматом аргументов и ответа.
|
|
||||||
|
|
||||||
### 4. Передача файлов только при наличии DataChannel
|
|
||||||
|
|
||||||
Если между устройствами есть WebRTC DataChannel, через него можно передавать технические сообщения для файлового обмена.
|
|
||||||
|
|
||||||
Предварительная модель:
|
|
||||||
- имя файла = `SHA-256` содержимого;
|
|
||||||
- можно запросить диапазон байт `from..to`;
|
|
||||||
- можно отправить диапазон байт `from..to`;
|
|
||||||
- homeserver хранит полученные данные на SD-карте;
|
|
||||||
- если DataChannel нет, на запрос файловой передачи возвращается ответ в духе `не могу передать, нет data channel`.
|
|
||||||
|
|
||||||
Фактически файл-обмен должен быть частным случаем общего протокола технических команд.
|
|
||||||
|
|
||||||
### 5. Установка data-соединения по явной команде
|
|
||||||
|
|
||||||
Нужна техническая команда уровня:
|
|
||||||
- `установить data-соединение`.
|
|
||||||
|
|
||||||
Ответ:
|
|
||||||
- либо `да`, после чего запускается обычная процедура `offer/answer/ICE`;
|
|
||||||
- либо `нет` и причина отказа.
|
|
||||||
|
|
||||||
### 6. Доставка на пользовательские сессии
|
|
||||||
|
|
||||||
Логика должна быть совместима с общей моделью SHiNE, где технические сигналы можно отправлять на конкретные активные сессии пользователя.
|
|
||||||
|
|
||||||
Идея:
|
|
||||||
- на любую активную сессию пользователя можно посылать техническую команду;
|
|
||||||
- контакт пользователя может инициировать такую техническую коммуникацию так же, как он уже инициирует звонок или другой служебный сигнал.
|
|
||||||
|
|
||||||
## Что нужно будет сделать при возврате к задаче
|
|
||||||
|
|
||||||
- Спроектировать отдельный формат технических команд и ack-ответов.
|
|
||||||
- Решить, будет ли это новый тип служебных сообщений в существующем протоколе блокчейн/сигналинга или отдельная ветка поверх уже имеющихся transport-операций.
|
|
||||||
- Отдельно продумать авторизацию: кто именно из контактов и какие команды имеет право слать.
|
|
||||||
- Ограничить набор допустимых команд, чтобы не превратить механизм в небезопасный удалённый shell.
|
|
||||||
- Спроектировать протокол чанков файлов: размер чанка, нумерация, повторная отправка, контроль целостности, дозагрузка, завершение файла.
|
|
||||||
- Продумать хранение на SD-карте: временные файлы, сборка чанков, проверка итогового `SHA-256`, очистка мусора.
|
|
||||||
- Продумать поведение при отсутствии DataChannel, таймаутах и дублирующихся командах.
|
|
||||||
- Проверить, как это лучше встраивать в текущие клиентские сессии, звонки и homeserver-логику.
|
|
||||||
|
|
||||||
## Вопросы для будущего уточнения
|
|
||||||
|
|
||||||
- Это должен быть строго служебный протокол или пользователь сможет вызывать его и вручную из UI.
|
|
||||||
- Нужен ли доступ только к заранее разрешённым каталогам/файлам.
|
|
||||||
- Нужна ли двусторонняя синхронизация файлов или достаточно ручных команд `запросить кусок` / `отправить кусок`.
|
|
||||||
- Нужно ли разрешать передачу файлов через сервер SHiNE как fallback, или файл-обмен должен идти только через DataChannel.
|
|
||||||
- Какой максимальный размер файлов и допустимый объём хранения на SD-карте.
|
|
||||||
|
|
||||||
## Что уже сделано
|
|
||||||
|
|
||||||
Пока только зафиксирована идея и базовая концепция. Реализация не начиналась.
|
|
||||||
|
|
||||||
## Какие документы нужно будет обновить при реализации
|
|
||||||
|
|
||||||
- `Dev_Docs/Blockchain/README.md` и связанные файлы, если изменятся типы служебных сообщений или форматы блокчейн-команд.
|
|
||||||
- `Dev_Docs/API/` если изменится публичный серверный API или появятся новые операции.
|
|
||||||
- `Dev_Docs/Personal_Messages/README.md` если часть маршрутизации или подтверждений будет встроена в существующую логику доставки/сессий.
|
|
||||||
- Документацию по homeserver/ESP32, если появится пользовательская или сервисная файловая логика на устройстве.
|
|
||||||
|
|
||||||
## С какого места продолжать позже
|
|
||||||
|
|
||||||
Возвращаться к задаче только после стабилизации запуска проекта и базовых текущих функций. Начинать с проектирования протокола команд и матрицы прав доступа, а уже потом переходить к DataChannel-файлообмену.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Дальнее будущее
|
|
||||||
|
|
||||||
Сюда переносить задачи, у которых нет понятного срока возврата и которые не нужно учитывать в ближайшем или среднесрочном планировании.
|
|
||||||
|
|
||||||
## Идеи
|
|
||||||
|
|
||||||
- `2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md` - технические команды для homeserver через сервер SHiNE или WebRTC DataChannel, а также файловый обмен чанками с хранением на SD-карте.
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# Кошелёк и пополнение баланса сияния
|
|
||||||
|
|
||||||
- Горизонт:
|
|
||||||
`medium`
|
|
||||||
- Ориентир:
|
|
||||||
среднесрочно
|
|
||||||
- Статус:
|
|
||||||
`proposal`
|
|
||||||
|
|
||||||
## Кратко
|
|
||||||
|
|
||||||
Нужно добавить кошелёк для внутреннего баланса сияния и пополнение этого баланса через блокчейн-логику проекта. Задача связана с регистрацией пользователя и будущим учётом баланса.
|
|
||||||
|
|
||||||
## Предполагаемый сценарий
|
|
||||||
|
|
||||||
1. Пользователь регистрируется и получает/подключает нужные кошельки.
|
|
||||||
2. В интерфейсе появляется баланс сияния.
|
|
||||||
3. Пользователь открывает пополнение баланса сияния.
|
|
||||||
4. Система создаёт или принимает блокчейн-операцию пополнения.
|
|
||||||
5. После подтверждения баланса UI обновляет значение.
|
|
||||||
|
|
||||||
## Что нужно продумать
|
|
||||||
|
|
||||||
1. Что именно является единицей баланса сияния.
|
|
||||||
2. Где хранится состояние баланса: в существующем блокчейне SHiNE, Solana-модуле или комбинированно.
|
|
||||||
3. Какая операция отвечает за пополнение.
|
|
||||||
4. Нужно ли делать отдельную регистрацию кошелька сияния или использовать существующую регистрацию пользователя.
|
|
||||||
5. Как баланс восстанавливается после перезагрузки клиента.
|
|
||||||
6. Какие права нужны для пополнения и списания.
|
|
||||||
7. Нужна ли история операций баланса.
|
|
||||||
|
|
||||||
## Вопросы перед реализацией
|
|
||||||
|
|
||||||
1. Пополнение баланса сияния должно идти через основной блокчейн SHiNE или через Solana-программу.
|
|
||||||
2. Нужна ли конвертация из SOL/AR в сияние.
|
|
||||||
3. Кто может выпускать или начислять сияние.
|
|
||||||
4. Нужно ли поддерживать перевод сияния между пользователями.
|
|
||||||
5. Нужны ли лимиты, комиссии или статусы подтверждения.
|
|
||||||
6. Какой экран должен показывать баланс: регистрация, профиль, кошелёк или отдельная страница.
|
|
||||||
7. Нужно ли отображать неподтверждённый баланс отдельно от подтверждённого.
|
|
||||||
|
|
||||||
## Важное ограничение
|
|
||||||
|
|
||||||
Если для баланса сияния потребуется новый формат блокчейн-блока или изменение существующего формата, перед реализацией нужно отдельно предупредить пользователя и получить явное подтверждение на изменение формата блокчейна.
|
|
||||||
|
|
||||||
Если потребуется новый серверный API или изменение существующих `op`, перед реализацией нужно отдельно предупредить пользователя и получить явное подтверждение на изменение API.
|
|
||||||
|
|
||||||
## Документы, которые обновить при реализации
|
|
||||||
|
|
||||||
- `Dev_Docs/Blockchain/`, если появятся или изменятся блоки баланса.
|
|
||||||
- `Dev_Docs/Blockchain/CHANGELOG.md`, если меняется блокчейн-формат.
|
|
||||||
- `Dev_Docs/API/`, если меняется серверный API.
|
|
||||||
- `Dev_Docs/Pending_Features/` - добавить файл ручной проверки после реализации.
|
|
||||||
- Документацию Solana-регистрации, если баланс будет связан с Solana-модулем.
|
|
||||||
|
|
||||||
## Минимальная проверка в будущем
|
|
||||||
|
|
||||||
1. Новый пользователь видит корректный начальный баланс.
|
|
||||||
2. Пополнение создаёт правильную операцию.
|
|
||||||
3. Баланс обновляется после подтверждения.
|
|
||||||
4. После перезагрузки UI баланс остаётся корректным.
|
|
||||||
5. Ошибочные или повторные операции не начисляют баланс дважды.
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# ESP32S3 как личное файловое хранилище SHiNE
|
|
||||||
|
|
||||||
## Горизонт
|
|
||||||
|
|
||||||
Среднесрочный: ближайшие недели или 1-2 месяца.
|
|
||||||
|
|
||||||
## Зачем нужна фича
|
|
||||||
|
|
||||||
Нужно проработать маленький физический сервер на ESP32S3 как персональное или доверенное файловое хранилище SHiNE.
|
|
||||||
|
|
||||||
Идея: при обмене сообщениями пользователи смогут использовать такой сервер для хранения своих файлов, вложений, файлов общих переписок и связанных данных.
|
|
||||||
|
|
||||||
## Что нужно сделать
|
|
||||||
|
|
||||||
- Описать роль ESP32S3-сервера в общей архитектуре ключей и сессий.
|
|
||||||
- Определить, какие ключи может хранить такое устройство.
|
|
||||||
- Решить, хранит ли устройство только файлы или также подписывает пользовательские операции.
|
|
||||||
- Описать протокол загрузки, скачивания и удаления файлов.
|
|
||||||
- Определить правила шифрования файлов до отправки на устройство.
|
|
||||||
- Продумать индексацию файлов для личных и общих переписок.
|
|
||||||
- Решить, как устройство авторизуется на основном сервере SHiNE.
|
|
||||||
|
|
||||||
## Вопросы перед реализацией
|
|
||||||
|
|
||||||
- ESP32S3 должен работать как полностью локальное устройство или как публично доступный мини-сервер?
|
|
||||||
- Нужен ли внешний relay, если устройство находится за NAT?
|
|
||||||
- Какие ограничения по размеру файла считаем допустимыми?
|
|
||||||
- Хранит ли устройство метаданные переписок или только зашифрованные blob-файлы?
|
|
||||||
- Как восстанавливать доступ, если устройство потеряно или заменено?
|
|
||||||
|
|
||||||
## Что уже сделано
|
|
||||||
|
|
||||||
Код не реализован. Идея зафиксирована как будущая задача после описания модели ключей.
|
|
||||||
|
|
||||||
## Документы, которые нужно обновить при возврате
|
|
||||||
|
|
||||||
- `Dev_Docs/Keys/README.md`
|
|
||||||
- `Dev_Docs/Personal_Messages/README.md`
|
|
||||||
- `Dev_Docs/API/`
|
|
||||||
- `Dev_Docs/Blockchain/`, если появятся новые блоки или команды для файлов.
|
|
||||||
|
|
||||||
## С какого места продолжать
|
|
||||||
|
|
||||||
Начать с короткого протокольного документа: роли устройства, авторизация, шифрование файлов, минимальные API-операции и сценарии восстановления.
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# Сессионные homeserver-ы в PDA пользователя
|
|
||||||
|
|
||||||
- Статус:
|
|
||||||
`future`
|
|
||||||
|
|
||||||
- Горизонт:
|
|
||||||
`medium`
|
|
||||||
|
|
||||||
- Ориентир:
|
|
||||||
после завершения первого этапа по пользовательским сессиям
|
|
||||||
|
|
||||||
- Основание:
|
|
||||||
Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних homeserver-ов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли.
|
|
||||||
|
|
||||||
## Зачем нужна фича
|
|
||||||
|
|
||||||
У одного пользователя может быть несколько доверенных внутренних homeserver-ов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели.
|
|
||||||
|
|
||||||
Это нужно, чтобы:
|
|
||||||
|
|
||||||
- хранить несколько homeserver-ов у одного пользователя одновременно;
|
|
||||||
- различать обычные клиентские сессии и серверные сессии по явному типу;
|
|
||||||
- дать расширяемый формат записи с версией;
|
|
||||||
- использовать единый подход для DM, звонков и внутренних команд между сессиями.
|
|
||||||
|
|
||||||
## Целевая идея
|
|
||||||
|
|
||||||
В пользовательском PDA должен появиться список записей сессий, где каждая запись содержит как минимум:
|
|
||||||
|
|
||||||
- `sessionType` (`u8`);
|
|
||||||
- `sessionVersion` (`u8`);
|
|
||||||
- `sessionName`;
|
|
||||||
- `sessionPubKey`.
|
|
||||||
|
|
||||||
Предварительные значения:
|
|
||||||
|
|
||||||
- тип `1` - обычная пользовательская сессия;
|
|
||||||
- тип `100` - homeserver пользователя;
|
|
||||||
- версия `1` - первая рабочая версия формата записи сессии.
|
|
||||||
|
|
||||||
На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`.
|
|
||||||
|
|
||||||
Важно: homeserver-ов у одного пользователя может быть несколько.
|
|
||||||
|
|
||||||
## Архитектурный принцип
|
|
||||||
|
|
||||||
Внутренний протокол взаимодействия должен оставаться транспортным.
|
|
||||||
|
|
||||||
То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки homeserver-а, а должен:
|
|
||||||
|
|
||||||
- доставлять сообщения между сессиями;
|
|
||||||
- доставлять сигналы звонков между сессиями;
|
|
||||||
- хранить и маршрутизировать адресацию;
|
|
||||||
- не принимать на себя бизнес-логику содержимого внутренних команд.
|
|
||||||
|
|
||||||
## Что уже подтверждается текущим кодом
|
|
||||||
|
|
||||||
- Личные сообщения уже доставляются по всем сессиям целевого пользователя с отдельным учётом доставки на каждую сессию.
|
|
||||||
- Подтверждение доставки DM уже идёт отдельно по каждой сессии.
|
|
||||||
- Вызов звонка уже рассылается по нескольким активным сессиям пользователя.
|
|
||||||
- Сигналы звонка уже адресуются конкретной сессии, а stop-сигналы дублируются на остальные сессии того же пользователя.
|
|
||||||
|
|
||||||
Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол homeserver-а".
|
|
||||||
|
|
||||||
## Что нужно сделать при возврате к задаче
|
|
||||||
|
|
||||||
1. Согласовать финальный бинарный формат записи сессии в PDA пользователя.
|
|
||||||
2. Проверить, не меняет ли это уже опубликованный формат пользовательской PDA-записи.
|
|
||||||
3. Если формат PDA меняется, заранее предупредить пользователя и получить отдельное подтверждение.
|
|
||||||
4. Решить, где именно хранится массив сессий:
|
|
||||||
- в основной записи пользователя;
|
|
||||||
- в отдельной PDA-структуре расширения;
|
|
||||||
- или в смешанной схеме с базовой записью и внешними индексами.
|
|
||||||
5. Зафиксировать ограничения:
|
|
||||||
- максимальное число сессий;
|
|
||||||
- максимальную длину `sessionName`;
|
|
||||||
- правила удаления и обновления записи;
|
|
||||||
- правила ротации `sessionPubKey`.
|
|
||||||
6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`.
|
|
||||||
7. Определить, какие внутренние сообщения homeserver-а останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
|
|
||||||
8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов.
|
|
||||||
9. После реализации обязательно обновить документацию.
|
|
||||||
|
|
||||||
## Что нужно обновить при реализации
|
|
||||||
|
|
||||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
|
||||||
- `Dev_Docs/Solana_Architecture/README.md`
|
|
||||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
|
||||||
- `Dev_Docs/Keys/README.md`
|
|
||||||
- `Dev_Docs/Personal_Messages/README.md`, если изменится адресация DM по типам сессий
|
|
||||||
- `Dev_Docs/API/`, если появятся новые серверные операции или изменятся ответы
|
|
||||||
|
|
||||||
## Что пока не делать
|
|
||||||
|
|
||||||
- Не включать это автоматически в основной deploy сервера.
|
|
||||||
- Не менять сейчас Solana PDA-формат без отдельного подтверждения.
|
|
||||||
- Не добавлять временные поля в публичный API "на всякий случай".
|
|
||||||
|
|
||||||
## С какого места продолжать
|
|
||||||
|
|
||||||
Продолжать после завершения первой части:
|
|
||||||
|
|
||||||
1. описать минимальный формат записи пользовательской сессии;
|
|
||||||
2. отдельно решить, живут ли homeserver-ы в том же списке, что и обычные сессии;
|
|
||||||
3. затем уже проектировать операции регистрации, обновления и отключения таких сессий.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Подключение других устройств через QR
|
|
||||||
|
|
||||||
- Горизонт:
|
|
||||||
`medium`
|
|
||||||
- Ориентир:
|
|
||||||
позже, не сейчас
|
|
||||||
- Статус:
|
|
||||||
`future`
|
|
||||||
|
|
||||||
## Зачем нужна фича
|
|
||||||
|
|
||||||
Нужно нормально довести подключение другого устройства через QR-код. Сейчас есть полуготовая заготовка, но сценарий работает нестабильно и требует отдельной доработки.
|
|
||||||
|
|
||||||
## Что уже есть
|
|
||||||
|
|
||||||
- В UI уже есть экраны:
|
|
||||||
- `shine-UI/js/pages/connect-device-view.js`
|
|
||||||
- `shine-UI/js/pages/device-qr-view.js`
|
|
||||||
- Есть сервис переноса ключей через QR:
|
|
||||||
- `shine-UI/js/services/qr-key-transfer-service.js`
|
|
||||||
- Логика частично собрана, но её нельзя считать завершённой или надёжной.
|
|
||||||
|
|
||||||
## Что нужно будет сделать потом
|
|
||||||
|
|
||||||
1. Проверить и довести формат QR-передачи.
|
|
||||||
2. Проверить сканирование и ручной ввод QR-текста.
|
|
||||||
3. Проверить перенос `device`, `blockchain`, `root` ключей только по реальному наличию на исходном устройстве.
|
|
||||||
4. Проверить, что после переноса очищается старая история нужного логина и не ломается вход.
|
|
||||||
5. Отдельно проверить сценарий без `BarcodeDetector`.
|
|
||||||
6. Довести экран подтверждения на втором устройстве.
|
|
||||||
|
|
||||||
## Что сейчас важно
|
|
||||||
|
|
||||||
- Не считать эту часть готовой.
|
|
||||||
- Не возвращать её в активную разработку без отдельной команды пользователя.
|
|
||||||
- Если вернёмся к задаче, сначала нужно понять, что именно уже работает, а что нет, и потом починить целиком.
|
|
||||||
|
|
||||||
## Что обновить при возврате
|
|
||||||
|
|
||||||
- `Dev_Docs/Pending_Features/README.md`
|
|
||||||
- `shine-UI/js/pages/connect-device-view.js`
|
|
||||||
- `shine-UI/js/pages/device-qr-view.js`
|
|
||||||
- `shine-UI/js/services/qr-key-transfer-service.js`
|
|
||||||
- документацию по ключам, если формат переноса меняется
|
|
||||||
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# Telegram-агент для разрешённых игроков
|
|
||||||
|
|
||||||
- Горизонт:
|
|
||||||
`near`
|
|
||||||
- Ориентир:
|
|
||||||
сегодня/завтра
|
|
||||||
- Статус:
|
|
||||||
`proposal`
|
|
||||||
|
|
||||||
## Кратко
|
|
||||||
|
|
||||||
Нужно расширить `SHiNE-agent-bot-coder`, чтобы агент мог принимать личные сообщения от заранее разрешённых пользователей, вести по каждому отдельную рабочую папку и историю, помогать им с обсуждениями/документами без изменения кода, а краткий результат публиковать в общий канал.
|
|
||||||
|
|
||||||
## Пользовательский сценарий
|
|
||||||
|
|
||||||
1. Разрешённый пользователь пишет агенту в личные сообщения текстом или голосом.
|
|
||||||
2. Голосовое сообщение распознаётся так же, как сейчас распознаются voice/audio-задачи.
|
|
||||||
3. Сервис определяет пользователя по разрешённому списку логинов.
|
|
||||||
4. Для пользователя используется отдельная папка в `Players/`.
|
|
||||||
5. Codex запускается с системным контекстом: от имени какого человека он работает, где лежит его папка, какие у него локальные инструкции.
|
|
||||||
6. Агент может читать код и документацию проекта, но писать должен только в папку этого пользователя, если нет отдельного согласования на изменение общего проекта.
|
|
||||||
7. После ответа пользователю агент отправляет в общий канал короткую сводку двумя сообщениями или двумя блоками: вопрос пользователя и полученный ответ.
|
|
||||||
8. Команда `/new` или `New` сбрасывает только сессию этого пользователя.
|
|
||||||
|
|
||||||
## Предлагаемая структура
|
|
||||||
|
|
||||||
- `Players/`
|
|
||||||
- `Ivan/`
|
|
||||||
- `AGENTS.md`
|
|
||||||
- `history/`
|
|
||||||
- `files/`
|
|
||||||
- `Sergey/`
|
|
||||||
- `AGENTS.md`
|
|
||||||
- `history/`
|
|
||||||
- `files/`
|
|
||||||
- `Milana/`
|
|
||||||
- `AGENTS.md`
|
|
||||||
- `history/`
|
|
||||||
- `files/`
|
|
||||||
|
|
||||||
Имена папок можно уточнить после получения точных Telegram-логинов.
|
|
||||||
|
|
||||||
## Что нужно сделать
|
|
||||||
|
|
||||||
1. Добавить конфигурацию разрешённых Telegram-пользователей.
|
|
||||||
2. Описать соответствие `telegram username -> имя игрока -> папка`.
|
|
||||||
3. Создавать или использовать отдельную историю диалога для каждого игрока.
|
|
||||||
4. Поддержать личные сообщения от разрешённых пользователей.
|
|
||||||
5. Запретить постановку задач от неизвестных пользователей.
|
|
||||||
6. Для групп/каналов оставить текущую логику: команды Айдара имеют приоритет.
|
|
||||||
7. При запуске Codex для игрока добавлять отдельный системный контекст:
|
|
||||||
- имя пользователя;
|
|
||||||
- путь к его папке;
|
|
||||||
- правило записи только в эту папку;
|
|
||||||
- путь к персональному `AGENTS.md`.
|
|
||||||
8. После ответа игроку отправлять краткую сводку в общий канал.
|
|
||||||
9. Поддержать `/new`/`New` как сброс только персональной сессии игрока.
|
|
||||||
10. Добавить защиту от случайного изменения общего кода в режиме игрока.
|
|
||||||
|
|
||||||
## Вопросы перед реализацией
|
|
||||||
|
|
||||||
1. Точные Telegram-логины Ивана, Сергея и Миланы.
|
|
||||||
2. Какой общий канал использовать для сводок: текущий `@shine_writing` или отдельный чат.
|
|
||||||
3. Нужно ли отправлять в общий канал полный текст вопроса/ответа или краткую выжимку.
|
|
||||||
4. Нужно ли пересылать вложения игроков в общий канал или только текстовые сводки.
|
|
||||||
5. Разрешить ли игрокам читать все документы проекта, включая технические заметки деплоя.
|
|
||||||
6. Что делать, если пользователь просит изменить код: отказать, создать предложение в своей папке или просить подтверждение Айдара.
|
|
||||||
7. Нужны ли русские имена папок (`Иван`, `Сергей`, `Милана`) или ASCII-имена (`Ivan`, `Sergey`, `Milana`).
|
|
||||||
8. Нужно ли хранить истории игроков в общей папке сервиса или внутри `Players/<name>/history/`.
|
|
||||||
|
|
||||||
## Риски и ограничения
|
|
||||||
|
|
||||||
- Нужно аккуратно разделить режим Айдара и режим игрока, чтобы игроки не могли случайно запустить изменение общего кода.
|
|
||||||
- Нужно не смешать истории разных пользователей.
|
|
||||||
- Нужно ограничить публикацию в общий канал, чтобы не утекали личные или слишком длинные ответы.
|
|
||||||
- Нужна проверка Telegram-идентификации: username может меняться, поэтому желательно хранить и `user_id`.
|
|
||||||
|
|
||||||
## Документы, которые обновить при реализации
|
|
||||||
|
|
||||||
- `SHiNE-agent-bot-coder/AGENTS.md`
|
|
||||||
- `SHiNE-agent-bot-coder/AGENT.md`
|
|
||||||
- `SHiNE-agent-bot-coder/README.md`
|
|
||||||
- `Dev_Docs/deploy/agent-bot-coder-local-systemd.md`, если появятся новые переменные окружения или настройки сервиса.
|
|
||||||
|
|
||||||
## Минимальная проверка
|
|
||||||
|
|
||||||
1. Айдар по-прежнему может ставить задачи из `@shine_writing`.
|
|
||||||
2. Неизвестный пользователь не ставит задачу в очередь.
|
|
||||||
3. Разрешённый игрок пишет личное текстовое сообщение и получает ответ.
|
|
||||||
4. Разрешённый игрок отправляет voice, оно распознаётся и обрабатывается.
|
|
||||||
5. История одного игрока не попадает в историю другого.
|
|
||||||
6. `/new` сбрасывает только историю текущего игрока.
|
|
||||||
7. Сводка вопрос/ответ появляется в общем канале.
|
|
||||||
8. В режиме игрока агент не пишет за пределы `Players/<name>/` без отдельного подтверждения.
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Пополнение Solana и Arweave через внешний сервис покупки
|
|
||||||
|
|
||||||
- Горизонт:
|
|
||||||
`near`
|
|
||||||
- Ориентир:
|
|
||||||
сегодня/завтра
|
|
||||||
- Статус:
|
|
||||||
`proposal`
|
|
||||||
|
|
||||||
## Кратко
|
|
||||||
|
|
||||||
Нужно добавить удобное пополнение кошельков на экране регистрации/кошелька: для Solana и Arweave дать отдельные действия `Пополнить`, которые ведут на международный сервис покупки криптовалюты с карты и помогают пользователю скопировать адрес кошелька.
|
|
||||||
|
|
||||||
## Пользовательский сценарий
|
|
||||||
|
|
||||||
1. Пользователь видит адрес кошелька Solana или Arweave.
|
|
||||||
2. Нажимает `Пополнить`.
|
|
||||||
3. Открывается промежуточное окно с инструкцией:
|
|
||||||
- сейчас пользователь перейдёт на страницу покупки/пополнения;
|
|
||||||
- нужно указать или проверить адрес кошелька;
|
|
||||||
- после оплаты нужно закрыть внешнюю страницу и вернуться назад;
|
|
||||||
- Solana обычно приходит быстро, ориентир 10-15 секунд после подтверждения сети;
|
|
||||||
- Arweave может идти дольше, точное время нужно уточнить по выбранному сервису.
|
|
||||||
4. В окне есть кнопки:
|
|
||||||
- `Скопировать адрес и перейти`;
|
|
||||||
- `Перейти без копирования`.
|
|
||||||
5. Для Solana и Arweave используются разные окна/инструкции и, возможно, разные внешние ссылки.
|
|
||||||
|
|
||||||
## Что нужно сделать
|
|
||||||
|
|
||||||
1. Найти текущий экран, где показываются кошельки при регистрации и пополнении.
|
|
||||||
2. Найти текущую ссылку покупки Arweave, если она уже есть в UI.
|
|
||||||
3. Выбрать международный сервис покупки Solana с карты, не российский.
|
|
||||||
4. Проверить, поддерживает ли сервис deep link с предзаполненным адресом кошелька.
|
|
||||||
5. Если deep link невозможен, реализовать промежуточное окно с копированием адреса.
|
|
||||||
6. Добавить отдельные действия для Solana и Arweave.
|
|
||||||
7. Сделать текст инструкции коротким и понятным.
|
|
||||||
8. Проверить, что адрес копируется в буфер обмена в браузере.
|
|
||||||
9. Проверить мобильный сценарий и desktop-сценарий.
|
|
||||||
|
|
||||||
## Вопросы перед реализацией
|
|
||||||
|
|
||||||
1. Какой сервис покупки Solana использовать: тот же провайдер, что для Arweave, или другой международный on-ramp.
|
|
||||||
2. Нужно ли разрешать покупку только SOL или также USDC/SPL-токены на Solana.
|
|
||||||
3. Где именно показывать кнопку `Пополнить`: только регистрация, настройки кошелька или оба места.
|
|
||||||
4. Нужно ли показывать предупреждение о комиссиях и стороннем сервисе.
|
|
||||||
5. Нужно ли открывать внешнюю страницу в новой вкладке или в текущем окне.
|
|
||||||
6. Нужно ли логировать факт нажатия `Пополнить` на сервере.
|
|
||||||
7. Какой точный текст использовать для времени прихода Arweave.
|
|
||||||
|
|
||||||
## Риски и ограничения
|
|
||||||
|
|
||||||
- On-ramp-сервисы меняют ссылки и параметры, поэтому deep link нужно проверять перед реализацией.
|
|
||||||
- Clipboard API может требовать HTTPS и пользовательский жест.
|
|
||||||
- Нельзя обещать точное время поступления средств: лучше писать ориентир и зависимость от сети/провайдера.
|
|
||||||
- Внешний сервис может быть недоступен в отдельных странах или для отдельных карт.
|
|
||||||
|
|
||||||
## Документы, которые обновить при реализации
|
|
||||||
|
|
||||||
- Документацию UI/кошельков, если такая есть.
|
|
||||||
- `Dev_Docs/Pending_Features/` - добавить файл ручной проверки после реализации.
|
|
||||||
- `Dev_Docs/API/`, только если появится новый серверный API или логирование.
|
|
||||||
|
|
||||||
## Минимальная проверка
|
|
||||||
|
|
||||||
1. На Solana-кошельке открывается правильное окно пополнения.
|
|
||||||
2. Кнопка `Скопировать адрес и перейти` копирует Solana-адрес и открывает внешний сервис.
|
|
||||||
3. Кнопка `Перейти без копирования` открывает внешний сервис без копирования.
|
|
||||||
4. Аналогичный сценарий работает для Arweave.
|
|
||||||
5. На мобильном экране текст и кнопки не перекрываются.
|
|
||||||
6. Возврат назад в приложение не ломает состояние регистрации/кошелька.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Быстрое скрытие экрана звонка и остановка гудков при отклонении
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
- Исправлена нижняя подпись вкладки личных сообщений на `личные`.
|
|
||||||
- Исправлена логика звонка, когда одна из входящих сессий отклоняет вызов до принятия: исходящая сессия теперь должна прекращать гудки даже если ранее `RINGING` пришёл от другой входящей сессии.
|
|
||||||
- На устройстве, где пользователь нажимает отмену/сброс звонка, экран вызова теперь скрывается сразу локально, без ожидания сетевого ответа.
|
|
||||||
|
|
||||||
- что именно проверять:
|
|
||||||
- В нижней панели с 5 кнопками подпись первой кнопки должна отображаться как `личные`.
|
|
||||||
- Сценарий: один пользователь звонит, у второго входящий вызов приходит на несколько устройств; на любом одном входящем устройстве нажать `Сбросить`.
|
|
||||||
- Проверить, что у звонящего сразу прекращаются гудки и экран вызова корректно завершается.
|
|
||||||
- Проверить, что на устройстве, где нажали `Сбросить` или `Положить трубку`, overlay звонка исчезает сразу, без заметной задержки.
|
|
||||||
- Проверить, что после принятия звонка на одном устройстве поздние отмены с других устройств не ломают уже выбранную пару соединения.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
- Подпись в нижней панели корректная.
|
|
||||||
- При отклонении входящего звонка любым устройством звонящего не оставляет в состоянии бесконечных гудков.
|
|
||||||
- Локальный экран звонка скрывается мгновенно после нажатия кнопки отмены.
|
|
||||||
- Уже зафиксированный сценарий соединения после `ACCEPT` не сбивается другими сессиями.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
- `pending`
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Отключение устаревшего TURN-узла `45.136.124.227`
|
|
||||||
|
|
||||||
- краткое описание:
|
|
||||||
- из конфигурации звонков убран устаревший TURN-узел `45.136.124.227:3478`;
|
|
||||||
- основным и единственным выдаваемым TURN-узлом оставлен `93.170.12.154:3478`.
|
|
||||||
- что проверять:
|
|
||||||
- сделать несколько тестовых звонков между разными устройствами/сетями;
|
|
||||||
- убедиться, что звонок доходит до стадии соединения и появляется звук;
|
|
||||||
- убедиться, что в логах `CallDeliveryReport` больше не фигурирует `45.136.124.227`.
|
|
||||||
- ожидаемый результат:
|
|
||||||
- клиентам больше не выдаётся устаревший TURN-адрес;
|
|
||||||
- звонки не заваливаются из-за попыток использовать отключённый TURN-узел.
|
|
||||||
- статус:
|
|
||||||
- pending
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Фикс самообрыва звонка из-за `stop_call` push своей же сессии
|
|
||||||
|
|
||||||
- краткое описание:
|
|
||||||
- исправлена ситуация, когда активный звонок мог оборваться сразу после соединения;
|
|
||||||
- причина была в том, что `stop_call` push, предназначенный для других сессий того же пользователя, обрабатывался и в исходной сессии.
|
|
||||||
- что проверять:
|
|
||||||
- открыть несколько вкладок/устройств одного пользователя;
|
|
||||||
- принять звонок на одной сессии;
|
|
||||||
- убедиться, что активная сессия не обрывает звонок сразу после соединения;
|
|
||||||
- убедиться, что лишние сессии при этом закрывают свой локальный экран звонка.
|
|
||||||
- ожидаемый результат:
|
|
||||||
- звонок не завершается сразу после `call_connected`;
|
|
||||||
- `accepted_on_other_device` и связанные `stop_call` события больше не убивают исходную активную сессию.
|
|
||||||
- статус:
|
|
||||||
- pending
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Фикс привязки call push к целевой sessionId
|
|
||||||
|
|
||||||
- краткое описание:
|
|
||||||
- push-события `incoming_call` и `stop_call` теперь помечаются целевой `sessionId`;
|
|
||||||
- UI и service worker обрабатывают call push только для своей целевой сессии;
|
|
||||||
- `stop_call` для лишних сессий закрывает локальный экран тихо, без обратных сигналов и без лишних тех-сообщений.
|
|
||||||
- что проверять:
|
|
||||||
- держать несколько сессий одного пользователя в одном браузере/на одном origin;
|
|
||||||
- позвонить этому пользователю и убедиться, что входящий экран закрывается корректно только на целевых сессиях;
|
|
||||||
- после `ACCEPT` одной сессии остальные должны тихо убрать экран вызова и не ломать выбранную пару;
|
|
||||||
- после отмены входящей сессией исходящая сессия должна централизованно завершить сценарий.
|
|
||||||
- ожидаемый результат:
|
|
||||||
- push одного session endpoint больше не влияет на чужие сессии этого же origin;
|
|
||||||
- исчезают ложные `stop_call_push:accepted_on_other_device` и `terminal_call_signal_150` на неправильных сессиях.
|
|
||||||
- статус:
|
|
||||||
- pending
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# ESP32 homeserver: заявки на подключение устройств
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
- на ESP32 homeserver в `SETTINGS` добавлен первый пункт `Device requests`, который появляется только после авторизации homeserver в SHiNE;
|
|
||||||
- экран показывает список активных pairing-заявок, позволяет открыть каждую заявку и подтвердить или отклонить её;
|
|
||||||
- формат кода подключения изменён на `10` цифр и показывается как `5` пар.
|
|
||||||
|
|
||||||
- что проверять:
|
|
||||||
- на обычном клиенте и в wallet-plugin код отображается как `XX XX XX XX XX`;
|
|
||||||
- на доверенном веб-клиенте экран `Подключить по коду` показывает все активные заявки без поля ручного ввода;
|
|
||||||
- на ESP32 после успешной homeserver-авторизации в `SETTINGS` появляется пункт `Device requests` первым;
|
|
||||||
- `REFRESH` реально загружает активные заявки;
|
|
||||||
- на экране видно две плитки, список листается вертикально;
|
|
||||||
- client-session заявка после `YES` подключается с передачей только `device key`;
|
|
||||||
- wallet-session заявка после `YES` подключается без передачи ключей, через выпуск отдельной wallet-session;
|
|
||||||
- `NO` отклоняет заявку и она исчезает из списка активных.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
- все три клиента используют единый формат кода;
|
|
||||||
- активные заявки видны без ручного ввода кода;
|
|
||||||
- ESP32 может одобрять и отклонять живые pairing-заявки пользователя.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
- pending
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Тестовые deploy-контуры `test2.shineup.me` и `test.shineup.me`
|
|
||||||
|
|
||||||
- краткое описание:
|
|
||||||
- default deploy-задачи `deployServer` и `deployUI` переведены на основной тестовый сервер `test2.shineup.me`;
|
|
||||||
- production-задачи вынесены в `deployServerProduction` и `deployUIProduction`;
|
|
||||||
- `test.shineup.me` оставлен как резервный тестовый сервер без обычного deploy по умолчанию.
|
|
||||||
|
|
||||||
- что проверять:
|
|
||||||
- `./gradlew deployServer` и `./gradlew deployUI` действительно направлены на `test2.shineup.me`;
|
|
||||||
- `./gradlew deployServerProduction` и `./gradlew deployUIProduction` больше не используются как default;
|
|
||||||
- `https://test2.shineup.me` открывает UI;
|
|
||||||
- `wss://test2.shineup.me/ws` отвечает;
|
|
||||||
- на `test2.shineup.me` после deploy есть копия продовой `shine.sqlite` и `.bch`.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
- default deploy идёт только на `test2.shineup.me`;
|
|
||||||
- production `shineup.me` меняется только после отдельного подтверждения;
|
|
||||||
- `test.shineup.me` остаётся резервным тестовым сервером;
|
|
||||||
- тестовый deploy не гоняет удалённые тесты и не создаёт пустую БД.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
- pending
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Регистрация: FAQ и режим пароля из 12 слов
|
|
||||||
|
|
||||||
- краткое описание:
|
|
||||||
- на экране регистрации добавлен блок частых вопросов с переходом на отдельный экран справки;
|
|
||||||
- добавлен альтернативный режим ввода пароля через 12 полей-слов в кошелёчном формате, которые склеиваются в одну строку без изменения API;
|
|
||||||
- такой же режим добавлен и на экран входа по логину и паролю.
|
|
||||||
|
|
||||||
- что проверять:
|
|
||||||
- на стартовом экране открыть `Зарегистрироваться`;
|
|
||||||
- убедиться, что внизу экрана есть кнопки FAQ;
|
|
||||||
- открыть несколько вопросов и проверить возврат обратно на регистрацию;
|
|
||||||
- включить галочку `Представить пароль в виде 12 слов`;
|
|
||||||
- убедиться, что появляется сетка с нумерованными полями в 3 колонки;
|
|
||||||
- ввести часть слов, перейти дальше и проверить, что шаг подтверждения и генерация ключей работают;
|
|
||||||
- выключить галочку и проверить, что пароль остаётся собранным в одном поле;
|
|
||||||
- открыть экран входа по паролю и повторить те же проверки для режима `12 слов`;
|
|
||||||
- пройти регистрацию до шага оплаты без ошибок интерфейса.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
- FAQ открывается отдельным экраном и содержит понятные ответы;
|
|
||||||
- режим `12 слов` не ломает регистрацию и вход и даёт тот же поток, что и обычный пароль;
|
|
||||||
- пароль не отправляется в новом формате, а продолжает использоваться как одна строка.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
- pending
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Временная бесплатная загрузка аватара в 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
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Исправление chatId личных сообщений через lowercase
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
- В клиентском UI SHiNE для личных сообщений технический `chatId` теперь канонизируется через `trim().toLowerCase()` при приёме DM, открытии чата и восстановлении сообщений из IndexedDB.
|
|
||||||
- Цель: исключить рассинхрон, когда unread-индикатор есть, а входящие сообщения конкретного собеседника не видны из-за разного регистра логина.
|
|
||||||
|
|
||||||
- что именно проверять:
|
|
||||||
- Отправить личные сообщения между двумя пользователями, у одного из которых логин отображается с заглавными буквами.
|
|
||||||
- Убедиться, что входящие сообщения показываются внутри открытого чата, а не только в общем unread-индикаторе.
|
|
||||||
- Перезагрузить страницу и проверить, что история чата после гидрации из IndexedDB остаётся в одном диалоге.
|
|
||||||
- Проверить, что переход в чат из списка диалогов и из графа связей открывает тот же диалог без дублирования.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
- Все сообщения одного собеседника попадают в один и тот же DM-чат независимо от регистра логина.
|
|
||||||
- Общий unread, список диалогов и содержимое открытого чата совпадают между собой.
|
|
||||||
- После перезагрузки UI не появляется отдельный дубль диалога с тем же логином в другом регистре.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
- pending
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Недопроверенные фичи
|
|
||||||
|
|
||||||
Эта папка хранит список доработок, которые уже реализованы, но ещё не подтверждены ручной проверкой.
|
|
||||||
|
|
||||||
## Как использовать
|
|
||||||
|
|
||||||
1. При каждом коммите с новыми пользовательскими фичами (если нужна ручная проверка) добавить новый файл:
|
|
||||||
- формат: `YYYY-MM-DD_HHMM_<short-feature-name>.md`
|
|
||||||
- название `<short-feature-name>` и текст файла по возможности писать на русском языке
|
|
||||||
2. В файле указать:
|
|
||||||
- что сделано;
|
|
||||||
- как проверять;
|
|
||||||
- ожидаемый результат;
|
|
||||||
- текущий статус (`pending` / `in_progress` / `done`).
|
|
||||||
3. После подтверждения работоспособности — удалить файл фичи из этой папки.
|
|
||||||
|
|
||||||
## Важно
|
|
||||||
|
|
||||||
- `README.md` не удаляется.
|
|
||||||
- Количество недопроверенных фич = число файлов `*.md` в этой папке, кроме `README.md`.
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
# Личные сообщения (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`
|
|
||||||
|
|
||||||
## Правила UI
|
|
||||||
|
|
||||||
UI сейчас работает так:
|
|
||||||
|
|
||||||
- показывает только текст `encryptedBody`;
|
|
||||||
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
|
|
||||||
- не показывает удалённые сообщения;
|
|
||||||
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
|
|
||||||
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
|
|
||||||
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
|
|
||||||
- не показывает и не принимает вложения.
|
|
||||||
|
|
||||||
## Что обязательно помнить
|
|
||||||
|
|
||||||
- вложения в DM сейчас отключены на уровне протокола и UI;
|
|
||||||
- любые старые описания `/f/...`, `/upload` и файловых таблиц для DM больше не актуальны;
|
|
||||||
- если позже вложения вернутся, их формат и серверная логика могут быть другими.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# TODO: доработка персональных сообщений для агентов
|
|
||||||
|
|
||||||
Статус: отложено.
|
|
||||||
|
|
||||||
## Что хотели сделать
|
|
||||||
|
|
||||||
Добавить упрощённую маршрутизацию персональных сообщений через служебную инструкцию в начале текстового payload (внутри подписанного DM-блока), чтобы:
|
|
||||||
|
|
||||||
- отличать сообщения человеку от сообщений агенту;
|
|
||||||
- отличать сообщения от человека и от агента;
|
|
||||||
- скрывать в обычном UI сообщения, адресованные агенту (`target=agent`);
|
|
||||||
- поддержать сценарий «сообщения самому себе между своими клиентами/устройствами», где один клиент/агент пишет другому в рамках одного логина.
|
|
||||||
|
|
||||||
## Базовая идея формата (черновик)
|
|
||||||
|
|
||||||
Пример префикса:
|
|
||||||
|
|
||||||
```text
|
|
||||||
@shine:pm:v1 {"target":"agent","agentId":"assistant","author":"human"}
|
|
||||||
Текст сообщения...
|
|
||||||
```
|
|
||||||
|
|
||||||
Пример ответа агента:
|
|
||||||
|
|
||||||
```text
|
|
||||||
@shine:pm:v1 {"target":"user","author":"agent","agentId":"assistant","agentLabel":"My Bot"}
|
|
||||||
Ответ агента...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Почему отложено
|
|
||||||
|
|
||||||
- нужно отдельно согласовать финальный формат инструкции;
|
|
||||||
- нужно определить строгие правила UI-фильтрации и fallback;
|
|
||||||
- нужно определить, нужен ли позднее отдельный серверный роутинг для agent-сессий.
|
|
||||||
|
|
||||||
## Что сделать при возвращении к задаче
|
|
||||||
|
|
||||||
1. Зафиксировать окончательный формат префикса и JSON-полей.
|
|
||||||
2. Описать правила парсинга/валидации (включая битые/неполные префиксы).
|
|
||||||
3. Добавить UI-логику показа/скрытия agent-сообщений.
|
|
||||||
4. Добавить маркировку «ответ агента» в диалоге.
|
|
||||||
5. Продумать режим self-chat (между своими клиентами/агентом) в рамках одного логина.
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Черновик будущих вложений в 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`.
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Деплой 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`
|
|
||||||
- Домен: `test2.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 на `test2.shineup.me`.
|
|
||||||
|
|
||||||
## Main test deploy (`test2.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 для `test2.shineup.me`.
|
|
||||||
- `deployUI` / `deployUITest2` публикуют UI в `/home/player/SHiNE/shine-ui` на `193.8.215.70`.
|
|
||||||
|
|
||||||
## Reserve test deploy (`test.shineup.me`)
|
|
||||||
|
|
||||||
- `test.shineup.me` пока не использовать для обычного deploy.
|
|
||||||
- Задачи `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/` обновлять в том же коммите.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Сервер `193.8.215.70` — основной test (`test2.shineup.me`)
|
|
||||||
|
|
||||||
- Пользователь: `player`
|
|
||||||
- Домен: `test2.shineup.me`
|
|
||||||
- Каталог 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`
|
|
||||||
- Сайты:
|
|
||||||
- `test2.shineup.me`
|
|
||||||
- `agent.shiningpeople.ru`
|
|
||||||
- Для `test2.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`
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Сервер `93.170.12.154` — 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`
|
|
||||||
- Эти задачи пока не использовать без отдельной причины.
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Сервер `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` сюда больше не относятся.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
# Деплой и инициализация Solana-регистрации (две обязательные программы)
|
|
||||||
|
|
||||||
## Коротко
|
|
||||||
|
|
||||||
Для рабочей регистрации пользователя нужны **обе** программы:
|
|
||||||
|
|
||||||
1. `shine_users` — хранение и обновление `user_pda`, economy-конфиг, логика регистрации.
|
|
||||||
2. `shine_login_guard` — проверка/классификация логина (CPI из `shine_users`).
|
|
||||||
|
|
||||||
Если задеплоена только одна из них — регистрация неработоспособна.
|
|
||||||
|
|
||||||
## Актуальные адреса (devnet)
|
|
||||||
|
|
||||||
- `shine_users` (регистрация пользователей):
|
|
||||||
`FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
|
||||||
- `shine_login_guard`:
|
|
||||||
`3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo`
|
|
||||||
- `shine_payments`:
|
|
||||||
`c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`
|
|
||||||
|
|
||||||
## Подтверждение деплоя
|
|
||||||
|
|
||||||
- Сеть: `https://api.devnet.solana.com`
|
|
||||||
- `shine_users`:
|
|
||||||
- `Program ID`: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
|
||||||
- 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`
|
|
||||||
|
|
||||||
### Сервер
|
|
||||||
|
|
||||||
- Серверные константы Solana:
|
|
||||||
- `shine-server-config/src/main/java/utils/config/SolanaProgramsConfig.java`
|
|
||||||
|
|
||||||
## Как запустить инициализацию 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: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
|
||||||
|
|
||||||
## Кто оплачивает create/update user_pda
|
|
||||||
|
|
||||||
- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `deviceKey`.
|
|
||||||
- В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера.
|
|
||||||
- `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию.
|
|
||||||
- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `deviceKey`.
|
|
||||||
|
|
||||||
## Важно
|
|
||||||
|
|
||||||
- `init_users_economy_config` выполняется один раз на программу.
|
|
||||||
Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение).
|
|
||||||
- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `deviceKey`, а содержимое записи подписывает `rootKey`.
|
|
||||||
- `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина.
|
|
||||||
Несовпадение адреса приведёт к ошибке регистрации.
|
|
||||||
@@ -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 и может пережить перезагрузку устройства.
|
||||||
+3
-3
@@ -11,7 +11,7 @@
|
|||||||
* legacy(empty password):
|
* legacy(empty password):
|
||||||
* secret = SHA256(base64(SHA256(password)) + "master.secret")
|
* secret = SHA256(base64(SHA256(password)) + "master.secret")
|
||||||
* keyPair_i = Ed25519(SHA256(base64(secret) + "|" + suffix_i))
|
* keyPair_i = Ed25519(SHA256(base64(secret) + "|" + suffix_i))
|
||||||
* suffixes = ["root.key", "bch.key", "dev.key"]
|
* suffixes = ["root.key", "blockchain.key", "client.key"]
|
||||||
*
|
*
|
||||||
* Плата: Waveshare ESP32-S3-Touch-AMOLED-2.16
|
* Плата: Waveshare ESP32-S3-Touch-AMOLED-2.16
|
||||||
* SD : SDMMC 1-bit CLK=GPIO2, CMD=GPIO1, D0=GPIO3
|
* SD : SDMMC 1-bit CLK=GPIO2, CMD=GPIO1, D0=GPIO3
|
||||||
@@ -116,8 +116,8 @@ static bool gKbNums = false;
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
struct KeyPair { uint8_t pub[32]; uint8_t priv[32]; };
|
struct KeyPair { uint8_t pub[32]; uint8_t priv[32]; };
|
||||||
static KeyPair gKeys[3];
|
static KeyPair gKeys[3];
|
||||||
static const char * KEY_SUFFIXES[3] = {"root.key", "bch.key", "dev.key"};
|
static const char * KEY_SUFFIXES[3] = {"root.key", "blockchain.key", "client.key"};
|
||||||
static const char * KEY_LABELS[3] = {"root.key", "bch.key", "dev.key"};
|
static const char * KEY_LABELS[3] = {"root.key", "blockchain.key", "client.key"};
|
||||||
static uint32_t gElapsedSec = 0;
|
static uint32_t gElapsedSec = 0;
|
||||||
|
|
||||||
// Base58 представления (43-44 символа для 32 байт + \0)
|
// Base58 представления (43-44 символа для 32 байт + \0)
|
||||||
|
|||||||
+1717
-190
File diff suppressed because it is too large
Load Diff
+15
-2
@@ -1,7 +1,6 @@
|
|||||||
#include "shine_secret_generation.h"
|
#include "shine_secret_generation.h"
|
||||||
|
|
||||||
#include <SD_MMC.h>
|
#include <SD_MMC.h>
|
||||||
#include <Preferences.h>
|
|
||||||
#include <mbedtls/sha256.h>
|
#include <mbedtls/sha256.h>
|
||||||
#include <mbedtls/base64.h>
|
#include <mbedtls/base64.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -80,6 +79,19 @@ static void setMessage(const char *message) {
|
|||||||
snprintf(gMessage, sizeof(gMessage), "%s", message ? 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) {
|
static void b2_compress(B2State *S, const uint8_t *blk) {
|
||||||
uint64_t m[16], v[16];
|
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 < 16; i++) m[i] = ((const uint64_t *)blk)[i];
|
||||||
@@ -449,7 +461,6 @@ bool shineSecretInitSd(String &error) {
|
|||||||
|
|
||||||
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error) {
|
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error) {
|
||||||
error = "";
|
error = "";
|
||||||
if (!shineSecretInitSd(error)) return false;
|
|
||||||
if (!normalizedLogin || !*normalizedLogin) {
|
if (!normalizedLogin || !*normalizedLogin) {
|
||||||
error = "login not set";
|
error = "login not set";
|
||||||
return false;
|
return false;
|
||||||
@@ -463,6 +474,8 @@ bool shineSecretStart(const char *normalizedLogin, const char *password, String
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!shineSecretInitSd(error)) return false;
|
||||||
|
|
||||||
if (gSdFile) gSdFile.close();
|
if (gSdFile) gSdFile.close();
|
||||||
SD_MMC.remove(SD_MEM_FILE);
|
SD_MMC.remove(SD_MEM_FILE);
|
||||||
gSdFile = SD_MMC.open(SD_MEM_FILE, "w+");
|
gSdFile = SD_MMC.open(SD_MEM_FILE, "w+");
|
||||||
|
|||||||
+36
-33
@@ -224,12 +224,14 @@ static int16_t gTouchLastY = 0;
|
|||||||
struct DerivedKeyState {
|
struct DerivedKeyState {
|
||||||
bool ready;
|
bool ready;
|
||||||
uint8_t masterSecret[32];
|
uint8_t masterSecret[32];
|
||||||
|
uint8_t recoveryPub[32];
|
||||||
|
uint8_t recoverySk[64];
|
||||||
uint8_t rootPub[32];
|
uint8_t rootPub[32];
|
||||||
uint8_t rootSk[64];
|
uint8_t rootSk[64];
|
||||||
uint8_t blockchainPub[32];
|
uint8_t blockchainPub[32];
|
||||||
uint8_t blockchainSk[64];
|
uint8_t blockchainSk[64];
|
||||||
uint8_t devicePub[32];
|
uint8_t clientPub[32];
|
||||||
uint8_t deviceSk[64];
|
uint8_t clientSk[64];
|
||||||
};
|
};
|
||||||
|
|
||||||
static DerivedKeyState gDerivedKeys = {};
|
static DerivedKeyState gDerivedKeys = {};
|
||||||
@@ -237,16 +239,16 @@ static DerivedKeyState gDerivedKeys = {};
|
|||||||
static const char *kSystemProgramId = "11111111111111111111111111111111";
|
static const char *kSystemProgramId = "11111111111111111111111111111111";
|
||||||
static const char *kEd25519ProgramId = "Ed25519SigVerify111111111111111111111111111";
|
static const char *kEd25519ProgramId = "Ed25519SigVerify111111111111111111111111111";
|
||||||
static const char *kSysvarInstructionsId = "Sysvar1nstructions1111111111111111111111111";
|
static const char *kSysvarInstructionsId = "Sysvar1nstructions1111111111111111111111111";
|
||||||
static const char *kShineUsersProgramId = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm";
|
static const char *kShineUsersProgramId = "SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6";
|
||||||
static const char *kShineLoginGuardProgramId = "3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo";
|
static const char *kShineLoginGuardProgramId = "SHiGxGsXGioQYCYhchQ5R7KWoxN5UjFAFsucPf6sfnh";
|
||||||
static const char *kShinePaymentsProgramId = "c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW";
|
static const char *kShinePaymentsProgramId = "SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v";
|
||||||
static const char *kUsersSeedPrefix = "user_login=";
|
static const char *kUsersSeedPrefix = "user_login=";
|
||||||
static const char *kUsersEconomyConfigSeed = "shine_users_economy_config";
|
static const char *kUsersEconomyConfigSeed = "shine_users_economy_config";
|
||||||
static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault";
|
static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault";
|
||||||
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
|
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
|
||||||
static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
|
static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
|
||||||
static const uint8_t kBlockTypeRootKey = 1;
|
static const uint8_t kBlockTypeRootKey = 1;
|
||||||
static const uint8_t kBlockTypeDeviceKey = 2;
|
static const uint8_t kBlockTypeClientKey = 2;
|
||||||
static const uint8_t kBlockTypeBlockchainRegistry = 3;
|
static const uint8_t kBlockTypeBlockchainRegistry = 3;
|
||||||
static const uint8_t kBlockTypeServerProfile = 30;
|
static const uint8_t kBlockTypeServerProfile = 30;
|
||||||
static const uint8_t kBlockTypeAccessServers = 40;
|
static const uint8_t kBlockTypeAccessServers = 40;
|
||||||
@@ -782,19 +784,20 @@ static void pushFixed(std::vector<uint8_t> &out, const uint8_t *data, size_t len
|
|||||||
static bool deriveKeysFromMasterSecret(const uint8_t masterSecret[32]) {
|
static bool deriveKeysFromMasterSecret(const uint8_t masterSecret[32]) {
|
||||||
memset(&gDerivedKeys, 0, sizeof(gDerivedKeys));
|
memset(&gDerivedKeys, 0, sizeof(gDerivedKeys));
|
||||||
memcpy(gDerivedKeys.masterSecret, masterSecret, 32);
|
memcpy(gDerivedKeys.masterSecret, masterSecret, 32);
|
||||||
String secretB64 = base64Encode(masterSecret, 32);
|
const char *prefix = "SHiNE-key";
|
||||||
if (secretB64.length() == 0) {
|
const char *suffixes[4] = {"recovery.key", "root.key", "blockchain.key", "client.key"};
|
||||||
return false;
|
uint8_t *pubs[4] = {gDerivedKeys.recoveryPub, gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.clientPub};
|
||||||
}
|
uint8_t *sks[4] = {gDerivedKeys.recoverySk, gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.clientSk};
|
||||||
const char *suffixes[3] = {"root.key", "bch.key", "dev.key"};
|
for (int i = 0; i < 4; i++) {
|
||||||
uint8_t *pubs[3] = {gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.devicePub};
|
std::vector<uint8_t> material;
|
||||||
uint8_t *sks[3] = {gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.deviceSk};
|
material.reserve(strlen(prefix) + 1 + 32 + 1 + strlen(suffixes[i]));
|
||||||
for (int i = 0; i < 3; i++) {
|
material.insert(material.end(), prefix, prefix + strlen(prefix));
|
||||||
String material = secretB64 + "|" + suffixes[i];
|
material.push_back(0);
|
||||||
|
material.insert(material.end(), masterSecret, masterSecret + 32);
|
||||||
|
material.push_back(0);
|
||||||
|
material.insert(material.end(), suffixes[i], suffixes[i] + strlen(suffixes[i]));
|
||||||
uint8_t seed[32];
|
uint8_t seed[32];
|
||||||
if (!sha256String(material, seed)) {
|
sha256Raw(material.data(), material.size(), seed);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (crypto_sign_seed_keypair(pubs[i], sks[i], seed) != 0) {
|
if (crypto_sign_seed_keypair(pubs[i], sks[i], seed) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -822,7 +825,7 @@ static bool restoreDerivedKeysFromSecret() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
gData.secretReady = true;
|
gData.secretReady = true;
|
||||||
gData.walletAddress = bytesToBase58(gDerivedKeys.devicePub, 32);
|
gData.walletAddress = bytesToBase58(gDerivedKeys.clientPub, 32);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,7 +838,7 @@ static bool deriveFreshSecretAndWallet() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
gData.secret = bytesToBase58(secret, sizeof(secret));
|
gData.secret = bytesToBase58(secret, sizeof(secret));
|
||||||
gData.walletAddress = bytesToBase58(gDerivedKeys.devicePub, 32);
|
gData.walletAddress = bytesToBase58(gDerivedKeys.clientPub, 32);
|
||||||
gData.secretReady = true;
|
gData.secretReady = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -889,7 +892,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
|
|||||||
const String &blockchainName,
|
const String &blockchainName,
|
||||||
const String &serverAddress,
|
const String &serverAddress,
|
||||||
const uint8_t rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
const uint8_t lastBlockSignature[64],
|
const uint8_t lastBlockSignature[64],
|
||||||
uint64_t createdAtMs) {
|
uint64_t createdAtMs) {
|
||||||
@@ -911,9 +914,9 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
|
|||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, rootPub, 32);
|
pushFixed(out, rootPub, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeDeviceKey);
|
out.push_back(kBlockTypeClientKey);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, devicePub, 32);
|
pushFixed(out, clientPub, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeBlockchainRegistry);
|
out.push_back(kBlockTypeBlockchainRegistry);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
@@ -960,7 +963,7 @@ static std::vector<uint8_t> buildCreateInstructionData(
|
|||||||
const String &blockchainName,
|
const String &blockchainName,
|
||||||
const String &serverAddress,
|
const String &serverAddress,
|
||||||
const uint8_t rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
const uint8_t lastBlockSignature[64],
|
const uint8_t lastBlockSignature[64],
|
||||||
const uint8_t rootSignature[64],
|
const uint8_t rootSignature[64],
|
||||||
@@ -972,7 +975,7 @@ static std::vector<uint8_t> buildCreateInstructionData(
|
|||||||
pushFixed(out, rootPub, 32);
|
pushFixed(out, rootPub, 32);
|
||||||
pushU64LE(out, createdAtMs);
|
pushU64LE(out, createdAtMs);
|
||||||
pushU64LE(out, 0);
|
pushU64LE(out, 0);
|
||||||
pushFixed(out, devicePub, 32);
|
pushFixed(out, clientPub, 32);
|
||||||
pushFixed(out, blockchainPub, 32);
|
pushFixed(out, blockchainPub, 32);
|
||||||
pushStrU8(out, blockchainName);
|
pushStrU8(out, blockchainName);
|
||||||
pushU64LE(out, 0);
|
pushU64LE(out, 0);
|
||||||
@@ -1079,7 +1082,7 @@ static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &me
|
|||||||
|
|
||||||
static std::vector<uint8_t> buildLegacyMessage(
|
static std::vector<uint8_t> buildLegacyMessage(
|
||||||
const uint8_t recentBlockhash[32],
|
const uint8_t recentBlockhash[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t userPda[32],
|
const uint8_t userPda[32],
|
||||||
const uint8_t inflowVault[32],
|
const uint8_t inflowVault[32],
|
||||||
const uint8_t economyConfig[32],
|
const uint8_t economyConfig[32],
|
||||||
@@ -1098,7 +1101,7 @@ static std::vector<uint8_t> buildLegacyMessage(
|
|||||||
base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram);
|
base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram);
|
||||||
|
|
||||||
std::vector<std::vector<uint8_t>> accountKeys;
|
std::vector<std::vector<uint8_t>> accountKeys;
|
||||||
accountKeys.emplace_back(devicePub, devicePub + 32);
|
accountKeys.emplace_back(clientPub, clientPub + 32);
|
||||||
accountKeys.emplace_back(userPda, userPda + 32);
|
accountKeys.emplace_back(userPda, userPda + 32);
|
||||||
accountKeys.emplace_back(inflowVault, inflowVault + 32);
|
accountKeys.emplace_back(inflowVault, inflowVault + 32);
|
||||||
accountKeys.emplace_back(systemProgram, systemProgram + 32);
|
accountKeys.emplace_back(systemProgram, systemProgram + 32);
|
||||||
@@ -1244,7 +1247,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
uint64_t createdAtMs = (uint64_t)millis() + 1704067200000ULL;
|
uint64_t createdAtMs = (uint64_t)millis() + 1704067200000ULL;
|
||||||
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
|
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
|
||||||
gData.login, blockchainName, gData.wsUrl,
|
gData.login, blockchainName, gData.wsUrl,
|
||||||
gDerivedKeys.rootPub, gDerivedKeys.devicePub, gDerivedKeys.blockchainPub,
|
gDerivedKeys.rootPub, gDerivedKeys.clientPub, gDerivedKeys.blockchainPub,
|
||||||
lastBlockSignature, createdAtMs);
|
lastBlockSignature, createdAtMs);
|
||||||
uint8_t unsignedHash[32];
|
uint8_t unsignedHash[32];
|
||||||
uint8_t rootSignature[64];
|
uint8_t rootSignature[64];
|
||||||
@@ -1256,7 +1259,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
|
|
||||||
std::vector<uint8_t> createData = buildCreateInstructionData(
|
std::vector<uint8_t> createData = buildCreateInstructionData(
|
||||||
gData.login, blockchainName, gData.wsUrl,
|
gData.login, blockchainName, gData.wsUrl,
|
||||||
gDerivedKeys.rootPub, gDerivedKeys.devicePub, gDerivedKeys.blockchainPub,
|
gDerivedKeys.rootPub, gDerivedKeys.clientPub, gDerivedKeys.blockchainPub,
|
||||||
lastBlockSignature, rootSignature, createdAtMs);
|
lastBlockSignature, rootSignature, createdAtMs);
|
||||||
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, gDerivedKeys.rootPub, unsignedHash);
|
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, gDerivedKeys.rootPub, unsignedHash);
|
||||||
std::vector<uint8_t> edBchData = buildEd25519InstructionData(lastBlockSignature, gDerivedKeys.blockchainPub, lastBlockHash);
|
std::vector<uint8_t> edBchData = buildEd25519InstructionData(lastBlockSignature, gDerivedKeys.blockchainPub, lastBlockHash);
|
||||||
@@ -1269,7 +1272,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
|
|
||||||
std::vector<uint8_t> message = buildLegacyMessage(
|
std::vector<uint8_t> message = buildLegacyMessage(
|
||||||
recentBlockhash,
|
recentBlockhash,
|
||||||
gDerivedKeys.devicePub,
|
gDerivedKeys.clientPub,
|
||||||
userPda,
|
userPda,
|
||||||
inflowVault,
|
inflowVault,
|
||||||
economyConfig,
|
economyConfig,
|
||||||
@@ -1277,7 +1280,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
edBchData,
|
edBchData,
|
||||||
createData);
|
createData);
|
||||||
uint8_t txSignature[64];
|
uint8_t txSignature[64];
|
||||||
if (!signMessageEd25519(message, gDerivedKeys.deviceSk, txSignature)) {
|
if (!signMessageEd25519(message, gDerivedKeys.clientSk, txSignature)) {
|
||||||
messageOut = "Не удалось подписать Solana-транзакцию";
|
messageOut = "Не удалось подписать Solana-транзакцию";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1311,7 +1314,7 @@ static String defaultApiUrl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String defaultRpcUrl() {
|
static String defaultRpcUrl() {
|
||||||
return "https://api.devnet.solana.com";
|
return "https://api.mainnet-beta.solana.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
static String defaultWsUrl() {
|
static String defaultWsUrl() {
|
||||||
@@ -2107,7 +2110,7 @@ static void drawConfirmScreen() {
|
|||||||
String text = "Выполнить действие?";
|
String text = "Выполнить действие?";
|
||||||
if (gConfirmTarget == CONFIRM_REGISTER) {
|
if (gConfirmTarget == CONFIRM_REGISTER) {
|
||||||
title = "Регистрация";
|
title = "Регистрация";
|
||||||
text = "Отправить create_user_pda в Solana через device key этого устройства?";
|
text = "Отправить create_user_pda в Solana через client key этого устройства?";
|
||||||
} else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
|
} else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
|
||||||
title = "Очистка";
|
title = "Очистка";
|
||||||
text = "Удалить секрет, кошелёк и статус регистрации?";
|
text = "Удалить секрет, кошелёк и статус регистрации?";
|
||||||
|
|||||||
+12
-10
@@ -70,11 +70,11 @@
|
|||||||
Фоновая логика:
|
Фоновая логика:
|
||||||
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
|
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
|
||||||
- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
|
- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
|
||||||
- сравниваются `root key`, `blockchain key`, `device key` и `homeserver` session-запись типа `100`;
|
- сравниваются `root key`, `blockchain key`, `client key` и `homeserver` session-запись типа `100`;
|
||||||
- для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE:
|
- для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE:
|
||||||
- авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`;
|
- авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`;
|
||||||
- session key = публичный `homeserver key`;
|
- session key = публичный `homeserver key`;
|
||||||
- подтверждение создания сессии подписывается `device key`;
|
- подтверждение создания сессии подписывается `client key`;
|
||||||
- heartbeat выполняется `Ping` раз в минуту.
|
- heartbeat выполняется `Ping` раз в минуту.
|
||||||
|
|
||||||
## SETTINGS_MENU
|
## SETTINGS_MENU
|
||||||
@@ -150,12 +150,12 @@
|
|||||||
- статусное сообщение;
|
- статусное сообщение;
|
||||||
- текущий `Solana RPC` адрес;
|
- текущий `Solana RPC` адрес;
|
||||||
- кнопку `SOLANA RPC`;
|
- кнопку `SOLANA RPC`;
|
||||||
- текущий `Shine server` адрес;
|
- текущий `SHiNE server login` или уже резолвленный адрес;
|
||||||
- кнопку `SHINE SERVER`.
|
- кнопку `SHiNE SERVER LOGIN`, если обычный `user PDA` ещё не зарегистрирован.
|
||||||
|
|
||||||
Значения по умолчанию:
|
Значения по умолчанию:
|
||||||
- Solana RPC: `https://api.devnet.solana.com`
|
- Solana RPC: `https://api.mainnet-beta.solana.com`
|
||||||
- Shine server: `https://shineup.me`
|
- SHiNE server login: `shineupme`
|
||||||
|
|
||||||
Нажатие на любую из двух кнопок открывает `TEXT_EDIT_SCREEN`.
|
Нажатие на любую из двух кнопок открывает `TEXT_EDIT_SCREEN`.
|
||||||
|
|
||||||
@@ -214,8 +214,8 @@
|
|||||||
- `Root key priv (base58)`;
|
- `Root key priv (base58)`;
|
||||||
- `Blockchain key (base58)`;
|
- `Blockchain key (base58)`;
|
||||||
- `Blockchain key priv (base58)`;
|
- `Blockchain key priv (base58)`;
|
||||||
- `Device key (base58)`;
|
- `Client key (base58)`;
|
||||||
- `Device key priv (base58)`;
|
- `Client key priv (base58)`;
|
||||||
- `Homeserver key (base58)`;
|
- `Homeserver key (base58)`;
|
||||||
- `Homeserver key priv (base58)`;
|
- `Homeserver key priv (base58)`;
|
||||||
- для каждого поля показывается формула derivation;
|
- для каждого поля показывается формула derivation;
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
Используется для:
|
Используется для:
|
||||||
- пароля Wi-Fi;
|
- пароля Wi-Fi;
|
||||||
- Solana RPC;
|
- Solana RPC;
|
||||||
- Shine server.
|
- SHiNE server login.
|
||||||
|
|
||||||
Показывает:
|
Показывает:
|
||||||
- заголовок;
|
- заголовок;
|
||||||
@@ -259,6 +259,7 @@
|
|||||||
- переключение страниц выполняется свайпом влево/вправо внутри `TEXT_EDIT_SCREEN`;
|
- переключение страниц выполняется свайпом влево/вправо внутри `TEXT_EDIT_SCREEN`;
|
||||||
- страница 1 содержит в основном буквы и базовые URL-символы;
|
- страница 1 содержит в основном буквы и базовые URL-символы;
|
||||||
- страница 2 содержит цифры и дополнительные URL/символьные кнопки;
|
- страница 2 содержит цифры и дополнительные URL/символьные кнопки;
|
||||||
|
- на символьных страницах должны быть доступны как минимум `!` и отдельная кнопка `SPACE`;
|
||||||
- переключение `ABC/123` и `SHIFT` не должно очищать уже введённый текст;
|
- переключение `ABC/123` и `SHIFT` не должно очищать уже введённый текст;
|
||||||
- переключение страниц клавиатуры свайпом тоже не должно очищать уже введённый текст, включая цифры, символы и уже набранные заглавные буквы;
|
- переключение страниц клавиатуры свайпом тоже не должно очищать уже введённый текст, включая цифры, символы и уже набранные заглавные буквы;
|
||||||
- есть специальные действия `DEL` и `CLR`.
|
- есть специальные действия `DEL` и `CLR`.
|
||||||
@@ -291,7 +292,7 @@
|
|||||||
|
|
||||||
Используется `Preferences` (NVS памяти ESP32):
|
Используется `Preferences` (NVS памяти ESP32):
|
||||||
- `solana_rpc`
|
- `solana_rpc`
|
||||||
- `shine_server`
|
- `shine_server_login`
|
||||||
|
|
||||||
## Хранение аккаунта
|
## Хранение аккаунта
|
||||||
|
|
||||||
@@ -308,6 +309,7 @@
|
|||||||
- половина букв находится на левой странице, половина на правой;
|
- половина букв находится на левой странице, половина на правой;
|
||||||
- на правой странице кнопки тоже стоят в ровных колонках, без сдвига рядов вправо;
|
- на правой странице кнопки тоже стоят в ровных колонках, без сдвига рядов вправо;
|
||||||
- отдельный режим `symbols` тоже разделён на 2 страницы;
|
- отдельный режим `symbols` тоже разделён на 2 страницы;
|
||||||
|
- в режиме `symbols` на правой странице есть отдельная кнопка `SPACE` и отдельная кнопка `!`;
|
||||||
- 3 основных ряда клавиш и нижний служебный ряд увеличены по высоте;
|
- 3 основных ряда клавиш и нижний служебный ряд увеличены по высоте;
|
||||||
- клавиши дополнительно увеличены по высоте по сравнению с предыдущим промежуточным вариантом;
|
- клавиши дополнительно увеличены по высоте по сравнению с предыдущим промежуточным вариантом;
|
||||||
- четвёртый ряд содержит:
|
- четвёртый ряд содержит:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
- локальный UI на тач-экране;
|
- локальный UI на тач-экране;
|
||||||
- хранение настроек и секретов во внутренней памяти `ESP32` через `NVS`;
|
- хранение настроек и секретов во внутренней памяти `ESP32` через `NVS`;
|
||||||
- русский текст в логике интерфейса; в текущей временной сборке отображение на экране идёт через ASCII-транслитерацию, потому что `U8g2`-шрифты на устройстве временно не рисуются;
|
- экранный UI устройства работает на английском языке, потому что кириллица на этом шрифтовом пути пока не поддерживается стабильно;
|
||||||
- экран пополнения с реальным `solana:` URI и рисованием QR-кода через `LVGL`;
|
- экран пополнения с реальным `solana:` URI и рисованием QR-кода через `LVGL`;
|
||||||
- реальное подключение к `Wi-Fi` по сохранённым `SSID/паролю`;
|
- реальное подключение к `Wi-Fi` по сохранённым `SSID/паролю`;
|
||||||
- реальная проверка доступности `API`, `RPC` и `WS`-адресов;
|
- реальная проверка доступности `API`, `RPC` и `WS`-адресов;
|
||||||
@@ -65,19 +65,20 @@
|
|||||||
- `user pda address`;
|
- `user pda address`;
|
||||||
- `registration signature`;
|
- `registration signature`;
|
||||||
- `balance`;
|
- `balance`;
|
||||||
- `server api url`;
|
- `server login` для первичной привязки;
|
||||||
- `server rpc url`;
|
- `resolved server api url` / `rpc url` / `ws url` после чтения PDA сервера;
|
||||||
- `server ws url`;
|
|
||||||
- флаги:
|
- флаги:
|
||||||
`wifiReady`, `serversReady`, `secretReady`, `registered`, `online`.
|
`wifiReady`, `serversReady`, `secretReady`, `registered`, `online`.
|
||||||
|
|
||||||
|
Для первой регистрации обычного `user PDA` устройство берёт `createdAtMs` из NTP прямо перед отправкой транзакции в Solana. При последующих обновлениях `user PDA` устройство так же берёт актуальный `updatedAtMs` из NTP перед отправкой update-транзакции. Дальше в `user PDA` сохраняется `accessServers`, где по умолчанию лежит `shineupme`.
|
||||||
|
|
||||||
## Правило серверной сессии SHiNE
|
## Правило серверной сессии SHiNE
|
||||||
|
|
||||||
При подключении к серверу `SHiNE` устройство должно авторизовываться как homeserver-сеанс:
|
При подключении к серверу `SHiNE` устройство должно авторизовываться как homeserver-сеанс:
|
||||||
|
|
||||||
- `sessionType = 100`
|
- `sessionType = 100`
|
||||||
- `clientPlatform = "ESP32"`
|
- `clientPlatform = "ESP32"`
|
||||||
- `clientInfo = "ESP32 homeserver"`
|
- `clientInfo = "ESP32 homeserver:<homeserverName>"`
|
||||||
|
|
||||||
Это относится и к `CreateAuthSession`, и к `SessionLogin`.
|
Это относится и к `CreateAuthSession`, и к `SessionLogin`.
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
Кнопка регистрации доступна только если одновременно выполнены условия:
|
Кнопка регистрации доступна только если одновременно выполнены условия:
|
||||||
|
|
||||||
1. настроен и подтверждён `Wi-Fi`;
|
1. настроен и подтверждён `Wi-Fi`;
|
||||||
2. заполнены и подтверждены серверные адреса;
|
2. задан и подтверждён `SHiNE server login`;
|
||||||
3. задан логин;
|
3. задан логин;
|
||||||
4. сгенерирован или введён секрет;
|
4. сгенерирован или введён секрет;
|
||||||
5. баланс кошелька не меньше `0.20 SOL`;
|
5. баланс кошелька не меньше `0.20 SOL`;
|
||||||
@@ -107,14 +108,15 @@
|
|||||||
7. `ACCOUNT`
|
7. `ACCOUNT`
|
||||||
8. `WALLET`
|
8. `WALLET`
|
||||||
9. `WALLET_QR`
|
9. `WALLET_QR`
|
||||||
10. `REQUESTS`
|
10. `WALLET_SIGN_REQUEST`
|
||||||
11. `REQUEST_DETAIL`
|
11. `REQUESTS`
|
||||||
12. `SETTINGS`
|
12. `REQUEST_DETAIL`
|
||||||
13. `PIN_EDIT`
|
13. `SETTINGS`
|
||||||
14. `TEXT_INPUT`
|
14. `PIN_EDIT`
|
||||||
15. `CONFIRM`
|
15. `TEXT_INPUT`
|
||||||
16. `REGISTER_ACCOUNT_CONFIRM`
|
16. `CONFIRM`
|
||||||
17. `REGISTER_ACCOUNT_RESULT`
|
17. `REGISTER_ACCOUNT_CONFIRM`
|
||||||
|
18. `REGISTER_ACCOUNT_RESULT`
|
||||||
|
|
||||||
## Общие правила интерфейса
|
## Общие правила интерфейса
|
||||||
|
|
||||||
@@ -125,9 +127,9 @@
|
|||||||
- затем, если устройство реально заряжается, маленькая иконка молнии;
|
- затем, если устройство реально заряжается, маленькая иконка молнии;
|
||||||
- затем контур батареи;
|
- затем контур батареи;
|
||||||
- затем индикатор `Wi-Fi`.
|
- затем индикатор `Wi-Fi`.
|
||||||
- Основной язык прототипа: русский.
|
- Основной язык прототипа: английский.
|
||||||
- Для вывода текста в текущей временной сборке используется стандартный шрифт `Arduino_GFX`.
|
- Для вывода текста в текущей временной сборке используется стандартный шрифт `Arduino_GFX`.
|
||||||
- Русские строки на экране временно показываются в ASCII-транслитерации.
|
- Русские строки в UI устройства пока не использовать.
|
||||||
- Кнопки крупные, с тач-ориентированным размером.
|
- Кнопки крупные, с тач-ориентированным размером.
|
||||||
- Опасные действия подтверждаются отдельным диалогом.
|
- Опасные действия подтверждаются отдельным диалогом.
|
||||||
- После изменения данных конфигурация сразу сохраняется в `NVS`.
|
- После изменения данных конфигурация сразу сохраняется в `NVS`.
|
||||||
@@ -172,20 +174,23 @@
|
|||||||
- блок `STATUS` поднят выше относительно предыдущей версии;
|
- блок `STATUS` поднят выше относительно предыдущей версии;
|
||||||
- если состояние хорошее, слово `connected` в строках `Wi-Fi` и `SHiNE` показывается зелёным.
|
- если состояние хорошее, слово `connected` в строках `Wi-Fi` и `SHiNE` показывается зелёным.
|
||||||
|
|
||||||
В зоне баланса:
|
В зоне кошелька:
|
||||||
|
|
||||||
- основная кнопка показа/обновления баланса занимает примерно 80% строки;
|
- вместо старых двух кнопок `баланс` и `QR` показывается одна широкая кнопка;
|
||||||
- текст на кнопке баланса выровнен левее центра;
|
- текст кнопки: `Wallet: <selected wallet name>`;
|
||||||
- справа от неё стоит отдельная кнопка `QR`;
|
- доступные имена:
|
||||||
- после старта устройства баланс пытается загрузиться автоматически, если уже есть секрет и `Wi-Fi`;
|
- `ClientKey`
|
||||||
- нажатие на кнопку `QR` открывает экран `WALLET_QR`.
|
- `RootKey`
|
||||||
|
- либо сохранённое имя `custom`-кошелька;
|
||||||
|
- после старта устройства баланс активного выбранного кошелька пытается загрузиться автоматически, если уже есть секрет и `Wi-Fi`;
|
||||||
|
- нажатие на эту кнопку открывает экран `WALLET`.
|
||||||
|
|
||||||
Нижние кнопки:
|
Нижние кнопки:
|
||||||
|
|
||||||
- `Статус`
|
- `Статус`
|
||||||
- `Подключение`
|
- `Подключение`
|
||||||
- `Аккаунт`
|
- `Аккаунт`
|
||||||
- `Кошелёк`
|
- `Wallet`
|
||||||
- `Запросы`
|
- `Запросы`
|
||||||
- `Настройки`
|
- `Настройки`
|
||||||
|
|
||||||
@@ -410,26 +415,43 @@
|
|||||||
|
|
||||||
Показывает:
|
Показывает:
|
||||||
|
|
||||||
- адрес кошелька устройства;
|
- кнопку `Wallet: <selected wallet name>`;
|
||||||
- баланс в `SOL`;
|
- строку текущего статуса/баланса активного кошелька;
|
||||||
- минимально рекомендуемую сумму для регистрации;
|
- сокращённый публичный ключ активного кошелька.
|
||||||
- статус `Хватает / Не хватает`.
|
|
||||||
|
|
||||||
Кнопки:
|
Кнопки:
|
||||||
|
|
||||||
- `QR и URI`
|
- `Wallet: <selected wallet name>`
|
||||||
- `+0.10 SOL`
|
- `SHOW BALANCE`
|
||||||
- `+0.25 SOL`
|
- `SHOW WALLET QR`
|
||||||
- `-0.10 SOL`
|
|
||||||
- `Проверить`
|
|
||||||
- `Назад`
|
|
||||||
|
|
||||||
Поведение:
|
Поведение:
|
||||||
|
|
||||||
- кнопки пополнения/уменьшения нужны для теста сценариев;
|
- верхняя кнопка открывает экран выбора кошелька `WALLET_SELECT`;
|
||||||
- `Проверить` читает реальный баланс из `Solana RPC`;
|
- `Показать баланс кошелька` читает реальный баланс именно активного выбранного кошелька из `Solana RPC`;
|
||||||
- адрес кошелька должен совпадать с `device key`, вычисленным из сохранённого `master secret`;
|
- `Показать QR-код кошелька` открывает экран `WALLET_QR`;
|
||||||
- отрицательный баланс не допускается.
|
- этот экран не меняет логику Solana-регистрации пользователя: on-chain регистрация и проверки `PDA` по-прежнему завязаны на `client key`.
|
||||||
|
|
||||||
|
## Экран WALLET_SELECT
|
||||||
|
|
||||||
|
Показывает:
|
||||||
|
|
||||||
|
- строку `Current: <selected wallet name>`;
|
||||||
|
- три кнопки выбора:
|
||||||
|
- `ClientKey`
|
||||||
|
- `RootKey`
|
||||||
|
- `Custom` или `Custom: <имя>`;
|
||||||
|
- у текущего выбора видна галочка.
|
||||||
|
|
||||||
|
Поведение:
|
||||||
|
|
||||||
|
- `ClientKey` активирует кошелёк, выведенный из suffix `client.key`;
|
||||||
|
- `RootKey` активирует кошелёк, выведенный из suffix `root.key`;
|
||||||
|
- `Custom` использует derivation:
|
||||||
|
`sha256(base64(secret32) + "|wallet." + customName)`;
|
||||||
|
- если имя `custom`-кошелька ещё не задано, нажатие `Custom` открывает экран текстового ввода;
|
||||||
|
- если `custom` уже выбран, повторное нажатие на него открывает экран редактирования имени;
|
||||||
|
- после выбора кошелька устройство возвращается на экран `WALLET`.
|
||||||
|
|
||||||
## Экран WALLET_QR
|
## Экран WALLET_QR
|
||||||
|
|
||||||
@@ -452,8 +474,29 @@
|
|||||||
Поведение:
|
Поведение:
|
||||||
|
|
||||||
- QR должен быть сканируемым, а не декоративным;
|
- QR должен быть сканируемым, а не декоративным;
|
||||||
- адрес кошелька берётся из `device key`, вычисленного из сохранённого `master secret`;
|
- адрес кошелька берётся из текущего активного выбранного кошелька;
|
||||||
- нажатие в любую точку экрана возвращает пользователя на `HOME`.
|
- нажатие в любую точку экрана возвращает пользователя на `WALLET`.
|
||||||
|
|
||||||
|
## Экран WALLET_SIGN_REQUEST
|
||||||
|
|
||||||
|
Экран показывается, когда браузерное wallet-расширение присылает на ESP32 запрос `sign_transaction`.
|
||||||
|
|
||||||
|
Показывает:
|
||||||
|
|
||||||
|
- заголовок `SIGN REQUEST`;
|
||||||
|
- вопрос о подписи транзакции текущим активным кошельком;
|
||||||
|
- сокращённый публичный ключ кошелька;
|
||||||
|
- комментарий, присланный полем `comment`;
|
||||||
|
- две кнопки:
|
||||||
|
- `REJECT`
|
||||||
|
- `APPROVE`
|
||||||
|
|
||||||
|
Поведение:
|
||||||
|
|
||||||
|
- если пользователь нажимает `APPROVE`, ESP32 подписывает транзакцию текущим активным кошельком и отправляет ответ в расширение;
|
||||||
|
- если пользователь нажимает `REJECT`, ESP32 отправляет отказ `rejected_by_user`;
|
||||||
|
- пока запрос активен, свайпом этот экран не закрывается;
|
||||||
|
- после ответа расширению устройство возвращается на предыдущий экран.
|
||||||
|
|
||||||
## Экран REQUESTS
|
## Экран REQUESTS
|
||||||
|
|
||||||
@@ -491,7 +534,7 @@
|
|||||||
- строку `Session: <platform/name>`;
|
- строку `Session: <platform/name>`;
|
||||||
- строку `Kind: Client session` или `Kind: Wallet session`;
|
- строку `Kind: Client session` или `Kind: Wallet session`;
|
||||||
- пояснение:
|
- пояснение:
|
||||||
- для client session: `Only device key will be transferred. No additional keys will be sent.`
|
- для client session: `Only client key will be transferred. No additional keys will be sent.`
|
||||||
- для wallet session: `No keys will be transferred.`
|
- для wallet session: `No keys will be transferred.`
|
||||||
|
|
||||||
Кнопки:
|
Кнопки:
|
||||||
@@ -502,7 +545,7 @@
|
|||||||
Поведение:
|
Поведение:
|
||||||
|
|
||||||
- `YES` подтверждает заявку:
|
- `YES` подтверждает заявку:
|
||||||
- для client session устройство передаёт только `device key`;
|
- для client session устройство передаёт только `client key`;
|
||||||
- для wallet session устройство выпускает отдельную `wallet-session` без передачи ключей;
|
- для wallet session устройство выпускает отдельную `wallet-session` без передачи ключей;
|
||||||
- `NO` отклоняет заявку;
|
- `NO` отклоняет заявку;
|
||||||
- после любого решения устройство возвращается в список `REQUESTS` и обновляет его;
|
- после любого решения устройство возвращается в список `REQUESTS` и обновляет его;
|
||||||
@@ -586,7 +629,7 @@
|
|||||||
2. открыть `Подключение -> Wi-Fi`;
|
2. открыть `Подключение -> Wi-Fi`;
|
||||||
3. ввести `SSID` и пароль, нажать `Проверить`;
|
3. ввести `SSID` и пароль, нажать `Проверить`;
|
||||||
4. открыть `Подключение -> Серверы`;
|
4. открыть `Подключение -> Серверы`;
|
||||||
5. проверить или задать серверные адреса;
|
5. проверить или задать `SHiNE server login` (по умолчанию `shineupme`);
|
||||||
6. открыть `Аккаунт`;
|
6. открыть `Аккаунт`;
|
||||||
7. ввести логин;
|
7. ввести логин;
|
||||||
8. задать имя homeserver;
|
8. задать имя homeserver;
|
||||||
@@ -595,14 +638,15 @@
|
|||||||
11. при необходимости пополнить баланс;
|
11. при необходимости пополнить баланс;
|
||||||
12. вернуться на `HOME`;
|
12. вернуться на `HOME`;
|
||||||
13. нажать `REGISTER ACCOUNT`;
|
13. нажать `REGISTER ACCOUNT`;
|
||||||
14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1` и при необходимости сообщение о неподключённом `Wi-Fi`;
|
14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1`, серверный login и при необходимости сообщение о неподключённом `Wi-Fi`;
|
||||||
15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`;
|
15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`;
|
||||||
16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку;
|
16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку;
|
||||||
17. после успешной регистрации увидеть статус `Homeserver активен`.
|
17. после успешной регистрации увидеть статус `Homeserver активен`.
|
||||||
|
|
||||||
Примечание:
|
Примечание:
|
||||||
|
|
||||||
- устройство реально отправляет `create_user_pda` в `shine_users`, а после подтверждения сохраняет `PDA` и `tx signature`.
|
- устройство реально отправляет `create_user_pda` в `shine_users`, а после подтверждения сохраняет `PDA` и `tx signature`;
|
||||||
|
- при первой регистрации для обычного `user PDA` не заполняется `serverAddress`, а `accessServers` получает `shineupme` или другой выбранный `SHiNE server login`.
|
||||||
|
|
||||||
## Сценарий входящего запроса
|
## Сценарий входящего запроса
|
||||||
|
|
||||||
@@ -616,8 +660,8 @@
|
|||||||
|
|
||||||
В текущей диагностической версии:
|
В текущей диагностической версии:
|
||||||
|
|
||||||
- строковые литералы в коде остаются русскими и в `UTF-8`;
|
- строковые литералы экранного UI должны оставаться английскими ASCII-совместимыми;
|
||||||
- перед выводом на экран они временно транслитерируются в ASCII;
|
- возвращение кириллицы допустимо только после отдельной доработки шрифтов и реальной проверки на устройстве;
|
||||||
- рендер выполняется стандартным шрифтом `Arduino_GFX`;
|
- рендер выполняется стандартным шрифтом `Arduino_GFX`;
|
||||||
- это обходной режим, пока `U8g2`-шрифты на устройстве не начнут рисоваться стабильно.
|
- это обходной режим, пока `U8g2`-шрифты на устройстве не начнут рисоваться стабильно.
|
||||||
|
|
||||||
@@ -626,7 +670,7 @@
|
|||||||
Минимально нужно проверить:
|
Минимально нужно проверить:
|
||||||
|
|
||||||
1. устройство загружается и сразу открывает `HOME`; экран блокировки временно отключён;
|
1. устройство загружается и сразу открывает `HOME`; экран блокировки временно отключён;
|
||||||
2. текст отображается читаемо хотя бы в ASCII-транслитерации;
|
2. весь экранный текст отображается читаемо на английском без замены символов;
|
||||||
3. ввод по экранной клавиатуре работает;
|
3. ввод по экранной клавиатуре работает;
|
||||||
4. после перезагрузки сохранённые поля остаются в памяти;
|
4. после перезагрузки сохранённые поля остаются в памяти;
|
||||||
5. секрет и адрес кошелька сохраняются на устройстве;
|
5. секрет и адрес кошелька сохраняются на устройстве;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Задание для Айдара: навести порядок в инструкциях агентов SHiNE
|
|
||||||
|
|
||||||
## Кратко
|
|
||||||
Нужно согласовать и оформить единый порядок инструкций для Codex/Telegram-агентов в проекте SHiNE, чтобы агенты стабильно понимали структуру проекта, границы ответственности и правила работы с сервером, UI, Solana-модулем, Telegram-ботом и игроками.
|
|
||||||
|
|
||||||
## Зачем это нужно
|
|
||||||
Сейчас проект состоит из нескольких связанных, но разных частей:
|
|
||||||
|
|
||||||
- основной сервер `SHiNE-server/`;
|
|
||||||
- UI `shine-UI/`;
|
|
||||||
- Solana/Anchor-модуль `shine-solana/shine/`;
|
|
||||||
- Telegram-агент-кодер `SHiNE-agent-bot-coder/`;
|
|
||||||
- TURN-сервер;
|
|
||||||
- документация `Dev_Docs/`;
|
|
||||||
- отдельные рабочие папки игроков `Players/`.
|
|
||||||
|
|
||||||
Без явных инструкций агент может путать эти зоны: например, смешать деплой Solana с деплоем сервера, изменить код от имени игрока, не обновить документацию API/DM/блокчейна или неправильно трактовать файл инструкций.
|
|
||||||
|
|
||||||
## Что предлагается сделать
|
|
||||||
1. Утвердить корневой `AGENTS.md` как главный набор правил проекта.
|
|
||||||
2. Проверить и при необходимости уточнить локальный `AGENTS.md` внутри `shine-solana/shine/`.
|
|
||||||
3. Оставить отдельные служебные инструкции Telegram-агента в `SHiNE-agent-bot-coder/AGENT.md`.
|
|
||||||
4. Оставить автоматически читаемые инструкции Telegram-агента в `SHiNE-agent-bot-coder/AGENTS.md`.
|
|
||||||
5. Явно закрепить режим игроков:
|
|
||||||
- игроки могут задавать вопросы, просить анализ, идеи и ТЗ;
|
|
||||||
- игроки не меняют код проекта напрямую;
|
|
||||||
- материалы игроков сохраняются только в `Players/<username>/`.
|
|
||||||
6. Зафиксировать правило: если пользователь говорит «агент MD» или похожую формулировку, считать, что речь про автоматически читаемый `AGENTS.md`.
|
|
||||||
7. Добавить простой процесс согласования изменений инструкций:
|
|
||||||
- Дима или другой участник готовит предложение;
|
|
||||||
- Айдар получает уведомление/заявку;
|
|
||||||
- Айдар отвечает: одобрить, отклонить или попросить доработать;
|
|
||||||
- только после одобрения агент вносит изменения в проектные инструкции.
|
|
||||||
|
|
||||||
## Предлагаемая логика уведомления Айдару
|
|
||||||
Минимальный вариант без сложной разработки:
|
|
||||||
|
|
||||||
1. Агент готовит текст заявки.
|
|
||||||
2. Текст отправляется Айдару в Telegram или в общий рабочий чат.
|
|
||||||
3. В заявке явно указаны варианты ответа:
|
|
||||||
- `одобрить`;
|
|
||||||
- `отклонить`;
|
|
||||||
- `доработать: ...`.
|
|
||||||
4. После ответа Айдара агент либо выполняет согласованные правки, либо фиксирует, что задача отклонена/нужна доработка.
|
|
||||||
|
|
||||||
Более удобный вариант на будущее:
|
|
||||||
|
|
||||||
- добавить в Telegram-бота команду или сценарий согласования задач, например:
|
|
||||||
- `/approve <id>`;
|
|
||||||
- `/reject <id> причина`;
|
|
||||||
- `/revise <id> комментарий`.
|
|
||||||
|
|
||||||
Но для начала достаточно простого текстового согласования через Telegram.
|
|
||||||
|
|
||||||
## Что нужно от Айдара
|
|
||||||
Подтвердить, что такой порядок подходит:
|
|
||||||
|
|
||||||
1. Корневой `AGENTS.md` остается главным правилом проекта.
|
|
||||||
2. Для Solana, Telegram-агента и игроков сохраняются отдельные локальные правила.
|
|
||||||
3. Игроки не меняют код напрямую, а готовят материалы и предложения.
|
|
||||||
4. Изменения инструкций выполняются только после явного одобрения Айдара.
|
|
||||||
5. Уведомления Айдару на первом этапе можно делать простым текстом в Telegram, без отдельной сложной системы заявок.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
После одобрения:
|
|
||||||
|
|
||||||
- агенты будут стабильнее понимать границы проекта;
|
|
||||||
- снизится риск случайных изменений не в той части системы;
|
|
||||||
- появится понятный порядок согласования задач от игроков;
|
|
||||||
- Айдар будет явно контролировать изменения в инструкциях и правилах работы агентов.
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
TELEGRAM_BOT_TOKEN=replace_me
|
|
||||||
OPENAI_API_KEY=replace_me
|
|
||||||
ALLOWED_TELEGRAM_USERNAME=AidarKC
|
|
||||||
ALLOWED_TELEGRAM_PLAYERS=malvviiina:Милана,zodiaktechnika32:Сергей,oidasyda:Иван,blackbyrd1:Ворон,dimasol1:Дима
|
|
||||||
ALLOWED_TELEGRAM_CHANNEL_USERNAME=shine_writing
|
|
||||||
BOT_USERNAME=aidar_su_bot
|
|
||||||
OPENAI_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
|
|
||||||
TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS=300
|
|
||||||
OPENAI_TRANSCRIBE_TIMEOUT_SECONDS=900
|
|
||||||
OPENAI_TTS_MODEL=gpt-4o-mini-tts
|
|
||||||
OPENAI_TTS_VOICE=alloy
|
|
||||||
OPENAI_TTS_RESPONSE_FORMAT=opus
|
|
||||||
OPENAI_TTS_TIMEOUT_SECONDS=180
|
|
||||||
OPENAI_TTS_CHUNK_CHARS=3500
|
|
||||||
CODEX_BIN=/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl
|
|
||||||
CODEX_WORKDIR=/home/ai/work/SHiNE/SHiNE-server-sha256
|
|
||||||
CODEX_TIMEOUT_SECONDS=900
|
|
||||||
MAX_RETRIES=3
|
|
||||||
DATA_DIR=./data
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.env
|
|
||||||
data/
|
|
||||||
logs/
|
|
||||||
run/
|
|
||||||
__pycache__/
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# AGENT.md для SHiNE-agent-bot-coder
|
|
||||||
|
|
||||||
Ты запущен как обработчик входящего Telegram-сообщения от пользователя.
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
- `SHiNE-agent-bot-coder` — локальный Telegram-бот-сервис агента-кодера для работы с этим проектом.
|
|
||||||
- Сервис принимает входящие сообщения от пользователя Telegram, сохраняет историю, ставит задачи в очередь и последовательно запускает Codex CLI в рабочем проекте.
|
|
||||||
- Текстовые сообщения обрабатываются напрямую, voice/audio сначала распознаются через OpenAI transcription, затем передаются как текстовая задача.
|
|
||||||
- История диалога хранится в JSONL-файле, путь передаётся в промпте.
|
|
||||||
- Сообщение может быть текстом или результатом распознавания голосового.
|
|
||||||
- Ответ пойдёт пользователю в Telegram как обычное текстовое сообщение.
|
|
||||||
- Единственная рабочая реализация сервиса — Python-скрипт `py_bot_service.py`; старая Java-реализация удалена как нерабочая и не должна восстанавливаться без отдельного решения Айдара.
|
|
||||||
- В репозитории также есть отдельный Solana/Anchor-модуль `shine-solana/shine/`; он логически связан с SHiNE, но не должен автоматически подключаться к основному серверному deploy без отдельной команды.
|
|
||||||
- Перед изменениями внутри `shine-solana/shine/` читать локальные инструкции `shine-solana/shine/AGENTS.md`; в git не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
|
|
||||||
|
|
||||||
## Авторитет команд и история
|
|
||||||
- Основной пользователь и источник команд — Айдар: `@AidarKC` / `@aidarkc`.
|
|
||||||
- Дополнительно разрешены игроки из whitelist (`ALLOWED_TELEGRAM_PLAYERS`), каждый со своей отдельной историей и рабочей папкой `Players/<username>/`.
|
|
||||||
- Игроки работают в режиме вопросов/анализа/подготовки материалов: в промпте явно задано правило не менять код проекта и писать материалы только в своей папке.
|
|
||||||
- Для неизвестных пользователей в личном чате сервис отвечает вежливым отказом.
|
|
||||||
- В Telegram-канале/группе `@shine_writing` сервис выполняет сообщения только от Айдара, а ответы отправляет в тот же чат.
|
|
||||||
- Если Telegram сообщает о миграции обычной группы в supergroup, сервис должен запомнить новый `chat_id` и отправлять ответы уже туда.
|
|
||||||
- На события подключения/отключения пользователей (join/leave) сервис не отвечает и ничего не отправляет.
|
|
||||||
|
|
||||||
## Очередь и состояние
|
|
||||||
- Входящие задачи записываются в файловую очередь и обрабатываются строго по одной, чтобы не смешивать изменения в проекте.
|
|
||||||
- Сервис ведёт состояние активной задачи и текущего файла истории, а после рестарта продолжает незавершённую обработку с учётом сохранённого состояния.
|
|
||||||
- Истории диалогов хранятся в JSONL по каждому разрешённому username отдельно: `data/history/<username>/`.
|
|
||||||
- Архив истории после `/new`: `data/history/<username>/archive/`.
|
|
||||||
- После `/new` для этого же пользователя должен сбрасываться и контекст продолжения Codex-сессии; следующий запрос запускается как новая сессия, не через resume.
|
|
||||||
- Для просмотра истории игрока открывать файлы в его папке истории по username.
|
|
||||||
- Дедупликация входящих Telegram update нужна, чтобы одно сообщение не попало в обработку повторно.
|
|
||||||
- Если Codex молчит во время активной задачи 2 минуты подряд, сервис отправляет аварийный статус с общим временем работы задачи; при дальнейшем молчании повторяет статус каждые 2 минуты.
|
|
||||||
- После успешной обработки задачи из личного чата Айдара сервис должен отправить публичный итоговый отчёт в группу `@shine_writing`: первым сообщением исходный запрос, вторым сообщением-ответом итоговый ответ Codex. Промежуточные статусы в группу не дублировать.
|
|
||||||
- Для приватных voice/audio-запросов в публичном отчёте первым сообщением отправлять исходный Telegram voice/audio-файл с подписью, где указан распознанный текст. В пользовательском тексте отчёта не показывать Telegram `file_id`.
|
|
||||||
- Озвучивание финальных ответов настраивается персонально для каждого Telegram-пользователя командами `/voice_on`, `/voice_off`; для новых пользователей оно включено по умолчанию.
|
|
||||||
- Адаптация текста перед озвучкой настраивается персонально командами `/voice_rewrite_on`, `/voice_rewrite_off`. Если она включена, сервис перед TTS вызывает дешёвую текстовую модель OpenAI и делает голосовую версию без длинных хэшей, путей, команд и технического шума, сохраняя смысл и порядок исходного ответа.
|
|
||||||
- Режим личных ответов настраивается персонально командами `/single_message_on`, `/single_message_off`: либо одно редактируемое сообщение по этапам, либо отдельные сообщения как раньше.
|
|
||||||
- Команда `/settings` должна сразу показывать текущее состояние всех персональных настроек пользователя и список команд для их изменения.
|
|
||||||
- Если озвучивание включено, после полного текстового финального ответа сервис дополнительно отправляет voice-файл с синтезированной речью через OpenAI TTS даже для текстовых запросов. Voice отправляется в исходный чат, а также в известный личный чат пользователя и в общий чат `@shine_writing`, если они отличаются и доступны. Промежуточные статусы не озвучивать.
|
|
||||||
- Команда `/status` должна показывать состояние очереди и персональные настройки: voice-ответы, адаптацию текста перед озвучкой и режим одного сообщения в личке.
|
|
||||||
|
|
||||||
## Правила голосовой версии ответа
|
|
||||||
- Текстовый финальный ответ должен оставаться полноценным: в нём можно указывать команды, пути, хэши коммитов, номера версий, результаты проверок и другие технические детали.
|
|
||||||
- Голосовую версию финального ответа нужно делать короче и проще для восприятия на слух. Основной механизм — персонально включаемая адаптация текста через дополнительный OpenAI-вызов перед TTS.
|
|
||||||
- В голосовой версии не зачитывать длинные хэши коммитов, токены, file_id, длинные команды, полные пути и другие строки, которые человек всё равно не сможет надёжно запомнить на слух.
|
|
||||||
- Для commit/push в голосовой версии достаточно сказать краткий итог: что коммит сделан, что именно изменено, проверки прошли без ошибок, push выполнен, рабочее дерево чистое.
|
|
||||||
- Если пользователю нужны точные команды, хэши или подробности, они должны оставаться в текстовом ответе.
|
|
||||||
|
|
||||||
## Планы и отложенные фичи
|
|
||||||
- Планы проекта по отложенным фичам хранятся в `Dev_Docs/Future_Features/`.
|
|
||||||
- Внутри есть три горизонта:
|
|
||||||
- `near/` - ближайшие планы, обычно сегодня/завтра;
|
|
||||||
- `medium/` - среднесрочные планы, обычно недели или 1-2 месяца;
|
|
||||||
- `far/` - дальнее будущее без понятного срока.
|
|
||||||
- Если пользователь спрашивает, какие есть планы или что можно продолжить, нужно смотреть эти три папки и отвечать кратким списком по горизонтам.
|
|
||||||
- Файлы из `Dev_Docs/Future_Features/` не начинать реализовывать без явной команды пользователя.
|
|
||||||
- После реализации фичи, требующей ручной проверки, нужно добавить отдельный файл в `Dev_Docs/Pending_Features/`.
|
|
||||||
|
|
||||||
## Центр задач и предложений
|
|
||||||
- Сервис хранит простые задачи и предложения в `data/task_center/items.json`.
|
|
||||||
- Айдар может смотреть список через `/tasks` или естественные фразы вроде «покажи мои задачи», «покажи задачи Миланы».
|
|
||||||
- Айдар может ставить задачи игрокам фразой вида «поставь задачу Милане: ...».
|
|
||||||
- Игроки могут отправлять предложения Айдару фразой вида `предложение: ...`, `идея: ...` или `заявка: ...`.
|
|
||||||
- Статусы меняются фразами с ID: `одобрить TC-0001`, `отклонить TC-0001`, `доработать TC-0001`, `закрыть TC-0001`.
|
|
||||||
- После финального ответа в личном чате сервис добавляет короткое напоминание, если у пользователя есть активные задачи или предложения.
|
|
||||||
|
|
||||||
## Локальный запуск и systemd
|
|
||||||
- Основной запуск сервиса выполняется Python-скриптом `py_bot_service.py` из папки `SHiNE-agent-bot-coder/`.
|
|
||||||
- Локальные секреты и параметры должны храниться в `.env`, этот файл не коммитится.
|
|
||||||
- Для проверки Codex без Telegram можно использовать self-test режим сервиса.
|
|
||||||
- Для постоянного локального запуска используется user-level systemd service `shine-agent-bot-coder`; скрипты установки лежат в `SHiNE-agent-bot-coder/scripts/systemd/`.
|
|
||||||
- Если меняется логика сервиса, после изменений нужно проверить запуск локально и при необходимости перезапустить user systemd service.
|
|
||||||
- Команда Telegram `/restart` (`/restart_service`) доступна только Айдару и выполняет отложенный рестарт после текущей задачи, до взятия следующей. Аварийный жёсткий рестарт доступен только Айдару командами `/restart_hard`, `/restart_now`, `/restart_force`.
|
|
||||||
|
|
||||||
## Правила ответа
|
|
||||||
- Пиши содержательно и коротко.
|
|
||||||
- Не упоминай внутренние служебные детали, файловую систему и технические логи.
|
|
||||||
- Если запрос требует действий с кодом/проектом, выполняй их в рабочей директории.
|
|
||||||
- Если для ответа данных недостаточно, задай ровно один уточняющий вопрос.
|
|
||||||
- Если была ошибка предыдущего запуска, в промпте будет пометка retry — учти это и продолжи с учётом текущего состояния проекта.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# AGENTS
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
- Это автоматически читаемые инструкции Codex для папки `SHiNE-agent-bot-coder/`.
|
|
||||||
- `SHiNE-agent-bot-coder` — локальный Telegram-бот-сервис агента-кодера для работы с проектом SHiNE.
|
|
||||||
- Если пользователь говорит «агент MD», «агент с MD» или похожим образом про файл инструкций Codex, считать, что имеется в виду `AGENTS.md`.
|
|
||||||
|
|
||||||
## Связанные инструкции
|
|
||||||
- Подробные служебные правила Telegram-обработчика лежат в `AGENT.md`.
|
|
||||||
- `AGENT.md` используется самим сервисом как файл инструкций, который передаётся в промпт обработчика входящих Telegram-сообщений.
|
|
||||||
- При изменении логики сервиса сначала читать `AGENT.md`, затем код `py_bot_service.py`.
|
|
||||||
|
|
||||||
## Планы и задачи
|
|
||||||
- Отложенные задачи проекта лежат в `../Dev_Docs/Future_Features/`.
|
|
||||||
- Точка входа по планам: `../Dev_Docs/Future_Features/README.md`.
|
|
||||||
- Горизонты планов:
|
|
||||||
- `near/` - ближайшие планы;
|
|
||||||
- `medium/` - среднесрочные планы;
|
|
||||||
- `far/` - дальнее будущее.
|
|
||||||
- Если пользователь спрашивает, какие есть планы или что можно продолжить, кратко перечислять задачи по этим горизонтам.
|
|
||||||
- Не начинать реализацию задач из `Future_Features` без явной команды пользователя.
|
|
||||||
|
|
||||||
## Проверка после изменений
|
|
||||||
- Если меняется логика Telegram-бота, проверить локальный запуск или self-test, когда это уместно.
|
|
||||||
- Если меняется только документация или инструкции, достаточно проверить, что ссылки на документы актуальны.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@AGENTS.md
|
|
||||||
@AGENT.md
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Промпты для режима игроков (на согласование)
|
|
||||||
|
|
||||||
## 1) Базовый служебный промпт (добавка к задаче игрока)
|
|
||||||
|
|
||||||
```text
|
|
||||||
Режим игрока (обязательно):
|
|
||||||
- Пользователь: <Имя> (@<username>).
|
|
||||||
- Рабочая папка игрока: <project>/Players/<username>
|
|
||||||
- Код проекта не изменять.
|
|
||||||
- Можно отвечать на вопросы по проекту, предлагать идеи и готовить ТЗ.
|
|
||||||
- Если нужны правки кода, описывать предложение текстом и сохранять материалы только в папке игрока.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2) Приветственное сообщение игроку (один раз)
|
|
||||||
|
|
||||||
```text
|
|
||||||
Привет, <Имя>.
|
|
||||||
Можно задавать вопросы по проекту, просить анализ, идеи и подготовку готового ТЗ.
|
|
||||||
Команда /new начинает новую сессию и архивирует текущую историю.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3) Отказ неизвестному пользователю
|
|
||||||
|
|
||||||
```text
|
|
||||||
Извините, доступ к этому агенту пока не выдан. Обратитесь к Айдару.
|
|
||||||
```
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# SHiNE-agent-bot-coder
|
|
||||||
|
|
||||||
Локальный Telegram-бот-сервис для пользователя `ai`:
|
|
||||||
- принимает сообщения от `@AidarKC`;
|
|
||||||
- поддерживает whitelist игроков (`ALLOWED_TELEGRAM_PLAYERS`) с отдельными историями;
|
|
||||||
- ведёт историю диалога в `JSONL`;
|
|
||||||
- ставит задачи в файловую очередь;
|
|
||||||
- обрабатывает задачи строго последовательно;
|
|
||||||
- поддерживает текстовые и голосовые сообщения (voice/audio через OpenAI transcription);
|
|
||||||
- вызывает Codex CLI и отправляет ответ в Telegram;
|
|
||||||
- в личном чате умеет работать в двух персонально переключаемых режимах: через одно редактируемое статусное сообщение или через отдельные сообщения по этапам;
|
|
||||||
- умеет персонально для каждого пользователя озвучивать финальный ответ через OpenAI TTS;
|
|
||||||
- при рестарте восстанавливает незавершённые задачи;
|
|
||||||
- отправляет аварийный статус только если Codex молчит 2 минуты подряд во время активной задачи;
|
|
||||||
- принимает сообщения из канала/группы `@shine_writing`, выполняет команды только от `@AidarKC`;
|
|
||||||
- учитывает миграцию обычной Telegram-группы в supergroup и перенаправляет ответы на новый `chat_id`.
|
|
||||||
|
|
||||||
Рабочая реализация сервиса — только `py_bot_service.py`. Старая Java-реализация удалена, потому что не заработала и больше не используется.
|
|
||||||
|
|
||||||
## Структура
|
|
||||||
- `.env` — локальные секреты и параметры запуска (не коммитится);
|
|
||||||
- `data/py_queue.jsonl` — очередь Python-сервиса;
|
|
||||||
- `data/py_state.json` — текущее состояние Python-сервиса;
|
|
||||||
- `data/py_processed_updates.log` — дедуп входящих update;
|
|
||||||
- `data/history/<username>/*.jsonl` — активные истории по пользователям;
|
|
||||||
- `data/history/<username>/archive/*.jsonl` — архивы после `/new`.
|
|
||||||
|
|
||||||
## Локальный запуск
|
|
||||||
1. Скопировать пример:
|
|
||||||
- `cp .env.example .env`
|
|
||||||
2. Заполнить секреты в `.env`.
|
|
||||||
- `TELEGRAM_BOT_TOKEN` — токен рабочего Telegram-бота.
|
|
||||||
- `ALLOWED_TELEGRAM_USERNAME` — пользователь, чьи сообщения выполняются как команды.
|
|
||||||
- `ALLOWED_TELEGRAM_PLAYERS` — whitelist игроков в формате `username:Имя,username2:Имя2`.
|
|
||||||
- `ALLOWED_TELEGRAM_CHANNEL_USERNAME` — канал, из которого принимаются `channel_post`; обычные group/supergroup-сообщения обрабатываются как `message`.
|
|
||||||
- `TELEGRAM_API_BASE_URL` — базовый URL Bot API; по умолчанию `https://api.telegram.org`. Для очень больших voice/audio можно поднять локальный `telegram-bot-api` и направить бота туда.
|
|
||||||
- `TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS` — тайм-аут скачивания voice/audio из Telegram, по умолчанию 300 секунд.
|
|
||||||
- `OPENAI_TRANSCRIBE_TIMEOUT_SECONDS` — тайм-аут распознавания voice/audio в OpenAI, по умолчанию 900 секунд.
|
|
||||||
- `OPENAI_TRANSCRIBE_MAX_UPLOAD_BYTES` — безопасный лимит размера одного куска для OpenAI transcription, по умолчанию `24 MiB`.
|
|
||||||
- `OPENAI_TRANSCRIBE_MAX_CHUNK_SECONDS` — максимальная длина одного куска при длинном аудио, по умолчанию `900` секунд.
|
|
||||||
- `OPENAI_TRANSCRIBE_OVERLAP_SECONDS` — перекрытие соседних кусков для более ровной склейки текста, по умолчанию `2` секунды.
|
|
||||||
- `OPENAI_TRANSCRIBE_REENCODE_BITRATE_KBPS` — битрейт локального пережатия длинного аудио через `ffmpeg`, по умолчанию `24`.
|
|
||||||
- `OPENAI_TRANSCRIBE_FFMPEG_TIMEOUT_SECONDS` — тайм-аут локальной обработки длинного аудио через `ffmpeg`/`ffprobe`, по умолчанию `1800`.
|
|
||||||
- `FFMPEG_BIN` и `FFPROBE_BIN` — пути к локальным бинарям `ffmpeg`/`ffprobe`, если они не лежат в `PATH`.
|
|
||||||
- `OPENAI_TTS_MODEL` — модель синтеза речи, по умолчанию `gpt-4o-mini-tts`.
|
|
||||||
- `OPENAI_TTS_VOICE` — голос синтеза речи, по умолчанию `alloy`.
|
|
||||||
- `OPENAI_TTS_RESPONSE_FORMAT` — аудиоформат для Telegram voice, по умолчанию `opus`.
|
|
||||||
- `OPENAI_TTS_TIMEOUT_SECONDS` — тайм-аут генерации одного фрагмента речи, по умолчанию 180 секунд.
|
|
||||||
- `OPENAI_TTS_CHUNK_CHARS` — максимальный размер одного фрагмента озвучки, по умолчанию 3500 символов.
|
|
||||||
3. Запуск:
|
|
||||||
- `python3 SHiNE-agent-bot-coder/py_bot_service.py`
|
|
||||||
|
|
||||||
## Быстрый self-test Codex (без Telegram)
|
|
||||||
```bash
|
|
||||||
python3 SHiNE-agent-bot-coder/py_bot_service.py --selftest-codex "Ответь одной строкой: Codex работает"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Длинные voice/audio
|
|
||||||
- Если аудио короткое, бот отправляет его в OpenAI как раньше.
|
|
||||||
- Если аудио большое или длинное, бот локально пережимает его через `ffmpeg`, при необходимости режет на куски и распознаёт последовательно.
|
|
||||||
- Если Telegram заранее сообщает большой размер файла, бот больше не отказывается сразу: сначала явно пишет, что пробует скачать файл, затем отдельно сообщает, удалось ли скачивание, и только после успешной загрузки переходит к подготовке аудио и OpenAI.
|
|
||||||
- Для очень больших файлов упираемся не только в OpenAI, но и в лимит обычного облачного Telegram Bot API на скачивание файла ботом. Для таких случаев нужно использовать локальный `telegram-bot-api` сервер и указать его через `TELEGRAM_API_BASE_URL`.
|
|
||||||
|
|
||||||
## Статусы в личке
|
|
||||||
- Для `private`-чата бот поддерживает персональную настройку режима ответа.
|
|
||||||
- По умолчанию он старается не засорять переписку промежуточными сообщениями: создаёт одно статусное сообщение и редактирует его по этапам.
|
|
||||||
- Если включить `/single_message_off`, бот возвращается к старому режиму и отправляет отдельные сообщения по этапам и финальный ответ отдельно.
|
|
||||||
- Если финальный текст в режиме одного сообщения не помещается целиком, бот оставляет первую часть в отредактированном статусном сообщении и отправляет максимум ещё одно дополнительное текстовое сообщение с хвостом ответа.
|
|
||||||
- Голосовой ответ, если он включён, всегда приходит отдельным новым сообщением.
|
|
||||||
|
|
||||||
## Запуск как systemd-сервис
|
|
||||||
Файлы для установки:
|
|
||||||
- `scripts/systemd/shine-agent-bot-coder.service`
|
|
||||||
- `scripts/systemd/install-local-systemd.sh`
|
|
||||||
|
|
||||||
Установка:
|
|
||||||
- `bash SHiNE-agent-bot-coder/scripts/systemd/install-local-systemd.sh`
|
|
||||||
|
|
||||||
Проверка:
|
|
||||||
- `systemctl --user status shine-agent-bot-coder --no-pager`
|
|
||||||
- `journalctl --user -u shine-agent-bot-coder -f`
|
|
||||||
|
|
||||||
Перезапуск после изменений:
|
|
||||||
- `systemctl --user restart shine-agent-bot-coder`
|
|
||||||
|
|
||||||
## Telegram-команды
|
|
||||||
- `/status` — активная задача и размер очереди.
|
|
||||||
- `/settings` — текущие пользовательские настройки и команды для их изменения.
|
|
||||||
- `/queue` — список задач в очереди.
|
|
||||||
- `/stop` — остановить текущую задачу.
|
|
||||||
- `/cancel <id|all>` — удалить задачу по id/префиксу или очистить очередь.
|
|
||||||
- `/new` — архивировать текущую историю, сбросить продолжение Codex-сессии для этого пользователя и начать новый диалог.
|
|
||||||
- `/voice_on` — включить озвучивание финальных ответов для текущего пользователя.
|
|
||||||
- `/voice_off` — выключить озвучивание финальных ответов для текущего пользователя.
|
|
||||||
- `/voice_rewrite_on` — включить адаптацию текста перед озвучкой.
|
|
||||||
- `/voice_rewrite_off` — выключить адаптацию текста перед озвучкой.
|
|
||||||
- `/single_message_on` — вести ответ в личке через одно редактируемое сообщение.
|
|
||||||
- `/single_message_off` — слать отдельные сообщения по этапам и отдельный финальный ответ.
|
|
||||||
- `/restart` или `/restart_service` — отложенный рестарт после текущей задачи, до взятия следующей (только для Айдара).
|
|
||||||
- `/restart_hard` — жёсткий рестарт прямо сейчас (только для Айдара).
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="/home/ai/work/SHiNE/SHiNE-server-sha256"
|
|
||||||
SERVICE_DIR="${ROOT_DIR}/SHiNE-agent-bot-coder"
|
|
||||||
UNIT_SRC="${SERVICE_DIR}/scripts/systemd/shine-agent-bot-coder.service"
|
|
||||||
UNIT_DST="${HOME}/.config/systemd/user/shine-agent-bot-coder.service"
|
|
||||||
|
|
||||||
echo "[1/6] Проверка python3..."
|
|
||||||
command -v python3 >/dev/null 2>&1 || { echo "python3 не найден"; exit 1; }
|
|
||||||
|
|
||||||
echo "[2/6] Подготовка папки логов..."
|
|
||||||
mkdir -p "${SERVICE_DIR}/logs"
|
|
||||||
|
|
||||||
echo "[3/6] Копирование user systemd unit..."
|
|
||||||
mkdir -p "$(dirname "${UNIT_DST}")"
|
|
||||||
cp "${UNIT_SRC}" "${UNIT_DST}"
|
|
||||||
|
|
||||||
echo "[4/6] daemon-reload..."
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
|
|
||||||
echo "[5/6] enable + start..."
|
|
||||||
systemctl --user enable --now shine-agent-bot-coder
|
|
||||||
|
|
||||||
echo "[6/6] Статус:"
|
|
||||||
systemctl --user status shine-agent-bot-coder --no-pager
|
|
||||||
|
|
||||||
echo "Готово. Логи: journalctl --user -u shine-agent-bot-coder -f"
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=SHiNE Agent Bot Coder (Telegram + Codex queue worker)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
WorkingDirectory=/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder
|
|
||||||
EnvironmentFile=/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/.env
|
|
||||||
ExecStart=/usr/bin/python3 /home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/py_bot_service.py
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
TimeoutStopSec=20
|
|
||||||
SuccessExitStatus=143 0
|
|
||||||
StandardOutput=append:/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/logs/service.log
|
|
||||||
StandardError=append:/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/logs/service.log
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
@@ -7,11 +7,13 @@ Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.
|
|||||||
- создать `wallet-session` через `StartTrustedDeviceLogin`;
|
- создать `wallet-session` через `StartTrustedDeviceLogin`;
|
||||||
- показать код подключения;
|
- показать код подключения;
|
||||||
- дождаться подтверждения на доверенном устройстве;
|
- дождаться подтверждения на доверенном устройстве;
|
||||||
- принять `session-only` payload без передачи `deviceKey/rootKey/blockchainKey`;
|
- принять `session-only` payload без передачи `clientKey/rootKey/blockchainKey`;
|
||||||
- сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin;
|
- сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin;
|
||||||
- восстанавливать session через `SessionChallenge -> SessionLogin`;
|
- восстанавливать session через `SessionChallenge -> SessionLogin`;
|
||||||
- держать wallet-state в `background service worker`, а popup использовать как UI.
|
- держать wallet-state в `background service worker`, а side panel использовать как UI.
|
||||||
- принимать не адрес сервера, а логин серверного аккаунта SHiNE и находить точный `https://...` / `wss://...` адрес через его PDA.
|
- принимать не адрес сервера, а логин серверного аккаунта SHiNE и находить точный `https://...` / `wss://...` адрес через его PDA.
|
||||||
|
- внедрять legacy `window.solana` / `window.phantom.solana` provider для сайтов.
|
||||||
|
- регистрировать кошелёк как `Wallet Standard` wallet для dapp, которые ищут стандартные кошельки.
|
||||||
|
|
||||||
## Как загрузить локально
|
## Как загрузить локально
|
||||||
|
|
||||||
@@ -19,13 +21,16 @@ Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.
|
|||||||
2. Включи `Developer mode`
|
2. Включи `Developer mode`
|
||||||
3. Нажми `Load unpacked`
|
3. Нажми `Load unpacked`
|
||||||
4. Выбери папку `SHiNE-browser-plugin-wallet/`
|
4. Выбери папку `SHiNE-browser-plugin-wallet/`
|
||||||
|
5. Нажми на иконку расширения в toolbar: откроется side panel SHiNE Wallet
|
||||||
|
|
||||||
## Ограничения текущего этапа
|
## Ограничения текущего этапа
|
||||||
|
|
||||||
- plugin пока не держит постоянный фоновый WS-канал после закрытия popup, но хранит wallet-state в `background`;
|
- plugin пока не держит постоянный фоновый WS-канал без активного действия пользователя, но хранит wallet-state в `background`;
|
||||||
- на этом этапе реализован только `session-only login`;
|
- на этом этапе реализован только `session-only login`;
|
||||||
- запросы на подпись будут следующим этапом.
|
- запросы на подпись будут следующим этапом.
|
||||||
- pairing-пароль, если он используется, должен генерироваться в формате `sha256$<hex>` от строки `shine-pairing|loginLower|password`.
|
- pairing-пароль, если он используется, должен генерироваться в формате `sha256$<hex>` от строки `shine-pairing|loginLower|password`.
|
||||||
|
- сторона side panel в Chromium выбирается самим браузером/пользователем; extension не закрепляет панель принудительно слева.
|
||||||
|
- для совместимости с некоторыми dapp расширение одновременно держит и legacy provider, и Wallet Standard регистрацию.
|
||||||
|
|
||||||
## Сборка crypto bundle
|
## Сборка crypto bundle
|
||||||
|
|
||||||
|
|||||||
@@ -26,19 +26,101 @@ const state = {
|
|||||||
connectionOnline: false,
|
connectionOnline: false,
|
||||||
walletProfile: null,
|
walletProfile: null,
|
||||||
signing: {
|
signing: {
|
||||||
selectedKeyId: 'device',
|
|
||||||
selectedDeviceName: '',
|
selectedDeviceName: '',
|
||||||
devicesResolvedAtMs: 0,
|
devicesResolvedAtMs: 0,
|
||||||
},
|
},
|
||||||
|
currentWallet: null,
|
||||||
|
pendingApprovals: [],
|
||||||
|
siteApprovalChain: Promise.resolve(),
|
||||||
|
sessionAttachInProgress: false,
|
||||||
statusText: '',
|
statusText: '',
|
||||||
statusKind: 'info',
|
statusKind: 'info',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WALLET_RPC_REQUEST_TYPE = 9100;
|
||||||
|
const WALLET_RPC_RESPONSE_TYPE = 9101;
|
||||||
|
|
||||||
|
async function configureSidePanelBehavior() {
|
||||||
|
if (!chrome.sidePanel?.setPanelBehavior) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to configure SHiNE side panel behavior:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOrigin(value = '') {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
if (!raw) return '';
|
||||||
|
try {
|
||||||
|
return new URL(raw).origin;
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCodeError(message, code) {
|
||||||
|
const error = new Error(String(message || 'Wallet error'));
|
||||||
|
error.code = String(code || '').trim().toUpperCase();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
function setStatus(message = '', kind = 'info') {
|
function setStatus(message = '', kind = 'info') {
|
||||||
state.statusText = String(message || '');
|
state.statusText = String(message || '');
|
||||||
state.statusKind = kind === 'error' ? 'error' : 'info';
|
state.statusKind = kind === 'error' ? 'error' : 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makePendingApprovalSnapshot(payload = {}) {
|
||||||
|
const pendingId = String(payload?.id || '').trim();
|
||||||
|
const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : [];
|
||||||
|
const queueIndex = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === pendingId);
|
||||||
|
const queueLength = pendingApprovals.length;
|
||||||
|
return {
|
||||||
|
id: pendingId,
|
||||||
|
kind: String(payload?.kind || 'sign_transaction').trim() || 'sign_transaction',
|
||||||
|
origin: String(payload?.origin || '').trim(),
|
||||||
|
publicKeyBase58: String(payload?.publicKeyBase58 || '').trim(),
|
||||||
|
comment: String(payload?.comment || '').trim(),
|
||||||
|
createdAtMs: Number(payload?.createdAtMs || Date.now()),
|
||||||
|
status: String(payload?.status || 'queued').trim() || 'queued',
|
||||||
|
queuePosition: queueIndex >= 0 ? queueIndex + 1 : 1,
|
||||||
|
queueLength: queueLength || 1,
|
||||||
|
transactionSummary: payload?.transactionSummary && typeof payload.transactionSummary === 'object'
|
||||||
|
? { ...payload.transactionSummary }
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentPendingApproval() {
|
||||||
|
return Array.isArray(state.pendingApprovals) && state.pendingApprovals.length
|
||||||
|
? state.pendingApprovals[0]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePendingApproval(pendingId, { rejectError = null } = {}) {
|
||||||
|
const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : [];
|
||||||
|
const index = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === String(pendingId || '').trim());
|
||||||
|
if (index < 0) return;
|
||||||
|
const [pending] = pendingApprovals.splice(index, 1);
|
||||||
|
if (pending.timeoutId) {
|
||||||
|
clearTimeout(pending.timeoutId);
|
||||||
|
}
|
||||||
|
if (rejectError && pending.abortController) {
|
||||||
|
try {
|
||||||
|
pending.abortController.abort(rejectError);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSidePanelForSender(sender) {
|
||||||
|
if (!chrome.sidePanel?.open || !sender?.tab?.id) return;
|
||||||
|
try {
|
||||||
|
await chrome.sidePanel.open({ tabId: sender.tab.id });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
function stopPoll() {
|
function stopPoll() {
|
||||||
if (state.pollTimer) {
|
if (state.pollTimer) {
|
||||||
clearTimeout(state.pollTimer);
|
clearTimeout(state.pollTimer);
|
||||||
@@ -71,14 +153,18 @@ async function loadStateFromStorage() {
|
|||||||
serverHttp: String(settings?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim() || buildHttpBase('shineup.me'),
|
serverHttp: String(settings?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim() || buildHttpBase('shineup.me'),
|
||||||
serverUrl: String(settings?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
|
serverUrl: String(settings?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
|
||||||
login: String(settings?.login || '').trim(),
|
login: String(settings?.login || '').trim(),
|
||||||
|
connectedOrigins: Array.isArray(settings?.connectedOrigins) ? settings.connectedOrigins.map((item) => normalizeOrigin(item)).filter(Boolean) : [],
|
||||||
};
|
};
|
||||||
state.activeSession = await loadSessionMaterial();
|
const storedSession = await loadSessionMaterial();
|
||||||
|
if (storedSession || !state.sessionAttachInProgress) {
|
||||||
|
state.activeSession = storedSession;
|
||||||
|
}
|
||||||
state.walletProfile = state.activeSession?.walletProfile || null;
|
state.walletProfile = state.activeSession?.walletProfile || null;
|
||||||
state.signing = {
|
state.signing = {
|
||||||
selectedKeyId: String(state.activeSession?.selectedKeyId || 'device'),
|
|
||||||
selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''),
|
selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''),
|
||||||
devicesResolvedAtMs: Number(state.activeSession?.devicesResolvedAtMs || 0),
|
devicesResolvedAtMs: Number(state.activeSession?.devicesResolvedAtMs || 0),
|
||||||
};
|
};
|
||||||
|
state.currentWallet = state.activeSession?.currentWallet || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistSettings(nextSettings = {}) {
|
async function persistSettings(nextSettings = {}) {
|
||||||
@@ -86,10 +172,29 @@ async function persistSettings(nextSettings = {}) {
|
|||||||
...state.settings,
|
...state.settings,
|
||||||
...nextSettings,
|
...nextSettings,
|
||||||
};
|
};
|
||||||
|
if (!Array.isArray(state.settings.connectedOrigins)) {
|
||||||
|
state.settings.connectedOrigins = [];
|
||||||
|
}
|
||||||
await savePluginSettings(state.settings);
|
await savePluginSettings(state.settings);
|
||||||
return state.settings;
|
return state.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOriginApproved(origin) {
|
||||||
|
const normalized = normalizeOrigin(origin);
|
||||||
|
return !!normalized && Array.isArray(state.settings.connectedOrigins) && state.settings.connectedOrigins.includes(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setOriginApproved(origin, approved) {
|
||||||
|
const normalized = normalizeOrigin(origin);
|
||||||
|
const current = new Set(Array.isArray(state.settings.connectedOrigins) ? state.settings.connectedOrigins : []);
|
||||||
|
if (approved) {
|
||||||
|
if (normalized) current.add(normalized);
|
||||||
|
} else if (normalized) {
|
||||||
|
current.delete(normalized);
|
||||||
|
}
|
||||||
|
await persistSettings({ connectedOrigins: [...current] });
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveServerForLogin(login) {
|
async function resolveServerForLogin(login) {
|
||||||
const cleanLogin = String(login || state.settings.login || '').trim();
|
const cleanLogin = String(login || state.settings.login || '').trim();
|
||||||
if (!cleanLogin) {
|
if (!cleanLogin) {
|
||||||
@@ -119,9 +224,9 @@ async function saveActiveSessionRecord() {
|
|||||||
const nextRecord = {
|
const nextRecord = {
|
||||||
...state.activeSession,
|
...state.activeSession,
|
||||||
walletProfile: state.walletProfile,
|
walletProfile: state.walletProfile,
|
||||||
selectedKeyId: state.signing.selectedKeyId,
|
|
||||||
selectedDeviceName: state.signing.selectedDeviceName,
|
selectedDeviceName: state.signing.selectedDeviceName,
|
||||||
devicesResolvedAtMs: state.signing.devicesResolvedAtMs,
|
devicesResolvedAtMs: state.signing.devicesResolvedAtMs,
|
||||||
|
currentWallet: state.currentWallet,
|
||||||
};
|
};
|
||||||
state.activeSession = nextRecord;
|
state.activeSession = nextRecord;
|
||||||
await saveSessionMaterial(nextRecord);
|
await saveSessionMaterial(nextRecord);
|
||||||
@@ -152,27 +257,10 @@ function toWalletErrorMessage(error, fallback = 'Не удалось выпол
|
|||||||
return raw || fallback;
|
return raw || fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSigningKeyOptions(walletProfile) {
|
function homeserverSessionNameFromClientInfo(value = '') {
|
||||||
const rootKey = String(walletProfile?.publicKeys?.rootKeyBase58 || '').trim();
|
const raw = String(value || '').trim();
|
||||||
const deviceKey = String(walletProfile?.publicKeys?.deviceKeyBase58 || '').trim();
|
const match = raw.match(/^ESP32 homeserver:(.+)$/i);
|
||||||
const options = [];
|
return match ? String(match[1] || '').trim() : '';
|
||||||
if (rootKey) {
|
|
||||||
options.push({
|
|
||||||
id: 'root',
|
|
||||||
label: `rootKey (ed25519, ${shortKey(rootKey)})`,
|
|
||||||
keyType: 'ed25519',
|
|
||||||
publicKeyBase58: rootKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (deviceKey) {
|
|
||||||
options.push({
|
|
||||||
id: 'device',
|
|
||||||
label: `deviceKey (ed25519, ${shortKey(deviceKey)})`,
|
|
||||||
keyType: 'ed25519',
|
|
||||||
publicKeyBase58: deviceKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = []) {
|
function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = []) {
|
||||||
@@ -181,18 +269,25 @@ function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = [])
|
|||||||
? serverSessions.filter((item) => Number(item?.sessionType || 0) === 100)
|
? serverSessions.filter((item) => Number(item?.sessionType || 0) === 100)
|
||||||
: [];
|
: [];
|
||||||
const onlineHomeservers = homeserverSessions.filter((item) => !!item?.onlineOnThisServer);
|
const onlineHomeservers = homeserverSessions.filter((item) => !!item?.onlineOnThisServer);
|
||||||
|
const byName = new Map();
|
||||||
|
onlineHomeservers.forEach((item) => {
|
||||||
|
const sessionName = homeserverSessionNameFromClientInfo(item?.clientInfoFromClient);
|
||||||
|
if (sessionName) {
|
||||||
|
byName.set(sessionName, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return published.map((item) => {
|
return published.map((item) => {
|
||||||
let onlineState = 'unknown';
|
const matched = byName.get(String(item?.sessionName || '').trim()) || null;
|
||||||
if (published.length === 1) {
|
let onlineState = matched ? 'online' : 'offline';
|
||||||
onlineState = onlineHomeservers.length > 0 ? 'online' : 'offline';
|
let activeSessionId = matched?.sessionId ? String(matched.sessionId) : '';
|
||||||
} else if (onlineHomeservers.length === 0) {
|
if (!matched && published.length === 1 && onlineHomeservers.length === 1) {
|
||||||
onlineState = 'offline';
|
|
||||||
} else if (onlineHomeservers.length === published.length) {
|
|
||||||
onlineState = 'online';
|
onlineState = 'online';
|
||||||
|
activeSessionId = String(onlineHomeservers[0]?.sessionId || '');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
|
activeSessionId,
|
||||||
onlineState,
|
onlineState,
|
||||||
onlineLabel: onlineState === 'online' ? 'online' : onlineState === 'offline' ? 'offline' : 'unknown',
|
onlineLabel: onlineState === 'online' ? 'online' : onlineState === 'offline' ? 'offline' : 'unknown',
|
||||||
};
|
};
|
||||||
@@ -203,25 +298,20 @@ async function hydrateWalletProfile(login) {
|
|||||||
const cleanLogin = String(login || state.activeSession?.login || state.settings.login || '').trim();
|
const cleanLogin = String(login || state.activeSession?.login || state.settings.login || '').trim();
|
||||||
if (!cleanLogin) throw new Error('Нет логина для чтения PDA кошелька.');
|
if (!cleanLogin) throw new Error('Нет логина для чтения PDA кошелька.');
|
||||||
const profile = await readWalletProfileByLogin(cleanLogin);
|
const profile = await readWalletProfileByLogin(cleanLogin);
|
||||||
const signingKeyOptions = buildSigningKeyOptions(profile);
|
|
||||||
const selectedKeyId = signingKeyOptions.some((item) => item.id === state.signing.selectedKeyId)
|
|
||||||
? state.signing.selectedKeyId
|
|
||||||
: (signingKeyOptions[0]?.id || '');
|
|
||||||
const selectedDeviceName = state.signing.selectedDeviceName
|
const selectedDeviceName = state.signing.selectedDeviceName
|
||||||
|| String(profile?.homeserverSessions?.[0]?.sessionName || '');
|
|| String(profile?.homeserverSessions?.[0]?.sessionName || '');
|
||||||
|
|
||||||
state.walletProfile = {
|
state.walletProfile = {
|
||||||
...profile,
|
...profile,
|
||||||
signingKeyOptions,
|
|
||||||
homeserverSessions: Array.isArray(profile.homeserverSessions) ? profile.homeserverSessions.map((item) => ({
|
homeserverSessions: Array.isArray(profile.homeserverSessions) ? profile.homeserverSessions.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
onlineState: 'unknown',
|
onlineState: 'unknown',
|
||||||
onlineLabel: 'unknown',
|
onlineLabel: 'unknown',
|
||||||
|
activeSessionId: '',
|
||||||
})) : [],
|
})) : [],
|
||||||
};
|
};
|
||||||
state.signing = {
|
state.signing = {
|
||||||
...state.signing,
|
...state.signing,
|
||||||
selectedKeyId,
|
|
||||||
selectedDeviceName,
|
selectedDeviceName,
|
||||||
};
|
};
|
||||||
await saveActiveSessionRecord();
|
await saveActiveSessionRecord();
|
||||||
@@ -282,17 +372,30 @@ async function attachApprovedSession(payload) {
|
|||||||
throw new Error('Получен неполный session-only payload');
|
throw new Error('Получен неполный session-only payload');
|
||||||
}
|
}
|
||||||
|
|
||||||
await clearSessionMaterial();
|
state.sessionAttachInProgress = true;
|
||||||
state.activeSession = sessionRecord;
|
try {
|
||||||
await hydrateWalletProfile(login);
|
state.activeSession = sessionRecord;
|
||||||
await saveActiveSessionRecord();
|
state.walletProfile = null;
|
||||||
await persistSettings({
|
state.currentWallet = null;
|
||||||
login: sessionRecord.login,
|
state.signing = {
|
||||||
serverLogin: sessionRecord.serverLogin,
|
...state.signing,
|
||||||
serverHttp: sessionRecord.serverHttp,
|
selectedDeviceName: '',
|
||||||
serverUrl: sessionRecord.serverUrl,
|
devicesResolvedAtMs: 0,
|
||||||
});
|
};
|
||||||
state.connectionOnline = false;
|
await saveActiveSessionRecord();
|
||||||
|
await hydrateWalletProfile(login);
|
||||||
|
await saveActiveSessionRecord();
|
||||||
|
await persistSettings({
|
||||||
|
login: sessionRecord.login,
|
||||||
|
serverLogin: sessionRecord.serverLogin,
|
||||||
|
serverHttp: sessionRecord.serverHttp,
|
||||||
|
serverUrl: sessionRecord.serverUrl,
|
||||||
|
});
|
||||||
|
state.connectionOnline = false;
|
||||||
|
state.currentWallet = null;
|
||||||
|
} finally {
|
||||||
|
state.sessionAttachInProgress = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollPairingStatus() {
|
async function pollPairingStatus() {
|
||||||
@@ -400,10 +503,10 @@ async function disconnectSession() {
|
|||||||
state.connectionOnline = false;
|
state.connectionOnline = false;
|
||||||
state.walletProfile = null;
|
state.walletProfile = null;
|
||||||
state.signing = {
|
state.signing = {
|
||||||
selectedKeyId: 'device',
|
|
||||||
selectedDeviceName: '',
|
selectedDeviceName: '',
|
||||||
devicesResolvedAtMs: 0,
|
devicesResolvedAtMs: 0,
|
||||||
};
|
};
|
||||||
|
state.currentWallet = null;
|
||||||
setStatus('Сохранённая wallet-session удалена из plugin.', 'info');
|
setStatus('Сохранённая wallet-session удалена из plugin.', 'info');
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
@@ -440,40 +543,296 @@ async function refreshWalletDevices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSigningSelection({ selectedKeyId, selectedDeviceName } = {}) {
|
async function updateSigningSelection({ selectedDeviceName } = {}) {
|
||||||
state.signing = {
|
state.signing = {
|
||||||
...state.signing,
|
...state.signing,
|
||||||
selectedKeyId: String(selectedKeyId || state.signing.selectedKeyId || ''),
|
|
||||||
selectedDeviceName: String(selectedDeviceName || state.signing.selectedDeviceName || ''),
|
selectedDeviceName: String(selectedDeviceName || state.signing.selectedDeviceName || ''),
|
||||||
};
|
};
|
||||||
await saveActiveSessionRecord();
|
await saveActiveSessionRecord();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareSignSignal() {
|
async function resolveSelectedHomeserverSession() {
|
||||||
if (!state.activeSession?.login) {
|
if (!state.activeSession?.login) {
|
||||||
throw new Error('Сначала подключите wallet-session.');
|
throw new Error('Сначала подключите wallet-session.');
|
||||||
}
|
}
|
||||||
if (!state.signing.selectedKeyId) {
|
|
||||||
throw new Error('Не выбран ключ подписи.');
|
|
||||||
}
|
|
||||||
if (!state.signing.selectedDeviceName) {
|
if (!state.signing.selectedDeviceName) {
|
||||||
throw new Error('Не выбрано устройство homeserver.');
|
throw new Error('Не выбрано устройство homeserver.');
|
||||||
}
|
}
|
||||||
const selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName);
|
let selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName);
|
||||||
if (!selectedDevice) {
|
if (!selectedDevice) {
|
||||||
throw new Error('Выбранное устройство не найдено в PDA аккаунта.');
|
throw new Error('Выбранное устройство не найдено в PDA аккаунта.');
|
||||||
}
|
}
|
||||||
setStatus(
|
if (!selectedDevice.activeSessionId) {
|
||||||
`Каркас готов: запрос подписи должен идти через ${selectedDevice.sessionName}. Сам signaling подписи ещё не доделан.`,
|
await refreshWalletDevices();
|
||||||
'info',
|
selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName);
|
||||||
);
|
}
|
||||||
|
if (!selectedDevice?.activeSessionId) {
|
||||||
|
throw new Error('Выбранный homeserver сейчас не найден онлайн на сервере SHiNE.');
|
||||||
|
}
|
||||||
|
return selectedDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callWalletRpc(requestData, timeoutMs = 8000, abortSignal = null) {
|
||||||
|
const selectedDevice = await resolveSelectedHomeserverSession();
|
||||||
|
const resumed = await resumeActiveSession({ keepConnected: true });
|
||||||
|
if (!resumed.ok) {
|
||||||
|
throw new Error(resumed.error || 'Не удалось открыть wallet-session.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = String(requestData?.requestId || `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`);
|
||||||
|
const callId = `wallet-rpc-${requestId}`;
|
||||||
|
const payload = {
|
||||||
|
...requestData,
|
||||||
|
requestId,
|
||||||
|
timeMs: Number(requestData?.timeMs || Date.now()),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await new Promise((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
let timeoutId = 0;
|
||||||
|
let off = () => {};
|
||||||
|
let removeAbortListener = () => {};
|
||||||
|
const cleanup = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
off();
|
||||||
|
removeAbortListener();
|
||||||
|
};
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error('Таймаут ответа от ESP32.'));
|
||||||
|
}, timeoutMs);
|
||||||
|
off = ensureApi().onEvent('IncomingCallSignal', (evt) => {
|
||||||
|
const eventPayload = evt?.payload || {};
|
||||||
|
if (String(eventPayload?.callId || '') !== callId) return;
|
||||||
|
if (Number(eventPayload?.type || 0) !== WALLET_RPC_RESPONSE_TYPE) return;
|
||||||
|
cleanup();
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(String(eventPayload?.data || '{}')));
|
||||||
|
} catch {
|
||||||
|
reject(new Error('ESP32 вернул некорректный JSON.'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (abortSignal) {
|
||||||
|
const onAbort = () => {
|
||||||
|
cleanup();
|
||||||
|
reject(abortSignal.reason instanceof Error ? abortSignal.reason : new Error('Ожидание подписи отменено.'));
|
||||||
|
};
|
||||||
|
if (abortSignal.aborted) {
|
||||||
|
onAbort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
abortSignal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
removeAbortListener = () => {
|
||||||
|
abortSignal.removeEventListener('abort', onAbort);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ensureApi().callSignalToSession({
|
||||||
|
toLogin: state.activeSession.login,
|
||||||
|
targetSessionId: selectedDevice.activeSessionId,
|
||||||
|
callId,
|
||||||
|
type: WALLET_RPC_REQUEST_TYPE,
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
}).catch((error) => {
|
||||||
|
cleanup();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { response, selectedDevice, requestId };
|
||||||
|
} finally {
|
||||||
|
ensureApi().close();
|
||||||
|
state.api = null;
|
||||||
|
state.connectionOnline = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyWalletAgainstPda(wallet) {
|
||||||
|
const type = String(wallet?.type || '').trim();
|
||||||
|
const pub = String(wallet?.publicKeyBase58 || '').trim();
|
||||||
|
const rootKey = String(state.walletProfile?.publicKeys?.rootKeyBase58 || '').trim();
|
||||||
|
const clientKey = String(
|
||||||
|
state.walletProfile?.publicKeys?.clientKeyBase58 || '',
|
||||||
|
).trim();
|
||||||
|
if (type === 'client.key') {
|
||||||
|
return {
|
||||||
|
verified: !!clientKey && clientKey === pub,
|
||||||
|
verificationText: clientKey === pub ? 'Совпадает с clientKey из PDA.' : 'Не совпадает с clientKey из PDA.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === 'root.key') {
|
||||||
|
return {
|
||||||
|
verified: !!rootKey && rootKey === pub,
|
||||||
|
verificationText: rootKey === pub ? 'Совпадает с rootKey из PDA.' : 'Не совпадает с rootKey из PDA.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
verified: null,
|
||||||
|
verificationText: 'Для custom-кошелька проверка через PDA пока не выполняется.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestCurrentWallet() {
|
||||||
|
const { response, selectedDevice, requestId } = await callWalletRpc({
|
||||||
|
v: 1,
|
||||||
|
operation: 'get_wallet_public_key',
|
||||||
|
requestId: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||||
|
});
|
||||||
|
if (!response?.ok) {
|
||||||
|
throw new Error(`ESP32 отклонил запрос: ${String(response?.error || 'unknown_error')}`);
|
||||||
|
}
|
||||||
|
state.currentWallet = {
|
||||||
|
type: String(response?.wallet?.type || '').trim(),
|
||||||
|
publicKeyBase58: String(response?.wallet?.publicKeyBase58 || '').trim(),
|
||||||
|
homeserverName: selectedDevice.sessionName,
|
||||||
|
requestId: String(response?.requestId || requestId),
|
||||||
|
timeMs: Number(response?.timeMs || 0),
|
||||||
|
...verifyWalletAgainstPda(response?.wallet || {}),
|
||||||
|
};
|
||||||
|
await saveActiveSessionRecord();
|
||||||
|
setStatus(`Кошелёк получен с ${selectedDevice.sessionName}.`, 'info');
|
||||||
|
return { ok: true, wallet: state.currentWallet };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelPendingSiteApproval() {
|
||||||
|
const pending = getCurrentPendingApproval();
|
||||||
|
if (!pending) {
|
||||||
|
setStatus('Сейчас нет активного ожидания подписи.', 'info');
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
removePendingApproval(pending.id, {
|
||||||
|
rejectError: makeCodeError('User canceled request in extension.', 'USER_REJECTED'),
|
||||||
|
});
|
||||||
|
setStatus('Ожидание подписи отменено в расширении.', 'info');
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markPendingSiteApprovalResolved(pendingId) {
|
||||||
|
removePendingApproval(pendingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueSiteApproval(work) {
|
||||||
|
const run = state.siteApprovalChain.then(work, work);
|
||||||
|
state.siteApprovalChain = run.catch(() => {});
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activatePendingApproval(pending, sender = null) {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
pending.status = 'active';
|
||||||
|
pending.abortController = abortController;
|
||||||
|
pending.timeoutId = setTimeout(() => {
|
||||||
|
removePendingApproval(pending.id, {
|
||||||
|
rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'),
|
||||||
|
});
|
||||||
|
setStatus('Ожидание подписи истекло в расширении.', 'error');
|
||||||
|
}, 120000);
|
||||||
|
setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info');
|
||||||
|
await openSidePanelForSender(sender);
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginSiteTransactionFlow(payload = {}) {
|
||||||
|
const pending = makePendingApprovalSnapshot({
|
||||||
|
...payload,
|
||||||
|
kind: 'sign_transaction',
|
||||||
|
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||||
|
createdAtMs: Date.now(),
|
||||||
|
status: 'queued',
|
||||||
|
});
|
||||||
|
state.pendingApprovals.push({
|
||||||
|
...pending,
|
||||||
|
timeoutId: 0,
|
||||||
|
abortController: null,
|
||||||
|
});
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function siteConnect({ origin, onlyIfTrusted = false } = {}) {
|
||||||
|
const normalizedOrigin = normalizeOrigin(origin);
|
||||||
|
if (!normalizedOrigin) {
|
||||||
|
throw makeCodeError('Site origin is missing.', 'BAD_ORIGIN');
|
||||||
|
}
|
||||||
|
if (onlyIfTrusted && !isOriginApproved(normalizedOrigin)) {
|
||||||
|
throw makeCodeError('Site is not trusted yet.', 'NOT_TRUSTED');
|
||||||
|
}
|
||||||
|
const result = await requestCurrentWallet();
|
||||||
|
const publicKeyBase58 = String(result?.wallet?.publicKeyBase58 || '').trim();
|
||||||
|
if (!publicKeyBase58) {
|
||||||
|
throw makeCodeError('Wallet public key is not available.', 'WALLET_UNAVAILABLE');
|
||||||
|
}
|
||||||
|
if (!isOriginApproved(normalizedOrigin)) {
|
||||||
|
await setOriginApproved(normalizedOrigin, true);
|
||||||
|
}
|
||||||
|
setStatus(`Site ${normalizedOrigin} connected to ${shortKey(publicKeyBase58, 8)}.`, 'info');
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
pending: true,
|
publicKeyBase58,
|
||||||
|
walletType: String(result?.wallet?.type || '').trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function siteDisconnect({ origin } = {}) {
|
||||||
|
const normalizedOrigin = normalizeOrigin(origin);
|
||||||
|
setStatus(normalizedOrigin ? `Site ${normalizedOrigin} disconnected.` : 'Site disconnected.', 'info');
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64, comment, transactionSummary } = {}, sender = null) {
|
||||||
|
const normalizedOrigin = normalizeOrigin(origin);
|
||||||
|
if (!normalizedOrigin) {
|
||||||
|
throw makeCodeError('Site origin is missing.', 'BAD_ORIGIN');
|
||||||
|
}
|
||||||
|
if (!isOriginApproved(normalizedOrigin)) {
|
||||||
|
throw makeCodeError('Site is not trusted yet.', 'NOT_TRUSTED');
|
||||||
|
}
|
||||||
|
const cleanPub = String(publicKeyBase58 || '').trim();
|
||||||
|
const cleanTx = String(transactionBase64 || '').trim();
|
||||||
|
if (!cleanPub || !cleanTx) {
|
||||||
|
throw makeCodeError('Transaction payload is incomplete.', 'BAD_REQUEST');
|
||||||
|
}
|
||||||
|
const pending = beginSiteTransactionFlow({
|
||||||
|
origin: normalizedOrigin,
|
||||||
|
publicKeyBase58: cleanPub,
|
||||||
|
comment: String(comment || '').trim(),
|
||||||
|
transactionSummary: transactionSummary || null,
|
||||||
|
});
|
||||||
|
return enqueueSiteApproval(async () => {
|
||||||
|
await activatePendingApproval(getCurrentPendingApproval() || pending, sender);
|
||||||
|
const activePending = getCurrentPendingApproval() || pending;
|
||||||
|
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
|
const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`;
|
||||||
|
try {
|
||||||
|
const { response } = await callWalletRpc({
|
||||||
|
v: 1,
|
||||||
|
operation: 'sign_transaction',
|
||||||
|
requestId,
|
||||||
|
publicKeyBase58: cleanPub,
|
||||||
|
transactionBase64: cleanTx,
|
||||||
|
comment: signComment,
|
||||||
|
}, 120000, activePending.abortController?.signal || null);
|
||||||
|
if (!response?.ok) {
|
||||||
|
const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase();
|
||||||
|
if (errorCode === 'REJECTED_BY_USER') {
|
||||||
|
throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED');
|
||||||
|
}
|
||||||
|
throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED');
|
||||||
|
}
|
||||||
|
setStatus(`Подпись для ${normalizedOrigin} завершена.`, 'info');
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
publicKeyBase58: String(response?.publicKeyBase58 || cleanPub).trim(),
|
||||||
|
signedTransactionBase64: String(response?.signedTransactionBase64 || '').trim(),
|
||||||
|
signatureBase58: String(response?.signatureBase58 || '').trim(),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await markPendingSiteApprovalResolved(activePending.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function snapshot() {
|
function snapshot() {
|
||||||
return {
|
return {
|
||||||
settings: { ...state.settings },
|
settings: { ...state.settings },
|
||||||
@@ -485,8 +844,10 @@ function snapshot() {
|
|||||||
trustedSessionOnline: state.trustedSessionOnline,
|
trustedSessionOnline: state.trustedSessionOnline,
|
||||||
},
|
},
|
||||||
session: state.activeSession ? { ...state.activeSession } : null,
|
session: state.activeSession ? { ...state.activeSession } : null,
|
||||||
connectionOnline: state.connectionOnline,
|
connectionOnline: !!state.activeSession,
|
||||||
walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
|
walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
|
||||||
|
currentWallet: state.currentWallet ? { ...state.currentWallet } : null,
|
||||||
|
pendingApproval: getCurrentPendingApproval() ? makePendingApprovalSnapshot(getCurrentPendingApproval()) : null,
|
||||||
signing: { ...state.signing },
|
signing: { ...state.signing },
|
||||||
status: {
|
status: {
|
||||||
text: state.statusText,
|
text: state.statusText,
|
||||||
@@ -538,8 +899,8 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
sendResponse({ ok: true, result, state: snapshot() });
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === 'wallet:prepareSignSignal') {
|
if (type === 'wallet:requestCurrentWallet') {
|
||||||
const result = await prepareSignSignal();
|
const result = await requestCurrentWallet();
|
||||||
sendResponse({ ok: true, result, state: snapshot() });
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -548,15 +909,45 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
sendResponse({ ok: true, result, state: snapshot() });
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (type === 'wallet:cancelPendingSiteApproval') {
|
||||||
|
const result = await cancelPendingSiteApproval();
|
||||||
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'wallet:siteConnect') {
|
||||||
|
const result = await siteConnect(message?.payload || {});
|
||||||
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'wallet:siteDisconnect') {
|
||||||
|
const result = await siteDisconnect(message?.payload || {});
|
||||||
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'wallet:siteSignTransaction') {
|
||||||
|
const result = await siteSignTransaction(message?.payload || {}, _sender);
|
||||||
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
sendResponse({ ok: false, error: 'UNKNOWN_MESSAGE' });
|
sendResponse({ ok: false, error: 'UNKNOWN_MESSAGE' });
|
||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
const message = toWalletErrorMessage(error, 'Unknown error');
|
const message = toWalletErrorMessage(error, 'Unknown error');
|
||||||
setStatus(message, 'error');
|
setStatus(message, 'error');
|
||||||
sendResponse({ ok: false, error: message, state: snapshot() });
|
sendResponse({ ok: false, error: message, code: String(error?.code || ''), state: snapshot() });
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
void configureSidePanelBehavior();
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
|
void configureSidePanelBehavior();
|
||||||
|
});
|
||||||
|
|
||||||
|
void configureSidePanelBehavior();
|
||||||
|
|
||||||
void loadStateFromStorage().then(async () => {
|
void loadStateFromStorage().then(async () => {
|
||||||
if (state.activeSession?.login) {
|
if (state.activeSession?.login) {
|
||||||
await hydrateWalletProfile(state.activeSession.login).catch(() => {});
|
await hydrateWalletProfile(state.activeSession.login).catch(() => {});
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const PAGE_REQUEST = 'shine-wallet-page-request';
|
||||||
|
const PAGE_RESPONSE = 'shine-wallet-page-response';
|
||||||
|
const PAGE_MESSAGE_TARGET_ORIGIN = '*';
|
||||||
|
|
||||||
|
function injectProviderBridge() {
|
||||||
|
const root = document.head || document.documentElement;
|
||||||
|
if (!root) return;
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.type = 'module';
|
||||||
|
script.src = chrome.runtime.getURL('provider-bridge.js');
|
||||||
|
script.dataset.shineWalletProvider = '1';
|
||||||
|
root.appendChild(script);
|
||||||
|
script.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function respondToPage(id, ok, result, error, code) {
|
||||||
|
window.postMessage({
|
||||||
|
target: PAGE_RESPONSE,
|
||||||
|
id: String(id || ''),
|
||||||
|
ok: !!ok,
|
||||||
|
result: result || null,
|
||||||
|
error: error ? String(error) : '',
|
||||||
|
code: code ? String(code) : '',
|
||||||
|
}, PAGE_MESSAGE_TARGET_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendRuntimeMessage(type, payload = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({ type, payload }, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
const raw = String(chrome.runtime.lastError.message || 'Runtime message failed');
|
||||||
|
if (/Extension context invalidated/i.test(raw)) {
|
||||||
|
const error = new Error('Расширение было перезагружено или отключено. Обновите страницу и откройте кошелёк заново.');
|
||||||
|
error.code = 'EXTENSION_CONTEXT_INVALIDATED';
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(new Error(raw));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response?.ok) {
|
||||||
|
const error = new Error(String(response?.error || 'Wallet operation failed'));
|
||||||
|
error.code = String(response?.code || '');
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.source !== window) return;
|
||||||
|
const data = event.data || {};
|
||||||
|
if (data?.target !== PAGE_REQUEST) return;
|
||||||
|
|
||||||
|
const id = String(data?.id || '');
|
||||||
|
const method = String(data?.method || '');
|
||||||
|
const params = data?.params || {};
|
||||||
|
const origin = window.location.origin;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
if (method === 'connect') {
|
||||||
|
const response = await sendRuntimeMessage('wallet:siteConnect', {
|
||||||
|
origin,
|
||||||
|
onlyIfTrusted: !!params?.onlyIfTrusted,
|
||||||
|
});
|
||||||
|
respondToPage(id, true, response.result || null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method === 'disconnect') {
|
||||||
|
const response = await sendRuntimeMessage('wallet:siteDisconnect', { origin });
|
||||||
|
respondToPage(id, true, response.result || null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method === 'signTransaction') {
|
||||||
|
const response = await sendRuntimeMessage('wallet:siteSignTransaction', {
|
||||||
|
origin,
|
||||||
|
publicKeyBase58: String(params?.publicKeyBase58 || '').trim(),
|
||||||
|
transactionBase64: String(params?.transactionBase64 || '').trim(),
|
||||||
|
comment: String(params?.comment || '').trim(),
|
||||||
|
transactionSummary: params?.transactionSummary || null,
|
||||||
|
});
|
||||||
|
respondToPage(id, true, response.result || null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
respondToPage(id, false, null, 'Unsupported provider method', 'UNSUPPORTED_METHOD');
|
||||||
|
})().catch((error) => {
|
||||||
|
respondToPage(id, false, null, error?.message || 'Wallet bridge error', error?.code || '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
injectProviderBridge();
|
||||||
@@ -75,6 +75,22 @@ export class ShineApiClient {
|
|||||||
return Array.isArray(response?.payload?.sessions) ? response.payload.sessions : [];
|
return Array.isArray(response?.payload?.sessions) ? response.payload.sessions : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async callSignalToSession({ toLogin, targetSessionId, callId, type, data = '' }) {
|
||||||
|
const response = await this.ws.request('CallSignalToSession', {
|
||||||
|
toLogin: String(toLogin || '').trim(),
|
||||||
|
targetSessionId: String(targetSessionId || '').trim(),
|
||||||
|
callId: String(callId || '').trim(),
|
||||||
|
type: Number(type) || 0,
|
||||||
|
data: String(data || ''),
|
||||||
|
});
|
||||||
|
if (response.status !== 200) throw opError('CallSignalToSession', response);
|
||||||
|
return response.payload || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent(op, handler) {
|
||||||
|
return this.ws.on(op, handler);
|
||||||
|
}
|
||||||
|
|
||||||
async resumeSession(sessionRecord) {
|
async resumeSession(sessionRecord) {
|
||||||
const login = String(sessionRecord?.login || '').trim();
|
const login = String(sessionRecord?.login || '').trim();
|
||||||
const sessionId = String(sessionRecord?.sessionId || '').trim();
|
const sessionId = String(sessionRecord?.sessionId || '').trim();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { base64ToBytes } from './crypto-utils.js';
|
import { base64ToBytes } from './crypto-utils.js';
|
||||||
import { PublicKey } from './vendor/solana-publickey-bundle.js';
|
import { PublicKey } from './vendor/solana-publickey-bundle.js';
|
||||||
|
|
||||||
const SOLANA_ENDPOINT_DEFAULT = 'https://api.devnet.solana.com';
|
const SOLANA_ENDPOINT_DEFAULT = 'https://solana-rpc.publicnode.com';
|
||||||
const SHINE_USERS_PROGRAM_ID = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm';
|
const SHINE_USERS_PROGRAM_ID = 'SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6';
|
||||||
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
|
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
|
||||||
const DEFAULT_SHINE_SERVER_LOGIN = 'shineupme';
|
const DEFAULT_SHINE_SERVER_LOGIN = 'shineupme';
|
||||||
const DEFAULT_SHINE_SERVER_ADDRESS = 'shineup.me';
|
const DEFAULT_SHINE_SERVER_ADDRESS = 'shineup.me';
|
||||||
@@ -69,8 +69,9 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
let isServer = false;
|
let isServer = false;
|
||||||
let serverAddress = '';
|
let serverAddress = '';
|
||||||
let accessServers = [];
|
let accessServers = [];
|
||||||
|
let recoveryKey32 = null;
|
||||||
let rootKey32 = null;
|
let rootKey32 = null;
|
||||||
let deviceKey32 = null;
|
let clientKey32 = null;
|
||||||
let blockchainKey32 = null;
|
let blockchainKey32 = null;
|
||||||
let blockchainName = '';
|
let blockchainName = '';
|
||||||
let homeserverSessions = [];
|
let homeserverSessions = [];
|
||||||
@@ -79,10 +80,11 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
const blockType = readU8(bytes, cursorRef);
|
const blockType = readU8(bytes, cursorRef);
|
||||||
cursorRef.value += 1; // block_version
|
cursorRef.value += 1; // block_version
|
||||||
|
|
||||||
if (blockType === 1 || blockType === 2) {
|
if (blockType === 0 || blockType === 1 || blockType === 2) {
|
||||||
const key32 = readBytes(bytes, cursorRef, 32);
|
const key32 = readBytes(bytes, cursorRef, 32);
|
||||||
|
if (blockType === 0) recoveryKey32 = key32;
|
||||||
if (blockType === 1) rootKey32 = key32;
|
if (blockType === 1) rootKey32 = key32;
|
||||||
if (blockType === 2) deviceKey32 = key32;
|
if (blockType === 2) clientKey32 = key32;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (blockType === 3) {
|
if (blockType === 3) {
|
||||||
@@ -150,8 +152,9 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
serverAddress: normalizeHostLike(serverAddress),
|
serverAddress: normalizeHostLike(serverAddress),
|
||||||
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
|
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
|
||||||
publicKeys: {
|
publicKeys: {
|
||||||
|
recoveryKeyBase58: recoveryKey32 ? new PublicKey(recoveryKey32).toBase58() : '',
|
||||||
rootKeyBase58: rootKey32 ? new PublicKey(rootKey32).toBase58() : '',
|
rootKeyBase58: rootKey32 ? new PublicKey(rootKey32).toBase58() : '',
|
||||||
deviceKeyBase58: deviceKey32 ? new PublicKey(deviceKey32).toBase58() : '',
|
clientKeyBase58: clientKey32 ? new PublicKey(clientKey32).toBase58() : '',
|
||||||
blockchainKeyBase58: blockchainKey32 ? new PublicKey(blockchainKey32).toBase58() : '',
|
blockchainKeyBase58: blockchainKey32 ? new PublicKey(blockchainKey32).toBase58() : '',
|
||||||
blockchainName,
|
blockchainName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12136,8 +12136,8 @@ function weierstrass(curveDef) {
|
|||||||
return drbg(seed, k2sig);
|
return drbg(seed, k2sig);
|
||||||
}
|
}
|
||||||
Point2.BASE._setWindowSize(8);
|
Point2.BASE._setWindowSize(8);
|
||||||
function verify2(signature, msgHash, publicKey2, opts = defaultVerOpts) {
|
function verify2(signature2, msgHash, publicKey2, opts = defaultVerOpts) {
|
||||||
const sg = signature;
|
const sg = signature2;
|
||||||
msgHash = ensureBytes("msgHash", msgHash);
|
msgHash = ensureBytes("msgHash", msgHash);
|
||||||
publicKey2 = ensureBytes("publicKey", publicKey2);
|
publicKey2 = ensureBytes("publicKey", publicKey2);
|
||||||
if ("strict" in opts)
|
if ("strict" in opts)
|
||||||
@@ -12515,30 +12515,30 @@ var PACKET_DATA_SIZE = 1280 - 40 - 8;
|
|||||||
var VERSION_PREFIX_MASK = 127;
|
var VERSION_PREFIX_MASK = 127;
|
||||||
var SIGNATURE_LENGTH_IN_BYTES = 64;
|
var SIGNATURE_LENGTH_IN_BYTES = 64;
|
||||||
var TransactionExpiredBlockheightExceededError = class extends Error {
|
var TransactionExpiredBlockheightExceededError = class extends Error {
|
||||||
constructor(signature) {
|
constructor(signature2) {
|
||||||
super(`Signature ${signature} has expired: block height exceeded.`);
|
super(`Signature ${signature2} has expired: block height exceeded.`);
|
||||||
this.signature = void 0;
|
this.signature = void 0;
|
||||||
this.signature = signature;
|
this.signature = signature2;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Object.defineProperty(TransactionExpiredBlockheightExceededError.prototype, "name", {
|
Object.defineProperty(TransactionExpiredBlockheightExceededError.prototype, "name", {
|
||||||
value: "TransactionExpiredBlockheightExceededError"
|
value: "TransactionExpiredBlockheightExceededError"
|
||||||
});
|
});
|
||||||
var TransactionExpiredTimeoutError = class extends Error {
|
var TransactionExpiredTimeoutError = class extends Error {
|
||||||
constructor(signature, timeoutSeconds) {
|
constructor(signature2, timeoutSeconds) {
|
||||||
super(`Transaction was not confirmed in ${timeoutSeconds.toFixed(2)} seconds. It is unknown if it succeeded or failed. Check signature ${signature} using the Solana Explorer or CLI tools.`);
|
super(`Transaction was not confirmed in ${timeoutSeconds.toFixed(2)} seconds. It is unknown if it succeeded or failed. Check signature ${signature2} using the Solana Explorer or CLI tools.`);
|
||||||
this.signature = void 0;
|
this.signature = void 0;
|
||||||
this.signature = signature;
|
this.signature = signature2;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Object.defineProperty(TransactionExpiredTimeoutError.prototype, "name", {
|
Object.defineProperty(TransactionExpiredTimeoutError.prototype, "name", {
|
||||||
value: "TransactionExpiredTimeoutError"
|
value: "TransactionExpiredTimeoutError"
|
||||||
});
|
});
|
||||||
var TransactionExpiredNonceInvalidError = class extends Error {
|
var TransactionExpiredNonceInvalidError = class extends Error {
|
||||||
constructor(signature) {
|
constructor(signature2) {
|
||||||
super(`Signature ${signature} has expired: the nonce is no longer valid.`);
|
super(`Signature ${signature2} has expired: the nonce is no longer valid.`);
|
||||||
this.signature = void 0;
|
this.signature = void 0;
|
||||||
this.signature = signature;
|
this.signature = signature2;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Object.defineProperty(TransactionExpiredNonceInvalidError.prototype, "name", {
|
Object.defineProperty(TransactionExpiredNonceInvalidError.prototype, "name", {
|
||||||
@@ -12598,6 +12598,9 @@ var MessageAccountKeys = class {
|
|||||||
var publicKey = (property = "publicKey") => {
|
var publicKey = (property = "publicKey") => {
|
||||||
return BufferLayout.blob(32, property);
|
return BufferLayout.blob(32, property);
|
||||||
};
|
};
|
||||||
|
var signature = (property = "signature") => {
|
||||||
|
return BufferLayout.blob(64, property);
|
||||||
|
};
|
||||||
var rustString = (property = "string") => {
|
var rustString = (property = "string") => {
|
||||||
const rsl = BufferLayout.struct([BufferLayout.u32("length"), BufferLayout.u32("lengthPadding"), BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), "chars")], property);
|
const rsl = BufferLayout.struct([BufferLayout.u32("length"), BufferLayout.u32("lengthPadding"), BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), "chars")], property);
|
||||||
const _decode = rsl.decode.bind(rsl);
|
const _decode = rsl.decode.bind(rsl);
|
||||||
@@ -12954,6 +12957,260 @@ var Message = class _Message {
|
|||||||
return new _Message(messageArgs);
|
return new _Message(messageArgs);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var MessageV0 = class _MessageV0 {
|
||||||
|
constructor(args) {
|
||||||
|
this.header = void 0;
|
||||||
|
this.staticAccountKeys = void 0;
|
||||||
|
this.recentBlockhash = void 0;
|
||||||
|
this.compiledInstructions = void 0;
|
||||||
|
this.addressTableLookups = void 0;
|
||||||
|
this.header = args.header;
|
||||||
|
this.staticAccountKeys = args.staticAccountKeys;
|
||||||
|
this.recentBlockhash = args.recentBlockhash;
|
||||||
|
this.compiledInstructions = args.compiledInstructions;
|
||||||
|
this.addressTableLookups = args.addressTableLookups;
|
||||||
|
}
|
||||||
|
get version() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
get numAccountKeysFromLookups() {
|
||||||
|
let count = 0;
|
||||||
|
for (const lookup of this.addressTableLookups) {
|
||||||
|
count += lookup.readonlyIndexes.length + lookup.writableIndexes.length;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
getAccountKeys(args) {
|
||||||
|
let accountKeysFromLookups;
|
||||||
|
if (args && "accountKeysFromLookups" in args && args.accountKeysFromLookups) {
|
||||||
|
if (this.numAccountKeysFromLookups != args.accountKeysFromLookups.writable.length + args.accountKeysFromLookups.readonly.length) {
|
||||||
|
throw new Error("Failed to get account keys because of a mismatch in the number of account keys from lookups");
|
||||||
|
}
|
||||||
|
accountKeysFromLookups = args.accountKeysFromLookups;
|
||||||
|
} else if (args && "addressLookupTableAccounts" in args && args.addressLookupTableAccounts) {
|
||||||
|
accountKeysFromLookups = this.resolveAddressTableLookups(args.addressLookupTableAccounts);
|
||||||
|
} else if (this.addressTableLookups.length > 0) {
|
||||||
|
throw new Error("Failed to get account keys because address table lookups were not resolved");
|
||||||
|
}
|
||||||
|
return new MessageAccountKeys(this.staticAccountKeys, accountKeysFromLookups);
|
||||||
|
}
|
||||||
|
isAccountSigner(index) {
|
||||||
|
return index < this.header.numRequiredSignatures;
|
||||||
|
}
|
||||||
|
isAccountWritable(index) {
|
||||||
|
const numSignedAccounts = this.header.numRequiredSignatures;
|
||||||
|
const numStaticAccountKeys = this.staticAccountKeys.length;
|
||||||
|
if (index >= numStaticAccountKeys) {
|
||||||
|
const lookupAccountKeysIndex = index - numStaticAccountKeys;
|
||||||
|
const numWritableLookupAccountKeys = this.addressTableLookups.reduce((count, lookup) => count + lookup.writableIndexes.length, 0);
|
||||||
|
return lookupAccountKeysIndex < numWritableLookupAccountKeys;
|
||||||
|
} else if (index >= this.header.numRequiredSignatures) {
|
||||||
|
const unsignedAccountIndex = index - numSignedAccounts;
|
||||||
|
const numUnsignedAccounts = numStaticAccountKeys - numSignedAccounts;
|
||||||
|
const numWritableUnsignedAccounts = numUnsignedAccounts - this.header.numReadonlyUnsignedAccounts;
|
||||||
|
return unsignedAccountIndex < numWritableUnsignedAccounts;
|
||||||
|
} else {
|
||||||
|
const numWritableSignedAccounts = numSignedAccounts - this.header.numReadonlySignedAccounts;
|
||||||
|
return index < numWritableSignedAccounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolveAddressTableLookups(addressLookupTableAccounts) {
|
||||||
|
const accountKeysFromLookups = {
|
||||||
|
writable: [],
|
||||||
|
readonly: []
|
||||||
|
};
|
||||||
|
for (const tableLookup of this.addressTableLookups) {
|
||||||
|
const tableAccount = addressLookupTableAccounts.find((account) => account.key.equals(tableLookup.accountKey));
|
||||||
|
if (!tableAccount) {
|
||||||
|
throw new Error(`Failed to find address lookup table account for table key ${tableLookup.accountKey.toBase58()}`);
|
||||||
|
}
|
||||||
|
for (const index of tableLookup.writableIndexes) {
|
||||||
|
if (index < tableAccount.state.addresses.length) {
|
||||||
|
accountKeysFromLookups.writable.push(tableAccount.state.addresses[index]);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to find address for index ${index} in address lookup table ${tableLookup.accountKey.toBase58()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const index of tableLookup.readonlyIndexes) {
|
||||||
|
if (index < tableAccount.state.addresses.length) {
|
||||||
|
accountKeysFromLookups.readonly.push(tableAccount.state.addresses[index]);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to find address for index ${index} in address lookup table ${tableLookup.accountKey.toBase58()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accountKeysFromLookups;
|
||||||
|
}
|
||||||
|
static compile(args) {
|
||||||
|
const compiledKeys = CompiledKeys.compile(args.instructions, args.payerKey);
|
||||||
|
const addressTableLookups = new Array();
|
||||||
|
const accountKeysFromLookups = {
|
||||||
|
writable: new Array(),
|
||||||
|
readonly: new Array()
|
||||||
|
};
|
||||||
|
const lookupTableAccounts = args.addressLookupTableAccounts || [];
|
||||||
|
for (const lookupTable of lookupTableAccounts) {
|
||||||
|
const extractResult = compiledKeys.extractTableLookup(lookupTable);
|
||||||
|
if (extractResult !== void 0) {
|
||||||
|
const [addressTableLookup, {
|
||||||
|
writable,
|
||||||
|
readonly
|
||||||
|
}] = extractResult;
|
||||||
|
addressTableLookups.push(addressTableLookup);
|
||||||
|
accountKeysFromLookups.writable.push(...writable);
|
||||||
|
accountKeysFromLookups.readonly.push(...readonly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [header, staticAccountKeys] = compiledKeys.getMessageComponents();
|
||||||
|
const accountKeys = new MessageAccountKeys(staticAccountKeys, accountKeysFromLookups);
|
||||||
|
const compiledInstructions = accountKeys.compileInstructions(args.instructions);
|
||||||
|
return new _MessageV0({
|
||||||
|
header,
|
||||||
|
staticAccountKeys,
|
||||||
|
recentBlockhash: args.recentBlockhash,
|
||||||
|
compiledInstructions,
|
||||||
|
addressTableLookups
|
||||||
|
});
|
||||||
|
}
|
||||||
|
serialize() {
|
||||||
|
const encodedStaticAccountKeysLength = Array();
|
||||||
|
encodeLength(encodedStaticAccountKeysLength, this.staticAccountKeys.length);
|
||||||
|
const serializedInstructions = this.serializeInstructions();
|
||||||
|
const encodedInstructionsLength = Array();
|
||||||
|
encodeLength(encodedInstructionsLength, this.compiledInstructions.length);
|
||||||
|
const serializedAddressTableLookups = this.serializeAddressTableLookups();
|
||||||
|
const encodedAddressTableLookupsLength = Array();
|
||||||
|
encodeLength(encodedAddressTableLookupsLength, this.addressTableLookups.length);
|
||||||
|
const messageLayout = BufferLayout.struct([BufferLayout.u8("prefix"), BufferLayout.struct([BufferLayout.u8("numRequiredSignatures"), BufferLayout.u8("numReadonlySignedAccounts"), BufferLayout.u8("numReadonlyUnsignedAccounts")], "header"), BufferLayout.blob(encodedStaticAccountKeysLength.length, "staticAccountKeysLength"), BufferLayout.seq(publicKey(), this.staticAccountKeys.length, "staticAccountKeys"), publicKey("recentBlockhash"), BufferLayout.blob(encodedInstructionsLength.length, "instructionsLength"), BufferLayout.blob(serializedInstructions.length, "serializedInstructions"), BufferLayout.blob(encodedAddressTableLookupsLength.length, "addressTableLookupsLength"), BufferLayout.blob(serializedAddressTableLookups.length, "serializedAddressTableLookups")]);
|
||||||
|
const serializedMessage = new Uint8Array(PACKET_DATA_SIZE);
|
||||||
|
const MESSAGE_VERSION_0_PREFIX = 1 << 7;
|
||||||
|
const serializedMessageLength = messageLayout.encode({
|
||||||
|
prefix: MESSAGE_VERSION_0_PREFIX,
|
||||||
|
header: this.header,
|
||||||
|
staticAccountKeysLength: new Uint8Array(encodedStaticAccountKeysLength),
|
||||||
|
staticAccountKeys: this.staticAccountKeys.map((key) => key.toBytes()),
|
||||||
|
recentBlockhash: import_bs58.default.decode(this.recentBlockhash),
|
||||||
|
instructionsLength: new Uint8Array(encodedInstructionsLength),
|
||||||
|
serializedInstructions,
|
||||||
|
addressTableLookupsLength: new Uint8Array(encodedAddressTableLookupsLength),
|
||||||
|
serializedAddressTableLookups
|
||||||
|
}, serializedMessage);
|
||||||
|
return serializedMessage.slice(0, serializedMessageLength);
|
||||||
|
}
|
||||||
|
serializeInstructions() {
|
||||||
|
let serializedLength = 0;
|
||||||
|
const serializedInstructions = new Uint8Array(PACKET_DATA_SIZE);
|
||||||
|
for (const instruction of this.compiledInstructions) {
|
||||||
|
const encodedAccountKeyIndexesLength = Array();
|
||||||
|
encodeLength(encodedAccountKeyIndexesLength, instruction.accountKeyIndexes.length);
|
||||||
|
const encodedDataLength = Array();
|
||||||
|
encodeLength(encodedDataLength, instruction.data.length);
|
||||||
|
const instructionLayout = BufferLayout.struct([BufferLayout.u8("programIdIndex"), BufferLayout.blob(encodedAccountKeyIndexesLength.length, "encodedAccountKeyIndexesLength"), BufferLayout.seq(BufferLayout.u8(), instruction.accountKeyIndexes.length, "accountKeyIndexes"), BufferLayout.blob(encodedDataLength.length, "encodedDataLength"), BufferLayout.blob(instruction.data.length, "data")]);
|
||||||
|
serializedLength += instructionLayout.encode({
|
||||||
|
programIdIndex: instruction.programIdIndex,
|
||||||
|
encodedAccountKeyIndexesLength: new Uint8Array(encodedAccountKeyIndexesLength),
|
||||||
|
accountKeyIndexes: instruction.accountKeyIndexes,
|
||||||
|
encodedDataLength: new Uint8Array(encodedDataLength),
|
||||||
|
data: instruction.data
|
||||||
|
}, serializedInstructions, serializedLength);
|
||||||
|
}
|
||||||
|
return serializedInstructions.slice(0, serializedLength);
|
||||||
|
}
|
||||||
|
serializeAddressTableLookups() {
|
||||||
|
let serializedLength = 0;
|
||||||
|
const serializedAddressTableLookups = new Uint8Array(PACKET_DATA_SIZE);
|
||||||
|
for (const lookup of this.addressTableLookups) {
|
||||||
|
const encodedWritableIndexesLength = Array();
|
||||||
|
encodeLength(encodedWritableIndexesLength, lookup.writableIndexes.length);
|
||||||
|
const encodedReadonlyIndexesLength = Array();
|
||||||
|
encodeLength(encodedReadonlyIndexesLength, lookup.readonlyIndexes.length);
|
||||||
|
const addressTableLookupLayout = BufferLayout.struct([publicKey("accountKey"), BufferLayout.blob(encodedWritableIndexesLength.length, "encodedWritableIndexesLength"), BufferLayout.seq(BufferLayout.u8(), lookup.writableIndexes.length, "writableIndexes"), BufferLayout.blob(encodedReadonlyIndexesLength.length, "encodedReadonlyIndexesLength"), BufferLayout.seq(BufferLayout.u8(), lookup.readonlyIndexes.length, "readonlyIndexes")]);
|
||||||
|
serializedLength += addressTableLookupLayout.encode({
|
||||||
|
accountKey: lookup.accountKey.toBytes(),
|
||||||
|
encodedWritableIndexesLength: new Uint8Array(encodedWritableIndexesLength),
|
||||||
|
writableIndexes: lookup.writableIndexes,
|
||||||
|
encodedReadonlyIndexesLength: new Uint8Array(encodedReadonlyIndexesLength),
|
||||||
|
readonlyIndexes: lookup.readonlyIndexes
|
||||||
|
}, serializedAddressTableLookups, serializedLength);
|
||||||
|
}
|
||||||
|
return serializedAddressTableLookups.slice(0, serializedLength);
|
||||||
|
}
|
||||||
|
static deserialize(serializedMessage) {
|
||||||
|
let byteArray = [...serializedMessage];
|
||||||
|
const prefix = guardedShift(byteArray);
|
||||||
|
const maskedPrefix = prefix & VERSION_PREFIX_MASK;
|
||||||
|
assert2(prefix !== maskedPrefix, `Expected versioned message but received legacy message`);
|
||||||
|
const version2 = maskedPrefix;
|
||||||
|
assert2(version2 === 0, `Expected versioned message with version 0 but found version ${version2}`);
|
||||||
|
const header = {
|
||||||
|
numRequiredSignatures: guardedShift(byteArray),
|
||||||
|
numReadonlySignedAccounts: guardedShift(byteArray),
|
||||||
|
numReadonlyUnsignedAccounts: guardedShift(byteArray)
|
||||||
|
};
|
||||||
|
const staticAccountKeys = [];
|
||||||
|
const staticAccountKeysLength = decodeLength(byteArray);
|
||||||
|
for (let i2 = 0; i2 < staticAccountKeysLength; i2++) {
|
||||||
|
staticAccountKeys.push(new PublicKey(guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH)));
|
||||||
|
}
|
||||||
|
const recentBlockhash = import_bs58.default.encode(guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH));
|
||||||
|
const instructionCount = decodeLength(byteArray);
|
||||||
|
const compiledInstructions = [];
|
||||||
|
for (let i2 = 0; i2 < instructionCount; i2++) {
|
||||||
|
const programIdIndex = guardedShift(byteArray);
|
||||||
|
const accountKeyIndexesLength = decodeLength(byteArray);
|
||||||
|
const accountKeyIndexes = guardedSplice(byteArray, 0, accountKeyIndexesLength);
|
||||||
|
const dataLength = decodeLength(byteArray);
|
||||||
|
const data = new Uint8Array(guardedSplice(byteArray, 0, dataLength));
|
||||||
|
compiledInstructions.push({
|
||||||
|
programIdIndex,
|
||||||
|
accountKeyIndexes,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const addressTableLookupsCount = decodeLength(byteArray);
|
||||||
|
const addressTableLookups = [];
|
||||||
|
for (let i2 = 0; i2 < addressTableLookupsCount; i2++) {
|
||||||
|
const accountKey = new PublicKey(guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH));
|
||||||
|
const writableIndexesLength = decodeLength(byteArray);
|
||||||
|
const writableIndexes = guardedSplice(byteArray, 0, writableIndexesLength);
|
||||||
|
const readonlyIndexesLength = decodeLength(byteArray);
|
||||||
|
const readonlyIndexes = guardedSplice(byteArray, 0, readonlyIndexesLength);
|
||||||
|
addressTableLookups.push({
|
||||||
|
accountKey,
|
||||||
|
writableIndexes,
|
||||||
|
readonlyIndexes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new _MessageV0({
|
||||||
|
header,
|
||||||
|
staticAccountKeys,
|
||||||
|
recentBlockhash,
|
||||||
|
compiledInstructions,
|
||||||
|
addressTableLookups
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var VersionedMessage = {
|
||||||
|
deserializeMessageVersion(serializedMessage) {
|
||||||
|
const prefix = serializedMessage[0];
|
||||||
|
const maskedPrefix = prefix & VERSION_PREFIX_MASK;
|
||||||
|
if (maskedPrefix === prefix) {
|
||||||
|
return "legacy";
|
||||||
|
}
|
||||||
|
return maskedPrefix;
|
||||||
|
},
|
||||||
|
deserialize: (serializedMessage) => {
|
||||||
|
const version2 = VersionedMessage.deserializeMessageVersion(serializedMessage);
|
||||||
|
if (version2 === "legacy") {
|
||||||
|
return Message.from(serializedMessage);
|
||||||
|
}
|
||||||
|
if (version2 === 0) {
|
||||||
|
return MessageV0.deserialize(serializedMessage);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Transaction message version ${version2} deserialization is not supported`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
var DEFAULT_SIGNATURE = import_buffer2.Buffer.alloc(SIGNATURE_LENGTH_IN_BYTES).fill(0);
|
var DEFAULT_SIGNATURE = import_buffer2.Buffer.alloc(SIGNATURE_LENGTH_IN_BYTES).fill(0);
|
||||||
var TransactionInstruction = class {
|
var TransactionInstruction = class {
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
@@ -13196,9 +13453,9 @@ var Transaction = class _Transaction {
|
|||||||
isWritable: true
|
isWritable: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const signature of this.signatures) {
|
for (const signature2 of this.signatures) {
|
||||||
const uniqueIndex = uniqueMetas.findIndex((x) => {
|
const uniqueIndex = uniqueMetas.findIndex((x) => {
|
||||||
return x.pubkey.equals(signature.publicKey);
|
return x.pubkey.equals(signature2.publicKey);
|
||||||
});
|
});
|
||||||
if (uniqueIndex > -1) {
|
if (uniqueIndex > -1) {
|
||||||
if (!uniqueMetas[uniqueIndex].isSigner) {
|
if (!uniqueMetas[uniqueIndex].isSigner) {
|
||||||
@@ -13206,7 +13463,7 @@ var Transaction = class _Transaction {
|
|||||||
console.warn("Transaction references a signature that is unnecessary, only the fee payer and instruction signer accounts should sign a transaction. This behavior is deprecated and will throw an error in the next major version release.");
|
console.warn("Transaction references a signature that is unnecessary, only the fee payer and instruction signer accounts should sign a transaction. This behavior is deprecated and will throw an error in the next major version release.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`unknown signer: ${signature.publicKey.toString()}`);
|
throw new Error(`unknown signer: ${signature2.publicKey.toString()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let numRequiredSignatures = 0;
|
let numRequiredSignatures = 0;
|
||||||
@@ -13392,8 +13649,8 @@ var Transaction = class _Transaction {
|
|||||||
_partialSign(message, ...signers) {
|
_partialSign(message, ...signers) {
|
||||||
const signData = message.serialize();
|
const signData = message.serialize();
|
||||||
signers.forEach((signer) => {
|
signers.forEach((signer) => {
|
||||||
const signature = sign(signData, signer.secretKey);
|
const signature2 = sign(signData, signer.secretKey);
|
||||||
this._addSignature(signer.publicKey, toBuffer(signature));
|
this._addSignature(signer.publicKey, toBuffer(signature2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -13404,20 +13661,20 @@ var Transaction = class _Transaction {
|
|||||||
* @param {PublicKey} pubkey Public key that will be added to the transaction.
|
* @param {PublicKey} pubkey Public key that will be added to the transaction.
|
||||||
* @param {Buffer} signature An externally created signature to add to the transaction.
|
* @param {Buffer} signature An externally created signature to add to the transaction.
|
||||||
*/
|
*/
|
||||||
addSignature(pubkey, signature) {
|
addSignature(pubkey, signature2) {
|
||||||
this._compile();
|
this._compile();
|
||||||
this._addSignature(pubkey, signature);
|
this._addSignature(pubkey, signature2);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_addSignature(pubkey, signature) {
|
_addSignature(pubkey, signature2) {
|
||||||
assert2(signature.length === 64);
|
assert2(signature2.length === 64);
|
||||||
const index = this.signatures.findIndex((sigpair) => pubkey.equals(sigpair.publicKey));
|
const index = this.signatures.findIndex((sigpair) => pubkey.equals(sigpair.publicKey));
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new Error(`unknown signer: ${pubkey.toString()}`);
|
throw new Error(`unknown signer: ${pubkey.toString()}`);
|
||||||
}
|
}
|
||||||
this.signatures[index].signature = import_buffer2.Buffer.from(signature);
|
this.signatures[index].signature = import_buffer2.Buffer.from(signature2);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Verify signatures of a Transaction
|
* Verify signatures of a Transaction
|
||||||
@@ -13436,15 +13693,15 @@ var Transaction = class _Transaction {
|
|||||||
_getMessageSignednessErrors(message, requireAllSignatures) {
|
_getMessageSignednessErrors(message, requireAllSignatures) {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
for (const {
|
for (const {
|
||||||
signature,
|
signature: signature2,
|
||||||
publicKey: publicKey2
|
publicKey: publicKey2
|
||||||
} of this.signatures) {
|
} of this.signatures) {
|
||||||
if (signature === null) {
|
if (signature2 === null) {
|
||||||
if (requireAllSignatures) {
|
if (requireAllSignatures) {
|
||||||
(errors.missing ||= []).push(publicKey2);
|
(errors.missing ||= []).push(publicKey2);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!verify(signature, message, publicKey2.toBytes())) {
|
if (!verify(signature2, message, publicKey2.toBytes())) {
|
||||||
(errors.invalid ||= []).push(publicKey2);
|
(errors.invalid ||= []).push(publicKey2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13498,11 +13755,11 @@ Missing signature for public key${sigErrors.missing.length === 1 ? "" : "(s)"} [
|
|||||||
assert2(signatures.length < 256);
|
assert2(signatures.length < 256);
|
||||||
import_buffer2.Buffer.from(signatureCount).copy(wireTransaction, 0);
|
import_buffer2.Buffer.from(signatureCount).copy(wireTransaction, 0);
|
||||||
signatures.forEach(({
|
signatures.forEach(({
|
||||||
signature
|
signature: signature2
|
||||||
}, index) => {
|
}, index) => {
|
||||||
if (signature !== null) {
|
if (signature2 !== null) {
|
||||||
assert2(signature.length === 64, `signature has invalid length`);
|
assert2(signature2.length === 64, `signature has invalid length`);
|
||||||
import_buffer2.Buffer.from(signature).copy(wireTransaction, signatureCount.length + index * 64);
|
import_buffer2.Buffer.from(signature2).copy(wireTransaction, signatureCount.length + index * 64);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
signData.copy(wireTransaction, signatureCount.length + signatures.length * 64);
|
signData.copy(wireTransaction, signatureCount.length + signatures.length * 64);
|
||||||
@@ -13545,8 +13802,8 @@ Missing signature for public key${sigErrors.missing.length === 1 ? "" : "(s)"} [
|
|||||||
const signatureCount = decodeLength(byteArray);
|
const signatureCount = decodeLength(byteArray);
|
||||||
let signatures = [];
|
let signatures = [];
|
||||||
for (let i2 = 0; i2 < signatureCount; i2++) {
|
for (let i2 = 0; i2 < signatureCount; i2++) {
|
||||||
const signature = guardedSplice(byteArray, 0, SIGNATURE_LENGTH_IN_BYTES);
|
const signature2 = guardedSplice(byteArray, 0, SIGNATURE_LENGTH_IN_BYTES);
|
||||||
signatures.push(import_bs58.default.encode(import_buffer2.Buffer.from(signature)));
|
signatures.push(import_bs58.default.encode(import_buffer2.Buffer.from(signature2)));
|
||||||
}
|
}
|
||||||
return _Transaction.populate(Message.from(byteArray), signatures);
|
return _Transaction.populate(Message.from(byteArray), signatures);
|
||||||
}
|
}
|
||||||
@@ -13564,9 +13821,9 @@ Missing signature for public key${sigErrors.missing.length === 1 ? "" : "(s)"} [
|
|||||||
if (message.header.numRequiredSignatures > 0) {
|
if (message.header.numRequiredSignatures > 0) {
|
||||||
transaction.feePayer = message.accountKeys[0];
|
transaction.feePayer = message.accountKeys[0];
|
||||||
}
|
}
|
||||||
signatures.forEach((signature, index) => {
|
signatures.forEach((signature2, index) => {
|
||||||
const sigPubkeyPair = {
|
const sigPubkeyPair = {
|
||||||
signature: signature == import_bs58.default.encode(DEFAULT_SIGNATURE) ? null : import_bs58.default.decode(signature),
|
signature: signature2 == import_bs58.default.encode(DEFAULT_SIGNATURE) ? null : import_bs58.default.decode(signature2),
|
||||||
publicKey: message.accountKeys[index]
|
publicKey: message.accountKeys[index]
|
||||||
};
|
};
|
||||||
transaction.signatures.push(sigPubkeyPair);
|
transaction.signatures.push(sigPubkeyPair);
|
||||||
@@ -13591,6 +13848,65 @@ Missing signature for public key${sigErrors.missing.length === 1 ? "" : "(s)"} [
|
|||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var VersionedTransaction = class _VersionedTransaction {
|
||||||
|
get version() {
|
||||||
|
return this.message.version;
|
||||||
|
}
|
||||||
|
constructor(message, signatures) {
|
||||||
|
this.signatures = void 0;
|
||||||
|
this.message = void 0;
|
||||||
|
if (signatures !== void 0) {
|
||||||
|
assert2(signatures.length === message.header.numRequiredSignatures, "Expected signatures length to be equal to the number of required signatures");
|
||||||
|
this.signatures = signatures;
|
||||||
|
} else {
|
||||||
|
const defaultSignatures = [];
|
||||||
|
for (let i2 = 0; i2 < message.header.numRequiredSignatures; i2++) {
|
||||||
|
defaultSignatures.push(new Uint8Array(SIGNATURE_LENGTH_IN_BYTES));
|
||||||
|
}
|
||||||
|
this.signatures = defaultSignatures;
|
||||||
|
}
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
serialize() {
|
||||||
|
const serializedMessage = this.message.serialize();
|
||||||
|
const encodedSignaturesLength = Array();
|
||||||
|
encodeLength(encodedSignaturesLength, this.signatures.length);
|
||||||
|
const transactionLayout = BufferLayout.struct([BufferLayout.blob(encodedSignaturesLength.length, "encodedSignaturesLength"), BufferLayout.seq(signature(), this.signatures.length, "signatures"), BufferLayout.blob(serializedMessage.length, "serializedMessage")]);
|
||||||
|
const serializedTransaction = new Uint8Array(2048);
|
||||||
|
const serializedTransactionLength = transactionLayout.encode({
|
||||||
|
encodedSignaturesLength: new Uint8Array(encodedSignaturesLength),
|
||||||
|
signatures: this.signatures,
|
||||||
|
serializedMessage
|
||||||
|
}, serializedTransaction);
|
||||||
|
return serializedTransaction.slice(0, serializedTransactionLength);
|
||||||
|
}
|
||||||
|
static deserialize(serializedTransaction) {
|
||||||
|
let byteArray = [...serializedTransaction];
|
||||||
|
const signatures = [];
|
||||||
|
const signaturesLength = decodeLength(byteArray);
|
||||||
|
for (let i2 = 0; i2 < signaturesLength; i2++) {
|
||||||
|
signatures.push(new Uint8Array(guardedSplice(byteArray, 0, SIGNATURE_LENGTH_IN_BYTES)));
|
||||||
|
}
|
||||||
|
const message = VersionedMessage.deserialize(new Uint8Array(byteArray));
|
||||||
|
return new _VersionedTransaction(message, signatures);
|
||||||
|
}
|
||||||
|
sign(signers) {
|
||||||
|
const messageData = this.message.serialize();
|
||||||
|
const signerPubkeys = this.message.staticAccountKeys.slice(0, this.message.header.numRequiredSignatures);
|
||||||
|
for (const signer of signers) {
|
||||||
|
const signerIndex = signerPubkeys.findIndex((pubkey) => pubkey.equals(signer.publicKey));
|
||||||
|
assert2(signerIndex >= 0, `Cannot sign with non signer key ${signer.publicKey.toBase58()}`);
|
||||||
|
this.signatures[signerIndex] = sign(messageData, signer.secretKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addSignature(publicKey2, signature2) {
|
||||||
|
assert2(signature2.byteLength === 64, "Signature must be 64 bytes long");
|
||||||
|
const signerPubkeys = this.message.staticAccountKeys.slice(0, this.message.header.numRequiredSignatures);
|
||||||
|
const signerIndex = signerPubkeys.findIndex((pubkey) => pubkey.equals(publicKey2));
|
||||||
|
assert2(signerIndex >= 0, `Can not add signature; \`${publicKey2.toBase58()}\` is not required to sign this transaction`);
|
||||||
|
this.signatures[signerIndex] = signature2;
|
||||||
|
}
|
||||||
|
};
|
||||||
var NUM_TICKS_PER_SECOND = 160;
|
var NUM_TICKS_PER_SECOND = 160;
|
||||||
var DEFAULT_TICKS_PER_SLOT = 64;
|
var DEFAULT_TICKS_PER_SLOT = 64;
|
||||||
var NUM_SLOTS_PER_SECOND = NUM_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT;
|
var NUM_SLOTS_PER_SECOND = NUM_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT;
|
||||||
@@ -13607,7 +13923,7 @@ var SYSVAR_STAKE_HISTORY_PUBKEY = new PublicKey("SysvarStakeHistory1111111111111
|
|||||||
var SendTransactionError = class extends Error {
|
var SendTransactionError = class extends Error {
|
||||||
constructor({
|
constructor({
|
||||||
action,
|
action,
|
||||||
signature,
|
signature: signature2,
|
||||||
transactionMessage,
|
transactionMessage,
|
||||||
logs
|
logs
|
||||||
}) {
|
}) {
|
||||||
@@ -13617,7 +13933,7 @@ ${JSON.stringify(logs.slice(-10), null, 2)}. ` : "";
|
|||||||
let message;
|
let message;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "send":
|
case "send":
|
||||||
message = `Transaction ${signature} resulted in an error.
|
message = `Transaction ${signature2} resulted in an error.
|
||||||
${transactionMessage}. ` + maybeLogsOutput + guideText;
|
${transactionMessage}. ` + maybeLogsOutput + guideText;
|
||||||
break;
|
break;
|
||||||
case "simulate":
|
case "simulate":
|
||||||
@@ -13633,7 +13949,7 @@ Message: ${transactionMessage}.
|
|||||||
this.signature = void 0;
|
this.signature = void 0;
|
||||||
this.transactionMessage = void 0;
|
this.transactionMessage = void 0;
|
||||||
this.transactionLogs = void 0;
|
this.transactionLogs = void 0;
|
||||||
this.signature = signature;
|
this.signature = signature2;
|
||||||
this.transactionMessage = transactionMessage;
|
this.transactionMessage = transactionMessage;
|
||||||
this.transactionLogs = logs ? logs : void 0;
|
this.transactionLogs = logs ? logs : void 0;
|
||||||
}
|
}
|
||||||
@@ -13675,12 +13991,12 @@ async function sendAndConfirmTransaction(connection, transaction, signers, optio
|
|||||||
maxRetries: options.maxRetries,
|
maxRetries: options.maxRetries,
|
||||||
minContextSlot: options.minContextSlot
|
minContextSlot: options.minContextSlot
|
||||||
};
|
};
|
||||||
const signature = await connection.sendTransaction(transaction, signers, sendOptions);
|
const signature2 = await connection.sendTransaction(transaction, signers, sendOptions);
|
||||||
let status;
|
let status;
|
||||||
if (transaction.recentBlockhash != null && transaction.lastValidBlockHeight != null) {
|
if (transaction.recentBlockhash != null && transaction.lastValidBlockHeight != null) {
|
||||||
status = (await connection.confirmTransaction({
|
status = (await connection.confirmTransaction({
|
||||||
abortSignal: options?.abortSignal,
|
abortSignal: options?.abortSignal,
|
||||||
signature,
|
signature: signature2,
|
||||||
blockhash: transaction.recentBlockhash,
|
blockhash: transaction.recentBlockhash,
|
||||||
lastValidBlockHeight: transaction.lastValidBlockHeight
|
lastValidBlockHeight: transaction.lastValidBlockHeight
|
||||||
}, options && options.commitment)).value;
|
}, options && options.commitment)).value;
|
||||||
@@ -13694,25 +14010,25 @@ async function sendAndConfirmTransaction(connection, transaction, signers, optio
|
|||||||
minContextSlot: transaction.minNonceContextSlot,
|
minContextSlot: transaction.minNonceContextSlot,
|
||||||
nonceAccountPubkey,
|
nonceAccountPubkey,
|
||||||
nonceValue: transaction.nonceInfo.nonce,
|
nonceValue: transaction.nonceInfo.nonce,
|
||||||
signature
|
signature: signature2
|
||||||
}, options && options.commitment)).value;
|
}, options && options.commitment)).value;
|
||||||
} else {
|
} else {
|
||||||
if (options?.abortSignal != null) {
|
if (options?.abortSignal != null) {
|
||||||
console.warn("sendAndConfirmTransaction(): A transaction with a deprecated confirmation strategy was supplied along with an `abortSignal`. Only transactions having `lastValidBlockHeight` or a combination of `nonceInfo` and `minNonceContextSlot` are abortable.");
|
console.warn("sendAndConfirmTransaction(): A transaction with a deprecated confirmation strategy was supplied along with an `abortSignal`. Only transactions having `lastValidBlockHeight` or a combination of `nonceInfo` and `minNonceContextSlot` are abortable.");
|
||||||
}
|
}
|
||||||
status = (await connection.confirmTransaction(signature, options && options.commitment)).value;
|
status = (await connection.confirmTransaction(signature2, options && options.commitment)).value;
|
||||||
}
|
}
|
||||||
if (status.err) {
|
if (status.err) {
|
||||||
if (signature != null) {
|
if (signature2 != null) {
|
||||||
throw new SendTransactionError({
|
throw new SendTransactionError({
|
||||||
action: "send",
|
action: "send",
|
||||||
signature,
|
signature: signature2,
|
||||||
transactionMessage: `Status: (${JSON.stringify(status)})`
|
transactionMessage: `Status: (${JSON.stringify(status)})`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw new Error(`Transaction ${signature} failed (${JSON.stringify(status)})`);
|
throw new Error(`Transaction ${signature2} failed (${JSON.stringify(status)})`);
|
||||||
}
|
}
|
||||||
return signature;
|
return signature2;
|
||||||
}
|
}
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -15218,14 +15534,14 @@ var Ed25519Program = class _Ed25519Program {
|
|||||||
const {
|
const {
|
||||||
publicKey: publicKey2,
|
publicKey: publicKey2,
|
||||||
message,
|
message,
|
||||||
signature,
|
signature: signature2,
|
||||||
instructionIndex
|
instructionIndex
|
||||||
} = params;
|
} = params;
|
||||||
assert2(publicKey2.length === PUBLIC_KEY_BYTES$1, `Public Key must be ${PUBLIC_KEY_BYTES$1} bytes but received ${publicKey2.length} bytes`);
|
assert2(publicKey2.length === PUBLIC_KEY_BYTES$1, `Public Key must be ${PUBLIC_KEY_BYTES$1} bytes but received ${publicKey2.length} bytes`);
|
||||||
assert2(signature.length === SIGNATURE_BYTES, `Signature must be ${SIGNATURE_BYTES} bytes but received ${signature.length} bytes`);
|
assert2(signature2.length === SIGNATURE_BYTES, `Signature must be ${SIGNATURE_BYTES} bytes but received ${signature2.length} bytes`);
|
||||||
const publicKeyOffset = ED25519_INSTRUCTION_LAYOUT.span;
|
const publicKeyOffset = ED25519_INSTRUCTION_LAYOUT.span;
|
||||||
const signatureOffset = publicKeyOffset + publicKey2.length;
|
const signatureOffset = publicKeyOffset + publicKey2.length;
|
||||||
const messageDataOffset = signatureOffset + signature.length;
|
const messageDataOffset = signatureOffset + signature2.length;
|
||||||
const numSignatures = 1;
|
const numSignatures = 1;
|
||||||
const instructionData = import_buffer2.Buffer.alloc(messageDataOffset + message.length);
|
const instructionData = import_buffer2.Buffer.alloc(messageDataOffset + message.length);
|
||||||
const index = instructionIndex == null ? 65535 : instructionIndex;
|
const index = instructionIndex == null ? 65535 : instructionIndex;
|
||||||
@@ -15241,7 +15557,7 @@ var Ed25519Program = class _Ed25519Program {
|
|||||||
messageInstructionIndex: index
|
messageInstructionIndex: index
|
||||||
}, instructionData);
|
}, instructionData);
|
||||||
instructionData.fill(publicKey2, publicKeyOffset);
|
instructionData.fill(publicKey2, publicKeyOffset);
|
||||||
instructionData.fill(signature, signatureOffset);
|
instructionData.fill(signature2, signatureOffset);
|
||||||
instructionData.fill(message, messageDataOffset);
|
instructionData.fill(message, messageDataOffset);
|
||||||
return new TransactionInstruction({
|
return new TransactionInstruction({
|
||||||
keys: [],
|
keys: [],
|
||||||
@@ -15263,11 +15579,11 @@ var Ed25519Program = class _Ed25519Program {
|
|||||||
try {
|
try {
|
||||||
const keypair = Keypair.fromSecretKey(privateKey);
|
const keypair = Keypair.fromSecretKey(privateKey);
|
||||||
const publicKey2 = keypair.publicKey.toBytes();
|
const publicKey2 = keypair.publicKey.toBytes();
|
||||||
const signature = sign(message, keypair.secretKey);
|
const signature2 = sign(message, keypair.secretKey);
|
||||||
return this.createInstructionWithPublicKey({
|
return this.createInstructionWithPublicKey({
|
||||||
publicKey: publicKey2,
|
publicKey: publicKey2,
|
||||||
message,
|
message,
|
||||||
signature,
|
signature: signature2,
|
||||||
instructionIndex
|
instructionIndex
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -15277,8 +15593,8 @@ var Ed25519Program = class _Ed25519Program {
|
|||||||
};
|
};
|
||||||
Ed25519Program.programId = new PublicKey("Ed25519SigVerify111111111111111111111111111");
|
Ed25519Program.programId = new PublicKey("Ed25519SigVerify111111111111111111111111111");
|
||||||
var ecdsaSign = (msgHash, privKey) => {
|
var ecdsaSign = (msgHash, privKey) => {
|
||||||
const signature = secp256k1.sign(msgHash, privKey);
|
const signature2 = secp256k1.sign(msgHash, privKey);
|
||||||
return [signature.toCompactRawBytes(), signature.recovery];
|
return [signature2.toCompactRawBytes(), signature2.recovery];
|
||||||
};
|
};
|
||||||
secp256k1.utils.isValidPrivateKey;
|
secp256k1.utils.isValidPrivateKey;
|
||||||
var publicKeyCreate = secp256k1.getPublicKey;
|
var publicKeyCreate = secp256k1.getPublicKey;
|
||||||
@@ -15316,14 +15632,14 @@ var Secp256k1Program = class _Secp256k1Program {
|
|||||||
const {
|
const {
|
||||||
publicKey: publicKey2,
|
publicKey: publicKey2,
|
||||||
message,
|
message,
|
||||||
signature,
|
signature: signature2,
|
||||||
recoveryId,
|
recoveryId,
|
||||||
instructionIndex
|
instructionIndex
|
||||||
} = params;
|
} = params;
|
||||||
return _Secp256k1Program.createInstructionWithEthAddress({
|
return _Secp256k1Program.createInstructionWithEthAddress({
|
||||||
ethAddress: _Secp256k1Program.publicKeyToEthAddress(publicKey2),
|
ethAddress: _Secp256k1Program.publicKeyToEthAddress(publicKey2),
|
||||||
message,
|
message,
|
||||||
signature,
|
signature: signature2,
|
||||||
recoveryId,
|
recoveryId,
|
||||||
instructionIndex
|
instructionIndex
|
||||||
});
|
});
|
||||||
@@ -15336,7 +15652,7 @@ var Secp256k1Program = class _Secp256k1Program {
|
|||||||
const {
|
const {
|
||||||
ethAddress: rawAddress,
|
ethAddress: rawAddress,
|
||||||
message,
|
message,
|
||||||
signature,
|
signature: signature2,
|
||||||
recoveryId,
|
recoveryId,
|
||||||
instructionIndex = 0
|
instructionIndex = 0
|
||||||
} = params;
|
} = params;
|
||||||
@@ -15354,7 +15670,7 @@ var Secp256k1Program = class _Secp256k1Program {
|
|||||||
const dataStart = 1 + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
|
const dataStart = 1 + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
|
||||||
const ethAddressOffset = dataStart;
|
const ethAddressOffset = dataStart;
|
||||||
const signatureOffset = dataStart + ethAddress.length;
|
const signatureOffset = dataStart + ethAddress.length;
|
||||||
const messageDataOffset = signatureOffset + signature.length + 1;
|
const messageDataOffset = signatureOffset + signature2.length + 1;
|
||||||
const numSignatures = 1;
|
const numSignatures = 1;
|
||||||
const instructionData = import_buffer2.Buffer.alloc(SECP256K1_INSTRUCTION_LAYOUT.span + message.length);
|
const instructionData = import_buffer2.Buffer.alloc(SECP256K1_INSTRUCTION_LAYOUT.span + message.length);
|
||||||
SECP256K1_INSTRUCTION_LAYOUT.encode({
|
SECP256K1_INSTRUCTION_LAYOUT.encode({
|
||||||
@@ -15366,7 +15682,7 @@ var Secp256k1Program = class _Secp256k1Program {
|
|||||||
messageDataOffset,
|
messageDataOffset,
|
||||||
messageDataSize: message.length,
|
messageDataSize: message.length,
|
||||||
messageInstructionIndex: instructionIndex,
|
messageInstructionIndex: instructionIndex,
|
||||||
signature: toBuffer(signature),
|
signature: toBuffer(signature2),
|
||||||
ethAddress: toBuffer(ethAddress),
|
ethAddress: toBuffer(ethAddress),
|
||||||
recoveryId
|
recoveryId
|
||||||
}, instructionData);
|
}, instructionData);
|
||||||
@@ -15396,11 +15712,11 @@ var Secp256k1Program = class _Secp256k1Program {
|
|||||||
/* isCompressed */
|
/* isCompressed */
|
||||||
).slice(1);
|
).slice(1);
|
||||||
const messageHash = import_buffer2.Buffer.from(keccak_256(toBuffer(message)));
|
const messageHash = import_buffer2.Buffer.from(keccak_256(toBuffer(message)));
|
||||||
const [signature, recoveryId] = ecdsaSign(messageHash, privateKey);
|
const [signature2, recoveryId] = ecdsaSign(messageHash, privateKey);
|
||||||
return this.createInstructionWithPublicKey({
|
return this.createInstructionWithPublicKey({
|
||||||
publicKey: publicKey2,
|
publicKey: publicKey2,
|
||||||
message,
|
message,
|
||||||
signature,
|
signature: signature2,
|
||||||
recoveryId,
|
recoveryId,
|
||||||
instructionIndex
|
instructionIndex
|
||||||
});
|
});
|
||||||
@@ -16179,7 +16495,9 @@ var VoteAccountLayout = BufferLayout.struct([
|
|||||||
BufferLayout.struct([BufferLayout.nu64("slot"), BufferLayout.nu64("timestamp")], "lastTimestamp")
|
BufferLayout.struct([BufferLayout.nu64("slot"), BufferLayout.nu64("timestamp")], "lastTimestamp")
|
||||||
]);
|
]);
|
||||||
export {
|
export {
|
||||||
PublicKey
|
PublicKey,
|
||||||
|
Transaction,
|
||||||
|
VersionedTransaction
|
||||||
};
|
};
|
||||||
/*! Bundled license information:
|
/*! Bundled license information:
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
|
||||||
|
|
||||||
export { PublicKey };
|
export { PublicKey, Transaction, VersionedTransaction };
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export class WsJsonClient {
|
|||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.openPromise = null;
|
this.openPromise = null;
|
||||||
this.pending = new Map();
|
this.pending = new Map();
|
||||||
|
this.listeners = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
async open() {
|
async open() {
|
||||||
@@ -78,14 +79,53 @@ export class WsJsonClient {
|
|||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data?.event) {
|
||||||
|
this.emit(String(data?.op || ''), data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const requestId = data?.requestId;
|
const requestId = data?.requestId;
|
||||||
if (!requestId) return;
|
if (!requestId) {
|
||||||
|
this.emit(String(data?.op || ''), data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const slot = this.pending.get(requestId);
|
const slot = this.pending.get(requestId);
|
||||||
if (!slot) return;
|
if (!slot) {
|
||||||
|
this.emit(String(data?.op || ''), data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.pending.delete(requestId);
|
this.pending.delete(requestId);
|
||||||
slot.resolve(data);
|
slot.resolve(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
on(op, handler) {
|
||||||
|
const key = String(op || '').trim();
|
||||||
|
if (!key || typeof handler !== 'function') {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
if (!this.listeners.has(key)) {
|
||||||
|
this.listeners.set(key, new Set());
|
||||||
|
}
|
||||||
|
this.listeners.get(key).add(handler);
|
||||||
|
return () => {
|
||||||
|
const bucket = this.listeners.get(key);
|
||||||
|
if (!bucket) return;
|
||||||
|
bucket.delete(handler);
|
||||||
|
if (!bucket.size) {
|
||||||
|
this.listeners.delete(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(op, payload) {
|
||||||
|
const bucket = this.listeners.get(String(op || '').trim());
|
||||||
|
if (!bucket?.size) return;
|
||||||
|
for (const handler of [...bucket]) {
|
||||||
|
try {
|
||||||
|
handler(payload);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
failPending(message) {
|
failPending(message) {
|
||||||
const error = new Error(message);
|
const error = new Error(message);
|
||||||
for (const slot of this.pending.values()) slot.reject(error);
|
for (const slot of this.pending.values()) slot.reject(error);
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Wallet-session plugin for SHiNE with session-only login via trusted device.",
|
"description": "Wallet-session plugin for SHiNE with session-only login via trusted device.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage"
|
"storage",
|
||||||
|
"sidePanel"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
@@ -13,8 +14,32 @@
|
|||||||
"service_worker": "background.js",
|
"service_worker": "background.js",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"content-script.js"
|
||||||
|
],
|
||||||
|
"run_at": "document_start"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"provider-bridge.js",
|
||||||
|
"js/lib/vendor/solana-publickey-bundle.js"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "SHiNE Wallet",
|
"default_title": "Open SHiNE Wallet"
|
||||||
"default_popup": "popup.html"
|
},
|
||||||
|
"side_panel": {
|
||||||
|
"default_path": "popup.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,34 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 360px;
|
min-width: 320px;
|
||||||
|
max-width: none;
|
||||||
|
min-height: 100vh;
|
||||||
background: #0f1720;
|
background: #0f1720;
|
||||||
color: #e8eef6;
|
color: #e8eef6;
|
||||||
font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-panel-body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
min-height: calc(100vh - 24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
@@ -140,9 +152,9 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
font-size: 34px;
|
font-size: 30px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-row {
|
.summary-row {
|
||||||
@@ -153,7 +165,7 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary-row code {
|
.summary-row code {
|
||||||
max-width: 180px;
|
max-width: 140px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -186,6 +198,12 @@ select {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.device-row {
|
.device-row {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid #243446;
|
border: 1px solid #243446;
|
||||||
@@ -193,6 +211,30 @@ select {
|
|||||||
background: #0d141d;
|
background: #0d141d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #243446;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0d141d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aabbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: #e8eef6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value.mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
color: #bed5f5;
|
||||||
|
}
|
||||||
|
|
||||||
.device-state {
|
.device-state {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
@@ -209,3 +251,15 @@ select {
|
|||||||
.device-state-unknown {
|
.device-state-unknown {
|
||||||
color: #f8e2a0;
|
color: #f8e2a0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wallet-pubkey {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #314459;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0d141d;
|
||||||
|
color: #bed5f5;
|
||||||
|
white-space: normal;
|
||||||
|
line-break: anywhere;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>SHiNE Wallet</title>
|
<title>SHiNE Wallet</title>
|
||||||
<link rel="stylesheet" href="./popup.css" />
|
<link rel="stylesheet" href="./popup.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="side-panel-body">
|
||||||
<main class="layout">
|
<main class="layout">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -14,49 +14,13 @@
|
|||||||
<h1>SHiNE Wallet</h1>
|
<h1>SHiNE Wallet</h1>
|
||||||
<p class="muted">Session-only wallet plugin</p>
|
<p class="muted">Session-only wallet plugin</p>
|
||||||
</div>
|
</div>
|
||||||
<span id="connection-pill" class="pill pill-offline">offline</span>
|
<span id="connection-pill" class="pill pill-offline">не подключено</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p id="server-login-info" class="muted small">Сервер SHiNE: —</p>
|
<p id="server-login-info" class="muted small">Сервер SHiNE: —</p>
|
||||||
<p id="server-address" class="muted small">Адрес: —</p>
|
|
||||||
|
|
||||||
<div id="session-card" class="card hidden">
|
<div id="connect-card" class="card">
|
||||||
<div class="card-title">Подключённая wallet-session</div>
|
<div class="card-title">Подключение</div>
|
||||||
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
|
||||||
<div class="summary-row"><span>Session ID</span><code id="session-id">—</code></div>
|
|
||||||
<div class="summary-row"><span>Тип</span><strong id="session-type">wallet</strong></div>
|
|
||||||
<div class="summary-row"><span>deviceKey</span><code id="device-key-short">—</code></div>
|
|
||||||
<div class="actions">
|
|
||||||
<button id="resume-btn" class="btn secondary" type="button">Проверить session</button>
|
|
||||||
<button id="refresh-devices-btn" class="btn secondary" type="button">Обновить устройства</button>
|
|
||||||
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="signing-card" class="card hidden">
|
|
||||||
<div class="card-title">Подготовка подписи</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>Ключ подписи</span>
|
|
||||||
<select id="sign-key-select"></select>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Устройство homeserver</span>
|
|
||||||
<select id="device-select"></select>
|
|
||||||
</label>
|
|
||||||
<div id="homeserver-list" class="device-list"></div>
|
|
||||||
<p class="muted small">
|
|
||||||
Для выбора доступны homeserver-сессии, опубликованные в PDA аккаунта. Online-статус определяется без постоянного удержания соединения.
|
|
||||||
</p>
|
|
||||||
<div class="actions">
|
|
||||||
<button id="prepare-sign-btn" class="btn primary" type="button">Запросить подпись</button>
|
|
||||||
</div>
|
|
||||||
<p class="muted small">
|
|
||||||
Сам signaling подтверждения подписи ещё не доделан. Сейчас доступен только каркас выбора ключа и устройства.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">Войти через другое устройство</div>
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Логин</span>
|
<span>Логин</span>
|
||||||
<input id="login-input" type="text" autocomplete="username" />
|
<input id="login-input" type="text" autocomplete="username" />
|
||||||
@@ -69,29 +33,68 @@
|
|||||||
<span>Пароль подключения</span>
|
<span>Пароль подключения</span>
|
||||||
<input id="password-input" type="password" autocomplete="current-password" />
|
<input id="password-input" type="password" autocomplete="current-password" />
|
||||||
</label>
|
</label>
|
||||||
<button id="start-btn" class="btn primary" type="button">Получить код</button>
|
<button id="start-btn" class="btn primary" type="button">Подключить</button>
|
||||||
<p class="muted small">
|
|
||||||
Wallet plugin создаёт временный requester keypair, ждёт подтверждение на доверенном устройстве
|
|
||||||
и получает только wallet-session без передачи постоянных ключей.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="pairing-card" class="card hidden">
|
<div id="pairing-card" class="card hidden">
|
||||||
<div class="card-title">Код подключения</div>
|
<div class="card-title">Код подключения</div>
|
||||||
<div id="short-code" class="code">00 00 00 00 00</div>
|
<div id="short-code" class="code">00 00 00 00 00</div>
|
||||||
<p id="pairing-hint" class="muted small">
|
<p id="pairing-hint" class="muted small">Покажите код на доверенном устройстве.</p>
|
||||||
Покажите код на доверенном устройстве в разделе «Подключить по коду».
|
|
||||||
</p>
|
|
||||||
<p id="pairing-expire" class="muted small"></p>
|
<p id="pairing-expire" class="muted small"></p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="cancel-btn" class="btn secondary" type="button">Отменить</button>
|
<button id="cancel-btn" class="btn secondary" type="button">Отменить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="session-card" class="card hidden">
|
||||||
|
<div class="card-title">Подключено</div>
|
||||||
|
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="resume-btn" class="btn secondary" type="button">Проверить session</button>
|
||||||
|
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wallet-card" class="card hidden">
|
||||||
|
<div class="card-title">Текущий кошелёк ESP32</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>Homeserver</span>
|
||||||
|
<select id="device-select"></select>
|
||||||
|
</label>
|
||||||
|
<div id="homeserver-list" class="device-list"></div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="refresh-devices-btn" class="btn secondary" type="button">Обновить устройства</button>
|
||||||
|
<button id="request-wallet-btn" class="btn primary" type="button">Запросить кошелёк</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pending-approval-card" class="card hidden">
|
||||||
|
<div class="card-title">Ожидается подпись</div>
|
||||||
|
<p id="pending-approval-subtitle" class="muted small">Сайт запросил подписание транзакции.</p>
|
||||||
|
<div id="pending-approval-details" class="device-list"></div>
|
||||||
|
<p class="muted small">Запрос уже отправлен на доверенное устройство. Здесь можно только отменить ожидание.</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="cancel-pending-approval-btn" class="btn danger" type="button">Отменить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wallet-result-card" class="card hidden">
|
||||||
|
<div class="card-title">Полученный кошелёк</div>
|
||||||
|
<div class="summary-row"><span>Тип</span><strong id="wallet-type">—</strong></div>
|
||||||
|
<div class="field">
|
||||||
|
<span>Public key</span>
|
||||||
|
<code id="wallet-pubkey" class="wallet-pubkey">—</code>
|
||||||
|
</div>
|
||||||
|
<p id="wallet-verify" class="muted small">—</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="copy-wallet-btn" class="btn secondary" type="button">Копировать ключ</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="status" class="status hidden"></div>
|
<div id="status" class="status hidden"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="./popup.js"></script>
|
<script type="module" src="./popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { formatPairingShortCode } from './js/lib/device-pairing.js';
|
|||||||
|
|
||||||
const els = {
|
const els = {
|
||||||
serverLoginInfo: document.querySelector('#server-login-info'),
|
serverLoginInfo: document.querySelector('#server-login-info'),
|
||||||
serverAddress: document.querySelector('#server-address'),
|
|
||||||
loginInput: document.querySelector('#login-input'),
|
loginInput: document.querySelector('#login-input'),
|
||||||
usePassword: document.querySelector('#use-password'),
|
usePassword: document.querySelector('#use-password'),
|
||||||
passwordField: document.querySelector('#password-field'),
|
passwordField: document.querySelector('#password-field'),
|
||||||
passwordInput: document.querySelector('#password-input'),
|
passwordInput: document.querySelector('#password-input'),
|
||||||
startBtn: document.querySelector('#start-btn'),
|
startBtn: document.querySelector('#start-btn'),
|
||||||
|
connectCard: document.querySelector('#connect-card'),
|
||||||
pairingCard: document.querySelector('#pairing-card'),
|
pairingCard: document.querySelector('#pairing-card'),
|
||||||
shortCode: document.querySelector('#short-code'),
|
shortCode: document.querySelector('#short-code'),
|
||||||
pairingHint: document.querySelector('#pairing-hint'),
|
pairingHint: document.querySelector('#pairing-hint'),
|
||||||
@@ -16,17 +16,22 @@ const els = {
|
|||||||
status: document.querySelector('#status'),
|
status: document.querySelector('#status'),
|
||||||
sessionCard: document.querySelector('#session-card'),
|
sessionCard: document.querySelector('#session-card'),
|
||||||
sessionLogin: document.querySelector('#session-login'),
|
sessionLogin: document.querySelector('#session-login'),
|
||||||
sessionId: document.querySelector('#session-id'),
|
|
||||||
sessionType: document.querySelector('#session-type'),
|
|
||||||
deviceKeyShort: document.querySelector('#device-key-short'),
|
|
||||||
resumeBtn: document.querySelector('#resume-btn'),
|
resumeBtn: document.querySelector('#resume-btn'),
|
||||||
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
|
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
|
||||||
disconnectBtn: document.querySelector('#disconnect-btn'),
|
disconnectBtn: document.querySelector('#disconnect-btn'),
|
||||||
signingCard: document.querySelector('#signing-card'),
|
walletCard: document.querySelector('#wallet-card'),
|
||||||
signKeySelect: document.querySelector('#sign-key-select'),
|
|
||||||
deviceSelect: document.querySelector('#device-select'),
|
deviceSelect: document.querySelector('#device-select'),
|
||||||
homeserverList: document.querySelector('#homeserver-list'),
|
homeserverList: document.querySelector('#homeserver-list'),
|
||||||
prepareSignBtn: document.querySelector('#prepare-sign-btn'),
|
requestWalletBtn: document.querySelector('#request-wallet-btn'),
|
||||||
|
pendingApprovalCard: document.querySelector('#pending-approval-card'),
|
||||||
|
pendingApprovalSubtitle: document.querySelector('#pending-approval-subtitle'),
|
||||||
|
pendingApprovalDetails: document.querySelector('#pending-approval-details'),
|
||||||
|
cancelPendingApprovalBtn: document.querySelector('#cancel-pending-approval-btn'),
|
||||||
|
walletResultCard: document.querySelector('#wallet-result-card'),
|
||||||
|
walletType: document.querySelector('#wallet-type'),
|
||||||
|
walletPubkey: document.querySelector('#wallet-pubkey'),
|
||||||
|
walletVerify: document.querySelector('#wallet-verify'),
|
||||||
|
copyWalletBtn: document.querySelector('#copy-wallet-btn'),
|
||||||
connectionPill: document.querySelector('#connection-pill'),
|
connectionPill: document.querySelector('#connection-pill'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,16 +39,20 @@ let state = {
|
|||||||
settings: {
|
settings: {
|
||||||
serverLogin: 'shineupme',
|
serverLogin: 'shineupme',
|
||||||
serverHttp: 'https://shineup.me',
|
serverHttp: 'https://shineup.me',
|
||||||
serverUrl: 'wss://shineup.me/ws',
|
|
||||||
login: '',
|
login: '',
|
||||||
},
|
},
|
||||||
pairing: {
|
pairing: {
|
||||||
active: false,
|
active: false,
|
||||||
pairingId: '',
|
|
||||||
expiresAtMs: 0,
|
expiresAtMs: 0,
|
||||||
|
shortCode: '',
|
||||||
},
|
},
|
||||||
session: null,
|
session: null,
|
||||||
connectionOnline: false,
|
walletProfile: null,
|
||||||
|
signing: {
|
||||||
|
selectedDeviceName: '',
|
||||||
|
},
|
||||||
|
currentWallet: null,
|
||||||
|
pendingApproval: null,
|
||||||
status: {
|
status: {
|
||||||
text: '',
|
text: '',
|
||||||
kind: 'info',
|
kind: 'info',
|
||||||
@@ -60,7 +69,7 @@ function setStatus(message, kind = 'info') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setConnectedPill(connected) {
|
function setConnectedPill(connected) {
|
||||||
els.connectionPill.textContent = connected ? 'online' : 'offline';
|
els.connectionPill.textContent = connected ? 'подключено' : 'не подключено';
|
||||||
els.connectionPill.className = connected ? 'pill pill-online' : 'pill pill-offline';
|
els.connectionPill.className = connected ? 'pill pill-online' : 'pill pill-offline';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,81 +108,119 @@ function renderHomeserverList(items = []) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPendingApproval(pendingApproval) {
|
||||||
|
els.pendingApprovalDetails.innerHTML = '';
|
||||||
|
if (!pendingApproval) return;
|
||||||
|
const summary = pendingApproval.transactionSummary || {};
|
||||||
|
const programs = Array.isArray(summary.programs) && summary.programs.length
|
||||||
|
? summary.programs.join(', ')
|
||||||
|
: 'не определены';
|
||||||
|
const details = [
|
||||||
|
{ label: 'Сайт', value: pendingApproval.origin || '—', mono: true },
|
||||||
|
{ label: 'Кошелёк', value: pendingApproval.publicKeyBase58 || '—', mono: true },
|
||||||
|
{ label: 'Очередь', value: `${pendingApproval.queuePosition || 1} из ${pendingApproval.queueLength || 1}` },
|
||||||
|
{ label: 'Комментарий', value: pendingApproval.comment || 'Транзакция запрошена сайтом' },
|
||||||
|
{ label: 'Тип', value: summary.kind || 'legacy' },
|
||||||
|
{ label: 'Инструкций', value: String(summary.instructionCount ?? 0) },
|
||||||
|
{ label: 'Программы', value: programs, mono: true },
|
||||||
|
];
|
||||||
|
if (summary.feePayer) {
|
||||||
|
details.push({ label: 'Fee payer', value: summary.feePayer, mono: true });
|
||||||
|
}
|
||||||
|
if (summary.recentBlockhash) {
|
||||||
|
details.push({ label: 'Blockhash', value: summary.recentBlockhash, mono: true });
|
||||||
|
}
|
||||||
|
for (const item of details) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'detail-row';
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'detail-label';
|
||||||
|
label.textContent = item.label;
|
||||||
|
const value = document.createElement('div');
|
||||||
|
value.className = `detail-value${item.mono ? ' mono' : ''}`;
|
||||||
|
value.textContent = item.value;
|
||||||
|
row.append(label, value);
|
||||||
|
els.pendingApprovalDetails.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyState(nextState) {
|
function applyState(nextState) {
|
||||||
state = nextState || state;
|
state = nextState || state;
|
||||||
const loginValue = String(state?.settings?.login || '');
|
const loginValue = String(state?.settings?.login || '');
|
||||||
const resolvedServerLogin = String(state?.settings?.serverLogin || '').trim();
|
const resolvedServerLogin = String(state?.settings?.serverLogin || '').trim();
|
||||||
const resolvedServerAddress = String(state?.settings?.serverHttp || '').trim();
|
const resolvedServerAddress = String(state?.settings?.serverHttp || '').trim();
|
||||||
if (loginValue && resolvedServerLogin && resolvedServerAddress) {
|
els.serverLoginInfo.textContent = resolvedServerLogin && resolvedServerAddress
|
||||||
els.serverLoginInfo.textContent = `Сервер SHiNE: ${resolvedServerLogin}`;
|
? `Сервер SHiNE: ${resolvedServerLogin} (${resolvedServerAddress})`
|
||||||
els.serverAddress.textContent = `Адрес: ${resolvedServerAddress}`;
|
: 'Сервер SHiNE: —';
|
||||||
} else {
|
|
||||||
els.serverLoginInfo.textContent = 'Сервер SHiNE: —';
|
|
||||||
els.serverAddress.textContent = 'Адрес: —';
|
|
||||||
}
|
|
||||||
if (document.activeElement !== els.loginInput) {
|
if (document.activeElement !== els.loginInput) {
|
||||||
els.loginInput.value = loginValue;
|
els.loginInput.value = loginValue;
|
||||||
}
|
}
|
||||||
setConnectedPill(!!state?.connectionOnline);
|
|
||||||
|
setConnectedPill(!!state?.session);
|
||||||
setStatus(state?.status?.text || '', state?.status?.kind || 'info');
|
setStatus(state?.status?.text || '', state?.status?.kind || 'info');
|
||||||
|
|
||||||
const session = state?.session;
|
const session = state?.session;
|
||||||
const walletProfile = state?.walletProfile;
|
const walletProfile = state?.walletProfile;
|
||||||
const signing = state?.signing || {};
|
const signing = state?.signing || {};
|
||||||
if (session) {
|
const currentWallet = state?.currentWallet || null;
|
||||||
els.sessionCard.classList.remove('hidden');
|
const pendingApproval = state?.pendingApproval || null;
|
||||||
els.sessionLogin.textContent = session.login || '—';
|
|
||||||
els.sessionId.textContent = session.sessionId || '—';
|
|
||||||
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
|
|
||||||
els.deviceKeyShort.textContent = shortKey(walletProfile?.publicKeys?.deviceKeyBase58 || '');
|
|
||||||
els.signingCard.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
els.sessionCard.classList.add('hidden');
|
|
||||||
els.sessionLogin.textContent = '—';
|
|
||||||
els.sessionId.textContent = '—';
|
|
||||||
els.sessionType.textContent = 'wallet';
|
|
||||||
els.deviceKeyShort.textContent = '—';
|
|
||||||
els.signingCard.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
const signKeyOptions = Array.isArray(walletProfile?.signingKeyOptions) ? walletProfile.signingKeyOptions : [];
|
els.connectCard.classList.toggle('hidden', !!session);
|
||||||
els.signKeySelect.innerHTML = '';
|
els.sessionCard.classList.toggle('hidden', !session);
|
||||||
signKeyOptions.forEach((item) => {
|
els.walletCard.classList.toggle('hidden', !session);
|
||||||
const option = document.createElement('option');
|
els.pendingApprovalCard.classList.toggle('hidden', !pendingApproval);
|
||||||
option.value = item.id;
|
|
||||||
option.textContent = item.label;
|
if (session) {
|
||||||
option.selected = item.id === signing.selectedKeyId;
|
els.sessionLogin.textContent = session.login || '—';
|
||||||
els.signKeySelect.append(option);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
|
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
|
||||||
els.deviceSelect.innerHTML = '';
|
els.deviceSelect.innerHTML = '';
|
||||||
homeservers.forEach((item) => {
|
homeservers.forEach((item) => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = item.sessionName;
|
option.value = item.sessionName;
|
||||||
option.textContent = `${item.sessionName} [${item.onlineState || 'unknown'}]`;
|
option.textContent = `${item.sessionName} [${item.onlineState || 'offline'}]`;
|
||||||
option.selected = item.sessionName === signing.selectedDeviceName;
|
option.selected = item.sessionName === signing.selectedDeviceName;
|
||||||
els.deviceSelect.append(option);
|
els.deviceSelect.append(option);
|
||||||
});
|
});
|
||||||
renderHomeserverList(homeservers);
|
renderHomeserverList(homeservers);
|
||||||
els.prepareSignBtn.disabled = !session || !signing.selectedKeyId || !signing.selectedDeviceName;
|
els.requestWalletBtn.disabled = !session || !signing.selectedDeviceName;
|
||||||
|
|
||||||
|
if (pendingApproval) {
|
||||||
|
const queueSuffix = (pendingApproval.queueLength || 1) > 1
|
||||||
|
? ` В очереди ${pendingApproval.queueLength} транзакции.`
|
||||||
|
: '';
|
||||||
|
els.pendingApprovalSubtitle.textContent = pendingApproval.origin
|
||||||
|
? `Сайт ${pendingApproval.origin} запросил подписание транзакции.${queueSuffix}`
|
||||||
|
: `Сайт запросил подписание транзакции.${queueSuffix}`;
|
||||||
|
renderPendingApproval(pendingApproval);
|
||||||
|
} else {
|
||||||
|
els.pendingApprovalSubtitle.textContent = 'Сайт запросил подписание транзакции.';
|
||||||
|
els.pendingApprovalDetails.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentWallet?.publicKeyBase58) {
|
||||||
|
els.walletResultCard.classList.remove('hidden');
|
||||||
|
els.walletType.textContent = currentWallet.type || '—';
|
||||||
|
els.walletPubkey.textContent = currentWallet.publicKeyBase58 || '—';
|
||||||
|
els.walletVerify.textContent = currentWallet.verificationText || '—';
|
||||||
|
} else {
|
||||||
|
els.walletResultCard.classList.add('hidden');
|
||||||
|
els.walletType.textContent = '—';
|
||||||
|
els.walletPubkey.textContent = '—';
|
||||||
|
els.walletVerify.textContent = '—';
|
||||||
|
}
|
||||||
|
|
||||||
const pairing = state?.pairing || {};
|
const pairing = state?.pairing || {};
|
||||||
if (pairing.active) {
|
if (pairing.active) {
|
||||||
els.pairingCard.classList.remove('hidden');
|
els.pairingCard.classList.remove('hidden');
|
||||||
const shortCode = String(pairing.shortCode || els.shortCode.dataset.shortCode || els.shortCode.textContent || '');
|
els.shortCode.textContent = formatPairingShortCode(String(pairing.shortCode || ''));
|
||||||
els.shortCode.dataset.shortCode = shortCode;
|
|
||||||
els.shortCode.textContent = formatPairingShortCode(shortCode);
|
|
||||||
els.pairingHint.textContent = pairing.trustedSessionOnline
|
|
||||||
? 'Покажите код на доверенном устройстве и подтвердите выпуск wallet-session.'
|
|
||||||
: 'Сейчас нет онлайн доверенной сессии. Откройте другое устройство и подтвердите заявку.';
|
|
||||||
const leftMs = Number(pairing.expiresAtMs || 0) - Date.now();
|
const leftMs = Number(pairing.expiresAtMs || 0) - Date.now();
|
||||||
els.pairingExpire.textContent = leftMs > 0 ? `Код действителен ещё ${formatRemaining(leftMs)}.` : 'Время ожидания истекло.';
|
els.pairingExpire.textContent = leftMs > 0 ? `Код действителен ещё ${formatRemaining(leftMs)}.` : 'Время ожидания истекло.';
|
||||||
els.startBtn.disabled = true;
|
els.startBtn.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
els.pairingCard.classList.add('hidden');
|
els.pairingCard.classList.add('hidden');
|
||||||
els.shortCode.textContent = formatPairingShortCode('');
|
els.shortCode.textContent = formatPairingShortCode('');
|
||||||
delete els.shortCode.dataset.shortCode;
|
|
||||||
els.pairingExpire.textContent = '';
|
els.pairingExpire.textContent = '';
|
||||||
els.startBtn.disabled = false;
|
els.startBtn.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -241,16 +288,13 @@ async function startPairing() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStatus('Создаём wallet-session заявку...', 'info');
|
setStatus('Создаём wallet-session заявку...', 'info');
|
||||||
els.startBtn.disabled = true;
|
|
||||||
try {
|
try {
|
||||||
const response = await sendMessage('wallet:startPairing', {
|
await sendMessage('wallet:startPairing', {
|
||||||
login,
|
login,
|
||||||
usePassword: !!els.usePassword.checked,
|
usePassword: !!els.usePassword.checked,
|
||||||
password: String(els.passwordInput.value || ''),
|
password: String(els.passwordInput.value || ''),
|
||||||
});
|
});
|
||||||
applyState(response.state);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
els.startBtn.disabled = false;
|
|
||||||
setStatus(error.message || 'Не удалось начать pairing.', 'error');
|
setStatus(error.message || 'Не удалось начать pairing.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,23 +333,41 @@ async function refreshDevices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSigningSelection() {
|
async function updateDeviceSelection() {
|
||||||
try {
|
try {
|
||||||
await sendMessage('wallet:updateSigningSelection', {
|
await sendMessage('wallet:updateSigningSelection', {
|
||||||
selectedKeyId: String(els.signKeySelect.value || '').trim(),
|
|
||||||
selectedDeviceName: String(els.deviceSelect.value || '').trim(),
|
selectedDeviceName: String(els.deviceSelect.value || '').trim(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error.message || 'Не удалось обновить выбор для подписи.', 'error');
|
setStatus(error.message || 'Не удалось обновить выбор homeserver.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareSignSignal() {
|
async function requestCurrentWallet() {
|
||||||
setStatus('Готовим каркас запроса подписи...', 'info');
|
setStatus('Запрашиваем текущий кошелёк с ESP32...', 'info');
|
||||||
try {
|
try {
|
||||||
await sendMessage('wallet:prepareSignSignal');
|
await sendMessage('wallet:requestCurrentWallet');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error.message || 'Не удалось подготовить запрос подписи.', 'error');
|
setStatus(error.message || 'Не удалось получить кошелёк с ESP32.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyWalletKey() {
|
||||||
|
const value = String(els.walletPubkey.textContent || '').trim();
|
||||||
|
if (!value || value === '—') return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
setStatus('Публичный ключ скопирован.', 'info');
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message || 'Не удалось скопировать ключ.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelPendingApproval() {
|
||||||
|
try {
|
||||||
|
await sendMessage('wallet:cancelPendingSiteApproval');
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message || 'Не удалось отменить ожидание подписи.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,9 +402,10 @@ function bindUi() {
|
|||||||
els.resumeBtn.addEventListener('click', () => { void resumeSession(); });
|
els.resumeBtn.addEventListener('click', () => { void resumeSession(); });
|
||||||
els.refreshDevicesBtn.addEventListener('click', () => { void refreshDevices(); });
|
els.refreshDevicesBtn.addEventListener('click', () => { void refreshDevices(); });
|
||||||
els.disconnectBtn.addEventListener('click', () => { void disconnectSession(); });
|
els.disconnectBtn.addEventListener('click', () => { void disconnectSession(); });
|
||||||
els.signKeySelect.addEventListener('change', () => { void updateSigningSelection(); });
|
els.deviceSelect.addEventListener('change', () => { void updateDeviceSelection(); });
|
||||||
els.deviceSelect.addEventListener('change', () => { void updateSigningSelection(); });
|
els.requestWalletBtn.addEventListener('click', () => { void requestCurrentWallet(); });
|
||||||
els.prepareSignBtn.addEventListener('click', () => { void prepareSignSignal(); });
|
els.copyWalletBtn.addEventListener('click', () => { void copyWalletKey(); });
|
||||||
|
els.cancelPendingApprovalBtn.addEventListener('click', () => { void cancelPendingApproval(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|||||||
@@ -0,0 +1,433 @@
|
|||||||
|
import { PublicKey, Transaction, VersionedTransaction } from './js/lib/vendor/solana-publickey-bundle.js';
|
||||||
|
|
||||||
|
const PAGE_REQUEST = 'shine-wallet-page-request';
|
||||||
|
const PAGE_RESPONSE = 'shine-wallet-page-response';
|
||||||
|
const PAGE_MESSAGE_TARGET_ORIGIN = '*';
|
||||||
|
const STANDARD_REGISTER_EVENT = 'wallet-standard:register-wallet';
|
||||||
|
const STANDARD_APP_READY_EVENT = 'wallet-standard:app-ready';
|
||||||
|
const SOLANA_CHAINS = ['solana:mainnet', 'solana:devnet', 'solana:testnet'];
|
||||||
|
const SOLANA_STANDARD_FEATURES = ['solana:signTransaction'];
|
||||||
|
const WALLET_ICON = `data:image/svg+xml;base64,${btoa(
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" fill="none"><rect width="96" height="96" rx="20" fill="#101722"/><path d="M23 28h50l-8 10H31l-8-10Z" fill="#3FB0FF"/><path d="M31 44h34l8 10H23l8-10Z" fill="#77D67A"/><path d="M23 68h50l-8-10H31l-8 10Z" fill="#A881FF"/></svg>'
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
function bytesToBase64(bytes) {
|
||||||
|
let binary = '';
|
||||||
|
const chunk = 0x8000;
|
||||||
|
for (let i = 0; i < bytes.length; i += chunk) {
|
||||||
|
const slice = bytes.subarray(i, i + chunk);
|
||||||
|
binary += String.fromCharCode(...slice);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToBytes(value) {
|
||||||
|
const binary = atob(String(value || '').trim());
|
||||||
|
const out = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i += 1) {
|
||||||
|
out[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProviderError(message, code = '') {
|
||||||
|
const error = new Error(String(message || 'Wallet provider error'));
|
||||||
|
if (code === 'USER_REJECTED' || code === 'NOT_TRUSTED') {
|
||||||
|
error.code = 4001;
|
||||||
|
} else if (code) {
|
||||||
|
error.code = code;
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeTransaction(transaction) {
|
||||||
|
const summary = {
|
||||||
|
kind: 'legacy',
|
||||||
|
instructionCount: 0,
|
||||||
|
accountCount: 0,
|
||||||
|
feePayer: '',
|
||||||
|
recentBlockhash: '',
|
||||||
|
programs: [],
|
||||||
|
};
|
||||||
|
if (!transaction) return summary;
|
||||||
|
|
||||||
|
const isVersioned = typeof transaction?.version === 'number' || transaction instanceof VersionedTransaction;
|
||||||
|
summary.kind = isVersioned ? `versioned:${String(transaction.version)}` : 'legacy';
|
||||||
|
summary.feePayer = String(transaction?.feePayer?.toBase58?.() || '').trim();
|
||||||
|
summary.recentBlockhash = String(transaction?.recentBlockhash || transaction?.message?.recentBlockhash || '').trim();
|
||||||
|
|
||||||
|
if (isVersioned) {
|
||||||
|
const message = transaction?.message || {};
|
||||||
|
const staticKeys = Array.isArray(message?.staticAccountKeys) ? message.staticAccountKeys : [];
|
||||||
|
const instructions = Array.isArray(message?.compiledInstructions) ? message.compiledInstructions : [];
|
||||||
|
summary.instructionCount = instructions.length;
|
||||||
|
summary.accountCount = staticKeys.length;
|
||||||
|
summary.programs = instructions
|
||||||
|
.map((instruction) => staticKeys[instruction?.programIdIndex]?.toBase58?.() || '')
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 5);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instructions = Array.isArray(transaction?.instructions) ? transaction.instructions : [];
|
||||||
|
summary.instructionCount = instructions.length;
|
||||||
|
summary.accountCount = Array.isArray(transaction?.signatures) ? transaction.signatures.length : 0;
|
||||||
|
summary.programs = instructions
|
||||||
|
.map((instruction) => instruction?.programId?.toBase58?.() || '')
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 5);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRequest(method, params = {}) {
|
||||||
|
const id = `shine-wallet-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onMessage = (event) => {
|
||||||
|
if (event.source !== window) return;
|
||||||
|
const data = event.data || {};
|
||||||
|
if (data?.target !== PAGE_RESPONSE || String(data?.id || '') !== id) return;
|
||||||
|
window.removeEventListener('message', onMessage);
|
||||||
|
if (!data?.ok) {
|
||||||
|
reject(createProviderError(data?.error || 'Wallet request failed', String(data?.code || '')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(data?.result || {});
|
||||||
|
};
|
||||||
|
window.addEventListener('message', onMessage);
|
||||||
|
window.postMessage({
|
||||||
|
target: PAGE_REQUEST,
|
||||||
|
id,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
}, PAGE_MESSAGE_TARGET_ORIGIN);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeTransactionBase64(transaction) {
|
||||||
|
if (!transaction || typeof transaction.serialize !== 'function') {
|
||||||
|
throw createProviderError('Unsupported transaction object', 'UNSUPPORTED_TRANSACTION');
|
||||||
|
}
|
||||||
|
let raw;
|
||||||
|
try {
|
||||||
|
raw = transaction.serialize({ requireAllSignatures: false, verifySignatures: false });
|
||||||
|
} catch {
|
||||||
|
raw = transaction.serialize();
|
||||||
|
}
|
||||||
|
const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
|
||||||
|
return bytesToBase64(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserializeSignedTransaction(base64, originalTransaction) {
|
||||||
|
const bytes = base64ToBytes(base64);
|
||||||
|
const ctor = originalTransaction?.constructor;
|
||||||
|
if (ctor && typeof ctor.deserialize === 'function') {
|
||||||
|
return ctor.deserialize(bytes);
|
||||||
|
}
|
||||||
|
if (ctor && typeof ctor.from === 'function') {
|
||||||
|
return ctor.from(bytes);
|
||||||
|
}
|
||||||
|
if (typeof originalTransaction?.version === 'number') {
|
||||||
|
return VersionedTransaction.deserialize(bytes);
|
||||||
|
}
|
||||||
|
return Transaction.from(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShineWalletAccount {
|
||||||
|
constructor(publicKeyBase58) {
|
||||||
|
this.address = publicKeyBase58;
|
||||||
|
this.publicKey = new Uint8Array(new PublicKey(publicKeyBase58).toBytes());
|
||||||
|
this.chains = SOLANA_CHAINS.slice();
|
||||||
|
this.features = SOLANA_STANDARD_FEATURES.slice();
|
||||||
|
this.label = 'SHiNE Wallet';
|
||||||
|
this.icon = WALLET_ICON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShineProviderCore {
|
||||||
|
constructor() {
|
||||||
|
this.publicKey = null;
|
||||||
|
this.isConnected = false;
|
||||||
|
this._legacyListeners = new Map();
|
||||||
|
this._standardListeners = new Set();
|
||||||
|
this._accounts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get publicKeyBase58() {
|
||||||
|
return this.publicKey?.toBase58?.() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get standardAccounts() {
|
||||||
|
return this._accounts.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(options = {}) {
|
||||||
|
const onlyIfTrusted = !!options?.onlyIfTrusted || !!options?.silent;
|
||||||
|
const result = await createRequest('connect', { onlyIfTrusted });
|
||||||
|
const nextKey = new PublicKey(String(result?.publicKeyBase58 || '').trim());
|
||||||
|
this.publicKey = nextKey;
|
||||||
|
this.isConnected = true;
|
||||||
|
this._accounts = [new ShineWalletAccount(nextKey.toBase58())];
|
||||||
|
this.emitLegacy('connect', nextKey);
|
||||||
|
this.emitLegacy('accountChanged', nextKey);
|
||||||
|
this.emitStandardChange();
|
||||||
|
return {
|
||||||
|
publicKey: nextKey,
|
||||||
|
accounts: this.standardAccounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
await createRequest('disconnect', {});
|
||||||
|
this.isConnected = false;
|
||||||
|
this.publicKey = null;
|
||||||
|
this._accounts = [];
|
||||||
|
this.emitLegacy('disconnect');
|
||||||
|
this.emitLegacy('accountChanged', null);
|
||||||
|
this.emitStandardChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async signTransaction(transaction, comment = '') {
|
||||||
|
if (!this.publicKey) {
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
const transactionBase64 = serializeTransactionBase64(transaction);
|
||||||
|
const transactionSummary = summarizeTransaction(transaction);
|
||||||
|
const result = await createRequest('signTransaction', {
|
||||||
|
publicKeyBase58: this.publicKeyBase58,
|
||||||
|
transactionBase64,
|
||||||
|
comment: String(comment || '').trim() || `Site ${window.location.origin} requested transaction signature`,
|
||||||
|
transactionSummary,
|
||||||
|
});
|
||||||
|
return deserializeSignedTransaction(String(result?.signedTransactionBase64 || ''), transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signTransactionBytes(transactionBytes, comment = '') {
|
||||||
|
if (!this.publicKey) {
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
const transactionSummary = {
|
||||||
|
kind: 'raw-bytes',
|
||||||
|
instructionCount: 0,
|
||||||
|
accountCount: 0,
|
||||||
|
feePayer: this.publicKeyBase58,
|
||||||
|
recentBlockhash: '',
|
||||||
|
programs: [],
|
||||||
|
byteLength: Number(transactionBytes?.length || 0),
|
||||||
|
};
|
||||||
|
const result = await createRequest('signTransaction', {
|
||||||
|
publicKeyBase58: this.publicKeyBase58,
|
||||||
|
transactionBase64: bytesToBase64(transactionBytes),
|
||||||
|
comment: String(comment || '').trim() || `Site ${window.location.origin} requested transaction signature`,
|
||||||
|
transactionSummary,
|
||||||
|
});
|
||||||
|
return base64ToBytes(String(result?.signedTransactionBase64 || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
onLegacy(event, handler) {
|
||||||
|
const key = String(event || '');
|
||||||
|
if (!this._legacyListeners.has(key)) {
|
||||||
|
this._legacyListeners.set(key, new Set());
|
||||||
|
}
|
||||||
|
this._legacyListeners.get(key).add(handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
offLegacy(event, handler) {
|
||||||
|
const key = String(event || '');
|
||||||
|
const bucket = this._legacyListeners.get(key);
|
||||||
|
if (!bucket) return this;
|
||||||
|
bucket.delete(handler);
|
||||||
|
if (!bucket.size) this._legacyListeners.delete(key);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitLegacy(event, payload) {
|
||||||
|
const bucket = this._legacyListeners.get(String(event || ''));
|
||||||
|
if (!bucket?.size) return;
|
||||||
|
for (const handler of [...bucket]) {
|
||||||
|
try {
|
||||||
|
handler(payload);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onStandardChange(listener) {
|
||||||
|
this._standardListeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
this._standardListeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
emitStandardChange() {
|
||||||
|
const properties = { accounts: this.standardAccounts };
|
||||||
|
for (const listener of [...this._standardListeners]) {
|
||||||
|
try {
|
||||||
|
listener(properties);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShineSolanaProvider {
|
||||||
|
constructor(core) {
|
||||||
|
this.core = core;
|
||||||
|
this.isSHiNE = true;
|
||||||
|
this.isPhantom = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get publicKey() {
|
||||||
|
return this.core.publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConnected() {
|
||||||
|
return this.core.isConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, handler) {
|
||||||
|
return this.core.onLegacy(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event, handler) {
|
||||||
|
return this.core.offLegacy(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(event, handler) {
|
||||||
|
return this.off(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(options = {}) {
|
||||||
|
const result = await this.core.connect(options);
|
||||||
|
return { publicKey: result.publicKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
await this.core.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async signTransaction(transaction) {
|
||||||
|
return this.core.signTransaction(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signAllTransactions(transactions = []) {
|
||||||
|
const list = Array.isArray(transactions) ? transactions : [];
|
||||||
|
const outputs = [];
|
||||||
|
for (const transaction of list) {
|
||||||
|
outputs.push(await this.core.signTransaction(transaction));
|
||||||
|
}
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(args = {}) {
|
||||||
|
const method = String(args?.method || '');
|
||||||
|
const params = args?.params;
|
||||||
|
if (method === 'connect') {
|
||||||
|
return this.connect(Array.isArray(params) ? params[0] : params || {});
|
||||||
|
}
|
||||||
|
if (method === 'disconnect') {
|
||||||
|
return this.disconnect();
|
||||||
|
}
|
||||||
|
if (method === 'signTransaction') {
|
||||||
|
const tx = Array.isArray(params) ? params[0] : params?.transaction || params;
|
||||||
|
return this.signTransaction(tx);
|
||||||
|
}
|
||||||
|
if (method === 'signAllTransactions') {
|
||||||
|
const transactions = Array.isArray(params)
|
||||||
|
? params
|
||||||
|
: Array.isArray(params?.transactions) ? params.transactions : [];
|
||||||
|
return this.signAllTransactions(transactions);
|
||||||
|
}
|
||||||
|
throw createProviderError(`Unsupported request method: ${method}`, 'UNSUPPORTED_METHOD');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShineStandardWallet {
|
||||||
|
constructor(core) {
|
||||||
|
this.core = core;
|
||||||
|
this.version = '1.0.0';
|
||||||
|
this.name = 'SHiNE Wallet';
|
||||||
|
this.icon = WALLET_ICON;
|
||||||
|
this.chains = SOLANA_CHAINS.slice();
|
||||||
|
this.features = {
|
||||||
|
'standard:connect': {
|
||||||
|
version: '1.0.0',
|
||||||
|
connect: async (input = {}) => {
|
||||||
|
const result = await this.core.connect({ silent: !!input?.silent });
|
||||||
|
return { accounts: result.accounts };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'standard:disconnect': {
|
||||||
|
version: '1.0.0',
|
||||||
|
disconnect: async () => {
|
||||||
|
await this.core.disconnect();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'standard:events': {
|
||||||
|
version: '1.0.0',
|
||||||
|
on: (event, listener) => {
|
||||||
|
if (event !== 'change' || typeof listener !== 'function') {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
return this.core.onStandardChange(listener);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'solana:signTransaction': {
|
||||||
|
version: '1.0.0',
|
||||||
|
supportedTransactionVersions: ['legacy', 0],
|
||||||
|
signTransaction: async (...inputs) => {
|
||||||
|
const outputs = [];
|
||||||
|
for (const input of inputs) {
|
||||||
|
const accountAddress = String(input?.account?.address || '').trim();
|
||||||
|
if (accountAddress && this.core.publicKeyBase58 && accountAddress !== this.core.publicKeyBase58) {
|
||||||
|
throw createProviderError('Requested account does not match current wallet account', 'ACCOUNT_MISMATCH');
|
||||||
|
}
|
||||||
|
const comment = `Site ${window.location.origin} requested transaction signature`;
|
||||||
|
const signedTransaction = await this.core.signTransactionBytes(new Uint8Array(input.transaction), comment);
|
||||||
|
outputs.push({ signedTransaction });
|
||||||
|
}
|
||||||
|
return outputs;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get accounts() {
|
||||||
|
return this.core.standardAccounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerStandardWallet(wallet) {
|
||||||
|
const callback = ({ register }) => register(wallet);
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(new CustomEvent(STANDARD_REGISTER_EVENT, { detail: callback }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('wallet-standard register dispatch failed', error);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.addEventListener(STANDARD_APP_READY_EVENT, ({ detail }) => {
|
||||||
|
try {
|
||||||
|
callback(detail);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('wallet-standard app-ready callback failed', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('wallet-standard app-ready listener failed', error);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.navigator.wallets = window.navigator.wallets || [];
|
||||||
|
window.navigator.wallets.push(callback);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const core = new ShineProviderCore();
|
||||||
|
const legacyProvider = new ShineSolanaProvider(core);
|
||||||
|
const standardWallet = new ShineStandardWallet(core);
|
||||||
|
|
||||||
|
registerStandardWallet(standardWallet);
|
||||||
|
|
||||||
|
if (!window.solana) {
|
||||||
|
window.solana = legacyProvider;
|
||||||
|
window.phantom = window.phantom || {};
|
||||||
|
window.phantom.solana = legacyProvider;
|
||||||
|
window.dispatchEvent(new Event('solana#initialized'));
|
||||||
|
}
|
||||||
+10
-29
@@ -9,7 +9,7 @@ SHiNE-server — серверная часть мессенджера SHiNE: Web
|
|||||||
|
|
||||||
- `shine-server-net-server/` — точка входа, запуск HTTP/WS сервера
|
- `shine-server-net-server/` — точка входа, запуск HTTP/WS сервера
|
||||||
- `shine-server-net-protocol/` — обработчики операций (RPC и события WS)
|
- `shine-server-net-protocol/` — обработчики операций (RPC и события WS)
|
||||||
- `shine-server-db/` — DAO, SQL-схема, SQLite
|
- `shine-server-db/` — DAO, SQL-схема, PostgreSQL runtime
|
||||||
- `shine-server-blockchain/` — логика хранения и проверки блоков блокчейна
|
- `shine-server-blockchain/` — логика хранения и проверки блоков блокчейна
|
||||||
- `shine-server-crypto/` — криптографические утилиты
|
- `shine-server-crypto/` — криптографические утилиты
|
||||||
- `shine-server-config/` — конфигурация сервера
|
- `shine-server-config/` — конфигурация сервера
|
||||||
@@ -42,42 +42,23 @@ shine-UI/server-ui.html
|
|||||||
Для обновления — только root + device (blockchain-ключ не нужен).
|
Для обновления — только root + device (blockchain-ключ не нужен).
|
||||||
|
|
||||||
Актуальные адреса программ Solana (devnet):
|
Актуальные адреса программ Solana (devnet):
|
||||||
- `shine_users`: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
- `shine_users`: `SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6`
|
||||||
- `shine_payments`: `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`
|
- `shine_payments`: `SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v`
|
||||||
|
|
||||||
Подробнее: `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
Подробнее: `docs/Инициализация_Solana_регистрации/README.md`
|
||||||
|
|
||||||
## Синхронизация с партнёрскими серверами
|
## Синхронизация с партнёрскими серверами
|
||||||
|
|
||||||
Сервер должен синхронизировать блоки блокчейна и DM с серверами-партнёрами из `sync_servers`.
|
Сервер должен синхронизировать блоки блокчейна и DM с серверами-партнёрами из `sync_servers`.
|
||||||
Детали: `Dev_Docs/Blockchain/sync-between-servers.md`
|
Детали: `docs/Blockchain/sync-between-servers.md`
|
||||||
|
|
||||||
## Деплой
|
## Деплой
|
||||||
|
|
||||||
```
|
- Основные инструкции по деплою находятся в `../deploy/AGENTS.md`.
|
||||||
./gradlew deployServer
|
- Deploy выполнять shell-скриптами из `../deploy/scripts/`.
|
||||||
./gradlew deployUI
|
- Gradle deploy-задачи не использовать: Gradle остаётся для сборки и локального запуска.
|
||||||
```
|
- Любые изменения на production (`shineup.me`, `server2.shineup.me`) делать только после отдельного явного подтверждения пользователя.
|
||||||
|
- Перед production deploy обязательно обновить/проверить backup в `deploy/backup/archive/`.
|
||||||
Default deploy по умолчанию идёт на `test2.shineup.me` (`player@193.8.215.70`).
|
|
||||||
|
|
||||||
Production deploy:
|
|
||||||
|
|
||||||
```
|
|
||||||
./gradlew deployServerProduction
|
|
||||||
./gradlew deployUIProduction
|
|
||||||
```
|
|
||||||
|
|
||||||
Любые изменения на `shineup.me` делать только после отдельного явного подтверждения пользователя.
|
|
||||||
|
|
||||||
Резервный test-контур:
|
|
||||||
|
|
||||||
```
|
|
||||||
./gradlew deployServerTest
|
|
||||||
./gradlew deployUITest
|
|
||||||
```
|
|
||||||
|
|
||||||
`test.shineup.me` пока не использовать для обычного deploy.
|
|
||||||
|
|
||||||
Логи на проде:
|
Логи на проде:
|
||||||
- `/home/player/SHiNE/shine-server/logs/app.log`
|
- `/home/player/SHiNE/shine-server/logs/app.log`
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
+11
-1
@@ -36,7 +36,12 @@ public final class BodyRecordParser {
|
|||||||
case TextBody.KEY -> {
|
case TextBody.KEY -> {
|
||||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF)
|
if (st == (MsgSubType.TEXT_POST & 0xFFFF)
|
||||||
|| st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
|| st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||||
|| st == (MsgSubType.TEXT_REPOST & 0xFFFF)) {
|
|| st == (MsgSubType.TEXT_EXERCISE & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.TEXT_SERVICE & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.TEXT_COURSE & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.TEXT_ENTRYPOINT & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)) {
|
||||||
yield new TextLineBody(subType, version, bodyBytes);
|
yield new TextLineBody(subType, version, bodyBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +50,17 @@ public final class BodyRecordParser {
|
|||||||
yield new TextReplyBody(subType, version, bodyBytes);
|
yield new TextReplyBody(subType, version, bodyBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_RATING & 0xFFFF)) {
|
||||||
|
yield new TextRatingBody(subType, version, bodyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("Unknown TEXT subType for type=1 ver=1: subType=" + st);
|
throw new IllegalArgumentException("Unknown TEXT subType for type=1 ver=1: subType=" + st);
|
||||||
}
|
}
|
||||||
|
|
||||||
case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
|
case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
|
||||||
case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
||||||
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
||||||
|
case StatusActionBody.KEY -> new StatusActionBody(subType, version, bodyBytes);
|
||||||
|
|
||||||
default -> throw new IllegalArgumentException(String.format(
|
default -> throw new IllegalArgumentException(String.format(
|
||||||
"Unknown body type/version from header: type=%d ver=%d subType=%d",
|
"Unknown body type/version from header: type=%d ver=%d subType=%d",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user