ESP32: регистрация по кнопке и зазор между кнопками

This commit is contained in:
AidarKC 2026-06-12 22:24:21 +04:00
parent d4a0185507
commit b83543d018
6 changed files with 613 additions and 19 deletions

View File

@ -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. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются.

View File

@ -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

View File

@ -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<uint8_t> &out);
static void pushU32LE(std::vector<uint8_t> &out, uint32_t value);
static void pushU64LE(std::vector<uint8_t> &out, uint64_t value);
static void pushStrU8(std::vector<uint8_t> &out, const String &value);
static void pushFixed(std::vector<uint8_t> &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 &paramsJson);
static bool rpcCallSolana(const char *method, const String &paramsJson, 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<uint8_t> buildLastBlockStateBytes(const String &login, const String &blockchainName);
static std::vector<uint8_t> 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<uint8_t> 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<uint8_t> buildEd25519InstructionData(const uint8_t signature[64], const uint8_t publicKey[32], const uint8_t messageHash[32]);
static std::vector<uint8_t> 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<uint8_t> &edRootData,
const std::vector<uint8_t> &edBchData,
const std::vector<uint8_t> &createData);
static bool signMessageEd25519(const std::vector<uint8_t> &message, const uint8_t secretKey[64], uint8_t signature[64]);
static String encodeTransactionBase64(const uint8_t signature[64], const std::vector<uint8_t> &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<uint8_t> &out) {
do {
uint8_t byte = value & 0x7F;
value >>= 7;
if (value) {
byte |= 0x80;
}
out.push_back(byte);
} while (value);
}
static void pushU32LE(std::vector<uint8_t> &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<uint8_t> &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<uint8_t> &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<uint8_t> &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 &paramsJson) {
return "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"" + String(method) + "\",\"params\":" + paramsJson + "}";
}
static bool rpcCallSolana(const char *method, const String &paramsJson, 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<uint8_t> buildLastBlockStateBytes(const String &login, const String &blockchainName) {
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<std::vector<uint8_t>> seeds = {
std::vector<uint8_t>((const uint8_t *)kUsersSeedPrefix, (const uint8_t *)kUsersSeedPrefix + strlen(kUsersSeedPrefix)),
std::vector<uint8_t>((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<uint8_t> 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<uint8_t> &edRootData,
const std::vector<uint8_t> &edBchData,
const std::vector<uint8_t> &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<std::vector<uint8_t>> 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<uint8_t> 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<uint8_t> &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<uint8_t> &message) {
std::vector<uint8_t> 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<uint8_t>((const uint8_t *)kUsersSeedPrefix, (const uint8_t *)kUsersSeedPrefix + strlen(kUsersSeedPrefix)),
std::vector<uint8_t>((const uint8_t *)cleanLogin.c_str(), (const uint8_t *)cleanLogin.c_str() + cleanLogin.length())
}, kShineUsersProgramId, userPda) ||
!findProgramAddress({
std::vector<uint8_t>((const uint8_t *)kUsersEconomyConfigSeed, (const uint8_t *)kUsersEconomyConfigSeed + strlen(kUsersEconomyConfigSeed))
}, kShineUsersProgramId, economyConfig) ||
!findProgramAddress({
std::vector<uint8_t>((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<uint8_t> lastBlockState = buildLastBlockStateBytes(cleanLogin, blockchainName);
uint8_t lastBlockHash[32];
uint8_t lastBlockSignature[64];
sha256calc(lastBlockState.data(), lastBlockState.size(), lastBlockHash);
if (!signMessageEd25519(std::vector<uint8_t>(lastBlockHash, lastBlockHash + 32), blockchainSec, lastBlockSignature)) {
messageOut = "Не удалось подписать LastBlockState";
return false;
}
uint64_t createdAtMs = shineNowMs();
std::vector<uint8_t> 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<uint8_t>(unsignedHash, unsignedHash + 32), rootSec, rootSignature)) {
messageOut = "Не удалось подписать PDA-запись";
return false;
}
std::vector<uint8_t> createData = buildCreateInstructionData(
cleanLogin, blockchainName, gShineServerUrl,
rootPub, devicePub, blockchainPub,
lastBlockSignature, rootSignature, createdAtMs);
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, rootPub, unsignedHash);
std::vector<uint8_t> edBchData = buildEd25519InstructionData(lastBlockSignature, blockchainPub, lastBlockHash);
uint8_t recentBlockhash[32];
String recentBlockhash58;
if (!getLatestBlockhashBytes(recentBlockhash, recentBlockhash58, messageOut)) {
return false;
}
std::vector<uint8_t> 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<uint8_t> &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();
}

View File

@ -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;

View File

@ -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 активен`.
Примечание:

View File

@ -1,2 +1,2 @@
client.version=1.2.163
server.version=1.2.152
client.version=1.2.164
server.version=1.2.153