diff --git a/SHiNE-promo-solana-devnet/.gitignore b/SHiNE-promo-solana-devnet/.gitignore deleted file mode 100644 index a420490..0000000 --- a/SHiNE-promo-solana-devnet/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.gradle -/build -.idea -out -*.log - -config/devnet-wallet.json - -!gradle/wrapper/gradle-wrapper.jar diff --git a/SHiNE-promo-solana-devnet/README.md b/SHiNE-promo-solana-devnet/README.md deleted file mode 100644 index 9c907ba..0000000 --- a/SHiNE-promo-solana-devnet/README.md +++ /dev/null @@ -1,198 +0,0 @@ -# SHiNE-promo-solana-devnet - -Временное промо-приложение для тестеров Web3-социальной сети SHiNE / «Сияние» в Solana Devnet. - -Основной сценарий: -- приложение SHiNE открывает страницу вида `/?wallet=SOLANA_PUBLIC_KEY`; -- пользователь вводит имя и промокод; -- backend проверяет промокод и отправляет **реальную** devnet-транзакцию на `0.1 SOL`; -- использованный промокод фиксируется в файле и больше не может быть применён. - -## Стек - -- Java 21 -- Spring Boot -- Gradle -- Thymeleaf + HTML/CSS/JS (server-side UI) - -## Локальный запуск - -1. Скопируйте пример настроек: - -```bash -cp config/application.example.properties src/main/resources/application.properties -``` - -2. Положите реальный keypair-файл Solana CLI формата в: - -```text -config/devnet-wallet.json -``` - -3. Запустите: - -```bash -./gradlew bootRun -``` - -Приложение будет доступно на `http://localhost:8021`. - -## Как указать порт - -По умолчанию используется: - -```properties -server.port=8021 -``` - -Изменить можно: -- в `src/main/resources/application.properties`; -- или через параметр запуска: - -```bash -./gradlew bootRun --args='--server.port=8090' -``` - -## Настройка Solana RPC Devnet - -Параметр: - -```properties -solana.rpc.url=https://api.devnet.solana.com -``` - -Для другого RPC достаточно заменить URL в properties. - -## Настройка devnet keypair - -- Файл должен быть в формате Solana keypair JSON: массив из 64 чисел (`0..255`). -- Пример лежит в `config/devnet-wallet.example.json`. -- Рабочий файл: `config/devnet-wallet.json`. - -Важно: настоящий keypair **нельзя коммитить в GitHub**. -Он добавлен в `.gitignore`. - -## Где лежат промокоды - -Файл: - -```text -data/promo-codes.txt -``` - -## Где лежит файл использованных промокодов - -Файл: - -```text -data/promo-used.txt -``` - -## Формат `promo-codes.txt` - -- одна строка = один промокод; -- пустые строки игнорируются; -- строки с `#` игнорируются как комментарии; -- промокод должен соответствовать regex: `[a-z0-9]{8}`. - -## Формат `promo-used.txt` - -Каждая запись в формате: - -```text -promoCode | wallet | name | yyyy.MM.dd HH:mm | signature -``` - -Пример: - -```text -aidar2km | 8xF...abc | Иван Петров | 2026.04.27 18:45 | 5xTxSignature... -``` - -## Пример URL - -```text -http://localhost:8021/?wallet=8zYQ...DevnetAddress -``` - -## Пример API-запроса - -```bash -curl -X POST http://localhost:8021/api/promo/top-up \ - -H "Content-Type: application/json" \ - -d '{ - "wallet":"SOLANA_PUBLIC_KEY", - "name":"Иван Петров", - "promoCode":"aidar2km" - }' -``` - -## Проверка транзакции в Solana Explorer Devnet - -Explorer URL формируется по шаблону: - -```text -https://explorer.solana.com/tx/{signature}?cluster=devnet -``` - -## Сборка jar через Gradle - -```bash -./gradlew clean build -``` - -JAR: - -```text -build/libs/SHiNE-promo-solana-devnet.jar -``` - -## Запуск jar - -```bash -java -jar build/libs/SHiNE-promo-solana-devnet.jar -``` - -## Health endpoint - -```text -GET /health -``` - -Ответ: - -```json -{ - "status": "ok", - "app": "SHiNE-promo-solana-devnet" -} -``` - -## Логи - -- логируется старт приложения; -- логируются успешные пополнения; -- логируются ошибки транзакций; -- приватный ключ не логируется. - -## Gradle-задачи для серверного деплоя - -В `build.gradle` добавлены задачи: - -- `buildServerBundle` — готовит bundle в `build/server-bundle`; -- `deployToServer` — копирует JAR и `application.properties` на сервер и перезапускает systemd-сервис. - -Запуск: - -```bash -./gradlew deployToServer -``` - -Переопределение хоста/пути: - -```bash -./gradlew deployToServer \ - -PdeployHost=user@10.147.20.7 \ - -PdeployPath=/home/user/docker/SHiNE-promo-solana-devnet \ - -PdeployService=SHiNE-promo-solana-devnet -``` diff --git a/SHiNE-promo-solana-devnet/build.gradle b/SHiNE-promo-solana-devnet/build.gradle deleted file mode 100644 index eb35d19..0000000 --- a/SHiNE-promo-solana-devnet/build.gradle +++ /dev/null @@ -1,103 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '3.3.6' - id 'io.spring.dependency-management' version '1.1.7' -} - -group = 'ru.shine' -version = '0.1.0' - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'com.mmorrell:solanaj:1.20.4' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' -} - -tasks.named('test') { - useJUnitPlatform() -} - -tasks.named('bootJar') { - archiveFileName = 'SHiNE-promo-solana-devnet.jar' -} - -// ------------------------------------------------------------ -// ДЕПЛОЙ ВРЕМЕННОГО СЕРВИСА "SHiNE-promo-solana-devnet" -// -// Назначение сервиса: -// - это отдельный продукт для тестовой раздачи SOL в devnet; -// - нужен для упрощённого онбординга тестовых пользователей; -// - работает как самостоятельное Spring Boot приложение. -// -// Почему отдельный деплой: -// - сервис изолирован от основного SHiNE-сервера; -// - его можно обновлять/перезапускать независимо; -// - жизненный цикл временного сервиса не должен ломать основной прод. -// -// ВАЖНО: -// - деплой по умолчанию выполняется ЧЕРЕЗ ДОМЕН (shineup.me), а не по IP; -// - целевая папка на сервере: /home/player/SHiNE/SHiNE-promo-solana-devnet; -// - целевой systemd-сервис: SHiNE-promo-solana-devnet. -// ------------------------------------------------------------ -def deployHost = project.findProperty('deployHost') ?: 'root@shineup.me' -def deployPath = project.findProperty('deployPath') ?: '/home/player/SHiNE/SHiNE-promo-solana-devnet' -def remoteServiceName = project.findProperty('deployService') ?: 'SHiNE-promo-solana-devnet' - -tasks.register('buildServerBundle', Copy) { - // Сборка минимального серверного бандла: - // 1) fat-jar приложения - // 2) шаблон application.properties (чтобы был эталон конфигурации) - dependsOn tasks.named('bootJar') - from(tasks.named('bootJar').flatMap { it.archiveFile }) { - rename { 'SHiNE-promo-solana-devnet.jar' } - } - from('config/application.example.properties') { - rename { 'application.properties' } - } - into(layout.buildDirectory.dir('server-bundle')) -} - -tasks.register('deployToServerMkdir', Exec) { - // Шаг 1: гарантируем существование целевой директории на удалённом сервере. - dependsOn tasks.named('buildServerBundle') - commandLine 'bash', '-lc', "ssh ${deployHost} 'mkdir -p ${deployPath}'" -} - -tasks.register('deployToServerJar', Exec) { - // Шаг 2: загружаем исполняемый jar. - dependsOn tasks.named('deployToServerMkdir') - commandLine 'bash', '-lc', "scp ${layout.buildDirectory.file('server-bundle/SHiNE-promo-solana-devnet.jar').get().asFile.absolutePath} ${deployHost}:${deployPath}/" -} - -tasks.register('deployToServerConfig', Exec) { - // Шаг 3: загружаем конфиг-шаблон. - // На проде при необходимости его можно заменить на рабочий конфиг с секретами. - dependsOn tasks.named('deployToServerJar') - commandLine 'bash', '-lc', "scp ${layout.buildDirectory.file('server-bundle/application.properties').get().asFile.absolutePath} ${deployHost}:${deployPath}/" -} - -tasks.register('deployToServerRestart', Exec) { - // Шаг 4: перезапускаем systemd-сервис и показываем статус. - // Если сервис не поднялся, ошибка будет видна сразу в этом шаге. - dependsOn tasks.named('deployToServerConfig') - commandLine 'bash', '-lc', "ssh ${deployHost} 'sudo systemctl restart ${remoteServiceName} && sudo systemctl --no-pager status ${remoteServiceName}'" -} - -tasks.register('deployToServer') { - // Единая точка входа для деплоя временного сервиса. - // Запуск: ./gradlew deployToServer - dependsOn tasks.named('deployToServerRestart') -} diff --git a/SHiNE-promo-solana-devnet/config/application.example.properties b/SHiNE-promo-solana-devnet/config/application.example.properties deleted file mode 100644 index 944690a..0000000 --- a/SHiNE-promo-solana-devnet/config/application.example.properties +++ /dev/null @@ -1,15 +0,0 @@ -server.port=8021 - -solana.rpc.url=https://api.devnet.solana.com -solana.sender.keypair-file=./config/devnet-wallet.json - -promo.transfer.amount-sol=0.1 -promo.codes.file=./data/promo-codes.txt -promo.used.file=./data/promo-used.txt - -promo.explorer.tx-url-template=https://explorer.solana.com/tx/%s?cluster=devnet - -# Вечный промокод для временной раздачи в devnet. -# Если enabled=true, код можно использовать неограниченно (он не "сгорает"). -promo.eternal-code.enabled=true -promo.eternal-code.value=0000 diff --git a/SHiNE-promo-solana-devnet/config/devnet-wallet.example.json b/SHiNE-promo-solana-devnet/config/devnet-wallet.example.json deleted file mode 100644 index 61463f7..0000000 --- a/SHiNE-promo-solana-devnet/config/devnet-wallet.example.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - 151, 22, 193, 47, 88, 234, 15, 201, 42, 19, 180, 76, 211, 5, 164, 91, - 207, 135, 44, 173, 61, 242, 14, 99, 158, 208, 39, 117, 10, 226, 95, 132, - 56, 72, 164, 209, 41, 189, 76, 239, 121, 18, 66, 213, 30, 145, 201, 9, - 177, 53, 120, 87, 200, 65, 33, 251, 102, 74, 186, 44, 160, 7, 92, 137 -] diff --git a/SHiNE-promo-solana-devnet/deploy/SHiNE-promo-solana-devnet.service b/SHiNE-promo-solana-devnet/deploy/SHiNE-promo-solana-devnet.service deleted file mode 100644 index c75f976..0000000 --- a/SHiNE-promo-solana-devnet/deploy/SHiNE-promo-solana-devnet.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=SHiNE Promo Solana Devnet -After=network.target - -[Service] -Type=simple -User=player -Group=player -WorkingDirectory=/home/player/SHiNE/SHiNE-promo-solana-devnet -ExecStart=/usr/bin/java -jar /home/player/SHiNE/SHiNE-promo-solana-devnet/SHiNE-promo-solana-devnet.jar --spring.config.location=/home/player/SHiNE/SHiNE-promo-solana-devnet/application.properties -Restart=always -RestartSec=5 -SuccessExitStatus=143 - -[Install] -WantedBy=multi-user.target diff --git a/SHiNE-promo-solana-devnet/gradle/wrapper/gradle-wrapper.jar b/SHiNE-promo-solana-devnet/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e06f1ed..0000000 Binary files a/SHiNE-promo-solana-devnet/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/SHiNE-promo-solana-devnet/gradle/wrapper/gradle-wrapper.properties b/SHiNE-promo-solana-devnet/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 5b07891..0000000 --- a/SHiNE-promo-solana-devnet/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Sun Apr 26 23:49:28 MSK 2026 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/SHiNE-promo-solana-devnet/gradlew b/SHiNE-promo-solana-devnet/gradlew deleted file mode 100755 index 2721119..0000000 --- a/SHiNE-promo-solana-devnet/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/SHiNE-promo-solana-devnet/gradlew.bat b/SHiNE-promo-solana-devnet/gradlew.bat deleted file mode 100644 index 9a51268..0000000 --- a/SHiNE-promo-solana-devnet/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/SHiNE-promo-solana-devnet/settings.gradle b/SHiNE-promo-solana-devnet/settings.gradle deleted file mode 100644 index 5c50eed..0000000 --- a/SHiNE-promo-solana-devnet/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'SHiNE-promo-solana-devnet' \ No newline at end of file diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/ShinePromoSolanaDevnetApplication.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/ShinePromoSolanaDevnetApplication.java deleted file mode 100644 index 989d6dd..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/ShinePromoSolanaDevnetApplication.java +++ /dev/null @@ -1,30 +0,0 @@ -package ru.shine.promo; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import ru.shine.promo.config.AppProperties; - -@SpringBootApplication -public class ShinePromoSolanaDevnetApplication { - - private static final Logger log = LoggerFactory.getLogger(ShinePromoSolanaDevnetApplication.class); - - public static void main(String[] args) { - SpringApplication.run(ShinePromoSolanaDevnetApplication.class, args); - } - - @Bean - CommandLineRunner startupLogger(AppProperties appProperties) { - return args -> log.info( - "SHiNE promo app started. RPC: {}, promoCodesFile: {}, usedFile: {}, transferAmountSol: {}", - appProperties.getSolanaRpcUrl(), - appProperties.getPromoCodesFile(), - appProperties.getPromoUsedFile(), - appProperties.getPromoTransferAmountSol() - ); - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/config/AppProperties.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/config/AppProperties.java deleted file mode 100644 index a7b3781..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/config/AppProperties.java +++ /dev/null @@ -1,77 +0,0 @@ -package ru.shine.promo.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; -import java.math.RoundingMode; - -@Component -public class AppProperties { - - private static final long LAMPORTS_PER_SOL = 1_000_000_000L; - - @Value("${solana.rpc.url}") - private String solanaRpcUrl; - - @Value("${solana.sender.keypair-file}") - private String solanaSenderKeypairFile; - - @Value("${promo.transfer.amount-sol}") - private BigDecimal promoTransferAmountSol; - - @Value("${promo.codes.file}") - private String promoCodesFile; - - @Value("${promo.used.file}") - private String promoUsedFile; - - @Value("${promo.explorer.tx-url-template}") - private String promoExplorerTxUrlTemplate; - - @Value("${promo.eternal-code.enabled:false}") - private boolean promoEternalCodeEnabled; - - @Value("${promo.eternal-code.value:0000}") - private String promoEternalCodeValue; - - public String getSolanaRpcUrl() { - return solanaRpcUrl; - } - - public String getSolanaSenderKeypairFile() { - return solanaSenderKeypairFile; - } - - public BigDecimal getPromoTransferAmountSol() { - return promoTransferAmountSol; - } - - public String getPromoCodesFile() { - return promoCodesFile; - } - - public String getPromoUsedFile() { - return promoUsedFile; - } - - public String getPromoExplorerTxUrlTemplate() { - return promoExplorerTxUrlTemplate; - } - - public boolean isPromoEternalCodeEnabled() { - return promoEternalCodeEnabled; - } - - public String getPromoEternalCodeValue() { - if (promoEternalCodeValue == null) { - return ""; - } - return promoEternalCodeValue.trim().toLowerCase(); - } - - public long getPromoTransferAmountLamports() { - BigDecimal lamports = promoTransferAmountSol.multiply(BigDecimal.valueOf(LAMPORTS_PER_SOL)); - return lamports.setScale(0, RoundingMode.UNNECESSARY).longValueExact(); - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/controller/PromoApiController.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/controller/PromoApiController.java deleted file mode 100644 index 18d6ee5..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/controller/PromoApiController.java +++ /dev/null @@ -1,136 +0,0 @@ -package ru.shine.promo.controller; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; -import ru.shine.promo.config.AppProperties; -import ru.shine.promo.dto.PromoRequest; -import ru.shine.promo.dto.PromoResponse; -import ru.shine.promo.service.PromoCodeService; -import ru.shine.promo.service.PromoException; -import ru.shine.promo.service.PromoTransferService; -import ru.shine.promo.service.UsedPromoStorageService; - -import java.util.Map; - -@RestController -public class PromoApiController { - - private static final Logger log = LoggerFactory.getLogger(PromoApiController.class); - - private final AppProperties appProperties; - private final PromoCodeService promoCodeService; - private final PromoTransferService promoTransferService; - private final UsedPromoStorageService usedPromoStorageService; - - public PromoApiController( - AppProperties appProperties, - PromoCodeService promoCodeService, - PromoTransferService promoTransferService, - UsedPromoStorageService usedPromoStorageService - ) { - this.appProperties = appProperties; - this.promoCodeService = promoCodeService; - this.promoTransferService = promoTransferService; - this.usedPromoStorageService = usedPromoStorageService; - } - - @PostMapping("/api/promo/top-up") - public ResponseEntity topUp(@RequestBody PromoRequest request) { - String wallet = trimToEmpty(request == null ? null : request.getWallet()); - String name = trimToEmpty(request == null ? null : request.getName()); - String promoCode = promoCodeService.normalizePromoCode(request == null ? null : request.getPromoCode()); - - if (wallet.isEmpty()) { - return error(HttpStatus.BAD_REQUEST, "Пустой wallet"); - } - if (!promoTransferService.isSolanaWalletValid(wallet)) { - return error(HttpStatus.BAD_REQUEST, "Неверный формат Solana-адреса"); - } - if (name.isEmpty()) { - return error(HttpStatus.BAD_REQUEST, "Пустое имя"); - } - if (name.length() < 2 || name.length() > 120) { - return error(HttpStatus.BAD_REQUEST, "Имя должно быть длиной от 2 до 120 символов"); - } - if (promoCode.isEmpty()) { - return error(HttpStatus.BAD_REQUEST, "Пустой промокод"); - } - boolean eternalPromo = promoCodeService.isEternalPromoCode(promoCode); - if (!eternalPromo && !promoCodeService.isPromoCodeFormatValid(promoCode)) { - return error(HttpStatus.BAD_REQUEST, "Неверный формат промокода"); - } - - try { - if (!eternalPromo && !promoCodeService.promoCodeExists(promoCode)) { - return error(HttpStatus.BAD_REQUEST, "Промокод не найден"); - } - - String signature = usedPromoStorageService.executeLocked(() -> { - if (!eternalPromo && usedPromoStorageService.isPromoUsed(promoCode)) { - throw new PromoException(HttpStatus.CONFLICT, "Промокод уже использован"); - } - - String txSignature = promoTransferService.sendPromoTransfer(wallet); - // Для вечного промокода ведём только лог использования, но не блокируем повторное применение. - usedPromoStorageService.appendUsedPromo(promoCode, wallet, name, txSignature); - return txSignature; - }); - - String explorerUrl = promoTransferService.buildExplorerUrl(signature); - String amount = appProperties.getPromoTransferAmountSol().stripTrailingZeros().toPlainString(); - - log.info( - "Promo top-up success: wallet={}, name={}, promoCode={}, signature={}", - wallet, - name, - promoCode, - signature - ); - - PromoResponse response = PromoResponse.success( - "Тестовое пополнение выполнено", - wallet, - name, - amount, - signature, - explorerUrl - ); - return ResponseEntity.ok(response); - } catch (PromoException e) { - if (e.getStatus().is5xxServerError()) { - log.error("Top-up failed: {}", e.getMessage(), e); - } else { - log.warn("Top-up rejected: {}", e.getMessage()); - } - return error(e.getStatus(), e.getMessage()); - } catch (Exception e) { - log.error("Unexpected top-up error", e); - return error(HttpStatus.INTERNAL_SERVER_ERROR, "Внутренняя ошибка сервера"); - } - } - - @GetMapping("/health") - public Map health() { - return Map.of( - "status", "ok", - "app", "SHiNE-promo-solana-devnet" - ); - } - - private ResponseEntity error(HttpStatus status, String message) { - return ResponseEntity.status(status).body(PromoResponse.error(message)); - } - - private String trimToEmpty(String value) { - if (value == null) { - return ""; - } - return value.trim().replaceAll("\\s{2,}", " "); - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/controller/PromoPageController.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/controller/PromoPageController.java deleted file mode 100644 index f9e38d1..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/controller/PromoPageController.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.shine.promo.controller; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; - -@Controller -public class PromoPageController { - - @GetMapping("/") - public String index(@RequestParam(name = "wallet", required = false) String wallet, Model model) { - model.addAttribute("wallet", wallet == null ? "" : wallet.trim()); - return "index"; - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/dto/PromoRequest.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/dto/PromoRequest.java deleted file mode 100644 index c0ef86a..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/dto/PromoRequest.java +++ /dev/null @@ -1,32 +0,0 @@ -package ru.shine.promo.dto; - -public class PromoRequest { - - private String wallet; - private String name; - private String promoCode; - - public String getWallet() { - return wallet; - } - - public void setWallet(String wallet) { - this.wallet = wallet; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getPromoCode() { - return promoCode; - } - - public void setPromoCode(String promoCode) { - this.promoCode = promoCode; - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/dto/PromoResponse.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/dto/PromoResponse.java deleted file mode 100644 index ebd7835..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/dto/PromoResponse.java +++ /dev/null @@ -1,69 +0,0 @@ -package ru.shine.promo.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class PromoResponse { - - private boolean success; - private String message; - private String wallet; - private String name; - private String amountSol; - private String signature; - private String explorerUrl; - - public static PromoResponse success( - String message, - String wallet, - String name, - String amountSol, - String signature, - String explorerUrl - ) { - PromoResponse response = new PromoResponse(); - response.success = true; - response.message = message; - response.wallet = wallet; - response.name = name; - response.amountSol = amountSol; - response.signature = signature; - response.explorerUrl = explorerUrl; - return response; - } - - public static PromoResponse error(String message) { - PromoResponse response = new PromoResponse(); - response.success = false; - response.message = message; - return response; - } - - public boolean isSuccess() { - return success; - } - - public String getMessage() { - return message; - } - - public String getWallet() { - return wallet; - } - - public String getName() { - return name; - } - - public String getAmountSol() { - return amountSol; - } - - public String getSignature() { - return signature; - } - - public String getExplorerUrl() { - return explorerUrl; - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoCodeService.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoCodeService.java deleted file mode 100644 index f14b4f8..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoCodeService.java +++ /dev/null @@ -1,77 +0,0 @@ -package ru.shine.promo.service; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import ru.shine.promo.config.AppProperties; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.regex.Pattern; - -@Service -public class PromoCodeService { - - private static final Logger log = LoggerFactory.getLogger(PromoCodeService.class); - private static final Pattern PROMO_CODE_PATTERN = Pattern.compile("^[a-z0-9]{8}$"); - - private final AppProperties appProperties; - - public PromoCodeService(AppProperties appProperties) { - this.appProperties = appProperties; - } - - public String normalizePromoCode(String rawPromoCode) { - if (rawPromoCode == null) { - return ""; - } - return rawPromoCode.trim().toLowerCase(); - } - - public boolean isPromoCodeFormatValid(String promoCode) { - return PROMO_CODE_PATTERN.matcher(promoCode).matches(); - } - - public boolean isEternalPromoCode(String promoCode) { - if (!appProperties.isPromoEternalCodeEnabled()) { - return false; - } - String configured = appProperties.getPromoEternalCodeValue(); - return !configured.isEmpty() && configured.equals(promoCode); - } - - public boolean promoCodeExists(String promoCode) { - Set codes = readPromoCodesFromFile(); - return codes.contains(promoCode); - } - - private Set readPromoCodesFromFile() { - Path file = Path.of(appProperties.getPromoCodesFile()); - if (!Files.exists(file)) { - throw new PromoException(HttpStatus.INTERNAL_SERVER_ERROR, "Ошибка чтения файла промокодов"); - } - - try { - Set result = new LinkedHashSet<>(); - for (String row : Files.readAllLines(file, StandardCharsets.UTF_8)) { - String line = row.trim().toLowerCase(); - if (line.isEmpty() || line.startsWith("#")) { - continue; - } - if (!isPromoCodeFormatValid(line)) { - log.warn("Skipped invalid promo code row in {}: {}", file, line); - continue; - } - result.add(line); - } - return result; - } catch (IOException e) { - throw new PromoException(HttpStatus.INTERNAL_SERVER_ERROR, "Ошибка чтения файла промокодов", e); - } - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoException.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoException.java deleted file mode 100644 index a28db39..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoException.java +++ /dev/null @@ -1,22 +0,0 @@ -package ru.shine.promo.service; - -import org.springframework.http.HttpStatus; - -public class PromoException extends RuntimeException { - - private final HttpStatus status; - - public PromoException(HttpStatus status, String message) { - super(message); - this.status = status; - } - - public PromoException(HttpStatus status, String message, Throwable cause) { - super(message, cause); - this.status = status; - } - - public HttpStatus getStatus() { - return status; - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoTransferService.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoTransferService.java deleted file mode 100644 index dda24a2..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/PromoTransferService.java +++ /dev/null @@ -1,139 +0,0 @@ -package ru.shine.promo.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.p2p.solanaj.core.Account; -import org.p2p.solanaj.core.PublicKey; -import org.p2p.solanaj.core.Transaction; -import org.p2p.solanaj.programs.SystemProgram; -import org.p2p.solanaj.rpc.RpcClient; -import org.p2p.solanaj.rpc.RpcException; -import org.p2p.solanaj.rpc.types.config.Commitment; -import org.p2p.solanaj.rpc.types.config.RpcSendTransactionConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import ru.shine.promo.config.AppProperties; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Locale; - -@Service -public class PromoTransferService { - - private static final Logger log = LoggerFactory.getLogger(PromoTransferService.class); - - private final AppProperties appProperties; - private final ObjectMapper objectMapper = new ObjectMapper(); - - public PromoTransferService(AppProperties appProperties) { - this.appProperties = appProperties; - } - - public boolean isSolanaWalletValid(String wallet) { - if (wallet == null || wallet.isBlank()) { - return false; - } - try { - PublicKey publicKey = new PublicKey(wallet.trim()); - return publicKey.toByteArray().length == PublicKey.PUBLIC_KEY_LENGTH; - } catch (Exception e) { - return false; - } - } - - public String sendPromoTransfer(String recipientWallet) { - PublicKey recipient = parseWalletOrThrow(recipientWallet); - Account sender = readSenderAccount(); - long lamports = appProperties.getPromoTransferAmountLamports(); - - RpcClient rpcClient = new RpcClient(appProperties.getSolanaRpcUrl()); - - try { - long senderBalance = rpcClient.getApi().getBalance(sender.getPublicKey(), Commitment.CONFIRMED); - if (senderBalance < lamports) { - throw new PromoException(HttpStatus.BAD_REQUEST, "Недостаточно средств на devnet-кошельке отправителя"); - } - - Transaction transaction = new Transaction() - .addInstruction(SystemProgram.transfer(sender.getPublicKey(), recipient, lamports)); - - RpcSendTransactionConfig config = RpcSendTransactionConfig.builder() - .encoding(RpcSendTransactionConfig.Encoding.base64) - .skipPreFlight(false) - .maxRetries(3) - .build(); - - return rpcClient.getApi().sendTransaction( - transaction, - Collections.singletonList(sender), - null, - config - ); - } catch (PromoException e) { - throw e; - } catch (RpcException e) { - String message = e.getMessage() == null ? "" : e.getMessage().toLowerCase(Locale.ROOT); - if (message.contains("insufficient")) { - throw new PromoException(HttpStatus.BAD_REQUEST, "Недостаточно средств на devnet-кошельке отправителя", e); - } - if (message.contains("rpc")) { - throw new PromoException(HttpStatus.BAD_GATEWAY, "Ошибка RPC Solana", e); - } - log.error("Solana transfer failed: {}", e.getMessage(), e); - throw new PromoException(HttpStatus.BAD_GATEWAY, "Ошибка отправки транзакции", e); - } catch (Exception e) { - log.error("Unexpected Solana transfer error: {}", e.getMessage(), e); - throw new PromoException(HttpStatus.BAD_GATEWAY, "Ошибка отправки транзакции", e); - } - } - - public String buildExplorerUrl(String signature) { - return String.format(appProperties.getPromoExplorerTxUrlTemplate(), signature); - } - - private PublicKey parseWalletOrThrow(String wallet) { - if (!isSolanaWalletValid(wallet)) { - throw new PromoException(HttpStatus.BAD_REQUEST, "Неверный формат Solana-адреса"); - } - return new PublicKey(wallet.trim()); - } - - private Account readSenderAccount() { - Path keypairFile = Path.of(appProperties.getSolanaSenderKeypairFile()); - if (!Files.exists(keypairFile)) { - throw new PromoException(HttpStatus.INTERNAL_SERVER_ERROR, "Файл keypair отправителя не найден"); - } - - try { - int[] keyArray = objectMapper.readValue(Files.readString(keypairFile), int[].class); - if (keyArray.length != 64) { - throw new PromoException( - HttpStatus.INTERNAL_SERVER_ERROR, - "Некорректный формат keypair отправителя: ожидается массив из 64 чисел" - ); - } - - byte[] secret = new byte[64]; - for (int i = 0; i < keyArray.length; i++) { - int value = keyArray[i]; - if (value < 0 || value > 255) { - throw new PromoException( - HttpStatus.INTERNAL_SERVER_ERROR, - "Некорректный формат keypair отправителя: числа должны быть в диапазоне 0..255" - ); - } - secret[i] = (byte) value; - } - - return new Account(secret); - } catch (PromoException e) { - throw e; - } catch (IOException e) { - throw new PromoException(HttpStatus.INTERNAL_SERVER_ERROR, "Ошибка чтения keypair отправителя", e); - } - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/UsedPromoStorageService.java b/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/UsedPromoStorageService.java deleted file mode 100644 index f27acaf..0000000 --- a/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/service/UsedPromoStorageService.java +++ /dev/null @@ -1,101 +0,0 @@ -package ru.shine.promo.service; - -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import ru.shine.promo.config.AppProperties; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.locks.ReentrantLock; - -@Service -public class UsedPromoStorageService { - - private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm", Locale.ROOT); - - private final AppProperties appProperties; - private final ReentrantLock promoLock = new ReentrantLock(true); - - public UsedPromoStorageService(AppProperties appProperties) { - this.appProperties = appProperties; - } - - public T executeLocked(LockedOperation operation) { - promoLock.lock(); - try { - return operation.run(); - } finally { - promoLock.unlock(); - } - } - - public boolean isPromoUsed(String promoCode) { - Path usedFile = Path.of(appProperties.getPromoUsedFile()); - if (!Files.exists(usedFile)) { - return false; - } - - try { - List rows = Files.readAllLines(usedFile, StandardCharsets.UTF_8); - for (String row : rows) { - String line = row.trim(); - if (line.isEmpty() || line.startsWith("#")) { - continue; - } - - String[] parts = line.split("\\|"); - if (parts.length == 0) { - continue; - } - - String usedCode = parts[0].trim().toLowerCase(Locale.ROOT); - if (usedCode.equals(promoCode)) { - return true; - } - } - return false; - } catch (IOException e) { - throw new PromoException(HttpStatus.INTERNAL_SERVER_ERROR, "Ошибка чтения файла использованных промокодов", e); - } - } - - public void appendUsedPromo(String promoCode, String wallet, String name, String signature) { - Path usedFile = Path.of(appProperties.getPromoUsedFile()); - try { - Path parent = usedFile.toAbsolutePath().getParent(); - if (parent != null) { - Files.createDirectories(parent); - } - if (!Files.exists(usedFile)) { - Files.createFile(usedFile); - } - - String timestamp = LocalDateTime.now().format(DATE_TIME_FORMAT); - String line = String.format( - Locale.ROOT, - "%s | %s | %s | %s | %s%n", - promoCode, - wallet, - name, - timestamp, - signature - ); - - Files.writeString(usedFile, line, StandardCharsets.UTF_8, StandardOpenOption.APPEND); - } catch (IOException e) { - throw new PromoException(HttpStatus.INTERNAL_SERVER_ERROR, "Ошибка записи файла использованных промокодов", e); - } - } - - @FunctionalInterface - public interface LockedOperation { - T run(); - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/resources/application.properties b/SHiNE-promo-solana-devnet/src/main/resources/application.properties deleted file mode 100644 index ef6e660..0000000 --- a/SHiNE-promo-solana-devnet/src/main/resources/application.properties +++ /dev/null @@ -1,17 +0,0 @@ -server.port=8021 - -spring.application.name=SHiNE-promo-solana-devnet -spring.thymeleaf.cache=false - -solana.rpc.url=https://api.devnet.solana.com -solana.sender.keypair-file=./config/devnet-wallet.json - -promo.transfer.amount-sol=0.1 -promo.codes.file=./data/promo-codes.txt -promo.used.file=./data/promo-used.txt -promo.explorer.tx-url-template=https://explorer.solana.com/tx/%s?cluster=devnet - -# Вечный промокод для временной раздачи в devnet. -# Если enabled=true, код можно использовать неограниченно (он не "сгорает"). -promo.eternal-code.enabled=true -promo.eternal-code.value=0000 diff --git a/SHiNE-promo-solana-devnet/src/main/resources/static/css/app.css b/SHiNE-promo-solana-devnet/src/main/resources/static/css/app.css deleted file mode 100644 index 1846927..0000000 --- a/SHiNE-promo-solana-devnet/src/main/resources/static/css/app.css +++ /dev/null @@ -1,200 +0,0 @@ -:root { - --bg-main: #070707; - --bg-card: #111217; - --bg-card-soft: #171a22; - --text-main: #f6f8fb; - --text-muted: #aeb4c1; - --accent: #51ffd3; - --accent-soft: rgba(81, 255, 211, 0.15); - --danger: #ff6f6f; - --border: rgba(255, 255, 255, 0.12); -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: "Segoe UI", "Noto Sans", system-ui, sans-serif; - color: var(--text-main); - background: - radial-gradient(circle at 20% 5%, rgba(81, 255, 211, 0.08), transparent 34%), - radial-gradient(circle at 100% 0%, rgba(86, 135, 255, 0.12), transparent 40%), - var(--bg-main); - min-height: 100vh; -} - -.page { - min-height: calc(100vh - 48px); - display: grid; - place-items: center; - padding: 24px 14px; -} - -.card { - width: min(720px, 100%); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01)); - border: 1px solid var(--border); - border-radius: 18px; - padding: 20px; - box-shadow: 0 12px 50px rgba(0, 0, 0, 0.45); -} - -.hero h1 { - margin: 0; - font-size: clamp(1.3rem, 3.8vw, 1.9rem); - line-height: 1.2; -} - -.subtitle { - color: var(--text-muted); - margin-top: 10px; - margin-bottom: 0; -} - -.lead { - margin-top: 18px; - line-height: 1.5; -} - -.warning { - border: 1px solid rgba(255, 255, 255, 0.2); - background: rgba(255, 255, 255, 0.03); - border-radius: 12px; - padding: 12px; - margin-top: 16px; - color: #f0f4ff; -} - -.promo-form { - display: grid; - gap: 10px; - margin-top: 18px; -} - -.promo-form label { - font-size: 0.95rem; - color: var(--text-muted); -} - -.promo-form input { - width: 100%; - background: var(--bg-card-soft); - color: var(--text-main); - border: 1px solid var(--border); - border-radius: 10px; - padding: 14px 12px; - font-size: 1rem; -} - -.promo-form input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-soft); -} - -.promo-form button { - margin-top: 8px; - border: none; - border-radius: 12px; - background: linear-gradient(140deg, #2dd4bf, #4cc9f0); - color: #0b1116; - font-weight: 700; - font-size: 1rem; - padding: 14px; - cursor: pointer; -} - -.promo-form button:disabled { - opacity: 0.7; - cursor: default; -} - -.status { - margin-top: 14px; - padding: 12px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border); -} - -.status-error { - color: #ffd6d6; - border-color: rgba(255, 111, 111, 0.6); - background: rgba(255, 111, 111, 0.12); -} - -.success { - margin-top: 16px; - border: 1px solid rgba(81, 255, 211, 0.35); - background: rgba(81, 255, 211, 0.08); - border-radius: 12px; - padding: 14px; -} - -.success h2 { - margin-top: 0; - font-size: 1.12rem; -} - -.success p { - margin-top: 8px; - line-height: 1.45; -} - -.success ul { - padding-left: 18px; - margin: 10px 0; - display: grid; - gap: 6px; - word-break: break-word; -} - -.success a { - color: var(--accent); -} - -.faq { - margin-top: 20px; -} - -.faq h3 { - margin-bottom: 10px; -} - -.faq details { - background: rgba(255, 255, 255, 0.02); - border: 1px solid var(--border); - border-radius: 10px; - padding: 10px 12px; - margin-bottom: 8px; -} - -.faq summary { - cursor: pointer; - font-weight: 600; -} - -.faq p { - margin-bottom: 4px; - color: var(--text-muted); - line-height: 1.45; -} - -.page-footer { - text-align: center; - color: #727a88; - font-size: 0.8rem; - padding-bottom: 18px; -} - -.hidden { - display: none; -} - -@media (min-width: 768px) { - .card { - padding: 26px; - } -} diff --git a/SHiNE-promo-solana-devnet/src/main/resources/static/js/app.js b/SHiNE-promo-solana-devnet/src/main/resources/static/js/app.js deleted file mode 100644 index df32781..0000000 --- a/SHiNE-promo-solana-devnet/src/main/resources/static/js/app.js +++ /dev/null @@ -1,82 +0,0 @@ -(() => { - const form = document.getElementById("promoForm"); - const walletInput = document.getElementById("wallet"); - const nameInput = document.getElementById("name"); - const promoCodeInput = document.getElementById("promoCode"); - const submitButton = document.getElementById("submitButton"); - - const loadingState = document.getElementById("loadingState"); - const errorState = document.getElementById("errorState"); - const successState = document.getElementById("successState"); - - const successAmount = document.getElementById("successAmount"); - const successWallet = document.getElementById("successWallet"); - const successName = document.getElementById("successName"); - const successSignature = document.getElementById("successSignature"); - const successExplorerUrl = document.getElementById("successExplorerUrl"); - - const params = new URLSearchParams(window.location.search); - const walletFromQuery = (params.get("wallet") || "").trim(); - if (walletFromQuery && !walletInput.value.trim()) { - walletInput.value = walletFromQuery; - } - - form.addEventListener("submit", async (event) => { - event.preventDefault(); - hideMessages(); - toggleLoading(true); - - try { - const payload = { - wallet: walletInput.value.trim(), - name: nameInput.value.trim(), - promoCode: promoCodeInput.value.trim() - }; - - const response = await fetch("/api/promo/top-up", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - if (!result.success) { - showError(result.message || "Не удалось выполнить операцию"); - return; - } - - showSuccess(result); - } catch (error) { - showError("Ошибка сети или недоступен backend"); - } finally { - toggleLoading(false); - } - }); - - function toggleLoading(active) { - submitButton.disabled = active; - loadingState.classList.toggle("hidden", !active); - submitButton.textContent = active ? "Отправляем..." : "Пополнить тестовый счёт"; - } - - function hideMessages() { - errorState.classList.add("hidden"); - successState.classList.add("hidden"); - } - - function showError(message) { - errorState.textContent = message; - errorState.classList.remove("hidden"); - } - - function showSuccess(data) { - successAmount.textContent = data.amountSol || "0.1"; - successWallet.textContent = data.wallet || "—"; - successName.textContent = data.name || "—"; - successSignature.textContent = data.signature || "—"; - successExplorerUrl.href = data.explorerUrl || "#"; - successState.classList.remove("hidden"); - } -})(); diff --git a/SHiNE-promo-solana-devnet/src/main/resources/templates/index.html b/SHiNE-promo-solana-devnet/src/main/resources/templates/index.html deleted file mode 100644 index fb0c853..0000000 --- a/SHiNE-promo-solana-devnet/src/main/resources/templates/index.html +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - SHiNE / Сияние — тестовое пополнение - - - -
-
-
-

SHiNE / Сияние — тестовое пополнение

-

- Временная devnet-страница для приглашённых тестеров Web3-социальной сети SHiNE. -

-
- -

- Если вы получили промокод, введите его ниже. Мы отправим на ваш Solana devnet-кошелёк 0.1 SOL для - тестирования функций SHiNE. -

- -
- Это тестовая сеть Solana Devnet. Эти SOL не являются настоящими деньгами и используются только для - проверки работы приложения. -
- -
- - - - - - - - - - -
- - - - - - -
-

FAQ

- -
- Что такое SHiNE / Сияние? -

- SHiNE / «Сияние» — это тестируемая Web3-социальная сеть, где аккаунты, ключи и часть действий - связаны с блокчейном Solana. Пользователь управляет своим кошельком, а не просто логином и - паролем на обычном сервере. -

-
- -
- Зачем нужен тестовый баланс? -

- В SHiNE регистрация и некоторые действия требуют небольших списаний. Это помогает тестировать - реальную экономику приложения: регистрацию, переводы, сообщения, звонки и другие функции. Сейчас - всё работает в Solana Devnet, поэтому средства тестовые. -

-
- -
- Сколько стоит регистрация? -

- Текущая тестовая стоимость регистрации в SHiNE — 0.1 SOL. Промо-страница отправляет стартовое - тестовое пополнение 0.1 SOL. Условия тестирования могут меняться по мере развития проекта. -

-
- -
- Можно ли пригласить друга? -

- Да. После получения тестового баланса и регистрации в SHiNE вы сможете переводить часть тестовых - SOL другим пользователям, например друзьям или родственникам, чтобы вместе проверить сообщения, - звонки и взаимодействие внутри социальной сети. -

-
- -
- Это настоящие деньги? -

- Нет. Это Solana Devnet — тестовая сеть. Devnet SOL не имеют реальной рыночной ценности и - используются только для разработки и тестирования. -

-
- -
- Почему в Web3 действия платные? -

- В Web3 часть действий связана с транзакциями, хранением данных, подписями и сетевой - инфраструктурой. Зато такая модель позволяет строить систему без навязчивой рекламы и с большей - прозрачностью: пользователь понимает, за что платит, а ключи остаются у него. -

-
- -
- Кто хранит мои ключи? -

- Эта промо-страница не просит и не хранит приватные ключи пользователя. Для пополнения нужен только - публичный адрес кошелька Solana Devnet. -

-
-
-
-
- -
SHiNE Devnet Promo · temporary testing page
- - - - diff --git a/VERSION.properties b/VERSION.properties index a2b093f..a8b6c74 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.215 -server.version=1.2.203 +client.version=1.2.216 +server.version=1.2.204 diff --git a/build.gradle b/build.gradle index 2385265..4866cff 100644 --- a/build.gradle +++ b/build.gradle @@ -292,17 +292,6 @@ tasks.register('startLocalWithBuild') { dependsOn tasks.named('startLocal') } -tasks.register('deployPromoSolanaDevnet', Exec) { - group = "!!deployment" - description = "Деплой отдельного временного сервиса SHiNE-promo-solana-devnet на сервер через домен shineup.me" - - // Этот сервис не входит в основной multi-module build данного репозитория. - // Поэтому деплой выполняется через отдельный Gradle-проект в подпапке - // SHiNE-promo-solana-devnet, где собраны его собственные задачи и зависимости. - workingDir = file('SHiNE-promo-solana-devnet') - commandLine 'bash', '-lc', './gradlew deployToServer' -} - tasks.named('startLocal').configure { mustRunAfter tasks.named('build') }