diff --git a/AGENTS.md b/AGENTS.md index a486665..ff7a0be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,6 @@ - Веб-панель администратора сервера (управление Solana PDA сервера) находится в `shine-UI/`: - точка входа `shine-UI/server-ui.html`; - остальные файлы серверного UI — в `shine-UI/server-ui/`. -- Старая папка `shine-server-UI-obsolete/` оставлена только как устаревшая справочная копия и не является актуальной точкой входа. - Локальный Telegram-бот агента-кодера находится в папке `SHiNE-agent-bot-coder/` и не является кодом основного серверного приложения. - Solana/Anchor-модуль находится в папке `shine-solana/shine/` и ведётся отдельно от основного server/UI деплоя. diff --git a/CLAUDE.md b/CLAUDE.md index d3f6a87..0e5b4c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,5 +7,5 @@ ## Справка по подпроектам - При работе внутри `SHiNE-agent-bot-coder/` — читать `SHiNE-agent-bot-coder/AGENTS.md` и `SHiNE-agent-bot-coder/AGENT.md`. - При работе внутри `shine-solana/shine/` — читать `shine-solana/shine/AGENTS.md`. -- При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`, а старую справочную копию при необходимости смотреть в `shine-server-UI-obsolete/AGENTS.md`. +- При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`. - При работе внутри `SHiNE-server/` — читать `SHiNE-server/AGENTS.md`. diff --git a/Dev_Docs/Pending_Features/2026-06-03_1508_перенос_server_ui_в_shine_ui.md b/Dev_Docs/Pending_Features/2026-06-03_1508_перенос_server_ui_в_shine_ui.md index 7f73e07..c3dc4bc 100644 --- a/Dev_Docs/Pending_Features/2026-06-03_1508_перенос_server_ui_в_shine_ui.md +++ b/Dev_Docs/Pending_Features/2026-06-03_1508_перенос_server_ui_в_shine_ui.md @@ -12,7 +12,7 @@ 4. Загрузку существующей серверной PDA на странице обновления. 5. Обновление `server_address` и `sync_servers` только по `root + device` без blockchain-ключа. 6. Корректное чтение нового формата `ServerProfileBlock` через общий PDA-модуль. - 7. То, что старая папка `shine-server-UI-obsolete/` не используется как актуальная точка входа. + 7. То, что актуальной точкой входа остаётся `shine-UI/server-ui.html`. - ожидаемый результат: 1. Новые страницы открываются без JS-ошибок. diff --git a/Dev_Docs/Pending_Features/2026-06-04_1347_fix_native_ed25519_update_server_pda.md b/Dev_Docs/Pending_Features/2026-06-04_1347_fix_native_ed25519_update_server_pda.md deleted file mode 100644 index c6ce5f5..0000000 --- a/Dev_Docs/Pending_Features/2026-06-04_1347_fix_native_ed25519_update_server_pda.md +++ /dev/null @@ -1,16 +0,0 @@ -# Фикс native Ed25519 для update server PDA - -- Краткое описание: - В `shine_users` восстановлена нативная проверка подписи через встроенные Solana Ed25519-инструкции без прямой Rust-верификации. Для `create_user_pda` и `update_user_pda` зафиксирован порядок инструкций в транзакции: сначала подпись `root_key`, затем подпись `blockchain_public_key`, затем вызов `shine_users`. -- Что проверять: - 1. В `shine-UI/server-ui/update-server-pda.html` загрузить существующий server PDA. - 2. Ввести правильный пароль, сгенерировать ключи и выполнить `Обновить PDA`. - 3. Убедиться, что транзакция проходит без `memory allocation failed, out of memory`. - 4. Отдельно проверить создание server PDA из `shine-UI/server-ui/create-server-pda.html`. - 5. Отдельно проверить обычную пользовательскую регистрацию через клиентский UI. -- Ожидаемый результат: - 1. `update server PDA` проходит успешно. - 2. `create server PDA` проходит успешно. - 3. Регистрация обычного пользователя через тот же JS-модуль работы с PDA тоже проходит успешно. - 4. Одинаковый общий JS-модуль используется и клиентским UI, и server UI. -- Статус: `pending` diff --git a/VERSION.properties b/VERSION.properties index 3094aed..8d6ef7f 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.122 -server.version=1.2.114 +client.version=1.2.123 +server.version=1.2.115 diff --git a/shine-server-UI-obsolete/AGENTS.md b/shine-server-UI-obsolete/AGENTS.md deleted file mode 100644 index 3d3b5c7..0000000 --- a/shine-server-UI-obsolete/AGENTS.md +++ /dev/null @@ -1,87 +0,0 @@ -# AGENTS.md — shine-server-UI-obsolete - -## Назначение - -`shine-server-UI-obsolete/` — устаревшая автономная веб-панель администратора для управления серверным аккаунтом SHiNE -в Solana (регистрация и обновление `user_pda` с флагом `is_server=true`). - -Эта папка оставлена только как справочная копия старой реализации. -Актуальная точка входа серверного UI теперь находится в: - -- `shine-UI/server-ui.html` -- `shine-UI/server-ui/` - -Никакого бэкенда нет. - -## Структура файлов - -``` -shine-server-UI-obsolete/ - index.html — главная страница с навигацией - create-server-pda.html — регистрация нового серверного аккаунта - update-server-pda.html — обновление адреса/sync_servers существующей PDA - styles.css — тёмная тема - js/ - server-pda-core.js — вся логика: парсинг PDA, Borsh, криптография, Solana -``` - -## Как пользоваться - -### Регистрация сервера (`create-server-pda.html`) - -Открыть страницу в браузере (требуется HTTPS для WebCrypto — локально либо через сервер). - -Ввести: -- **Логин сервера** — уникальный логин в Solana (только a-z, 0-9, _ ; без точки ; макс. 20 символов). -- **Адрес сервера** — полный WebSocket/HTTP URL, например `https://shineup.me/ws`. -- **sync_servers** — логины SHiNE-аккаунтов серверов-партнёров (по одному на строку). - -**Способ ввода ключей (переключатель):** - -- **«Из пароля»** — ввести пароль. Ключи автоматически выводятся из логина + пароля - по той же схеме, что SHiNE-клиент (Argon2id + Ed25519). Занимает 2–5 сек. - На страницах сервера публичные и приватные ключи показываются в base58, приватный ключ - хранится как 32-байтовый seed в base58. -- **«JSON ключей»** — вставить keyBundle JSON с тремя парами (rootPair, devicePair, blockchainPair). - -На **device-ключе** должно быть достаточно SOL для оплаты транзакции регистрации. - -### Обновление настроек сервера (`update-server-pda.html`) - -1. Ввести логин и нажать **«Загрузить PDA»** — страница прочитает существующую PDA из Solana и - покажет текущие данные. -2. Изменить адрес сервера или список sync_servers. -3. Выбрать способ ввода ключей: - - **«Из пароля»** — ввести пароль (логин берётся из поля выше); - - **«JSON ключей»** — вставить keyBundle (достаточно rootPair + devicePair). - Blockchain-ключ для обновления не нужен — существующая подпись из PDA переиспользуется. - При ручном вводе допустим base58 seed; если blockchain seed не указан, обновление - использует уже сохранённую подпись последнего блока. -4. Нажать **«Обновить PDA»**. - -## Ключевой файл логики - -`js/server-pda-core.js` — автономный ES-модуль (без зависимостей на shine-UI). - -Экспортирует: -- `readServerPdaData({ login, solanaEndpoint })` — читает и парсит PDA из Solana; -- `registerServerOnSolana({ login, keyBundle, serverAddress, syncServers, ... })`; -- `updateServerOnSolana({ login, keyBundle, serverAddress, syncServers, ... })`; -- `parsePdaData(rawBytes)` — парсит бинарный формат PDA (matches Rust `deserialize_record_from_pda`). - -## Связанные документы - -- Формат PDA: `shine-solana/shine/doc/SHiNE-user-format-v.1.0.md` -- Деплой Solana-программ: `Dev_Docs/Инициализация_Solana_регистрации/README.md` -- Синхронизация между серверами: `Dev_Docs/Blockchain/sync-between-servers.md` -- Настройки сервера: `SHiNE-server/AGENTS.md` - -## Правила при доработке - -- Формат Borsh-аргументов в `server-pda-core.js` должен строго соответствовать - `UserMutableFields` в `shine-solana/shine/programs/shine_users/src/users.rs`. -- Бинарный формат PDA в `buildUnsignedRecordBytesServer` должен совпадать с - `serialize_unsigned_record` в Rust. -- При любом изменении формата Solana-программы (`users.rs`) — обновлять `server-pda-core.js` - и документ формата PDA в том же коммите. -- Язык кода и комментариев: русский. diff --git a/shine-server-UI-obsolete/create-server-pda.html b/shine-server-UI-obsolete/create-server-pda.html deleted file mode 100644 index 4a8a16a..0000000 --- a/shine-server-UI-obsolete/create-server-pda.html +++ /dev/null @@ -1,468 +0,0 @@ - - - - - - Регистрация сервера — SHiNE Server Admin - - - - -
- - -

Регистрация серверного аккаунта

-

Создаёт user_pda в Solana с флагом is_server=true

- -
-

Параметры Solana

-
- - -
devnet: https://api.devnet.solana.com · mainnet: https://api.mainnet-beta.solana.com
-
-
- -
-

Данные сервера

-
- - -
Только a-z, 0-9, _ · без точки · макс. 20 символов
-
-
- - -
-
- - -
-
- - -
-
- -
-

Ключи сервера

- -
- -
- - -
-
Нажмите «Сгенерировать» — поля ниже заполнятся из логина + пароля (Argon2id).
Или введите ключи вручную.
-
- -
- -
-
- -
Секрет (master secret, base58)
-
- -
- -
Ключевые пары (base58)
- -
-
Root Key — подпись PDA-записи
-
Публичный
-
Приватный
-
-
-
Blockchain Key — подпись LastBlockState
-
Публичный
-
Приватный
-
-
-
Device Key — оплата транзакции Solana
-
Публичный
-
Приватный
-
-
Положите SOL на этот адрес перед регистрацией:
-
-
Это Solana-адрес device-ключа (public key в base58 = Solana-адрес). С него оплачивается создание PDA.
-
-
-
- -
- -
-
-
- - - - diff --git a/shine-server-UI-obsolete/index.html b/shine-server-UI-obsolete/index.html deleted file mode 100644 index 955b62a..0000000 --- a/shine-server-UI-obsolete/index.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - SHiNE Server Admin - - - -
-

SHiNE Server Admin

-

Панель управления Solana PDA для серверного аккаунта SHiNE

- -
-

Действия

- -
- - - -
-
- - - -
-
- -
-

Как это работает

-

- Каждый SHiNE-сервер регистрирует свой аккаунт в Solana в виде user_pda - с флагом is_server=true.

- В PDA хранятся:
-  • адрес сервера (например, https://shineup.me/ws);
-  • список серверов-партнёров для синхронизации блокчейна и DM;
-  • криптографический корневой ключ сервера.

- Клиенты читают PDA прямо из Solana при попытке дозвониться до пользователя или - установить WebSocket-соединение через сервер. -

-
- -
-

Что потребуется

-

- Для создания: полный keyBundle сервера (rootPair + devicePair + blockchainPair), - логин сервера (без точки, не более 20 символов), URL-адрес сервера, Solana-эндпоинт, - достаточный баланс SOL на device-ключе для комиссии.

- Для обновления: только rootPair + devicePair (blockchain-ключ не нужен). -

-
-
- - diff --git a/shine-server-UI-obsolete/js/server-pda-core.js b/shine-server-UI-obsolete/js/server-pda-core.js deleted file mode 100644 index 480b6d6..0000000 --- a/shine-server-UI-obsolete/js/server-pda-core.js +++ /dev/null @@ -1,710 +0,0 @@ -// Логика управления серверной PDA в Solana (shine_users) -// Автономный модуль для панели администратора сервера SHiNE - -const SHINE_USERS_PROGRAM_ID = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm'; -const SHINE_PAYMENTS_PROGRAM_ID = 'm48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR'; -const SHINE_LOGIN_GUARD_PROGRAM_ID = '3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo'; -const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111'; -const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111'; - -// Discriminator create_user_pda (sha256("global:create_user_pda")[0..8]) -const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]); - -let _solanaLib = null; -async function loadSolanaLib() { - if (!_solanaLib) _solanaLib = await import('https://esm.sh/@solana/web3.js@1.98.4?bundle'); - return _solanaLib; -} - -let _argon2Lib = null; -async function loadArgon2() { - if (!_argon2Lib) _argon2Lib = await import('https://esm.sh/@noble/hashes@1.8.0/argon2.js'); - return _argon2Lib; -} - -// ------------------------------------------------------------------- -// Crypto (WebCrypto, Ed25519) -// ------------------------------------------------------------------- - -async function sha256Bytes(bytes) { - const buf = await crypto.subtle.digest('SHA-256', bytes); - return new Uint8Array(buf); -} - -async function signEd25519(pkcs8B64, messageBytes) { - const pkcs8 = Uint8Array.from(atob(pkcs8B64), c => c.charCodeAt(0)); - const key = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, false, ['sign']); - const sig = await crypto.subtle.sign({ name: 'Ed25519' }, key, messageBytes); - return new Uint8Array(sig); -} - -function base64ToBytes(b64) { - return Uint8Array.from(atob(b64), c => c.charCodeAt(0)); -} - -function extractSeed32FromPkcs8B64(pkcs8B64) { - // Ed25519 PKCS8 (48 байт): seed расположен начиная с байта 16 - return base64ToBytes(pkcs8B64).slice(16, 48); -} - -async function anchorDiscriminator(name) { - const hash = await sha256Bytes(new TextEncoder().encode(`global:${name}`)); - return hash.slice(0, 8); -} - -// ------------------------------------------------------------------- -// Borsh-кодирование (Anchor-совместимое) -// ------------------------------------------------------------------- - -function pushU32LE(buf, v) { - const n = v >>> 0; - buf.push(n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF); -} - -function pushU64LE(buf, bigV) { - const b = typeof bigV === 'bigint' ? bigV : BigInt(bigV); - const lo = Number(b & 0xFFFFFFFFn) >>> 0; - const hi = Number((b >> 32n) & 0xFFFFFFFFn) >>> 0; - pushU32LE(buf, lo); - pushU32LE(buf, hi); -} - -class BorshBuf { - constructor() { this._b = []; } - u8(v) { this._b.push(v & 0xFF); } - u32(v) { pushU32LE(this._b, v); } - u64(v) { pushU64LE(this._b, v); } - bool(v) { this.u8(v ? 1 : 0); } - bytes32(b) { for (const x of b) this._b.push(x); } - vecU8(b) { this.u32(b.length); for (const x of b) this._b.push(x); } - str(s) { - const enc = new TextEncoder().encode(s); - this.u32(enc.length); - for (const x of enc) this._b.push(x); - } - vecStr(arr) { - this.u32(arr.length); - for (const s of arr) this.str(s); - } - raw(bytes) { for (const x of bytes) this._b.push(x); } - result() { return new Uint8Array(this._b); } -} - -// ------------------------------------------------------------------- -// Построение бинарного формата PDA (matches Rust serialize_unsigned_record) -// ------------------------------------------------------------------- - -function buildLastBlockStateBytes(login, blockchainName, lastBlockNumber, lastBlockHash32, usedBytes) { - const enc = new TextEncoder(); - const buf = []; - for (const x of enc.encode('SHiNE_LAST_BLOCK')) buf.push(x); - const loginB = enc.encode(login); - buf.push(loginB.length); for (const x of loginB) buf.push(x); - const bchB = enc.encode(blockchainName); - buf.push(bchB.length); for (const x of bchB) buf.push(x); - pushU32LE(buf, lastBlockNumber); - for (const x of lastBlockHash32) buf.push(x); - pushU64LE(buf, usedBytes); - return new Uint8Array(buf); -} - -function buildUnsignedRecordBytesServer({ - login, createdAtMs, updatedAtMs, recordNumber, prevHash32, - rootKey32, deviceKey32, blockchainKey32, blockchainName, - paidLimitBytes, usedBytes, lastBlockNumber, lastBlockHash32, lastBlockSig64, arweaveTxId, - serverAddress, addressFormatType, addressFormatVersion, syncServers, accessServers, trustedCount, -}) { - const enc = new TextEncoder(); - const loginB = enc.encode(login); - const bchB = enc.encode(blockchainName); - const buf = []; - - // Заголовок: MAGIC(5) + FORMAT_MAJOR(1) + FORMAT_MINOR(1) + record_len_placeholder(2) - buf.push(0x53, 0x48, 0x69, 0x4E, 0x45, 1, 0, 0, 0); // байты 0..8 - - pushU64LE(buf, createdAtMs); - pushU64LE(buf, updatedAtMs); - pushU32LE(buf, recordNumber); - for (const x of prevHash32) buf.push(x); - - buf.push(loginB.length); - for (const x of loginB) buf.push(x); - - buf.push(6); // blocks_count = 6 (сервер) - - // RootKeyBlock (type=1, ver=0) - buf.push(1, 0); - for (const x of rootKey32) buf.push(x); - - // DeviceKeyBlock (type=2, ver=0) - buf.push(2, 0); - for (const x of deviceKey32) buf.push(x); - - // BlockchainRegistryBlock (type=3, ver=0, count=1, blockchain_type=1) - buf.push(3, 0, 1, 1); - buf.push(bchB.length); for (const x of bchB) buf.push(x); - for (const x of blockchainKey32) buf.push(x); - pushU64LE(buf, paidLimitBytes); - pushU64LE(buf, usedBytes); - pushU32LE(buf, lastBlockNumber); - for (const x of lastBlockHash32) buf.push(x); - for (const x of lastBlockSig64) buf.push(x); - if (arweaveTxId) { - buf.push(1); - const aTxB = enc.encode(arweaveTxId); - buf.push(aTxB.length); for (const x of aTxB) buf.push(x); - } else { - buf.push(0); - } - - // ServerProfileBlock (type=30, ver=0) - buf.push(30, 0); - buf.push(1); // is_server = 1 - buf.push(addressFormatType & 0xFF); - buf.push(addressFormatVersion & 0xFF); - const srvB = enc.encode(serverAddress); - buf.push(srvB.length); for (const x of srvB) buf.push(x); - buf.push(syncServers.length); - for (const srv of syncServers) { - const sB = enc.encode(srv); - buf.push(sB.length); for (const x of sB) buf.push(x); - } - - // AccessServersBlock (type=40, ver=0) - buf.push(40, 0, accessServers.length); - for (const srv of accessServers) { - const sB = enc.encode(srv); - buf.push(sB.length); for (const x of sB) buf.push(x); - } - - // TrustedStateBlock (type=50, ver=0) - buf.push(50, 0, trustedCount & 0xFF); - - // Записываем record_len: (длина буфера + 64 байта подписи) - const recLen = buf.length + 64; - buf[7] = recLen & 0xFF; - buf[8] = (recLen >> 8) & 0xFF; - - return new Uint8Array(buf); -} - -// ------------------------------------------------------------------- -// Borsh-сериализация Anchor-инструкций -// ------------------------------------------------------------------- - -function serializeCreateServerPdaArgs({ - login, rootKey32, createdAtMs, deviceKey32, blockchainKey32, - blockchainName, usedBytes, lastBlockNumber, lastBlockHash32, - lastBlockSig64, arweaveTxId, serverAddress, addressFormatType, - addressFormatVersion, syncServers, accessServers, trustedCount, rootSig64, -}) { - const b = new BorshBuf(); - b.raw(CREATE_USER_PDA_DISCRIMINATOR); - b.str(login); - b.bytes32(rootKey32); - b.u64(createdAtMs); - b.u64(0n); // additional_limit - // UserMutableFields: - b.bytes32(deviceKey32); - b.bytes32(blockchainKey32); - b.str(blockchainName); - b.u64(usedBytes); - b.u32(lastBlockNumber); - b.vecU8(lastBlockHash32); - b.vecU8(lastBlockSig64); - b.str(arweaveTxId); - b.bool(true); // is_server - b.u8(addressFormatType); - b.u8(addressFormatVersion); - b.str(serverAddress); - b.vecStr(syncServers); - b.vecStr(accessServers); - b.u8(trustedCount); - b.vecU8(rootSig64); - return b.result(); -} - -async function serializeUpdateServerPdaArgs({ - login, rootKey32, createdAtMs, updatedAtMs, version, prevHash32, - deviceKey32, blockchainKey32, blockchainName, usedBytes, - lastBlockNumber, lastBlockHash32, lastBlockSig64, arweaveTxId, - serverAddress, addressFormatType, addressFormatVersion, - syncServers, accessServers, trustedCount, rootSig64, -}) { - const discriminator = await anchorDiscriminator('update_user_pda'); - const b = new BorshBuf(); - b.raw(discriminator); - b.str(login); - b.bytes32(rootKey32); - b.u64(createdAtMs); - b.u64(updatedAtMs); - b.u32(version); - b.vecU8(prevHash32); - b.u64(0n); // additional_limit - // UserMutableFields: - b.bytes32(deviceKey32); - b.bytes32(blockchainKey32); - b.str(blockchainName); - b.u64(usedBytes); - b.u32(lastBlockNumber); - b.vecU8(lastBlockHash32); - b.vecU8(lastBlockSig64); - b.str(arweaveTxId); - b.bool(true); // is_server - b.u8(addressFormatType); - b.u8(addressFormatVersion); - b.str(serverAddress); - b.vecStr(syncServers); - b.vecStr(accessServers); - b.u8(trustedCount); - b.vecU8(rootSig64); - return b.result(); -} - -// ------------------------------------------------------------------- -// Построитель Ed25519-инструкции Solana -// ------------------------------------------------------------------- - -function buildEd25519IxData(sig64, pubkey32, msgHash32) { - const sigOff = 16, pkOff = 80, msgOff = 112; - const data = new Uint8Array(msgOff + 32); - const v = new DataView(data.buffer); - data[0] = 1; data[1] = 0; - v.setUint16(2, sigOff, true); v.setUint16(4, 0xFFFF, true); - v.setUint16(6, pkOff, true); v.setUint16(8, 0xFFFF, true); - v.setUint16(10, msgOff, true); v.setUint16(12, 32, true); v.setUint16(14, 0xFFFF, true); - data.set(sig64, sigOff); - data.set(pubkey32, pkOff); - data.set(msgHash32, msgOff); - return data; -} - -// ------------------------------------------------------------------- -// Парсер бинарных данных PDA (matches Rust deserialize_record_from_pda) -// ------------------------------------------------------------------- - -export function parsePdaData(raw) { - const d = raw instanceof Uint8Array ? raw : new Uint8Array(raw); - if (d.length < 9) throw new Error('PDA слишком короткая'); - if (String.fromCharCode(d[0], d[1], d[2], d[3], d[4]) !== 'SHiNE') { - throw new Error('Неверный magic в PDA'); - } - - const view = new DataView(d.buffer, d.byteOffset); - const recordLen = view.getUint16(7, true); - if (recordLen < 9 + 64 || recordLen > d.length) throw new Error('Неверный record_len'); - - // Подписанная часть = байты [0 .. recordLen-64) - const unsignedBytes = d.slice(0, recordLen - 64); - - let cur = 9; - const ru8 = () => d[cur++]; - const ru32 = () => { const v = view.getUint32(cur, true); cur += 4; return v; }; - const ru64 = () => { const v = view.getBigUint64(cur, true); cur += 8; return v; }; - const rBytes = n => { const s = d.slice(cur, cur + n); cur += n; return s; }; - const rStr = () => { const len = ru8(); return new TextDecoder().decode(rBytes(len)); }; - - const createdAtMs = ru64(); - const updatedAtMs = ru64(); - const recordNumber = ru32(); - const prevRecordHash = rBytes(32); - const login = rStr(); - const blocksCount = ru8(); - - let rootKey32 = null, deviceKey32 = null, blockchainData = null; - let isServer = false, serverData = null; - let accessServers = [], trustedCount = 0; - - for (let i = 0; i < blocksCount; i++) { - const blockType = ru8(); - ru8(); // block_version - - if (blockType === 1) { - rootKey32 = rBytes(32); - } else if (blockType === 2) { - deviceKey32 = rBytes(32); - } else if (blockType === 3) { - const count = ru8(); - const blockchainType = ru8(); - const blockchainName = rStr(); - const blockchainPublicKey = rBytes(32); - const paidLimitBytes = ru64(); - const usedBytes = ru64(); - const lastBlockNumber = ru32(); - const lastBlockHash = rBytes(32); - const lastBlockSignature = rBytes(64); - const arweavePresent = ru8(); - const arweaveTxId = arweavePresent === 1 ? rStr() : ''; - blockchainData = { - blockchainType, blockchainName, blockchainPublicKey, - paidLimitBytes, usedBytes, lastBlockNumber, - lastBlockHash, lastBlockSignature, arweaveTxId, - }; - } else if (blockType === 30) { - if (ru8() === 1) { - isServer = true; - const addressFormatType = ru8(); - const addressFormatVersion = ru8(); - const serverAddress = rStr(); - const syncCount = ru8(); - const syncServers = []; - for (let j = 0; j < syncCount; j++) syncServers.push(rStr()); - serverData = { addressFormatType, addressFormatVersion, serverAddress, syncServers }; - } - } else if (blockType === 40) { - const cnt = ru8(); - for (let j = 0; j < cnt; j++) accessServers.push(rStr()); - } else if (blockType === 50) { - trustedCount = ru8(); - } - } - - const signature = d.slice(cur, cur + 64); - - return { - recordLen, unsignedBytes, - createdAtMs, updatedAtMs, recordNumber, prevRecordHash, - login, rootKey32, deviceKey32, blockchainData, - isServer, serverData, accessServers, trustedCount, signature, - }; -} - -// ------------------------------------------------------------------- -// Вспомогательная: читает start_bonus_limit из economy config PDA -// ------------------------------------------------------------------- - -function readStartBonusLimit(data) { - // Borsh: version(u8=1) + reg_fee(u64) + lamports_per_step(u64) = 17 байт до start_bonus_limit - return new DataView(data.buffer, data.byteOffset, data.byteLength).getBigUint64(17, true); -} - -// ------------------------------------------------------------------- -// Читает и парсит существующую PDA с блокчейна -// ------------------------------------------------------------------- - -export async function readServerPdaData({ login, solanaEndpoint }) { - const solana = await loadSolanaLib(); - const connection = new solana.Connection(String(solanaEndpoint), 'confirmed'); - const loginNorm = String(login).trim().toLowerCase(); - const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID); - const enc = new TextEncoder(); - const [userPda] = solana.PublicKey.findProgramAddressSync( - [enc.encode('login='), enc.encode(loginNorm)], - usersProgram, - ); - const ai = await connection.getAccountInfo(userPda, 'confirmed'); - if (!ai) throw new Error(`PDA не найдена для логина «${loginNorm}»`); - const parsed = parsePdaData(ai.data); - parsed.pdaAddress = userPda.toBase58(); - return parsed; -} - -// ------------------------------------------------------------------- -// Регистрация нового серверного аккаунта в Solana -// ------------------------------------------------------------------- - -export async function registerServerOnSolana({ - login, keyBundle, serverAddress, - addressFormatType = 1, addressFormatVersion = 0, - syncServers = [], accessServers = [], - solanaEndpoint, -}) { - const solana = await loadSolanaLib(); - const connection = new solana.Connection(String(solanaEndpoint), 'confirmed'); - - const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID); - const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID); - const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID); - const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID); - const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID); - - const enc = new TextEncoder(); - const loginNorm = String(login).trim().toLowerCase(); - const blockchainName = `${loginNorm}-001`; - const zeroHash32 = new Uint8Array(32); - - const [userPda] = solana.PublicKey.findProgramAddressSync( - [enc.encode('login='), enc.encode(loginNorm)], usersProgram); - const [economyConfigPda] = solana.PublicKey.findProgramAddressSync( - [enc.encode('shine_users_economy_config')], usersProgram); - const [inflowVault] = solana.PublicKey.findProgramAddressSync( - [enc.encode('shine_payments_inflow_vault')], paymentsProgram); - - const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64); - const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64); - const deviceKey32 = base64ToBytes(keyBundle.devicePair.publicKeyB64); - const deviceKeypair = solana.Keypair.fromSeed( - extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64)); - - const ecoAccount = await connection.getAccountInfo(economyConfigPda); - if (!ecoAccount) throw new Error('Economy config не инициализирован'); - const paidLimitBytes = readStartBonusLimit(ecoAccount.data); // additional_limit = 0 - const createdAtMs = BigInt(Date.now()); - - // Подписываем LastBlockState ключом блокчейна (начальное состояние: всё нули) - const lbsBytes = buildLastBlockStateBytes(loginNorm, blockchainName, 0, zeroHash32, 0n); - const lbsHash = await sha256Bytes(lbsBytes); - const lastBlockSig64 = await signEd25519(keyBundle.blockchainPair.privatePkcs8B64, lbsHash); - - // Строим и подписываем беззнаковую запись PDA корневым ключом - const unsignedRecord = buildUnsignedRecordBytesServer({ - login: loginNorm, createdAtMs, updatedAtMs: createdAtMs, - recordNumber: 0, prevHash32: zeroHash32, - rootKey32, deviceKey32, blockchainKey32, blockchainName, - paidLimitBytes, usedBytes: 0n, lastBlockNumber: 0, - lastBlockHash32: zeroHash32, lastBlockSig64, arweaveTxId: '', - serverAddress, addressFormatType, addressFormatVersion, - syncServers, accessServers, trustedCount: 0, - }); - const unsignedHash = await sha256Bytes(unsignedRecord); - const rootSig64 = await signEd25519(keyBundle.rootPair.privatePkcs8B64, unsignedHash); - - const ixData = serializeCreateServerPdaArgs({ - login: loginNorm, rootKey32, createdAtMs, deviceKey32, blockchainKey32, - blockchainName, usedBytes: 0n, lastBlockNumber: 0, - lastBlockHash32: zeroHash32, lastBlockSig64, arweaveTxId: '', - serverAddress, addressFormatType, addressFormatVersion, - syncServers, accessServers, trustedCount: 0, rootSig64, - }); - - const tx = new solana.Transaction().add( - new solana.TransactionInstruction({ - programId: ed25519Program, keys: [], - data: buildEd25519IxData(rootSig64, rootKey32, unsignedHash), - }), - new solana.TransactionInstruction({ - programId: ed25519Program, keys: [], - data: buildEd25519IxData(lastBlockSig64, blockchainKey32, lbsHash), - }), - new solana.TransactionInstruction({ - programId: usersProgram, - keys: [ - { pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true }, - { pubkey: userPda, isSigner: false, isWritable: true }, - { pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: inflowVault, isSigner: false, isWritable: true }, - { pubkey: sysvarInstructions, isSigner: false, isWritable: false }, - { pubkey: economyConfigPda, isSigner: false, isWritable: false }, - { pubkey: loginGuardProgram, isSigner: false, isWritable: false }, - ], - data: ixData, - }), - ); - - const signature = await solana.sendAndConfirmTransaction( - connection, tx, [deviceKeypair], { commitment: 'confirmed' }); - - return { signature, pdaAddress: userPda.toBase58(), blockchainName }; -} - -// ------------------------------------------------------------------- -// Обновление серверного профиля в существующей PDA -// Для обновления нужен только root-ключ (подпись записи) + device-ключ (оплата). -// Blockchain-ключ не нужен — переиспользуем существующую подпись LastBlockState из PDA. -// ------------------------------------------------------------------- - -export async function updateServerOnSolana({ - login, keyBundle, serverAddress, - addressFormatType, addressFormatVersion, - syncServers, - solanaEndpoint, -}) { - const solana = await loadSolanaLib(); - const connection = new solana.Connection(String(solanaEndpoint), 'confirmed'); - - const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID); - const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID); - const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID); - const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID); - - const enc = new TextEncoder(); - const loginNorm = String(login).trim().toLowerCase(); - - const [userPda] = solana.PublicKey.findProgramAddressSync( - [enc.encode('login='), enc.encode(loginNorm)], usersProgram); - const [economyConfigPda] = solana.PublicKey.findProgramAddressSync( - [enc.encode('shine_users_economy_config')], usersProgram); - const [inflowVault] = solana.PublicKey.findProgramAddressSync( - [enc.encode('shine_payments_inflow_vault')], paymentsProgram); - - // Читаем существующую PDA - const ai = await connection.getAccountInfo(userPda, 'confirmed'); - if (!ai) throw new Error(`PDA не найдена для логина «${loginNorm}»`); - const pda = parsePdaData(ai.data); - if (!pda.isServer) throw new Error('Эта PDA не является серверной (is_server = false)'); - - const bch = pda.blockchainData; - const deviceKeypair = solana.Keypair.fromSeed( - extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64)); - - // Формат адреса: берём из аргументов или из существующей PDA - const fmtType = addressFormatType ?? pda.serverData?.addressFormatType ?? 1; - const fmtVersion = addressFormatVersion ?? pda.serverData?.addressFormatVersion ?? 0; - - // prev_hash = sha256(unsigned_bytes предыдущей записи) - const prevHash32 = await sha256Bytes(pda.unsignedBytes); - const updatedAtMs = BigInt(Date.now()); - const newVersion = pda.recordNumber + 1; - - // Строим новую беззнаковую запись - const unsignedRecord = buildUnsignedRecordBytesServer({ - login: loginNorm, - createdAtMs: pda.createdAtMs, updatedAtMs, - recordNumber: newVersion, prevHash32, - rootKey32: pda.rootKey32, deviceKey32: pda.deviceKey32, - blockchainKey32: bch.blockchainPublicKey, blockchainName: bch.blockchainName, - paidLimitBytes: bch.paidLimitBytes, usedBytes: bch.usedBytes, - lastBlockNumber: bch.lastBlockNumber, - lastBlockHash32: bch.lastBlockHash, lastBlockSig64: bch.lastBlockSignature, - arweaveTxId: bch.arweaveTxId, - serverAddress, addressFormatType: fmtType, addressFormatVersion: fmtVersion, - syncServers, accessServers: pda.accessServers, trustedCount: pda.trustedCount, - }); - const unsignedHash = await sha256Bytes(unsignedRecord); - const rootSig64 = await signEd25519(keyBundle.rootPair.privatePkcs8B64, unsignedHash); - - // Хэш LastBlockState из существующей PDA (те же данные — та же подпись) - const lbsBytes = buildLastBlockStateBytes( - loginNorm, bch.blockchainName, - bch.lastBlockNumber, bch.lastBlockHash, bch.usedBytes); - const lbsHash = await sha256Bytes(lbsBytes); - - const ixData = await serializeUpdateServerPdaArgs({ - login: loginNorm, rootKey32: pda.rootKey32, - createdAtMs: pda.createdAtMs, updatedAtMs, - version: newVersion, prevHash32, - deviceKey32: pda.deviceKey32, blockchainKey32: bch.blockchainPublicKey, - blockchainName: bch.blockchainName, - usedBytes: bch.usedBytes, lastBlockNumber: bch.lastBlockNumber, - lastBlockHash32: bch.lastBlockHash, lastBlockSig64: bch.lastBlockSignature, - arweaveTxId: bch.arweaveTxId, - serverAddress, addressFormatType: fmtType, addressFormatVersion: fmtVersion, - syncServers, accessServers: pda.accessServers, trustedCount: pda.trustedCount, - rootSig64, - }); - - const tx = new solana.Transaction().add( - // Ed25519: подпись новой записи корневым ключом - new solana.TransactionInstruction({ - programId: ed25519Program, keys: [], - data: buildEd25519IxData(rootSig64, pda.rootKey32, unsignedHash), - }), - // Ed25519: переиспользуем существующую подпись LastBlockState из PDA - new solana.TransactionInstruction({ - programId: ed25519Program, keys: [], - data: buildEd25519IxData(bch.lastBlockSignature, bch.blockchainPublicKey, lbsHash), - }), - new solana.TransactionInstruction({ - programId: usersProgram, - keys: [ - { pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true }, - { pubkey: userPda, isSigner: false, isWritable: true }, - { pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: inflowVault, isSigner: false, isWritable: true }, - { pubkey: sysvarInstructions, isSigner: false, isWritable: false }, - { pubkey: economyConfigPda, isSigner: false, isWritable: false }, - ], - data: ixData, - }), - ); - - const signature = await solana.sendAndConfirmTransaction( - connection, tx, [deviceKeypair], { commitment: 'confirmed' }); - - return { signature, pdaAddress: userPda.toBase58() }; -} - -// ------------------------------------------------------------------- -// Деривация keyBundle из логина + пароля -// Идентична логике SHiNE-клиента (crypto-utils.js): -// masterSecret = Argon2id(login+"\n"+password, salt=sha256("shine-auth-v2|login=...|suffix=master.secret")) -// rootPair = Ed25519(sha256(base64(master) + "|root.key")) -// blockchainPair = Ed25519(sha256(base64(master) + "|bch.key")) -// devicePair = Ed25519(sha256(base64(master) + "|dev.key")) -// ------------------------------------------------------------------- - -function _b64urlToStd(s) { - const n = s.replace(/-/g, '+').replace(/_/g, '/'); - return n + '='.repeat((4 - n.length % 4) % 4); -} - -function _ed25519Pkcs8FromSeed(seed32) { - const prefix = new Uint8Array([ - 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20, - ]); - const out = new Uint8Array(prefix.length + 32); - out.set(prefix); out.set(seed32, prefix.length); - return out; -} - -async function _deriveEd25519PairFromMasterSecret(masterSecret32, suffix) { - const enc = new TextEncoder(); - const material = `${btoa(String.fromCharCode(...masterSecret32))}|${suffix}`; - const seed = await sha256Bytes(enc.encode(material)); - const pkcs8 = _ed25519Pkcs8FromSeed(seed); - const privateKey = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']); - const jwk = await crypto.subtle.exportKey('jwk', privateKey); - if (!jwk.x) throw new Error(`Не удалось получить публичный ключ (suffix=${suffix})`); - const pubBytes = base64ToBytes(_b64urlToStd(jwk.x)); - return { - publicKeyB64: btoa(String.fromCharCode(...pubBytes)), - privatePkcs8B64: btoa(String.fromCharCode(...pkcs8)), - }; -} - -/** - * Выводит полный keyBundle из логина и пароля. - * Та же самая логика, что используется в SHiNE-клиенте при регистрации. - * - * @param {string} login — логин сервера (нормализуется в нижний регистр) - * @param {string} password — пароль - * @param {function} [onProgress] — коллбэк(0..1) прогресса Argon2id - * @returns {{ rootPair, blockchainPair, devicePair }} - */ -export async function deriveKeyBundleFromPassword({ login, password, onProgress }) { - const { argon2idAsync } = await loadArgon2(); - const enc = new TextEncoder(); - const loginNorm = String(login || '').trim().toLowerCase(); - const pwd = String(password ?? ''); - - // Salt для master secret = sha256("shine-auth-v2|login=...|suffix=master.secret")[0..16] - const saltSource = `shine-auth-v2|login=${loginNorm}|suffix=master.secret`; - const saltFull = await sha256Bytes(enc.encode(saltSource)); - const salt = saltFull.slice(0, 16); - - const passBytes = enc.encode(`${loginNorm}\n${pwd}`); - const masterRaw = await argon2idAsync(passBytes, salt, { - t: 2, m: 65536, p: 1, dkLen: 32, - onProgress, - }); - const masterSecret32 = new Uint8Array(masterRaw); - - const [rootPair, blockchainPair, devicePair] = await Promise.all([ - _deriveEd25519PairFromMasterSecret(masterSecret32, 'root.key'), - _deriveEd25519PairFromMasterSecret(masterSecret32, 'bch.key'), - _deriveEd25519PairFromMasterSecret(masterSecret32, 'dev.key'), - ]); - - const masterSecretB64 = btoa(String.fromCharCode(...masterSecret32)); - return { masterSecretB64, rootPair, blockchainPair, devicePair }; -} - -// ------------------------------------------------------------------- -// Кодирование байт в base58 (для отображения Solana-адреса) -// ------------------------------------------------------------------- - -export function base58Encode(bytes) { - const ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; - let num = 0n; - for (const b of bytes) num = (num << 8n) | BigInt(b); - let result = ''; - while (num > 0n) { - result = ALPHA[Number(num % 58n)] + result; - num /= 58n; - } - for (const b of bytes) { - if (b !== 0) break; - result = '1' + result; - } - return result; -} diff --git a/shine-server-UI-obsolete/styles.css b/shine-server-UI-obsolete/styles.css deleted file mode 100644 index 5f87282..0000000 --- a/shine-server-UI-obsolete/styles.css +++ /dev/null @@ -1,193 +0,0 @@ -/* SHiNE Server Admin UI — тёмная тема */ -:root { - --bg: #111; - --surface: #1a1a1a; - --border: #2a2a2a; - --text: #e0e0e0; - --text-muted: #888; - --accent: #4a9eff; - --accent-hover: #6ab4ff; - --success: #4caf50; - --error: #f44336; - --warning: #ff9800; - --radius: 8px; -} - -* { box-sizing: border-box; margin: 0; padding: 0; } - -body { - background: var(--bg); - color: var(--text); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; - font-size: 14px; - line-height: 1.5; - padding: 24px 16px; -} - -.container { - max-width: 640px; - margin: 0 auto; -} - -h1 { - font-size: 20px; - font-weight: 600; - color: var(--accent); - margin-bottom: 4px; -} - -.subtitle { - color: var(--text-muted); - margin-bottom: 24px; - font-size: 13px; -} - -.card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 20px; - margin-bottom: 16px; -} - -.card h2 { - font-size: 15px; - font-weight: 600; - margin-bottom: 16px; - color: var(--text); -} - -.field { - margin-bottom: 14px; -} - -label { - display: block; - font-size: 12px; - color: var(--text-muted); - margin-bottom: 6px; - text-transform: uppercase; - letter-spacing: 0.04em; -} - -input[type="text"], input[type="password"], textarea { - width: 100%; - background: #0d0d0d; - border: 1px solid var(--border); - border-radius: var(--radius); - color: var(--text); - font-family: monospace; - font-size: 13px; - padding: 10px 12px; - outline: none; - transition: border-color 0.15s; - resize: vertical; -} - -input[type="text"]:focus, input[type="password"]:focus, textarea:focus { - border-color: var(--accent); -} - -input[type="text"][readonly] { - opacity: 0.6; -} - -textarea { - min-height: 80px; -} - -.hint { - font-size: 11px; - color: var(--text-muted); - margin-top: 4px; -} - -.btn-row { - display: flex; - gap: 10px; - margin-top: 20px; - flex-wrap: wrap; -} - -button { - padding: 10px 20px; - border-radius: var(--radius); - border: none; - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: opacity 0.15s, background 0.15s; -} - -button:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.btn-primary { - background: var(--accent); - color: #fff; -} - -.btn-primary:hover:not(:disabled) { background: var(--accent-hover); } - -.btn-secondary { - background: transparent; - color: var(--text-muted); - border: 1px solid var(--border); -} - -.btn-secondary:hover:not(:disabled) { - border-color: var(--accent); - color: var(--accent); -} - -.status { - padding: 12px 16px; - border-radius: var(--radius); - font-size: 13px; - margin-top: 16px; - word-break: break-all; - display: none; -} - -.status.info { display: block; background: #1a2433; border: 1px solid #2a4a6a; color: #7bb8ff; } -.status.success { display: block; background: #1a2e1a; border: 1px solid #2a4a2a; color: #7dcc7d; } -.status.error { display: block; background: #2e1a1a; border: 1px solid #5a2a2a; color: #f08080; } - -.pda-info { - display: none; - margin-top: 12px; -} - -.pda-row { - display: flex; - justify-content: space-between; - padding: 6px 0; - border-bottom: 1px solid var(--border); - font-size: 12px; -} - -.pda-row:last-child { border-bottom: none; } - -.pda-key { color: var(--text-muted); min-width: 160px; } -.pda-value { color: var(--text); font-family: monospace; text-align: right; word-break: break-all; } - -.nav-links { - margin-bottom: 20px; -} - -.nav-links a { - color: var(--accent); - text-decoration: none; - margin-right: 16px; - font-size: 13px; -} - -.nav-links a:hover { text-decoration: underline; } - -.section-divider { - border: none; - border-top: 1px solid var(--border); - margin: 20px 0; -} diff --git a/shine-server-UI-obsolete/update-server-pda.html b/shine-server-UI-obsolete/update-server-pda.html deleted file mode 100644 index 02ed43e..0000000 --- a/shine-server-UI-obsolete/update-server-pda.html +++ /dev/null @@ -1,484 +0,0 @@ - - - - - - Обновление PDA сервера — SHiNE Server Admin - - - - -
- - -

Обновление PDA сервера

-

Меняет адрес сервера или список серверов синхронизации

- -
-

Параметры Solana

-
- - -
-
- -
-

Загрузить существующую PDA

-
- - -
-
- -
-
-
-
PDA адрес
-
Версия
-
Создан
-
Обновлён
-
Адрес сервера
-
sync_servers
-
Blockchain
-
Paid limit
-
-
- - - -
-
- - - -