SHA256
Compare commits
8
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
daa516babe | ||
|
|
2890cdd457 | ||
|
|
e4dfb43b5e | ||
|
|
3926d561c0 | ||
|
|
85133db483 | ||
|
|
6fc7d75e17 | ||
|
|
be321813fa | ||
|
|
d2c0ecdf62 |
@@ -86,20 +86,18 @@
|
||||
- Обычные коммиты делать стандартным `git commit`; переменная `$GITEA_TOKEN` для коммитов не нужна и не используется.
|
||||
|
||||
## Deploy
|
||||
- Все документы и заметки по деплою хранить в папке `Dev_Docs/deploy/`.
|
||||
- Все документы и заметки по деплою хранить в папке `Deploy/`.
|
||||
- Production-хост SHiNE: `player@shineup.me` (`178.208.64.62`).
|
||||
- Основной test-хост SHiNE: `player@193.8.215.70` (`t.shineup.me`).
|
||||
- Резервный test-хост SHiNE: `player@93.170.12.154` (`test.shineup.me`).
|
||||
- Второй production-хост SHiNE: `player@193.8.215.70` (`server2.shineup.me`).
|
||||
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`).
|
||||
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
|
||||
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
|
||||
- Любые изменения и любой деплой на production `shineup.me` выполнять только после отдельного явного подтверждения пользователя.
|
||||
- Если пользователь пишет просто `задеплой` без уточнения production/test, по умолчанию деплоить на `t.shineup.me`.
|
||||
- Если пользователь пишет просто `задеплой` без уточнения production/test, по умолчанию деплоить на `server2.shineup.me`.
|
||||
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`.
|
||||
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`.
|
||||
- Production server deploy: `./gradlew deployServerProduction`.
|
||||
- Production UI deploy: `./gradlew deployUIProduction`.
|
||||
- Резервный test deploy на `test.shineup.me`: `./gradlew deployServerTest` и `./gradlew deployUITest`, но пока их не использовать без отдельной причины.
|
||||
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
|
||||
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
|
||||
- Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`.
|
||||
|
||||
@@ -11,11 +11,9 @@
|
||||
и
|
||||
|
||||
- `player@193.8.215.70`
|
||||
- домен `t.shineup.me`
|
||||
- домен `server2.shineup.me`
|
||||
- IP `193.8.215.70`
|
||||
|
||||
Отдельно резервный хост `test.shineup.me` сейчас помечен как временно неработающий.
|
||||
|
||||
## 1. Основной production-хост
|
||||
|
||||
- SSH: `player@shineup.me`
|
||||
@@ -55,7 +53,7 @@ Deploy:
|
||||
## 2. Второй production-сервер
|
||||
|
||||
- SSH: `player@193.8.215.70`
|
||||
- домен: `t.shineup.me`
|
||||
- домен: `server2.shineup.me`
|
||||
- IP: `193.8.215.70`
|
||||
- пользователь: `player`
|
||||
- базовый путь: `/home/player`
|
||||
@@ -89,7 +87,6 @@ Deploy:
|
||||
|
||||
Не production:
|
||||
|
||||
- `test.shineup.me` (`93.170.12.154`) — резервный хост, временно не работает
|
||||
- `t1.shineup.me`
|
||||
- `t2.shineup.me`
|
||||
- `t3.shineup.me`
|
||||
|
||||
+3
-9
@@ -4,7 +4,7 @@
|
||||
|
||||
Эта папка служит краткой картой окружений:
|
||||
|
||||
- [TEST_SERVERS.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Deploy/TEST_SERVERS.md) — тестовые серверы и стенды;
|
||||
- [TEST_SERVERS.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Deploy/TEST_SERVERS.md) — тестовые стенды;
|
||||
- [PRODUCTION_SERVERS.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Deploy/PRODUCTION_SERVERS.md) — production-контур и связанные публичные публикации.
|
||||
|
||||
Ниже краткая сводка.
|
||||
@@ -17,13 +17,8 @@
|
||||
- IP `178.208.64.62`
|
||||
- Второй production SHiNE:
|
||||
- `player@193.8.215.70`
|
||||
- домен `t.shineup.me`
|
||||
- домен `server2.shineup.me`
|
||||
- IP `193.8.215.70`
|
||||
- Временно неработающий резервный test:
|
||||
- `player@93.170.12.154`
|
||||
- домен `test.shineup.me`
|
||||
- IP `93.170.12.154`
|
||||
|
||||
## Отдельный quad-devnet стенд
|
||||
|
||||
На отдельном VPS `178.208.90.249` подняты 4 независимых test/devnet-инстанса:
|
||||
@@ -35,7 +30,6 @@
|
||||
|
||||
## Важно
|
||||
|
||||
- Production-контура SHiNE сейчас два: `shineup.me` и `t.shineup.me`.
|
||||
- `test.shineup.me` сейчас помечен как временно неработающий резервный test-хост.
|
||||
- Production-контура SHiNE сейчас два: `shineup.me` и `server2.shineup.me`.
|
||||
- `t1..t4.shineup.me` — это отдельные тестовые/devnet-контуры, не production.
|
||||
- Любые изменения на `shineup.me` делать только после отдельного подтверждения пользователя.
|
||||
|
||||
+7
-33
@@ -1,11 +1,11 @@
|
||||
# Тестовые серверы SHiNE
|
||||
|
||||
Этот файл описывает тестовые контуры, которые сейчас фигурируют в проекте.
|
||||
Этот файл описывает тестовые стенды, которые сейчас фигурируют в проекте.
|
||||
|
||||
## 1. Исторический `test2`, теперь второй production-сервер
|
||||
|
||||
- SSH: `player@193.8.215.70`
|
||||
- Домен: `t.shineup.me`
|
||||
- Домен: `server2.shineup.me`
|
||||
- IP: `193.8.215.70`
|
||||
- Назначение: второй production-контур SHiNE
|
||||
|
||||
@@ -33,35 +33,10 @@ Deploy:
|
||||
|
||||
- этот хост больше не считать test-контуром;
|
||||
- исторические имена deploy-задач `deployServerTest2` / `deployUITest2` сохранены, но сам хост считать production;
|
||||
- задача `deployUITest2` по умолчанию выкладывает UI на `server2.shineup.me`, а не на `t2.shineup.me`;
|
||||
- при описании окружений перечислять его как второй production-сервер.
|
||||
|
||||
## 2. Резервный test-сервер, временно неработающий
|
||||
|
||||
- SSH: `player@93.170.12.154`
|
||||
- Домен: `test.shineup.me`
|
||||
- IP: `93.170.12.154`
|
||||
- Назначение: резервный test
|
||||
|
||||
Структура по проектным докам:
|
||||
|
||||
- каталог SHiNE: `/home/player/SHiNE`
|
||||
- сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||
- UI: `/home/player/SHiNE/shine-ui`
|
||||
- данные: `/home/player/SHiNE/shine-server/data/`
|
||||
- логи: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||
|
||||
Deploy:
|
||||
|
||||
- `./gradlew deployServerTest`
|
||||
- `./gradlew deployUITest`
|
||||
|
||||
Примечания:
|
||||
|
||||
- этот хост резервный;
|
||||
- сейчас помечен как временно неработающий;
|
||||
- использовать его без отдельной причины не нужно.
|
||||
|
||||
## 3. Отдельный quad-devnet стенд `t1..t4`
|
||||
## 2. Отдельный quad-devnet стенд `t1..t4`
|
||||
|
||||
- VPS: `178.208.90.249`
|
||||
- пользователь: `player`
|
||||
@@ -124,13 +99,12 @@ Operational-нюанс:
|
||||
- при одновременных рестартах возможны `HTTP 429` от `api.devnet.solana.com`;
|
||||
- поэтому сервисы `shine-t1..shine-t4` лучше перезапускать по одному, с паузой.
|
||||
|
||||
## 4. Что проверять первым делом
|
||||
## 3. Что проверять первым делом
|
||||
|
||||
Для любого test-контура полезны такие быстрые проверки:
|
||||
|
||||
```bash
|
||||
curl -I https://t.shineup.me
|
||||
curl -I https://test.shineup.me
|
||||
curl -I https://server2.shineup.me
|
||||
curl -I https://t1.shineup.me
|
||||
curl -I https://t2.shineup.me
|
||||
curl -I https://t3.shineup.me
|
||||
@@ -143,7 +117,7 @@ curl -I https://t4.shineup.me
|
||||
sudo systemctl --no-pager --full status caddy shine-t1 shine-t2 shine-t3 shine-t4
|
||||
```
|
||||
|
||||
Для основного/резервного test:
|
||||
Для второго production-контура:
|
||||
|
||||
```bash
|
||||
sudo systemctl --no-pager --full status shine-server caddy
|
||||
|
||||
+1
-10
@@ -59,7 +59,7 @@ shine-UI/server-ui.html
|
||||
./gradlew deployUI
|
||||
```
|
||||
|
||||
Default deploy по умолчанию идёт на `t.shineup.me` (`player@193.8.215.70`).
|
||||
Default deploy по умолчанию идёт на `server2.shineup.me` (`player@193.8.215.70`).
|
||||
|
||||
Production deploy:
|
||||
|
||||
@@ -70,15 +70,6 @@ Production deploy:
|
||||
|
||||
Любые изменения на `shineup.me` делать только после отдельного явного подтверждения пользователя.
|
||||
|
||||
Резервный test-контур:
|
||||
|
||||
```
|
||||
./gradlew deployServerTest
|
||||
./gradlew deployUITest
|
||||
```
|
||||
|
||||
`test.shineup.me` считается резервным тестовым сервером и в обычный deploy не включается.
|
||||
|
||||
Логи на проде:
|
||||
- `/home/player/SHiNE/shine-server/logs/app.log`
|
||||
- `/home/player/SHiNE/shine-server/logs/call-delivery-events.log`
|
||||
|
||||
@@ -35,6 +35,5 @@
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `Dev_Docs/deploy/`;
|
||||
- `Deploy/`;
|
||||
- `Dev_Docs/Blockchain/sync-between-servers.md`, если изменится поведение остановки/восстановления.
|
||||
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
client.version=1.2.313
|
||||
server.version=1.2.292
|
||||
client.version=1.2.329
|
||||
server.version=1.2.301
|
||||
|
||||
+8
-23
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
проверь ещё id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
}
|
||||
|
||||
def appVersionProps = new Properties()
|
||||
@@ -207,52 +207,37 @@ tasks.register('deployUIProduction', Exec) {
|
||||
group = "!!deployment"
|
||||
description = "Production UI deploy: shineup.me (только после явного подтверждения)"
|
||||
workingDir = rootDir
|
||||
commandLine 'bash', file('deploy_shine-PWA.sh').absolutePath
|
||||
commandLine 'bash', file('deploy_shine-ui_production_shineupme.sh').absolutePath
|
||||
}
|
||||
|
||||
tasks.register('deployServer', Exec) {
|
||||
group = "!!deployment"
|
||||
description = "Default deploy server: t.shineup.me"
|
||||
description = "Default deploy server: server2.shineup.me"
|
||||
dependsOn shadowJar
|
||||
workingDir = rootDir
|
||||
environment 'LOCAL_JAR', file('SHiNE-server/build/libs/shine-server.jar').absolutePath
|
||||
commandLine 'bash', file('deploy_shine-server_test2.sh').absolutePath
|
||||
commandLine 'bash', file('deploy_shine-server_server2shineupme.sh').absolutePath
|
||||
}
|
||||
|
||||
tasks.register('deployUI', Exec) {
|
||||
group = "!!deployment"
|
||||
description = "Default deploy UI: t.shineup.me"
|
||||
description = "Default deploy UI: server2.shineup.me"
|
||||
workingDir = rootDir
|
||||
commandLine 'bash', file('deploy_shine-ui_test2.sh').absolutePath
|
||||
commandLine 'bash', file('deploy_shine-ui_production_server2shineupme.sh').absolutePath
|
||||
}
|
||||
|
||||
tasks.register('deployServerTest2') {
|
||||
group = "!!deployment"
|
||||
description = "Явный алиас основного test deploy server: t.shineup.me"
|
||||
description = "Явный алиас второго production deploy server: server2.shineup.me"
|
||||
dependsOn tasks.named('deployServer')
|
||||
}
|
||||
|
||||
tasks.register('deployUITest2') {
|
||||
group = "!!deployment"
|
||||
description = "Явный алиас основного test deploy UI: t.shineup.me"
|
||||
description = "Явный алиас второго production deploy UI: server2.shineup.me"
|
||||
dependsOn tasks.named('deployUI')
|
||||
}
|
||||
|
||||
tasks.register('deployServerTest', Exec) {
|
||||
group = "!!deployment"
|
||||
description = "Резервный test deploy: test.shineup.me (пока не использовать)"
|
||||
dependsOn shadowJar
|
||||
workingDir = rootDir
|
||||
environment 'LOCAL_JAR', file('SHiNE-server/build/libs/shine-server.jar').absolutePath
|
||||
commandLine 'bash', file('deploy_shine-server_test.sh').absolutePath
|
||||
}
|
||||
|
||||
tasks.register('deployUITest', Exec) {
|
||||
group = "!!deployment"
|
||||
description = "Резервный test UI deploy: test.shineup.me (пока не использовать)"
|
||||
workingDir = rootDir
|
||||
commandLine 'bash', file('deploy_shine-ui_test.sh').absolutePath
|
||||
}
|
||||
|
||||
tasks.register('startLocal', Exec) {
|
||||
group = "!!run"
|
||||
|
||||
@@ -28,6 +28,13 @@ TARGET_URL="${TARGET_URL:-https://${EXPECTED_CADDY_SITE}}"
|
||||
REMOTE_DIR="${REMOTE_UI_DIR}"
|
||||
DEPLOY_SERVER_LOGIN="${DEPLOY_SERVER_LOGIN:-}"
|
||||
DEPLOY_SERVER_ADDRESS="${DEPLOY_SERVER_ADDRESS:-}"
|
||||
DEPLOY_SOLANA_CLUSTER="${DEPLOY_SOLANA_CLUSTER:-}"
|
||||
DEPLOY_SOLANA_ENDPOINT="${DEPLOY_SOLANA_ENDPOINT:-}"
|
||||
DEPLOY_SOLANA_CLUSTER_NORMALIZED="$DEPLOY_SOLANA_CLUSTER"
|
||||
|
||||
if [[ "$DEPLOY_SOLANA_CLUSTER_NORMALIZED" == "mainnet" ]]; then
|
||||
DEPLOY_SOLANA_CLUSTER_NORMALIZED="mainnet-beta"
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
@@ -52,6 +59,12 @@ if [[ -f "$DEPLOY_CONFIG_FILE" ]]; then
|
||||
if [[ -n "$DEPLOY_SERVER_ADDRESS" ]]; then
|
||||
perl -0pi -e "s/export const defaultServerAddress = '.*?';/export const defaultServerAddress = '$DEPLOY_SERVER_ADDRESS';/s" "$DEPLOY_CONFIG_FILE"
|
||||
fi
|
||||
if [[ -n "$DEPLOY_SOLANA_CLUSTER_NORMALIZED" ]]; then
|
||||
perl -0pi -e "s/export const defaultSolanaCluster = '.*?';/export const defaultSolanaCluster = '$DEPLOY_SOLANA_CLUSTER_NORMALIZED';/s" "$DEPLOY_CONFIG_FILE"
|
||||
fi
|
||||
if [[ -n "$DEPLOY_SOLANA_ENDPOINT" ]]; then
|
||||
perl -0pi -e "s|export const defaultSolanaEndpoint = '.*?';|export const defaultSolanaEndpoint = '$DEPLOY_SOLANA_ENDPOINT';|s" "$DEPLOY_CONFIG_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
INDEX_FILE="$TMP_DIR/index.html"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_HOST="${TARGET_HOST:-player@193.8.215.70}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-t.shineup.me}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-server2.shineup.me}"
|
||||
REMOTE_BASE="${REMOTE_BASE:-/home/player/SHiNE}"
|
||||
REMOTE_SERVER_DIR="${REMOTE_SERVER_DIR:-$REMOTE_BASE/shine-server}"
|
||||
REMOTE_DATA_DIR="${REMOTE_DATA_DIR:-$REMOTE_SERVER_DIR/data}"
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PROD_HOST="${PROD_HOST:-player@shineup.me}"
|
||||
TEST_HOST="${TEST_HOST:-player@93.170.12.154}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-test.shineup.me}"
|
||||
REMOTE_BASE="${REMOTE_BASE:-/home/player/SHiNE}"
|
||||
REMOTE_SERVER_DIR="${REMOTE_SERVER_DIR:-$REMOTE_BASE/shine-server}"
|
||||
REMOTE_UI_DIR="${REMOTE_UI_DIR:-$REMOTE_BASE/shine-ui}"
|
||||
REMOTE_DATA_DIR="${REMOTE_DATA_DIR:-$REMOTE_SERVER_DIR/data}"
|
||||
REMOTE_LOGS_DIR="${REMOTE_LOGS_DIR:-$REMOTE_SERVER_DIR/logs}"
|
||||
REMOTE_SERVICE_NAME="${REMOTE_SERVICE_NAME:-shine-server}"
|
||||
LOCAL_JAR="${LOCAL_JAR:-build/libs/shine-server.jar}"
|
||||
PROD_DATA_DIR="${PROD_DATA_DIR:-/home/player/SHiNE/shine-server/data}"
|
||||
PROD_APP_PROPS="${PROD_APP_PROPS:-/home/player/SHiNE/shine-server/application.properties}"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
require_file() {
|
||||
local path="$1"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
echo "ERROR: файл не найден: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "==> Проверка локального jar"
|
||||
require_file "$LOCAL_JAR"
|
||||
jar_size="$(stat -c %s "$LOCAL_JAR")"
|
||||
if [[ "$jar_size" -lt 10485760 ]]; then
|
||||
echo "ERROR: jar слишком маленький для fat-jar: $jar_size bytes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Проверка SSH и sudo"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "$PROD_HOST" "echo SSH OK" >/dev/null
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "$TEST_HOST" "echo SSH OK" >/dev/null
|
||||
ssh "$TEST_HOST" "sudo -n true"
|
||||
|
||||
echo "==> Подготовка Caddy для $TARGET_DOMAIN"
|
||||
TEST_HOST="$TEST_HOST" \
|
||||
TARGET_DOMAIN="$TARGET_DOMAIN" \
|
||||
REMOTE_UI_DIR="$REMOTE_UI_DIR" \
|
||||
bash "$(dirname "$0")/scripts/install_test_caddyfile.sh"
|
||||
|
||||
echo "==> Забираем продовые данные и application.properties"
|
||||
mkdir -p "$TMP_DIR/data"
|
||||
rsync -az --delete "$PROD_HOST:$PROD_DATA_DIR/" "$TMP_DIR/data/"
|
||||
scp -p "$PROD_HOST:$PROD_APP_PROPS" "$TMP_DIR/application.properties" >/dev/null
|
||||
|
||||
if grep -q '^server\.ui\.indexPath=' "$TMP_DIR/application.properties"; then
|
||||
perl -0pi -e 's@^server\.ui\.indexPath=.*$@server.ui.indexPath=/home/player/SHiNE/shine-ui/index.html@m' "$TMP_DIR/application.properties"
|
||||
else
|
||||
printf '\nserver.ui.indexPath=/home/player/SHiNE/shine-ui/index.html\n' >>"$TMP_DIR/application.properties"
|
||||
fi
|
||||
|
||||
cat >"$TMP_DIR/shine-server.service" <<EOF
|
||||
[Unit]
|
||||
Description=SHiNE Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=player
|
||||
Group=player
|
||||
WorkingDirectory=$REMOTE_SERVER_DIR
|
||||
ExecStart=/usr/bin/java -Dserver.port=7070 -jar $REMOTE_SERVER_DIR/shine-server.jar
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
StandardOutput=append:$REMOTE_LOGS_DIR/app.log
|
||||
StandardError=append:$REMOTE_LOGS_DIR/app.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo "==> Останавливаем текущий сервер на тестовом хосте"
|
||||
ssh "$TEST_HOST" "sudo systemctl stop $REMOTE_SERVICE_NAME || true"
|
||||
|
||||
echo "==> Создаём каталоги"
|
||||
ssh "$TEST_HOST" "mkdir -p '$REMOTE_SERVER_DIR' '$REMOTE_DATA_DIR' '$REMOTE_LOGS_DIR' '$REMOTE_UI_DIR' '$REMOTE_BASE/caddy'"
|
||||
|
||||
echo "==> Копируем продовую БД и blockchain-данные"
|
||||
rsync -az --delete "$TMP_DIR/data/" "$TEST_HOST:$REMOTE_DATA_DIR/"
|
||||
|
||||
echo "==> Загружаем новый jar и конфиг"
|
||||
rsync -az --timeout=120 "$LOCAL_JAR" "$TEST_HOST:$REMOTE_SERVER_DIR/shine-server.jar.new"
|
||||
rsync -az --timeout=30 "$TMP_DIR/application.properties" "$TEST_HOST:$REMOTE_SERVER_DIR/application.properties.new"
|
||||
rsync -az --timeout=30 "$TMP_DIR/shine-server.service" "$TEST_HOST:/tmp/shine-server.service.new"
|
||||
|
||||
echo "==> Применяем systemd unit и файлы сервера"
|
||||
ssh "$TEST_HOST" "set -euo pipefail; \
|
||||
mv -f '$REMOTE_SERVER_DIR/shine-server.jar.new' '$REMOTE_SERVER_DIR/shine-server.jar'; \
|
||||
mv -f '$REMOTE_SERVER_DIR/application.properties.new' '$REMOTE_SERVER_DIR/application.properties'; \
|
||||
sudo mv -f /tmp/shine-server.service.new /etc/systemd/system/shine-server.service; \
|
||||
sudo chown root:root /etc/systemd/system/shine-server.service; \
|
||||
chmod 644 '$REMOTE_SERVER_DIR/application.properties'; \
|
||||
chmod 664 '$REMOTE_SERVER_DIR/shine-server.jar'; \
|
||||
mkdir -p '$REMOTE_LOGS_DIR'; \
|
||||
sudo chown -R player:player '$REMOTE_SERVER_DIR'; \
|
||||
touch '$REMOTE_LOGS_DIR/app.log'; \
|
||||
sudo systemctl daemon-reload; \
|
||||
sudo systemctl enable '$REMOTE_SERVICE_NAME'; \
|
||||
sudo systemctl restart '$REMOTE_SERVICE_NAME'"
|
||||
|
||||
echo "==> Ждём порт 7070"
|
||||
for _ in $(seq 1 50); do
|
||||
if ssh "$TEST_HOST" "ss -ltn '( sport = :7070 )' | grep -q 7070"; then
|
||||
echo "==> Порт 7070 поднялся"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! ssh "$TEST_HOST" "ss -ltn '( sport = :7070 )' | grep -q 7070"; then
|
||||
echo "ERROR: тестовый сервер не поднял порт 7070" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Проверяем статус сервиса"
|
||||
ssh "$TEST_HOST" "sudo systemctl --no-pager --full status '$REMOTE_SERVICE_NAME' | sed -n '1,20p'"
|
||||
|
||||
echo "test_server_deploy_done"
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_HOST="${TARGET_HOST:-player@193.8.215.70}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-t.shineup.me}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-server2.shineup.me}"
|
||||
REMOTE_UI_DIR="${REMOTE_UI_DIR:-/home/player/SHiNE/shine-ui}"
|
||||
|
||||
TARGET_HOST="$TARGET_HOST" TARGET_DOMAIN="$TARGET_DOMAIN" REMOTE_UI_DIR="$REMOTE_UI_DIR" \
|
||||
@@ -12,8 +12,10 @@ REMOTE_HOST="$TARGET_HOST" \
|
||||
REMOTE_UI_DIR="$REMOTE_UI_DIR" \
|
||||
EXPECTED_CADDY_UI_ROOT="$REMOTE_UI_DIR" \
|
||||
EXPECTED_CADDY_SITE="$TARGET_DOMAIN" \
|
||||
DEPLOY_SERVER_LOGIN="tshineupme" \
|
||||
DEPLOY_SERVER_LOGIN="server2shineupme" \
|
||||
DEPLOY_SERVER_ADDRESS="$TARGET_DOMAIN" \
|
||||
DEPLOY_SOLANA_CLUSTER="mainnet" \
|
||||
DEPLOY_SOLANA_ENDPOINT="https://solana-rpc.publicnode.com" \
|
||||
TARGET_URL="https://$TARGET_DOMAIN" \
|
||||
bash "$(dirname "$0")/deploy_shine-PWA.sh"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="player@shineup.me" \
|
||||
REMOTE_UI_DIR="/home/player/SHiNE/shine-ui" \
|
||||
EXPECTED_CADDY_UI_ROOT="/home/player/SHiNE/shine-ui" \
|
||||
EXPECTED_CADDY_SITE="shineup.me" \
|
||||
DEPLOY_SERVER_LOGIN="shineupme" \
|
||||
DEPLOY_SERVER_ADDRESS="shineup.me" \
|
||||
DEPLOY_SOLANA_CLUSTER="mainnet" \
|
||||
DEPLOY_SOLANA_ENDPOINT="https://solana-rpc.publicnode.com" \
|
||||
TARGET_URL="https://shineup.me" \
|
||||
bash "$(dirname "$0")/deploy_shine-PWA.sh"
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="player@178.208.90.249" \
|
||||
REMOTE_UI_DIR="/home/player/t1/UI" \
|
||||
EXPECTED_CADDY_UI_ROOT="/home/player/t1/UI" \
|
||||
EXPECTED_CADDY_SITE="t1.shineup.me" \
|
||||
DEPLOY_SERVER_LOGIN="server_t1" \
|
||||
DEPLOY_SERVER_ADDRESS="t1.shineup.me" \
|
||||
DEPLOY_SOLANA_CLUSTER="devnet" \
|
||||
DEPLOY_SOLANA_ENDPOINT="https://api.devnet.solana.com" \
|
||||
TARGET_URL="https://t1.shineup.me" \
|
||||
bash "$(dirname "$0")/deploy_shine-PWA.sh"
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="player@178.208.90.249" \
|
||||
REMOTE_UI_DIR="/home/player/t2/UI" \
|
||||
EXPECTED_CADDY_UI_ROOT="/home/player/t2/UI" \
|
||||
EXPECTED_CADDY_SITE="t2.shineup.me" \
|
||||
DEPLOY_SERVER_LOGIN="server_t2" \
|
||||
DEPLOY_SERVER_ADDRESS="t2.shineup.me" \
|
||||
DEPLOY_SOLANA_CLUSTER="devnet" \
|
||||
DEPLOY_SOLANA_ENDPOINT="https://api.devnet.solana.com" \
|
||||
TARGET_URL="https://t2.shineup.me" \
|
||||
bash "$(dirname "$0")/deploy_shine-PWA.sh"
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="player@178.208.90.249" \
|
||||
REMOTE_UI_DIR="/home/player/t3/UI" \
|
||||
EXPECTED_CADDY_UI_ROOT="/home/player/t3/UI" \
|
||||
EXPECTED_CADDY_SITE="t3.shineup.me" \
|
||||
DEPLOY_SERVER_LOGIN="server_t3" \
|
||||
DEPLOY_SERVER_ADDRESS="t3.shineup.me" \
|
||||
DEPLOY_SOLANA_CLUSTER="devnet" \
|
||||
DEPLOY_SOLANA_ENDPOINT="https://api.devnet.solana.com" \
|
||||
TARGET_URL="https://t3.shineup.me" \
|
||||
bash "$(dirname "$0")/deploy_shine-PWA.sh"
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="player@178.208.90.249" \
|
||||
REMOTE_UI_DIR="/home/player/t4/UI" \
|
||||
EXPECTED_CADDY_UI_ROOT="/home/player/t4/UI" \
|
||||
EXPECTED_CADDY_SITE="t4.shineup.me" \
|
||||
DEPLOY_SERVER_LOGIN="server_t4" \
|
||||
DEPLOY_SERVER_ADDRESS="t4.shineup.me" \
|
||||
DEPLOY_SOLANA_CLUSTER="devnet" \
|
||||
DEPLOY_SOLANA_ENDPOINT="https://api.devnet.solana.com" \
|
||||
TARGET_URL="https://t4.shineup.me" \
|
||||
bash "$(dirname "$0")/deploy_shine-PWA.sh"
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TEST_HOST="${TEST_HOST:-player@93.170.12.154}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-test.shineup.me}"
|
||||
REMOTE_UI_DIR="${REMOTE_UI_DIR:-/home/player/SHiNE/shine-ui}"
|
||||
|
||||
echo "==> Подготовка Caddy для $TARGET_DOMAIN"
|
||||
TEST_HOST="$TEST_HOST" \
|
||||
TARGET_DOMAIN="$TARGET_DOMAIN" \
|
||||
REMOTE_UI_DIR="$REMOTE_UI_DIR" \
|
||||
bash "$(dirname "$0")/scripts/install_test_caddyfile.sh"
|
||||
|
||||
echo "==> Деплой UI на $TARGET_DOMAIN"
|
||||
REMOTE_HOST="$TEST_HOST" \
|
||||
REMOTE_UI_DIR="$REMOTE_UI_DIR" \
|
||||
EXPECTED_CADDY_UI_ROOT="$REMOTE_UI_DIR" \
|
||||
EXPECTED_CADDY_SITE="$TARGET_DOMAIN" \
|
||||
TARGET_URL="https://$TARGET_DOMAIN" \
|
||||
bash "$(dirname "$0")/deploy_shine-PWA.sh"
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
# 2026-06-26 17:45:18 +0400
|
||||
- Базовый коммит-ориентир: `44a1ba0`.
|
||||
- На `t.shineup.me` подтверждена рабочая схема startup sync и full-resync:
|
||||
- На `server2.shineup.me` подтверждена рабочая схема startup sync и full-resync:
|
||||
- после рестарта сервер добивает `BlockchainTmpRecovery` и `BlockchainResyncRecovery`;
|
||||
- `aidartest-001` успешно подтягивается с `shineup.me`;
|
||||
- итоговое локальное состояние по `aidartest-001` дошло до `last_block_number=13`.
|
||||
|
||||
@@ -244,7 +244,7 @@ Full resync запускается только тогда, когда:
|
||||
|
||||
### 8.1 Практическая проверка на тестовом сервере
|
||||
|
||||
Проверка на `t.shineup.me` показала, что текущая схема действительно поднимает цепочку при старте:
|
||||
Проверка на `server2.shineup.me` показала, что текущая схема действительно поднимает цепочку при старте:
|
||||
|
||||
- после рестарта сервер сначала проходит `BlockchainTmpRecovery`;
|
||||
- затем обрабатывает `BlockchainResyncRecovery`;
|
||||
|
||||
@@ -36,4 +36,4 @@
|
||||
## Примечание
|
||||
|
||||
Подробная схема размещения описана в:
|
||||
- [Dev_Docs/deploy/test-server/178.208.90.249_quad_devnet.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Dev_Docs/deploy/test-server/178.208.90.249_quad_devnet.md)
|
||||
- [docs/deploy/test-server/178.208.90.249_quad_devnet.md](/home/ai/work/SHiNE/SHiNE-server-sha256/docs/deploy/test-server/178.208.90.249_quad_devnet.md)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Регистрация: глаз у пароля, автопроверка логина и сохранение связки с 12 словами
|
||||
|
||||
- краткое описание:
|
||||
На экране регистрации добавлены кнопка-глаз у поля пароля, автопроверка логина с задержкой около 1.5 секунды и сохранение связки между обычным полем пароля и режимом `12 слов`, пока пользователь вручную не изменит пароль.
|
||||
- что проверять:
|
||||
Открыть экран регистрации и убедиться, что справа от поля пароля есть квадратная кнопка-глаз, которая переключает скрытый и открытый показ пароля.
|
||||
Включить режим `Представить пароль в виде 12 слов`, заполнить несколько или все слова, затем выключить режим и проверить, что в поле пароля появился склеенный пароль.
|
||||
Снова включить режим `12 слов` без ручного редактирования пароля и проверить, что ранее введённые слова восстановились.
|
||||
После этого вручную изменить пароль в обычном поле и снова включить режим `12 слов` — набор слов должен быть очищен.
|
||||
Ввести логин и перестать печатать примерно на 1.5 секунды — под полем логина должен автоматически появиться актуальный статус доступности.
|
||||
Проверить, что кнопка `Проверить логин` находится ниже статуса, а текст про частые вопросы снизу страницы отсутствует.
|
||||
- ожидаемый результат:
|
||||
Регистрация ведёт себя предсказуемо: пароль можно показать или скрыть, слова не теряются при простом скрытии режима `12 слов`, а статус логина обновляется автоматически и вручную.
|
||||
- статус:
|
||||
pending
|
||||
@@ -0,0 +1,23 @@
|
||||
# Экран проверки public Solana RPC в UI
|
||||
|
||||
- статус: `pending`
|
||||
|
||||
## Кратко
|
||||
|
||||
Добавлен отдельный экран разработчика для проверки публичных mainnet Solana RPC прямо из браузера.
|
||||
Экран отправляет реальный JSON-RPC `getVersion` запрос к списку public endpoint-ов и показывает,
|
||||
какие ноды реально подходят для browser use.
|
||||
|
||||
## Что проверять
|
||||
|
||||
- в `Настройки разработчика` появилась кнопка `Solana: проверить public RPC`;
|
||||
- экран открывается без ошибок;
|
||||
- по каждой mainnet ноде показывается итоговый статус;
|
||||
- для доступных нод видно HTTP-статус, время ответа и версию `solana-core`;
|
||||
- длинные URL и ошибки не распирают экран по ширине на телефоне.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
- можно визуально увидеть, какие public Solana RPC доступны именно из браузера;
|
||||
- проверка работает без промежуточного сервера;
|
||||
- экран остаётся mobile-first и не требует горизонтального скролла.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
## Кратко
|
||||
- Переделан финальный flow регистрации: сначала экран ожидания подтверждения Solana с прогрессом и повторными проверками, потом отдельный экран успешной регистрации с кнопкой входа в стандартный экран сохранения ключей.
|
||||
|
||||
## Что проверять
|
||||
- После отправки регистрации открывается экран ожидания с текстом про подтверждение Solana и кликабельным `Tx ID`.
|
||||
- Прогресс-бар сначала идёт быстрее, потом заметно замедляется.
|
||||
- Первая проверка регистрации начинается примерно через 4 секунды, далее повторяется раз в 2 секунды.
|
||||
- Пока подтверждения нет, снизу показываются понятные статусы ожидания.
|
||||
- После подтверждения открывается экран «Поздравляем с регистрацией» с одной кнопкой `Войти в аккаунт`.
|
||||
- Кнопка `Войти в аккаунт` открывает штатный экран сохранения ключей с тремя галочками.
|
||||
- Стрелка назад в левом верхнем углу на обоих экранах возвращает в главное меню.
|
||||
- После успешного сохранения ключей регистрационный черновик и адрес кошелька очищаются.
|
||||
|
||||
## Ожидаемый результат
|
||||
- Пользователь не видит преждевременное поздравление до подтверждения регистрации в Solana.
|
||||
- Финальный успех показывается только после появления подтверждения регистрации.
|
||||
- Вход после регистрации идёт через тот же стандартный сценарий сохранения ключей, что и в обычном login-flow.
|
||||
|
||||
## Статус
|
||||
- pending
|
||||
+6
-21
@@ -21,35 +21,26 @@
|
||||
- IP: `178.208.64.62`
|
||||
- Second production:
|
||||
- SSH: `player@193.8.215.70`
|
||||
- Домен: `t.shineup.me`
|
||||
- Домен: `server2.shineup.me`
|
||||
- IP: `193.8.215.70`
|
||||
- Reserve test (temporarily down):
|
||||
- SSH: `player@93.170.12.154`
|
||||
- Домен: `test.shineup.me`
|
||||
- IP: `93.170.12.154`
|
||||
|
||||
## Локальные команды
|
||||
|
||||
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`
|
||||
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`
|
||||
- Production server deploy: `./gradlew deployServerProduction`
|
||||
- Production UI deploy: `./gradlew deployUIProduction`
|
||||
- Reserve test server deploy: `./gradlew deployServerTest`
|
||||
- Reserve test UI deploy: `./gradlew deployUITest`
|
||||
- Локальный запуск: `./gradlew startLocal`
|
||||
|
||||
## Отдельные тестовые VPS
|
||||
|
||||
- Отдельный стенд с 4 devnet-серверами на одном VPS описан в:
|
||||
- `Dev_Docs/deploy/test-server/README.md`
|
||||
- `docs/deploy/test-server/README.md`
|
||||
|
||||
## Политика подтверждений
|
||||
|
||||
- `shineup.me` и `t.shineup.me` считать production-контурами.
|
||||
- `shineup.me` и `server2.shineup.me` считать production-контурами.
|
||||
- Любые изменения на production-контурах, включая deploy сервера, deploy UI, конфиги, перезапуски и миграции, делать только после отдельного явного подтверждения пользователя.
|
||||
- `test.shineup.me` считать резервным тестовым хостом, который сейчас временно не работает.
|
||||
|
||||
## Second production deploy (`t.shineup.me`)
|
||||
## Second production deploy (`server2.shineup.me`)
|
||||
|
||||
- Это второй production-сервер SHiNE.
|
||||
- Исторические задачи `deployServer`, `deployUI`, `deployServerTest2`, `deployUITest2` по-прежнему могут указывать сюда.
|
||||
@@ -60,15 +51,9 @@
|
||||
- перенос `application.properties` с production с поправкой `server.ui.indexPath` на `/home/player/SHiNE/shine-ui/index.html`;
|
||||
- установку `systemd` unit на `193.8.215.70`;
|
||||
- перезапуск `shine-server.service`;
|
||||
- установку/проверку Caddy для `t.shineup.me`.
|
||||
- установку/проверку Caddy для `server2.shineup.me`.
|
||||
- `deployUI` / `deployUITest2` публикуют UI в `/home/player/SHiNE/shine-ui` на `193.8.215.70`.
|
||||
|
||||
## Reserve test deploy (`test.shineup.me`)
|
||||
|
||||
- `test.shineup.me` считается резервным тестовым сервером.
|
||||
- Сейчас он помечен как временно неработающий.
|
||||
- Его настройки и адрес не менять без отдельной задачи.
|
||||
- Задачи `deployServerTest` и `deployUITest` считаются резервными и требуют отдельной причины.
|
||||
- Важно: историческое имя `deployUITest2` вводит в заблуждение. По умолчанию эта задача деплоит на `server2.shineup.me`, а не на `t2.shineup.me`.
|
||||
|
||||
## UI-деплой и Caddy (обязательно)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Сервер `193.8.215.70` — второй production (`t.shineup.me`)
|
||||
# Сервер `193.8.215.70` — второй production (`server2.shineup.me`)
|
||||
|
||||
- Пользователь: `player`
|
||||
- Домен: `t.shineup.me`
|
||||
- Логин сервера: `tshineupme`
|
||||
- Домен: `server2.shineup.me`
|
||||
- Логин сервера: `server2shineupme`
|
||||
- Каталог SHiNE: `/home/player/SHiNE`
|
||||
- UI публикация для Caddy: `/home/player/SHiNE/shine-ui`
|
||||
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
- Конфиг: `/etc/caddy/Caddyfile`
|
||||
- Сайты:
|
||||
- `t.shineup.me`
|
||||
- `server2.shineup.me`
|
||||
- `agent.shiningpeople.ru`
|
||||
- Для `t.shineup.me`:
|
||||
- Для `server2.shineup.me`:
|
||||
- `root * /home/player/SHiNE/shine-ui`
|
||||
- `try_files {path} /index.html`
|
||||
- `reverse_proxy /ws* -> 127.0.0.1:7070`
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Сервер `93.170.12.154` — резервный test (`test.shineup.me`)
|
||||
|
||||
- Пользователь: `player`
|
||||
- Каталог SHiNE: `/home/player/SHiNE`
|
||||
- Домен: `test.shineup.me`
|
||||
- Роль: резервный тестовый сервер
|
||||
- UI публикация для Caddy: `/home/player/SHiNE/shine-ui`
|
||||
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||
- Данные: `/home/player/SHiNE/shine-server/data/`
|
||||
- `shine.sqlite`
|
||||
- `*.bch`
|
||||
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||
|
||||
## Сервисы
|
||||
|
||||
- `shine-server.service` (systemd)
|
||||
- `caddy.service` (systemd)
|
||||
|
||||
## Статус
|
||||
|
||||
- Резервный тестовый сервер для SHiNE.
|
||||
- Сейчас помечен как временно неработающий.
|
||||
- Источник данных для тестовой БД: production `shineup.me`.
|
||||
- Пока не использовать для обычного deploy.
|
||||
- Основной прод-сервер: `shineup.me` (`185.229.109.118`).
|
||||
|
||||
## Caddy
|
||||
|
||||
- Конфиг: `/etc/caddy/Caddyfile`
|
||||
- Настройки:
|
||||
- `no-store/no-cache` заголовки;
|
||||
- `try_files {path} /index.html` (SPA fallback);
|
||||
- `reverse_proxy /ws* -> 127.0.0.1:7070`;
|
||||
- целевой сайт: `test.shineup.me`.
|
||||
|
||||
## Deploy
|
||||
|
||||
- Резервные задачи:
|
||||
- `./gradlew deployServerTest`
|
||||
- `./gradlew deployUITest`
|
||||
- Эти задачи пока не использовать без отдельной причины.
|
||||
@@ -9,4 +9,4 @@
|
||||
- внешний HTTPS и маршрутизацию `/ws` обслуживает один общий `Caddy`
|
||||
|
||||
Детальное описание:
|
||||
- [178.208.90.249_quad_devnet.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Dev_Docs/deploy/test-server/178.208.90.249_quad_devnet.md)
|
||||
- [178.208.90.249_quad_devnet.md](/home/ai/work/SHiNE/SHiNE-server-sha256/docs/deploy/test-server/178.208.90.249_quad_devnet.md)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_HOST="${TARGET_HOST:-player@193.8.215.70}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-t.shineup.me}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-server2.shineup.me}"
|
||||
REMOTE_UI_DIR="${REMOTE_UI_DIR:-/home/player/SHiNE/shine-ui}"
|
||||
REMOTE_CADDYFILE="${REMOTE_CADDYFILE:-/etc/caddy/Caddyfile}"
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TEST_HOST="${TEST_HOST:-player@93.170.12.154}"
|
||||
TARGET_DOMAIN="${TARGET_DOMAIN:-test.shineup.me}"
|
||||
REMOTE_UI_DIR="${REMOTE_UI_DIR:-/home/player/SHiNE/shine-ui}"
|
||||
REMOTE_CADDYFILE="${REMOTE_CADDYFILE:-/etc/caddy/Caddyfile}"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cat >"$TMP_DIR/Caddyfile" <<EOF
|
||||
{
|
||||
auto_https disable_redirects
|
||||
}
|
||||
|
||||
$TARGET_DOMAIN {
|
||||
encode zstd gzip
|
||||
|
||||
@ws path /ws /ws/*
|
||||
handle @ws {
|
||||
reverse_proxy 127.0.0.1:7070
|
||||
}
|
||||
|
||||
handle {
|
||||
root * $REMOTE_UI_DIR
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
header -Etag
|
||||
header {
|
||||
Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
||||
Pragma "no-cache"
|
||||
Expires "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
encode zstd gzip
|
||||
|
||||
@ws path /ws /ws/*
|
||||
handle @ws {
|
||||
reverse_proxy 127.0.0.1:7070
|
||||
}
|
||||
|
||||
handle {
|
||||
root * $REMOTE_UI_DIR
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
header -Etag
|
||||
header {
|
||||
Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
||||
Pragma "no-cache"
|
||||
Expires "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "==> Проверка SSH и sudo на $TEST_HOST"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "$TEST_HOST" "echo SSH OK" >/dev/null
|
||||
ssh "$TEST_HOST" "sudo -n true"
|
||||
|
||||
echo "==> Установка Caddy-конфига для $TARGET_DOMAIN"
|
||||
scp -p "$TMP_DIR/Caddyfile" "$TEST_HOST:/tmp/shine-test-caddyfile.new" >/dev/null
|
||||
ssh "$TEST_HOST" "sudo mkdir -p \"$(dirname "$REMOTE_CADDYFILE")\" && \
|
||||
sudo mv -f /tmp/shine-test-caddyfile.new \"$REMOTE_CADDYFILE\" && \
|
||||
sudo chown root:root \"$REMOTE_CADDYFILE\" && \
|
||||
sudo caddy validate --config \"$REMOTE_CADDYFILE\" && \
|
||||
sudo systemctl reload caddy"
|
||||
|
||||
echo "==> Caddy настроен для $TARGET_DOMAIN"
|
||||
@@ -72,6 +72,7 @@ import * as languageView from './pages/language-view.js';
|
||||
import * as appLogView from './pages/app-log-view.js';
|
||||
import * as pwaDiagnosticsView from './pages/pwa-diagnostics-view.js';
|
||||
import * as solanaUsersInitView from './pages/solana-users-init-view.js';
|
||||
import * as solanaRpcCheckView from './pages/solana-rpc-check-view.js';
|
||||
import * as messagesList from './pages/messages-list.js';
|
||||
import * as contactSearchView from './pages/contact-search-view.js';
|
||||
import * as chatView from './pages/chat-view.js';
|
||||
@@ -132,6 +133,7 @@ const routes = {
|
||||
'app-log-view': appLogView,
|
||||
'pwa-diagnostics-view': pwaDiagnosticsView,
|
||||
'solana-users-init-view': solanaUsersInitView,
|
||||
'solana-rpc-check-view': solanaRpcCheckView,
|
||||
'messages-list': messagesList,
|
||||
'contact-search-view': contactSearchView,
|
||||
'chat-view': chatView,
|
||||
|
||||
@@ -264,6 +264,7 @@ export function render({ navigate }) {
|
||||
<button class="text-btn" type="button" id="settings-force-update-help">Клиент не обновляется?</button>
|
||||
<button class="text-btn" type="button" id="settings-ui-error-reporting">Отправлять ошибки на сервер</button>
|
||||
<button class="text-btn" type="button" id="settings-solana-users-init">Solana: init регистрации</button>
|
||||
<button class="text-btn" type="button" id="settings-solana-rpc-check">Solana: проверить public RPC</button>
|
||||
<button class="text-btn" type="button" id="settings-app-log">Лог приложения</button>
|
||||
<button class="text-btn" type="button" id="settings-pwa-diagnostics">Диагностика PWA / Push</button>
|
||||
<button class="text-btn" type="button" id="settings-pwa-install">Как установить PWA</button>
|
||||
@@ -278,9 +279,11 @@ export function render({ navigate }) {
|
||||
const forceUpdateHelpBtn = card.querySelector('#settings-force-update-help');
|
||||
const uiErrorReportingBtn = card.querySelector('#settings-ui-error-reporting');
|
||||
const solanaUsersInitBtn = card.querySelector('#settings-solana-users-init');
|
||||
const solanaRpcCheckBtn = card.querySelector('#settings-solana-rpc-check');
|
||||
|
||||
appLogBtn?.addEventListener('click', () => navigate('app-log-view'));
|
||||
solanaUsersInitBtn?.addEventListener('click', () => navigate('solana-users-init-view'));
|
||||
solanaRpcCheckBtn?.addEventListener('click', () => navigate('solana-rpc-check-view'));
|
||||
diagnosticsBtn?.addEventListener('click', () => navigate('pwa-diagnostics-view'));
|
||||
uploadAvatarBtn?.addEventListener('click', () => {
|
||||
openDeveloperAvatarUploadModal({
|
||||
|
||||
@@ -103,7 +103,9 @@ export function render({ navigate }) {
|
||||
draft.shineServer = resolved.wsUrl;
|
||||
applyStatus(resolved.status, resolved.httpBase);
|
||||
} else {
|
||||
const next = await checkServerAvailabilityByKey(field.key, input.value);
|
||||
const next = await checkServerAvailabilityByKey(field.key, input.value, {
|
||||
solanaEndpoint: draft.solanaServer,
|
||||
});
|
||||
applyStatus(next);
|
||||
}
|
||||
} finally {
|
||||
@@ -171,16 +173,6 @@ export function render({ navigate }) {
|
||||
|
||||
actions.append(serverUiButton, cancelButton, saveButton);
|
||||
|
||||
const help = document.createElement('button');
|
||||
help.className = 'help-fab';
|
||||
help.type = 'button';
|
||||
help.textContent = '?';
|
||||
help.addEventListener('click', () => {
|
||||
window.alert(
|
||||
'Текст для разработчиков: для SHiNE вводится логин серверного аккаунта. Клиент читает его PDA, берёт server_address, показывает точный https-адрес и проверяет доступность WS-канала автоматически.',
|
||||
);
|
||||
});
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Настройки входа',
|
||||
@@ -188,7 +180,6 @@ export function render({ navigate }) {
|
||||
}),
|
||||
body,
|
||||
actions,
|
||||
help,
|
||||
);
|
||||
|
||||
screen.cleanup = () => {
|
||||
|
||||
@@ -163,6 +163,7 @@ export function render({ navigate }) {
|
||||
words: passwordWords,
|
||||
onInput: (index, value) => {
|
||||
passwordWords[index] = value;
|
||||
passwordInput.value = composePasswordFromWords(passwordWords);
|
||||
passwordWordsLinked = true;
|
||||
syncDraftState();
|
||||
updateWordsPreview();
|
||||
@@ -258,11 +259,13 @@ export function render({ navigate }) {
|
||||
let generationRunId = 0;
|
||||
|
||||
function getCurrentPassword() {
|
||||
return passwordMode === 'words' ? composePasswordFromWords(passwordWords) : String(passwordInput.value || '');
|
||||
return String(passwordInput.value || '');
|
||||
}
|
||||
|
||||
function updateWordsPreview() {
|
||||
const password = getCurrentPassword();
|
||||
const password = passwordMode === 'words'
|
||||
? composePasswordFromWords(passwordWords)
|
||||
: String(passwordInput.value || '');
|
||||
const text = `Итоговая длина пароля: ${password.length} символов.`;
|
||||
wordsPreview.textContent = text;
|
||||
passwordLengthText.textContent = text;
|
||||
@@ -306,8 +309,8 @@ export function render({ navigate }) {
|
||||
function updatePasswordModeVisibility() {
|
||||
const wordsMode = passwordMode === 'words';
|
||||
wordsSection.style.display = wordsMode ? 'grid' : 'none';
|
||||
if (passwordField) passwordField.style.display = wordsMode ? 'none' : 'grid';
|
||||
passwordInputRow.style.display = wordsMode ? 'grid' : 'none';
|
||||
if (passwordField) passwordField.style.display = 'grid';
|
||||
passwordInputRow.style.display = 'grid';
|
||||
updateWordsPreview();
|
||||
}
|
||||
|
||||
@@ -397,7 +400,6 @@ export function render({ navigate }) {
|
||||
if (runId !== loginCheckRunId) return false;
|
||||
const isFree = !check.exists;
|
||||
let className = '';
|
||||
let precheckWarning = '';
|
||||
if (isFree) {
|
||||
if (promoAccepted) {
|
||||
className = 'promo';
|
||||
@@ -410,7 +412,7 @@ export function render({ navigate }) {
|
||||
className = precheck.className;
|
||||
} catch (precheckError) {
|
||||
className = 'free';
|
||||
precheckWarning = formatSolanaErrorDetails(precheckError);
|
||||
console.warn('Solana login precheck fallback to free', formatSolanaErrorDetails(precheckError));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,12 +424,7 @@ export function render({ navigate }) {
|
||||
} else if (className === 'promo') {
|
||||
setStatusMessage('Логин свободен ✅ Временный код принят', 'is-available');
|
||||
} else if (className === 'free') {
|
||||
setStatusMessage(
|
||||
precheckWarning
|
||||
? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})`
|
||||
: 'Логин свободен ✅',
|
||||
'is-available',
|
||||
);
|
||||
setStatusMessage('Логин свободен ✅', 'is-available');
|
||||
} else if (className === 'premium') {
|
||||
setStatusMessage('Логин свободен, но это премиум-логин (покупка через DAO) ❌', 'is-unavailable');
|
||||
} else if (className === 'company') {
|
||||
@@ -480,11 +477,11 @@ export function render({ navigate }) {
|
||||
wordInputs.forEach((input) => {
|
||||
input.value = '';
|
||||
});
|
||||
passwordInput.value = '';
|
||||
} else {
|
||||
passwordInput.value = composePasswordFromWords(passwordWords);
|
||||
}
|
||||
} else {
|
||||
} else if (passwordWordsLinked) {
|
||||
passwordInput.value = composePasswordFromWords(passwordWords);
|
||||
passwordWordsLinked = true;
|
||||
}
|
||||
passwordMode = nextMode;
|
||||
updatePasswordModeVisibility();
|
||||
|
||||
@@ -162,6 +162,9 @@ export function render({ navigate }) {
|
||||
state.registrationDraft.sessionId = '';
|
||||
state.registrationDraft.pendingKeyBundle = null;
|
||||
state.registrationDraft.pendingSessionMaterial = null;
|
||||
state.registrationDraft.preGeneratedKeyBundle = null;
|
||||
state.registrationPayment.walletAddress = '';
|
||||
state.registrationPayment.balanceSOL = '0.0000';
|
||||
|
||||
await refreshSessions();
|
||||
setAuthInfo(isLoginFlow
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import {
|
||||
authService,
|
||||
authorizeSession,
|
||||
refreshSessions,
|
||||
setAuthError,
|
||||
setAuthInfo,
|
||||
state,
|
||||
@@ -16,6 +14,7 @@ import {
|
||||
} from '../services/solana-wallet-service.js';
|
||||
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
|
||||
import {
|
||||
checkLoginExistsOnSolana,
|
||||
formatSolanaErrorDetails,
|
||||
isUserAlreadyExistsSolanaError,
|
||||
registerUserOnSolana,
|
||||
@@ -24,8 +23,30 @@ import { defaultServerLogin } from '../deploy-config.js';
|
||||
|
||||
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
||||
const MIN_REQUIRED_SOL = 0.01;
|
||||
const SOLANA_SYNC_WAIT_SEC = 12;
|
||||
const EMPTY_PASSWORD_WORDS = Array.from({ length: 12 }, () => '');
|
||||
const REGISTRATION_PROGRESS_DURATION_MS = 12000;
|
||||
const REGISTRATION_POLL_START_DELAY_MS = 4000;
|
||||
const REGISTRATION_POLL_INTERVAL_MS = 2000;
|
||||
const REGISTRATION_CONFIRM_TIMEOUT_MS = 25000;
|
||||
const REGISTRATION_SUCCESS_SETTLE_DELAY_MS = 2000;
|
||||
|
||||
function getExplorerClusterName(endpoint) {
|
||||
const source = String(endpoint || '').trim().toLowerCase();
|
||||
if (!source) return 'mainnet-beta';
|
||||
if (source.includes('devnet')) return 'devnet';
|
||||
if (source.includes('testnet')) return 'testnet';
|
||||
return 'mainnet-beta';
|
||||
}
|
||||
|
||||
function makeSolanaExplorerTxUrl(signature, endpoint) {
|
||||
const cleanSignature = String(signature || '').trim();
|
||||
if (!cleanSignature) return '';
|
||||
const cluster = getExplorerClusterName(endpoint);
|
||||
const url = new URL(`https://explorer.solana.com/tx/${encodeURIComponent(cleanSignature)}`);
|
||||
if (cluster !== 'mainnet-beta') {
|
||||
url.searchParams.set('cluster', cluster);
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function parseBalanceSol(value) {
|
||||
const parsed = Number.parseFloat(String(value || '').replace(',', '.'));
|
||||
@@ -40,70 +61,23 @@ function getCryptoRuntimeState() {
|
||||
return { hasCrypto, hasGetRandomValues, hasSubtle, secureContext };
|
||||
}
|
||||
|
||||
async function completeRegistrationLogin({ navigate, keyBundle }) {
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
async function prepareRegistrationLoginForKeyStorage({ keyBundle }) {
|
||||
await authService.reconnect(state.entrySettings.shineServer);
|
||||
const result = await authService.createSessionForExistingUser(
|
||||
state.registrationDraft.login,
|
||||
state.registrationDraft.password,
|
||||
);
|
||||
|
||||
state.keyStorage.saveRoot = Boolean(state.keyStorage.saveRoot);
|
||||
state.keyStorage.saveBlockchain = Boolean(state.keyStorage.saveBlockchain);
|
||||
|
||||
await authService.persistSelectedKeys(
|
||||
result.login,
|
||||
result.storagePwd,
|
||||
keyBundle,
|
||||
{
|
||||
saveRoot: state.keyStorage.saveRoot,
|
||||
saveBlockchain: state.keyStorage.saveBlockchain,
|
||||
},
|
||||
);
|
||||
await authService.persistSessionMaterial(result.login, result.sessionMaterial);
|
||||
await clearStoredMessages().catch(() => {});
|
||||
|
||||
const resumed = await authService.resumeSession(result.login, result.sessionId);
|
||||
const resumedLogin = resumed.login || result.login;
|
||||
const resumedSessionId = resumed.sessionId || result.sessionId;
|
||||
const resumedStoragePwd = resumed.storagePwd || result.storagePwd;
|
||||
|
||||
authorizeSession({
|
||||
login: resumedLogin,
|
||||
sessionId: resumedSessionId,
|
||||
storagePwd: resumedStoragePwd,
|
||||
});
|
||||
|
||||
state.loginDraft.login = resumedLogin;
|
||||
state.loginDraft.password = '';
|
||||
state.loginDraft.passwordMode = 'single';
|
||||
state.loginDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice();
|
||||
|
||||
state.registrationDraft.flowType = '';
|
||||
state.registrationDraft.password = '';
|
||||
state.registrationDraft.passwordMode = 'single';
|
||||
state.registrationDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice();
|
||||
state.registrationDraft.passwordWordsLinked = false;
|
||||
state.registrationDraft.usePromoCode = false;
|
||||
state.registrationDraft.promoCode = '';
|
||||
state.registrationDraft.storagePwd = '';
|
||||
state.registrationDraft.sessionId = '';
|
||||
state.registrationDraft.pendingKeyBundle = null;
|
||||
state.registrationDraft.pendingSessionMaterial = null;
|
||||
state.registrationDraft.preGeneratedKeyBundle = null;
|
||||
|
||||
state.registrationPayment.walletAddress = '';
|
||||
state.registrationPayment.balanceSOL = '0.0000';
|
||||
|
||||
await refreshSessions();
|
||||
setAuthInfo(`Регистрация завершена. Вы автоматически вошли как @${resumedLogin}.`);
|
||||
|
||||
const nextHash = String(state.authReturnHash || '').trim();
|
||||
state.authReturnHash = '';
|
||||
if (nextHash.startsWith('/')) {
|
||||
navigate(nextHash.slice(1));
|
||||
} else {
|
||||
navigate('profile-view');
|
||||
}
|
||||
state.registrationDraft.flowType = 'register';
|
||||
state.registrationDraft.login = result.login;
|
||||
state.registrationDraft.sessionId = result.sessionId;
|
||||
state.registrationDraft.storagePwd = result.storagePwd;
|
||||
state.registrationDraft.pendingKeyBundle = keyBundle || result.keyBundle;
|
||||
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
@@ -261,14 +235,16 @@ export function render({ navigate }) {
|
||||
|
||||
// Регистрация на Solana (смарт контракт)
|
||||
submitButton.textContent = 'Регистрация в Solana...';
|
||||
let registrationTxId = '';
|
||||
try {
|
||||
await registerUserOnSolana({
|
||||
const registrationResult = await registerUserOnSolana({
|
||||
login: state.registrationDraft.login,
|
||||
keyBundle,
|
||||
solanaEndpoint: state.entrySettings.solanaServer,
|
||||
accessServers: [state.entrySettings.shineServerLogin || defaultServerLogin],
|
||||
promoCode: '',
|
||||
});
|
||||
registrationTxId = String(registrationResult?.signature || '').trim();
|
||||
} catch (solanaError) {
|
||||
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
||||
// Пользователь уже зарегистрирован в Solana — продолжаем
|
||||
@@ -277,7 +253,7 @@ export function render({ navigate }) {
|
||||
}
|
||||
}
|
||||
|
||||
renderSolanaDoneStage({ navigate, status, keyBundle });
|
||||
renderSolanaRegistrationStage({ navigate, status, keyBundle, registrationTxId });
|
||||
} catch (error) {
|
||||
const message = toUserMessage(error, 'Не удалось завершить регистрацию.');
|
||||
setAuthError(message);
|
||||
@@ -323,88 +299,261 @@ export function render({ navigate }) {
|
||||
return screen;
|
||||
}
|
||||
|
||||
function renderSolanaDoneStage({ navigate, status, keyBundle }) {
|
||||
function renderSolanaRegistrationStage({ navigate, status, keyBundle, registrationTxId = '' }) {
|
||||
const screen = document.querySelector('section.stack');
|
||||
if (!screen) return;
|
||||
const headerBackButton = screen.querySelector('.page-header .header-left .icon-btn');
|
||||
const card = screen.querySelector('.card.stack');
|
||||
if (!card) return;
|
||||
card.classList.add('registration-finish-card');
|
||||
|
||||
let progressTimerId = null;
|
||||
let pollStartTimerId = null;
|
||||
let pollTimerId = null;
|
||||
let successTimerId = null;
|
||||
let confirmationInFlight = false;
|
||||
let stageClosed = false;
|
||||
let successPending = false;
|
||||
let timeoutShown = false;
|
||||
const stageStartedAt = Date.now();
|
||||
const txExplorerUrl = makeSolanaExplorerTxUrl(registrationTxId, state.entrySettings.solanaServer);
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'registration-finish-title';
|
||||
title.textContent = 'Идёт регистрация в блокчейне...';
|
||||
|
||||
const hint = document.createElement('p');
|
||||
hint.className = 'auth-copy registration-finish-text';
|
||||
hint.textContent = 'Ждём, пока транзакция будет полностью одобрена сетью Solana.';
|
||||
|
||||
const txIdLine = document.createElement('p');
|
||||
txIdLine.className = 'meta-muted registration-finish-tx';
|
||||
if (registrationTxId && txExplorerUrl) {
|
||||
const txLabel = document.createElement('span');
|
||||
txLabel.textContent = 'Tx ID регистрации: ';
|
||||
const txLink = document.createElement('a');
|
||||
txLink.className = 'registration-finish-tx-link';
|
||||
txLink.href = txExplorerUrl;
|
||||
txLink.target = '_blank';
|
||||
txLink.rel = 'noopener noreferrer';
|
||||
txLink.textContent = registrationTxId;
|
||||
txIdLine.append(txLabel, txLink);
|
||||
} else {
|
||||
txIdLine.textContent = 'Tx ID регистрации: ожидаем подтверждённую подпись';
|
||||
}
|
||||
|
||||
const progressWrap = document.createElement('div');
|
||||
progressWrap.className = 'registration-progress';
|
||||
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.className = 'registration-progress-bar';
|
||||
progressWrap.append(progressBar);
|
||||
|
||||
const progress = document.createElement('p');
|
||||
progress.className = 'meta-muted registration-finish-progress';
|
||||
progress.textContent = 'Подготавливаем проверку подтверждения...';
|
||||
|
||||
const pollStatus = document.createElement('p');
|
||||
pollStatus.className = 'meta-muted registration-finish-progress';
|
||||
pollStatus.textContent = 'Через несколько секунд начнём проверять подтверждение регистрации.';
|
||||
|
||||
const stopTimers = () => {
|
||||
if (progressTimerId) {
|
||||
window.clearInterval(progressTimerId);
|
||||
progressTimerId = null;
|
||||
}
|
||||
if (pollStartTimerId) {
|
||||
window.clearTimeout(pollStartTimerId);
|
||||
pollStartTimerId = null;
|
||||
}
|
||||
if (pollTimerId) {
|
||||
window.clearInterval(pollTimerId);
|
||||
pollTimerId = null;
|
||||
}
|
||||
if (successTimerId) {
|
||||
window.clearTimeout(successTimerId);
|
||||
successTimerId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const updateVisualProgress = () => {
|
||||
const elapsed = Date.now() - stageStartedAt;
|
||||
let widthPercent = 0;
|
||||
if (elapsed <= REGISTRATION_PROGRESS_DURATION_MS) {
|
||||
const phaseRatio = clamp(elapsed / REGISTRATION_PROGRESS_DURATION_MS, 0, 1);
|
||||
widthPercent = 100 * (1 - ((1 - phaseRatio) ** 1.85));
|
||||
} else {
|
||||
const extraRatio = 1 - Math.exp(-(elapsed - REGISTRATION_PROGRESS_DURATION_MS) / 9000);
|
||||
widthPercent = 88 + (10 * clamp(extraRatio, 0, 1));
|
||||
}
|
||||
progressBar.style.width = `${clamp(widthPercent, 0, 98)}%`;
|
||||
|
||||
if (elapsed < REGISTRATION_POLL_START_DELAY_MS) {
|
||||
progress.textContent = 'Отправили регистрацию в сеть. Даём транзакции несколько секунд на обработку.';
|
||||
return;
|
||||
}
|
||||
if (elapsed < REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
||||
progress.textContent = 'Идёт ожидание подтверждения Solana. Индикатор может замедлиться, это нормально.';
|
||||
return;
|
||||
}
|
||||
progress.textContent = 'Подтверждение затянулось. Продолжаем автоматическую проверку.';
|
||||
};
|
||||
|
||||
const finalizeSuccess = () => {
|
||||
if (stageClosed || successPending) return;
|
||||
successPending = true;
|
||||
stopTimers();
|
||||
progress.textContent = 'Подтверждение получено. Завершаем регистрацию...';
|
||||
pollStatus.textContent = 'Ещё секунду: запись уже найдена в Solana, синхронизируем финальный экран.';
|
||||
status.style.display = 'none';
|
||||
progressBar.style.transition = `width ${REGISTRATION_SUCCESS_SETTLE_DELAY_MS}ms ease-out`;
|
||||
progressBar.style.width = '100%';
|
||||
successTimerId = window.setTimeout(() => {
|
||||
if (stageClosed) return;
|
||||
stageClosed = true;
|
||||
renderRegistrationSuccessStage({ navigate, status, keyBundle, registrationTxId });
|
||||
}, REGISTRATION_SUCCESS_SETTLE_DELAY_MS);
|
||||
};
|
||||
|
||||
const showTimeoutState = () => {
|
||||
if (timeoutShown || stageClosed) return;
|
||||
timeoutShown = true;
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = 'Подтверждение не пришло вовремя. Возможно, регистрация уже прошла, а возможно ещё нет. Проверьте Tx ID ниже: мы продолжим автоматическую проверку.';
|
||||
status.style.display = '';
|
||||
pollStatus.textContent = 'Пока подтверждения нет. Подождите ещё секундочку, мы продолжаем проверять регистрацию.';
|
||||
};
|
||||
|
||||
const tryCheckRegistration = async () => {
|
||||
if (stageClosed || confirmationInFlight) return;
|
||||
confirmationInFlight = true;
|
||||
progress.textContent = 'Проверяем подтверждение регистрации в Solana...';
|
||||
status.style.display = timeoutShown ? '' : 'none';
|
||||
try {
|
||||
const result = await checkLoginExistsOnSolana({
|
||||
login: state.registrationDraft.login,
|
||||
solanaEndpoint: state.entrySettings.solanaServer,
|
||||
});
|
||||
if (result?.exists) {
|
||||
finalizeSuccess();
|
||||
return;
|
||||
}
|
||||
pollStatus.textContent = 'Пока ещё не прошла, подождите ещё секундочку...';
|
||||
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
||||
showTimeoutState();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Registration confirmation check failed', toUserMessage(error, 'registration-confirmation'));
|
||||
pollStatus.textContent = 'Проверяем регистрацию повторно...';
|
||||
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
||||
showTimeoutState();
|
||||
}
|
||||
} finally {
|
||||
confirmationInFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
if (headerBackButton) {
|
||||
const replacement = headerBackButton.cloneNode(true);
|
||||
replacement.addEventListener('click', () => {
|
||||
stageClosed = true;
|
||||
stopTimers();
|
||||
navigate('start-view');
|
||||
});
|
||||
headerBackButton.replaceWith(replacement);
|
||||
}
|
||||
|
||||
card.innerHTML = '';
|
||||
status.style.display = 'none';
|
||||
card.append(title, hint, txIdLine, progressWrap, progress, pollStatus, status);
|
||||
|
||||
updateVisualProgress();
|
||||
progressTimerId = window.setInterval(() => {
|
||||
if (stageClosed) return;
|
||||
updateVisualProgress();
|
||||
if ((Date.now() - stageStartedAt) >= REGISTRATION_CONFIRM_TIMEOUT_MS) {
|
||||
showTimeoutState();
|
||||
}
|
||||
}, 160);
|
||||
|
||||
pollStartTimerId = window.setTimeout(() => {
|
||||
if (stageClosed) return;
|
||||
void tryCheckRegistration();
|
||||
pollTimerId = window.setInterval(() => {
|
||||
void tryCheckRegistration();
|
||||
}, REGISTRATION_POLL_INTERVAL_MS);
|
||||
}, REGISTRATION_POLL_START_DELAY_MS);
|
||||
}
|
||||
|
||||
function renderRegistrationSuccessStage({ navigate, status, keyBundle, registrationTxId = '' }) {
|
||||
const screen = document.querySelector('section.stack');
|
||||
if (!screen) return;
|
||||
const headerBackButton = screen.querySelector('.page-header .header-left .icon-btn');
|
||||
const card = screen.querySelector('.card.stack');
|
||||
if (!card) return;
|
||||
|
||||
let remainingSec = SOLANA_SYNC_WAIT_SEC;
|
||||
let timerId = null;
|
||||
let loginStarted = false;
|
||||
const txExplorerUrl = makeSolanaExplorerTxUrl(registrationTxId, state.entrySettings.solanaServer);
|
||||
let loginInFlight = false;
|
||||
|
||||
const info = document.createElement('p');
|
||||
info.className = 'auth-copy';
|
||||
info.textContent = 'Регистрация завершена успешно.';
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'registration-finish-title';
|
||||
title.textContent = 'Поздравляем с регистрацией';
|
||||
|
||||
const hint = document.createElement('p');
|
||||
hint.className = 'meta-muted';
|
||||
hint.textContent = 'Нужно подождать 10–15 секунд, пока в блокчейне SHiNE обновится ваша запись о регистрации. После этого мы автоматически войдём в ваш аккаунт.';
|
||||
hint.className = 'auth-copy registration-finish-text';
|
||||
hint.textContent = 'Регистрация подтверждена в блокчейне Solana. Теперь можно войти в аккаунт и выбрать, какие ключи сохранить на устройстве.';
|
||||
|
||||
const timer = document.createElement('p');
|
||||
timer.className = 'meta-muted';
|
||||
timer.textContent = `Готовим автоматический вход: ${remainingSec} сек`;
|
||||
const txIdLine = document.createElement('p');
|
||||
txIdLine.className = 'meta-muted registration-finish-tx';
|
||||
if (registrationTxId && txExplorerUrl) {
|
||||
const txLabel = document.createElement('span');
|
||||
txLabel.textContent = 'Tx ID регистрации: ';
|
||||
const txLink = document.createElement('a');
|
||||
txLink.className = 'registration-finish-tx-link';
|
||||
txLink.href = txExplorerUrl;
|
||||
txLink.target = '_blank';
|
||||
txLink.rel = 'noopener noreferrer';
|
||||
txLink.textContent = registrationTxId;
|
||||
txIdLine.append(txLabel, txLink);
|
||||
} else {
|
||||
txIdLine.textContent = 'Регистрация подтверждена. Tx ID недоступен.';
|
||||
}
|
||||
|
||||
const tryLoginBtn = document.createElement('button');
|
||||
tryLoginBtn.className = 'primary-btn';
|
||||
tryLoginBtn.type = 'button';
|
||||
tryLoginBtn.textContent = 'Входим автоматически...';
|
||||
tryLoginBtn.disabled = true;
|
||||
|
||||
const statusHint = document.createElement('p');
|
||||
statusHint.className = 'meta-muted';
|
||||
statusHint.textContent = 'Ничего нажимать не нужно. Если сервер ответит раньше, войдём сразу.';
|
||||
|
||||
const stopTimer = () => {
|
||||
if (timerId) {
|
||||
window.clearInterval(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startAutoLogin = async () => {
|
||||
if (loginStarted) return;
|
||||
loginStarted = true;
|
||||
stopTimer();
|
||||
status.style.display = 'none';
|
||||
timer.textContent = 'Пробуем автоматически войти...';
|
||||
tryLoginBtn.textContent = 'Входим...';
|
||||
const enterButton = document.createElement('button');
|
||||
enterButton.className = 'primary-btn';
|
||||
enterButton.type = 'button';
|
||||
enterButton.textContent = 'Войти в аккаунт';
|
||||
enterButton.addEventListener('click', async () => {
|
||||
if (loginInFlight) return;
|
||||
loginInFlight = true;
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Готовим вход и экран сохранения ключей...';
|
||||
status.style.display = '';
|
||||
enterButton.disabled = true;
|
||||
try {
|
||||
await completeRegistrationLogin({ navigate, keyBundle });
|
||||
await prepareRegistrationLoginForKeyStorage({ keyBundle });
|
||||
await clearStoredMessages().catch(() => {});
|
||||
setAuthInfo(`Регистрация подтверждена для @${state.registrationDraft.login}. Выберите, какие ключи сохранить на устройстве.`);
|
||||
navigate('registration-keys-view');
|
||||
} catch (error) {
|
||||
loginStarted = false;
|
||||
const message = toUserMessage(error, 'Не удалось подготовить вход после регистрации.');
|
||||
setAuthError(message);
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = toUserMessage(error, 'Пока не удалось автоматически войти. Попробуйте ещё раз через несколько секунд.');
|
||||
status.textContent = message;
|
||||
status.style.display = '';
|
||||
timer.textContent = 'Автовход пока не удался.';
|
||||
tryLoginBtn.disabled = false;
|
||||
tryLoginBtn.textContent = 'Попробовать войти сейчас';
|
||||
} finally {
|
||||
loginInFlight = false;
|
||||
enterButton.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateTimerUi = () => {
|
||||
if (remainingSec > 0) {
|
||||
timer.textContent = `Готовим автоматический вход: ${remainingSec} сек`;
|
||||
tryLoginBtn.textContent = `Автовход через ${remainingSec} сек`;
|
||||
tryLoginBtn.disabled = true;
|
||||
} else {
|
||||
timer.textContent = 'Запускаем автоматический вход...';
|
||||
tryLoginBtn.textContent = 'Входим...';
|
||||
tryLoginBtn.disabled = true;
|
||||
void startAutoLogin();
|
||||
}
|
||||
};
|
||||
|
||||
tryLoginBtn.addEventListener('click', async () => {
|
||||
if (loginStarted) return;
|
||||
await startAutoLogin();
|
||||
});
|
||||
|
||||
if (headerBackButton) {
|
||||
const replacement = headerBackButton.cloneNode(true);
|
||||
replacement.addEventListener('click', () => navigate('start-view'));
|
||||
headerBackButton.replaceWith(replacement);
|
||||
}
|
||||
|
||||
card.innerHTML = '';
|
||||
card.append(info, hint, statusHint, timer, tryLoginBtn, status);
|
||||
updateTimerUi();
|
||||
timerId = window.setInterval(() => {
|
||||
remainingSec -= 1;
|
||||
updateTimerUi();
|
||||
}, 1000);
|
||||
status.style.display = 'none';
|
||||
card.append(title, hint, txIdLine, enterButton, status);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,9 @@ export function render({ navigate }) {
|
||||
draft.shineServer = resolved.wsUrl;
|
||||
applyStatus(resolved.status, resolved.httpBase);
|
||||
} else {
|
||||
const next = await checkServerAvailabilityByKey(field.key, input.value);
|
||||
const next = await checkServerAvailabilityByKey(field.key, input.value, {
|
||||
solanaEndpoint: draft.solanaServer,
|
||||
});
|
||||
applyStatus(next);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
|
||||
export const pageMeta = { id: 'solana-rpc-check-view', title: 'Проверка Solana RPC' };
|
||||
|
||||
const MAINNET_RPC_ENDPOINTS = [
|
||||
{ name: 'GetBlock', url: 'https://go.getblock.us/86aac42ad4484f3c813079afc201451c' },
|
||||
{ name: 'PublicNode', url: 'https://solana-rpc.publicnode.com' },
|
||||
{ name: 'BlockEden', url: 'https://api.blockeden.xyz/solana/KeCh6p22EX5AeRHxMSmc' },
|
||||
{ name: 'dRPC', url: 'https://solana.drpc.org/' },
|
||||
{ name: 'Grove', url: 'https://solana.rpc.grove.city/v1/01fdb492' },
|
||||
{ name: 'LeoRPC', url: 'https://solana.leorpc.com/?api_key=FREE' },
|
||||
{ name: 'OnFinality', url: 'https://solana.api.onfinality.io/public' },
|
||||
{ name: 'Solana Foundation', url: 'https://api.mainnet-beta.solana.com' },
|
||||
{ name: 'Solana Vibe Station', url: 'https://public.rpc.solanavibestation.com/' },
|
||||
{ name: 'TheRPC', url: 'https://solana.therpc.io' },
|
||||
];
|
||||
|
||||
const RPC_PAYLOAD = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'getVersion',
|
||||
params: [],
|
||||
};
|
||||
|
||||
function classifyResult(result) {
|
||||
if (!result) return { label: 'Нет данных', tone: 'neutral' };
|
||||
if (result.ok) return { label: 'Работает в браузере', tone: 'ok' };
|
||||
if (result.httpStatus === 402) return { label: 'Требует платный план', tone: 'bad' };
|
||||
if (result.httpStatus === 403) return { label: 'Доступ запрещён', tone: 'bad' };
|
||||
if (result.httpStatus === 429) return { label: 'Упёрлась в rate limit', tone: 'warn' };
|
||||
if (result.httpStatus >= 500) return { label: 'Ошибка на стороне RPC', tone: 'bad' };
|
||||
if (result.errorCode === 35) return { label: 'Free tier не даёт mainnet', tone: 'bad' };
|
||||
if (result.networkError) return { label: 'Не проходит из браузера', tone: 'bad' };
|
||||
return { label: 'Отвечает, но не подходит', tone: 'warn' };
|
||||
}
|
||||
|
||||
function shortenText(value, max = 140) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) return '—';
|
||||
return text.length > max ? `${text.slice(0, max)}...` : text;
|
||||
}
|
||||
|
||||
async function testEndpoint(url, timeoutMs = 12000) {
|
||||
const startedAt = performance.now();
|
||||
const controller = new AbortController();
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
cache: 'no-store',
|
||||
credentials: 'omit',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(RPC_PAYLOAD),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const elapsedMs = Math.round(performance.now() - startedAt);
|
||||
let data = null;
|
||||
let raw = '';
|
||||
try {
|
||||
raw = await response.text();
|
||||
data = raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
return {
|
||||
url,
|
||||
ok: response.ok && !!data?.result,
|
||||
httpStatus: response.status,
|
||||
elapsedMs,
|
||||
result: data?.result || null,
|
||||
errorCode: data?.error?.code ?? null,
|
||||
errorMessage: data?.error?.message || '',
|
||||
raw: shortenText(raw, 180),
|
||||
networkError: '',
|
||||
};
|
||||
} catch (error) {
|
||||
const elapsedMs = Math.round(performance.now() - startedAt);
|
||||
return {
|
||||
url,
|
||||
ok: false,
|
||||
httpStatus: 0,
|
||||
elapsedMs,
|
||||
result: null,
|
||||
errorCode: null,
|
||||
errorMessage: '',
|
||||
raw: '',
|
||||
networkError: error?.name === 'AbortError' ? 'Timeout' : (error?.message || 'Network error'),
|
||||
};
|
||||
} finally {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
function makeResultCard(endpoint) {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card stack solana-rpc-card';
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'solana-rpc-card-head';
|
||||
|
||||
const title = document.createElement('strong');
|
||||
title.textContent = endpoint.name;
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge alt solana-rpc-badge';
|
||||
badge.textContent = 'Ожидает проверки';
|
||||
|
||||
head.append(title, badge);
|
||||
|
||||
const endpointLine = document.createElement('a');
|
||||
endpointLine.className = 'solana-rpc-url';
|
||||
endpointLine.href = endpoint.url;
|
||||
endpointLine.target = '_blank';
|
||||
endpointLine.rel = 'noopener noreferrer';
|
||||
endpointLine.textContent = endpoint.url;
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'solana-rpc-meta';
|
||||
|
||||
const statusLine = document.createElement('div');
|
||||
statusLine.className = 'meta-muted';
|
||||
statusLine.textContent = 'Ещё не проверяли.';
|
||||
|
||||
const details = document.createElement('div');
|
||||
details.className = 'meta-muted solana-rpc-details';
|
||||
details.textContent = '—';
|
||||
|
||||
meta.append(statusLine, details);
|
||||
card.append(head, endpointLine, meta);
|
||||
|
||||
return { card, badge, statusLine, details };
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const intro = document.createElement('div');
|
||||
intro.className = 'card stack';
|
||||
intro.innerHTML = `
|
||||
<strong>Проверка mainnet RPC из браузера</strong>
|
||||
<p class="meta-muted">
|
||||
Этот экран делает реальный browser fetch к публичным Solana mainnet RPC.
|
||||
Если ответ приходит прямо здесь, значит нода годится для клиентского UI без промежуточного сервера.
|
||||
</p>
|
||||
`;
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'auth-footer-actions';
|
||||
|
||||
const runBtn = document.createElement('button');
|
||||
runBtn.className = 'primary-btn';
|
||||
runBtn.type = 'button';
|
||||
runBtn.textContent = 'Проверить все ноды';
|
||||
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.className = 'ghost-btn';
|
||||
resetBtn.type = 'button';
|
||||
resetBtn.textContent = 'Сбросить';
|
||||
|
||||
controls.append(runBtn, resetBtn);
|
||||
intro.append(controls);
|
||||
|
||||
const summary = document.createElement('p');
|
||||
summary.className = 'status-line';
|
||||
summary.textContent = 'Пока проверки не запускались.';
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'stack';
|
||||
|
||||
const cards = MAINNET_RPC_ENDPOINTS.map((endpoint) => {
|
||||
const ui = makeResultCard(endpoint);
|
||||
grid.append(ui.card);
|
||||
return { endpoint, ui };
|
||||
});
|
||||
|
||||
let runToken = 0;
|
||||
|
||||
const resetState = () => {
|
||||
runToken += 1;
|
||||
summary.className = 'status-line';
|
||||
summary.textContent = 'Пока проверки не запускались.';
|
||||
cards.forEach(({ ui }) => {
|
||||
ui.badge.className = 'badge alt solana-rpc-badge';
|
||||
ui.badge.textContent = 'Ожидает проверки';
|
||||
ui.statusLine.textContent = 'Ещё не проверяли.';
|
||||
ui.details.textContent = '—';
|
||||
});
|
||||
};
|
||||
|
||||
const runChecks = async () => {
|
||||
const token = ++runToken;
|
||||
runBtn.disabled = true;
|
||||
resetBtn.disabled = true;
|
||||
summary.className = 'status-line';
|
||||
summary.textContent = 'Проверяем mainnet RPC из браузера...';
|
||||
|
||||
let okCount = 0;
|
||||
let warnCount = 0;
|
||||
let badCount = 0;
|
||||
|
||||
const tasks = cards.map(async ({ endpoint, ui }) => {
|
||||
ui.badge.className = 'badge alt solana-rpc-badge';
|
||||
ui.badge.textContent = 'Проверяем...';
|
||||
ui.statusLine.textContent = 'Отправляем JSON-RPC запрос getVersion...';
|
||||
ui.details.textContent = 'Ждём ответ.';
|
||||
|
||||
const result = await testEndpoint(endpoint.url);
|
||||
if (token !== runToken) return;
|
||||
|
||||
const verdict = classifyResult(result);
|
||||
if (verdict.tone === 'ok') okCount += 1;
|
||||
else if (verdict.tone === 'warn') warnCount += 1;
|
||||
else if (verdict.tone === 'bad') badCount += 1;
|
||||
|
||||
ui.badge.className = `badge solana-rpc-badge is-${verdict.tone}`;
|
||||
ui.badge.textContent = verdict.label;
|
||||
ui.statusLine.textContent = `HTTP ${result.httpStatus || 'ERR'} • ${result.elapsedMs} мс`;
|
||||
if (result.ok) {
|
||||
ui.details.textContent = `solana-core: ${result.result?.['solana-core'] || 'unknown'} • feature-set: ${result.result?.['feature-set'] || 'unknown'}`;
|
||||
} else if (result.networkError) {
|
||||
ui.details.textContent = `Browser error: ${result.networkError}`;
|
||||
} else if (result.errorMessage) {
|
||||
ui.details.textContent = shortenText(result.errorMessage, 180);
|
||||
} else {
|
||||
ui.details.textContent = result.raw || 'Нода ответила без пригодного JSON-RPC result.';
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(tasks);
|
||||
if (token !== runToken) return;
|
||||
|
||||
summary.className = 'status-line is-available';
|
||||
summary.textContent = `Готово. Для браузера подходят: ${okCount}. С оговорками: ${warnCount}. Не подходят: ${badCount}.`;
|
||||
runBtn.disabled = false;
|
||||
resetBtn.disabled = false;
|
||||
};
|
||||
|
||||
runBtn.addEventListener('click', () => {
|
||||
void runChecks();
|
||||
});
|
||||
resetBtn.addEventListener('click', resetState);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Проверка Solana RPC',
|
||||
leftAction: { label: '←', onClick: () => navigate('developer-settings-view') },
|
||||
}),
|
||||
intro,
|
||||
summary,
|
||||
grid,
|
||||
);
|
||||
|
||||
void runChecks();
|
||||
|
||||
return screen;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolveShineServerByServerLogin } from './shine-server-resolver.js';
|
||||
import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
function normalizeUrl(value) {
|
||||
return String(value || '').trim();
|
||||
@@ -121,12 +121,13 @@ export async function resolveAndCheckShineServerLogin(serverLogin, solanaEndpoin
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkServerAvailabilityByKey(key, url) {
|
||||
export async function checkServerAvailabilityByKey(key, url, options = {}) {
|
||||
const selectedSolanaEndpoint = normalizeUrl(options?.solanaEndpoint || state.entrySettings.solanaServer);
|
||||
if (key === 'solanaServer') {
|
||||
return (await checkSolanaRpc(url)) ? 'available' : 'unavailable';
|
||||
}
|
||||
if (key === 'shineServerLogin') {
|
||||
return (await resolveAndCheckShineServerLogin(url, SOLANA_ENDPOINT_DEFAULT)).status;
|
||||
return (await resolveAndCheckShineServerLogin(url, selectedSolanaEndpoint)).status;
|
||||
}
|
||||
if (key === 'shineServer') {
|
||||
return (await checkShineWs(url)) ? 'available' : 'unavailable';
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
|
||||
|
||||
const CLASSIFY_LOGIN_INSTRUCTION_TAG = 1;
|
||||
const PRECHECK_SIM_PAYER = 'SHiJ58FvbJQJnPEUa5mZNVpWd1ym9CkjjPgeHYzUJmh';
|
||||
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
|
||||
|
||||
let solanaLibPromise = null;
|
||||
@@ -86,7 +85,6 @@ export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) {
|
||||
const solana = await loadSolanaLib();
|
||||
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
|
||||
const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID);
|
||||
const payer = new solana.PublicKey(PRECHECK_SIM_PAYER);
|
||||
const ix = new solana.TransactionInstruction({
|
||||
programId: loginGuardProgram,
|
||||
keys: [],
|
||||
@@ -94,7 +92,9 @@ export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) {
|
||||
});
|
||||
const { blockhash } = await connection.getLatestBlockhash('confirmed');
|
||||
const v0Message = new solana.TransactionMessage({
|
||||
payerKey: payer,
|
||||
// Для simulateTransaction нужен существующий аккаунт payer.
|
||||
// Используем сам program id, чтобы не зависеть от внешнего тестового адреса.
|
||||
payerKey: loginGuardProgram,
|
||||
recentBlockhash: blockhash,
|
||||
instructions: [ix],
|
||||
}).compileToV0Message();
|
||||
|
||||
+1
-10
@@ -95,16 +95,7 @@ const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
|
||||
function normalizeStoredSolanaServer(value) {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return DEFAULT_SOLANA_SERVER;
|
||||
const normalized = raw.replace(/\/+$/u, '').toLowerCase();
|
||||
if (
|
||||
normalized === 'https://api.devnet.solana.com'
|
||||
|| normalized === 'http://api.devnet.solana.com'
|
||||
|| normalized === 'https://api.mainnet-beta.solana.com'
|
||||
|| normalized === 'http://api.mainnet-beta.solana.com'
|
||||
) {
|
||||
return DEFAULT_SOLANA_SERVER;
|
||||
}
|
||||
return raw;
|
||||
return raw.replace(/\/+$/u, '');
|
||||
}
|
||||
|
||||
export function normalizeDmChatId(value) {
|
||||
|
||||
@@ -755,6 +755,43 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.registration-finish-card {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.registration-finish-title {
|
||||
margin: 0;
|
||||
font-size: clamp(1.9rem, 7vw, 2.45rem);
|
||||
line-height: 1.18;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.registration-finish-text,
|
||||
.registration-finish-tx,
|
||||
.registration-finish-progress {
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.registration-finish-card .primary-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.registration-finish-tx-link {
|
||||
color: #9fd8ff;
|
||||
text-decoration: underline;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.registration-finish-tx-link:hover,
|
||||
.registration-finish-tx-link:focus-visible {
|
||||
color: #c9ebff;
|
||||
}
|
||||
|
||||
.session-status {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
@@ -1267,6 +1304,59 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.solana-rpc-card {
|
||||
width: min(100%, 420px);
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.solana-rpc-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.solana-rpc-badge {
|
||||
flex: 0 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.solana-rpc-badge.is-ok {
|
||||
border-color: rgba(132, 244, 161, 0.42);
|
||||
background: rgba(132, 244, 161, 0.12);
|
||||
color: #d7ffe3;
|
||||
}
|
||||
|
||||
.solana-rpc-badge.is-warn {
|
||||
border-color: rgba(242, 210, 129, 0.42);
|
||||
background: rgba(242, 210, 129, 0.12);
|
||||
color: #fff0c7;
|
||||
}
|
||||
|
||||
.solana-rpc-badge.is-bad {
|
||||
border-color: rgba(255, 113, 143, 0.42);
|
||||
background: rgba(255, 113, 143, 0.12);
|
||||
color: #ffd7df;
|
||||
}
|
||||
|
||||
.solana-rpc-url {
|
||||
color: #9fd8ff;
|
||||
text-decoration: underline;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.solana-rpc-meta {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.solana-rpc-details {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.settings-developer-panel {
|
||||
margin-top: 2px;
|
||||
padding-top: 8px;
|
||||
|
||||
Reference in New Issue
Block a user