Синхронизировать изменения проекта

This commit is contained in:
AidarKC
2026-07-05 18:18:03 +04:00
parent 0240db59ee
commit 81bef1e1cc
36 changed files with 573 additions and 335 deletions
+9 -7
View File
@@ -135,17 +135,19 @@
- После подтверждения, что фича проверена и работает корректно, соответствующий файл удалять.
- В `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`, затем при необходимости конкретные файлы из горизонтов.
## Будущие фичи / TODO
- Папка для задач, сознательно отложенных на будущее: `TODO/`.
- Точка входа по планам: `TODO/README.md`.
- Внутри планы разделены по горизонтам: `near/`, `medium/`, `far/` и тематическим подпапкам.
- Если пользователь спрашивает, какие есть планы или что можно продолжить, сначала читать `TODO/README.md`, затем при необходимости конкретные файлы из подпапок.
- Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя.
- Если часть кода временно отключена или закомментирована, в файле будущей фичи подробно описывать:
- Старую папку `Dev_Docs/Future_Features/` считать выведенной из использования и больше не использовать для новых записей.
- Если часть кода временно отключена или закомментирована, либо удалена как временная заглушка, в TODO-файле подробно описывать:
- какие файлы и участки отключены;
- что осталось в коде как заготовка;
- какие документы нужно обновить при возврате;
- с какого сценария продолжать разработку.
- с какого сценария продолжать разработку;
- из какого коммита брать последнюю полную реализацию.
## Коммуникация по новым задачам (обязательно)
- При получении нового задания сначала кратко пересказать задачу своими словами.
@@ -37,9 +37,15 @@
- отображаются новые program id;
- дефолтный Solana RPC теперь mainnet.
6. Временный режим регистрации:
- в основном UI логины длиной 5-7 символов без временного кода не проходят;
- в основном UI логины длиной 5-7 символов с временным кодом проходят локальную проверку;
- в основном UI любой другой временный код отклоняется сразу;
- ESP32 и прямой on-chain вызов `shine_users` полагаются на текущий временный `shine_login_guard`, то есть валидные логины длиной от 5 символов допускаются без словарной классификации.
## Ожидаемый результат
- Во всех перечисленных поверхностях используются новые mainnet program id.
- Запросы по умолчанию идут в mainnet, кроме явно devnet-утилит.
- Нет обращений к старым devnet program id.
- Временный UI-барьер для коротких логинов работает так, как задумано, и не ломает обычную регистрацию для логинов от 8 символов.
@@ -0,0 +1,42 @@
# Временный режим коротких логинов
- статус: `pending`
## Кратко
Временно упрощён `shine_login_guard`:
- on-chain допускаются любые валидные логины длиной от `5` символов;
- словарная premium/trademark-классификация временно отключена.
Поверх этого основной UI вводит временное ограничение:
- логины длиной `8+` проходят обычную проверку;
- логины длиной `5..7` допускаются только при вводе временного кода;
- любой другой код отклоняется сразу.
ESP32 в этом временном режиме не ограничивается UI-кодом и использует только on-chain правила.
## Что проверять
1. Основной UI:
- логин длиной `4` символа отклоняется;
- логин длиной `5..7` без временного кода отклоняется;
- логин длиной `5..7` с корректным временным кодом проходит локальную проверку;
- логин длиной `5..7` с любым другим кодом отклоняется;
- логин длиной `8+` проходит без временного кода.
2. Solana on-chain:
- `shine_login_guard` возвращает `CLASS_FREE` для валидных логинов длиной от `5`;
- `shine_login_guard` возвращает `CLASS_PREMIUM` для логинов короче `5` или с невалидными символами.
3. ESP32 / прямой вызов:
- валидный логин длиной `5+` регистрируется без временного кода;
- логин короче `5` не регистрируется.
## Ожидаемый результат
- Основной UI удерживает обычных пользователей от коротких логинов без временного кода.
- Свои пользователи могут временно регистрировать короткие логины через UI-код.
- ESP32 продолжает работать без отдельной промо-логики.
- On-chain логика остаётся простой и компактной.
+4 -4
View File
@@ -16,7 +16,7 @@
В Solana-модуле сейчас три основные программы:
1. `shine_login_guard`проверяет логин и возвращает класс логина: обычный, premium или trademark.
1. `shine_login_guard`в текущем временном режиме проверяет базовую валидность логина и пропускает on-chain любые валидные логины длиной от 5 символов.
2. `shine_users` — создает и обновляет пользовательскую PDA-запись, проверяет подписи и берет оплату за регистрацию/увеличение лимита.
3. `shine_payments` — принимает входящий поток средств в `inflow_vault`, ведет очереди тикетов, позволяет DAO выдавать лимиты менеджерам и выполняет выплаты.
@@ -35,7 +35,7 @@ DAO в текущем виде не является отдельной Anchor-
| Блок | Папка/имя | Текущие функции из кода | Основной смысл |
| --- | --- | --- | --- |
| 1 | `shine_login_guard` | `classify_login` | Проверка логина перед регистрацией. |
| 1 | `shine_login_guard` | `classify_login` | Временная on-chain проверка логина перед регистрацией. |
| 2 | `shine_users` | `init_users_economy_config`, `update_users_economy_config`, `create_user_pda`, `update_user_pda` | Регистрация пользователя, обновление записи, экономика лимита. |
| 3 | `shine_payments` | `init`, `update_coef_limit`, `grant_manager_limits`, `buy_ticket`, `buy_ticket_usd`, `buy_ticket_sol`, `manager_add_ticket`, `step_payout`, `change_ticket_recipient` | Vault, билеты, очереди, выплаты, DAO-настройки, лимиты менеджеров. |
| DAO | governance/authority | Вызовы через governance и управляющие ключи | Управление правами, казной, настройками и будущими обновлениями программ. |
@@ -62,7 +62,7 @@ DAO в текущем виде не является отдельной Anchor-
Для удобного понимания на старте можно считать, что есть четыре группы ключей:
1. `key_1` / authority программы `shine_login_guard`.
- Сейчас программа только классифицирует логин.
- Сейчас программа только выполняет временную базовую проверку логина.
- На первом этапе ее можно оставить под отдельным ключом.
- В будущем право обновления можно передать DAO.
@@ -140,7 +140,7 @@ Solana-модуль лежит в основном репозитории как
1. `shine_login_guard`
- Пока оставить на отдельном ключе `key_1`.
- Передачу DAO сделать позже, когда логика premium/trademark стабилизируется.
- Передачу DAO сделать позже, когда временный режим будет заменён на постоянную логику промокодов и/или premium/trademark.
2. `shine_users`
- Economy-настройки уже должны обновляться DAO-authority.
@@ -6083,6 +6083,13 @@ static void editorKeyCb(lv_event_t *event) {
rebuildScreen();
return;
}
if (strcmp(token, "<SPACE>") == 0) {
keepCursorAtEnd();
lv_textarea_add_text(gInputTextArea, " ");
syncEditValueFromTextarea();
keepCursorAtEnd();
return;
}
String out = token;
if (gKeyboardMode == KEYBOARD_MODE_ALPHA && gKeyboardShift) {
@@ -7253,10 +7260,10 @@ static void drawKeyRow(const char *const *tokens,
lv_coord_t h,
lv_coord_t gap,
uint32_t bgColor,
bool uppercase = false,
bool uppercase = false,
const lv_font_t *font = &lv_font_montserrat_18) {
for (int i = 0; i < count; ++i) {
String label = tokens[i];
String label = strcmp(tokens[i], "<SPACE>") == 0 ? String("SPACE") : String(tokens[i]);
if (uppercase) {
label.toUpperCase();
}
@@ -7276,8 +7283,8 @@ static void drawTextEditorKeyboard(lv_coord_t originY) {
static const char *symLeftRow1[] = {"://", "/", ":", "_", "@"};
static const char *symLeftRow2[] = {".com", ".net", ".org", ".me", ".io"};
static const char *symRightRow0[] = {"6", "7", "8", "9", "0"};
static const char *symRightRow1[] = {"?", "&", "=", "+", "-"};
static const char *symRightRow2[] = {"%", "#", "~", ":", "/"};
static const char *symRightRow1[] = {"?", "!", "=", "+", "-"};
static const char *symRightRow2[] = {"<SPACE>", "#", "~", ":", "/"};
const lv_coord_t keyH = 62;
const lv_coord_t keyW5 = 84;
@@ -259,6 +259,7 @@
- переключение страниц выполняется свайпом влево/вправо внутри `TEXT_EDIT_SCREEN`;
- страница 1 содержит в основном буквы и базовые URL-символы;
- страница 2 содержит цифры и дополнительные URL/символьные кнопки;
- на символьных страницах должны быть доступны как минимум `!` и отдельная кнопка `SPACE`;
- переключение `ABC/123` и `SHIFT` не должно очищать уже введённый текст;
- переключение страниц клавиатуры свайпом тоже не должно очищать уже введённый текст, включая цифры, символы и уже набранные заглавные буквы;
- есть специальные действия `DEL` и `CLR`.
@@ -308,6 +309,7 @@
- половина букв находится на левой странице, половина на правой;
- на правой странице кнопки тоже стоят в ровных колонках, без сдвига рядов вправо;
- отдельный режим `symbols` тоже разделён на 2 страницы;
- в режиме `symbols` на правой странице есть отдельная кнопка `SPACE` и отдельная кнопка `!`;
- 3 основных ряда клавиш и нижний служебный ряд увеличены по высоте;
- клавиши дополнительно увеличены по высоте по сравнению с предыдущим промежуточным вариантом;
- четвёртый ряд содержит:
+2 -2
View File
@@ -48,13 +48,13 @@
- Если пользователю нужны точные команды, хэши или подробности, они должны оставаться в текстовом ответе.
## Планы и отложенные фичи
- Планы проекта по отложенным фичам хранятся в `Dev_Docs/Future_Features/`.
- Планы проекта по отложенным фичам хранятся в `TODO/`.
- Внутри есть три горизонта:
- `near/` - ближайшие планы, обычно сегодня/завтра;
- `medium/` - среднесрочные планы, обычно недели или 1-2 месяца;
- `far/` - дальнее будущее без понятного срока.
- Если пользователь спрашивает, какие есть планы или что можно продолжить, нужно смотреть эти три папки и отвечать кратким списком по горизонтам.
- Файлы из `Dev_Docs/Future_Features/` не начинать реализовывать без явной команды пользователя.
- Файлы из `TODO/` не начинать реализовывать без явной команды пользователя.
- После реализации фичи, требующей ручной проверки, нужно добавить отдельный файл в `Dev_Docs/Pending_Features/`.
## Центр задач и предложений
+3 -3
View File
@@ -11,14 +11,14 @@
- При изменении логики сервиса сначала читать `AGENT.md`, затем код `py_bot_service.py`.
## Планы и задачи
- Отложенные задачи проекта лежат в `../Dev_Docs/Future_Features/`.
- Точка входа по планам: `../Dev_Docs/Future_Features/README.md`.
- Отложенные задачи проекта лежат в `../TODO/`.
- Точка входа по планам: `../TODO/README.md`.
- Горизонты планов:
- `near/` - ближайшие планы;
- `medium/` - среднесрочные планы;
- `far/` - дальнее будущее.
- Если пользователь спрашивает, какие есть планы или что можно продолжить, кратко перечислять задачи по этим горизонтам.
- Не начинать реализацию задач из `Future_Features` без явной команды пользователя.
- Не начинать реализацию задач из `TODO` без явной команды пользователя.
## Проверка после изменений
- Если меняется логика Telegram-бота, проверить локальный запуск или self-test, когда это уместно.
@@ -1,7 +1,7 @@
import { base64ToBytes } from './crypto-utils.js';
import { PublicKey } from './vendor/solana-publickey-bundle.js';
const SOLANA_ENDPOINT_DEFAULT = 'https://api.mainnet-beta.solana.com';
const SOLANA_ENDPOINT_DEFAULT = 'https://solana-rpc.publicnode.com';
const SHINE_USERS_PROGRAM_ID = 'SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6';
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
const DEFAULT_SHINE_SERVER_LOGIN = 'shineupme';
@@ -19,12 +19,11 @@ QR-подключение других устройств сейчас есть
## Откуда продолжать
- от текущих документов в `Dev_Docs/Future_Features/medium/`;
- от текущих документов в `TODO/medium/`;
- отдельно проверить, какие поля уже есть в PDA и UI.
## Какие документы потом обновить
- `Dev_Docs/Solana_Architecture/README.md`;
- `Dev_Docs/Future_Features/medium/2026-06-03_подключение_других_устройств_через_qr.md`;
- `Dev_Docs/Future_Features/medium/2026-06-02_сессионные_homeserver_в_pda.md`.
- `TODO/medium/2026-06-03_подключение_других_устройств_через_qr.md`;
- `TODO/medium/2026-06-02_сессионные_homeserver_в_pda.md`.
@@ -18,12 +18,11 @@
## Откуда продолжать
- от `Dev_Docs/Future_Features/medium/2026-05-26_0029_esp32s3_file_storage.md`;
- от `TODO/medium/2026-05-26_0029_esp32s3_file_storage.md`;
- от документации по ESP32 UI homeserver.
## Какие документы потом обновить
- `Dev_Docs/Future_Features/medium/2026-05-26_0029_esp32s3_file_storage.md`;
- `Dev_Docs/Future_Features/README.md`;
- `TODO/medium/2026-05-26_0029_esp32s3_file_storage.md`;
- `TODO/README.md`;
- документацию по ESP32 UI homeserver, если добавятся экраны или статусы.
+1
View File
@@ -12,6 +12,7 @@
- откуда продолжать;
- какие документы потом надо обновить.
- Это не активная разработка. Тут только план и контекст.
- Старую папку `Dev_Docs/Future_Features/` считать архивной и больше не использовать как источник новых задач.
## Текущие задачи
@@ -86,7 +86,7 @@
- `Dev_Docs/Blockchain/CHANGELOG.md`;
- `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md`;
- документы API чтения каналов/тредов, если изменятся поля ответа.
9. После реализации перенести задачу из `Dev_Docs/Future_Features/` в `Dev_Docs/Pending_Features/` как фичу, требующую ручной проверки.
9. После реализации перенести задачу из `TODO/` в `Dev_Docs/Pending_Features/` как фичу, требующую ручной проверки.
## Минимальный чек-лист ручной проверки в будущем
@@ -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>/` без отдельного подтверждения.
@@ -0,0 +1,182 @@
# Восстановить полную логику `shine_login_guard`
Статус: отложено.
## Зачем это нужно
Сейчас в проекте включена временная заглушка для регистрации коротких логинов:
- on-chain `shine_login_guard` пропускает любые валидные логины длиной от `5` символов;
- словарная `premium/trademark`-классификация временно отключена;
- основной UI удерживает обычных пользователей от логинов `5..7` через временный код;
- это сделано только до внедрения полноценной схемы продажи/выдачи красивых имён и промокодов.
Когда полноценная логика будет готова, нужно вернуть полный режим `shine_login_guard`.
## Что именно было в полной версии
Последняя полная словарная реализация зафиксирована в коммите:
- `0240db5``Подготовка перед деплоем в mainnet`
Ключевой файл полной реализации:
- `shine-solana/shine/programs/shine_login_guard/src/lib.rs`
Как быстро посмотреть старую версию:
```bash
git show 0240db5:shine-solana/shine/programs/shine_login_guard/src/lib.rs
```
## Как работала старая логика
Полная версия делала следующее:
1. Нормализовала логин:
- удаляла `_`;
- требовала только ASCII alnum;
- приводила к lower-case;
- запрещала пустой логин и логин длиннее `20`.
2. Пыталась разбить логин на `1..3` словарных слова:
- premium-слова;
- trademark-слова.
3. Приоритеты классов были такие:
- если найден хотя бы один trademark-компонент -> `CLASS_TRADEMARK`;
- если найден только premium-набор -> `CLASS_PREMIUM`;
- если словарного матча нет и длина нормализованного логина `<= 7` -> `CLASS_PREMIUM`;
- иначе -> `CLASS_FREE`.
4. Для разбивки использовался DFS/backtracking по префиксам строки.
## Какие файлы участвовали в полной логике
### Обязательно восстановить или проверить
1. Код программы:
- `shine-solana/shine/programs/shine_login_guard/src/lib.rs`
2. Генерация словаря:
- `shine-solana/shine/programs/shine_login_guard/build.rs`
3. Словари:
- `shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/**/*.txt`
- `shine-solana/shine/programs/shine_login_guard/src/dictionaries/trademarks/**/*.txt`
4. Документация программы:
- `shine-solana/shine/doc/programs/shine_login_guard.md`
5. Архитектурная документация:
- `Dev_Docs/Solana_Architecture/README.md`
6. UI-логика precheck:
- `shine-UI/js/pages/register-view.js`
- `shine-UI/js/pages/registration-payment-view.js`
## Что именно сейчас временно удалено из `lib.rs`
Временная упрощённая версия убрала:
- `mod wordlist { include!(...) }`
- `MAX_WORDS_PER_LOGIN`
- `classify_split(...)`
- `is_premium_word(...)`
- `is_trademark_word(...)`
- словарную DFS-сегментацию
- fallback `len <= 7 => premium` в пользу временного `len < 5 => premium`
Сейчас `classify(login)` делает только:
1. нормализацию;
2. проверку минимальной длины `5`;
3. возврат `CLASS_FREE` для всего остального.
## Как вернуть полную логику
### Минимальный путь
1. Взять старую версию `lib.rs` из коммита `0240db5`.
2. Вернуть в файл:
- `mod wordlist`
- `MAX_WORDS_PER_LOGIN`
- `classify_split`
- `is_premium_word`
- `is_trademark_word`
3. Вернуть старые тесты и при необходимости дописать новые.
4. Проверить, что `build.rs` всё ещё генерирует `generated_dictionary.rs` в прежнем формате.
5. Сверить актуальность словарей в `src/dictionaries`.
6. Обновить `shine_login_guard.md` обратно под словарную логику.
7. Обновить `Dev_Docs/Solana_Architecture/README.md`.
### Проверка после возврата
Нужно убедиться, что:
- trademark-логины снова помечаются как `CLASS_TRADEMARK`;
- premium-логины снова помечаются как `CLASS_PREMIUM`;
- короткие логины `<= 7` без словарного совпадения снова считаются premium;
- UI больше не использует временный код для коротких логинов.
## Что поменять в UI при возврате
После возврата полной on-chain логики нужно убрать временный клиентский костыль:
1. Из `shine-UI/js/pages/register-view.js` удалить:
- `TEMP_MIN_LOGIN_WITHOUT_PROMO`
- `TEMP_ABSOLUTE_MIN_LOGIN_LEN`
- `TEMP_PROMO_HASH_HEX`
- локальную проверку временного кода
- временные тексты про код для логинов `5..7`
2. В `shine-UI/js/pages/registration-payment-view.js` пересмотреть временную строку:
- сейчас `promoCode: ''` передаётся принудительно;
- после внедрения нормальной promo/on-chain схемы это место надо вернуть к реальной логике промокодов.
## Что учитывать по размеру программы
Текущая временная версия сильно уменьшила размер `shine_login_guard.so`.
При возврате полной словарной логики:
- бинарь снова заметно вырастет;
- потребуется больший `programdata` rent;
- для апгрейда понадобится больший временный buffer.
Перед апгрейдом стоит отдельно:
1. собрать `.so`;
2. измерить размер;
3. проверить `solana rent <size>`;
4. убедиться, что на кошельке достаточно SOL для upgrade buffer.
## Что ещё проверить перед возвратом
1. Не осталось ли временных пометок в документации и UI.
2. Не завязаны ли новые сценарии на допуск логинов длиной `5..7`.
3. Готова ли полноценная схема:
- продажа красивых имён;
- промокоды;
- менеджеры/селлеры;
- DAO-квоты;
- или другая финальная модель, которая заменит временный режим.
## Откуда продолжать
Практически продолжать так:
```bash
git show 0240db5:shine-solana/shine/programs/shine_login_guard/src/lib.rs
git show 0240db5:shine-solana/shine/programs/shine_login_guard/build.rs
git show 0240db5:shine-solana/shine/doc/programs/shine_login_guard.md
```
Потом:
1. вернуть код;
2. обновить UI;
3. обновить документы;
4. собрать `cargo test`;
5. собрать `cargo build-sbf`;
6. только потом делать апгрейд on-chain программы.
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.2.294
server.version=1.2.274
client.version=1.2.295
server.version=1.2.275
-24
View File
@@ -1,24 +0,0 @@
# SHiNE Predeploy Servers
## Access policy
- VPS-05 (`45.136.124.227`): use `player@45.136.124.227` for regular deployment operations.
- TURN shared secret (both TURN nodes): `def6d444734d380d2f67a9d345b1debf985eaba0973c343e392c060d97c30106`
## Servers
1. `VPS-02` (legacy): `root@194.87.0.247`
- Legacy production host.
- caddy installed.
- coturn installed.
2. `VPS-05` (target): `root@45.136.124.227`
- New migration target.
- User `player` created, in `sudo`.
- SHiNE base dir: `/home/player/SHiNE`.
- TURN config path: `/etc/turnserver.conf` (coturn), work docs/scripts in `/home/player/SHiNE/coturn`.
3. `promo-node-93` (TURN node): `ubuntu@93.170.12.154`
- TURN installed: `coturn` active.
- TURN ports: `3478/tcp`, `3478/udp`, `5349/tcp`, `5349/udp`.
- TURN relay ports: `49152-50152/udp`.
- TURN config path: `/etc/turnserver.conf`.
- SHiNE dir for player: `/home/player/SHiNE/coturn`.
- Access user `player` created with sudo and SSH key copied from `ubuntu`.
- Legacy reverse SSH tunnel to `194.87.0.247:1200` was disabled (`reverse-ssh.service`).
+92 -21
View File
@@ -13,10 +13,39 @@ import {
PASSWORD_MAX_LENGTH,
PASSWORD_WORDS_COUNT,
} from '../services/password-words.js';
import { sha256Text } from '../services/crypto-utils.js';
import { openRegistrationFaq } from './registration-faq-view.js';
import { defaultServerHttp } from '../deploy-config.js';
export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false };
// ВРЕМЕННАЯ UI-ЗАГЛУШКА:
// пока полноценная логика продажи/выдачи коротких имён не внедрена on-chain,
// UI пускает логины 5..7 символов только по временному коду.
const TEMP_MIN_LOGIN_WITHOUT_PROMO = 8;
const TEMP_ABSOLUTE_MIN_LOGIN_LEN = 5;
const TEMP_PROMO_HASH_HEX = 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3';
function bytesToHex(bytes) {
return Array.from(bytes || [], (b) => b.toString(16).padStart(2, '0')).join('');
}
async function isTemporaryPromoAccepted(value) {
const digest = await sha256Text(String(value || ''));
return bytesToHex(digest) === TEMP_PROMO_HASH_HEX;
}
function normalizeLoginForTemporaryUiGuard(login) {
const source = String(login || '').trim();
if (!source || source.length > 20) return null;
let normalized = '';
for (const ch of source) {
if (ch === '_') continue;
if (!/[0-9A-Za-z]/.test(ch)) return null;
normalized += ch.toLowerCase();
}
if (!normalized || normalized.length > 20) return null;
return normalized;
}
function createWordsLayout({ words, onInput }) {
const section = document.createElement('div');
@@ -148,7 +177,7 @@ export function render({ navigate }) {
const promoHint = document.createElement('p');
promoHint.className = 'meta-muted';
promoHint.textContent = 'Если промокод включён, обычная проверка premium/trademark логина пропускается.';
promoHint.textContent = 'Временный режим: логины длиной 5-7 символов доступны только по специальному коду. Любое другое значение считается неверным промокодом.';
promoField.append(promoFieldLabel, promoInput, promoHint);
@@ -247,26 +276,58 @@ export function render({ navigate }) {
async function runAvailabilityCheck() {
const login = loginInput.value.trim();
const promoCode = String(promoInput.value || '').trim();
if (!login) {
statusText.textContent = 'Введите логин';
formError.style.display = 'none';
return false;
}
if (usePromoCode) {
lastCheckedLogin = login;
lastCheckedFree = true;
lastCheckedClassName = 'promo';
statusText.textContent = 'Промокод включён: обычная проверка premium/trademark логина пропущена';
statusText.className = 'status-line is-available';
const normalizedLogin = normalizeLoginForTemporaryUiGuard(login);
if (!normalizedLogin) {
statusText.textContent = 'Логин содержит недопустимые символы или имеет неверную длину ❌';
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return true;
return false;
}
if (normalizedLogin.length < TEMP_ABSOLUTE_MIN_LOGIN_LEN) {
statusText.textContent = `Логин должен быть не короче ${TEMP_ABSOLUTE_MIN_LOGIN_LEN} символов ❌`;
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return false;
}
const wantsShortLogin = normalizedLogin.length < TEMP_MIN_LOGIN_WITHOUT_PROMO;
let promoAccepted = false;
if (usePromoCode) {
if (!promoCode) {
statusText.textContent = 'Введите промокод ❌';
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return false;
}
promoAccepted = await isTemporaryPromoAccepted(promoCode);
if (!promoAccepted) {
statusText.textContent = 'Неверный промокод ❌';
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return false;
}
} else if (wantsShortLogin) {
statusText.textContent = `Логины короче ${TEMP_MIN_LOGIN_WITHOUT_PROMO} символов временно доступны только по специальному коду ❌`;
statusText.className = 'status-line is-unavailable';
formError.style.display = 'none';
return false;
}
if (login === lastCheckedLogin) {
if (!lastCheckedFree) {
statusText.textContent = 'Логин уже занят ❌';
statusText.className = 'status-line is-unavailable';
} else if (lastCheckedClassName === 'promo') {
statusText.textContent = 'Логин свободен ✅ Временный код принят';
statusText.className = 'status-line is-available';
} else if (lastCheckedClassName === 'free') {
statusText.textContent = 'Логин свободен ✅';
statusText.className = 'status-line is-available';
@@ -281,7 +342,7 @@ export function render({ navigate }) {
statusText.className = 'status-line is-unavailable';
}
formError.style.display = 'none';
return lastCheckedFree && lastCheckedClassName === 'free';
return lastCheckedFree && (lastCheckedClassName === 'free' || lastCheckedClassName === 'promo');
}
checkButton.disabled = true;
@@ -295,15 +356,19 @@ export function render({ navigate }) {
let className = '';
let precheckWarning = '';
if (isFree) {
try {
const precheck = await precheckLoginClassOnSolana({
login,
solanaEndpoint: state.entrySettings.solanaServer,
});
className = precheck.className;
} catch (precheckError) {
className = 'free';
precheckWarning = formatSolanaErrorDetails(precheckError);
if (promoAccepted) {
className = 'promo';
} else {
try {
const precheck = await precheckLoginClassOnSolana({
login,
solanaEndpoint: state.entrySettings.solanaServer,
});
className = precheck.className;
} catch (precheckError) {
className = 'free';
precheckWarning = formatSolanaErrorDetails(precheckError);
}
}
}
lastCheckedLogin = login;
@@ -312,6 +377,9 @@ export function render({ navigate }) {
if (!isFree) {
statusText.textContent = 'Логин уже занят ❌';
statusText.className = 'status-line is-unavailable';
} else if (className === 'promo') {
statusText.textContent = 'Логин свободен ✅ Временный код принят';
statusText.className = 'status-line is-available';
} else if (className === 'free') {
statusText.textContent = precheckWarning
? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})`
@@ -328,7 +396,7 @@ export function render({ navigate }) {
statusText.className = 'status-line is-unavailable';
}
formError.style.display = 'none';
return isFree && className === 'free';
return isFree && (className === 'free' || className === 'promo');
} catch (error) {
const base = toUserMessage(error, 'Не удалось проверить логин');
const details = formatSolanaErrorDetails(error);
@@ -379,7 +447,7 @@ export function render({ navigate }) {
updatePromoVisibility();
syncDraftState();
if (usePromoCode) {
statusText.textContent = 'Промокод включён: проверка логина будет пропущена';
statusText.textContent = 'Временный код включён: для логинов 5-7 символов будет локальная проверка';
statusText.className = 'status-line is-available';
} else {
statusText.textContent = 'Проверка логина: не выполнена';
@@ -388,6 +456,9 @@ export function render({ navigate }) {
});
promoInput.addEventListener('input', () => {
lastCheckedLogin = '';
lastCheckedFree = false;
lastCheckedClassName = '';
syncDraftState();
});
@@ -395,7 +466,7 @@ export function render({ navigate }) {
formError.style.display = 'none';
const promoCode = String(promoInput.value || '').trim();
if (usePromoCode && !promoCode) {
formError.textContent = 'Если включён промокод, поле промокода должно быть заполнено.';
formError.textContent = 'Если включён временный код, поле должно быть заполнено.';
formError.style.display = '';
return;
}
@@ -266,7 +266,7 @@ export function render({ navigate }) {
keyBundle,
solanaEndpoint: state.entrySettings.solanaServer,
accessServers: [state.entrySettings.shineServerLogin || defaultServerLogin],
promoCode: state.registrationDraft.usePromoCode ? state.registrationDraft.promoCode : '',
promoCode: '',
});
} catch (solanaError) {
const solanaMsg = formatSolanaErrorDetails(solanaError);
+2 -1
View File
@@ -1,5 +1,6 @@
export const SOLANA_CLUSTER = 'mainnet-beta';
export const SOLANA_ENDPOINT_DEFAULT = 'https://api.mainnet-beta.solana.com';
// Для браузерного UI используем endpoint, который не режет JSON-RPC по Origin.
export const SOLANA_ENDPOINT_DEFAULT = 'https://solana-rpc.publicnode.com';
// Единый файл актуальных Solana Program ID для UI-части SHiNE.
// При смене адресов обновлять этот файл и все его потребители внутри shine-UI.
+18 -3
View File
@@ -92,6 +92,21 @@ const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
const DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
function normalizeStoredSolanaServer(value) {
const raw = String(value || '').trim();
if (!raw) return DEFAULT_SOLANA_SERVER;
const normalized = raw.replace(/\/+$/u, '').toLowerCase();
if (
normalized === 'https://api.devnet.solana.com'
|| normalized === 'http://api.devnet.solana.com'
|| normalized === 'https://api.mainnet-beta.solana.com'
|| normalized === 'http://api.mainnet-beta.solana.com'
) {
return DEFAULT_SOLANA_SERVER;
}
return raw;
}
export function normalizeDmChatId(value) {
return String(value || '').trim().toLowerCase();
}
@@ -182,7 +197,7 @@ function persistEntrySettings(settings) {
try {
const payload = {
language: String(settings?.language || 'ru'),
solanaServer: String(settings?.solanaServer || DEFAULT_SOLANA_SERVER),
solanaServer: normalizeStoredSolanaServer(settings?.solanaServer),
shineServer: String(settings?.shineServer || DEFAULT_SHINE_SERVER),
shineServerLogin: String(settings?.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE),
shineServerHttp: String(settings?.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE),
@@ -249,7 +264,7 @@ function createInitialState({ withStoredSession = true } = {}) {
startHint: '',
entrySettings: {
language: String(storedEntrySettings?.language || 'ru'),
solanaServer: String(storedEntrySettings?.solanaServer || DEFAULT_SOLANA_SERVER),
solanaServer: normalizeStoredSolanaServer(storedEntrySettings?.solanaServer),
shineServer: String(LOCAL_WS_OVERRIDE_URL || storedEntrySettings?.shineServer || initialShineServer),
shineServerLogin: String(storedEntrySettings?.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE),
shineServerHttp: String(storedEntrySettings?.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE),
@@ -718,7 +733,7 @@ export function checkServerAvailability(address) {
}
export async function saveEntrySettings(nextSettings) {
const nextSolanaServer = String(nextSettings?.solanaServer || state.entrySettings.solanaServer || DEFAULT_SOLANA_SERVER);
const nextSolanaServer = normalizeStoredSolanaServer(nextSettings?.solanaServer || state.entrySettings.solanaServer || DEFAULT_SOLANA_SERVER);
const nextShineServerLogin = String(nextSettings?.shineServerLogin || state.entrySettings.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE).trim().toLowerCase()
|| DEFAULT_SHINE_SERVER_LOGIN_VALUE;
let forcedShineServer = LOCAL_WS_OVERRIDE_URL || '';
+2 -2
View File
@@ -48,8 +48,8 @@
<h2>Параметры Solana</h2>
<div class="field">
<label>Solana Endpoint</label>
<input type="text" id="endpoint" value="https://api.mainnet-beta.solana.com" />
<div class="hint">mainnet: https://api.mainnet-beta.solana.com · devnet: https://api.devnet.solana.com</div>
<input type="text" id="endpoint" value="https://solana-rpc.publicnode.com" />
<div class="hint">mainnet: https://solana-rpc.publicnode.com · devnet: https://api.devnet.solana.com</div>
</div>
</div>
+1 -1
View File
@@ -52,7 +52,7 @@
<h2>Параметры Solana</h2>
<div class="field">
<label>Solana Endpoint</label>
<input type="text" id="endpoint" value="https://api.mainnet-beta.solana.com" />
<input type="text" id="endpoint" value="https://solana-rpc.publicnode.com" />
</div>
</div>
+1 -1
View File
@@ -56,7 +56,7 @@
<h2>Параметры Solana</h2>
<div class="field">
<label>Solana Endpoint</label>
<input type="text" id="endpoint" value="https://api.mainnet-beta.solana.com" />
<input type="text" id="endpoint" value="https://solana-rpc.publicnode.com" />
</div>
</div>
@@ -1,6 +1,6 @@
# Программа `shine_login_guard`
Документ описывает целевое поведение программы классификации логинов SHiNE.
Документ описывает текущее временное поведение программы классификации логинов SHiNE.
Задача программы:
@@ -12,11 +12,12 @@
`shine_login_guard` — маленькая служебная программа, которую вызывает `shine_users` через CPI до регистрации пользователя.
Она отвечает только на вопрос:
В текущем временном режиме она отвечает только на вопрос:
- можно ли зарегистрировать логин автоматически;
- является ли логин premium;
- является ли логин trademark/company.
- является ли логин допустимым для автоматической регистрации;
- нужно ли отклонить слишком короткий или некорректный логин.
UI может накладывать дополнительные временные ограничения поверх этой логики, но сама программа про них не знает.
## 2. Program ID
@@ -28,34 +29,9 @@
У программы нет PDA-состояний и persistent storage.
Она опирается на статически сгенерированные словари, встроенные в бинарник на этапе сборки.
Программа полностью stateless и не опирается на словари при классификации.
## 4. Внешние артефакты
Словари хранятся в:
- `src/dictionaries/premium/**/*.txt`
- `src/dictionaries/trademarks/**/*.txt`
На этапе build:
- все `.txt` файлы рекурсивно собираются;
- слова нормализуются;
- дубль-слова детектятся и логируются как warning;
- итоговые массивы слов вшиваются в generated Rust file.
## 5. Нормализация словаря
Слово словаря считается валидным, если:
- не пустое;
- длина не больше `20`;
- содержит только ASCII-буквы и цифры;
- после `trim()` и `lowercase()` остаётся валидным.
Символ `_` в словарных словах не допускается.
## 6. Единственная инструкция
## 4. Единственная инструкция
Инструкция:
@@ -73,9 +49,9 @@
- аккаунты не требуются
Подпись транзакции для самой классификации не имеет бизнес-смысла. Если вызов идёт через CPI из `shine_users`, программа `shine_login_guard` не требует signer-аккаунтов вообще.
Если вызов идёт через CPI из `shine_users`, программа `shine_login_guard` не требует signer-аккаунтов.
## 7. Выходные классы
## 5. Выходные классы
Программа возвращает `u32` через `set_return_data`:
@@ -83,7 +59,14 @@
- `1``CLASS_PREMIUM`
- `2``CLASS_TRADEMARK`
## 8. Правила нормализации логина
Сейчас фактически используются только:
- `CLASS_FREE`
- `CLASS_PREMIUM`
`CLASS_TRADEMARK` сохранён для совместимости ABI, но временная логика его не возвращает.
## 6. Правила нормализации логина
Перед классификацией логин нормализуется:
@@ -97,50 +80,51 @@
- логин считается `CLASS_PREMIUM`.
Это намеренно жёсткое поведение: сомнительный логин не должен автоматически попадать в free-класс.
Это означает, что `shine_users` не пустит такой логин в обычную регистрацию.
## 9. Правила классификации
## 7. Временные правила классификации
Алгоритм:
Текущий временный алгоритм:
1. Нормализовать логин.
2. Попробовать разрезать его на `1..3` слов из словарей.
3. Если среди найденных частей есть trademark-слово, вернуть `CLASS_TRADEMARK`.
4. Если есть только premium-слова, вернуть `CLASS_PREMIUM`.
5. Если словарное совпадение не найдено и длина нормализованного логина `<= 7`, вернуть `CLASS_PREMIUM`.
6. Иначе вернуть `CLASS_FREE`.
2. Если нормализованный логин короче `5` символов, вернуть `CLASS_PREMIUM`.
3. Иначе вернуть `CLASS_FREE`.
## 10. Разбиение на слова
То есть on-chain программа в этом режиме:
Максимум слов в логине:
- пропускает любые валидные логины длиной от `5` символов;
- не использует premium-словари;
- не использует trademark-словари;
- не выполняет словарную сегментацию логина.
- `3`
## 8. Граница ответственности
Алгоритм разбиения:
Важно разделять уровни:
- DFS/backtracking по префиксам строки;
- на каждом шаге берётся префикс длиной `1..20`;
- если префикс найден в premium или trademark словаре, поиск продолжается на остатке строки;
- если найден хотя бы один путь с trademark-словом, итог = `CLASS_TRADEMARK`;
- если найден путь только с premium-словами, итог = `CLASS_PREMIUM`.
1. `shine_login_guard`
- проверяет только базовую валидность и минимальную длину `5`.
## 11. Инварианты
2. UI
- может вводить дополнительные временные ограничения;
- например, пускать логины длиной `5..7` только по временному коду.
Должно быть сохранено при переписи:
3. `shine_users`
- остаётся финальной on-chain точкой регистрации;
- дополнительно проверяет общий формат логина в собственной логике.
- тот же способ нормализации логина;
- та же семантика классов `0/1/2`;
- тот же лимит `MAX_WORDS_PER_LOGIN = 3`;
- тот же fallback `len <= 7 => premium`, если словарь ничего не нашёл;
- тот же порядок приоритета `TRADEMARK > PREMIUM > FREE`.
## 9. Совместимость
## 12. Что не обязано сохраниться при переписи
Сохранено:
Можно менять без изменения бизнес-логики:
- тот же ABI инструкции;
- тот же формат `return_data`;
- те же значения классов `0/1/2`;
- тот же `Program ID`.
- build pipeline словарей;
- структуру хранения слов внутри бинарника;
- способ поиска слов, если сохраняется тот же результат;
- техническую форму возврата, если внешний контракт для `shine_users` останется совместимым.
Изменено относительно полной словарной версии:
Но если меняется именно интерфейс `return_data` или классы, это уже требует обновления `shine_users` и документации.
- нет словарной классификации;
- нет разделения на premium/trademark по содержимому логина;
- короткий порог временно снижен до `5`.
При возврате к полной модели нужно будет обновить этот документ снова синхронно с кодом.
@@ -7,16 +7,15 @@ use solana_program::{
pubkey::Pubkey,
};
mod wordlist {
include!(concat!(env!("OUT_DIR"), "/generated_dictionary.rs"));
}
pub const INSTRUCTION_CLASSIFY_LOGIN: u8 = 1;
pub const CLASS_FREE: u32 = 0;
pub const CLASS_PREMIUM: u32 = 1;
pub const CLASS_TRADEMARK: u32 = 2;
pub const MAX_WORDS_PER_LOGIN: usize = 3;
pub const MAX_LOGIN_LEN: usize = 20;
// ВРЕМЕННАЯ ЗАГЛУШКА:
// до внедрения полноценной on-chain логики продажи/выдачи красивых имён
// пропускаем любой валидный логин длиной от 5 символов.
pub const TEMP_MIN_FREE_LOGIN_LEN: usize = 5;
solana_program::declare_id!("SHiGxGsXGioQYCYhchQ5R7KWoxN5UjFAFsucPf6sfnh");
@@ -75,13 +74,11 @@ pub fn build_classify_login_instruction_data(login: &str) -> Vec<u8> {
}
pub fn classify(login: &str) -> u32 {
// Временный режим: словарная premium/trademark классификация отключена.
let Some(normalized) = normalize_login(login) else {
return CLASS_PREMIUM;
};
if let Some(v) = classify_split(&normalized) {
return v;
}
if normalized.len() <= 7 {
if normalized.len() < TEMP_MIN_FREE_LOGIN_LEN {
return CLASS_PREMIUM;
}
CLASS_FREE
@@ -107,49 +104,6 @@ fn normalize_login(login: &str) -> Option<String> {
Some(out)
}
fn classify_split(login: &str) -> Option<u32> {
fn dfs(rest: &str, depth: usize, has_tm: bool) -> Option<u32> {
if rest.is_empty() {
if depth > 0 && depth <= MAX_WORDS_PER_LOGIN {
return Some(if has_tm { CLASS_TRADEMARK } else { CLASS_PREMIUM });
}
return None;
}
if depth >= MAX_WORDS_PER_LOGIN {
return None;
}
let max_piece = rest.len().min(MAX_LOGIN_LEN);
let mut premium_found = false;
for i in 1..=max_piece {
let candidate = &rest[..i];
let is_tm = is_trademark_word(candidate);
let is_pr = is_tm || is_premium_word(candidate);
if !is_pr {
continue;
}
match dfs(&rest[i..], depth + 1, has_tm || is_tm) {
Some(CLASS_TRADEMARK) => return Some(CLASS_TRADEMARK),
Some(CLASS_PREMIUM) => premium_found = true,
_ => {}
}
}
if premium_found {
Some(CLASS_PREMIUM)
} else {
None
}
}
dfs(login, 0, false)
}
fn is_premium_word(word: &str) -> bool {
wordlist::PREMIUM_WORDS.binary_search(&word).is_ok()
}
fn is_trademark_word(word: &str) -> bool {
wordlist::TRADEMARK_WORDS.binary_search(&word).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -165,12 +119,13 @@ mod tests {
}
#[test]
fn short_unknown_login_is_premium() {
assert_eq!(classify("abc123"), CLASS_PREMIUM);
fn login_shorter_than_temporary_threshold_is_premium() {
assert_eq!(classify("ab_1"), CLASS_PREMIUM);
}
#[test]
fn long_unknown_login_is_free() {
fn login_from_temporary_threshold_and_longer_is_free() {
assert_eq!(classify("abcde"), CLASS_FREE);
assert_eq!(classify("abcdefgh9"), CLASS_FREE);
}
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тех. инструменты — Shine Payments Devnet</title>
<title>Тех. инструменты — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -43,7 +43,7 @@
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Техническая страница (Devnet)</h1>
<h1>Техническая страница (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -112,7 +112,7 @@
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const USERS_PROGRAM_ID = new solanaWeb3.PublicKey("SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_config",
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Покупка билета — Shine Payments Devnet</title>
<title>Покупка билета — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -40,7 +40,7 @@
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Покупка билета (Devnet)</h1>
<h1>Покупка билета (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -80,7 +80,7 @@
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const ORACLE_ACCOUNT = new solanaWeb3.PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE");
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DAO revoke vote — Shine Payments Devnet</title>
<title>DAO revoke vote — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -39,7 +39,7 @@
<body>
<div class="wrap">
<div style="margin-bottom: 12px;"><a class="back" href="./index.html">← На главную</a></div>
<h1>DAO: голосование на revoke/burn membership token (Devnet)</h1>
<h1>DAO: голосование на revoke/burn membership token (Mainnet)</h1>
<div class="muted">Governance program: <code id="govPid"></code></div>
<div class="panel">
@@ -97,8 +97,7 @@
import {
Connection,
PublicKey,
Transaction,
clusterApiUrl
Transaction
} from "https://esm.sh/@solana/web3.js@1.95.3";
import {
PROGRAM_VERSION_V3,
@@ -117,7 +116,7 @@
} from "https://esm.sh/@solana/spl-governance@0.3.28";
const GOVERNANCE_PROGRAM_ID = new PublicKey("GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw");
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const connection = new Connection("https://solana-rpc.publicnode.com", "confirmed");
document.getElementById("govPid").textContent = GOVERNANCE_PROGRAM_ID.toBase58();
let wallet = null;
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DAO-права менеджеров — Shine Payments Devnet</title>
<title>DAO-права менеджеров — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -39,7 +39,7 @@
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>DAO: права менеджеров (Devnet)</h1>
<h1>DAO: права менеджеров (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -85,7 +85,7 @@
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_config",
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Главная — Shine Payments Devnet</title>
<title>Главная — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -36,7 +36,7 @@
</head>
<body>
<div class="wrap">
<h1>Shine Payments Devnet</h1>
<h1>Shine Payments Mainnet</h1>
<div class="panel">
<div>Выберите страницу:</div>
</div>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Логика работы — Shine Payments Devnet</title>
<title>Логика работы — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -27,7 +27,7 @@
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Логика работы Shine Payments (тестовый этап)</h1>
<div class="panel">
<p>Система работает в <b>Devnet</b>. Экономика хранится в <b>USD-центах</b>, а реальные переводы происходят в SOL.</p>
<p>Система работает в <b>Mainnet</b>. Экономика хранится в <b>USD-центах</b>, а реальные переводы происходят в SOL.</p>
<p>Курс SOL/USD берётся из Pyth прямо в контракте при покупке и при шаге выплаты. Цена проверяется на актуальность (не старше 120 секунд).</p>
</div>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Менеджерские билеты — Shine Payments Devnet</title>
<title>Менеджерские билеты — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -33,13 +33,25 @@
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.err { color: var(--err); white-space: pre-wrap; }
.err-box {
margin-top: 10px;
padding: 10px 12px;
border: 1px solid #4c2730;
border-radius: 8px;
background: #23151a;
color: var(--err);
white-space: pre-wrap;
line-height: 1.45;
font-family: monospace;
font-size: 13px;
}
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</style>
</head>
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Менеджер: создание билетов (Devnet)</h1>
<h1>Менеджер: создание билетов (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -78,7 +90,7 @@
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
managerAllowance: "shine_p_manager_allow",
@@ -130,6 +142,55 @@
return s.includes("managerlimitexceeded") || s.includes("invalidmanagerwallet");
}
function safeJson(value) {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function formatSolanaErrorDetails(error) {
const parts = [];
const msg = String(error?.message || error || "").trim();
if (msg) parts.push(msg);
const logs = error?.logs || error?.transactionLogs || error?.simulationLogs || error?.data?.logs;
if (Array.isArray(logs) && logs.length) {
parts.push(`Logs:\n${logs.join("\n")}`);
}
const errObj = error?.value?.err || error?.err || error?.data?.err;
if (errObj) {
parts.push(`Err:\n${safeJson(errObj)}`);
}
if (error?.signature) {
parts.push(`Signature: ${error.signature}`);
}
return parts.length ? parts.join("\n\n") : "unknown";
}
async function attachSolanaLogs(error) {
if (!error) return error;
if (typeof error.getLogs === "function") {
try {
const logs = await error.getLogs(connection);
if (Array.isArray(logs) && logs.length) {
error.logs = logs;
error.transactionLogs = logs;
error.simulationLogs = logs;
}
} catch {
// Игнорируем: часть RPC не умеет отдавать логи для expired/confirm ошибок.
}
}
return error;
}
function parseManagerAllowance(data) {
let o = 0;
const version = data[o++];
@@ -183,8 +244,29 @@
tx.recentBlockhash = bh.blockhash;
const signed = await provider.signTransaction(tx);
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
await connection.confirmTransaction({ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight }, "confirmed");
return sig;
try {
await connection.confirmTransaction(
{ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight },
"confirmed",
);
return { signature: sig, confirmed: true };
} catch (confirmError) {
confirmError.signature = sig;
const txInfo = await connection.getTransaction(sig, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
});
if (txInfo?.meta?.logMessages?.length) {
confirmError.logs = txInfo.meta.logMessages;
confirmError.transactionLogs = txInfo.meta.logMessages;
}
await attachSolanaLogs(confirmError);
const postCheck = await Promise.resolve(typeof ix._postCheck === "function" ? ix._postCheck(sig) : null);
if (postCheck?.success) {
return { signature: sig, confirmed: false, postCheck };
}
throw confirmError;
}
}
function deriveManagerAllowancePda(managerWallet) {
@@ -272,8 +354,17 @@
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Билет создан. Tx: <code>${sig}</code></span>`;
ix._postCheck = async () => {
const ai = await connection.getAccountInfo(ticketPda, "confirmed");
if (!ai) return { success: false };
return { success: true, ticketPda: ticketPda.toBase58() };
};
const result = await sendInstruction(ix);
if (result.confirmed) {
out.innerHTML = `<span class="ok">Билет создан. Tx: <code>${result.signature}</code></span>`;
} else {
out.innerHTML = `<span class="warn">Транзакция подтверждалась с задержкой, но билет создан on-chain. Tx: <code>${result.signature}</code></span>`;
}
await refresh();
} catch (e) {
const raw = String(e.message || e);
@@ -281,7 +372,7 @@
out.innerHTML = `<span class="warn">Операция отклонена: лимит менеджера недостаточен или кошелек не имеет прав менеджера.</span>`;
return;
}
out.innerHTML = `<span class="err">${raw}</span>`;
out.innerHTML = `<span class="err">Ошибка создания билета:</span><div class="err-box">${formatSolanaErrorDetails(e)}</div>`;
}
}
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Что ещё нужно до DAO — Shine Payments Devnet</title>
<title>Что ещё нужно до DAO — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -25,7 +25,7 @@
<h1>Что ещё нужно до реального DAO</h1>
<div class="panel">
<p>Сейчас роль DAO выполняет обычный кошелёк (тестовый режим Devnet).</p>
<p>Сейчас роль DAO выполняет обычный кошелёк, а полное on-chain управление через DAO ещё не включено.</p>
</div>
<div class="panel">
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Сценарий тестирования — Shine Payments Devnet</title>
<title>Сценарий тестирования — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -22,7 +22,7 @@
</head>
<body>
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Сценарий тестирования Shine Payments (Devnet)</h1>
<h1>Сценарий тестирования Shine Payments (Mainnet)</h1>
<div class="panel">
<h3>Вариант А: один кошелёк</h3>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Отслеживание билета — Shine Payments Devnet</title>
<title>Отслеживание билета — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -42,7 +42,7 @@
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Отслеживание билета (Devnet)</h1>
<h1>Отслеживание билета (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -78,7 +78,7 @@
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const ORACLE_ACCOUNT = new solanaWeb3.PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE");
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");