diff --git a/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md b/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md index f3356da..bef19aa 100644 --- a/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md +++ b/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md @@ -12,9 +12,9 @@ 6. Открыть `Аккаунт`, ввести логин, имя homeserver и нажать `Сгенерировать`; проверить, что появились секрет и адрес кошелька, а после перезагрузки они не исчезают. 7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка. 8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению. - 9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной. - 10. Выполнить регистрацию и убедиться, что статус меняется на `Homeserver активен`, онлайн-статус становится активным, а на экране появляются краткие отпечатки `PDA/TX`. - 11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана и соответствует `device`-адресу устройства. + 9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной; также убедиться, что между двумя нижними кнопками есть небольшой зазор. + 10. Нажать кнопку регистрации и убедиться, что она запускает on-chain flow сразу без отдельного confirm-экрана, а после завершения статус меняется на `Homeserver активен`, онлайн-статус становится активным, и на экране аккаунта появляются краткие отпечатки `PDA/TX`. + 11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства. 12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус. 13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте. 14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются. diff --git a/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md b/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md index 2e44049..f45add7 100644 --- a/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md +++ b/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md @@ -1,6 +1,6 @@ # ESP32 nav minimal test -- Краткое описание: раннее имя основного UI-скетча `shine_homeserver_main/` для homeserver на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста. +- Краткое описание: легаси-заметка по старому навигационному тесту UI для homeserver на базе `LVGL + subserver touch`. Актуальный основной скетч уже переехал в `shine_homeserver_main/`, а этот файл оставлен как историческая справка по старой версии. - Что проверять: - стартует экран `HOME`; - на `HOME` видны реальное значение homeserver или `homeserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка баланса, строка `SHiNE: ...`, кнопка `SETTINGS` уменьшенной ширины у правого края и нижняя подпись `SHiNE homeserver (v.0.18)`; @@ -27,7 +27,7 @@ - кнопка `SETTINGS` открывает `SETTINGS_MENU`; - свайп влево на `HOME` открывает `SETTINGS_MENU`; - если пользователь не найден в Solana PDA, слева снизу появляется `REGISTER ACCOUNT`; - - `REGISTER ACCOUNT` открывает экран-заглушку; + - `REGISTER ACCOUNT` открывает экран-заглушку только в старой тестовой версии; - в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`; - обе видимые карточки меню одного цвета; - свайп вверх показывает `Server` и `Account`; @@ -101,4 +101,4 @@ - если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`. - Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32. - Дополнительно ожидается: `HOME` уже показывает реальный Solana/WS-статус homeserver, а отсутствие пользователя в Solana заметно сразу без перехода в настройки. -- Статус: pending +- Статус: done diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino index ae9bc23..5919f63 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino @@ -52,6 +52,15 @@ static const char *kShineUsersProgramId = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm"; static const char *kShineUsersUserPdaSeedPrefix = "user_login="; +static const char *kShineLoginGuardProgramId = "3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo"; +static const char *kShinePaymentsProgramId = "c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW"; +static const char *kSystemProgramId = "11111111111111111111111111111111"; +static const char *kEd25519ProgramId = "Ed25519SigVerify111111111111111111111111111"; +static const char *kSysvarInstructionsId = "Sysvar1nstructions1111111111111111111111111"; +static const char *kUsersSeedPrefix = "user_login="; +static const char *kUsersEconomyConfigSeed = "shine_users_economy_config"; +static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault"; +static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK"; static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress"; static const uint8_t kBlockTypeRootKey = 1; static const uint8_t kBlockTypeDeviceKey = 2; @@ -234,6 +243,8 @@ static String gAccountPdaStatusMessage = "Account not checked"; static bool gAccountCheckPending = true; static unsigned long gLastAccountCheckMs = 0; static bool gShowRegisterAccountButton = false; +static String gUserPdaAddress; +static String gRegistrationSignature; static String gShineStatusLine = "SHiNE: account not configured"; static String gShineSessionId; static String gShineSessionKey; @@ -329,6 +340,51 @@ static String shineWsUrl(); static String shineHomeLine(); static String balanceHomeLine(); static uint64_t shineNowMs(); +static void shortVecEncode(size_t value, std::vector &out); +static void pushU32LE(std::vector &out, uint32_t value); +static void pushU64LE(std::vector &out, uint64_t value); +static void pushStrU8(std::vector &out, const String &value); +static void pushFixed(std::vector &out, const uint8_t *data, size_t len); +static String bytesToBase58(const uint8_t *data, size_t len); +static String buildBaseRpcRequest(const char *method, const String ¶msJson); +static bool rpcCallSolana(const char *method, const String ¶msJson, String &payloadOut); +static bool rpcResponseHasError(const String &payload); +static bool getLatestBlockhashBytes(uint8_t out[32], String &blockhashB58, String &messageOut); +static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &messageOut); +static std::vector buildLastBlockStateBytes(const String &login, const String &blockchainName); +static std::vector buildUnsignedCreateRecord( + const String &login, + const String &blockchainName, + const String &serverAddress, + const uint8_t rootPub[32], + const uint8_t devicePub[32], + const uint8_t blockchainPub[32], + const uint8_t lastBlockSignature[64], + uint64_t createdAtMs); +static std::vector buildCreateInstructionData( + const String &login, + const String &blockchainName, + const String &serverAddress, + const uint8_t rootPub[32], + const uint8_t devicePub[32], + const uint8_t blockchainPub[32], + const uint8_t lastBlockSignature[64], + const uint8_t rootSignature[64], + uint64_t createdAtMs); +static std::vector buildEd25519InstructionData(const uint8_t signature[64], const uint8_t publicKey[32], const uint8_t messageHash[32]); +static std::vector buildLegacyMessage( + const uint8_t recentBlockhash[32], + const uint8_t devicePub[32], + const uint8_t userPda[32], + const uint8_t inflowVault[32], + const uint8_t economyConfig[32], + const std::vector &edRootData, + const std::vector &edBchData, + const std::vector &createData); +static bool signMessageEd25519(const std::vector &message, const uint8_t secretKey[64], uint8_t signature[64]); +static String encodeTransactionBase64(const uint8_t signature[64], const std::vector &message); +static bool awaitTransactionConfirmation(const String &signatureB58, String &messageOut); +static bool registerHomeserverOnSolana(String &messageOut); static String buildSessionKeyStringFromPublicBase64(const String &pubB64); static bool deriveKeypairFromSeed32(const uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]); static bool deriveSeedKeypairFromBase58(const String &seedB58, uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]); @@ -569,6 +625,13 @@ static String normalizeLoginValue(const String &value) { return out; } +static String abbreviateValue(const String &value, size_t head = 8, size_t tail = 6) { + if (value.length() <= head + tail + 3) { + return value; + } + return value.substring(0, head) + "..." + value.substring(value.length() - tail); +} + static String jsonEscape(const String &value) { String out; out.reserve(value.length() + 8); @@ -781,6 +844,8 @@ static void markAccountStateDirty() { gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN; gAccountPdaStatusMessage = "Account not checked"; gShowRegisterAccountButton = false; + gUserPdaAddress = ""; + gRegistrationSignature = ""; clearShineSessionState(true); gShineStatusLine = "SHiNE: account not configured"; } @@ -965,6 +1030,501 @@ static uint64_t shineNowMs() { return value > 0 ? (uint64_t)value : (uint64_t)millis(); } +static void shortVecEncode(size_t value, std::vector &out) { + do { + uint8_t byte = value & 0x7F; + value >>= 7; + if (value) { + byte |= 0x80; + } + out.push_back(byte); + } while (value); +} + +static void pushU32LE(std::vector &out, uint32_t value) { + out.push_back((uint8_t)(value & 0xFF)); + out.push_back((uint8_t)((value >> 8) & 0xFF)); + out.push_back((uint8_t)((value >> 16) & 0xFF)); + out.push_back((uint8_t)((value >> 24) & 0xFF)); +} + +static void pushU64LE(std::vector &out, uint64_t value) { + for (int i = 0; i < 8; ++i) { + out.push_back((uint8_t)((value >> (8 * i)) & 0xFF)); + } +} + +static void pushStrU8(std::vector &out, const String &value) { + shortVecEncode(value.length(), out); + for (size_t i = 0; i < value.length(); ++i) { + out.push_back((uint8_t)value.charAt(i)); + } +} + +static void pushFixed(std::vector &out, const uint8_t *data, size_t len) { + out.insert(out.end(), data, data + len); +} + +static String bytesToBase58(const uint8_t *data, size_t len) { + char out[160] = {}; + shineSecretBase58Encode(data, len, out, sizeof(out)); + return String(out); +} + +static String buildBaseRpcRequest(const char *method, const String ¶msJson) { + return "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"" + String(method) + "\",\"params\":" + paramsJson + "}"; +} + +static bool rpcCallSolana(const char *method, const String ¶msJson, String &payloadOut) { + int code = -1; + if (!httpPostJson(gSolanaRpcUrl, buildBaseRpcRequest(method, paramsJson), code, payloadOut)) { + return false; + } + return code >= 200 && code < 300; +} + +static bool rpcResponseHasError(const String &payload) { + return payload.indexOf("\"error\"") >= 0; +} + +static std::vector buildLastBlockStateBytes(const String &login, const String &blockchainName) { + std::vector out; + out.reserve(80); + pushFixed(out, (const uint8_t *)kLastBlockPrefix, strlen(kLastBlockPrefix)); + pushStrU8(out, login); + pushStrU8(out, blockchainName); + pushU32LE(out, 0); + out.insert(out.end(), 32, 0); + pushU64LE(out, 0); + return out; +} + +static std::vector buildUnsignedCreateRecord( + const String &login, + const String &blockchainName, + const String &serverAddress, + const uint8_t rootPub[32], + const uint8_t devicePub[32], + const uint8_t blockchainPub[32], + const uint8_t lastBlockSignature[64], + uint64_t createdAtMs) { + std::vector out; + out.reserve(512); + pushFixed(out, (const uint8_t *)"SHiNE", 5); + out.push_back(1); + out.push_back(0); + out.push_back(0); + out.push_back(0); + pushU64LE(out, createdAtMs); + pushU64LE(out, createdAtMs); + pushU32LE(out, 0); + out.insert(out.end(), 32, 0); + pushStrU8(out, login); + out.push_back(7); + + out.push_back(kBlockTypeRootKey); + out.push_back(0); + pushFixed(out, rootPub, 32); + + out.push_back(kBlockTypeDeviceKey); + out.push_back(0); + pushFixed(out, devicePub, 32); + + out.push_back(kBlockTypeBlockchainRegistry); + out.push_back(0); + out.push_back(1); + out.push_back(1); + pushStrU8(out, blockchainName); + pushFixed(out, blockchainPub, 32); + pushU64LE(out, 100000); + pushU64LE(out, 0); + pushU32LE(out, 0); + out.insert(out.end(), 32, 0); + pushFixed(out, lastBlockSignature, 64); + out.push_back(0); + + out.push_back(kBlockTypeServerProfile); + out.push_back(0); + out.push_back(1); + out.push_back(1); + out.push_back(0); + pushStrU8(out, serverAddress); + out.push_back(0); + + out.push_back(kBlockTypeAccessServers); + out.push_back(0); + out.push_back(0); + + out.push_back(kBlockTypeSessions); + out.push_back(0); + out.push_back(1); + out.push_back(0); + + out.push_back(kBlockTypeTrustedState); + out.push_back(0); + out.push_back(0); + + uint16_t recordLen = (uint16_t)(out.size() + 64); + out[7] = (uint8_t)(recordLen & 0xFF); + out[8] = (uint8_t)((recordLen >> 8) & 0xFF); + return out; +} + +static std::vector buildCreateInstructionData( + const String &login, + const String &blockchainName, + const String &serverAddress, + const uint8_t rootPub[32], + const uint8_t devicePub[32], + const uint8_t blockchainPub[32], + const uint8_t lastBlockSignature[64], + const uint8_t rootSignature[64], + uint64_t createdAtMs) { + std::vector out; + out.reserve(512); + out.push_back(3); + pushStrU8(out, login); + pushFixed(out, rootPub, 32); + pushU64LE(out, createdAtMs); + pushU64LE(out, 0); + pushFixed(out, devicePub, 32); + pushFixed(out, blockchainPub, 32); + pushStrU8(out, blockchainName); + pushU64LE(out, 0); + pushU32LE(out, 0); + out.insert(out.end(), 32, 0); + pushFixed(out, lastBlockSignature, 64); + out.push_back(0); + out.push_back(1); + out.push_back(0); + pushStrU8(out, serverAddress); + out.push_back(0); + out.push_back(0); + out.push_back(1); + out.push_back(0); + out.push_back(0); + pushFixed(out, rootSignature, 64); + return out; +} + +static std::vector buildEd25519InstructionData(const uint8_t signature[64], const uint8_t publicKey[32], const uint8_t messageHash[32]) { + const uint16_t sigOff = 16; + const uint16_t pkOff = sigOff + 64; + const uint16_t msgOff = pkOff + 32; + std::vector out(msgOff + 32, 0); + out[0] = 1; + out[1] = 0; + out[2] = (uint8_t)(sigOff & 0xFF); + out[3] = (uint8_t)((sigOff >> 8) & 0xFF); + out[6] = (uint8_t)(pkOff & 0xFF); + out[7] = (uint8_t)((pkOff >> 8) & 0xFF); + out[10] = (uint8_t)(msgOff & 0xFF); + out[11] = (uint8_t)((msgOff >> 8) & 0xFF); + out[12] = 32; + out[4] = out[8] = out[14] = 0xFF; + out[5] = out[9] = out[15] = 0xFF; + memcpy(out.data() + sigOff, signature, 64); + memcpy(out.data() + pkOff, publicKey, 32); + memcpy(out.data() + msgOff, messageHash, 32); + return out; +} + +static bool getLatestBlockhashBytes(uint8_t out[32], String &blockhashB58, String &messageOut) { + String payload; + if (!rpcCallSolana("getLatestBlockhash", "[{\"commitment\":\"confirmed\"}]", payload)) { + messageOut = "RPC не вернул blockhash"; + return false; + } + if (!jsonStringField(payload, "blockhash", blockhashB58) || blockhashB58.isEmpty()) { + messageOut = "В ответе нет blockhash"; + return false; + } + if (!base58ToFixed32(blockhashB58, out)) { + messageOut = "Некорректный blockhash"; + return false; + } + return true; +} + +static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &messageOut) { + uint8_t userPda[32]; + std::vector> seeds = { + std::vector((const uint8_t *)kUsersSeedPrefix, (const uint8_t *)kUsersSeedPrefix + strlen(kUsersSeedPrefix)), + std::vector((const uint8_t *)login.c_str(), (const uint8_t *)login.c_str() + login.length())}; + if (!findProgramAddress(seeds, kShineUsersProgramId, userPda)) { + messageOut = "Не удалось вычислить user PDA"; + return false; + } + pdaAddress = bytesToBase58(userPda, 32); + String payload; + if (!rpcCallSolana("getAccountInfo", "[\"" + pdaAddress + "\",{\"encoding\":\"base64\",\"commitment\":\"confirmed\"}]", payload)) { + messageOut = "Не удалось проверить PDA"; + return false; + } + if (payload.indexOf("\"value\":null") >= 0) { + return false; + } + if (payload.indexOf("\"value\"") >= 0) { + messageOut = "Такой логин уже зарегистрирован"; + return true; + } + messageOut = "Непонятный ответ getAccountInfo"; + return false; +} + +static std::vector buildLegacyMessage( + const uint8_t recentBlockhash[32], + const uint8_t devicePub[32], + const uint8_t userPda[32], + const uint8_t inflowVault[32], + const uint8_t economyConfig[32], + const std::vector &edRootData, + const std::vector &edBchData, + const std::vector &createData) { + uint8_t systemProgram[32]; + uint8_t ed25519Program[32]; + uint8_t sysvarInstructions[32]; + uint8_t usersProgram[32]; + uint8_t loginGuardProgram[32]; + base58ToFixed32(kSystemProgramId, systemProgram); + base58ToFixed32(kEd25519ProgramId, ed25519Program); + base58ToFixed32(kSysvarInstructionsId, sysvarInstructions); + base58ToFixed32(kShineUsersProgramId, usersProgram); + base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram); + + std::vector> accountKeys; + accountKeys.emplace_back(devicePub, devicePub + 32); + accountKeys.emplace_back(userPda, userPda + 32); + accountKeys.emplace_back(inflowVault, inflowVault + 32); + accountKeys.emplace_back(systemProgram, systemProgram + 32); + accountKeys.emplace_back(sysvarInstructions, sysvarInstructions + 32); + accountKeys.emplace_back(economyConfig, economyConfig + 32); + accountKeys.emplace_back(loginGuardProgram, loginGuardProgram + 32); + accountKeys.emplace_back(ed25519Program, ed25519Program + 32); + accountKeys.emplace_back(usersProgram, usersProgram + 32); + + std::vector msg; + msg.reserve(512); + msg.push_back(1); + msg.push_back(0); + msg.push_back(6); + shortVecEncode(accountKeys.size(), msg); + for (const auto &key : accountKeys) { + msg.insert(msg.end(), key.begin(), key.end()); + } + msg.insert(msg.end(), recentBlockhash, recentBlockhash + 32); + shortVecEncode(3, msg); + + msg.push_back(7); + msg.push_back(0); + shortVecEncode(edRootData.size(), msg); + msg.insert(msg.end(), edRootData.begin(), edRootData.end()); + + msg.push_back(7); + msg.push_back(0); + shortVecEncode(edBchData.size(), msg); + msg.insert(msg.end(), edBchData.begin(), edBchData.end()); + + msg.push_back(8); + msg.push_back(7); + msg.push_back(0); + msg.push_back(1); + msg.push_back(3); + msg.push_back(2); + msg.push_back(4); + msg.push_back(5); + msg.push_back(6); + shortVecEncode(createData.size(), msg); + msg.insert(msg.end(), createData.begin(), createData.end()); + return msg; +} + +static bool signMessageEd25519(const std::vector &message, const uint8_t secretKey[64], uint8_t signature[64]) { + return crypto_sign_ed25519_detached(signature, nullptr, message.data(), (unsigned long long)message.size(), secretKey) == 0; +} + +static String encodeTransactionBase64(const uint8_t signature[64], const std::vector &message) { + std::vector tx; + tx.reserve(1 + 64 + message.size()); + shortVecEncode(1, tx); + pushFixed(tx, signature, 64); + tx.insert(tx.end(), message.begin(), message.end()); + return bytesToBase64String(tx.data(), tx.size()); +} + +static bool awaitTransactionConfirmation(const String &signatureB58, String &messageOut) { + for (int attempt = 0; attempt < 15; attempt++) { + String payload; + if (!rpcCallSolana("getSignatureStatuses", "[[\"" + signatureB58 + "\"],{\"searchTransactionHistory\":true}]", payload)) { + delay(1000); + continue; + } + if (payload.indexOf("\"err\":null") >= 0 && + (payload.indexOf("\"confirmationStatus\":\"confirmed\"") >= 0 || + payload.indexOf("\"confirmationStatus\":\"finalized\"") >= 0)) { + return true; + } + if (payload.indexOf("\"err\":{") >= 0 || payload.indexOf("\"err\":\"") >= 0) { + messageOut = "Транзакция отклонена сетью"; + return false; + } + delay(1000); + } + messageOut = "RPC не подтвердил транзакцию вовремя"; + return false; +} + +static bool registerHomeserverOnSolana(String &messageOut) { + messageOut = ""; + String cleanLogin = normalizeLoginValue(gLoginValue); + if (cleanLogin.isEmpty()) { + messageOut = "Логин не задан"; + return false; + } + if (!gSecretConfigured || gRootPrivB58.isEmpty() || gBlockchainPrivB58.isEmpty() || gDevicePrivB58.isEmpty()) { + messageOut = "Секрет не готов"; + return false; + } + if (WiFi.status() != WL_CONNECTED) { + messageOut = "Сначала подключите Wi-Fi"; + return false; + } + if (gSolanaRpcUrl.isEmpty()) { + messageOut = "Сначала задайте Solana RPC"; + return false; + } + + String existingPda; + String pdaCheckMessage; + if (pdaAlreadyExists(cleanLogin, existingPda, pdaCheckMessage)) { + gUserPdaAddress = existingPda; + gRegistrationSignature = ""; + saveAccountPrefs(); + gAccountPdaStatus = ACCOUNT_PDA_OK; + gAccountPdaStatusMessage = "Пользователь уже зарегистрирован"; + gShowRegisterAccountButton = false; + gAccountStatusMessage = "Пользователь уже зарегистрирован"; + gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered"; + refreshAccountPdaStatus(); + return true; + } + if (pdaCheckMessage == "Не удалось вычислить user PDA" || pdaCheckMessage == "Не удалось проверить PDA" || pdaCheckMessage == "Непонятный ответ getAccountInfo") { + messageOut = pdaCheckMessage; + return false; + } + + uint8_t userPda[32]; + uint8_t economyConfig[32]; + uint8_t inflowVault[32]; + if (!findProgramAddress({ + std::vector((const uint8_t *)kUsersSeedPrefix, (const uint8_t *)kUsersSeedPrefix + strlen(kUsersSeedPrefix)), + std::vector((const uint8_t *)cleanLogin.c_str(), (const uint8_t *)cleanLogin.c_str() + cleanLogin.length()) + }, kShineUsersProgramId, userPda) || + !findProgramAddress({ + std::vector((const uint8_t *)kUsersEconomyConfigSeed, (const uint8_t *)kUsersEconomyConfigSeed + strlen(kUsersEconomyConfigSeed)) + }, kShineUsersProgramId, economyConfig) || + !findProgramAddress({ + std::vector((const uint8_t *)kPaymentsInflowSeed, (const uint8_t *)kPaymentsInflowSeed + strlen(kPaymentsInflowSeed)) + }, kShinePaymentsProgramId, inflowVault)) { + messageOut = "Не удалось вычислить обязательные PDA"; + return false; + } + + uint8_t rootSeed[32] = {}; + uint8_t rootPub[32] = {}; + uint8_t rootSec[64] = {}; + uint8_t blockchainSeed[32] = {}; + uint8_t blockchainPub[32] = {}; + uint8_t blockchainSec[64] = {}; + uint8_t deviceSeed[32] = {}; + uint8_t devicePub[32] = {}; + uint8_t deviceSec[64] = {}; + if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) || + !deriveSeedKeypairFromBase58(gBlockchainPrivB58, blockchainSeed, blockchainPub, blockchainSec) || + !deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) { + messageOut = "Не удалось восстановить ключи"; + return false; + } + + String blockchainName = cleanLogin + "-001"; + std::vector lastBlockState = buildLastBlockStateBytes(cleanLogin, blockchainName); + uint8_t lastBlockHash[32]; + uint8_t lastBlockSignature[64]; + sha256calc(lastBlockState.data(), lastBlockState.size(), lastBlockHash); + if (!signMessageEd25519(std::vector(lastBlockHash, lastBlockHash + 32), blockchainSec, lastBlockSignature)) { + messageOut = "Не удалось подписать LastBlockState"; + return false; + } + + uint64_t createdAtMs = shineNowMs(); + std::vector unsignedRecord = buildUnsignedCreateRecord( + cleanLogin, blockchainName, gShineServerUrl, + rootPub, devicePub, blockchainPub, + lastBlockSignature, createdAtMs); + uint8_t unsignedHash[32]; + uint8_t rootSignature[64]; + sha256calc(unsignedRecord.data(), unsignedRecord.size(), unsignedHash); + if (!signMessageEd25519(std::vector(unsignedHash, unsignedHash + 32), rootSec, rootSignature)) { + messageOut = "Не удалось подписать PDA-запись"; + return false; + } + + std::vector createData = buildCreateInstructionData( + cleanLogin, blockchainName, gShineServerUrl, + rootPub, devicePub, blockchainPub, + lastBlockSignature, rootSignature, createdAtMs); + std::vector edRootData = buildEd25519InstructionData(rootSignature, rootPub, unsignedHash); + std::vector edBchData = buildEd25519InstructionData(lastBlockSignature, blockchainPub, lastBlockHash); + + uint8_t recentBlockhash[32]; + String recentBlockhash58; + if (!getLatestBlockhashBytes(recentBlockhash, recentBlockhash58, messageOut)) { + return false; + } + + std::vector message = buildLegacyMessage( + recentBlockhash, + devicePub, + userPda, + inflowVault, + economyConfig, + edRootData, + edBchData, + createData); + uint8_t txSignature[64]; + if (!signMessageEd25519(message, deviceSec, txSignature)) { + messageOut = "Не удалось подписать Solana-транзакцию"; + return false; + } + String txBase64 = encodeTransactionBase64(txSignature, message); + String signatureB58 = bytesToBase58(txSignature, 64); + + String payload; + if (!rpcCallSolana("sendTransaction", "[\"" + txBase64 + "\",{\"encoding\":\"base64\",\"preflightCommitment\":\"confirmed\"}]", payload)) { + messageOut = "RPC не принял транзакцию"; + return false; + } + if (rpcResponseHasError(payload)) { + messageOut = "RPC вернул ошибку sendTransaction"; + return false; + } + if (!awaitTransactionConfirmation(signatureB58, messageOut)) { + return false; + } + + gUserPdaAddress = bytesToBase58(userPda, 32); + gRegistrationSignature = signatureB58; + gAccountStatusMessage = "Solana-регистрация завершена"; + gAccountPdaStatus = ACCOUNT_PDA_OK; + gAccountPdaStatusMessage = "Пользователь зарегистрирован"; + gShowRegisterAccountButton = false; + gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered"; + saveAccountPrefs(); + refreshAccountPdaStatus(); + messageOut = "Solana-регистрация подтверждена"; + return true; +} + static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUserState &outState, String &errorOut) { outState = ShinePdaUserState{}; errorOut = ""; @@ -1817,6 +2377,8 @@ static void loadPrefs() { gShineSessionId = gPrefs.getString("shine_sess_id", ""); gShineSessionKey = gPrefs.getString("shine_sess_key", ""); gShineStoragePwd = gPrefs.getString("shine_store_pwd", ""); + gUserPdaAddress = gPrefs.getString("user_pda", ""); + gRegistrationSignature = gPrefs.getString("registration_sig", ""); gBalanceStatusMessage = gDevicePubB58.isEmpty() ? "Balance: secret not set" : "Balance: tap to load"; gAccountCheckPending = true; gLastAccountCheckMs = 0; @@ -1853,6 +2415,16 @@ static void saveAccountPrefs() { gPrefs.putString("homeserver", gHomeserverValue); gPrefs.putBool("secret_set", gSecretConfigured); gPrefs.putString("secret_b58", gSecretBase58); + if (gUserPdaAddress.isEmpty()) { + gPrefs.remove("user_pda"); + } else { + gPrefs.putString("user_pda", gUserPdaAddress); + } + if (gRegistrationSignature.isEmpty()) { + gPrefs.remove("registration_sig"); + } else { + gPrefs.putString("registration_sig", gRegistrationSignature); + } if (gSecretConfigured) { gPrefs.putBytes("secret_bytes", gSecretBytes, 32); } else { @@ -2410,7 +2982,18 @@ static void actionButtonCb(lv_event_t *event) { showScreen(SCREEN_SETTINGS_MENU); break; case ACTION_REGISTER_ACCOUNT: - showScreen(SCREEN_REGISTER_ACCOUNT_PLACEHOLDER); + gAccountStatusMessage = "Регистрация запущена..."; + gShineStatusLine = "SHiNE: регистрация запущена"; + { + String registerMessage; + if (registerHomeserverOnSolana(registerMessage)) { + gAccountStatusMessage = registerMessage; + } else { + gAccountStatusMessage = registerMessage; + gShineStatusLine = String("SHiNE: ") + registerMessage; + } + rebuildScreen(); + } break; case ACTION_OPEN_WIFI: gWifiViewMode = WIFI_VIEW_OVERVIEW; @@ -2677,9 +3260,9 @@ static void drawHome() { makeButton(balanceHomeLine().c_str(), 22, 254, 436, 56, 0x355C7D, ACTION_REFRESH_BALANCE, &lv_font_montserrat_18); showMessageAt(shineHomeLine(), 322); if (gShowRegisterAccountButton) { - makeButton("REGISTER ACCOUNT", 22, 360, 220, 78, 0x2A9D8F, ACTION_REGISTER_ACCOUNT, &lv_font_montserrat_20); + makeButton("REGISTER ACCOUNT", 20, 360, 210, 78, 0x2A9D8F, ACTION_REGISTER_ACCOUNT, &lv_font_montserrat_20); } - makeButton("SETTINGS", 238, 360, 220, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24); + makeButton("SETTINGS", 250, 360, 210, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24); makeVersionTag(); } @@ -2791,6 +3374,12 @@ static void drawAccountScreen() { makeTitle("ACCOUNT", 18, &lv_font_montserrat_24); showMessageAt(gAccountStatusMessage, 56); + if (!gUserPdaAddress.isEmpty()) { + showMessageAt(String("PDA: ") + abbreviateValue(gUserPdaAddress, 10, 6), 80); + } + if (!gRegistrationSignature.isEmpty()) { + showMessageAt(String("TX: ") + abbreviateValue(gRegistrationSignature, 10, 6), 100); + } String loginButton = String("Login (") + (gLoginValue.isEmpty() ? "not set" : gLoginValue) + ")"; String homeserverButton = String("Homeserver (") + (gHomeserverValue.isEmpty() ? "not set" : gHomeserverValue) + ")"; @@ -2939,8 +3528,8 @@ static void drawSecretGenerateCancelConfirmScreen() { static void drawRegisterAccountPlaceholderScreen() { setRootStyle(); makeTitle("REGISTER ACCOUNT", 22, &lv_font_montserrat_24); - makeBody("Registration flow is not implemented yet.", 112, 420); - makeBody("This button is shown when login is not found in Solana PDA.", 156, 420); + makeBody("Registration now starts directly from the home screen button.", 112, 420); + makeBody("This screen is kept only as a fallback status page.", 156, 420); makeButton("BACK", 140, 360, 200, 72, 0x5A6570, ACTION_BACK_HOME, &lv_font_montserrat_22); makeVersionTag(); } diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md index a3db5d4..9ca9e84 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md @@ -1,7 +1,7 @@ # SHiNE ESP32 Homeserver UI Nav Minimal Spec -Минимальный навигационный прототип для `Waveshare ESP32-S3-Touch-AMOLED-2.16`. -Этот прототип был перенесён в основной скетч `../main-device/shine_homeserver_main/`, а старое имя `lvgl-nav-minimal-test` осталось только как историческая ссылка. +Легаси-спецификация старого навигационного теста для `Waveshare ESP32-S3-Touch-AMOLED-2.16`. +Актуальный основной скетч теперь находится в `../main-device/shine_homeserver_main/`; этот документ оставлен только как историческая справка по старому тестовому UI. ## Цель @@ -64,6 +64,9 @@ - кнопка `SETTINGS` -> `SETTINGS_MENU`; - свайп влево -> `SETTINGS_MENU`. +Примечание: +- поведение `REGISTER ACCOUNT -> REGISTER_ACCOUNT_PLACEHOLDER` относится к старой тестовой версии и не является актуальным для основного скетча. + Фоновая логика: - пока открыт `HOME`, экран сам обновляется примерно раз в секунду; - при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC; diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md index 63b4e8e..471ebd8 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md @@ -25,7 +25,7 @@ - реальная проверка доступности `API`, `RPC` и `WS`-адресов; - реальное чтение баланса кошелька из `Solana RPC`; - проверка обязательных условий перед регистрацией; -- живая on-chain регистрация серверного `user_pda` в `shine_users` через `device key` устройства; +- живая on-chain регистрация серверного `user_pda` в `shine_users` по нажатию кнопки на главном экране; - прототип входящих запросов с подтверждением и отклонением; - PIN-блокировка (в текущей временной сборке вход по PIN отключён, устройство открывает `HOME` сразу после старта); - базовые настройки, статус и главный экран; @@ -159,11 +159,12 @@ Дополнительная большая кнопка: -- `Зарегистрировать` +- `REGISTER ACCOUNT` Если регистрация уже сделана: - вместо призыва к регистрации показывается статус `Homeserver активен`. +- две нижние кнопки внизу экрана не прилегают вплотную друг к другу, между ними есть небольшой зазор. ## Экран STATUS @@ -277,6 +278,7 @@ - `Сгенерировать` создаёт новый `master secret` и пересчитывает из него `device`-кошелёк; - `Очистить` удаляет секрет, адрес кошелька, `PDA`, `tx`, регистрацию и онлайн-статус; - логин приводится к нижнему регистру и trim. +- после успешной регистрации на экране сохраняются и отображаются краткие отпечатки `PDA` и `TX`. ## Экран WALLET @@ -438,8 +440,8 @@ QR должен быть сканируемым, а не декоративны 10. открыть `Кошелёк`; 11. при необходимости пополнить баланс; 12. вернуться на `HOME`; -13. нажать `Зарегистрировать`; -14. после подтверждения увидеть статус `Homeserver активен`. +13. нажать `REGISTER ACCOUNT`; +14. после завершения увидеть статус `Homeserver активен`. Примечание: diff --git a/VERSION.properties b/VERSION.properties index 1f4cc56..0f2cf43 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.163 -server.version=1.2.152 +client.version=1.2.164 +server.version=1.2.153