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 <Ed25519.h>
#include <sodium.h>
#include <time.h>
#include <sys/time.h>
#include <vector>
#define XPOWERS_CHIP_AXP2101
#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 *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
static const char *kDefaultShineServerLogin = "shineupme";
static const uint8_t kBlockTypeRecoveryKey = 0;
static const uint8_t kBlockTypeRootKey = 1;
static const uint8_t kBlockTypeClientKey = 2;
@ -322,8 +325,10 @@ static int gScanResultCount = 0;
static WifiViewMode gWifiViewMode = WIFI_VIEW_OVERVIEW;
static String gSolanaRpcUrl = "https://api.devnet.solana.com";
static String gShineServerUrl = "https://shineup.me";
static String gServerStatusMessage = "Edit RPC or shine host";
static String gShineServerLogin = kDefaultShineServerLogin;
static String gShineServerUrl;
static String gResolvedShineServerLogin;
static String gServerStatusMessage = "Edit RPC or server login";
static String gLoginValue;
static String gHomeserverValue = "homeserver1";
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 pushFixed(std::vector<uint8_t> &out, 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 bool rpcCallSolana(const char *method, const String &paramsJson, String &payloadOut);
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(
const String &login,
const String &blockchainName,
const String &serverAddress,
const std::vector<String> &accessServers,
const uint8_t recoveryPub[32],
const uint8_t rootPub[32],
const uint8_t clientPub[32],
@ -561,7 +571,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
static std::vector<uint8_t> buildCreateInstructionData(
const String &login,
const String &blockchainName,
const String &serverAddress,
const std::vector<String> &accessServers,
const uint8_t recoveryPub[32],
const uint8_t rootPub[32],
const uint8_t clientPub[32],
@ -890,6 +900,19 @@ static String normalizeLoginValue(const String &value) {
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) {
if (value.length() <= head + tail + 3) {
return value;
@ -1622,6 +1645,16 @@ static String balanceHomeLine() {
return gBalanceStatusMessage;
}
static String shineServerDisplayLabel() {
if (!gShineServerUrl.isEmpty()) {
return gShineServerUrl;
}
if (!currentShineServerLoginSource().isEmpty()) {
return currentShineServerLoginSource();
}
return "not set";
}
static String wifiHomeRichLine() {
String ssid = gWifiSavedSsid.isEmpty() ? String("not configured") : gWifiSavedSsid;
if (WiFi.status() == WL_CONNECTED) {
@ -1631,7 +1664,7 @@ static String wifiHomeRichLine() {
}
static String shineHomeRichLine() {
String serverLabel = gShineServerUrl.isEmpty() ? String("not set") : gShineServerUrl;
String serverLabel = shineServerDisplayLabel();
if (gShineStatusLine.endsWith(" connected")) {
return String("SHiNE: ") + serverLabel + " #38B26D connected#";
}
@ -1711,6 +1744,96 @@ static uint64_t shineNowMs() {
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) {
do {
uint8_t byte = value & 0x7F;
@ -1948,7 +2071,7 @@ static std::vector<uint8_t> buildUpdateInstructionData(const ShinePdaUserState &
static std::vector<uint8_t> buildUnsignedCreateRecord(
const String &login,
const String &blockchainName,
const String &serverAddress,
const std::vector<String> &accessServers,
const uint8_t recoveryPub[32],
const uint8_t rootPub[32],
const uint8_t clientPub[32],
@ -1968,7 +2091,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
pushU32LE(out, 0);
out.insert(out.end(), 32, 0);
pushStrU8(out, login);
out.push_back(8);
out.push_back(7);
out.push_back(kBlockTypeRecoveryKey);
out.push_back(0);
@ -1995,17 +2118,12 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
pushFixed(out, lastBlockSignature, 64);
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(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(0);
@ -2025,7 +2143,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
static std::vector<uint8_t> buildCreateInstructionData(
const String &login,
const String &blockchainName,
const String &serverAddress,
const std::vector<String> &accessServers,
const uint8_t recoveryPub[32],
const uint8_t rootPub[32],
const uint8_t clientPub[32],
@ -2049,12 +2167,11 @@ static std::vector<uint8_t> buildCreateInstructionData(
out.insert(out.end(), 32, 0);
pushFixed(out, lastBlockSignature, 64);
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((uint8_t)accessServers.size());
for (const auto &value : accessServers) {
pushStrU8(out, value);
}
out.push_back(1);
out.push_back(0);
out.push_back(0);
@ -2683,11 +2800,15 @@ static bool registerHomeserverOnSolana(String &messageOut) {
};
String cleanLogin = normalizeLoginValue(gLoginValue);
String accessServerLogin = normalizeLoginValue(gShineServerLogin);
if (!isValidShineServerLoginValue(accessServerLogin)) {
accessServerLogin = kDefaultShineServerLogin;
}
diagDetails += String("trigger=") + gRegisterTriggerSource + "\n";
diagDetails += String("test_uptime_ms=") + String(millis()) + "\n";
diagDetails += String("login=") + cleanLogin + "\n";
diagDetails += String("rpc=") + gSolanaRpcUrl + "\n";
diagDetails += String("shine_server=") + gShineServerUrl + "\n";
diagDetails += String("shine_server_login=") + accessServerLogin + "\n";
diagDetails += String("homeserver=") + gHomeserverValue + "\n";
if (cleanLogin.isEmpty()) {
@ -2715,7 +2836,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
gAccountPdaStatusMessage = "User is already registered";
gShowRegisterAccountButton = false;
gAccountStatusMessage = "User is already registered";
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered";
gShineStatusLine = String("SHiNE: ") + (!gShineServerUrl.isEmpty() ? gShineServerUrl : accessServerLogin) + " registered";
refreshAccountPdaStatus();
diagDetails += String("user_pda=") + existingPda + "\n";
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_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";
std::vector<String> accessServers = {accessServerLogin};
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
cleanLogin, blockchainName, gShineServerUrl,
cleanLogin, blockchainName, accessServers,
recoveryPub,
rootPub, clientPub, blockchainPub,
lastBlockSignature, startBonusLimit, createdAtMs);
@ -2830,7 +2959,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
diagDetails += String("root_signature_b64=") + bytesToBase64String(rootSignature, 64) + "\n";
std::vector<uint8_t> createData = buildCreateInstructionData(
cleanLogin, blockchainName, gShineServerUrl,
cleanLogin, blockchainName, accessServers,
recoveryPub,
rootPub, clientPub, blockchainPub,
lastBlockSignature, rootSignature, createdAtMs);
@ -2901,7 +3030,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
gAccountPdaStatus = ACCOUNT_PDA_OK;
gAccountPdaStatusMessage = "User registered";
gShowRegisterAccountButton = false;
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered";
gShineStatusLine = String("SHiNE: ") + (!gShineServerUrl.isEmpty() ? gShineServerUrl : accessServerLogin) + " registered";
saveAccountPrefs();
refreshAccountPdaStatus();
messageOut = "Solana registration confirmed";
@ -3604,7 +3733,7 @@ static void refreshAccountPdaStatus() {
if (gLoginValue.isEmpty() || !gSecretConfigured) {
gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN;
gAccountPdaStatusMessage = "account not configured";
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured";
gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured";
clearShineSessionState(false);
return;
}
@ -3614,7 +3743,7 @@ static void refreshAccountPdaStatus() {
if (!readShineUserPda(gLoginValue, pdaState, error)) {
gAccountPdaStatus = ACCOUNT_PDA_MISMATCH;
gAccountPdaStatusMessage = error.isEmpty() ? "solana check failed" : error;
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " unavailable";
gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " unavailable";
clearShineSessionState(false);
if (error == "Solana RPC unavailable") {
gAccountCheckPending = true;
@ -3626,7 +3755,7 @@ static void refreshAccountPdaStatus() {
gAccountPdaStatus = ACCOUNT_PDA_NOT_FOUND;
gAccountPdaStatusMessage = "user not found";
gShowRegisterAccountButton = true;
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured";
gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured";
clearShineSessionState(false);
return;
}
@ -3641,7 +3770,7 @@ static void refreshAccountPdaStatus() {
|| !base58ToFixed32(gBlockchainPubB58, blockchainPub)) {
gAccountPdaStatus = ACCOUNT_PDA_MISMATCH;
gAccountPdaStatusMessage = "local keys invalid";
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured";
gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured";
clearShineSessionState(false);
return;
}
@ -3694,13 +3823,13 @@ static void refreshAccountPdaStatus() {
gHomeserverPdaActionReason = mismatch;
gHomeserverPdaCanFix = true;
}
gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured";
gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured";
clearShineSessionState(false);
return;
}
gCachedAccountPdaState = pdaState;
gCachedAccountPdaLogin = gLoginValue;
gCachedAccountPdaLogin = normalizeLoginValue(gLoginValue);
gCachedAccountPdaValid = true;
gAccountPdaStatus = ACCOUNT_PDA_OK;
gAccountPdaStatusMessage = "ok";
@ -4338,8 +4467,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
diagDetails += String("uptime_ms=") + String(millis()) + "\n";
diagDetails += String("login=") + gLoginValue + "\n";
diagDetails += String("homeserver=") + gHomeserverValue + "\n";
diagDetails += String("server_url=") + gShineServerUrl + "\n";
diagDetails += String("ws_url=") + shineWsUrl() + "\n";
diagDetails += String("server_login=") + currentShineServerLoginSource() + "\n";
diagDetails += String("pda_status=") + gAccountPdaStatusMessage + "\n";
if (WiFi.status() != WL_CONNECTED) {
diagDetails += "wifi=disconnected\n";
@ -4352,6 +4480,12 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
if (gAccountPdaStatus != ACCOUNT_PDA_OK) {
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();
if (wsUrl.isEmpty()) {
@ -4513,7 +4647,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
}
static void manageShineConnection() {
String serverLabel = gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl;
String serverLabel = shineServerDisplayLabel();
if (gLoginValue.isEmpty() || !gSecretConfigured || gHomeserverValue.isEmpty()) {
gShineStatusLine = String("SHiNE: ") + serverLabel + " account not configured";
clearShineSessionState(false);
@ -4543,6 +4677,13 @@ static void manageShineConnection() {
}
gLastShineAttemptMs = now;
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)) {
gShineStatusLine = String("SHiNE: ") + serverLabel + " connected";
gLastShinePingMs = now;
@ -4626,7 +4767,18 @@ static void loadPrefs() {
upsertKnownWifi(gWifiSavedSsid, gWifiSavedPassword);
}
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", "");
gHomeserverValue = gPrefs.getString("homeserver", "homeserver1");
String walletTypeStored = gPrefs.getString("wallet_type", "client.key");
@ -4695,7 +4847,8 @@ static void saveWifiPrefs() {
static void saveServerPrefs() {
gPrefs.putString("solana_rpc", gSolanaRpcUrl);
gPrefs.putString("shine_server", gShineServerUrl);
gPrefs.putString("shine_server_login", gShineServerLogin);
gPrefs.remove("shine_server");
}
static void saveAccountPrefs() {
@ -5217,13 +5370,18 @@ static void applyEditorValue() {
}
if (gEditContext == EDIT_CONTEXT_SHINE_SERVER) {
gShineServerUrl = value;
gShineServerLogin = normalizeLoginValue(value);
if (!isValidShineServerLoginValue(gShineServerLogin)) {
gShineServerLogin = kDefaultShineServerLogin;
}
gShineServerUrl = "";
gResolvedShineServerLogin = "";
saveServerPrefs();
gServerStatusMessage = "Shine server saved";
gServerStatusMessage = "SHiNE server login saved";
clearShineSessionState(false);
gShineReconnectDelayMs = SHINE_RECONNECT_MIN_MS;
gLastShineAttemptMs = 0;
gShineStatusLine = "SHiNE: reconnect pending";
gShineStatusLine = String("SHiNE: ") + gShineServerLogin + " reconnect pending";
showScreen(SCREEN_SERVER);
return;
}
@ -5577,9 +5735,9 @@ static void actionButtonCb(lv_event_t *event) {
case ACTION_SERVER_EDIT_SHINE:
openEditor(EDIT_CONTEXT_SHINE_SERVER,
SCREEN_SERVER,
"EDIT SHINE HOST",
"EDIT SHINE SERVER LOGIN",
"",
gShineServerUrl,
gShineServerLogin,
false);
break;
case ACTION_ACCOUNT_EDIT_LOGIN:
@ -6324,8 +6482,12 @@ static void drawServerScreen() {
showMessageAt(gServerStatusMessage, 56);
showMessageAt(String("Solana: ") + gSolanaRpcUrl, 96);
makeButton("SOLANA RPC", 22, 146, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SOLANA, &lv_font_montserrat_24);
showMessageAt(String("Shine: ") + gShineServerUrl, 248);
makeButton("SHINE SERVER", 22, 298, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SHINE, &lv_font_montserrat_24);
showMessageAt(String("SHiNE: ") + shineServerDisplayLabel(), 248);
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);
makeVersionTag();
}

View File

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

View File

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