Compare commits
10 Commits
b461431197
...
4b94303d67
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
4b94303d67 | ||
|
|
08628704c7 | ||
|
|
f1c1132690 | ||
|
|
d2426c473c | ||
|
|
66986b804c | ||
|
|
95daa230bb | ||
|
|
365b22d778 | ||
|
|
cf2b54464e | ||
|
|
4e60c1274a | ||
|
|
2f65e63fbe |
@ -0,0 +1,26 @@
|
|||||||
|
## Кратко
|
||||||
|
|
||||||
|
Исправлена ESP32-ветка обновления `user_pda` для добавления `homeserver`-сессии после миграции формата PDA на `RecoveryKeyBlock`.
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
- В `shine_homeserver_main.ino` синхронизирован `create/update` payload с новым форматом `shine_users`.
|
||||||
|
- В сериализацию и парсинг PDA добавлен `RecoveryKeyBlock`.
|
||||||
|
- Для ветки `Add Homeserver` добавлены промежуточные checkpoint-записи в NVS, чтобы после crash или reset было видно, на каком шаге оборвалась операция.
|
||||||
|
- В `ESP32/AGENTS.md` добавлена памятка по чтению `last_error`.
|
||||||
|
|
||||||
|
## Что проверять
|
||||||
|
|
||||||
|
- Зарегистрировать или использовать уже существующий аккаунт на ESP32.
|
||||||
|
- Дойти до состояния `homeserver not in PDA`.
|
||||||
|
- Нажать `Add Homeserver`.
|
||||||
|
- Если операция не успешна, считать `last_error` по USB serial и убедиться, что видна свежая запись именно по шагам `Homeserver PDA update ...`, а не старый diag.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
|
||||||
|
- `Add Homeserver` добавляет `homeserver1` в `sessions` блока `SessionsBlock`.
|
||||||
|
- Если операция падает, в NVS сохраняется свежая диагностическая запись с текущим этапом, а не устаревший лог регистрации.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
`pending`
|
||||||
@ -15,3 +15,14 @@
|
|||||||
- Основной способ проверки и прошивки скетчей для `ESP32-S3-Touch-AMOLED-2.16` - `main-device/burn.sh`.
|
- Основной способ проверки и прошивки скетчей для `ESP32-S3-Touch-AMOLED-2.16` - `main-device/burn.sh`.
|
||||||
- Не собирать эти скетчи напрямую через `arduino-cli compile` без `burn.sh`, потому что скрипт добавляет нужные локальные библиотеки и конфиги из `official-demo/examples/Arduino-v3.3.5/libraries`.
|
- Не собирать эти скетчи напрямую через `arduino-cli compile` без `burn.sh`, потому что скрипт добавляет нужные локальные библиотеки и конфиги из `official-demo/examples/Arduino-v3.3.5/libraries`.
|
||||||
- Если сборка падает по `lv_conf.h` или `TouchDrvCSTXXX.hpp`, сначала проверять именно `burn.sh` и его `--library` пути, а не считать, что файл пропал из репозитория.
|
- Если сборка падает по `lv_conf.h` или `TouchDrvCSTXXX.hpp`, сначала проверять именно `burn.sh` и его `--library` пути, а не считать, что файл пропал из репозитория.
|
||||||
|
|
||||||
|
## Диагностика ESP32
|
||||||
|
|
||||||
|
- Последнюю сохранённую ошибку или диагностическую запись читать с устройства через USB serial monitor на `115200`.
|
||||||
|
- Базовая команда:
|
||||||
|
`arduino-cli monitor -p /dev/ttyACM0 --config baudrate=115200`
|
||||||
|
- После подключения отправлять одну из команд:
|
||||||
|
`last_error`, `last_diag` или `reg_diag`
|
||||||
|
- Для очистки сохранённой диагностики использовать:
|
||||||
|
`clear_error` или `clear_diag`
|
||||||
|
- При падениях в ветках регистрации и обновления PDA сначала читать именно `last_error`: запись хранится в NVS и может пережить перезагрузку устройства.
|
||||||
|
|||||||
@ -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,8 @@ 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 kBlockTypeRootKey = 1;
|
static const uint8_t kBlockTypeRootKey = 1;
|
||||||
static const uint8_t kBlockTypeClientKey = 2;
|
static const uint8_t kBlockTypeClientKey = 2;
|
||||||
static const uint8_t kBlockTypeBlockchainRegistry = 3;
|
static const uint8_t kBlockTypeBlockchainRegistry = 3;
|
||||||
@ -229,6 +233,7 @@ struct ShinePdaUserState {
|
|||||||
std::vector<String> accessServers;
|
std::vector<String> accessServers;
|
||||||
uint8_t sessionsMode = 1;
|
uint8_t sessionsMode = 1;
|
||||||
uint8_t trustedCount = 0;
|
uint8_t trustedCount = 0;
|
||||||
|
uint8_t recoveryKey32[32] = {};
|
||||||
uint8_t rootKey32[32] = {};
|
uint8_t rootKey32[32] = {};
|
||||||
uint8_t clientKey32[32] = {};
|
uint8_t clientKey32[32] = {};
|
||||||
uint8_t blockchainKey32[32] = {};
|
uint8_t blockchainKey32[32] = {};
|
||||||
@ -320,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;
|
||||||
@ -345,6 +352,9 @@ static unsigned long gLastAccountCheckMs = 0;
|
|||||||
static bool gShowRegisterAccountButton = false;
|
static bool gShowRegisterAccountButton = false;
|
||||||
static bool gShowHomeserverPdaActionButton = false;
|
static bool gShowHomeserverPdaActionButton = false;
|
||||||
static String gHomeserverPdaActionReason;
|
static String gHomeserverPdaActionReason;
|
||||||
|
static bool gCachedAccountPdaValid = false;
|
||||||
|
static String gCachedAccountPdaLogin;
|
||||||
|
static ShinePdaUserState gCachedAccountPdaState;
|
||||||
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";
|
||||||
@ -392,6 +402,8 @@ struct DerivedKeyInfo {
|
|||||||
|
|
||||||
static String gRootPubB58;
|
static String gRootPubB58;
|
||||||
static String gRootPrivB58;
|
static String gRootPrivB58;
|
||||||
|
static String gRecoveryPubB58;
|
||||||
|
static String gRecoveryPrivB58;
|
||||||
static String gBlockchainPubB58;
|
static String gBlockchainPubB58;
|
||||||
static String gBlockchainPrivB58;
|
static String gBlockchainPrivB58;
|
||||||
static String gDevicePubB58;
|
static String gDevicePubB58;
|
||||||
@ -407,6 +419,9 @@ static const int kWalletRpcSignalTypeRequest = 9100;
|
|||||||
static const int kWalletRpcSignalTypeResponse = 9101;
|
static const int kWalletRpcSignalTypeResponse = 9101;
|
||||||
static ActiveWalletSignRequest gActiveWalletSignRequest;
|
static ActiveWalletSignRequest gActiveWalletSignRequest;
|
||||||
static String gWalletSignStatusMessage;
|
static String gWalletSignStatusMessage;
|
||||||
|
static lv_indev_drv_t *gSecretScrollIndevDriver = nullptr;
|
||||||
|
static uint8_t gSecretScrollThrowSaved = 0;
|
||||||
|
static bool gSecretScrollThrowApplied = false;
|
||||||
|
|
||||||
static EditContext gEditContext = EDIT_CONTEXT_NONE;
|
static EditContext gEditContext = EDIT_CONTEXT_NONE;
|
||||||
static Screen gEditReturnScreen = SCREEN_HOME;
|
static Screen gEditReturnScreen = SCREEN_HOME;
|
||||||
@ -517,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 ¶msJson);
|
static String buildBaseRpcRequest(const char *method, const String ¶msJson);
|
||||||
static bool rpcCallSolana(const char *method, const String ¶msJson, String &payloadOut);
|
static bool rpcCallSolana(const char *method, const String ¶msJson, String &payloadOut);
|
||||||
static bool rpcResponseHasError(const String &payload);
|
static bool rpcResponseHasError(const String &payload);
|
||||||
@ -540,7 +560,8 @@ 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 rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t clientPub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
@ -550,7 +571,8 @@ 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 rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t clientPub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
@ -610,6 +632,7 @@ static void loadRegisterDiagDetailsFromPrefs();
|
|||||||
static void saveRegisterDiagDetailsToPrefs(const String &details);
|
static void saveRegisterDiagDetailsToPrefs(const String &details);
|
||||||
static void clearRegisterDiagDetailsFromPrefs();
|
static void clearRegisterDiagDetailsFromPrefs();
|
||||||
static void saveRegisterDiag(const String &status, const String &summary, const String &details);
|
static void saveRegisterDiag(const String &status, const String &summary, const String &details);
|
||||||
|
static void saveRegisterDiagCheckpoint(const String &summary, const String &details);
|
||||||
static void printRegisterDiagToSerial();
|
static void printRegisterDiagToSerial();
|
||||||
static void clearRegisterDiag();
|
static void clearRegisterDiag();
|
||||||
static void handleUsbSerialCommands();
|
static void handleUsbSerialCommands();
|
||||||
@ -877,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;
|
||||||
@ -1206,10 +1242,17 @@ static void deriveKeyPairFromSecretSuffix(const uint8_t *secret32, const String
|
|||||||
if (!secret32) {
|
if (!secret32) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String material = base64Std(secret32, 32) + "|" + suffix;
|
const char *prefix = "SHiNE-key";
|
||||||
uint8_t seed[32] = {};
|
uint8_t seed[32] = {};
|
||||||
uint8_t pub[32] = {};
|
uint8_t pub[32] = {};
|
||||||
sha256calc(reinterpret_cast<const uint8_t *>(material.c_str()), material.length(), seed);
|
std::vector<uint8_t> material;
|
||||||
|
material.reserve(10 + 1 + 32 + 1 + suffix.length());
|
||||||
|
material.insert(material.end(), prefix, prefix + strlen(prefix));
|
||||||
|
material.push_back(0);
|
||||||
|
material.insert(material.end(), secret32, secret32 + 32);
|
||||||
|
material.push_back(0);
|
||||||
|
material.insert(material.end(), suffix.c_str(), suffix.c_str() + suffix.length());
|
||||||
|
sha256calc(material.data(), material.size(), seed);
|
||||||
Ed25519::derivePublicKey(pub, seed);
|
Ed25519::derivePublicKey(pub, seed);
|
||||||
privB58 = base58From32(seed);
|
privB58 = base58From32(seed);
|
||||||
pubB58 = base58From32(pub);
|
pubB58 = base58From32(pub);
|
||||||
@ -1218,6 +1261,8 @@ static void deriveKeyPairFromSecretSuffix(const uint8_t *secret32, const String
|
|||||||
static void clearDerivedKeys() {
|
static void clearDerivedKeys() {
|
||||||
gRootPubB58 = "";
|
gRootPubB58 = "";
|
||||||
gRootPrivB58 = "";
|
gRootPrivB58 = "";
|
||||||
|
gRecoveryPubB58 = "";
|
||||||
|
gRecoveryPrivB58 = "";
|
||||||
gBlockchainPubB58 = "";
|
gBlockchainPubB58 = "";
|
||||||
gBlockchainPrivB58 = "";
|
gBlockchainPrivB58 = "";
|
||||||
gDevicePubB58 = "";
|
gDevicePubB58 = "";
|
||||||
@ -1233,6 +1278,7 @@ static void refreshDerivedKeys() {
|
|||||||
if (!gSecretConfigured) {
|
if (!gSecretConfigured) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
deriveKeyPairFromSecretSuffix(gSecretBytes, "recovery.key", gRecoveryPubB58, gRecoveryPrivB58);
|
||||||
deriveKeyPairFromSecretSuffix(gSecretBytes, "root.key", gRootPubB58, gRootPrivB58);
|
deriveKeyPairFromSecretSuffix(gSecretBytes, "root.key", gRootPubB58, gRootPrivB58);
|
||||||
deriveKeyPairFromSecretSuffix(gSecretBytes, "blockchain.key", gBlockchainPubB58, gBlockchainPrivB58);
|
deriveKeyPairFromSecretSuffix(gSecretBytes, "blockchain.key", gBlockchainPubB58, gBlockchainPrivB58);
|
||||||
deriveKeyPairFromSecretSuffix(gSecretBytes, "client.key", gDevicePubB58, gDevicePrivB58);
|
deriveKeyPairFromSecretSuffix(gSecretBytes, "client.key", gDevicePubB58, gDevicePrivB58);
|
||||||
@ -1310,6 +1356,43 @@ static String selectedWalletPrivateKeyB58() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void secretScrollBoostSet(bool enable) {
|
||||||
|
if (enable) {
|
||||||
|
lv_indev_t *indev = lv_indev_get_act();
|
||||||
|
if (!indev || !indev->driver) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!gSecretScrollThrowApplied) {
|
||||||
|
gSecretScrollIndevDriver = indev->driver;
|
||||||
|
gSecretScrollThrowSaved = gSecretScrollIndevDriver->scroll_throw;
|
||||||
|
gSecretScrollIndevDriver->scroll_throw = (uint8_t)(gSecretScrollThrowSaved / 4);
|
||||||
|
gSecretScrollThrowApplied = true;
|
||||||
|
}
|
||||||
|
} else if (gSecretScrollThrowApplied && gSecretScrollIndevDriver) {
|
||||||
|
gSecretScrollIndevDriver->scroll_throw = gSecretScrollThrowSaved;
|
||||||
|
gSecretScrollIndevDriver = nullptr;
|
||||||
|
gSecretScrollThrowSaved = 0;
|
||||||
|
gSecretScrollThrowApplied = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void secretScrollEventCb(lv_event_t *e) {
|
||||||
|
lv_event_code_t code = lv_event_get_code(e);
|
||||||
|
if (code == LV_EVENT_SCROLL_BEGIN) {
|
||||||
|
lv_anim_t *anim = lv_event_get_scroll_anim(e);
|
||||||
|
if (anim && anim->time > 0) {
|
||||||
|
uint32_t boostedTime = (uint32_t)anim->time / 4;
|
||||||
|
if (boostedTime < 80U) {
|
||||||
|
boostedTime = 80U;
|
||||||
|
}
|
||||||
|
lv_anim_set_time(anim, boostedTime);
|
||||||
|
}
|
||||||
|
secretScrollBoostSet(true);
|
||||||
|
} else if (code == LV_EVENT_SCROLL_END || code == LV_EVENT_DELETE) {
|
||||||
|
secretScrollBoostSet(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static bool selectedWalletAvailable() {
|
static bool selectedWalletAvailable() {
|
||||||
return !selectedWalletPublicKeyB58().isEmpty();
|
return !selectedWalletPublicKeyB58().isEmpty();
|
||||||
}
|
}
|
||||||
@ -1396,6 +1479,9 @@ static void markAccountStateDirty() {
|
|||||||
gHomeserverPdaResultMessage = "";
|
gHomeserverPdaResultMessage = "";
|
||||||
gHomeserverPdaResultDetails = "";
|
gHomeserverPdaResultDetails = "";
|
||||||
gHomeserverPdaResultSuccess = false;
|
gHomeserverPdaResultSuccess = false;
|
||||||
|
gCachedAccountPdaValid = false;
|
||||||
|
gCachedAccountPdaLogin = "";
|
||||||
|
gCachedAccountPdaState = ShinePdaUserState{};
|
||||||
gUserPdaAddress = "";
|
gUserPdaAddress = "";
|
||||||
gRegistrationSignature = "";
|
gRegistrationSignature = "";
|
||||||
gRegisterConfirmMessage = "";
|
gRegisterConfirmMessage = "";
|
||||||
@ -1559,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) {
|
||||||
@ -1568,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#";
|
||||||
}
|
}
|
||||||
@ -1648,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;
|
||||||
@ -1753,7 +1939,11 @@ static std::vector<uint8_t> serializeUnsignedRecordState(const ShinePdaUserState
|
|||||||
pushU32LE(out, state.recordNumber);
|
pushU32LE(out, state.recordNumber);
|
||||||
pushFixed(out, state.prevRecordHash32, 32);
|
pushFixed(out, state.prevRecordHash32, 32);
|
||||||
pushStrU8(out, state.login);
|
pushStrU8(out, state.login);
|
||||||
out.push_back(state.isServer ? 7 : 6);
|
out.push_back(state.isServer ? 8 : 7);
|
||||||
|
|
||||||
|
out.push_back(kBlockTypeRecoveryKey);
|
||||||
|
out.push_back(0);
|
||||||
|
pushFixed(out, state.recoveryKey32, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeRootKey);
|
out.push_back(kBlockTypeRootKey);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
@ -1831,6 +2021,7 @@ static std::vector<uint8_t> buildUpdateInstructionData(const ShinePdaUserState &
|
|||||||
out.reserve(640);
|
out.reserve(640);
|
||||||
out.push_back(4);
|
out.push_back(4);
|
||||||
pushStrU8(out, state.login);
|
pushStrU8(out, state.login);
|
||||||
|
pushFixed(out, state.recoveryKey32, 32);
|
||||||
pushFixed(out, state.rootKey32, 32);
|
pushFixed(out, state.rootKey32, 32);
|
||||||
pushU64LE(out, state.createdAtMs);
|
pushU64LE(out, state.createdAtMs);
|
||||||
pushU64LE(out, updatedAtMs);
|
pushU64LE(out, updatedAtMs);
|
||||||
@ -1880,7 +2071,8 @@ 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 rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t clientPub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
@ -1901,6 +2093,10 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
|
|||||||
pushStrU8(out, login);
|
pushStrU8(out, login);
|
||||||
out.push_back(7);
|
out.push_back(7);
|
||||||
|
|
||||||
|
out.push_back(kBlockTypeRecoveryKey);
|
||||||
|
out.push_back(0);
|
||||||
|
pushFixed(out, recoveryPub, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeRootKey);
|
out.push_back(kBlockTypeRootKey);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, rootPub, 32);
|
pushFixed(out, rootPub, 32);
|
||||||
@ -1922,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);
|
||||||
@ -1952,7 +2143,8 @@ 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 rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t clientPub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
@ -1963,6 +2155,7 @@ static std::vector<uint8_t> buildCreateInstructionData(
|
|||||||
out.reserve(512);
|
out.reserve(512);
|
||||||
out.push_back(3);
|
out.push_back(3);
|
||||||
pushStrU8(out, login);
|
pushStrU8(out, login);
|
||||||
|
pushFixed(out, recoveryPub, 32);
|
||||||
pushFixed(out, rootPub, 32);
|
pushFixed(out, rootPub, 32);
|
||||||
pushU64LE(out, createdAtMs);
|
pushU64LE(out, createdAtMs);
|
||||||
pushU64LE(out, 0);
|
pushU64LE(out, 0);
|
||||||
@ -1974,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);
|
||||||
@ -2608,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()) {
|
||||||
@ -2640,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);
|
||||||
@ -2705,17 +2901,22 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
uint8_t rootSeed[32] = {};
|
uint8_t rootSeed[32] = {};
|
||||||
uint8_t rootPub[32] = {};
|
uint8_t rootPub[32] = {};
|
||||||
uint8_t rootSec[64] = {};
|
uint8_t rootSec[64] = {};
|
||||||
|
uint8_t recoverySeed[32] = {};
|
||||||
|
uint8_t recoveryPub[32] = {};
|
||||||
|
uint8_t recoverySec[64] = {};
|
||||||
uint8_t blockchainSeed[32] = {};
|
uint8_t blockchainSeed[32] = {};
|
||||||
uint8_t blockchainPub[32] = {};
|
uint8_t blockchainPub[32] = {};
|
||||||
uint8_t blockchainSec[64] = {};
|
uint8_t blockchainSec[64] = {};
|
||||||
uint8_t clientSeed[32] = {};
|
uint8_t clientSeed[32] = {};
|
||||||
uint8_t clientPub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t deviceSec[64] = {};
|
uint8_t deviceSec[64] = {};
|
||||||
if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) ||
|
if (!deriveSeedKeypairFromBase58(gRecoveryPrivB58, recoverySeed, recoveryPub, recoverySec) ||
|
||||||
|
!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) ||
|
||||||
!deriveSeedKeypairFromBase58(gBlockchainPrivB58, blockchainSeed, blockchainPub, blockchainSec) ||
|
!deriveSeedKeypairFromBase58(gBlockchainPrivB58, blockchainSeed, blockchainPub, blockchainSec) ||
|
||||||
!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)) {
|
!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)) {
|
||||||
return failWithDiag("Failed to restore keys");
|
return failWithDiag("Failed to restore keys");
|
||||||
}
|
}
|
||||||
|
diagDetails += String("recovery_pub=") + bytesToBase58(recoveryPub, 32) + "\n";
|
||||||
diagDetails += String("root_pub=") + bytesToBase58(rootPub, 32) + "\n";
|
diagDetails += String("root_pub=") + bytesToBase58(rootPub, 32) + "\n";
|
||||||
diagDetails += String("blockchain_pub=") + bytesToBase58(blockchainPub, 32) + "\n";
|
diagDetails += String("blockchain_pub=") + bytesToBase58(blockchainPub, 32) + "\n";
|
||||||
diagDetails += String("device_pub=") + bytesToBase58(clientPub, 32) + "\n";
|
diagDetails += String("device_pub=") + bytesToBase58(clientPub, 32) + "\n";
|
||||||
@ -2732,10 +2933,19 @@ 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,
|
||||||
rootPub, clientPub, blockchainPub,
|
rootPub, clientPub, blockchainPub,
|
||||||
lastBlockSignature, startBonusLimit, createdAtMs);
|
lastBlockSignature, startBonusLimit, createdAtMs);
|
||||||
uint8_t unsignedHash[32];
|
uint8_t unsignedHash[32];
|
||||||
@ -2749,7 +2959,8 @@ 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,
|
||||||
rootPub, clientPub, blockchainPub,
|
rootPub, clientPub, blockchainPub,
|
||||||
lastBlockSignature, rootSignature, createdAtMs);
|
lastBlockSignature, rootSignature, createdAtMs);
|
||||||
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, rootPub, unsignedHash);
|
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, rootPub, unsignedHash);
|
||||||
@ -2819,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";
|
||||||
@ -2983,6 +3194,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
diagDetails += String("login=") + cleanLogin + "\n";
|
diagDetails += String("login=") + cleanLogin + "\n";
|
||||||
diagDetails += String("homeserver=") + gHomeserverValue + "\n";
|
diagDetails += String("homeserver=") + gHomeserverValue + "\n";
|
||||||
diagDetails += String("rpc=") + gSolanaRpcUrl + "\n";
|
diagDetails += String("rpc=") + gSolanaRpcUrl + "\n";
|
||||||
|
saveRegisterDiagCheckpoint("Homeserver PDA update started", diagDetails);
|
||||||
|
|
||||||
if (cleanLogin.isEmpty()) {
|
if (cleanLogin.isEmpty()) {
|
||||||
return failWithDiag("Login is not set");
|
return failWithDiag("Login is not set");
|
||||||
@ -3001,6 +3213,12 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
|
|
||||||
ShinePdaUserState currentState;
|
ShinePdaUserState currentState;
|
||||||
String stateError;
|
String stateError;
|
||||||
|
bool usedCachedPda = false;
|
||||||
|
if (gCachedAccountPdaValid && gCachedAccountPdaLogin == cleanLogin && gCachedAccountPdaState.found) {
|
||||||
|
currentState = gCachedAccountPdaState;
|
||||||
|
usedCachedPda = true;
|
||||||
|
diagDetails += "read_pda_source=cache\n";
|
||||||
|
} else {
|
||||||
if (!readShineUserPda(cleanLogin, currentState, stateError)) {
|
if (!readShineUserPda(cleanLogin, currentState, stateError)) {
|
||||||
diagDetails += String("read_pda_error=") + stateError + "\n";
|
diagDetails += String("read_pda_error=") + stateError + "\n";
|
||||||
return failWithDiag(stateError.isEmpty() ? "Failed to read user PDA" : stateError);
|
return failWithDiag(stateError.isEmpty() ? "Failed to read user PDA" : stateError);
|
||||||
@ -3008,7 +3226,13 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
if (!currentState.found) {
|
if (!currentState.found) {
|
||||||
return failWithDiag("User PDA does not exist yet");
|
return failWithDiag("User PDA does not exist yet");
|
||||||
}
|
}
|
||||||
|
diagDetails += "read_pda_source=rpc\n";
|
||||||
|
}
|
||||||
|
saveRegisterDiagCheckpoint(usedCachedPda ? "Homeserver PDA read from cache" : "Homeserver PDA read", diagDetails);
|
||||||
|
|
||||||
|
uint8_t recoverySeed[32] = {};
|
||||||
|
uint8_t recoveryPub[32] = {};
|
||||||
|
uint8_t recoverySec[64] = {};
|
||||||
uint8_t rootSeed[32] = {};
|
uint8_t rootSeed[32] = {};
|
||||||
uint8_t rootPub[32] = {};
|
uint8_t rootPub[32] = {};
|
||||||
uint8_t rootSec[64] = {};
|
uint8_t rootSec[64] = {};
|
||||||
@ -3016,14 +3240,19 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
uint8_t clientPub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t deviceSec[64] = {};
|
uint8_t deviceSec[64] = {};
|
||||||
uint8_t homeserverPub[32] = {};
|
uint8_t homeserverPub[32] = {};
|
||||||
if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec)
|
if (!deriveSeedKeypairFromBase58(gRecoveryPrivB58, recoverySeed, recoveryPub, recoverySec)
|
||||||
|
|| !deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec)
|
||||||
|| !deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)
|
|| !deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)
|
||||||
|| !base58ToFixed32(gHomeserverPubB58, homeserverPub)) {
|
|| !base58ToFixed32(gHomeserverPubB58, homeserverPub)) {
|
||||||
return failWithDiag("Failed to restore local keys");
|
return failWithDiag("Failed to restore local keys");
|
||||||
}
|
}
|
||||||
|
if (memcmp(recoveryPub, currentState.recoveryKey32, 32) != 0) {
|
||||||
|
return failWithDiag("Recovery key does not match PDA");
|
||||||
|
}
|
||||||
if (memcmp(clientPub, currentState.clientKey32, 32) != 0) {
|
if (memcmp(clientPub, currentState.clientKey32, 32) != 0) {
|
||||||
return failWithDiag("Client key does not match PDA");
|
return failWithDiag("Client key does not match PDA");
|
||||||
}
|
}
|
||||||
|
saveRegisterDiagCheckpoint("Local keys restored", diagDetails);
|
||||||
|
|
||||||
uint8_t userPda[32] = {};
|
uint8_t userPda[32] = {};
|
||||||
uint8_t economyConfig[32] = {};
|
uint8_t economyConfig[32] = {};
|
||||||
@ -3047,6 +3276,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
diagDetails += String("inflow_vault_pda=") + bytesToBase58(inflowVault, 32) + "\n";
|
diagDetails += String("inflow_vault_pda=") + bytesToBase58(inflowVault, 32) + "\n";
|
||||||
|
|
||||||
ShinePdaUserState nextState = currentState;
|
ShinePdaUserState nextState = currentState;
|
||||||
|
memcpy(nextState.recoveryKey32, recoveryPub, 32);
|
||||||
memcpy(nextState.rootKey32, rootPub, 32);
|
memcpy(nextState.rootKey32, rootPub, 32);
|
||||||
memcpy(nextState.clientKey32, clientPub, 32);
|
memcpy(nextState.clientKey32, clientPub, 32);
|
||||||
nextState.updatedAtMs = shineNowMs();
|
nextState.updatedAtMs = shineNowMs();
|
||||||
@ -3075,6 +3305,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
memcpy(rec.sessionPubKey32, homeserverPub, 32);
|
memcpy(rec.sessionPubKey32, homeserverPub, 32);
|
||||||
nextState.sessions.push_back(rec);
|
nextState.sessions.push_back(rec);
|
||||||
}
|
}
|
||||||
|
saveRegisterDiagCheckpoint("Homeserver session merged", diagDetails);
|
||||||
|
|
||||||
std::vector<uint8_t> oldUnsigned = serializeUnsignedRecordState(currentState);
|
std::vector<uint8_t> oldUnsigned = serializeUnsignedRecordState(currentState);
|
||||||
uint8_t prevHash32[32] = {};
|
uint8_t prevHash32[32] = {};
|
||||||
@ -3091,6 +3322,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
}
|
}
|
||||||
diagDetails += String("unsigned_record_len=") + String((unsigned long)newUnsigned.size()) + "\n";
|
diagDetails += String("unsigned_record_len=") + String((unsigned long)newUnsigned.size()) + "\n";
|
||||||
diagDetails += String("unsigned_record_hash=") + bytesToHexString(unsignedHash, 32) + "\n";
|
diagDetails += String("unsigned_record_hash=") + bytesToHexString(unsignedHash, 32) + "\n";
|
||||||
|
saveRegisterDiagCheckpoint("Unsigned update built", diagDetails);
|
||||||
|
|
||||||
std::vector<uint8_t> lastBlockState = buildLastBlockStateBytes(
|
std::vector<uint8_t> lastBlockState = buildLastBlockStateBytes(
|
||||||
cleanLogin,
|
cleanLogin,
|
||||||
@ -3118,6 +3350,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
return failWithDiag(messageOut);
|
return failWithDiag(messageOut);
|
||||||
}
|
}
|
||||||
diagDetails += String("recent_blockhash=") + recentBlockhash58 + "\n";
|
diagDetails += String("recent_blockhash=") + recentBlockhash58 + "\n";
|
||||||
|
saveRegisterDiagCheckpoint("Recent blockhash loaded", diagDetails);
|
||||||
|
|
||||||
std::vector<uint8_t> message = buildUpdateLegacyMessage(
|
std::vector<uint8_t> message = buildUpdateLegacyMessage(
|
||||||
recentBlockhash,
|
recentBlockhash,
|
||||||
@ -3135,6 +3368,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
String txBase64 = encodeTransactionBase64(txSignature, message);
|
String txBase64 = encodeTransactionBase64(txSignature, message);
|
||||||
String signatureB58 = bytesToBase58(txSignature, 64);
|
String signatureB58 = bytesToBase58(txSignature, 64);
|
||||||
diagDetails += String("tx_signature=") + signatureB58 + "\n";
|
diagDetails += String("tx_signature=") + signatureB58 + "\n";
|
||||||
|
saveRegisterDiagCheckpoint("Signed update transaction", diagDetails);
|
||||||
|
|
||||||
String payload;
|
String payload;
|
||||||
if (!rpcCallSolana("sendTransaction", "[\"" + txBase64 + "\",{\"encoding\":\"base64\",\"preflightCommitment\":\"confirmed\"}]", payload)) {
|
if (!rpcCallSolana("sendTransaction", "[\"" + txBase64 + "\",{\"encoding\":\"base64\",\"preflightCommitment\":\"confirmed\"}]", payload)) {
|
||||||
@ -3282,6 +3516,13 @@ static bool parseShineUserPdaBytes(const std::vector<uint8_t> &bytes, ShinePdaUs
|
|||||||
errorOut = "Bad PDA block header";
|
errorOut = "Bad PDA block header";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (blockType == kBlockTypeRecoveryKey) {
|
||||||
|
if (!readBytes(outState.recoveryKey32, 32)) {
|
||||||
|
errorOut = "Bad recovery key block";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (blockType == kBlockTypeRootKey) {
|
if (blockType == kBlockTypeRootKey) {
|
||||||
if (!readBytes(outState.rootKey32, 32)) {
|
if (!readBytes(outState.rootKey32, 32)) {
|
||||||
errorOut = "Bad root key block";
|
errorOut = "Bad root key block";
|
||||||
@ -3492,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;
|
||||||
}
|
}
|
||||||
@ -3502,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;
|
||||||
@ -3514,26 +3755,30 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t recoveryPub[32] = {};
|
||||||
uint8_t rootPub[32] = {};
|
uint8_t rootPub[32] = {};
|
||||||
uint8_t clientPub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t blockchainPub[32] = {};
|
uint8_t blockchainPub[32] = {};
|
||||||
if (!base58ToFixed32(gRootPubB58, rootPub)
|
if (!base58ToFixed32(gRecoveryPubB58, recoveryPub)
|
||||||
|
|| !base58ToFixed32(gRootPubB58, rootPub)
|
||||||
|| !base58ToFixed32(gDevicePubB58, clientPub)
|
|| !base58ToFixed32(gDevicePubB58, clientPub)
|
||||||
|| !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;
|
||||||
}
|
}
|
||||||
|
|
||||||
String mismatch;
|
String mismatch;
|
||||||
if (memcmp(rootPub, pdaState.rootKey32, 32) != 0) {
|
if (memcmp(recoveryPub, pdaState.recoveryKey32, 32) != 0) {
|
||||||
|
mismatch = "recovery key mismatch";
|
||||||
|
} else if (memcmp(rootPub, pdaState.rootKey32, 32) != 0) {
|
||||||
mismatch = "root key mismatch";
|
mismatch = "root key mismatch";
|
||||||
} else if (memcmp(blockchainPub, pdaState.blockchainKey32, 32) != 0) {
|
} else if (memcmp(blockchainPub, pdaState.blockchainKey32, 32) != 0) {
|
||||||
mismatch = "blockchain key mismatch";
|
mismatch = "blockchain key mismatch";
|
||||||
@ -3578,11 +3823,14 @@ 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;
|
||||||
|
gCachedAccountPdaLogin = normalizeLoginValue(gLoginValue);
|
||||||
|
gCachedAccountPdaValid = true;
|
||||||
gAccountPdaStatus = ACCOUNT_PDA_OK;
|
gAccountPdaStatus = ACCOUNT_PDA_OK;
|
||||||
gAccountPdaStatusMessage = "ok";
|
gAccountPdaStatusMessage = "ok";
|
||||||
}
|
}
|
||||||
@ -4219,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";
|
||||||
@ -4233,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()) {
|
||||||
@ -4394,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);
|
||||||
@ -4424,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;
|
||||||
@ -4507,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");
|
||||||
@ -4576,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() {
|
||||||
@ -4730,6 +5002,10 @@ static void saveRegisterDiag(const String &status, const String &summary, const
|
|||||||
saveRegisterDiagDetailsToPrefs(details);
|
saveRegisterDiagDetailsToPrefs(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void saveRegisterDiagCheckpoint(const String &summary, const String &details) {
|
||||||
|
saveRegisterDiag("progress", summary, details);
|
||||||
|
}
|
||||||
|
|
||||||
static void clearRegisterDiag() {
|
static void clearRegisterDiag() {
|
||||||
gLastRegisterDiagStatus = "none";
|
gLastRegisterDiagStatus = "none";
|
||||||
gLastRegisterDiagSummary = "";
|
gLastRegisterDiagSummary = "";
|
||||||
@ -5094,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;
|
||||||
}
|
}
|
||||||
@ -5454,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:
|
||||||
@ -6201,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();
|
||||||
}
|
}
|
||||||
@ -6243,14 +6528,14 @@ static void drawAccountHomeserverScreen() {
|
|||||||
|
|
||||||
static void drawAccountSecretScreen() {
|
static void drawAccountSecretScreen() {
|
||||||
setRootStyle();
|
setRootStyle();
|
||||||
makeTitle("SECRET", 18, &lv_font_montserrat_24);
|
makeTitle("MASTER SECRET", 18, &lv_font_montserrat_24);
|
||||||
showMessageAt(gAccountStatusMessage, 56);
|
showMessageAt(gAccountStatusMessage, 56);
|
||||||
makeButton(gSecretConfigured ? "SHOW SECRET" : "SECRET NOT SET",
|
makeButton(gSecretConfigured ? "SHOW MASTER SECRET" : "MASTER SECRET NOT SET",
|
||||||
22, 118, 436, 84, gSecretConfigured ? 0x2A6F97 : 0x4A5560,
|
22, 118, 436, 84, gSecretConfigured ? 0x2A6F97 : 0x4A5560,
|
||||||
gSecretConfigured ? ACTION_SECRET_SHOW : ACTION_NONE, &lv_font_montserrat_22);
|
gSecretConfigured ? ACTION_SECRET_SHOW : ACTION_NONE, &lv_font_montserrat_22);
|
||||||
makeButton("ENTER SECRET MANUALLY (NOT RECOMMENDED)",
|
makeButton("ENTER MASTER SECRET MANUALLY (NOT RECOMMENDED)",
|
||||||
22, 222, 436, 84, 0x355C7D, ACTION_SECRET_MANUAL, &lv_font_montserrat_18);
|
22, 222, 436, 84, 0x355C7D, ACTION_SECRET_MANUAL, &lv_font_montserrat_18);
|
||||||
makeButton("GENERATE SECRET",
|
makeButton("GENERATE MASTER SECRET",
|
||||||
22, 326, 436, 84, 0x2A9D8F, ACTION_SECRET_GENERATE, &lv_font_montserrat_22);
|
22, 326, 436, 84, 0x2A9D8F, ACTION_SECRET_GENERATE, &lv_font_montserrat_22);
|
||||||
makeBody("Swipe right to return to Account.", 420, 420);
|
makeBody("Swipe right to return to Account.", 420, 420);
|
||||||
makeVersionTag();
|
makeVersionTag();
|
||||||
@ -6258,7 +6543,7 @@ static void drawAccountSecretScreen() {
|
|||||||
|
|
||||||
static void drawSecretShowScreen() {
|
static void drawSecretShowScreen() {
|
||||||
setRootStyle();
|
setRootStyle();
|
||||||
makeTitle("SECRET", 18, &lv_font_montserrat_24);
|
makeTitle("MASTER SECRET", 18, &lv_font_montserrat_24);
|
||||||
if (gSecretConfigured) {
|
if (gSecretConfigured) {
|
||||||
lv_obj_t *panel = lv_obj_create(gRoot);
|
lv_obj_t *panel = lv_obj_create(gRoot);
|
||||||
lv_obj_set_size(panel, 440, 320);
|
lv_obj_set_size(panel, 440, 320);
|
||||||
@ -6268,25 +6553,25 @@ static void drawSecretShowScreen() {
|
|||||||
lv_obj_set_style_border_width(panel, 1, 0);
|
lv_obj_set_style_border_width(panel, 1, 0);
|
||||||
lv_obj_set_style_border_color(panel, lv_color_hex(0x425466), 0);
|
lv_obj_set_style_border_color(panel, lv_color_hex(0x425466), 0);
|
||||||
lv_obj_set_style_radius(panel, 14, 0);
|
lv_obj_set_style_radius(panel, 14, 0);
|
||||||
lv_obj_set_style_pad_all(panel, 14, 0);
|
lv_obj_set_style_pad_top(panel, 14, 0);
|
||||||
|
lv_obj_set_style_pad_bottom(panel, 14, 0);
|
||||||
|
lv_obj_set_style_pad_left(panel, 30, 0);
|
||||||
|
lv_obj_set_style_pad_right(panel, 14, 0);
|
||||||
lv_obj_set_style_pad_row(panel, 8, 0);
|
lv_obj_set_style_pad_row(panel, 8, 0);
|
||||||
lv_obj_set_scroll_dir(panel, LV_DIR_VER);
|
lv_obj_set_scroll_dir(panel, LV_DIR_VER);
|
||||||
lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_ACTIVE);
|
lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_ACTIVE);
|
||||||
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
||||||
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||||
|
lv_obj_add_event_cb(panel, secretScrollEventCb, LV_EVENT_SCROLL_BEGIN, nullptr);
|
||||||
|
lv_obj_add_event_cb(panel, secretScrollEventCb, LV_EVENT_SCROLL_END, nullptr);
|
||||||
|
lv_obj_add_event_cb(panel, secretScrollEventCb, LV_EVENT_DELETE, nullptr);
|
||||||
|
|
||||||
auto addKeyBlock = [&](const String &title, const String &formula, const String &value) {
|
auto addKeyBlock = [&](const String &title, const String &value) {
|
||||||
lv_obj_t *titleLabel = lv_label_create(panel);
|
lv_obj_t *titleLabel = lv_label_create(panel);
|
||||||
lv_label_set_text(titleLabel, title.c_str());
|
lv_label_set_text(titleLabel, title.c_str());
|
||||||
lv_obj_set_width(titleLabel, 400);
|
lv_obj_set_width(titleLabel, 400);
|
||||||
lv_obj_set_style_text_font(titleLabel, &lv_font_montserrat_18, 0);
|
lv_obj_set_style_text_font(titleLabel, &lv_font_montserrat_18, 0);
|
||||||
lv_obj_set_style_text_color(titleLabel, lv_color_hex(0xFFFFFF), 0);
|
lv_obj_set_style_text_color(titleLabel, lv_color_hex(0xA7D8FF), 0);
|
||||||
|
|
||||||
lv_obj_t *formulaLabel = lv_label_create(panel);
|
|
||||||
lv_label_set_text(formulaLabel, formula.c_str());
|
|
||||||
lv_obj_set_width(formulaLabel, 400);
|
|
||||||
lv_obj_set_style_text_font(formulaLabel, &lv_font_montserrat_12, 0);
|
|
||||||
lv_obj_set_style_text_color(formulaLabel, lv_color_hex(0x8FA4B8), 0);
|
|
||||||
|
|
||||||
lv_obj_t *valueLabel = lv_label_create(panel);
|
lv_obj_t *valueLabel = lv_label_create(panel);
|
||||||
lv_label_set_text(valueLabel, value.c_str());
|
lv_label_set_text(valueLabel, value.c_str());
|
||||||
@ -6296,15 +6581,17 @@ static void drawSecretShowScreen() {
|
|||||||
lv_obj_set_style_text_color(valueLabel, lv_color_hex(0xD9E1EA), 0);
|
lv_obj_set_style_text_color(valueLabel, lv_color_hex(0xD9E1EA), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
addKeyBlock("Secret (base58)", "master secret", gSecretBase58);
|
addKeyBlock("Master Secret", gSecretBase58);
|
||||||
addKeyBlock("Root key (base58)", "pub from sha256(base64(secret)|root.key)", gRootPubB58);
|
addKeyBlock("Recovery key", gRecoveryPubB58);
|
||||||
addKeyBlock("Root key priv (base58)", "sha256(base64(secret)|root.key)", gRootPrivB58);
|
addKeyBlock("Recovery key priv", gRecoveryPrivB58);
|
||||||
addKeyBlock("Blockchain key (base58)", "pub from sha256(base64(secret)|bch.key)", gBlockchainPubB58);
|
addKeyBlock("Root key", gRootPubB58);
|
||||||
addKeyBlock("Blockchain key priv (base58)", "sha256(base64(secret)|bch.key)", gBlockchainPrivB58);
|
addKeyBlock("Root key priv", gRootPrivB58);
|
||||||
addKeyBlock("Client key (base58)", "pub from sha256(base64(secret)|client.key)", gDevicePubB58);
|
addKeyBlock("Blockchain key", gBlockchainPubB58);
|
||||||
addKeyBlock("Client key priv (base58)", "sha256(base64(secret)|client.key)", gDevicePrivB58);
|
addKeyBlock("Blockchain key priv", gBlockchainPrivB58);
|
||||||
addKeyBlock("Homeserver key (base58)", String("pub from sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPubB58);
|
addKeyBlock("Client key", gDevicePubB58);
|
||||||
addKeyBlock("Homeserver key priv (base58)", String("sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPrivB58);
|
addKeyBlock("Client key priv", gDevicePrivB58);
|
||||||
|
addKeyBlock("Homeserver key", gHomeserverPubB58);
|
||||||
|
addKeyBlock("Homeserver key priv", gHomeserverPrivB58);
|
||||||
} else {
|
} else {
|
||||||
showMessageAt("Secret not set", 96);
|
showMessageAt("Secret not set", 96);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
#include "shine_secret_generation.h"
|
#include "shine_secret_generation.h"
|
||||||
|
|
||||||
#include <SD_MMC.h>
|
#include <SD_MMC.h>
|
||||||
#include <Preferences.h>
|
|
||||||
#include <mbedtls/sha256.h>
|
#include <mbedtls/sha256.h>
|
||||||
#include <mbedtls/base64.h>
|
#include <mbedtls/base64.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@ -80,6 +79,19 @@ static void setMessage(const char *message) {
|
|||||||
snprintf(gMessage, sizeof(gMessage), "%s", message ? message : "");
|
snprintf(gMessage, sizeof(gMessage), "%s", message ? message : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32);
|
||||||
|
|
||||||
|
static void finishSecretFromBytes(const uint8_t secret32[32], const char *message) {
|
||||||
|
memcpy(gSecret, secret32, 32);
|
||||||
|
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
|
||||||
|
gDone = true;
|
||||||
|
gRunning = false;
|
||||||
|
gError = false;
|
||||||
|
gInitDone = false;
|
||||||
|
gDoneBlocks = TOTAL_FILLS;
|
||||||
|
setMessage(message ? message : "Secret generated");
|
||||||
|
}
|
||||||
|
|
||||||
static void b2_compress(B2State *S, const uint8_t *blk) {
|
static void b2_compress(B2State *S, const uint8_t *blk) {
|
||||||
uint64_t m[16], v[16];
|
uint64_t m[16], v[16];
|
||||||
for (int i = 0; i < 16; i++) m[i] = ((const uint64_t *)blk)[i];
|
for (int i = 0; i < 16; i++) m[i] = ((const uint64_t *)blk)[i];
|
||||||
@ -449,7 +461,6 @@ bool shineSecretInitSd(String &error) {
|
|||||||
|
|
||||||
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error) {
|
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error) {
|
||||||
error = "";
|
error = "";
|
||||||
if (!shineSecretInitSd(error)) return false;
|
|
||||||
if (!normalizedLogin || !*normalizedLogin) {
|
if (!normalizedLogin || !*normalizedLogin) {
|
||||||
error = "login not set";
|
error = "login not set";
|
||||||
return false;
|
return false;
|
||||||
@ -463,6 +474,8 @@ bool shineSecretStart(const char *normalizedLogin, const char *password, String
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!shineSecretInitSd(error)) return false;
|
||||||
|
|
||||||
if (gSdFile) gSdFile.close();
|
if (gSdFile) gSdFile.close();
|
||||||
SD_MMC.remove(SD_MEM_FILE);
|
SD_MMC.remove(SD_MEM_FILE);
|
||||||
gSdFile = SD_MMC.open(SD_MEM_FILE, "w+");
|
gSdFile = SD_MMC.open(SD_MEM_FILE, "w+");
|
||||||
|
|||||||
@ -224,12 +224,14 @@ static int16_t gTouchLastY = 0;
|
|||||||
struct DerivedKeyState {
|
struct DerivedKeyState {
|
||||||
bool ready;
|
bool ready;
|
||||||
uint8_t masterSecret[32];
|
uint8_t masterSecret[32];
|
||||||
|
uint8_t recoveryPub[32];
|
||||||
|
uint8_t recoverySk[64];
|
||||||
uint8_t rootPub[32];
|
uint8_t rootPub[32];
|
||||||
uint8_t rootSk[64];
|
uint8_t rootSk[64];
|
||||||
uint8_t blockchainPub[32];
|
uint8_t blockchainPub[32];
|
||||||
uint8_t blockchainSk[64];
|
uint8_t blockchainSk[64];
|
||||||
uint8_t clientPub[32];
|
uint8_t clientPub[32];
|
||||||
uint8_t deviceSk[64];
|
uint8_t clientSk[64];
|
||||||
};
|
};
|
||||||
|
|
||||||
static DerivedKeyState gDerivedKeys = {};
|
static DerivedKeyState gDerivedKeys = {};
|
||||||
@ -782,19 +784,20 @@ static void pushFixed(std::vector<uint8_t> &out, const uint8_t *data, size_t len
|
|||||||
static bool deriveKeysFromMasterSecret(const uint8_t masterSecret[32]) {
|
static bool deriveKeysFromMasterSecret(const uint8_t masterSecret[32]) {
|
||||||
memset(&gDerivedKeys, 0, sizeof(gDerivedKeys));
|
memset(&gDerivedKeys, 0, sizeof(gDerivedKeys));
|
||||||
memcpy(gDerivedKeys.masterSecret, masterSecret, 32);
|
memcpy(gDerivedKeys.masterSecret, masterSecret, 32);
|
||||||
String secretB64 = base64Encode(masterSecret, 32);
|
const char *prefix = "SHiNE-key";
|
||||||
if (secretB64.length() == 0) {
|
const char *suffixes[4] = {"recovery.key", "root.key", "blockchain.key", "client.key"};
|
||||||
return false;
|
uint8_t *pubs[4] = {gDerivedKeys.recoveryPub, gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.clientPub};
|
||||||
}
|
uint8_t *sks[4] = {gDerivedKeys.recoverySk, gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.clientSk};
|
||||||
const char *suffixes[3] = {"root.key", "blockchain.key", "client.key"};
|
for (int i = 0; i < 4; i++) {
|
||||||
uint8_t *pubs[3] = {gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.clientPub};
|
std::vector<uint8_t> material;
|
||||||
uint8_t *sks[3] = {gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.deviceSk};
|
material.reserve(strlen(prefix) + 1 + 32 + 1 + strlen(suffixes[i]));
|
||||||
for (int i = 0; i < 3; i++) {
|
material.insert(material.end(), prefix, prefix + strlen(prefix));
|
||||||
String material = secretB64 + "|" + suffixes[i];
|
material.push_back(0);
|
||||||
|
material.insert(material.end(), masterSecret, masterSecret + 32);
|
||||||
|
material.push_back(0);
|
||||||
|
material.insert(material.end(), suffixes[i], suffixes[i] + strlen(suffixes[i]));
|
||||||
uint8_t seed[32];
|
uint8_t seed[32];
|
||||||
if (!sha256String(material, seed)) {
|
sha256Raw(material.data(), material.size(), seed);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (crypto_sign_seed_keypair(pubs[i], sks[i], seed) != 0) {
|
if (crypto_sign_seed_keypair(pubs[i], sks[i], seed) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1277,7 +1280,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
edBchData,
|
edBchData,
|
||||||
createData);
|
createData);
|
||||||
uint8_t txSignature[64];
|
uint8_t txSignature[64];
|
||||||
if (!signMessageEd25519(message, gDerivedKeys.deviceSk, txSignature)) {
|
if (!signMessageEd25519(message, gDerivedKeys.clientSk, txSignature)) {
|
||||||
messageOut = "Не удалось подписать Solana-транзакцию";
|
messageOut = "Не удалось подписать Solana-транзакцию";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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`
|
||||||
|
|
||||||
## Хранение аккаунта
|
## Хранение аккаунта
|
||||||
|
|
||||||
|
|||||||
@ -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`.
|
||||||
|
|
||||||
## Сценарий входящего запроса
|
## Сценарий входящего запроса
|
||||||
|
|
||||||
|
|||||||
@ -167,7 +167,9 @@ public final class SolanaUserPdaImportService {
|
|||||||
int blockVer = u8(raw, c++);
|
int blockVer = u8(raw, c++);
|
||||||
if (blockVer != 0) return null;
|
if (blockVer != 0) return null;
|
||||||
|
|
||||||
if (blockType == 1) {
|
if (blockType == 0) {
|
||||||
|
c += 32; // recovery_key
|
||||||
|
} else if (blockType == 1) {
|
||||||
c += 32;
|
c += 32;
|
||||||
} else if (blockType == 2) {
|
} else if (blockType == 2) {
|
||||||
clientKey32 = slice(raw, c, 32);
|
clientKey32 = slice(raw, c, 32);
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.234
|
client.version=1.2.246
|
||||||
server.version=1.2.220
|
server.version=1.2.231
|
||||||
|
|||||||
7
shine-UI/js/deploy-config.js
Normal file
7
shine-UI/js/deploy-config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Конфиг по умолчанию для конкретного деплоя UI.
|
||||||
|
// Перед выкладкой на другой хост можно поменять этот файл без правки логики приложения.
|
||||||
|
|
||||||
|
export const defaultServerLogin = 'shineupme';
|
||||||
|
export const defaultServerAddress = 'shineup.me';
|
||||||
|
export const defaultServerHttp = `https://${defaultServerAddress}`;
|
||||||
|
export const defaultServerWs = `wss://${defaultServerAddress}/ws`;
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
uploadArweaveFile,
|
uploadArweaveFile,
|
||||||
validateAvatarSourceFile,
|
validateAvatarSourceFile,
|
||||||
} from '../services/arweave-file-service.js';
|
} from '../services/arweave-file-service.js';
|
||||||
|
import { defaultServerAddress, defaultServerHttp } from '../deploy-config.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'developer-settings-view', title: 'Настройки разработчика' };
|
export const pageMeta = { id: 'developer-settings-view', title: 'Настройки разработчика' };
|
||||||
|
|
||||||
@ -207,9 +208,9 @@ function showClientUpdateHelp() {
|
|||||||
window.alert(
|
window.alert(
|
||||||
'Если UI не обновился:\n\n'
|
'Если UI не обновился:\n\n'
|
||||||
+ '1) Закройте вкладки с SHiNE.\n'
|
+ '1) Закройте вкладки с SHiNE.\n'
|
||||||
+ '2) Откройте chrome://settings/siteData и удалите данные для shineup.me.\n'
|
+ `2) Откройте chrome://settings/siteData и удалите данные для ${defaultServerAddress}.\n`
|
||||||
+ '3) Если приложение установлено как PWA — удалите его с устройства.\n'
|
+ '3) Если приложение установлено как PWA — удалите его с устройства.\n'
|
||||||
+ '4) Откройте https://shineup.me заново и выполните вход.\n'
|
+ `4) Откройте ${defaultServerHttp} заново и выполните вход.\n`
|
||||||
+ '5) Если всё ещё старая версия — откройте в режиме инкогнито и проверьте версию в Настройки -> Версии.'
|
+ '5) Если всё ещё старая версия — откройте в режиме инкогнито и проверьте версию в Настройки -> Версии.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,6 +100,7 @@ export function render({ navigate }) {
|
|||||||
onInput: (index, value) => {
|
onInput: (index, value) => {
|
||||||
passwordWords[index] = value;
|
passwordWords[index] = value;
|
||||||
syncDraftState();
|
syncDraftState();
|
||||||
|
updateWordsPreview();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -165,7 +166,10 @@ export function render({ navigate }) {
|
|||||||
syncDraftState();
|
syncDraftState();
|
||||||
|
|
||||||
loginInput.addEventListener('input', syncDraftState);
|
loginInput.addEventListener('input', syncDraftState);
|
||||||
passwordInput.addEventListener('input', syncDraftState);
|
passwordInput.addEventListener('input', () => {
|
||||||
|
syncDraftState();
|
||||||
|
updateWordsPreview();
|
||||||
|
});
|
||||||
|
|
||||||
passwordModeCheckbox.addEventListener('change', () => {
|
passwordModeCheckbox.addEventListener('change', () => {
|
||||||
const nextMode = passwordModeCheckbox.checked ? 'words' : 'single';
|
const nextMode = passwordModeCheckbox.checked ? 'words' : 'single';
|
||||||
@ -181,6 +185,7 @@ export function render({ navigate }) {
|
|||||||
}
|
}
|
||||||
passwordMode = nextMode;
|
passwordMode = nextMode;
|
||||||
updatePasswordModeVisibility();
|
updatePasswordModeVisibility();
|
||||||
|
updateWordsPreview();
|
||||||
syncDraftState();
|
syncDraftState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
PASSWORD_WORDS_COUNT,
|
PASSWORD_WORDS_COUNT,
|
||||||
} from '../services/password-words.js';
|
} from '../services/password-words.js';
|
||||||
import { openRegistrationFaq } from './registration-faq-view.js';
|
import { openRegistrationFaq } from './registration-faq-view.js';
|
||||||
|
import { defaultServerHttp, defaultServerLogin } from '../deploy-config.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false };
|
export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false };
|
||||||
|
|
||||||
@ -100,6 +101,7 @@ export function render({ navigate }) {
|
|||||||
onInput: (index, value) => {
|
onInput: (index, value) => {
|
||||||
passwordWords[index] = value;
|
passwordWords[index] = value;
|
||||||
syncDraftState();
|
syncDraftState();
|
||||||
|
updateWordsPreview();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -123,8 +125,8 @@ export function render({ navigate }) {
|
|||||||
serverNotice.className = 'card stack';
|
serverNotice.className = 'card stack';
|
||||||
serverNotice.innerHTML = `
|
serverNotice.innerHTML = `
|
||||||
<p class="field-label">Первый сервер SHiNE</p>
|
<p class="field-label">Первый сервер SHiNE</p>
|
||||||
<p class="meta-muted">Сейчас вашим первым и основным сервером будет серверный аккаунт <strong>${state.entrySettings.shineServerLogin || 'shineupme'}</strong>.</p>
|
<p class="meta-muted">Сейчас вашим первым и основным сервером будет серверный аккаунт <strong>${state.entrySettings.shineServerLogin || defaultServerLogin}</strong>.</p>
|
||||||
<p class="meta-muted">Текущий адрес этого сервера: <strong>${state.entrySettings.shineServerHttp || 'https://shineup.me'}</strong>.</p>
|
<p class="meta-muted">Текущий адрес этого сервера: <strong>${state.entrySettings.shineServerHttp || defaultServerHttp}</strong>.</p>
|
||||||
<p class="meta-muted">Это первый сервер, на который вам будут писать и звонить после регистрации. Позже его можно будет сменить, а в будущем использовать и несколько серверов сразу.</p>
|
<p class="meta-muted">Это первый сервер, на который вам будут писать и звонить после регистрации. Позже его можно будет сменить, а в будущем использовать и несколько серверов сразу.</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -299,6 +301,7 @@ export function render({ navigate }) {
|
|||||||
|
|
||||||
passwordInput.addEventListener('input', () => {
|
passwordInput.addEventListener('input', () => {
|
||||||
syncDraftState();
|
syncDraftState();
|
||||||
|
updateWordsPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
passwordModeCheckbox.addEventListener('change', () => {
|
passwordModeCheckbox.addEventListener('change', () => {
|
||||||
@ -315,6 +318,7 @@ export function render({ navigate }) {
|
|||||||
}
|
}
|
||||||
passwordMode = nextMode;
|
passwordMode = nextMode;
|
||||||
updatePasswordModeVisibility();
|
updatePasswordModeVisibility();
|
||||||
|
updateWordsPreview();
|
||||||
syncDraftState();
|
syncDraftState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
isUserAlreadyExistsSolanaError,
|
isUserAlreadyExistsSolanaError,
|
||||||
registerUserOnSolana,
|
registerUserOnSolana,
|
||||||
} from '../services/solana-register-service.js';
|
} from '../services/solana-register-service.js';
|
||||||
|
import { defaultServerLogin } from '../deploy-config.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
||||||
const MIN_REQUIRED_SOL = 0.01;
|
const MIN_REQUIRED_SOL = 0.01;
|
||||||
@ -194,7 +195,7 @@ export function render({ navigate }) {
|
|||||||
login: state.registrationDraft.login,
|
login: state.registrationDraft.login,
|
||||||
keyBundle,
|
keyBundle,
|
||||||
solanaEndpoint: state.entrySettings.solanaServer,
|
solanaEndpoint: state.entrySettings.solanaServer,
|
||||||
accessServers: [state.entrySettings.shineServerLogin || 'shineupme'],
|
accessServers: [state.entrySettings.shineServerLogin || defaultServerLogin],
|
||||||
});
|
});
|
||||||
} catch (solanaError) {
|
} catch (solanaError) {
|
||||||
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
saveEncryptedUserSecrets,
|
saveEncryptedUserSecrets,
|
||||||
saveSessionMaterial,
|
saveSessionMaterial,
|
||||||
} from './key-vault.js';
|
} from './key-vault.js';
|
||||||
|
import { defaultServerWs } from '../deploy-config.js';
|
||||||
|
|
||||||
const BCH_SUFFIX = '001';
|
const BCH_SUFFIX = '001';
|
||||||
const ZERO64 = '0'.repeat(64);
|
const ZERO64 = '0'.repeat(64);
|
||||||
@ -73,7 +74,7 @@ const CONNECTION_SUBTYPES = Object.freeze({
|
|||||||
|
|
||||||
function normalizeServerUrl(url) {
|
function normalizeServerUrl(url) {
|
||||||
const value = (url || '').trim();
|
const value = (url || '').trim();
|
||||||
if (!value) return 'wss://shineup.me/ws';
|
if (!value) return defaultServerWs;
|
||||||
if (value.startsWith('ws://') || value.startsWith('wss://')) {
|
if (value.startsWith('ws://') || value.startsWith('wss://')) {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(value);
|
const parsed = new URL(value);
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
import { readShineUserPda } from './shine-user-pda-service.js';
|
import { readShineUserPda } from './shine-user-pda-service.js';
|
||||||
|
import {
|
||||||
|
defaultServerAddress,
|
||||||
|
defaultServerHttp,
|
||||||
|
defaultServerLogin,
|
||||||
|
defaultServerWs,
|
||||||
|
} from '../deploy-config.js';
|
||||||
|
|
||||||
export const DEFAULT_SHINE_SERVER_LOGIN = 'shineupme';
|
export const DEFAULT_SHINE_SERVER_LOGIN = defaultServerLogin;
|
||||||
export const DEFAULT_SHINE_SERVER_ADDRESS = 'shineup.me';
|
export const DEFAULT_SHINE_SERVER_ADDRESS = defaultServerAddress;
|
||||||
export const DEFAULT_SHINE_SERVER_HTTP = `https://${DEFAULT_SHINE_SERVER_ADDRESS}`;
|
export const DEFAULT_SHINE_SERVER_HTTP = defaultServerHttp;
|
||||||
export const DEFAULT_SHINE_SERVER_WS = `wss://${DEFAULT_SHINE_SERVER_ADDRESS}/ws`;
|
export const DEFAULT_SHINE_SERVER_WS = defaultServerWs;
|
||||||
|
|
||||||
function normalizeHostLike(value) {
|
function normalizeHostLike(value) {
|
||||||
const raw = String(value || '').trim();
|
const raw = String(value || '').trim();
|
||||||
|
|||||||
@ -623,6 +623,38 @@ export async function readShineUserPda({ login, solanaEndpoint }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readShineUserPdaByAddress({ pdaAddress, solanaEndpoint }) {
|
||||||
|
const address = String(pdaAddress || '').trim();
|
||||||
|
const endpoint = String(solanaEndpoint || '').trim();
|
||||||
|
if (!address) throw new Error('Не указан адрес PDA');
|
||||||
|
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
|
||||||
|
const solana = await loadSolanaLib();
|
||||||
|
const connection = new solana.Connection(endpoint, 'confirmed');
|
||||||
|
const accountAddress = new solana.PublicKey(address);
|
||||||
|
const accountInfo = await connection.getAccountInfo(accountAddress, 'confirmed');
|
||||||
|
if (!accountInfo?.data) throw new Error(`PDA не найдена: ${address}`);
|
||||||
|
return {
|
||||||
|
...parseShineUserPda(accountInfo.data),
|
||||||
|
userPda: accountAddress.toBase58(),
|
||||||
|
pdaAddress: accountAddress.toBase58(),
|
||||||
|
endpoint,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readShineUserPdaByRef({ value, solanaEndpoint }) {
|
||||||
|
const ref = String(value || '').trim();
|
||||||
|
if (!ref) throw new Error('Не указан логин или адрес PDA');
|
||||||
|
try {
|
||||||
|
const bytes = base58ToBytes(ref);
|
||||||
|
if (bytes.length === 32) {
|
||||||
|
return await readShineUserPdaByAddress({ pdaAddress: ref, solanaEndpoint });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Если это не адрес, читаем как логин.
|
||||||
|
}
|
||||||
|
return readShineUserPda({ login: ref, solanaEndpoint });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
|
export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
|
||||||
const parsed = await readShineUserPda({ login, solanaEndpoint });
|
const parsed = await readShineUserPda({ login, solanaEndpoint });
|
||||||
const bch = parsed.blockchain;
|
const bch = parsed.blockchain;
|
||||||
@ -653,6 +685,28 @@ function parseHex32(value) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function attachSolanaLogs(error, connection) {
|
||||||
|
if (!error || typeof error.getLogs !== 'function' || !connection) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logs = await error.getLogs(connection);
|
||||||
|
if (Array.isArray(logs) && logs.length) {
|
||||||
|
error.logs = logs;
|
||||||
|
error.transactionLogs = logs;
|
||||||
|
error.simulationLogs = logs;
|
||||||
|
if (!String(error.message || '').includes('Logs:')) {
|
||||||
|
error.message = `${String(error.message || 'Solana transaction failed')} :: Logs: ${logs.join(' | ')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Если RPC не вернул логи, оставляем исходную ошибку как есть.
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
|
async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
|
||||||
const cleanLogin = normalizeLogin(login);
|
const cleanLogin = normalizeLogin(login);
|
||||||
const endpoint = String(solanaEndpoint || '').trim();
|
const endpoint = String(solanaEndpoint || '').trim();
|
||||||
@ -816,12 +870,17 @@ async function createShineUserPdaOnSolana({
|
|||||||
data: ixData,
|
data: ixData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const signature = await ctx.solana.sendAndConfirmTransaction(
|
let signature;
|
||||||
|
try {
|
||||||
|
signature = await ctx.solana.sendAndConfirmTransaction(
|
||||||
ctx.connection,
|
ctx.connection,
|
||||||
new ctx.solana.Transaction().add(ed25519RootIx, ed25519BchIx, createIx),
|
new ctx.solana.Transaction().add(ed25519RootIx, ed25519BchIx, createIx),
|
||||||
[ctx.clientKeypair],
|
[ctx.clientKeypair],
|
||||||
{ commitment: 'confirmed' },
|
{ commitment: 'confirmed' },
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw await attachSolanaLogs(error, ctx.connection);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
signature,
|
signature,
|
||||||
@ -1030,12 +1089,17 @@ export async function updateShineUserPdaOnSolana({
|
|||||||
const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 800_000 });
|
const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 800_000 });
|
||||||
const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 262_144 });
|
const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 262_144 });
|
||||||
|
|
||||||
const signature = await solana.sendAndConfirmTransaction(
|
let signature;
|
||||||
|
try {
|
||||||
|
signature = await solana.sendAndConfirmTransaction(
|
||||||
connection,
|
connection,
|
||||||
new solana.Transaction().add(computeIx, heapIx, edIxRoot, edIxBch, updateIx),
|
new solana.Transaction().add(computeIx, heapIx, edIxRoot, edIxBch, updateIx),
|
||||||
[clientKeypair],
|
[clientKeypair],
|
||||||
{ commitment: 'confirmed' },
|
{ commitment: 'confirmed' },
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw await attachSolanaLogs(error, connection);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
signature,
|
signature,
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { captureClientError } from './client-error-reporter.js';
|
import { captureClientError } from './client-error-reporter.js';
|
||||||
|
import { defaultServerWs } from '../deploy-config.js';
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT_MS = 12000;
|
const DEFAULT_TIMEOUT_MS = 12000;
|
||||||
|
|
||||||
function buildWsUrl(raw) {
|
function buildWsUrl(raw) {
|
||||||
const value = (raw || '').trim();
|
const value = (raw || '').trim();
|
||||||
if (!value) return 'wss://shineup.me/ws';
|
if (!value) return defaultServerWs;
|
||||||
if (value.startsWith('/')) {
|
if (value.startsWith('/')) {
|
||||||
const secure = window.location.protocol === 'https:';
|
const secure = window.location.protocol === 'https:';
|
||||||
const scheme = secure ? 'wss' : 'ws';
|
const scheme = secure ? 'wss' : 'ws';
|
||||||
|
|||||||
@ -28,6 +28,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<a href="server-ui/read-pda.html">
|
||||||
|
<button class="btn-secondary" style="width:100%">
|
||||||
|
Просмотреть любой PDA
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="../server-ui.html">← Назад</a>
|
<a href="../server-ui.html">← Назад</a>
|
||||||
<a href="update-server-pda.html">Обновить PDA</a>
|
<a href="update-server-pda.html">Обновить PDA</a>
|
||||||
|
<a href="read-pda.html">Просмотреть PDA</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Регистрация серверного аккаунта</h1>
|
<h1>Регистрация серверного аккаунта</h1>
|
||||||
@ -56,12 +57,12 @@
|
|||||||
<h2>Данные сервера</h2>
|
<h2>Данные сервера</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Логин сервера</label>
|
<label>Логин сервера</label>
|
||||||
<input type="text" id="login" placeholder="shineupme" maxlength="20" />
|
<input type="text" id="login" placeholder="Логин сервера" maxlength="20" />
|
||||||
<div class="hint">Только a-z, 0-9, _ · без точки · макс. 20 символов</div>
|
<div class="hint">Только a-z, 0-9, _ · без точки · макс. 20 символов</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Адрес сервера (URL)</label>
|
<label>Адрес сервера (URL)</label>
|
||||||
<input type="text" id="serverAddress" placeholder="https://shineup.me/ws" />
|
<input type="text" id="serverAddress" placeholder="Адрес сервера" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Серверы синхронизации (sync_servers)</label>
|
<label>Серверы синхронизации (sync_servers)</label>
|
||||||
@ -97,6 +98,11 @@
|
|||||||
|
|
||||||
<div class="sec-lbl">Ключевые пары (base58)</div>
|
<div class="sec-lbl">Ключевые пары (base58)</div>
|
||||||
|
|
||||||
|
<div class="kp-block">
|
||||||
|
<div class="kp-title">Recovery Key — восстановление аккаунта</div>
|
||||||
|
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="recoveryPub" placeholder="base58, ~44 символа" /></div>
|
||||||
|
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="recoveryPriv" placeholder="seed base58, ~44 символа" /></div>
|
||||||
|
</div>
|
||||||
<div class="kp-block">
|
<div class="kp-block">
|
||||||
<div class="kp-title">Root Key — подпись PDA-записи</div>
|
<div class="kp-title">Root Key — подпись PDA-записи</div>
|
||||||
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="rootPub" placeholder="base58, ~44 символа" /></div>
|
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="rootPub" placeholder="base58, ~44 символа" /></div>
|
||||||
|
|||||||
@ -16,9 +16,12 @@ import {
|
|||||||
validateLoginOrThrow,
|
validateLoginOrThrow,
|
||||||
wireDeviceAddressPreview,
|
wireDeviceAddressPreview,
|
||||||
} from './server-ui-shared.js';
|
} from './server-ui-shared.js';
|
||||||
|
import { defaultServerAddress, defaultServerLogin } from '../../js/deploy-config.js';
|
||||||
|
|
||||||
const fieldMap = {
|
const fieldMap = {
|
||||||
masterSecret: 'masterSecret',
|
masterSecret: 'masterSecret',
|
||||||
|
recoveryPub: 'recoveryPub',
|
||||||
|
recoveryPriv: 'recoveryPriv',
|
||||||
rootPub: 'rootPub',
|
rootPub: 'rootPub',
|
||||||
rootPriv: 'rootPriv',
|
rootPriv: 'rootPriv',
|
||||||
bchPub: 'bchPub',
|
bchPub: 'bchPub',
|
||||||
@ -32,6 +35,8 @@ const fieldMap = {
|
|||||||
setupPasswordEye($('btnEye'), $('password'));
|
setupPasswordEye($('btnEye'), $('password'));
|
||||||
wireDeviceAddressPreview(fieldMap);
|
wireDeviceAddressPreview(fieldMap);
|
||||||
$('password').value = '';
|
$('password').value = '';
|
||||||
|
$('login').placeholder = defaultServerLogin;
|
||||||
|
$('serverAddress').placeholder = defaultServerAddress;
|
||||||
|
|
||||||
$('btnTopupDevnet').addEventListener('click', () => {
|
$('btnTopupDevnet').addEventListener('click', () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
242
shine-UI/server-ui/js/read-pda-page.js
Normal file
242
shine-UI/server-ui/js/read-pda-page.js
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import { readShineUserPdaByRef } from '../../js/services/shine-user-pda-service.js';
|
||||||
|
import { bytesToBase58 } from '../../js/services/crypto-utils.js';
|
||||||
|
import { $, clearStatus, formatBigInt, formatTimestamp, setStatus } from './server-ui-shared.js';
|
||||||
|
|
||||||
|
function hex(bytes) {
|
||||||
|
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||||||
|
return Array.from(data).map((x) => x.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function base58(bytes) {
|
||||||
|
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||||||
|
return bytesToBase58(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRows(rows) {
|
||||||
|
const container = $('summaryRows');
|
||||||
|
container.innerHTML = '';
|
||||||
|
for (const row of rows) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'field-row';
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'field-label';
|
||||||
|
label.textContent = row.label;
|
||||||
|
const value = document.createElement('div');
|
||||||
|
value.className = `field-value${row.muted ? ' muted' : ''}`;
|
||||||
|
value.textContent = row.value;
|
||||||
|
el.append(label, value);
|
||||||
|
container.appendChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMiniGrid(items) {
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'mini-grid';
|
||||||
|
for (const item of items) {
|
||||||
|
const node = document.createElement('div');
|
||||||
|
node.className = 'mini-item';
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'mini-label';
|
||||||
|
label.textContent = item.label;
|
||||||
|
const value = document.createElement('div');
|
||||||
|
value.className = 'mini-value';
|
||||||
|
value.textContent = item.value;
|
||||||
|
node.append(label, value);
|
||||||
|
wrap.appendChild(node);
|
||||||
|
}
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBlock(title, subtitle, items) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'block-card';
|
||||||
|
const ttl = document.createElement('div');
|
||||||
|
ttl.className = 'block-title';
|
||||||
|
ttl.textContent = title;
|
||||||
|
card.appendChild(ttl);
|
||||||
|
if (subtitle) {
|
||||||
|
const sub = document.createElement('div');
|
||||||
|
sub.className = 'block-subtitle';
|
||||||
|
sub.textContent = subtitle;
|
||||||
|
card.appendChild(sub);
|
||||||
|
}
|
||||||
|
card.appendChild(renderMiniGrid(items));
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderArrayLine(values, emptyLabel = '—') {
|
||||||
|
const arr = Array.isArray(values) ? values : [];
|
||||||
|
return arr.length ? arr.join(', ') : emptyLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSessionList(sessions) {
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'details-wrap';
|
||||||
|
const details = document.createElement('details');
|
||||||
|
details.open = true;
|
||||||
|
const summary = document.createElement('summary');
|
||||||
|
summary.textContent = `Сессии (${Array.isArray(sessions) ? sessions.length : 0})`;
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'details-body';
|
||||||
|
if (!Array.isArray(sessions) || sessions.length === 0) {
|
||||||
|
body.innerHTML = '<div class="field-value muted">Сессий нет.</div>';
|
||||||
|
} else {
|
||||||
|
const grid = document.createElement('div');
|
||||||
|
grid.className = 'block-list';
|
||||||
|
sessions.forEach((session, idx) => {
|
||||||
|
grid.appendChild(renderBlock(
|
||||||
|
`Session #${idx + 1}`,
|
||||||
|
`type=${session.sessionType}, version=${session.sessionVersion}`,
|
||||||
|
[
|
||||||
|
{ label: 'Имя', value: session.sessionName || '—' },
|
||||||
|
{ label: 'PubKey', value: base58(session.sessionPubKey32) },
|
||||||
|
],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
body.appendChild(grid);
|
||||||
|
}
|
||||||
|
details.append(summary, body);
|
||||||
|
list.appendChild(details);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderParsed(parsed) {
|
||||||
|
$('summaryCard').style.display = 'block';
|
||||||
|
$('blocksCard').style.display = 'block';
|
||||||
|
$('rawCard').style.display = 'block';
|
||||||
|
|
||||||
|
renderRows([
|
||||||
|
{ label: 'PDA адрес', value: parsed.pdaAddress },
|
||||||
|
{ label: 'Логин', value: parsed.login },
|
||||||
|
{ label: 'Статус', value: parsed.isServer ? 'server' : 'not server' },
|
||||||
|
{ label: 'recordNumber', value: String(parsed.recordNumber) },
|
||||||
|
{ label: 'createdAtMs', value: `${parsed.createdAtMs} · ${formatTimestamp(parsed.createdAtMs)}` },
|
||||||
|
{ label: 'updatedAtMs', value: `${parsed.updatedAtMs} · ${formatTimestamp(parsed.updatedAtMs)}` },
|
||||||
|
{ label: 'recordLen', value: String(parsed.recordLen) },
|
||||||
|
{ label: 'prevRecordHash32', value: hex(parsed.prevRecordHash) },
|
||||||
|
{ label: 'signature64', value: base58(parsed.signature) },
|
||||||
|
{ label: 'unsignedBytesLen', value: String(parsed.unsignedBytes?.length || 0) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const badgeLine = $('badgeLine');
|
||||||
|
badgeLine.innerHTML = '';
|
||||||
|
const badges = [
|
||||||
|
{ label: `server=${parsed.isServer ? '1' : '0'}`, kind: parsed.isServer ? 'ok' : 'warn' },
|
||||||
|
{ label: `trusted=${Number(parsed.trustedCount || 0)}`, kind: Number(parsed.trustedCount || 0) > 0 ? 'ok' : 'warn' },
|
||||||
|
{ label: `sessions=${Array.isArray(parsed.sessions) ? parsed.sessions.length : 0}`, kind: 'info' },
|
||||||
|
{ label: `access=${Array.isArray(parsed.accessServers) ? parsed.accessServers.length : 0}`, kind: 'info' },
|
||||||
|
{ label: `sync=${Array.isArray(parsed.syncServers) ? parsed.syncServers.length : 0}`, kind: 'info' },
|
||||||
|
];
|
||||||
|
badges.forEach((item) => {
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = `badge ${item.kind}`;
|
||||||
|
badge.textContent = item.label;
|
||||||
|
badgeLine.appendChild(badge);
|
||||||
|
});
|
||||||
|
|
||||||
|
const blocks = $('blocksList');
|
||||||
|
blocks.innerHTML = '';
|
||||||
|
blocks.appendChild(renderBlock('RecoveryKeyBlock', 'block_type=0', [
|
||||||
|
{ label: 'recoveryKey32', value: base58(parsed.recoveryKey) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('RootKeyBlock', 'block_type=1', [
|
||||||
|
{ label: 'rootKey32', value: base58(parsed.rootKey) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('ClientKeyBlock', 'block_type=2', [
|
||||||
|
{ label: 'clientKey32', value: base58(parsed.clientKey) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('BlockchainRegistryBlock', 'block_type=3', [
|
||||||
|
{ label: 'blockchainType', value: String(parsed.blockchain?.blockchainType ?? 0) },
|
||||||
|
{ label: 'blockchainName', value: parsed.blockchain?.blockchainName || '—' },
|
||||||
|
{ label: 'blockchainPublicKey32', value: base58(parsed.blockchain?.blockchainPublicKey) },
|
||||||
|
{ label: 'paidLimitBytes', value: formatBigInt(parsed.blockchain?.paidLimitBytes || 0n) },
|
||||||
|
{ label: 'usedBytes', value: formatBigInt(parsed.blockchain?.usedBytes || 0n) },
|
||||||
|
{ label: 'lastBlockNumber', value: String(parsed.blockchain?.lastBlockNumber ?? 0) },
|
||||||
|
{ label: 'lastBlockHash32', value: hex(parsed.blockchain?.lastBlockHash) },
|
||||||
|
{ label: 'lastBlockSignature64', value: base58(parsed.blockchain?.lastBlockSignature) },
|
||||||
|
{ label: 'arweaveTxId', value: parsed.blockchain?.arweaveTxId || '—' },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('ServerProfileBlock', 'block_type=30', [
|
||||||
|
{ label: 'isServer', value: parsed.isServer ? '1' : '0' },
|
||||||
|
{ label: 'addressFormatType', value: String(parsed.addressFormatType ?? 0) },
|
||||||
|
{ label: 'addressFormatVersion', value: String(parsed.addressFormatVersion ?? 0) },
|
||||||
|
{ label: 'serverAddress', value: parsed.serverAddress || '—' },
|
||||||
|
{ label: 'syncServersCount', value: String(parsed.syncServers?.length || 0) },
|
||||||
|
{ label: 'syncServers', value: renderArrayLine(parsed.syncServers) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('AccessServersBlock', 'block_type=40', [
|
||||||
|
{ label: 'accessServersCount', value: String(parsed.accessServers?.length || 0) },
|
||||||
|
{ label: 'accessServers', value: renderArrayLine(parsed.accessServers) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('SessionsBlock', 'block_type=50', [
|
||||||
|
{ label: 'sessionsMode', value: String(parsed.sessionsMode ?? 0) },
|
||||||
|
{ label: 'sessionsCount', value: String(parsed.sessions?.length || 0) },
|
||||||
|
{ label: 'sessions', value: parsed.sessions?.length ? 'ниже' : '—' },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('TrustedStateBlock', 'block_type=70', [
|
||||||
|
{ label: 'trustedCount', value: String(parsed.trustedCount ?? 0) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderSessionList(parsed.sessions));
|
||||||
|
|
||||||
|
$('rawJson').textContent = JSON.stringify({
|
||||||
|
pdaAddress: parsed.pdaAddress,
|
||||||
|
login: parsed.login,
|
||||||
|
isServer: parsed.isServer,
|
||||||
|
recordNumber: parsed.recordNumber,
|
||||||
|
createdAtMs: parsed.createdAtMs.toString(),
|
||||||
|
updatedAtMs: parsed.updatedAtMs.toString(),
|
||||||
|
recordLen: parsed.recordLen,
|
||||||
|
trustedCount: parsed.trustedCount,
|
||||||
|
addressFormatType: parsed.addressFormatType,
|
||||||
|
addressFormatVersion: parsed.addressFormatVersion,
|
||||||
|
serverAddress: parsed.serverAddress,
|
||||||
|
syncServers: parsed.syncServers,
|
||||||
|
accessServers: parsed.accessServers,
|
||||||
|
sessionsMode: parsed.sessionsMode,
|
||||||
|
sessions: parsed.sessions.map((s) => ({
|
||||||
|
sessionType: s.sessionType,
|
||||||
|
sessionVersion: s.sessionVersion,
|
||||||
|
sessionName: s.sessionName,
|
||||||
|
sessionPubKey32: base58(s.sessionPubKey32),
|
||||||
|
})),
|
||||||
|
recoveryKey: base58(parsed.recoveryKey),
|
||||||
|
rootKey: base58(parsed.rootKey),
|
||||||
|
clientKey: base58(parsed.clientKey),
|
||||||
|
blockchain: {
|
||||||
|
blockchainType: parsed.blockchain.blockchainType,
|
||||||
|
blockchainName: parsed.blockchain.blockchainName,
|
||||||
|
blockchainPublicKey: base58(parsed.blockchain.blockchainPublicKey),
|
||||||
|
paidLimitBytes: parsed.blockchain.paidLimitBytes.toString(),
|
||||||
|
usedBytes: parsed.blockchain.usedBytes.toString(),
|
||||||
|
lastBlockNumber: parsed.blockchain.lastBlockNumber,
|
||||||
|
lastBlockHash: hex(parsed.blockchain.lastBlockHash),
|
||||||
|
lastBlockSignature: base58(parsed.blockchain.lastBlockSignature),
|
||||||
|
arweaveTxId: parsed.blockchain.arweaveTxId || '',
|
||||||
|
},
|
||||||
|
}, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('btnLoad').addEventListener('click', async () => {
|
||||||
|
clearStatus($('status'));
|
||||||
|
$('btnLoad').disabled = true;
|
||||||
|
$('summaryCard').style.display = 'none';
|
||||||
|
$('blocksCard').style.display = 'none';
|
||||||
|
$('rawCard').style.display = 'none';
|
||||||
|
try {
|
||||||
|
const endpoint = String($('endpoint').value || '').trim();
|
||||||
|
const ref = String($('ref').value || '').trim();
|
||||||
|
if (!endpoint) throw new Error('Укажите Solana endpoint');
|
||||||
|
if (!ref) throw new Error('Укажите логин или адрес PDA');
|
||||||
|
|
||||||
|
setStatus($('status'), 'Чтение PDA...', 'info');
|
||||||
|
const parsed = await readShineUserPdaByRef({ value: ref, solanaEndpoint: endpoint });
|
||||||
|
renderParsed(parsed);
|
||||||
|
setStatus($('status'), 'PDA загружена.', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
setStatus($('status'), error?.message || String(error), 'error');
|
||||||
|
} finally {
|
||||||
|
$('btnLoad').disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.dataset.ready = '1';
|
||||||
@ -22,9 +22,12 @@ import {
|
|||||||
validateLoginOrThrow,
|
validateLoginOrThrow,
|
||||||
wireDeviceAddressPreview,
|
wireDeviceAddressPreview,
|
||||||
} from './server-ui-shared.js';
|
} from './server-ui-shared.js';
|
||||||
|
import { defaultServerAddress, defaultServerLogin } from '../../js/deploy-config.js';
|
||||||
|
|
||||||
const fieldMap = {
|
const fieldMap = {
|
||||||
masterSecret: 'masterSecret',
|
masterSecret: 'masterSecret',
|
||||||
|
recoveryPub: 'recoveryPub',
|
||||||
|
recoveryPriv: 'recoveryPriv',
|
||||||
rootPub: 'rootPub',
|
rootPub: 'rootPub',
|
||||||
rootPriv: 'rootPriv',
|
rootPriv: 'rootPriv',
|
||||||
bchPub: 'bchPub',
|
bchPub: 'bchPub',
|
||||||
@ -39,6 +42,7 @@ let currentPda = null;
|
|||||||
|
|
||||||
function resetExpectedKeysUi() {
|
function resetExpectedKeysUi() {
|
||||||
$('expectedKeysBox').style.display = 'none';
|
$('expectedKeysBox').style.display = 'none';
|
||||||
|
setText('expectedRecoveryPub', '');
|
||||||
setText('expectedRootPub', '');
|
setText('expectedRootPub', '');
|
||||||
setText('expectedBchPub', '');
|
setText('expectedBchPub', '');
|
||||||
setText('expectedDevPub', '');
|
setText('expectedDevPub', '');
|
||||||
@ -48,6 +52,7 @@ function resetExpectedKeysUi() {
|
|||||||
|
|
||||||
function renderExpectedKeys(parsed) {
|
function renderExpectedKeys(parsed) {
|
||||||
$('expectedKeysBox').style.display = 'block';
|
$('expectedKeysBox').style.display = 'block';
|
||||||
|
setText('expectedRecoveryPub', publicKeyBytesToBase58(parsed.recoveryKey));
|
||||||
setText('expectedRootPub', publicKeyBytesToBase58(parsed.rootKey));
|
setText('expectedRootPub', publicKeyBytesToBase58(parsed.rootKey));
|
||||||
setText('expectedBchPub', publicKeyBytesToBase58(parsed.blockchain.blockchainPublicKey));
|
setText('expectedBchPub', publicKeyBytesToBase58(parsed.blockchain.blockchainPublicKey));
|
||||||
setText('expectedDevPub', publicKeyBytesToBase58(parsed.clientKey));
|
setText('expectedDevPub', publicKeyBytesToBase58(parsed.clientKey));
|
||||||
@ -59,6 +64,7 @@ function compareCurrentFormKeysWithPda() {
|
|||||||
const blockchainActual = String($('bchPub').value || '').trim();
|
const blockchainActual = String($('bchPub').value || '').trim();
|
||||||
return {
|
return {
|
||||||
resultMap: {
|
resultMap: {
|
||||||
|
recovery: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.recoveryKey), $('recoveryPub').value),
|
||||||
root: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.rootKey), $('rootPub').value),
|
root: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.rootKey), $('rootPub').value),
|
||||||
blockchain: blockchainActual
|
blockchain: blockchainActual
|
||||||
? compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), blockchainActual)
|
? compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), blockchainActual)
|
||||||
@ -92,6 +98,8 @@ function renderComparisonStatus() {
|
|||||||
setupPasswordEye($('btnEye'), $('password'));
|
setupPasswordEye($('btnEye'), $('password'));
|
||||||
wireDeviceAddressPreview(fieldMap);
|
wireDeviceAddressPreview(fieldMap);
|
||||||
$('password').value = '';
|
$('password').value = '';
|
||||||
|
$('login').placeholder = defaultServerLogin;
|
||||||
|
$('serverAddress').placeholder = defaultServerAddress;
|
||||||
resetExpectedKeysUi();
|
resetExpectedKeysUi();
|
||||||
|
|
||||||
$('btnTopupDevnet').addEventListener('click', () => {
|
$('btnTopupDevnet').addEventListener('click', () => {
|
||||||
|
|||||||
91
shine-UI/server-ui/read-pda.html
Normal file
91
shine-UI/server-ui/read-pda.html
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Просмотр PDA — SHiNE Server Admin</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<style>
|
||||||
|
.field-row { display: grid; grid-template-columns: 180px 1fr; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.field-row:last-child { border-bottom: 0; }
|
||||||
|
.field-label { color: var(--text-muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
|
||||||
|
.field-value { font-family: monospace; font-size: 12px; word-break: break-all; white-space: pre-wrap; }
|
||||||
|
.field-value.muted { color: var(--text-muted); }
|
||||||
|
.block-list { display: grid; gap: 12px; }
|
||||||
|
.block-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; background: #121212; }
|
||||||
|
.block-title { font-size: 13px; font-weight: 700; color: var(--accent); margin-bottom: 10px; }
|
||||||
|
.block-subtitle { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; }
|
||||||
|
.mini-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; }
|
||||||
|
.mini-item { min-width: 0; }
|
||||||
|
.mini-label { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; text-transform: uppercase; letter-spacing: .04em; }
|
||||||
|
.mini-value { font-family: monospace; font-size: 12px; word-break: break-all; white-space: pre-wrap; }
|
||||||
|
.mono-box { font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; background: #0d0d0d; border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; }
|
||||||
|
.summary-grid { display: grid; gap: 0; }
|
||||||
|
.badge-line { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.badge { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px; font-size: 11px; color: var(--text-muted); background: #0d0d0d; }
|
||||||
|
.badge.ok { color: #7dcc7d; border-color: #2a4a2a; background: #1a2e1a; }
|
||||||
|
.badge.warn { color: #ffd37a; border-color: #5f4b22; background: #2f2614; }
|
||||||
|
.badge.err { color: #f08080; border-color: #5a2a2a; background: #2e1a1a; }
|
||||||
|
.details-wrap { margin-top: 12px; }
|
||||||
|
.details-wrap details { border: 1px solid var(--border); border-radius: var(--radius); background: #121212; }
|
||||||
|
.details-wrap summary { cursor: pointer; padding: 12px 14px; color: var(--accent); font-weight: 600; }
|
||||||
|
.details-body { padding: 0 14px 14px; }
|
||||||
|
.tight { margin-bottom: 8px; }
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.field-row { grid-template-columns: 1fr; gap: 4px; }
|
||||||
|
.mini-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="../server-ui.html">← Назад</a>
|
||||||
|
<a href="create-server-pda.html">Создать PDA</a>
|
||||||
|
<a href="update-server-pda.html">Обновить PDA</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Просмотр PDA</h1>
|
||||||
|
<p class="subtitle">Читает user_pda или server PDA по логину либо по адресу и показывает все поля</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Параметры Solana</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label>Solana Endpoint</label>
|
||||||
|
<input type="text" id="endpoint" value="https://api.devnet.solana.com" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Источник PDA</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label>Логин или адрес PDA</label>
|
||||||
|
<input type="text" id="ref" placeholder="Логин сервера / пользователя или base58-адрес PDA" />
|
||||||
|
<div class="hint">Если введён base58-адрес длиной 32 байта, он читается напрямую. Иначе значение трактуется как логин.</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn-primary" id="btnLoad">Загрузить PDA</button>
|
||||||
|
</div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="summaryCard" style="display:none">
|
||||||
|
<h2>Сводка</h2>
|
||||||
|
<div class="summary-grid" id="summaryRows"></div>
|
||||||
|
<div class="badge-line" style="margin-top:12px" id="badgeLine"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="blocksCard" style="display:none">
|
||||||
|
<h2>Блоки и поля</h2>
|
||||||
|
<div class="block-list" id="blocksList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="rawCard" style="display:none">
|
||||||
|
<h2>Raw JSON</h2>
|
||||||
|
<pre class="mono-box" id="rawJson"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="./js/read-pda-page.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -46,6 +46,7 @@
|
|||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="../server-ui.html">← Назад</a>
|
<a href="../server-ui.html">← Назад</a>
|
||||||
<a href="create-server-pda.html">Создать PDA</a>
|
<a href="create-server-pda.html">Создать PDA</a>
|
||||||
|
<a href="read-pda.html">Просмотреть PDA</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Обновление PDA сервера</h1>
|
<h1>Обновление PDA сервера</h1>
|
||||||
@ -63,7 +64,7 @@
|
|||||||
<h2>Загрузить существующую PDA</h2>
|
<h2>Загрузить существующую PDA</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Логин сервера</label>
|
<label>Логин сервера</label>
|
||||||
<input type="text" id="login" placeholder="shineupme" maxlength="20" />
|
<input type="text" id="login" placeholder="Логин сервера" maxlength="20" />
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn-secondary" id="btnLoad">Загрузить PDA</button>
|
<button class="btn-secondary" id="btnLoad">Загрузить PDA</button>
|
||||||
@ -86,7 +87,7 @@
|
|||||||
<h2>Новые параметры сервера</h2>
|
<h2>Новые параметры сервера</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Новый адрес сервера (URL)</label>
|
<label>Новый адрес сервера (URL)</label>
|
||||||
<input type="text" id="serverAddress" placeholder="https://shineup.me/ws" />
|
<input type="text" id="serverAddress" placeholder="Адрес сервера" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Новые серверы синхронизации (sync_servers)</label>
|
<label>Новые серверы синхронизации (sync_servers)</label>
|
||||||
@ -118,6 +119,11 @@
|
|||||||
|
|
||||||
<div class="sec-lbl">Ключевые пары (base58)</div>
|
<div class="sec-lbl">Ключевые пары (base58)</div>
|
||||||
|
|
||||||
|
<div class="kp-block">
|
||||||
|
<div class="kp-title">Recovery Key — восстановление аккаунта</div>
|
||||||
|
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="recoveryPub" placeholder="base58, ~44 символа" /></div>
|
||||||
|
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="recoveryPriv" placeholder="seed base58, ~44 символа" /></div>
|
||||||
|
</div>
|
||||||
<div class="kp-block">
|
<div class="kp-block">
|
||||||
<div class="kp-title">Root Key — подпись PDA-записи</div>
|
<div class="kp-title">Root Key — подпись PDA-записи</div>
|
||||||
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="rootPub" placeholder="base58, ~44 символа" /></div>
|
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="rootPub" placeholder="base58, ~44 символа" /></div>
|
||||||
@ -144,6 +150,10 @@
|
|||||||
|
|
||||||
<div class="expected-card" id="expectedKeysBox" style="display:none">
|
<div class="expected-card" id="expectedKeysBox" style="display:none">
|
||||||
<div class="expected-ttl">Какие ключи ожидаются по уже загруженной PDA</div>
|
<div class="expected-ttl">Какие ключи ожидаются по уже загруженной PDA</div>
|
||||||
|
<div class="expected-row">
|
||||||
|
<div class="expected-lbl">Ожидаемый recovery public key</div>
|
||||||
|
<div class="expected-val" id="expectedRecoveryPub"></div>
|
||||||
|
</div>
|
||||||
<div class="expected-row">
|
<div class="expected-row">
|
||||||
<div class="expected-lbl">Ожидаемый root public key</div>
|
<div class="expected-lbl">Ожидаемый root public key</div>
|
||||||
<div class="expected-val" id="expectedRootPub"></div>
|
<div class="expected-val" id="expectedRootPub"></div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user