ESP32: регистрация по кнопке и зазор между кнопками
This commit is contained in:
parent
d4a0185507
commit
b83543d018
@ -12,9 +12,9 @@
|
|||||||
6. Открыть `Аккаунт`, ввести логин, имя homeserver и нажать `Сгенерировать`; проверить, что появились секрет и адрес кошелька, а после перезагрузки они не исчезают.
|
6. Открыть `Аккаунт`, ввести логин, имя homeserver и нажать `Сгенерировать`; проверить, что появились секрет и адрес кошелька, а после перезагрузки они не исчезают.
|
||||||
7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка.
|
7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка.
|
||||||
8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению.
|
8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению.
|
||||||
9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной.
|
9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной; также убедиться, что между двумя нижними кнопками есть небольшой зазор.
|
||||||
10. Выполнить регистрацию и убедиться, что статус меняется на `Homeserver активен`, онлайн-статус становится активным, а на экране появляются краткие отпечатки `PDA/TX`.
|
10. Нажать кнопку регистрации и убедиться, что она запускает on-chain flow сразу без отдельного confirm-экрана, а после завершения статус меняется на `Homeserver активен`, онлайн-статус становится активным, и на экране аккаунта появляются краткие отпечатки `PDA/TX`.
|
||||||
11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана и соответствует `device`-адресу устройства.
|
11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства.
|
||||||
12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус.
|
12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус.
|
||||||
13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте.
|
13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте.
|
||||||
14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются.
|
14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# ESP32 nav minimal test
|
# 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`;
|
||||||
- на `HOME` видны реальное значение homeserver или `homeserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка баланса, строка `SHiNE: ...`, кнопка `SETTINGS` уменьшенной ширины у правого края и нижняя подпись `SHiNE homeserver (v.0.18)`;
|
- на `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`;
|
- кнопка `SETTINGS` открывает `SETTINGS_MENU`;
|
||||||
- свайп влево на `HOME` открывает `SETTINGS_MENU`;
|
- свайп влево на `HOME` открывает `SETTINGS_MENU`;
|
||||||
- если пользователь не найден в Solana PDA, слева снизу появляется `REGISTER ACCOUNT`;
|
- если пользователь не найден в Solana PDA, слева снизу появляется `REGISTER ACCOUNT`;
|
||||||
- `REGISTER ACCOUNT` открывает экран-заглушку;
|
- `REGISTER ACCOUNT` открывает экран-заглушку только в старой тестовой версии;
|
||||||
- в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`;
|
- в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`;
|
||||||
- обе видимые карточки меню одного цвета;
|
- обе видимые карточки меню одного цвета;
|
||||||
- свайп вверх показывает `Server` и `Account`;
|
- свайп вверх показывает `Server` и `Account`;
|
||||||
@ -101,4 +101,4 @@
|
|||||||
- если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`.
|
- если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`.
|
||||||
- Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32.
|
- Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32.
|
||||||
- Дополнительно ожидается: `HOME` уже показывает реальный Solana/WS-статус homeserver, а отсутствие пользователя в Solana заметно сразу без перехода в настройки.
|
- Дополнительно ожидается: `HOME` уже показывает реальный Solana/WS-статус homeserver, а отсутствие пользователя в Solana заметно сразу без перехода в настройки.
|
||||||
- Статус: pending
|
- Статус: done
|
||||||
|
|||||||
@ -52,6 +52,15 @@
|
|||||||
|
|
||||||
static const char *kShineUsersProgramId = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm";
|
static const char *kShineUsersProgramId = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm";
|
||||||
static const char *kShineUsersUserPdaSeedPrefix = "user_login=";
|
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 char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
|
||||||
static const uint8_t kBlockTypeRootKey = 1;
|
static const uint8_t kBlockTypeRootKey = 1;
|
||||||
static const uint8_t kBlockTypeDeviceKey = 2;
|
static const uint8_t kBlockTypeDeviceKey = 2;
|
||||||
@ -234,6 +243,8 @@ static String gAccountPdaStatusMessage = "Account not checked";
|
|||||||
static bool gAccountCheckPending = true;
|
static bool gAccountCheckPending = true;
|
||||||
static unsigned long gLastAccountCheckMs = 0;
|
static unsigned long gLastAccountCheckMs = 0;
|
||||||
static bool gShowRegisterAccountButton = false;
|
static bool gShowRegisterAccountButton = false;
|
||||||
|
static String gUserPdaAddress;
|
||||||
|
static String gRegistrationSignature;
|
||||||
static String gShineStatusLine = "SHiNE: account not configured";
|
static String gShineStatusLine = "SHiNE: account not configured";
|
||||||
static String gShineSessionId;
|
static String gShineSessionId;
|
||||||
static String gShineSessionKey;
|
static String gShineSessionKey;
|
||||||
@ -329,6 +340,51 @@ static String shineWsUrl();
|
|||||||
static String shineHomeLine();
|
static String shineHomeLine();
|
||||||
static String balanceHomeLine();
|
static String balanceHomeLine();
|
||||||
static uint64_t shineNowMs();
|
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 ¶msJson);
|
||||||
|
static bool rpcCallSolana(const char *method, const String ¶msJson, String &payloadOut);
|
||||||
|
static bool rpcResponseHasError(const String &payload);
|
||||||
|
static bool getLatestBlockhashBytes(uint8_t out[32], String &blockhashB58, String &messageOut);
|
||||||
|
static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &messageOut);
|
||||||
|
static std::vector<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 String buildSessionKeyStringFromPublicBase64(const String &pubB64);
|
||||||
static bool deriveKeypairFromSeed32(const uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]);
|
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]);
|
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;
|
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) {
|
static String jsonEscape(const String &value) {
|
||||||
String out;
|
String out;
|
||||||
out.reserve(value.length() + 8);
|
out.reserve(value.length() + 8);
|
||||||
@ -781,6 +844,8 @@ static void markAccountStateDirty() {
|
|||||||
gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN;
|
gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN;
|
||||||
gAccountPdaStatusMessage = "Account not checked";
|
gAccountPdaStatusMessage = "Account not checked";
|
||||||
gShowRegisterAccountButton = false;
|
gShowRegisterAccountButton = false;
|
||||||
|
gUserPdaAddress = "";
|
||||||
|
gRegistrationSignature = "";
|
||||||
clearShineSessionState(true);
|
clearShineSessionState(true);
|
||||||
gShineStatusLine = "SHiNE: account not configured";
|
gShineStatusLine = "SHiNE: account not configured";
|
||||||
}
|
}
|
||||||
@ -965,6 +1030,501 @@ static uint64_t shineNowMs() {
|
|||||||
return value > 0 ? (uint64_t)value : (uint64_t)millis();
|
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 ¶msJson) {
|
||||||
|
return "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"" + String(method) + "\",\"params\":" + paramsJson + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rpcCallSolana(const char *method, const String ¶msJson, String &payloadOut) {
|
||||||
|
int code = -1;
|
||||||
|
if (!httpPostJson(gSolanaRpcUrl, buildBaseRpcRequest(method, paramsJson), code, payloadOut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return code >= 200 && code < 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rpcResponseHasError(const String &payload) {
|
||||||
|
return payload.indexOf("\"error\"") >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<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) {
|
static bool parseShineUserPdaBytes(const std::vector<uint8_t> &bytes, ShinePdaUserState &outState, String &errorOut) {
|
||||||
outState = ShinePdaUserState{};
|
outState = ShinePdaUserState{};
|
||||||
errorOut = "";
|
errorOut = "";
|
||||||
@ -1817,6 +2377,8 @@ static void loadPrefs() {
|
|||||||
gShineSessionId = gPrefs.getString("shine_sess_id", "");
|
gShineSessionId = gPrefs.getString("shine_sess_id", "");
|
||||||
gShineSessionKey = gPrefs.getString("shine_sess_key", "");
|
gShineSessionKey = gPrefs.getString("shine_sess_key", "");
|
||||||
gShineStoragePwd = gPrefs.getString("shine_store_pwd", "");
|
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";
|
gBalanceStatusMessage = gDevicePubB58.isEmpty() ? "Balance: secret not set" : "Balance: tap to load";
|
||||||
gAccountCheckPending = true;
|
gAccountCheckPending = true;
|
||||||
gLastAccountCheckMs = 0;
|
gLastAccountCheckMs = 0;
|
||||||
@ -1853,6 +2415,16 @@ static void saveAccountPrefs() {
|
|||||||
gPrefs.putString("homeserver", gHomeserverValue);
|
gPrefs.putString("homeserver", gHomeserverValue);
|
||||||
gPrefs.putBool("secret_set", gSecretConfigured);
|
gPrefs.putBool("secret_set", gSecretConfigured);
|
||||||
gPrefs.putString("secret_b58", gSecretBase58);
|
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) {
|
if (gSecretConfigured) {
|
||||||
gPrefs.putBytes("secret_bytes", gSecretBytes, 32);
|
gPrefs.putBytes("secret_bytes", gSecretBytes, 32);
|
||||||
} else {
|
} else {
|
||||||
@ -2410,7 +2982,18 @@ static void actionButtonCb(lv_event_t *event) {
|
|||||||
showScreen(SCREEN_SETTINGS_MENU);
|
showScreen(SCREEN_SETTINGS_MENU);
|
||||||
break;
|
break;
|
||||||
case ACTION_REGISTER_ACCOUNT:
|
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;
|
break;
|
||||||
case ACTION_OPEN_WIFI:
|
case ACTION_OPEN_WIFI:
|
||||||
gWifiViewMode = WIFI_VIEW_OVERVIEW;
|
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);
|
makeButton(balanceHomeLine().c_str(), 22, 254, 436, 56, 0x355C7D, ACTION_REFRESH_BALANCE, &lv_font_montserrat_18);
|
||||||
showMessageAt(shineHomeLine(), 322);
|
showMessageAt(shineHomeLine(), 322);
|
||||||
if (gShowRegisterAccountButton) {
|
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();
|
makeVersionTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2791,6 +3374,12 @@ static void drawAccountScreen() {
|
|||||||
|
|
||||||
makeTitle("ACCOUNT", 18, &lv_font_montserrat_24);
|
makeTitle("ACCOUNT", 18, &lv_font_montserrat_24);
|
||||||
showMessageAt(gAccountStatusMessage, 56);
|
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 loginButton = String("Login (") + (gLoginValue.isEmpty() ? "not set" : gLoginValue) + ")";
|
||||||
String homeserverButton = String("Homeserver (") + (gHomeserverValue.isEmpty() ? "not set" : gHomeserverValue) + ")";
|
String homeserverButton = String("Homeserver (") + (gHomeserverValue.isEmpty() ? "not set" : gHomeserverValue) + ")";
|
||||||
@ -2939,8 +3528,8 @@ static void drawSecretGenerateCancelConfirmScreen() {
|
|||||||
static void drawRegisterAccountPlaceholderScreen() {
|
static void drawRegisterAccountPlaceholderScreen() {
|
||||||
setRootStyle();
|
setRootStyle();
|
||||||
makeTitle("REGISTER ACCOUNT", 22, &lv_font_montserrat_24);
|
makeTitle("REGISTER ACCOUNT", 22, &lv_font_montserrat_24);
|
||||||
makeBody("Registration flow is not implemented yet.", 112, 420);
|
makeBody("Registration now starts directly from the home screen button.", 112, 420);
|
||||||
makeBody("This button is shown when login is not found in Solana PDA.", 156, 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);
|
makeButton("BACK", 140, 360, 200, 72, 0x5A6570, ACTION_BACK_HOME, &lv_font_montserrat_22);
|
||||||
makeVersionTag();
|
makeVersionTag();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# SHiNE ESP32 Homeserver UI Nav Minimal Spec
|
# SHiNE ESP32 Homeserver UI Nav Minimal Spec
|
||||||
|
|
||||||
Минимальный навигационный прототип для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
Легаси-спецификация старого навигационного теста для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
||||||
Этот прототип был перенесён в основной скетч `../main-device/shine_homeserver_main/`, а старое имя `lvgl-nav-minimal-test` осталось только как историческая ссылка.
|
Актуальный основной скетч теперь находится в `../main-device/shine_homeserver_main/`; этот документ оставлен только как историческая справка по старому тестовому UI.
|
||||||
|
|
||||||
## Цель
|
## Цель
|
||||||
|
|
||||||
@ -64,6 +64,9 @@
|
|||||||
- кнопка `SETTINGS` -> `SETTINGS_MENU`;
|
- кнопка `SETTINGS` -> `SETTINGS_MENU`;
|
||||||
- свайп влево -> `SETTINGS_MENU`.
|
- свайп влево -> `SETTINGS_MENU`.
|
||||||
|
|
||||||
|
Примечание:
|
||||||
|
- поведение `REGISTER ACCOUNT -> REGISTER_ACCOUNT_PLACEHOLDER` относится к старой тестовой версии и не является актуальным для основного скетча.
|
||||||
|
|
||||||
Фоновая логика:
|
Фоновая логика:
|
||||||
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
|
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
|
||||||
- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
|
- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
- реальная проверка доступности `API`, `RPC` и `WS`-адресов;
|
- реальная проверка доступности `API`, `RPC` и `WS`-адресов;
|
||||||
- реальное чтение баланса кошелька из `Solana RPC`;
|
- реальное чтение баланса кошелька из `Solana RPC`;
|
||||||
- проверка обязательных условий перед регистрацией;
|
- проверка обязательных условий перед регистрацией;
|
||||||
- живая on-chain регистрация серверного `user_pda` в `shine_users` через `device key` устройства;
|
- живая on-chain регистрация серверного `user_pda` в `shine_users` по нажатию кнопки на главном экране;
|
||||||
- прототип входящих запросов с подтверждением и отклонением;
|
- прототип входящих запросов с подтверждением и отклонением;
|
||||||
- PIN-блокировка (в текущей временной сборке вход по PIN отключён, устройство открывает `HOME` сразу после старта);
|
- PIN-блокировка (в текущей временной сборке вход по PIN отключён, устройство открывает `HOME` сразу после старта);
|
||||||
- базовые настройки, статус и главный экран;
|
- базовые настройки, статус и главный экран;
|
||||||
@ -159,11 +159,12 @@
|
|||||||
|
|
||||||
Дополнительная большая кнопка:
|
Дополнительная большая кнопка:
|
||||||
|
|
||||||
- `Зарегистрировать`
|
- `REGISTER ACCOUNT`
|
||||||
|
|
||||||
Если регистрация уже сделана:
|
Если регистрация уже сделана:
|
||||||
|
|
||||||
- вместо призыва к регистрации показывается статус `Homeserver активен`.
|
- вместо призыва к регистрации показывается статус `Homeserver активен`.
|
||||||
|
- две нижние кнопки внизу экрана не прилегают вплотную друг к другу, между ними есть небольшой зазор.
|
||||||
|
|
||||||
## Экран STATUS
|
## Экран STATUS
|
||||||
|
|
||||||
@ -277,6 +278,7 @@
|
|||||||
- `Сгенерировать` создаёт новый `master secret` и пересчитывает из него `device`-кошелёк;
|
- `Сгенерировать` создаёт новый `master secret` и пересчитывает из него `device`-кошелёк;
|
||||||
- `Очистить` удаляет секрет, адрес кошелька, `PDA`, `tx`, регистрацию и онлайн-статус;
|
- `Очистить` удаляет секрет, адрес кошелька, `PDA`, `tx`, регистрацию и онлайн-статус;
|
||||||
- логин приводится к нижнему регистру и trim.
|
- логин приводится к нижнему регистру и trim.
|
||||||
|
- после успешной регистрации на экране сохраняются и отображаются краткие отпечатки `PDA` и `TX`.
|
||||||
|
|
||||||
## Экран WALLET
|
## Экран WALLET
|
||||||
|
|
||||||
@ -438,8 +440,8 @@ QR должен быть сканируемым, а не декоративны
|
|||||||
10. открыть `Кошелёк`;
|
10. открыть `Кошелёк`;
|
||||||
11. при необходимости пополнить баланс;
|
11. при необходимости пополнить баланс;
|
||||||
12. вернуться на `HOME`;
|
12. вернуться на `HOME`;
|
||||||
13. нажать `Зарегистрировать`;
|
13. нажать `REGISTER ACCOUNT`;
|
||||||
14. после подтверждения увидеть статус `Homeserver активен`.
|
14. после завершения увидеть статус `Homeserver активен`.
|
||||||
|
|
||||||
Примечание:
|
Примечание:
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.163
|
client.version=1.2.164
|
||||||
server.version=1.2.152
|
server.version=1.2.153
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user