ESP32: server login и NTP для регистрации

This commit is contained in:
AidarKC 2026-06-23 18:11:11 +04:00
parent 08628704c7
commit 4b94303d67
4 changed files with 224 additions and 60 deletions

View File

@ -12,6 +12,8 @@
#include <mbedtls/base64.h> #include <mbedtls/base64.h>
#include <Ed25519.h> #include <Ed25519.h>
#include <sodium.h> #include <sodium.h>
#include <time.h>
#include <sys/time.h>
#include <vector> #include <vector>
#define XPOWERS_CHIP_AXP2101 #define XPOWERS_CHIP_AXP2101
#include "XPowersLib.h" #include "XPowersLib.h"
@ -80,6 +82,7 @@ static const char *kUsersEconomyConfigSeed = "shine_users_economy_config";
static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault"; static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault";
static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK"; static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress"; static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
static const char *kDefaultShineServerLogin = "shineupme";
static const uint8_t kBlockTypeRecoveryKey = 0; static const uint8_t kBlockTypeRecoveryKey = 0;
static const uint8_t kBlockTypeRootKey = 1; static const uint8_t kBlockTypeRootKey = 1;
static const uint8_t kBlockTypeClientKey = 2; static const uint8_t kBlockTypeClientKey = 2;
@ -322,8 +325,10 @@ static int gScanResultCount = 0;
static WifiViewMode gWifiViewMode = WIFI_VIEW_OVERVIEW; static WifiViewMode gWifiViewMode = WIFI_VIEW_OVERVIEW;
static String gSolanaRpcUrl = "https://api.devnet.solana.com"; static String gSolanaRpcUrl = "https://api.devnet.solana.com";
static String gShineServerUrl = "https://shineup.me"; static String gShineServerLogin = kDefaultShineServerLogin;
static String gServerStatusMessage = "Edit RPC or shine host"; static String gShineServerUrl;
static String gResolvedShineServerLogin;
static String gServerStatusMessage = "Edit RPC or server login";
static String gLoginValue; static String gLoginValue;
static String gHomeserverValue = "homeserver1"; static String gHomeserverValue = "homeserver1";
static bool gSecretConfigured = false; static bool gSecretConfigured = false;
@ -527,6 +532,11 @@ static void pushU64LE(std::vector<uint8_t> &out, uint64_t value);
static void pushStrU8(std::vector<uint8_t> &out, const String &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 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 bytesToBase58(const uint8_t *data, size_t len);
static bool getSystemEpochMs(uint64_t &epochMsOut);
static bool ensureNtpTimeSynced(String &errorOut);
static bool resolveShineServerUrlFromLogin(const String &serverLogin, String &serverUrlOut, String &errorOut);
static String currentShineServerLoginSource();
static bool ensureCurrentShineServerUrl(String &errorOut);
static String buildBaseRpcRequest(const char *method, const String &paramsJson); static String buildBaseRpcRequest(const char *method, const String &paramsJson);
static bool rpcCallSolana(const char *method, const String &paramsJson, String &payloadOut); static bool rpcCallSolana(const char *method, const String &paramsJson, String &payloadOut);
static bool rpcResponseHasError(const String &payload); static bool rpcResponseHasError(const String &payload);
@ -550,7 +560,7 @@ static std::vector<uint8_t> buildLastBlockStateBytes(const String &login,
static std::vector<uint8_t> buildUnsignedCreateRecord( static std::vector<uint8_t> buildUnsignedCreateRecord(
const String &login, const String &login,
const String &blockchainName, const String &blockchainName,
const String &serverAddress, const std::vector<String> &accessServers,
const uint8_t recoveryPub[32], const uint8_t recoveryPub[32],
const uint8_t rootPub[32], const uint8_t rootPub[32],
const uint8_t clientPub[32], const uint8_t clientPub[32],
@ -561,7 +571,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
static std::vector<uint8_t> buildCreateInstructionData( static std::vector<uint8_t> buildCreateInstructionData(
const String &login, const String &login,
const String &blockchainName, const String &blockchainName,
const String &serverAddress, const std::vector<String> &accessServers,
const uint8_t recoveryPub[32], const uint8_t recoveryPub[32],
const uint8_t rootPub[32], const uint8_t rootPub[32],
const uint8_t clientPub[32], const uint8_t clientPub[32],
@ -890,6 +900,19 @@ static String normalizeLoginValue(const String &value) {
return out; return out;
} }
static bool isValidShineServerLoginValue(const String &value) {
if (value.isEmpty() || value.length() > 20) {
return false;
}
for (size_t i = 0; i < value.length(); ++i) {
char ch = value.charAt(i);
if (!(isAlphaNumeric((unsigned char)ch) || ch == '_')) {
return false;
}
}
return true;
}
static String abbreviateValue(const String &value, size_t head = 8, size_t tail = 6) { static String abbreviateValue(const String &value, size_t head = 8, size_t tail = 6) {
if (value.length() <= head + tail + 3) { if (value.length() <= head + tail + 3) {
return value; return value;
@ -1622,6 +1645,16 @@ static String balanceHomeLine() {
return gBalanceStatusMessage; return gBalanceStatusMessage;
} }
static String shineServerDisplayLabel() {
if (!gShineServerUrl.isEmpty()) {
return gShineServerUrl;
}
if (!currentShineServerLoginSource().isEmpty()) {
return currentShineServerLoginSource();
}
return "not set";
}
static String wifiHomeRichLine() { static String wifiHomeRichLine() {
String ssid = gWifiSavedSsid.isEmpty() ? String("not configured") : gWifiSavedSsid; String ssid = gWifiSavedSsid.isEmpty() ? String("not configured") : gWifiSavedSsid;
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
@ -1631,7 +1664,7 @@ static String wifiHomeRichLine() {
} }
static String shineHomeRichLine() { static String shineHomeRichLine() {
String serverLabel = gShineServerUrl.isEmpty() ? String("not set") : gShineServerUrl; String serverLabel = shineServerDisplayLabel();
if (gShineStatusLine.endsWith(" connected")) { if (gShineStatusLine.endsWith(" connected")) {
return String("SHiNE: ") + serverLabel + " #38B26D connected#"; return String("SHiNE: ") + serverLabel + " #38B26D connected#";
} }
@ -1711,6 +1744,96 @@ static uint64_t shineNowMs() {
return value > 0 ? (uint64_t)value : (uint64_t)millis(); return value > 0 ? (uint64_t)value : (uint64_t)millis();
} }
static bool getSystemEpochMs(uint64_t &epochMsOut) {
struct timeval tv {};
if (gettimeofday(&tv, nullptr) != 0) {
return false;
}
if (tv.tv_sec < 1700000000) {
return false;
}
epochMsOut = (uint64_t)tv.tv_sec * 1000ULL + (uint64_t)(tv.tv_usec / 1000ULL);
return true;
}
static bool ensureNtpTimeSynced(String &errorOut) {
errorOut = "";
uint64_t epochMs = 0;
if (getSystemEpochMs(epochMs)) {
return true;
}
configTime(0, 0, "pool.ntp.org", "time.cloudflare.com", "time.google.com");
for (int i = 0; i < 30; ++i) {
delay(500);
if (getSystemEpochMs(epochMs)) {
return true;
}
}
errorOut = "NTP time sync failed";
return false;
}
static bool resolveShineServerUrlFromLogin(const String &serverLogin, String &serverUrlOut, String &errorOut) {
errorOut = "";
serverUrlOut = "";
String cleanLogin = normalizeLoginValue(serverLogin);
if (cleanLogin.isEmpty()) {
errorOut = "Shine server login is not set";
return false;
}
ShinePdaUserState serverState;
if (!readShineUserPda(cleanLogin, serverState, errorOut)) {
if (errorOut.isEmpty()) {
errorOut = "Failed to read Shine server PDA";
}
return false;
}
if (!serverState.found) {
errorOut = "Shine server PDA not found";
return false;
}
if (!serverState.isServer) {
errorOut = "Shine server PDA is not a server";
return false;
}
if (serverState.serverAddress.isEmpty()) {
errorOut = "Shine server address is empty";
return false;
}
serverUrlOut = serverState.serverAddress;
return true;
}
static String currentShineServerLoginSource() {
if (gCachedAccountPdaValid && gCachedAccountPdaLogin == normalizeLoginValue(gLoginValue) && !gCachedAccountPdaState.accessServers.empty()) {
String fromPda = normalizeLoginValue(gCachedAccountPdaState.accessServers.front());
if (!fromPda.isEmpty()) {
return fromPda;
}
}
return normalizeLoginValue(gShineServerLogin);
}
static bool ensureCurrentShineServerUrl(String &errorOut) {
errorOut = "";
String login = currentShineServerLoginSource();
if (login.isEmpty()) {
errorOut = "Shine server login is not set";
return false;
}
if (gShineServerUrl.isEmpty() || gResolvedShineServerLogin != login) {
String resolvedUrl;
if (!resolveShineServerUrlFromLogin(login, resolvedUrl, errorOut)) {
return false;
}
gShineServerUrl = resolvedUrl;
gResolvedShineServerLogin = login;
}
return true;
}
static void shortVecEncode(size_t value, std::vector<uint8_t> &out) { static void shortVecEncode(size_t value, std::vector<uint8_t> &out) {
do { do {
uint8_t byte = value & 0x7F; uint8_t byte = value & 0x7F;
@ -1948,7 +2071,7 @@ static std::vector<uint8_t> buildUpdateInstructionData(const ShinePdaUserState &
static std::vector<uint8_t> buildUnsignedCreateRecord( static std::vector<uint8_t> buildUnsignedCreateRecord(
const String &login, const String &login,
const String &blockchainName, const String &blockchainName,
const String &serverAddress, const std::vector<String> &accessServers,
const uint8_t recoveryPub[32], const uint8_t recoveryPub[32],
const uint8_t rootPub[32], const uint8_t rootPub[32],
const uint8_t clientPub[32], const uint8_t clientPub[32],
@ -1968,7 +2091,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
pushU32LE(out, 0); pushU32LE(out, 0);
out.insert(out.end(), 32, 0); out.insert(out.end(), 32, 0);
pushStrU8(out, login); pushStrU8(out, login);
out.push_back(8); out.push_back(7);
out.push_back(kBlockTypeRecoveryKey); out.push_back(kBlockTypeRecoveryKey);
out.push_back(0); out.push_back(0);
@ -1995,17 +2118,12 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
pushFixed(out, lastBlockSignature, 64); pushFixed(out, lastBlockSignature, 64);
out.push_back(0); out.push_back(0);
out.push_back(kBlockTypeServerProfile);
out.push_back(0);
out.push_back(1);
out.push_back(0);
out.push_back(0);
pushStrU8(out, serverAddress);
out.push_back(0);
out.push_back(kBlockTypeAccessServers); out.push_back(kBlockTypeAccessServers);
out.push_back(0); out.push_back(0);
out.push_back(0); out.push_back((uint8_t)accessServers.size());
for (const auto &value : accessServers) {
pushStrU8(out, value);
}
out.push_back(kBlockTypeSessions); out.push_back(kBlockTypeSessions);
out.push_back(0); out.push_back(0);
@ -2025,7 +2143,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
static std::vector<uint8_t> buildCreateInstructionData( static std::vector<uint8_t> buildCreateInstructionData(
const String &login, const String &login,
const String &blockchainName, const String &blockchainName,
const String &serverAddress, const std::vector<String> &accessServers,
const uint8_t recoveryPub[32], const uint8_t recoveryPub[32],
const uint8_t rootPub[32], const uint8_t rootPub[32],
const uint8_t clientPub[32], const uint8_t clientPub[32],
@ -2049,12 +2167,11 @@ static std::vector<uint8_t> buildCreateInstructionData(
out.insert(out.end(), 32, 0); out.insert(out.end(), 32, 0);
pushFixed(out, lastBlockSignature, 64); pushFixed(out, lastBlockSignature, 64);
out.push_back(0); out.push_back(0);
out.push_back(1);
out.push_back(0);
out.push_back(0);
pushStrU8(out, serverAddress);
out.push_back(0);
out.push_back(0); out.push_back(0);
out.push_back((uint8_t)accessServers.size());
for (const auto &value : accessServers) {
pushStrU8(out, value);
}
out.push_back(1); out.push_back(1);
out.push_back(0); out.push_back(0);
out.push_back(0); out.push_back(0);
@ -2683,11 +2800,15 @@ static bool registerHomeserverOnSolana(String &messageOut) {
}; };
String cleanLogin = normalizeLoginValue(gLoginValue); String cleanLogin = normalizeLoginValue(gLoginValue);
String accessServerLogin = normalizeLoginValue(gShineServerLogin);
if (!isValidShineServerLoginValue(accessServerLogin)) {
accessServerLogin = kDefaultShineServerLogin;
}
diagDetails += String("trigger=") + gRegisterTriggerSource + "\n"; diagDetails += String("trigger=") + gRegisterTriggerSource + "\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";
diagDetails += String("shine_server=") + gShineServerUrl + "\n"; diagDetails += String("shine_server_login=") + accessServerLogin + "\n";
diagDetails += String("homeserver=") + gHomeserverValue + "\n"; diagDetails += String("homeserver=") + gHomeserverValue + "\n";
if (cleanLogin.isEmpty()) { if (cleanLogin.isEmpty()) {
@ -2715,7 +2836,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
gAccountPdaStatusMessage = "User is already registered"; gAccountPdaStatusMessage = "User is already registered";
gShowRegisterAccountButton = false; gShowRegisterAccountButton = false;
gAccountStatusMessage = "User is already registered"; gAccountStatusMessage = "User is already registered";
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered"; gShineStatusLine = String("SHiNE: ") + (!gShineServerUrl.isEmpty() ? gShineServerUrl : accessServerLogin) + " registered";
refreshAccountPdaStatus(); refreshAccountPdaStatus();
diagDetails += String("user_pda=") + existingPda + "\n"; diagDetails += String("user_pda=") + existingPda + "\n";
saveRegisterDiag("ok", "User is already registered", diagDetails); saveRegisterDiag("ok", "User is already registered", diagDetails);
@ -2812,10 +2933,18 @@ static bool registerHomeserverOnSolana(String &messageOut) {
diagDetails += String("last_block_hash=") + bytesToHexString(lastBlockHash, 32) + "\n"; diagDetails += String("last_block_hash=") + bytesToHexString(lastBlockHash, 32) + "\n";
diagDetails += String("last_block_signature_b64=") + bytesToBase64String(lastBlockSignature, 64) + "\n"; diagDetails += String("last_block_signature_b64=") + bytesToBase64String(lastBlockSignature, 64) + "\n";
uint64_t createdAtMs = shineNowMs(); if (!ensureNtpTimeSynced(messageOut)) {
diagDetails += String("ntp_error=") + messageOut + "\n";
return failWithDiag(messageOut);
}
uint64_t createdAtMs = 0;
if (!getSystemEpochMs(createdAtMs)) {
return failWithDiag("NTP time is not ready");
}
diagDetails += String("created_at_ms=") + String((unsigned long long)createdAtMs) + "\n"; diagDetails += String("created_at_ms=") + String((unsigned long long)createdAtMs) + "\n";
std::vector<String> accessServers = {accessServerLogin};
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord( std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
cleanLogin, blockchainName, gShineServerUrl, cleanLogin, blockchainName, accessServers,
recoveryPub, recoveryPub,
rootPub, clientPub, blockchainPub, rootPub, clientPub, blockchainPub,
lastBlockSignature, startBonusLimit, createdAtMs); lastBlockSignature, startBonusLimit, createdAtMs);
@ -2830,7 +2959,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
diagDetails += String("root_signature_b64=") + bytesToBase64String(rootSignature, 64) + "\n"; diagDetails += String("root_signature_b64=") + bytesToBase64String(rootSignature, 64) + "\n";
std::vector<uint8_t> createData = buildCreateInstructionData( std::vector<uint8_t> createData = buildCreateInstructionData(
cleanLogin, blockchainName, gShineServerUrl, cleanLogin, blockchainName, accessServers,
recoveryPub, recoveryPub,
rootPub, clientPub, blockchainPub, rootPub, clientPub, blockchainPub,
lastBlockSignature, rootSignature, createdAtMs); lastBlockSignature, rootSignature, createdAtMs);
@ -2901,7 +3030,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
gAccountPdaStatus = ACCOUNT_PDA_OK; gAccountPdaStatus = ACCOUNT_PDA_OK;
gAccountPdaStatusMessage = "User registered"; gAccountPdaStatusMessage = "User registered";
gShowRegisterAccountButton = false; gShowRegisterAccountButton = false;
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered"; gShineStatusLine = String("SHiNE: ") + (!gShineServerUrl.isEmpty() ? gShineServerUrl : accessServerLogin) + " registered";
saveAccountPrefs(); saveAccountPrefs();
refreshAccountPdaStatus(); refreshAccountPdaStatus();
messageOut = "Solana registration confirmed"; messageOut = "Solana registration confirmed";
@ -3604,7 +3733,7 @@ static void refreshAccountPdaStatus() {
if (gLoginValue.isEmpty() || !gSecretConfigured) { if (gLoginValue.isEmpty() || !gSecretConfigured) {
gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN; gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN;
gAccountPdaStatusMessage = "account not configured"; gAccountPdaStatusMessage = "account not configured";
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured";
clearShineSessionState(false); clearShineSessionState(false);
return; return;
} }
@ -3614,7 +3743,7 @@ static void refreshAccountPdaStatus() {
if (!readShineUserPda(gLoginValue, pdaState, error)) { if (!readShineUserPda(gLoginValue, pdaState, error)) {
gAccountPdaStatus = ACCOUNT_PDA_MISMATCH; gAccountPdaStatus = ACCOUNT_PDA_MISMATCH;
gAccountPdaStatusMessage = error.isEmpty() ? "solana check failed" : error; gAccountPdaStatusMessage = error.isEmpty() ? "solana check failed" : error;
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " unavailable"; gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " unavailable";
clearShineSessionState(false); clearShineSessionState(false);
if (error == "Solana RPC unavailable") { if (error == "Solana RPC unavailable") {
gAccountCheckPending = true; gAccountCheckPending = true;
@ -3626,7 +3755,7 @@ static void refreshAccountPdaStatus() {
gAccountPdaStatus = ACCOUNT_PDA_NOT_FOUND; gAccountPdaStatus = ACCOUNT_PDA_NOT_FOUND;
gAccountPdaStatusMessage = "user not found"; gAccountPdaStatusMessage = "user not found";
gShowRegisterAccountButton = true; gShowRegisterAccountButton = true;
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured";
clearShineSessionState(false); clearShineSessionState(false);
return; return;
} }
@ -3641,7 +3770,7 @@ static void refreshAccountPdaStatus() {
|| !base58ToFixed32(gBlockchainPubB58, blockchainPub)) { || !base58ToFixed32(gBlockchainPubB58, blockchainPub)) {
gAccountPdaStatus = ACCOUNT_PDA_MISMATCH; gAccountPdaStatus = ACCOUNT_PDA_MISMATCH;
gAccountPdaStatusMessage = "local keys invalid"; gAccountPdaStatusMessage = "local keys invalid";
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured";
clearShineSessionState(false); clearShineSessionState(false);
return; return;
} }
@ -3694,13 +3823,13 @@ static void refreshAccountPdaStatus() {
gHomeserverPdaActionReason = mismatch; gHomeserverPdaActionReason = mismatch;
gHomeserverPdaCanFix = true; gHomeserverPdaCanFix = true;
} }
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured";
clearShineSessionState(false); clearShineSessionState(false);
return; return;
} }
gCachedAccountPdaState = pdaState; gCachedAccountPdaState = pdaState;
gCachedAccountPdaLogin = gLoginValue; gCachedAccountPdaLogin = normalizeLoginValue(gLoginValue);
gCachedAccountPdaValid = true; gCachedAccountPdaValid = true;
gAccountPdaStatus = ACCOUNT_PDA_OK; gAccountPdaStatus = ACCOUNT_PDA_OK;
gAccountPdaStatusMessage = "ok"; gAccountPdaStatusMessage = "ok";
@ -4338,8 +4467,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
diagDetails += String("uptime_ms=") + String(millis()) + "\n"; diagDetails += String("uptime_ms=") + String(millis()) + "\n";
diagDetails += String("login=") + gLoginValue + "\n"; diagDetails += String("login=") + gLoginValue + "\n";
diagDetails += String("homeserver=") + gHomeserverValue + "\n"; diagDetails += String("homeserver=") + gHomeserverValue + "\n";
diagDetails += String("server_url=") + gShineServerUrl + "\n"; diagDetails += String("server_login=") + currentShineServerLoginSource() + "\n";
diagDetails += String("ws_url=") + shineWsUrl() + "\n";
diagDetails += String("pda_status=") + gAccountPdaStatusMessage + "\n"; diagDetails += String("pda_status=") + gAccountPdaStatusMessage + "\n";
if (WiFi.status() != WL_CONNECTED) { if (WiFi.status() != WL_CONNECTED) {
diagDetails += "wifi=disconnected\n"; diagDetails += "wifi=disconnected\n";
@ -4352,6 +4480,12 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
if (gAccountPdaStatus != ACCOUNT_PDA_OK) { if (gAccountPdaStatus != ACCOUNT_PDA_OK) {
return failWithDiag("account not ready"); return failWithDiag("account not ready");
} }
if (!ensureCurrentShineServerUrl(errorOut)) {
diagDetails += String("server_resolve_error=") + errorOut + "\n";
return failWithDiag(errorOut);
}
diagDetails += String("server_url=") + gShineServerUrl + "\n";
diagDetails += String("ws_url=") + shineWsUrl() + "\n";
String wsUrl = shineWsUrl(); String wsUrl = shineWsUrl();
if (wsUrl.isEmpty()) { if (wsUrl.isEmpty()) {
@ -4513,7 +4647,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
} }
static void manageShineConnection() { static void manageShineConnection() {
String serverLabel = gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl; String serverLabel = shineServerDisplayLabel();
if (gLoginValue.isEmpty() || !gSecretConfigured || gHomeserverValue.isEmpty()) { if (gLoginValue.isEmpty() || !gSecretConfigured || gHomeserverValue.isEmpty()) {
gShineStatusLine = String("SHiNE: ") + serverLabel + " account not configured"; gShineStatusLine = String("SHiNE: ") + serverLabel + " account not configured";
clearShineSessionState(false); clearShineSessionState(false);
@ -4543,6 +4677,13 @@ static void manageShineConnection() {
} }
gLastShineAttemptMs = now; gLastShineAttemptMs = now;
String error; String error;
if (!ensureCurrentShineServerUrl(error)) {
gShineStatusLine = String("SHiNE: ") + serverLabel + " unavailable";
clearShineSessionState(false);
gShineReconnectDelayMs = min(gShineReconnectDelayMs + SHINE_RECONNECT_MIN_MS, (unsigned long)SHINE_RECONNECT_MAX_MS);
return;
}
serverLabel = shineServerDisplayLabel();
if (ensureShineSessionAuthenticated(error)) { if (ensureShineSessionAuthenticated(error)) {
gShineStatusLine = String("SHiNE: ") + serverLabel + " connected"; gShineStatusLine = String("SHiNE: ") + serverLabel + " connected";
gLastShinePingMs = now; gLastShinePingMs = now;
@ -4626,7 +4767,18 @@ static void loadPrefs() {
upsertKnownWifi(gWifiSavedSsid, gWifiSavedPassword); upsertKnownWifi(gWifiSavedSsid, gWifiSavedPassword);
} }
gSolanaRpcUrl = gPrefs.getString("solana_rpc", "https://api.devnet.solana.com"); gSolanaRpcUrl = gPrefs.getString("solana_rpc", "https://api.devnet.solana.com");
gShineServerUrl = gPrefs.getString("shine_server", "https://shineup.me"); String storedShineServerLogin = normalizeLoginValue(gPrefs.getString("shine_server_login", ""));
if (!isValidShineServerLoginValue(storedShineServerLogin)) {
String legacyShineServer = normalizeLoginValue(gPrefs.getString("shine_server", ""));
if (isValidShineServerLoginValue(legacyShineServer)) {
storedShineServerLogin = legacyShineServer;
} else {
storedShineServerLogin = kDefaultShineServerLogin;
}
}
gShineServerLogin = storedShineServerLogin;
gShineServerUrl = "";
gResolvedShineServerLogin = "";
gLoginValue = gPrefs.getString("login", ""); gLoginValue = gPrefs.getString("login", "");
gHomeserverValue = gPrefs.getString("homeserver", "homeserver1"); gHomeserverValue = gPrefs.getString("homeserver", "homeserver1");
String walletTypeStored = gPrefs.getString("wallet_type", "client.key"); String walletTypeStored = gPrefs.getString("wallet_type", "client.key");
@ -4695,7 +4847,8 @@ static void saveWifiPrefs() {
static void saveServerPrefs() { static void saveServerPrefs() {
gPrefs.putString("solana_rpc", gSolanaRpcUrl); gPrefs.putString("solana_rpc", gSolanaRpcUrl);
gPrefs.putString("shine_server", gShineServerUrl); gPrefs.putString("shine_server_login", gShineServerLogin);
gPrefs.remove("shine_server");
} }
static void saveAccountPrefs() { static void saveAccountPrefs() {
@ -5217,13 +5370,18 @@ static void applyEditorValue() {
} }
if (gEditContext == EDIT_CONTEXT_SHINE_SERVER) { if (gEditContext == EDIT_CONTEXT_SHINE_SERVER) {
gShineServerUrl = value; gShineServerLogin = normalizeLoginValue(value);
if (!isValidShineServerLoginValue(gShineServerLogin)) {
gShineServerLogin = kDefaultShineServerLogin;
}
gShineServerUrl = "";
gResolvedShineServerLogin = "";
saveServerPrefs(); saveServerPrefs();
gServerStatusMessage = "Shine server saved"; gServerStatusMessage = "SHiNE server login saved";
clearShineSessionState(false); clearShineSessionState(false);
gShineReconnectDelayMs = SHINE_RECONNECT_MIN_MS; gShineReconnectDelayMs = SHINE_RECONNECT_MIN_MS;
gLastShineAttemptMs = 0; gLastShineAttemptMs = 0;
gShineStatusLine = "SHiNE: reconnect pending"; gShineStatusLine = String("SHiNE: ") + gShineServerLogin + " reconnect pending";
showScreen(SCREEN_SERVER); showScreen(SCREEN_SERVER);
return; return;
} }
@ -5577,9 +5735,9 @@ static void actionButtonCb(lv_event_t *event) {
case ACTION_SERVER_EDIT_SHINE: case ACTION_SERVER_EDIT_SHINE:
openEditor(EDIT_CONTEXT_SHINE_SERVER, openEditor(EDIT_CONTEXT_SHINE_SERVER,
SCREEN_SERVER, SCREEN_SERVER,
"EDIT SHINE HOST", "EDIT SHINE SERVER LOGIN",
"", "",
gShineServerUrl, gShineServerLogin,
false); false);
break; break;
case ACTION_ACCOUNT_EDIT_LOGIN: case ACTION_ACCOUNT_EDIT_LOGIN:
@ -6324,8 +6482,12 @@ static void drawServerScreen() {
showMessageAt(gServerStatusMessage, 56); showMessageAt(gServerStatusMessage, 56);
showMessageAt(String("Solana: ") + gSolanaRpcUrl, 96); showMessageAt(String("Solana: ") + gSolanaRpcUrl, 96);
makeButton("SOLANA RPC", 22, 146, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SOLANA, &lv_font_montserrat_24); makeButton("SOLANA RPC", 22, 146, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SOLANA, &lv_font_montserrat_24);
showMessageAt(String("Shine: ") + gShineServerUrl, 248); showMessageAt(String("SHiNE: ") + shineServerDisplayLabel(), 248);
makeButton("SHINE SERVER", 22, 298, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SHINE, &lv_font_montserrat_24); if (gUserPdaAddress.isEmpty()) {
makeButton("SHiNE SERVER LOGIN", 22, 298, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SHINE, &lv_font_montserrat_22);
} else {
makeBody("SHiNE server login is read from PDA.", 312, 360);
}
makeBody("Swipe right to return to Settings.", 396, 420); makeBody("Swipe right to return to Settings.", 396, 420);
makeVersionTag(); makeVersionTag();
} }

View File

@ -150,12 +150,12 @@
- статусное сообщение; - статусное сообщение;
- текущий `Solana RPC` адрес; - текущий `Solana RPC` адрес;
- кнопку `SOLANA RPC`; - кнопку `SOLANA RPC`;
- текущий `Shine server` адрес; - текущий `SHiNE server login` или уже резолвленный адрес;
- кнопку `SHINE SERVER`. - кнопку `SHiNE SERVER LOGIN`, если обычный `user PDA` ещё не зарегистрирован.
Значения по умолчанию: Значения по умолчанию:
- Solana RPC: `https://api.devnet.solana.com` - Solana RPC: `https://api.devnet.solana.com`
- Shine server: `https://shineup.me` - SHiNE server login: `shineupme`
Нажатие на любую из двух кнопок открывает `TEXT_EDIT_SCREEN`. Нажатие на любую из двух кнопок открывает `TEXT_EDIT_SCREEN`.
@ -229,7 +229,7 @@
Используется для: Используется для:
- пароля Wi-Fi; - пароля Wi-Fi;
- Solana RPC; - Solana RPC;
- Shine server. - SHiNE server login.
Показывает: Показывает:
- заголовок; - заголовок;
@ -291,7 +291,7 @@
Используется `Preferences` (NVS памяти ESP32): Используется `Preferences` (NVS памяти ESP32):
- `solana_rpc` - `solana_rpc`
- `shine_server` - `shine_server_login`
## Хранение аккаунта ## Хранение аккаунта

View File

@ -65,12 +65,13 @@
- `user pda address`; - `user pda address`;
- `registration signature`; - `registration signature`;
- `balance`; - `balance`;
- `server api url`; - `server login` для первичной привязки;
- `server rpc url`; - `resolved server api url` / `rpc url` / `ws url` после чтения PDA сервера;
- `server ws url`;
- флаги: - флаги:
`wifiReady`, `serversReady`, `secretReady`, `registered`, `online`. `wifiReady`, `serversReady`, `secretReady`, `registered`, `online`.
Для первой регистрации обычного `user PDA` устройство берёт `createdAtMs` / `updatedAtMs` из NTP прямо перед отправкой транзакции в Solana. Дальше в `user PDA` сохраняется `accessServers`, где по умолчанию лежит `shineupme`.
## Правило серверной сессии SHiNE ## Правило серверной сессии SHiNE
При подключении к серверу `SHiNE` устройство должно авторизовываться как homeserver-сеанс: При подключении к серверу `SHiNE` устройство должно авторизовываться как homeserver-сеанс:
@ -86,7 +87,7 @@
Кнопка регистрации доступна только если одновременно выполнены условия: Кнопка регистрации доступна только если одновременно выполнены условия:
1. настроен и подтверждён `Wi-Fi`; 1. настроен и подтверждён `Wi-Fi`;
2. заполнены и подтверждены серверные адреса; 2. задан и подтверждён `SHiNE server login`;
3. задан логин; 3. задан логин;
4. сгенерирован или введён секрет; 4. сгенерирован или введён секрет;
5. баланс кошелька не меньше `0.20 SOL`; 5. баланс кошелька не меньше `0.20 SOL`;
@ -628,7 +629,7 @@
2. открыть `Подключение -> Wi-Fi`; 2. открыть `Подключение -> Wi-Fi`;
3. ввести `SSID` и пароль, нажать `Проверить`; 3. ввести `SSID` и пароль, нажать `Проверить`;
4. открыть `Подключение -> Серверы`; 4. открыть `Подключение -> Серверы`;
5. проверить или задать серверные адреса; 5. проверить или задать `SHiNE server login` (по умолчанию `shineupme`);
6. открыть `Аккаунт`; 6. открыть `Аккаунт`;
7. ввести логин; 7. ввести логин;
8. задать имя homeserver; 8. задать имя homeserver;
@ -637,14 +638,15 @@
11. при необходимости пополнить баланс; 11. при необходимости пополнить баланс;
12. вернуться на `HOME`; 12. вернуться на `HOME`;
13. нажать `REGISTER ACCOUNT`; 13. нажать `REGISTER ACCOUNT`;
14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1` и при необходимости сообщение о неподключённом `Wi-Fi`; 14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1`, серверный login и при необходимости сообщение о неподключённом `Wi-Fi`;
15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`; 15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`;
16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку; 16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку;
17. после успешной регистрации увидеть статус `Homeserver активен`. 17. после успешной регистрации увидеть статус `Homeserver активен`.
Примечание: Примечание:
- устройство реально отправляет `create_user_pda` в `shine_users`, а после подтверждения сохраняет `PDA` и `tx signature`. - устройство реально отправляет `create_user_pda` в `shine_users`, а после подтверждения сохраняет `PDA` и `tx signature`;
- при первой регистрации для обычного `user PDA` не заполняется `serverAddress`, а `accessServers` получает `shineupme` или другой выбранный `SHiNE server login`.
## Сценарий входящего запроса ## Сценарий входящего запроса

View File

@ -1,2 +1,2 @@
client.version=1.2.245 client.version=1.2.246
server.version=1.2.230 server.version=1.2.231