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 0d8ef8c..874809d 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 @@ -14,14 +14,19 @@ 8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению. 9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной; также убедиться, что между двумя нижними кнопками есть небольшой зазор. 10. Нажать кнопку регистрации и убедиться, что открывается отдельный экран проверки, где ещё раз видно `login`, статус свободного `PDA`, баланс, `homeserver1` с пометкой о стандартном значении и сообщение, если `Wi-Fi` не подключён. - 11. На экране проверки нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ` и убедиться, что после этого появляется отдельный экран результата с успехом либо подробной ошибкой. + 11. На экране проверки нажать `REGISTER IN SHINE` и убедиться, что после этого появляется отдельный экран результата с успехом либо подробной ошибкой. 12. После успешной регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства, а `tx signature` тоже сохранён. - 13. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус. - 14. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте. - 15. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются. + 13. После успешной регистрации вернуться на `HOME` и проверить новую жёлтую кнопку: + - если в `PDA` ещё нет текущего homeserver, должна появиться `ADD HOMESERVER`; + - если ключ homeserver в `PDA` не совпадает с локальным секретом, должна появиться `FIX HOMESERVER PASSWORD`. + 14. Нажать жёлтую кнопку и убедиться, что открывается отдельный экран пояснения, а затем экран результата обновления `PDA`. + 15. После успешного `ADD/FIX HOMESERVER` проверить, что основной экран больше не показывает `homeserver not in PDA` или `homeserver key mismatch`, а `SHiNE` может перейти к авторизации. + 16. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус. + 17. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте. + 18. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются. - ожидаемый результат: - новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации homeserver. + новый `ESP32`-скетч стабильно запускается, показывает читаемый англоязычный интерфейс, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI, позволяет вручную пройти on-chain регистрацию пользователя и затем отдельным действием записать/исправить homeserver-сессию в `shine_users`. - статус: pending diff --git a/Dev_Docs/Pending_Features/2026-06-13_1545_esp32_homeserver_pda_update.md b/Dev_Docs/Pending_Features/2026-06-13_1545_esp32_homeserver_pda_update.md new file mode 100644 index 0000000..b007af3 --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-13_1545_esp32_homeserver_pda_update.md @@ -0,0 +1,24 @@ +# ESP32: добавление и исправление homeserver в user PDA + +- краткое описание фичи: + в основном ESP32-скетче добавлен отдельный flow после регистрации пользователя: если homeserver-сессия отсутствует в `shine_users` PDA или её ключ не совпадает с локальным секретом, на главном экране показывается жёлтая кнопка `ADD HOMESERVER` или `FIX HOMESERVER PASSWORD`. Действие открывает отдельный экран объяснения, затем отправляет `update_user_pda` и показывает экран результата. + +- что именно проверять: + 1. Зарегистрировать пользователя или взять уже существующего пользователя без корректной homeserver-сессии. + 2. На `HOME` убедиться, что вместо кнопки регистрации появилась жёлтая кнопка: + `ADD HOMESERVER` либо `FIX HOMESERVER PASSWORD`. + 3. Нажать её и проверить, что открывается отдельный экран с пояснением причины и кнопкой действия. + 4. Выполнить действие и дождаться экрана результата. + 5. После успеха проверить, что: + - `homeserver not in PDA` или `homeserver key mismatch` исчезли; + - в USB-диагностике есть успешный `tx_signature`; + - `SHiNE` может перейти к авторизации на сервере. + 6. Если действие завершается ошибкой, проверить, что: + - текст ошибки показан на экране результата; + - команда `last_error` по USB возвращает полный сохранённый payload. + +- ожидаемый результат: + устройство после обычной регистрации пользователя способно отдельной транзакцией добавить или исправить homeserver-сессию в `shine_users` PDA, а ошибки этого шага сохраняются в ту же USB/NVS-диагностику. + +- статус: + pending 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 a2ee65b..a964ada 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 @@ -97,6 +97,8 @@ enum Screen { SCREEN_TEXT_EDIT, SCREEN_REGISTER_ACCOUNT_CONFIRM, SCREEN_REGISTER_ACCOUNT_RESULT, + SCREEN_HOMESERVER_PDA_CONFIRM, + SCREEN_HOMESERVER_PDA_RESULT, }; enum SwipeDirection { @@ -136,6 +138,9 @@ enum ActionId { ACTION_REFRESH_BALANCE, ACTION_REGISTER_ACCOUNT, ACTION_REGISTER_ACCOUNT_EXECUTE, + ACTION_HOMESERVER_PDA_ACTION, + ACTION_HOMESERVER_PDA_ADD_EXECUTE, + ACTION_HOMESERVER_PDA_FIX_EXECUTE, ACTION_EDITOR_SAVE, ACTION_EDITOR_CANCEL, }; @@ -178,11 +183,29 @@ struct ShinePdaSessionRecord { struct ShinePdaUserState { bool found = false; String login; + uint64_t createdAtMs = 0; + uint64_t updatedAtMs = 0; + uint32_t recordNumber = 0; + uint8_t prevRecordHash32[32] = {}; + uint8_t blockchainType = 1; bool isServer = false; + uint8_t addressFormatType = 0; + uint8_t addressFormatVersion = 0; String serverAddress; + std::vector syncServers; + std::vector accessServers; + uint8_t sessionsMode = 1; + uint8_t trustedCount = 0; uint8_t rootKey32[32] = {}; uint8_t deviceKey32[32] = {}; uint8_t blockchainKey32[32] = {}; + String blockchainName; + uint64_t paidLimitBytes = 0; + uint64_t usedBytes = 0; + uint32_t lastBlockNumber = 0; + uint8_t lastBlockHash32[32] = {}; + uint8_t lastBlockSignature64[64] = {}; + String arweaveTxId; std::vector sessions; }; @@ -256,6 +279,8 @@ static String gAccountPdaStatusMessage = "Account not checked"; static bool gAccountCheckPending = true; static unsigned long gLastAccountCheckMs = 0; static bool gShowRegisterAccountButton = false; +static bool gShowHomeserverPdaActionButton = false; +static String gHomeserverPdaActionReason; static String gUserPdaAddress; static String gRegistrationSignature; static String gShineStatusLine = "SHiNE: account not configured"; @@ -267,15 +292,18 @@ static bool gRegisterConfirmCanSubmit = false; static String gRegisterResultMessage; static String gRegisterResultDetails; static bool gRegisterResultSuccess = false; +static String gHomeserverPdaActionMessage; +static String gHomeserverPdaActionDetail; +static bool gHomeserverPdaCanAdd = false; +static bool gHomeserverPdaCanFix = false; +static String gHomeserverPdaResultMessage; +static String gHomeserverPdaResultDetails; +static bool gHomeserverPdaResultSuccess = false; static String gLastRegisterDiagStatus = "none"; static String gLastRegisterDiagSummary; static String gLastRegisterDiagDetails; static String gLastRegisterDiagTime; static String gRegisterTriggerSource = "manual"; -static unsigned long gBootMillis = 0; -static bool gAutoRegisterTestArmed = true; -static bool gAutoRegisterTestStarted = false; -static bool gAutoRegisterTestFinished = false; static String gSerialCommandBuffer; static String gShineSessionId; static String gShineSessionKey; @@ -393,7 +421,11 @@ static bool extractRpcErrorSummary(const String &payload, String &messageOut); static String compactRpcLogs(const String &payload, int maxLines = 3); static bool simulateTransactionPayload(const String &txBase64, String &payloadOut); static bool simulateTransactionForError(const String &txBase64, String &messageOut); -static std::vector buildLastBlockStateBytes(const String &login, const String &blockchainName); +static std::vector buildLastBlockStateBytes(const String &login, + const String &blockchainName, + uint32_t lastBlockNumber = 0, + const uint8_t *lastBlockHash32 = nullptr, + uint64_t usedBytes = 0); static std::vector buildUnsignedCreateRecord( const String &login, const String &blockchainName, @@ -415,6 +447,12 @@ static std::vector buildCreateInstructionData( 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 serializeUnsignedRecordState(const ShinePdaUserState &state); +static std::vector buildUpdateInstructionData(const ShinePdaUserState &state, + uint32_t nextVersion, + uint64_t updatedAtMs, + const uint8_t prevHash32[32], + const uint8_t rootSignature64[64]); static std::vector buildLegacyMessage( const uint8_t recentBlockhash[32], const uint8_t devicePub[32], @@ -424,11 +462,21 @@ static std::vector buildLegacyMessage( const std::vector &edRootData, const std::vector &edBchData, const std::vector &createData); +static std::vector buildUpdateLegacyMessage( + 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 &updateData); 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 void executeRegisterAccountFlow(const char *triggerSource, bool showResultScreen); +static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messageOut); static void loadRegisterDiagDetailsFromPrefs(); static void saveRegisterDiagDetailsToPrefs(const String &details); static void clearRegisterDiagDetailsFromPrefs(); @@ -437,6 +485,7 @@ static void printRegisterDiagToSerial(); static void clearRegisterDiag(); static void handleUsbSerialCommands(); static void prepareRegisterAccountScreen(); +static void prepareHomeserverPdaActionScreen(); 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]); @@ -912,6 +961,15 @@ static void markAccountStateDirty() { gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN; gAccountPdaStatusMessage = "Account not checked"; gShowRegisterAccountButton = false; + gShowHomeserverPdaActionButton = false; + gHomeserverPdaActionReason = ""; + gHomeserverPdaActionMessage = ""; + gHomeserverPdaActionDetail = ""; + gHomeserverPdaCanAdd = false; + gHomeserverPdaCanFix = false; + gHomeserverPdaResultMessage = ""; + gHomeserverPdaResultDetails = ""; + gHomeserverPdaResultSuccess = false; gUserPdaAddress = ""; gRegistrationSignature = ""; gRegisterConfirmMessage = ""; @@ -1170,15 +1228,160 @@ static bool rpcResponseHasError(const String &payload) { return payload.indexOf("\"error\"") >= 0; } -static std::vector buildLastBlockStateBytes(const String &login, const String &blockchainName) { +static std::vector buildLastBlockStateBytes(const String &login, + const String &blockchainName, + uint32_t lastBlockNumber, + const uint8_t *lastBlockHash32, + uint64_t usedBytes) { 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); + pushU32LE(out, lastBlockNumber); + if (lastBlockHash32) { + pushFixed(out, lastBlockHash32, 32); + } else { + out.insert(out.end(), 32, 0); + } + pushU64LE(out, usedBytes); + return out; +} + +static std::vector serializeUnsignedRecordState(const ShinePdaUserState &state) { + std::vector out; + out.reserve(640); + 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, state.createdAtMs); + pushU64LE(out, state.updatedAtMs); + pushU32LE(out, state.recordNumber); + pushFixed(out, state.prevRecordHash32, 32); + pushStrU8(out, state.login); + out.push_back(state.isServer ? 7 : 6); + + out.push_back(kBlockTypeRootKey); + out.push_back(0); + pushFixed(out, state.rootKey32, 32); + + out.push_back(kBlockTypeDeviceKey); + out.push_back(0); + pushFixed(out, state.deviceKey32, 32); + + out.push_back(kBlockTypeBlockchainRegistry); + out.push_back(0); + out.push_back(1); + out.push_back(state.blockchainType); + pushStrU8(out, state.blockchainName); + pushFixed(out, state.blockchainKey32, 32); + pushU64LE(out, state.paidLimitBytes); + pushU64LE(out, state.usedBytes); + pushU32LE(out, state.lastBlockNumber); + pushFixed(out, state.lastBlockHash32, 32); + pushFixed(out, state.lastBlockSignature64, 64); + if (state.arweaveTxId.isEmpty()) { + out.push_back(0); + } else { + out.push_back(1); + pushStrU8(out, state.arweaveTxId); + } + + if (state.isServer) { + out.push_back(kBlockTypeServerProfile); + out.push_back(0); + out.push_back(1); + out.push_back(state.addressFormatType); + out.push_back(state.addressFormatVersion); + pushStrU8(out, state.serverAddress); + out.push_back((uint8_t)state.syncServers.size()); + for (const auto &value : state.syncServers) { + pushStrU8(out, value); + } + } + + out.push_back(kBlockTypeAccessServers); + out.push_back(0); + out.push_back((uint8_t)state.accessServers.size()); + for (const auto &value : state.accessServers) { + pushStrU8(out, value); + } + + out.push_back(kBlockTypeSessions); + out.push_back(0); + out.push_back(state.sessionsMode); + out.push_back((uint8_t)state.sessions.size()); + for (const auto &session : state.sessions) { + out.push_back(session.sessionType); + out.push_back(session.sessionVersion); + pushStrU8(out, session.sessionName); + pushFixed(out, session.sessionPubKey32, 32); + } + + out.push_back(kBlockTypeTrustedState); + out.push_back(0); + out.push_back(state.trustedCount); + + 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 buildUpdateInstructionData(const ShinePdaUserState &state, + uint32_t nextVersion, + uint64_t updatedAtMs, + const uint8_t prevHash32[32], + const uint8_t rootSignature64[64]) { + std::vector out; + out.reserve(640); + out.push_back(4); + pushStrU8(out, state.login); + pushFixed(out, state.rootKey32, 32); + pushU64LE(out, state.createdAtMs); + pushU64LE(out, updatedAtMs); + pushU32LE(out, nextVersion); + pushFixed(out, prevHash32, 32); pushU64LE(out, 0); + pushFixed(out, state.deviceKey32, 32); + pushFixed(out, state.blockchainKey32, 32); + pushStrU8(out, state.blockchainName); + pushU64LE(out, state.usedBytes); + pushU32LE(out, state.lastBlockNumber); + pushFixed(out, state.lastBlockHash32, 32); + pushFixed(out, state.lastBlockSignature64, 64); + if (state.arweaveTxId.isEmpty()) { + out.push_back(0); + } else { + out.push_back(1); + pushStrU8(out, state.arweaveTxId); + } + out.push_back(state.isServer ? 1 : 0); + if (state.isServer) { + out.push_back(state.addressFormatType); + out.push_back(state.addressFormatVersion); + pushStrU8(out, state.serverAddress); + out.push_back((uint8_t)state.syncServers.size()); + for (const auto &value : state.syncServers) { + pushStrU8(out, value); + } + } + out.push_back((uint8_t)state.accessServers.size()); + for (const auto &value : state.accessServers) { + pushStrU8(out, value); + } + out.push_back(state.sessionsMode); + out.push_back((uint8_t)state.sessions.size()); + for (const auto &session : state.sessions) { + out.push_back(session.sessionType); + out.push_back(session.sessionVersion); + pushStrU8(out, session.sessionName); + pushFixed(out, session.sessionPubKey32, 32); + } + out.push_back(state.trustedCount); + pushFixed(out, rootSignature64, 64); return out; } @@ -1585,6 +1788,69 @@ static std::vector buildLegacyMessage( return msg; } +static std::vector buildUpdateLegacyMessage( + 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 &updateData) { + uint8_t systemProgram[32]; + uint8_t ed25519Program[32]; + uint8_t usersProgram[32]; + uint8_t sysvarInstructions[32]; + base58ToFixed32(kSystemProgramId, systemProgram); + base58ToFixed32(kEd25519ProgramId, ed25519Program); + base58ToFixed32(kShineUsersProgramId, usersProgram); + base58ToFixed32(kSysvarInstructionsId, sysvarInstructions); + + 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(economyConfig, economyConfig + 32); + accountKeys.emplace_back(ed25519Program, ed25519Program + 32); + accountKeys.emplace_back(usersProgram, usersProgram + 32); + accountKeys.emplace_back(sysvarInstructions, sysvarInstructions + 32); + + std::vector msg; + msg.reserve(512); + msg.push_back(1); + msg.push_back(0); + msg.push_back(5); + 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(5); + msg.push_back(0); + shortVecEncode(edRootData.size(), msg); + msg.insert(msg.end(), edRootData.begin(), edRootData.end()); + + msg.push_back(5); + msg.push_back(0); + shortVecEncode(edBchData.size(), msg); + msg.insert(msg.end(), edBchData.begin(), edBchData.end()); + + msg.push_back(6); + msg.push_back(6); + msg.push_back(0); + msg.push_back(1); + msg.push_back(3); + msg.push_back(2); + msg.push_back(7); + msg.push_back(4); + shortVecEncode(updateData.size(), msg); + msg.insert(msg.end(), updateData.begin(), updateData.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; } @@ -1632,7 +1898,6 @@ static bool registerHomeserverOnSolana(String &messageOut) { String cleanLogin = normalizeLoginValue(gLoginValue); diagDetails += String("trigger=") + gRegisterTriggerSource + "\n"; - diagDetails += String("boot_millis=") + String(gBootMillis) + "\n"; diagDetails += String("test_uptime_ms=") + String(millis()) + "\n"; diagDetails += String("login=") + cleanLogin + "\n"; diagDetails += String("rpc=") + gSolanaRpcUrl + "\n"; @@ -1746,7 +2011,7 @@ static bool registerHomeserverOnSolana(String &messageOut) { String blockchainName = cleanLogin + "-001"; diagDetails += String("blockchain_name=") + blockchainName + "\n"; - std::vector lastBlockState = buildLastBlockStateBytes(cleanLogin, blockchainName); + std::vector lastBlockState = buildLastBlockStateBytes(cleanLogin, blockchainName, 0, nullptr, 0); uint8_t lastBlockHash[32]; uint8_t lastBlockSignature[64]; sha256calc(lastBlockState.data(), lastBlockState.size(), lastBlockHash); @@ -1947,6 +2212,264 @@ static void prepareRegisterAccountScreen() { gRegisterConfirmCanSubmit = true; } +static void prepareHomeserverPdaActionScreen() { + gHomeserverPdaActionMessage = ""; + gHomeserverPdaActionDetail = ""; + gHomeserverPdaCanAdd = false; + gHomeserverPdaCanFix = false; + gHomeserverPdaResultMessage = ""; + gHomeserverPdaResultDetails = ""; + gHomeserverPdaResultSuccess = false; + + if (gLoginValue.isEmpty()) { + gHomeserverPdaActionMessage = "Login is not set"; + return; + } + if (!gSecretConfigured) { + gHomeserverPdaActionMessage = "Secret is not set"; + return; + } + if (gHomeserverValue.isEmpty()) { + gHomeserverPdaActionMessage = "Homeserver is not set"; + return; + } + if (WiFi.status() != WL_CONNECTED) { + gHomeserverPdaActionMessage = "Wi-Fi is not connected"; + gHomeserverPdaActionDetail = "Connect Wi-Fi, then try again."; + return; + } + + if (gHomeserverPdaActionReason == "homeserver key mismatch") { + gHomeserverPdaActionMessage = "Homeserver key in PDA does not match local secret"; + gHomeserverPdaActionDetail = String("Homeserver: ") + gHomeserverValue; + gHomeserverPdaCanFix = true; + return; + } + if (gHomeserverPdaActionReason == "homeserver not in PDA") { + gHomeserverPdaActionMessage = "Homeserver session is missing from user PDA"; + gHomeserverPdaActionDetail = String("Homeserver: ") + gHomeserverValue; + gHomeserverPdaCanAdd = true; + return; + } + + gHomeserverPdaActionMessage = "Homeserver PDA action is not available"; +} + +static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messageOut) { + messageOut = ""; + String diagDetails; + auto failWithDiag = [&](const String &summary) -> bool { + messageOut = summary; + saveRegisterDiag("error", summary, diagDetails); + printRegisterDiagToSerial(); + return false; + }; + + String cleanLogin = normalizeLoginValue(gLoginValue); + diagDetails += String("trigger=") + gRegisterTriggerSource + "\n"; + diagDetails += String("action=") + (requireExisting ? "fix_homeserver_key" : "add_homeserver") + "\n"; + diagDetails += String("uptime_ms=") + String(millis()) + "\n"; + diagDetails += String("login=") + cleanLogin + "\n"; + diagDetails += String("homeserver=") + gHomeserverValue + "\n"; + diagDetails += String("rpc=") + gSolanaRpcUrl + "\n"; + + if (cleanLogin.isEmpty()) { + return failWithDiag("Login is not set"); + } + if (!gSecretConfigured) { + return failWithDiag("Secret is not ready"); + } + if (gHomeserverValue.isEmpty()) { + return failWithDiag("Homeserver is not set"); + } + if (WiFi.status() != WL_CONNECTED) { + diagDetails += "wifi=disconnected\n"; + return failWithDiag("Connect Wi-Fi first"); + } + diagDetails += String("wifi=connected:") + WiFi.localIP().toString() + "\n"; + + ShinePdaUserState currentState; + String stateError; + if (!readShineUserPda(cleanLogin, currentState, stateError)) { + diagDetails += String("read_pda_error=") + stateError + "\n"; + return failWithDiag(stateError.isEmpty() ? "Failed to read user PDA" : stateError); + } + if (!currentState.found) { + return failWithDiag("User PDA does not exist yet"); + } + + uint8_t rootSeed[32] = {}; + uint8_t rootPub[32] = {}; + uint8_t rootSec[64] = {}; + uint8_t deviceSeed[32] = {}; + uint8_t devicePub[32] = {}; + uint8_t deviceSec[64] = {}; + uint8_t homeserverPub[32] = {}; + if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) + || !deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec) + || !base58ToFixed32(gHomeserverPubB58, homeserverPub)) { + return failWithDiag("Failed to restore local keys"); + } + if (memcmp(devicePub, currentState.deviceKey32, 32) != 0) { + return failWithDiag("Device key does not match PDA"); + } + + 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)) { + return failWithDiag("Failed to derive required PDAs"); + } + + String userPdaB58 = bytesToBase58(userPda, 32); + diagDetails += String("user_pda=") + userPdaB58 + "\n"; + diagDetails += String("users_economy_config_pda=") + bytesToBase58(economyConfig, 32) + "\n"; + diagDetails += String("inflow_vault_pda=") + bytesToBase58(inflowVault, 32) + "\n"; + + ShinePdaUserState nextState = currentState; + memcpy(nextState.rootKey32, rootPub, 32); + memcpy(nextState.deviceKey32, devicePub, 32); + nextState.updatedAtMs = shineNowMs(); + nextState.recordNumber = currentState.recordNumber + 1; + if (nextState.sessionsMode == 0) { + nextState.sessionsMode = 1; + } + + bool foundSession = false; + for (auto &session : nextState.sessions) { + if (session.sessionType == kSessionTypeHomeserver && session.sessionName == gHomeserverValue) { + foundSession = true; + memcpy(session.sessionPubKey32, homeserverPub, 32); + session.sessionVersion = 0; + break; + } + } + if (!foundSession) { + if (requireExisting) { + return failWithDiag("Homeserver session is missing in PDA"); + } + ShinePdaSessionRecord rec; + rec.sessionType = kSessionTypeHomeserver; + rec.sessionVersion = 0; + rec.sessionName = gHomeserverValue; + memcpy(rec.sessionPubKey32, homeserverPub, 32); + nextState.sessions.push_back(rec); + } + + std::vector oldUnsigned = serializeUnsignedRecordState(currentState); + uint8_t prevHash32[32] = {}; + sha256calc(oldUnsigned.data(), oldUnsigned.size(), prevHash32); + memcpy(nextState.prevRecordHash32, prevHash32, 32); + diagDetails += String("prev_hash=") + bytesToHexString(prevHash32, 32) + "\n"; + + std::vector newUnsigned = serializeUnsignedRecordState(nextState); + uint8_t unsignedHash[32] = {}; + uint8_t rootSignature[64] = {}; + sha256calc(newUnsigned.data(), newUnsigned.size(), unsignedHash); + if (!signMessageEd25519(std::vector(unsignedHash, unsignedHash + 32), rootSec, rootSignature)) { + return failWithDiag("Failed to sign updated PDA record"); + } + diagDetails += String("unsigned_record_len=") + String((unsigned long)newUnsigned.size()) + "\n"; + diagDetails += String("unsigned_record_hash=") + bytesToHexString(unsignedHash, 32) + "\n"; + + std::vector lastBlockState = buildLastBlockStateBytes( + cleanLogin, + currentState.blockchainName, + currentState.lastBlockNumber, + currentState.lastBlockHash32, + currentState.usedBytes); + uint8_t lastBlockHash[32] = {}; + sha256calc(lastBlockState.data(), lastBlockState.size(), lastBlockHash); + diagDetails += String("last_block_hash=") + bytesToHexString(lastBlockHash, 32) + "\n"; + + std::vector updateData = buildUpdateInstructionData( + nextState, + nextState.recordNumber, + nextState.updatedAtMs, + prevHash32, + rootSignature); + std::vector edRootData = buildEd25519InstructionData(rootSignature, rootPub, unsignedHash); + std::vector edBchData = buildEd25519InstructionData(currentState.lastBlockSignature64, currentState.blockchainKey32, lastBlockHash); + + uint8_t recentBlockhash[32] = {}; + String recentBlockhash58; + if (!getLatestBlockhashBytes(recentBlockhash, recentBlockhash58, messageOut)) { + diagDetails += String("blockhash_error=") + messageOut + "\n"; + return failWithDiag(messageOut); + } + diagDetails += String("recent_blockhash=") + recentBlockhash58 + "\n"; + + std::vector message = buildUpdateLegacyMessage( + recentBlockhash, + devicePub, + userPda, + inflowVault, + economyConfig, + edRootData, + edBchData, + updateData); + uint8_t txSignature[64] = {}; + if (!signMessageEd25519(message, deviceSec, txSignature)) { + return failWithDiag("Failed to sign Solana transaction"); + } + String txBase64 = encodeTransactionBase64(txSignature, message); + String signatureB58 = bytesToBase58(txSignature, 64); + diagDetails += String("tx_signature=") + signatureB58 + "\n"; + + String payload; + if (!rpcCallSolana("sendTransaction", "[\"" + txBase64 + "\",{\"encoding\":\"base64\",\"preflightCommitment\":\"confirmed\"}]", payload)) { + diagDetails += "send_transaction_rpc_error=true\n"; + return failWithDiag("RPC did not accept transaction"); + } + if (rpcResponseHasError(payload)) { + if (!extractRpcErrorSummary(payload, messageOut)) { + messageOut = "RPC returned sendTransaction error"; + } + diagDetails += "send_transaction_payload<<\n"; + diagDetails += payload; + diagDetails += "\n>>send_transaction_payload\n"; + String simulated; + String simulatedPayload; + if (simulateTransactionPayload(txBase64, simulatedPayload)) { + extractRpcErrorSummary(simulatedPayload, simulated); + if (messageOut.isEmpty()) { + messageOut = simulated; + } else if (messageOut.indexOf(simulated) < 0) { + messageOut += " | simulate: " + simulated; + } + diagDetails += "simulate_payload<<\n"; + diagDetails += simulatedPayload; + diagDetails += "\n>>simulate_payload\n"; + } + return failWithDiag(messageOut); + } + if (!awaitTransactionConfirmation(signatureB58, messageOut)) { + diagDetails += String("confirmation_error=") + messageOut + "\n"; + return failWithDiag(messageOut); + } + + gUserPdaAddress = userPdaB58; + gRegistrationSignature = signatureB58; + saveAccountPrefs(); + refreshAccountPdaStatus(); + gHomeserverPdaResultSuccess = true; + gHomeserverPdaResultMessage = requireExisting ? "Homeserver key updated in PDA" : "Homeserver added to user PDA"; + gHomeserverPdaResultDetails = String("tx: ") + abbreviateValue(signatureB58, 12, 8); + messageOut = gHomeserverPdaResultMessage; + saveRegisterDiag("ok", messageOut, diagDetails); + printRegisterDiagToSerial(); + return true; +} + static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUserState &outState, String &errorOut) { outState = ShinePdaUserState{}; errorOut = ""; @@ -2014,8 +2537,6 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs uint8_t version = 0; uint8_t flags = 0; uint16_t recordLen = 0; - uint64_t ignoreU64 = 0; - uint32_t ignoreU32 = 0; if (!readU8(version) || !readU8(flags) || !readU16(recordLen)) { errorOut = "Bad PDA header"; return false; @@ -2025,11 +2546,14 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs return false; } offset = 9; - if (!readU64(ignoreU64) || !readU64(ignoreU64) || !readU32(ignoreU32)) { + if (!readU64(outState.createdAtMs) || !readU64(outState.updatedAtMs) || !readU32(outState.recordNumber)) { errorOut = "Bad PDA fixed fields"; return false; } - offset += 32; // prev hash + if (!readBytes(outState.prevRecordHash32, 32)) { + errorOut = "Bad PDA prev hash"; + return false; + } if (!readStringU8(outState.login)) { errorOut = "Bad PDA login"; return false; @@ -2095,7 +2619,15 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs return false; } if (j == 0) { + outState.blockchainType = blockchainType; + outState.blockchainName = blockchainName; memcpy(outState.blockchainKey32, blockchainKey, 32); + outState.paidLimitBytes = paidLimit; + outState.usedBytes = usedBytes; + outState.lastBlockNumber = lastBlockNumber; + memcpy(outState.lastBlockHash32, lastBlockHash, 32); + memcpy(outState.lastBlockSignature64, lastBlockSig, 64); + outState.arweaveTxId = arweaveTxId; } } continue; @@ -2108,22 +2640,24 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs } outState.isServer = isServer == 1; if (outState.isServer) { - uint8_t addressFormatType = 0; - uint8_t addressFormatVersion = 0; + outState.addressFormatType = 0; + outState.addressFormatVersion = 0; uint8_t syncCount = 0; - if (!readU8(addressFormatType) - || !readU8(addressFormatVersion) + if (!readU8(outState.addressFormatType) + || !readU8(outState.addressFormatVersion) || !readStringU8(outState.serverAddress) || !readU8(syncCount)) { errorOut = "Bad server address"; return false; } + outState.syncServers.clear(); for (uint8_t j = 0; j < syncCount; ++j) { - String ignoreSync; - if (!readStringU8(ignoreSync)) { + String syncServer; + if (!readStringU8(syncServer)) { errorOut = "Bad sync_servers"; return false; } + outState.syncServers.push_back(syncServer); } } continue; @@ -2134,12 +2668,14 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs errorOut = "Bad access servers"; return false; } + outState.accessServers.clear(); for (uint8_t j = 0; j < accessCount; ++j) { - String ignoreAccess; - if (!readStringU8(ignoreAccess)) { + String accessServer; + if (!readStringU8(accessServer)) { errorOut = "Bad access server item"; return false; } + outState.accessServers.push_back(accessServer); } continue; } @@ -2150,6 +2686,7 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs errorOut = "Bad sessions block"; return false; } + outState.sessionsMode = sessionsMode; outState.sessions.clear(); for (uint8_t j = 0; j < sessionsCount; ++j) { ShinePdaSessionRecord rec; @@ -2165,8 +2702,7 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs continue; } if (blockType == kBlockTypeTrustedState) { - uint8_t trustedCount = 0; - if (!readU8(trustedCount)) { + if (!readU8(outState.trustedCount)) { errorOut = "Bad trusted block"; return false; } @@ -2176,6 +2712,10 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs return false; } + if (offset + 64 > recordLen) { + errorOut = "Missing PDA signature"; + return false; + } outState.found = true; return true; } @@ -2233,6 +2773,10 @@ static bool readShineUserPda(const String &login, ShinePdaUserState &outState, S static void refreshAccountPdaStatus() { gAccountCheckPending = false; gShowRegisterAccountButton = false; + gShowHomeserverPdaActionButton = false; + gHomeserverPdaActionReason = ""; + gHomeserverPdaCanAdd = false; + gHomeserverPdaCanFix = false; if (gLoginValue.isEmpty() || !gSecretConfigured) { gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN; @@ -2314,6 +2858,15 @@ static void refreshAccountPdaStatus() { if (!mismatch.isEmpty()) { gAccountPdaStatus = ACCOUNT_PDA_MISMATCH; gAccountPdaStatusMessage = mismatch; + if (mismatch == "homeserver not in PDA") { + gShowHomeserverPdaActionButton = true; + gHomeserverPdaActionReason = mismatch; + gHomeserverPdaCanAdd = true; + } else if (mismatch == "homeserver key mismatch") { + gShowHomeserverPdaActionButton = true; + gHomeserverPdaActionReason = mismatch; + gHomeserverPdaCanFix = true; + } gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; clearShineSessionState(false); return; @@ -2811,6 +3364,15 @@ static void loadPrefs() { gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN; gAccountPdaStatusMessage = "Account not checked"; gShowRegisterAccountButton = false; + gShowHomeserverPdaActionButton = false; + gHomeserverPdaActionReason = ""; + gHomeserverPdaActionMessage = ""; + gHomeserverPdaActionDetail = ""; + gHomeserverPdaCanAdd = false; + gHomeserverPdaCanFix = false; + gHomeserverPdaResultMessage = ""; + gHomeserverPdaResultDetails = ""; + gHomeserverPdaResultSuccess = false; gShineStatusLine = "SHiNE: account not configured"; } @@ -3534,6 +4096,32 @@ static void actionButtonCb(lv_event_t *event) { case ACTION_REGISTER_ACCOUNT_EXECUTE: executeRegisterAccountFlow("manual", true); break; + case ACTION_HOMESERVER_PDA_ACTION: + prepareHomeserverPdaActionScreen(); + showScreen(SCREEN_HOMESERVER_PDA_CONFIRM); + break; + case ACTION_HOMESERVER_PDA_ADD_EXECUTE: { + gRegisterTriggerSource = "manual-homeserver-add"; + String updateMessage; + if (!updateHomeserverSessionOnSolana(false, updateMessage)) { + gHomeserverPdaResultSuccess = false; + gHomeserverPdaResultMessage = updateMessage; + gHomeserverPdaResultDetails = ""; + } + showScreen(SCREEN_HOMESERVER_PDA_RESULT); + break; + } + case ACTION_HOMESERVER_PDA_FIX_EXECUTE: { + gRegisterTriggerSource = "manual-homeserver-fix"; + String updateMessage; + if (!updateHomeserverSessionOnSolana(true, updateMessage)) { + gHomeserverPdaResultSuccess = false; + gHomeserverPdaResultMessage = updateMessage; + gHomeserverPdaResultDetails = ""; + } + showScreen(SCREEN_HOMESERVER_PDA_RESULT); + break; + } case ACTION_OPEN_WIFI: gWifiViewMode = WIFI_VIEW_OVERVIEW; showScreen(SCREEN_WIFI); @@ -3800,6 +4388,11 @@ static void drawHome() { showMessageAt(shineHomeLine(), 322); if (gShowRegisterAccountButton) { makeButton("REGISTER ACCOUNT", 20, 360, 210, 78, 0x2A9D8F, ACTION_REGISTER_ACCOUNT, &lv_font_montserrat_20); + } else if (gShowHomeserverPdaActionButton) { + const char *label = gHomeserverPdaActionReason == "homeserver key mismatch" + ? "FIX HOMESERVER PASSWORD" + : "ADD HOMESERVER"; + makeButton(label, 20, 360, 210, 78, 0xC59B2A, ACTION_HOMESERVER_PDA_ACTION, &lv_font_montserrat_18); } makeButton("SETTINGS", 250, 360, 210, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24); makeVersionTag(); @@ -4111,6 +4704,43 @@ static void drawRegisterAccountResultScreen() { makeVersionTag(); } +static void drawHomeserverPdaConfirmScreen() { + setRootStyle(); + makeTitle("HOMESERVER PDA", 22, &lv_font_montserrat_24); + String topLine = gHomeserverPdaActionMessage.isEmpty() ? String("Homeserver action") : gHomeserverPdaActionMessage; + makeBody(topLine.c_str(), 96, 420); + if (!gHomeserverPdaActionDetail.isEmpty()) { + makeBody(gHomeserverPdaActionDetail.c_str(), 146, 420); + } + String loginLine = String("Login: ") + loginDisplayValue(); + String homeserverLine = String("Homeserver: ") + homeserverDisplayValue(); + makeBody(loginLine.c_str(), 196, 420); + makeBody(homeserverLine.c_str(), 236, 420); + if (gHomeserverPdaCanAdd) { + makeButton("ADD HOMESERVER", 22, 300, 436, 70, 0xC59B2A, ACTION_HOMESERVER_PDA_ADD_EXECUTE, &lv_font_montserrat_20); + } else if (gHomeserverPdaCanFix) { + makeButton("FIX HOMESERVER PASSWORD", 22, 300, 436, 70, 0xC59B2A, ACTION_HOMESERVER_PDA_FIX_EXECUTE, &lv_font_montserrat_16); + } else { + makeButton("UNAVAILABLE", 22, 300, 436, 70, 0x4A5560, ACTION_NONE, &lv_font_montserrat_20); + } + makeButton("BACK", 140, 386, 200, 54, 0x5A6570, ACTION_BACK_HOME, &lv_font_montserrat_20); + makeVersionTag(); +} + +static void drawHomeserverPdaResultScreen() { + setRootStyle(); + makeTitle("HOMESERVER RESULT", 22, &lv_font_montserrat_24); + String resultTopLine = gHomeserverPdaResultSuccess ? String("Homeserver PDA updated") : String("Homeserver update failed"); + makeBody(resultTopLine.c_str(), 96, 420); + makeBody(gHomeserverPdaResultMessage.c_str(), 146, 420); + if (!gHomeserverPdaResultDetails.isEmpty()) { + makeBody(gHomeserverPdaResultDetails.c_str(), 196, 420); + } + makeButton("BACK HOME", 22, 372, 200, 58, 0x5A6570, ACTION_BACK_HOME, &lv_font_montserrat_20); + makeButton("ACCOUNT", 258, 372, 200, 58, 0x2A6F97, ACTION_OPEN_ACCOUNT, &lv_font_montserrat_20); + makeVersionTag(); +} + static void drawKeyRow(const char *const *tokens, int count, lv_coord_t x, @@ -4277,6 +4907,12 @@ static void rebuildScreen() { case SCREEN_REGISTER_ACCOUNT_RESULT: drawRegisterAccountResultScreen(); break; + case SCREEN_HOMESERVER_PDA_CONFIRM: + drawHomeserverPdaConfirmScreen(); + break; + case SCREEN_HOMESERVER_PDA_RESULT: + drawHomeserverPdaResultScreen(); + break; } } @@ -4403,6 +5039,8 @@ static void handleSwipe(SwipeDirection swipe) { break; case SCREEN_REGISTER_ACCOUNT_CONFIRM: case SCREEN_REGISTER_ACCOUNT_RESULT: + case SCREEN_HOMESERVER_PDA_CONFIRM: + case SCREEN_HOMESERVER_PDA_RESULT: handleHomeSwipe(swipe); break; } @@ -4411,7 +5049,6 @@ static void handleSwipe(SwipeDirection swipe) { void setup() { Serial.begin(115200); sodium_init(); - gBootMillis = millis(); Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL); initPowerManagement(); @@ -4473,15 +5110,6 @@ void loop() { manageAccountPdaRefresh(); manageShineConnection(); - if (gAutoRegisterTestArmed && !gAutoRegisterTestStarted && millis() - gBootMillis >= 15000UL) { - gAutoRegisterTestStarted = true; - gAutoRegisterTestArmed = false; - Serial.println("AUTO_REGISTER_TEST_BEGIN"); - executeRegisterAccountFlow("auto-boot-15s", false); - gAutoRegisterTestFinished = true; - Serial.println("AUTO_REGISTER_TEST_END"); - } - static unsigned long lastHomeRefreshMs = 0; if (gCurrentScreen == SCREEN_HOME && !gTouchDown && millis() - lastHomeRefreshMs >= HOME_REFRESH_MS) { lastHomeRefreshMs = millis(); 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 58c1a72..348d4d4 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 @@ -162,10 +162,14 @@ Дополнительная большая кнопка: - `REGISTER ACCOUNT` +- либо жёлтая `ADD HOMESERVER` +- либо жёлтая `FIX HOMESERVER PASSWORD` Если регистрация уже сделана: -- вместо призыва к регистрации показывается статус `Homeserver активен`. +- если пользователь создан, но в `PDA` ещё нет сессии текущего homeserver, показывается жёлтая кнопка `ADD HOMESERVER`; +- если в `PDA` есть homeserver с тем же именем, но с другим ключом, показывается жёлтая кнопка `FIX HOMESERVER PASSWORD`; +- если и пользователь, и homeserver-сессия уже корректны, вместо призыва к регистрации показывается статус `Homeserver активен`. - две нижние кнопки внизу экрана не прилегают вплотную друг к другу, между ними есть небольшой зазор. ## Экран REGISTER_ACCOUNT_CONFIRM @@ -215,6 +219,49 @@ - при ошибке на экране показывается причина отказа; - если ошибку вернул `sendTransaction`, экран старается показать не только общий текст, но и детали `RPC`/preflight/simulate-логов. +## Экран HOMESERVER_PDA_CONFIRM + +Показывает, что именно не так с homeserver-секцией уже существующей пользовательской `PDA`. + +Отображается: + +- причина (`homeserver` отсутствует в `PDA` или ключ не совпадает с локальным секретом); +- `login`; +- имя `homeserver`; +- короткое пояснение, что именно будет сделано. + +Кнопки: + +- `ADD HOMESERVER` или `FIX HOMESERVER PASSWORD` +- `BACK` + +Поведение: + +- если `Wi-Fi` не подключён, действие недоступно; +- экран не создаёт нового пользователя, а запускает `update_user_pda`; +- при `ADD HOMESERVER` в блок `sessions` добавляется запись `session_type=100`; +- при `FIX HOMESERVER PASSWORD` обновляется публичный ключ уже существующей записи `homeserver`. + +## Экран HOMESERVER_PDA_RESULT + +Показывает результат обновления `sessions` в пользовательской `PDA`. + +Отображается: + +- успех или ошибка; +- короткое сообщение; +- при успехе краткий `tx signature`. + +Кнопки: + +- `BACK HOME` +- `ACCOUNT` + +Поведение: + +- при ошибке текст ошибки сохраняется в ту же USB/NVS-диагностику, что и регистрация; +- после успешного обновления выполняется повторная проверка `PDA`, и основной экран должен перейти в состояние `ok`. + ## Экран STATUS Показывает сводку: diff --git a/VERSION.properties b/VERSION.properties index 5b5e63c..516881a 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.175 -server.version=1.2.164 +client.version=1.2.176 +server.version=1.2.165