diff --git a/Dev_Docs/Pending_Features/2026-06-12_1335_esp32_usb_диагностика_регистрации.md b/Dev_Docs/Pending_Features/2026-06-12_1335_esp32_usb_диагностика_регистрации.md new file mode 100644 index 0000000..c40edab --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-12_1335_esp32_usb_диагностика_регистрации.md @@ -0,0 +1,45 @@ +# ESP32 USB-диагностика регистрации Solana + +- статус: `pending` + +## Что сделано + +- В основной скетч `shine_homeserver_main` добавено сохранение последней диагностики регистрации Solana в `Preferences`. +- Добавлены USB-команды через `Serial`: + - `last_error` + - `last_diag` + - `reg_diag` + - `clear_error` + - `clear_diag` + - `help` +- Перед отправкой `create_user_pda` добавлена RPC-проверка `users_economy_config_pda`. +- Стартовый `paid_limit_bytes` в подписываемой записи теперь берётся из on-chain `users_economy_config`, а не из хардкода. + +## Что проверять + +1. Подключить устройство по USB. +2. Открыть последовательный порт `115200`. +3. Отправить команду `last_error`. +4. Убедиться, что устройство печатает сохранённую диагностику между маркерами: + - `LAST_REGISTER_DIAG_BEGIN` + - `LAST_REGISTER_DIAG_END` +5. Запустить регистрацию с устройства и дождаться ошибки или успеха. +6. Снова отправить `last_error`. +7. Проверить, что в диагностике есть: + - `status` + - `summary` + - `rpc` + - `user_pda` + - `users_economy_config_pda` + - `inflow_vault_pda` + - `root_pub` + - `blockchain_pub` + - `device_pub` +8. При ошибке `0x3` проверить, что текст стал конкретнее и помогает понять, какая PDA или RPC-конфигурация не совпала. + +## Ожидаемый результат + +- Последняя ошибка регистрации читается по USB без просмотра экрана. +- После неудачной регистрации на устройстве остаётся подробная диагностическая запись. +- Если `users_economy_config_pda` отсутствует или принадлежит не той программе, это явно видно до отправки транзакции. + 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 6232505..2355b1a 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 @@ -256,6 +256,11 @@ static bool gRegisterConfirmCanSubmit = false; static String gRegisterResultMessage; static String gRegisterResultDetails; static bool gRegisterResultSuccess = false; +static String gLastRegisterDiagStatus = "none"; +static String gLastRegisterDiagSummary; +static String gLastRegisterDiagDetails; +static String gLastRegisterDiagTime; +static String gSerialCommandBuffer; static String gShineSessionId; static String gShineSessionKey; static String gShineStoragePwd; @@ -362,6 +367,12 @@ static bool rpcCallSolana(const char *method, const String ¶msJson, String & 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 bool loadUsersEconomyConfigState(const String &economyConfigAddress, + uint64_t ®istrationFeeLamportsOut, + uint64_t &lamportsPerLimitStepOut, + uint64_t &startBonusLimitOut, + String &messageOut); +static bool loadAccountOwner(const String &address, String &ownerOut, bool &existsOut, String &messageOut); static bool extractRpcErrorSummary(const String &payload, String &messageOut); static String compactRpcLogs(const String &payload, int maxLines = 3); static bool simulateTransactionForError(const String &txBase64, String &messageOut); @@ -374,6 +385,7 @@ static std::vector buildUnsignedCreateRecord( const uint8_t devicePub[32], const uint8_t blockchainPub[32], const uint8_t lastBlockSignature[64], + uint64_t paidLimitBytes, uint64_t createdAtMs); static std::vector buildCreateInstructionData( const String &login, @@ -399,6 +411,10 @@ static bool signMessageEd25519(const std::vector &message, const uint8_ 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 saveRegisterDiag(const String &status, const String &summary, const String &details); +static void printRegisterDiagToSerial(); +static void clearRegisterDiag(); +static void handleUsbSerialCommands(); static void prepareRegisterAccountScreen(); static String buildSessionKeyStringFromPublicBase64(const String &pubB64); static bool deriveKeypairFromSeed32(const uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]); @@ -1140,6 +1156,7 @@ static std::vector buildUnsignedCreateRecord( const uint8_t devicePub[32], const uint8_t blockchainPub[32], const uint8_t lastBlockSignature[64], + uint64_t paidLimitBytes, uint64_t createdAtMs) { std::vector out; out.reserve(512); @@ -1169,7 +1186,7 @@ static std::vector buildUnsignedCreateRecord( out.push_back(1); pushStrU8(out, blockchainName); pushFixed(out, blockchainPub, 32); - pushU64LE(out, 100000); + pushU64LE(out, paidLimitBytes); pushU64LE(out, 0); pushU32LE(out, 0); out.insert(out.end(), 32, 0); @@ -1306,6 +1323,82 @@ static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &me return false; } +static bool loadAccountOwner(const String &address, String &ownerOut, bool &existsOut, String &messageOut) { + ownerOut = ""; + existsOut = false; + String payload; + if (!rpcCallSolana("getAccountInfo", "[\"" + address + "\",{\"encoding\":\"base64\",\"commitment\":\"confirmed\"}]", payload)) { + messageOut = "Failed to read account info"; + return false; + } + if (payload.indexOf("\"value\":null") >= 0) { + existsOut = false; + return true; + } + existsOut = true; + if (!jsonStringField(payload, "owner", ownerOut) || ownerOut.isEmpty()) { + messageOut = "Account owner missing in RPC response"; + return false; + } + return true; +} + +static bool loadUsersEconomyConfigState(const String &economyConfigAddress, + uint64_t ®istrationFeeLamportsOut, + uint64_t &lamportsPerLimitStepOut, + uint64_t &startBonusLimitOut, + String &messageOut) { + registrationFeeLamportsOut = 0; + lamportsPerLimitStepOut = 0; + startBonusLimitOut = 0; + + String payload; + if (!rpcCallSolana("getAccountInfo", "[\"" + economyConfigAddress + "\",{\"encoding\":\"base64\",\"commitment\":\"confirmed\"}]", payload)) { + messageOut = "Failed to read users economy config"; + return false; + } + if (payload.indexOf("\"value\":null") >= 0) { + messageOut = "Users economy config is missing on this Solana RPC"; + return false; + } + + String owner; + if (!jsonStringField(payload, "owner", owner) || owner.isEmpty()) { + messageOut = "Users economy config owner is missing"; + return false; + } + if (owner != String(kShineUsersProgramId)) { + messageOut = String("Users economy config owner mismatch: ") + owner; + return false; + } + + String dataB64; + if (!jsonStringField(payload, "data", dataB64) || dataB64.isEmpty()) { + messageOut = "Users economy config base64 is missing"; + return false; + } + + std::vector raw; + if (!base64DecodeStd(dataB64, raw)) { + messageOut = "Users economy config base64 decode failed"; + return false; + } + if (raw.size() < 25) { + messageOut = "Users economy config is too short"; + return false; + } + + registrationFeeLamportsOut = 0; + lamportsPerLimitStepOut = 0; + startBonusLimitOut = 0; + for (int i = 0; i < 8; ++i) { + registrationFeeLamportsOut |= ((uint64_t)raw[1 + i]) << (8 * i); + lamportsPerLimitStepOut |= ((uint64_t)raw[9 + i]) << (8 * i); + startBonusLimitOut |= ((uint64_t)raw[17 + i]) << (8 * i); + } + return true; +} + static String compactRpcLogs(const String &payload, int maxLines) { String out; int pos = payload.indexOf("\"logs\""); @@ -1490,22 +1583,33 @@ static bool awaitTransactionConfirmation(const String &signatureB58, String &mes static bool registerHomeserverOnSolana(String &messageOut) { messageOut = ""; - String cleanLogin = normalizeLoginValue(gLoginValue); - if (cleanLogin.isEmpty()) { - messageOut = "Login is not set"; + String diagDetails; + auto failWithDiag = [&](const String &summary) -> bool { + messageOut = summary; + saveRegisterDiag("error", summary, diagDetails); + printRegisterDiagToSerial(); return false; + }; + + String cleanLogin = normalizeLoginValue(gLoginValue); + diagDetails += String("login=") + cleanLogin + "\n"; + diagDetails += String("rpc=") + gSolanaRpcUrl + "\n"; + diagDetails += String("shine_server=") + gShineServerUrl + "\n"; + diagDetails += String("homeserver=") + gHomeserverValue + "\n"; + + if (cleanLogin.isEmpty()) { + return failWithDiag("Login is not set"); } if (!gSecretConfigured || gRootPrivB58.isEmpty() || gBlockchainPrivB58.isEmpty() || gDevicePrivB58.isEmpty()) { - messageOut = "Secret is not ready"; - return false; + return failWithDiag("Secret is not ready"); } if (WiFi.status() != WL_CONNECTED) { - messageOut = "Connect Wi-Fi first"; - return false; + diagDetails += "wifi=disconnected\n"; + return failWithDiag("Connect Wi-Fi first"); } + diagDetails += String("wifi=connected:") + WiFi.localIP().toString() + "\n"; if (gSolanaRpcUrl.isEmpty()) { - messageOut = "Set Solana RPC first"; - return false; + return failWithDiag("Set Solana RPC first"); } String existingPda; @@ -1520,11 +1624,13 @@ static bool registerHomeserverOnSolana(String &messageOut) { gAccountStatusMessage = "User is already registered"; gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered"; refreshAccountPdaStatus(); + diagDetails += String("user_pda=") + existingPda + "\n"; + saveRegisterDiag("ok", "User is already registered", diagDetails); return true; } if (pdaCheckMessage == "Failed to derive user PDA" || pdaCheckMessage == "Failed to check PDA" || pdaCheckMessage == "Unexpected getAccountInfo response") { - messageOut = pdaCheckMessage; - return false; + diagDetails += String("user_pda_check=") + pdaCheckMessage + "\n"; + return failWithDiag(pdaCheckMessage); } uint8_t userPda[32]; @@ -1540,10 +1646,44 @@ static bool registerHomeserverOnSolana(String &messageOut) { !findProgramAddress({ std::vector((const uint8_t *)kPaymentsInflowSeed, (const uint8_t *)kPaymentsInflowSeed + strlen(kPaymentsInflowSeed)) }, kShinePaymentsProgramId, inflowVault)) { - messageOut = "Failed to derive required PDAs"; - return false; + return failWithDiag("Failed to derive required PDAs"); } + String userPdaB58 = bytesToBase58(userPda, 32); + String economyConfigB58 = bytesToBase58(economyConfig, 32); + String inflowVaultB58 = bytesToBase58(inflowVault, 32); + diagDetails += String("user_pda=") + userPdaB58 + "\n"; + diagDetails += String("users_economy_config_pda=") + economyConfigB58 + "\n"; + diagDetails += String("inflow_vault_pda=") + inflowVaultB58 + "\n"; + + String accountInfoMessage; + String ownerValue; + bool ownerExists = false; + if (!loadAccountOwner(userPdaB58, ownerValue, ownerExists, accountInfoMessage)) { + diagDetails += String("user_pda_rpc_error=") + accountInfoMessage + "\n"; + return failWithDiag(accountInfoMessage); + } + diagDetails += String("user_pda_exists=") + (ownerExists ? "true" : "false") + "\n"; + if (ownerExists) { + diagDetails += String("user_pda_owner=") + ownerValue + "\n"; + return failWithDiag("User PDA already exists on RPC"); + } + + uint64_t registrationFeeLamports = 0; + uint64_t lamportsPerLimitStep = 0; + uint64_t startBonusLimit = 0; + if (!loadUsersEconomyConfigState(economyConfigB58, + registrationFeeLamports, + lamportsPerLimitStep, + startBonusLimit, + accountInfoMessage)) { + diagDetails += String("users_economy_config_error=") + accountInfoMessage + "\n"; + return failWithDiag(accountInfoMessage); + } + diagDetails += String("registration_fee_lamports=") + String((unsigned long long)registrationFeeLamports) + "\n"; + diagDetails += String("lamports_per_limit_step=") + String((unsigned long long)lamportsPerLimitStep) + "\n"; + diagDetails += String("start_bonus_limit=") + String((unsigned long long)startBonusLimit) + "\n"; + uint8_t rootSeed[32] = {}; uint8_t rootPub[32] = {}; uint8_t rootSec[64] = {}; @@ -1556,9 +1696,11 @@ static bool registerHomeserverOnSolana(String &messageOut) { if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) || !deriveSeedKeypairFromBase58(gBlockchainPrivB58, blockchainSeed, blockchainPub, blockchainSec) || !deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) { - messageOut = "Failed to restore keys"; - return false; + return failWithDiag("Failed to restore keys"); } + diagDetails += String("root_pub=") + bytesToBase58(rootPub, 32) + "\n"; + diagDetails += String("blockchain_pub=") + bytesToBase58(blockchainPub, 32) + "\n"; + diagDetails += String("device_pub=") + bytesToBase58(devicePub, 32) + "\n"; String blockchainName = cleanLogin + "-001"; std::vector lastBlockState = buildLastBlockStateBytes(cleanLogin, blockchainName); @@ -1566,21 +1708,19 @@ static bool registerHomeserverOnSolana(String &messageOut) { uint8_t lastBlockSignature[64]; sha256calc(lastBlockState.data(), lastBlockState.size(), lastBlockHash); if (!signMessageEd25519(std::vector(lastBlockHash, lastBlockHash + 32), blockchainSec, lastBlockSignature)) { - messageOut = "Failed to sign LastBlockState"; - return false; + return failWithDiag("Failed to sign LastBlockState"); } uint64_t createdAtMs = shineNowMs(); std::vector unsignedRecord = buildUnsignedCreateRecord( cleanLogin, blockchainName, gShineServerUrl, rootPub, devicePub, blockchainPub, - lastBlockSignature, createdAtMs); + lastBlockSignature, startBonusLimit, 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 = "Failed to sign PDA record"; - return false; + return failWithDiag("Failed to sign PDA record"); } std::vector createData = buildCreateInstructionData( @@ -1593,8 +1733,10 @@ static bool registerHomeserverOnSolana(String &messageOut) { uint8_t recentBlockhash[32]; String recentBlockhash58; if (!getLatestBlockhashBytes(recentBlockhash, recentBlockhash58, messageOut)) { - return false; + diagDetails += String("blockhash_error=") + messageOut + "\n"; + return failWithDiag(messageOut); } + diagDetails += String("recent_blockhash=") + recentBlockhash58 + "\n"; std::vector message = buildLegacyMessage( recentBlockhash, @@ -1607,16 +1749,16 @@ static bool registerHomeserverOnSolana(String &messageOut) { createData); uint8_t txSignature[64]; if (!signMessageEd25519(message, deviceSec, txSignature)) { - messageOut = "Failed to sign Solana transaction"; - return false; + 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)) { - messageOut = "RPC did not accept transaction"; - return false; + diagDetails += "send_transaction_rpc_error=true\n"; + return failWithDiag("RPC did not accept transaction"); } if (rpcResponseHasError(payload)) { if (!extractRpcErrorSummary(payload, messageOut)) { @@ -1630,13 +1772,15 @@ static bool registerHomeserverOnSolana(String &messageOut) { messageOut += " | simulate: " + simulated; } } - return false; + diagDetails += String("send_transaction_error=") + messageOut + "\n"; + return failWithDiag(messageOut); } if (!awaitTransactionConfirmation(signatureB58, messageOut)) { - return false; + diagDetails += String("confirmation_error=") + messageOut + "\n"; + return failWithDiag(messageOut); } - gUserPdaAddress = bytesToBase58(userPda, 32); + gUserPdaAddress = userPdaB58; gRegistrationSignature = signatureB58; gAccountStatusMessage = "Solana registration complete"; gAccountPdaStatus = ACCOUNT_PDA_OK; @@ -1646,6 +1790,8 @@ static bool registerHomeserverOnSolana(String &messageOut) { saveAccountPrefs(); refreshAccountPdaStatus(); messageOut = "Solana registration confirmed"; + saveRegisterDiag("ok", messageOut, diagDetails); + printRegisterDiagToSerial(); return true; } @@ -2573,6 +2719,10 @@ static void loadPrefs() { gShineStoragePwd = gPrefs.getString("shine_store_pwd", ""); gUserPdaAddress = gPrefs.getString("user_pda", ""); gRegistrationSignature = gPrefs.getString("registration_sig", ""); + gLastRegisterDiagStatus = gPrefs.getString("reg_diag_status", "none"); + gLastRegisterDiagSummary = gPrefs.getString("reg_diag_summary", ""); + gLastRegisterDiagDetails = gPrefs.getString("reg_diag_details", ""); + gLastRegisterDiagTime = gPrefs.getString("reg_diag_time", ""); gBalanceStatusMessage = gDevicePubB58.isEmpty() ? "Balance: secret not set" : "Balance: tap to load"; gAccountCheckPending = true; gLastAccountCheckMs = 0; @@ -2687,6 +2837,71 @@ static String wifiHomeSummary() { return String("Wi-Fi (") + gWifiSavedSsid + ") disconnected"; } +static void saveRegisterDiag(const String &status, const String &summary, const String &details) { + gLastRegisterDiagStatus = status; + gLastRegisterDiagSummary = summary.length() > 240 ? summary.substring(0, 240) : summary; + gLastRegisterDiagDetails = details.length() > 1800 ? details.substring(0, 1800) : details; + gLastRegisterDiagTime = String(shineNowMs()); + gPrefs.putString("reg_diag_status", gLastRegisterDiagStatus); + gPrefs.putString("reg_diag_summary", gLastRegisterDiagSummary); + gPrefs.putString("reg_diag_details", gLastRegisterDiagDetails); + gPrefs.putString("reg_diag_time", gLastRegisterDiagTime); +} + +static void clearRegisterDiag() { + gLastRegisterDiagStatus = "none"; + gLastRegisterDiagSummary = ""; + gLastRegisterDiagDetails = ""; + gLastRegisterDiagTime = ""; + gPrefs.remove("reg_diag_status"); + gPrefs.remove("reg_diag_summary"); + gPrefs.remove("reg_diag_details"); + gPrefs.remove("reg_diag_time"); +} + +static void printRegisterDiagToSerial() { + Serial.println("LAST_REGISTER_DIAG_BEGIN"); + Serial.println(String("status=") + gLastRegisterDiagStatus); + Serial.println(String("time_ms=") + gLastRegisterDiagTime); + Serial.println(String("summary=") + gLastRegisterDiagSummary); + Serial.println("details<<"); + Serial.println(gLastRegisterDiagDetails); + Serial.println(">>details"); + Serial.println("LAST_REGISTER_DIAG_END"); +} + +static void handleUsbSerialCommands() { + while (Serial.available() > 0) { + char ch = (char)Serial.read(); + if (ch == '\r') { + continue; + } + if (ch == '\n') { + String cmd = gSerialCommandBuffer; + gSerialCommandBuffer = ""; + cmd.trim(); + cmd.toLowerCase(); + if (cmd.isEmpty()) { + continue; + } + if (cmd == "last_error" || cmd == "last_diag" || cmd == "reg_diag") { + printRegisterDiagToSerial(); + } else if (cmd == "clear_error" || cmd == "clear_diag") { + clearRegisterDiag(); + Serial.println("register diag cleared"); + } else if (cmd == "help") { + Serial.println("commands: last_error, last_diag, reg_diag, clear_error, clear_diag, help"); + } else { + Serial.println(String("unknown command: ") + cmd); + } + continue; + } + if (gSerialCommandBuffer.length() < 120) { + gSerialCommandBuffer += ch; + } + } +} + static String loginDisplayValue() { return gLoginValue.isEmpty() ? "login not set" : gLoginValue; } @@ -4128,9 +4343,11 @@ void setup() { rebuildScreen(); Serial.println("Minimal nav test ready"); + Serial.println("USB diag commands: last_error, last_diag, reg_diag, clear_error, help"); } void loop() { + handleUsbSerialCommands(); lv_timer_handler(); manageWifiReconnect(); manageAccountPdaRefresh(); diff --git a/VERSION.properties b/VERSION.properties index 6f8bed5..e9b9027 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.168 -server.version=1.2.157 +client.version=1.2.169 +server.version=1.2.158