ESP32: добавить flow обновления homeserver в user PDA

This commit is contained in:
AidarKC 2026-06-13 09:07:49 +04:00
parent 19fd5611b2
commit a1da814030
5 changed files with 746 additions and 42 deletions

View File

@ -14,14 +14,19 @@
8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению. 8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению.
9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной; также убедиться, что между двумя нижними кнопками есть небольшой зазор. 9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной; также убедиться, что между двумя нижними кнопками есть небольшой зазор.
10. Нажать кнопку регистрации и убедиться, что открывается отдельный экран проверки, где ещё раз видно `login`, статус свободного `PDA`, баланс, `homeserver1` с пометкой о стандартном значении и сообщение, если `Wi-Fi` не подключён. 10. Нажать кнопку регистрации и убедиться, что открывается отдельный экран проверки, где ещё раз видно `login`, статус свободного `PDA`, баланс, `homeserver1` с пометкой о стандартном значении и сообщение, если `Wi-Fi` не подключён.
11. На экране проверки нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ` и убедиться, что после этого появляется отдельный экран результата с успехом либо подробной ошибкой. 11. На экране проверки нажать `REGISTER IN SHINE` и убедиться, что после этого появляется отдельный экран результата с успехом либо подробной ошибкой.
12. После успешной регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства, а `tx signature` тоже сохранён. 12. После успешной регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства, а `tx signature` тоже сохранён.
13. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус. 13. После успешной регистрации вернуться на `HOME` и проверить новую жёлтую кнопку:
14. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте. - если в `PDA` ещё нет текущего homeserver, должна появиться `ADD HOMESERVER`;
15. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются. - если ключ homeserver в `PDA` не совпадает с локальным секретом, должна появиться `FIX HOMESERVER PASSWORD`.
14. Нажать жёлтую кнопку и убедиться, что открывается отдельный экран пояснения, а затем экран результата обновления `PDA`.
15. После успешного `ADD/FIX HOMESERVER` проверить, что основной экран больше не показывает `homeserver not in PDA` или `homeserver key mismatch`, а `SHiNE` может перейти к авторизации.
16. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус.
17. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте.
18. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются.
- ожидаемый результат: - ожидаемый результат:
новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации homeserver. новый `ESP32`-скетч стабильно запускается, показывает читаемый англоязычный интерфейс, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI, позволяет вручную пройти on-chain регистрацию пользователя и затем отдельным действием записать/исправить homeserver-сессию в `shine_users`.
- статус: - статус:
pending pending

View File

@ -0,0 +1,24 @@
# ESP32: добавление и исправление homeserver в user PDA
- краткое описание фичи:
в основном ESP32-скетче добавлен отдельный flow после регистрации пользователя: если homeserver-сессия отсутствует в `shine_users` PDA или её ключ не совпадает с локальным секретом, на главном экране показывается жёлтая кнопка `ADD HOMESERVER` или `FIX HOMESERVER PASSWORD`. Действие открывает отдельный экран объяснения, затем отправляет `update_user_pda` и показывает экран результата.
- что именно проверять:
1. Зарегистрировать пользователя или взять уже существующего пользователя без корректной homeserver-сессии.
2. На `HOME` убедиться, что вместо кнопки регистрации появилась жёлтая кнопка:
`ADD HOMESERVER` либо `FIX HOMESERVER PASSWORD`.
3. Нажать её и проверить, что открывается отдельный экран с пояснением причины и кнопкой действия.
4. Выполнить действие и дождаться экрана результата.
5. После успеха проверить, что:
- `homeserver not in PDA` или `homeserver key mismatch` исчезли;
- в USB-диагностике есть успешный `tx_signature`;
- `SHiNE` может перейти к авторизации на сервере.
6. Если действие завершается ошибкой, проверить, что:
- текст ошибки показан на экране результата;
- команда `last_error` по USB возвращает полный сохранённый payload.
- ожидаемый результат:
устройство после обычной регистрации пользователя способно отдельной транзакцией добавить или исправить homeserver-сессию в `shine_users` PDA, а ошибки этого шага сохраняются в ту же USB/NVS-диагностику.
- статус:
pending

View File

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

View File

@ -162,10 +162,14 @@
Дополнительная большая кнопка: Дополнительная большая кнопка:
- `REGISTER ACCOUNT` - `REGISTER ACCOUNT`
- либо жёлтая `ADD HOMESERVER`
- либо жёлтая `FIX HOMESERVER PASSWORD`
Если регистрация уже сделана: Если регистрация уже сделана:
- вместо призыва к регистрации показывается статус `Homeserver активен`. - если пользователь создан, но в `PDA` ещё нет сессии текущего homeserver, показывается жёлтая кнопка `ADD HOMESERVER`;
- если в `PDA` есть homeserver с тем же именем, но с другим ключом, показывается жёлтая кнопка `FIX HOMESERVER PASSWORD`;
- если и пользователь, и homeserver-сессия уже корректны, вместо призыва к регистрации показывается статус `Homeserver активен`.
- две нижние кнопки внизу экрана не прилегают вплотную друг к другу, между ними есть небольшой зазор. - две нижние кнопки внизу экрана не прилегают вплотную друг к другу, между ними есть небольшой зазор.
## Экран REGISTER_ACCOUNT_CONFIRM ## Экран REGISTER_ACCOUNT_CONFIRM
@ -215,6 +219,49 @@
- при ошибке на экране показывается причина отказа; - при ошибке на экране показывается причина отказа;
- если ошибку вернул `sendTransaction`, экран старается показать не только общий текст, но и детали `RPC`/preflight/simulate-логов. - если ошибку вернул `sendTransaction`, экран старается показать не только общий текст, но и детали `RPC`/preflight/simulate-логов.
## Экран HOMESERVER_PDA_CONFIRM
Показывает, что именно не так с homeserver-секцией уже существующей пользовательской `PDA`.
Отображается:
- причина (`homeserver` отсутствует в `PDA` или ключ не совпадает с локальным секретом);
- `login`;
- имя `homeserver`;
- короткое пояснение, что именно будет сделано.
Кнопки:
- `ADD HOMESERVER` или `FIX HOMESERVER PASSWORD`
- `BACK`
Поведение:
- если `Wi-Fi` не подключён, действие недоступно;
- экран не создаёт нового пользователя, а запускает `update_user_pda`;
- при `ADD HOMESERVER` в блок `sessions` добавляется запись `session_type=100`;
- при `FIX HOMESERVER PASSWORD` обновляется публичный ключ уже существующей записи `homeserver`.
## Экран HOMESERVER_PDA_RESULT
Показывает результат обновления `sessions` в пользовательской `PDA`.
Отображается:
- успех или ошибка;
- короткое сообщение;
- при успехе краткий `tx signature`.
Кнопки:
- `BACK HOME`
- `ACCOUNT`
Поведение:
- при ошибке текст ошибки сохраняется в ту же USB/NVS-диагностику, что и регистрация;
- после успешного обновления выполняется повторная проверка `PDA`, и основной экран должен перейти в состояние `ok`.
## Экран STATUS ## Экран STATUS
Показывает сводку: Показывает сводку:

View File

@ -1,2 +1,2 @@
client.version=1.2.175 client.version=1.2.176
server.version=1.2.164 server.version=1.2.165