Промежуточная версия
This commit is contained in:
AidarKC 2025-12-17 13:06:08 +03:00
parent ab44cc5282
commit eaf1affb27
30 changed files with 926 additions and 563 deletions

View File

@ -1,19 +1,38 @@
package blockchain.body;
/**
* Общий интерфейс для всех тел (body) блоков.
*.
* Каждый тип тела реализует:
* - check() проверку корректности данных
* - toBytes() опциональную сериализацию обратно в байты
* BodyRecord_new общий контракт для всех типов body (тела блока).
*
* Идея:
* - На каждый тип body (Header, Text, File, ...) отдельный класс.
* - Десериализация из байтов делается КОНСТРУКТОРОМ:
* new XxxBody_new(byte[] bodyBytes)
* (конструктор обязан распарсить байты или кинуть IllegalArgumentException).
*
* - Валидация делается методом check().
* check() должен:
* - вернуть this, если всё корректно
* - кинуть IllegalArgumentException, если данные некорректны
*
* - Сериализация обратно в байты делается методом toBytes().
*
* - type() и version() это идентификаторы формата body.
* Они должны быть константами для класса (например TYPE=1, VERSION=1).
*/
public interface BodyRecord {
/** Проверить корректность содержимого. */
/** Код типа записи (совпадает с recordType в BchBlockEntry). */
short type();
/** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */
short version();
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
BodyRecord check();
/** (опционально) Сериализация тела обратно в байты. */
default byte[] toBytes() {
throw new UnsupportedOperationException("toBytes() не реализован");
}
/**
* Сериализовать тело записи в байты (ровно то, что кладётся в block.body).
* Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version).
*/
byte[] toBytes();
}

View File

@ -9,11 +9,11 @@ import java.nio.ByteOrder;
* Правило совместимости (строгое):
* - если (type, version) неизвестны кидаем IllegalArgumentException
*/
public final class BodyRecordParser_new {
public final class BodyRecordParser {
private BodyRecordParser_new() {}
private BodyRecordParser() {}
public static BodyRecord_new parse(byte[] bodyBytes) {
public static BodyRecord parse(byte[] bodyBytes) {
if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null");
if (bodyBytes.length < 4) throw new IllegalArgumentException("bodyBytes too short (<4)");
@ -25,8 +25,8 @@ public final class BodyRecordParser_new {
int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF);
return switch (key) {
case 0x0000_0001 -> new HeaderBody_new(bodyBytes); // type=0, ver=1
case 0x0001_0001 -> new TextBody_new(bodyBytes); // type=1, ver=1
case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, ver=1
case 0x0001_0001 -> new TextBody(bodyBytes); // type=1, ver=1
default -> throw new IllegalArgumentException(String.format(
"Unknown body type/version: type=%d ver=%d (key=0x%08X)",
(type & 0xFFFF), (ver & 0xFFFF), key

View File

@ -1,38 +0,0 @@
package blockchain.body;
/**
* BodyRecord_new общий контракт для всех типов body (тела блока).
*
* Идея:
* - На каждый тип body (Header, Text, File, ...) отдельный класс.
* - Десериализация из байтов делается КОНСТРУКТОРОМ:
* new XxxBody_new(byte[] bodyBytes)
* (конструктор обязан распарсить байты или кинуть IllegalArgumentException).
*
* - Валидация делается методом check().
* check() должен:
* - вернуть this, если всё корректно
* - кинуть IllegalArgumentException, если данные некорректны
*
* - Сериализация обратно в байты делается методом toBytes().
*
* - type() и version() это идентификаторы формата body.
* Они должны быть константами для класса (например TYPE=1, VERSION=1).
*/
public interface BodyRecord_new {
/** Код типа записи (совпадает с recordType в BchBlockEntry). */
short type();
/** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */
short version();
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
BodyRecord_new check();
/**
* Сериализовать тело записи в байты (ровно то, что кладётся в block.body).
* Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version).
*/
byte[] toBytes();
}

View File

@ -7,128 +7,98 @@ import java.util.Arrays;
import java.util.Objects;
/**
* ============================================================================
* HeaderBody тело записи типа 0 (заглавие блокчейна)
* ============================================================================
*.
* 🧩 Назначение:
* Первый блок каждой пользовательской цепочки (.bch) это "заголовок".
* Он хранит базовую информацию о владельце, версии и публичном ключе.
*.
* Этот блок всегда имеет:
* recordType = 0
* recordNumber = 0
* recordTypeVersion = 1
*.
* ----------------------------------------------------------------------------
* 🔹 Формат body (без общих 20 байт заголовка блока BchBlock)
*.
* | Смещение | Размер | Поле | Формат | Описание |
* |-----------|--------|--------------------|---------|-----------|
* | 0x00 | 8 | tag | ASCII | Статическая сигнатура "SHiNE001" |
* | 0x08 | 8 | blockchainId | long BE | Уникальный идентификатор цепочки |
* | 0x10 | 1 | userLoginLength=N | uint8 | Длина логина пользователя |
* | 0x11 | N | userLogin | UTF-8 | Логин пользователя |
* | 0x11+N | 4 | blockchainType | int BE | Зарезервировано (всегда 0) |
* | 0x15+N | 4 | blockchainNumber | int BE | Зарезервировано (всегда 0) |
* | 0x19+N | 2 | versionUserBch | short BE| Версия формата (всегда 1) |
* | 0x1B+N | 8 | prevUserBchId | long BE | Зарезервировано (всегда 0) |
* | 0x23+N | 32 | publicKey32 | raw | Публичный ключ (Ed25519, 32 байта) |
*.
* ----------------------------------------------------------------------------
* 💡 Пример структуры в байтах:
*.
* 0000: 53 48 69 4E 45 30 30 31 "SHiNE001"
* 0008: 00 00 00 00 01 23 45 67 blockchainId
* 0010: 05 userLoginLength = 5
* 0011: 41 69 64 61 72 userLogin = "Aidar"
* 0016: 00 00 00 00 blockchainType = 0
* 001A: 00 00 00 00 blockchainNumber = 0
* 001E: 00 01 versionUserBch = 1
* 0020: 00 00 00 00 00 00 00 00 prevUserBchId = 0
* 0028: [32 байта публичного ключа]
*.
* ----------------------------------------------------------------------------
* 📘 Замечания:
* Поля blockchainType, blockchainNumber, versionUserBch, prevUserBchId
* зарезервированы для будущего расширения формата.
* На данный момент все они принимают фиксированные значения:
* blockchainType = 0
* blockchainNumber = 0
* versionUserBch = 1
* prevUserBchId = 0
*.
* ============================================================================
* HeaderBody_new type=0, version=1.
*
* Полный bodyBytes:
* [2] type=0
* [2] version=1
* [payload...]
*
* Payload (как у текущего HeaderBody):
* [8] tag ASCII "SHiNE001"
* [8] blockchainId (long BE)
* [1] loginLength=N (uint8)
* [N] userLogin UTF-8
* [4] blockchainType (int BE) (резерв)
* [4] blockchainNumber (int BE) (резерв)
* [2] versionUserBch (short BE) (резерв)
* [8] prevUserBchId (long BE) (резерв)
* [32] publicKey32 (raw)
*/
public final class HeaderBody implements BodyRecord {
public static final short TYPE = 0;
public static final short VER = 1;
public static final String TAG = "SHiNE001";
public static final int PUBKEY_LEN = 32;
public final String tag; // всегда "SHiNE001"
public final String tag; // "SHiNE001"
public final long blockchainId;
public final String userLogin; // UTF-8
public final int blockchainType; // пока 0
public final int blockchainNumber; // пока 0
public final short versionUserBch; // пока 1
public final long prevUserBchId; // пока 0
public final byte[] publicKey32; // 32 байта
public final String userLogin;
public final int blockchainType;
public final int blockchainNumber;
public final short versionUserBch;
public final long prevUserBchId;
public final byte[] publicKey32;
// ------------------------------------------------------------
// Конструктор 1 из массива байт (для парсинга существующего блока)
// ------------------------------------------------------------
public HeaderBody(byte[] body) {
Objects.requireNonNull(body, "body == null");
if (body.length < 8 + 8 + 1 + 2 + 4 + 4 + 8 + 32)
throw new IllegalArgumentException("HeaderBody слишком короткое");
/**
* Десериализация из полного bodyBytes (ВКЛЮЧАЯ первые 4 байта type/version).
*/
public HeaderBody(byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
if (bodyBytes.length < 4) throw new IllegalArgumentException("HeaderBody_new too short");
ByteBuffer buf = ByteBuffer.wrap(body).order(ByteOrder.BIG_ENDIAN);
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
short type = bb.getShort();
short ver = bb.getShort();
if (type != TYPE || ver != VER)
throw new IllegalArgumentException("Not HeaderBody_new: type=" + type + " ver=" + ver);
// Теперь bb стоит на payload
if (bb.remaining() < 8 + 8 + 1 + 4 + 4 + 2 + 8 + 32)
throw new IllegalArgumentException("Header payload too short");
// [8] тег
byte[] tagBytes = new byte[8];
buf.get(tagBytes);
String tag = new String(tagBytes, StandardCharsets.US_ASCII);
if (!TAG.equals(tag))
throw new IllegalArgumentException("Неверный тег: " + tag);
this.tag = tag;
bb.get(tagBytes);
String t = new String(tagBytes, StandardCharsets.US_ASCII);
if (!TAG.equals(t)) throw new IllegalArgumentException("Bad tag: " + t);
this.tag = t;
// [8] blockchainId
this.blockchainId = buf.getLong();
this.blockchainId = bb.getLong();
// [1] длина логина
int loginLen = Byte.toUnsignedInt(buf.get());
if (loginLen == 0 || buf.remaining() < loginLen + 4 + 4 + 2 + 8 + 32)
throw new IllegalArgumentException("Некорректная длина логина");
int loginLen = Byte.toUnsignedInt(bb.get());
if (loginLen <= 0 || bb.remaining() < loginLen + 4 + 4 + 2 + 8 + 32)
throw new IllegalArgumentException("Bad login length");
// [N] логин
byte[] loginBytes = new byte[loginLen];
buf.get(loginBytes);
bb.get(loginBytes);
this.userLogin = new String(loginBytes, StandardCharsets.UTF_8);
// Остальные поля
this.blockchainType = buf.getInt();
this.blockchainNumber = buf.getInt();
this.versionUserBch = buf.getShort();
this.prevUserBchId = buf.getLong();
this.blockchainType = bb.getInt();
this.blockchainNumber = bb.getInt();
this.versionUserBch = bb.getShort();
this.prevUserBchId = bb.getLong();
this.publicKey32 = new byte[PUBKEY_LEN];
buf.get(this.publicKey32);
bb.get(this.publicKey32);
}
// ------------------------------------------------------------
// Конструктор 2 из параметров (для создания нового заголовка)
// ------------------------------------------------------------
public HeaderBody(long blockchainId, String userLogin,
int blockchainType, int blockchainNumber,
short versionUserBch, long prevUserBchId,
/**
* Создание вручную (для генерации первого блока).
*/
public HeaderBody(long blockchainId,
String userLogin,
int blockchainType,
int blockchainNumber,
short versionUserBch,
long prevUserBchId,
byte[] publicKey32) {
Objects.requireNonNull(userLogin, "userLogin == null");
Objects.requireNonNull(publicKey32, "publicKey32 == null");
if (publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("Публичный ключ должен состоять из 32 байт");
throw new IllegalArgumentException("publicKey32 must be 32 bytes");
this.tag = TAG;
this.blockchainId = blockchainId;
@ -140,17 +110,17 @@ public final class HeaderBody implements BodyRecord {
this.publicKey32 = Arrays.copyOf(publicKey32, PUBKEY_LEN);
}
// ------------------------------------------------------------
// Проверка и сериализация
// ------------------------------------------------------------
@Override public short type() { return TYPE; }
@Override public short version() { return VER; }
@Override
public HeaderBody check() {
if (userLogin == null || userLogin.isBlank())
throw new IllegalArgumentException("Логин не может быть пустым");
throw new IllegalArgumentException("Login is blank");
if (!userLogin.matches("^[A-Za-z0-9_]+$"))
throw new IllegalArgumentException("Логин может содержать только латиницу, цифры и _");
throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$");
if (publicKey32 == null || publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("Публичный ключ должен быть 32 байта");
throw new IllegalArgumentException("publicKey32 must be 32 bytes");
return this;
}
@ -158,34 +128,28 @@ public final class HeaderBody implements BodyRecord {
public byte[] toBytes() {
byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8);
if (loginUtf8.length > 255)
throw new IllegalArgumentException("Логин слишком длинный (>255 байт)");
throw new IllegalArgumentException("Login too long (>255 bytes)");
int cap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32;
ByteBuffer buf = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
int payloadCap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32;
int cap = 4 + payloadCap;
buf.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8]
buf.putLong(blockchainId); // [8]
buf.put((byte) loginUtf8.length); // [1]
buf.put(loginUtf8); // [N]
buf.putInt(blockchainType); // [4]
buf.putInt(blockchainNumber); // [4]
buf.putShort(versionUserBch); // [2]
buf.putLong(prevUserBchId); // [8]
buf.put(publicKey32); // [32]
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
return buf.array();
// [type/version]
bb.putShort(TYPE);
bb.putShort(VER);
// payload
bb.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8]
bb.putLong(blockchainId); // [8]
bb.put((byte) loginUtf8.length); // [1]
bb.put(loginUtf8); // [N]
bb.putInt(blockchainType); // [4]
bb.putInt(blockchainNumber); // [4]
bb.putShort(versionUserBch); // [2]
bb.putLong(prevUserBchId); // [8]
bb.put(publicKey32); // [32]
return bb.array();
}
@Override
public String toString() {
return "HeaderBody{" +
"id=" + blockchainId +
", login='" + userLogin + '\'' +
", type=" + blockchainType +
", num=" + blockchainNumber +
", ver=" + versionUserBch +
", prev=" + prevUserBchId +
", pubkey32=" + Arrays.toString(Arrays.copyOf(publicKey32, 4)) + "..." +
'}';
}
}
}

View File

@ -1,155 +0,0 @@
package blockchain.body;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* HeaderBody_new type=0, version=1.
*
* Полный bodyBytes:
* [2] type=0
* [2] version=1
* [payload...]
*
* Payload (как у текущего HeaderBody):
* [8] tag ASCII "SHiNE001"
* [8] blockchainId (long BE)
* [1] loginLength=N (uint8)
* [N] userLogin UTF-8
* [4] blockchainType (int BE) (резерв)
* [4] blockchainNumber (int BE) (резерв)
* [2] versionUserBch (short BE) (резерв)
* [8] prevUserBchId (long BE) (резерв)
* [32] publicKey32 (raw)
*/
public final class HeaderBody_new implements BodyRecord_new {
public static final short TYPE = 0;
public static final short VER = 1;
public static final String TAG = "SHiNE001";
public static final int PUBKEY_LEN = 32;
public final String tag; // "SHiNE001"
public final long blockchainId;
public final String userLogin;
public final int blockchainType;
public final int blockchainNumber;
public final short versionUserBch;
public final long prevUserBchId;
public final byte[] publicKey32;
/**
* Десериализация из полного bodyBytes (ВКЛЮЧАЯ первые 4 байта type/version).
*/
public HeaderBody_new(byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
if (bodyBytes.length < 4) throw new IllegalArgumentException("HeaderBody_new too short");
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
short type = bb.getShort();
short ver = bb.getShort();
if (type != TYPE || ver != VER)
throw new IllegalArgumentException("Not HeaderBody_new: type=" + type + " ver=" + ver);
// Теперь bb стоит на payload
if (bb.remaining() < 8 + 8 + 1 + 4 + 4 + 2 + 8 + 32)
throw new IllegalArgumentException("Header payload too short");
byte[] tagBytes = new byte[8];
bb.get(tagBytes);
String t = new String(tagBytes, StandardCharsets.US_ASCII);
if (!TAG.equals(t)) throw new IllegalArgumentException("Bad tag: " + t);
this.tag = t;
this.blockchainId = bb.getLong();
int loginLen = Byte.toUnsignedInt(bb.get());
if (loginLen <= 0 || bb.remaining() < loginLen + 4 + 4 + 2 + 8 + 32)
throw new IllegalArgumentException("Bad login length");
byte[] loginBytes = new byte[loginLen];
bb.get(loginBytes);
this.userLogin = new String(loginBytes, StandardCharsets.UTF_8);
this.blockchainType = bb.getInt();
this.blockchainNumber = bb.getInt();
this.versionUserBch = bb.getShort();
this.prevUserBchId = bb.getLong();
this.publicKey32 = new byte[PUBKEY_LEN];
bb.get(this.publicKey32);
}
/**
* Создание вручную (для генерации первого блока).
*/
public HeaderBody_new(long blockchainId,
String userLogin,
int blockchainType,
int blockchainNumber,
short versionUserBch,
long prevUserBchId,
byte[] publicKey32) {
Objects.requireNonNull(userLogin, "userLogin == null");
Objects.requireNonNull(publicKey32, "publicKey32 == null");
if (publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("publicKey32 must be 32 bytes");
this.tag = TAG;
this.blockchainId = blockchainId;
this.userLogin = userLogin;
this.blockchainType = blockchainType;
this.blockchainNumber = blockchainNumber;
this.versionUserBch = versionUserBch;
this.prevUserBchId = prevUserBchId;
this.publicKey32 = Arrays.copyOf(publicKey32, PUBKEY_LEN);
}
@Override public short type() { return TYPE; }
@Override public short version() { return VER; }
@Override
public HeaderBody_new check() {
if (userLogin == null || userLogin.isBlank())
throw new IllegalArgumentException("Login is blank");
if (!userLogin.matches("^[A-Za-z0-9_]+$"))
throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$");
if (publicKey32 == null || publicKey32.length != PUBKEY_LEN)
throw new IllegalArgumentException("publicKey32 must be 32 bytes");
return this;
}
@Override
public byte[] toBytes() {
byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8);
if (loginUtf8.length > 255)
throw new IllegalArgumentException("Login too long (>255 bytes)");
int payloadCap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32;
int cap = 4 + payloadCap;
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
// [type/version]
bb.putShort(TYPE);
bb.putShort(VER);
// payload
bb.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8]
bb.putLong(blockchainId); // [8]
bb.put((byte) loginUtf8.length); // [1]
bb.put(loginUtf8); // [N]
bb.putInt(blockchainType); // [4]
bb.putInt(blockchainNumber); // [4]
bb.putShort(versionUserBch); // [2]
bb.putLong(prevUserBchId); // [8]
bb.put(publicKey32); // [32]
return bb.array();
}
}

View File

@ -1,77 +1,89 @@
package blockchain.body;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* TextBody тело записи типа 1 (простое текстовое сообщение).
*.
* Формат body:
* [N] message (UTF-8)
*.
* Тело полностью состоит из UTF-8-строки без каких-либо метаданных.
* TextBody_new type=1, version=1.
*
* Полный bodyBytes:
* [2] type=1
* [2] version=1
* [payload...]
*
* Payload:
* UTF-8 bytes (N>0)
*/
public final class TextBody implements BodyRecord {
public static final short TYPE = 1;
public static final short VER = 1;
public final String message;
// ------------------------------------------------------------
// Конструктор 1 из массива байт (для парсинга)
// ------------------------------------------------------------
public TextBody(byte[] body) {
Objects.requireNonNull(body, "body == null");
if (body.length == 0)
throw new IllegalArgumentException("Тело текстового сообщения пустое");
/** Десериализация из полного bodyBytes (включая type/version). */
public TextBody(byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
if (bodyBytes.length < 5) // минимум: 4 байта type/ver + 1 байт текста
throw new IllegalArgumentException("TextBody_new too short");
// строгая проверка валидности UTF-8
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
short type = bb.getShort();
short ver = bb.getShort();
if (type != TYPE || ver != VER)
throw new IllegalArgumentException("Not TextBody_new: type=" + type + " ver=" + ver);
byte[] payload = new byte[bb.remaining()];
bb.get(payload);
// строгая проверка UTF-8
var decoder = StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
var chars = decoder.decode(ByteBuffer.wrap(body));
this.message = chars.toString();
this.message = decoder.decode(ByteBuffer.wrap(payload)).toString();
} catch (CharacterCodingException e) {
throw new IllegalArgumentException("Тело не является корректным UTF-8", e);
throw new IllegalArgumentException("Text payload is not valid UTF-8", e);
}
if (this.message.isBlank())
throw new IllegalArgumentException("Text message is blank");
}
// ------------------------------------------------------------
// Конструктор 2 из строки (для создания нового сообщения)
// ------------------------------------------------------------
/** Создание из строки. */
public TextBody(String message) {
Objects.requireNonNull(message, "message == null");
if (message.isBlank())
throw new IllegalArgumentException("Текст сообщения не может быть пустым");
throw new IllegalArgumentException("message is blank");
this.message = message;
}
// ------------------------------------------------------------
// Проверка и сериализация
// ------------------------------------------------------------
@Override public short type() { return TYPE; }
@Override public short version() { return VER; }
@Override
public TextBody check() {
if (message == null || message.isBlank())
throw new IllegalArgumentException("Текст сообщения не может быть пустым");
throw new IllegalArgumentException("Text message is blank");
return this;
}
@Override
public byte[] toBytes() {
return message.getBytes(StandardCharsets.UTF_8);
}
byte[] msg = message.getBytes(StandardCharsets.UTF_8);
if (msg.length == 0)
throw new IllegalArgumentException("Text payload is empty");
@Override
public String toString() {
return "TextBody{" +
"len=" + message.length() +
", msg='" + (message.length() > 60 ? message.substring(0, 57) + "..." : message) + '\'' +
'}';
ByteBuffer bb = ByteBuffer.allocate(4 + msg.length).order(ByteOrder.BIG_ENDIAN);
bb.putShort(TYPE);
bb.putShort(VER);
bb.put(msg);
return bb.array();
}
}
}

View File

@ -1,89 +0,0 @@
package blockchain.body;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* TextBody_new type=1, version=1.
*
* Полный bodyBytes:
* [2] type=1
* [2] version=1
* [payload...]
*
* Payload:
* UTF-8 bytes (N>0)
*/
public final class TextBody_new implements BodyRecord_new {
public static final short TYPE = 1;
public static final short VER = 1;
public final String message;
/** Десериализация из полного bodyBytes (включая type/version). */
public TextBody_new(byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
if (bodyBytes.length < 5) // минимум: 4 байта type/ver + 1 байт текста
throw new IllegalArgumentException("TextBody_new too short");
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
short type = bb.getShort();
short ver = bb.getShort();
if (type != TYPE || ver != VER)
throw new IllegalArgumentException("Not TextBody_new: type=" + type + " ver=" + ver);
byte[] payload = new byte[bb.remaining()];
bb.get(payload);
// строгая проверка UTF-8
var decoder = StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
this.message = decoder.decode(ByteBuffer.wrap(payload)).toString();
} catch (CharacterCodingException e) {
throw new IllegalArgumentException("Text payload is not valid UTF-8", e);
}
if (this.message.isBlank())
throw new IllegalArgumentException("Text message is blank");
}
/** Создание из строки. */
public TextBody_new(String message) {
Objects.requireNonNull(message, "message == null");
if (message.isBlank())
throw new IllegalArgumentException("message is blank");
this.message = message;
}
@Override public short type() { return TYPE; }
@Override public short version() { return VER; }
@Override
public TextBody_new check() {
if (message == null || message.isBlank())
throw new IllegalArgumentException("Text message is blank");
return this;
}
@Override
public byte[] toBytes() {
byte[] msg = message.getBytes(StandardCharsets.UTF_8);
if (msg.length == 0)
throw new IllegalArgumentException("Text payload is empty");
ByteBuffer bb = ByteBuffer.allocate(4 + msg.length).order(ByteOrder.BIG_ENDIAN);
bb.putShort(TYPE);
bb.putShort(VER);
bb.put(msg);
return bb.array();
}
}

View File

@ -0,0 +1,91 @@
package utils.crypto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* BchCryptoVerifier проверка хэша и подписи Ed25519 для .bch сущностей.
*.
* Канонический пре-имидж:
* [N] userLogin UTF-8 (без длины! строго как байты строки)
* [8] blockchainId (big-endian long)
* [32] prevHash32
* [*] rawBytes (без подписи и без хэша)
*.
* Проверяем:
* hash32 == SHA-256(preimage)
* signature64 валидна как Ed25519(preimage, publicKey32)
*/
public final class BchCryptoVerifier {
private static final Logger log = LoggerFactory.getLogger(BchCryptoVerifier.class);
private BchCryptoVerifier() {}
public static boolean verifyAll(String userLogin,
long blockchainId,
byte[] prevHash32,
byte[] rawBytes,
byte[] signature64,
byte[] hash32,
byte[] publicKey32) {
try {
Objects.requireNonNull(userLogin, "userLogin");
requireLen(prevHash32, 32, "prevHash32");
requireLen(signature64, 64, "signature64");
requireLen(hash32, 32, "hash32");
requireLen(publicKey32, 32, "publicKey32");
Objects.requireNonNull(rawBytes, "rawBytes");
byte[] preimage = buildPreimage(userLogin, blockchainId, prevHash32, rawBytes);
// 1) Проверка хэша (BC)
byte[] calcHash = HashSHA256Util.sha256(preimage);
boolean hashOk = Arrays.equals(calcHash, hash32);
// 2) Проверка подписи Ed25519
boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32);
if (!hashOk) log.warn("Hash mismatch: hash32 != SHA-256(preimage)");
if (!sigOk) log.warn("Signature mismatch: Ed25519 verify failed");
return hashOk && sigOk;
} catch (IllegalArgumentException ex) {
log.error("verifyAll: bad arguments", ex);
return false;
}
}
/** Собрать канонический пре-имидж без длины логина. */
public static byte[] buildPreimage(String userLogin,
long blockchainId,
byte[] prevHash32,
byte[] rawBytes) {
Objects.requireNonNull(userLogin, "userLogin");
Objects.requireNonNull(prevHash32, "prevHash32");
Objects.requireNonNull(rawBytes, "rawBytes");
byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8);
requireLen(prevHash32, 32, "prevHash32");
int capacity = loginUtf8.length + 8 + 32 + rawBytes.length;
ByteBuffer buf = ByteBuffer.allocate(capacity).order(ByteOrder.BIG_ENDIAN);
buf.put(loginUtf8);
buf.putLong(blockchainId);
buf.put(prevHash32);
buf.put(rawBytes);
return buf.array();
}
private static void requireLen(byte[] arr, int len, String name) {
if (arr == null) throw new IllegalArgumentException(name + " is null");
if (arr.length != len) {
throw new IllegalArgumentException(name + " length != " + len + " (got " + arr.length + ")");
}
}
}

View File

@ -0,0 +1,50 @@
package utils.crypto;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public final class CryptoSelfTest {
private CryptoSelfTest() {}
/**
* Простой запуск: убедиться, что всё собрано и работает.
* Выводит ключи в Base64, знак/проверка подписи OK/FAIL.
*/
public static void main(String[] args) {
System.out.println("=== Ed25519 self-check ===");
// 1) Генерация ключей
byte[] priv = Ed25519Util.generatePrivateKey();
byte[] pub = Ed25519Util.derivePublicKey(priv);
// 2) Конвертация в/из Base64 (чисто для демонстрации)
String privB64 = Ed25519Util.keyToBase64(priv);
String pubB64 = Ed25519Util.keyToBase64(pub);
System.out.println("Private (seed) Base64: " + privB64);
System.out.println("Public Base64 : " + pubB64);
byte[] priv2 = Ed25519Util.keyFromBase64(privB64);
byte[] pub2 = Ed25519Util.keyFromBase64(pubB64);
if (!Arrays.equals(priv, priv2) || !Arrays.equals(pub, pub2)) {
throw new IllegalStateException("Base64 ⇆ bytes дала несовпадение (не должно случаться).");
}
// 3) Подпись и проверка
byte[] data = "Привет, мир Ed25519!".getBytes(StandardCharsets.UTF_8);
byte[] sig = Ed25519Util.sign(data, priv);
boolean ok = Ed25519Util.verify(data, sig, pub);
System.out.println("Verify OK? " + ok);
// 4) Негативный тест: портим данные
byte[] bad = "Привет, мир Ed25519?".getBytes(StandardCharsets.UTF_8);
boolean shouldFail = Ed25519Util.verify(bad, sig, pub);
System.out.println("Verify on changed data (should be false): " + shouldFail);
if (!ok || shouldFail) {
throw new IllegalStateException("Self-test failed.");
}
System.out.println("Self-test passed ✅");
}
}

View File

@ -0,0 +1,173 @@
package utils.crypto;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Objects;
/**
* ===============================================================
* Ed25519Util статическая утилита для работы с подписями Ed25519
* на базе Bouncy Castle (bcprov). Совместимо с Java 17.
* ---------------------------------------------------------------
* Возможности:
* generatePrivateKey() приватный ключ 32 байта (seed) из SecureRandom.
* generatePrivateKeyFromString(String) приватный ключ 32 байта из строки через SHA-256.
* derivePublicKey(byte[32]) публичный ключ 32 байта из приватного.
* sign(byte[], byte[32]) подпись 64 байта.
* verify(byte[], byte[64], byte[32]) проверка подписи (true/false).
* keyToBase64(byte[32]) / keyFromBase64(String) Base64 ключ (ровно 32 байта).
*.
* Форматы:
* Приватный ключ 32-байтный seed Ed25519.
* Публичный ключ 32-байтный public key.
* Подпись 64 байта.
*.
* Важно:
* Здесь используется «классический» Ed25519 (подпись сырых данных).
* Если нужен режим Ed25519ph (prehash), делай отдельный класс.
*.
* Зависимость (Gradle/Groovy):
* implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
* ===============================================================
*/
public final class Ed25519Util {
/** Длина приватного ключа (seed) в байтах. */
public static final int PRIVATE_KEY_LEN = 32;
/** Длина публичного ключа в байтах. */
public static final int PUBLIC_KEY_LEN = 32;
/** Длина подписи в байтах. */
public static final int SIGNATURE_LEN = 64;
// Запрещаем инстанцирование: только статические методы
private Ed25519Util() {}
// ===== Надёжный генератор случайных чисел (ленивая инициализация) =====
private static final SecureRandom SECURE_RANDOM = createSecureRandom();
private static SecureRandom createSecureRandom() {
try {
return SecureRandom.getInstanceStrong();
} catch (Exception ignore) {
return new SecureRandom();
}
}
// =====================================================================
// API
// =====================================================================
/**
* Сгенерировать приватный ключ (seed) Ed25519: 32 случайных байта.
*/
public static byte[] generatePrivateKey() {
byte[] seed = new byte[PRIVATE_KEY_LEN];
SECURE_RANDOM.nextBytes(seed);
return seed;
}
/**
* Сгенерировать приватный ключ (seed, 32 байта) из произвольной строки:
* строка UTF-8 SHA-256 32 байта.
*
* @param anyString любая строка (не null)
* @return массив 32 байта (seed)
*/
public static byte[] generatePrivateKeyFromString(String anyString) {
Objects.requireNonNull(anyString, "Строка для генерации приватного ключа не должна быть null");
byte[] input = anyString.getBytes(StandardCharsets.UTF_8);
return HashSHA256Util.sha256(input); // ровно 32 байта
}
/**
* Получить публичный ключ (32 байта) из приватного (seed, 32 байта).
*/
public static byte[] derivePublicKey(byte[] privateKey32) {
requireLength(privateKey32, PRIVATE_KEY_LEN, "приватного ключа (seed)");
Ed25519PrivateKeyParameters priv = new Ed25519PrivateKeyParameters(privateKey32, 0);
Ed25519PublicKeyParameters pub = priv.generatePublicKey();
return pub.getEncoded(); // 32 байта
}
/**
* Подписать сырые данные (без предварительного хеширования) приватным ключом Ed25519.
*
* @param data данные для подписи (не null)
* @param privateKey32 приватный ключ (seed) 32 байта
* @return подпись длиной 64 байта
*/
public static byte[] sign(byte[] data, byte[] privateKey32) {
Objects.requireNonNull(data, "Данные для подписи не должны быть null");
requireLength(privateKey32, PRIVATE_KEY_LEN, "приватного ключа (seed)");
Ed25519PrivateKeyParameters priv = new Ed25519PrivateKeyParameters(privateKey32, 0);
Ed25519Signer signer = new Ed25519Signer();
signer.init(true, priv);
signer.update(data, 0, data.length);
byte[] signature = signer.generateSignature();
if (signature == null || signature.length != SIGNATURE_LEN) {
throw new IllegalStateException("Ожидалась подпись длиной 64 байта.");
}
return signature;
}
/**
* Проверить подпись Ed25519.
*
* @param data исходные данные
* @param signature64 подпись 64 байта
* @param publicKey32 публичный ключ 32 байта
* @return true, если подпись корректна для этих данных и ключа
*/
public static boolean verify(byte[] data, byte[] signature64, byte[] publicKey32) {
Objects.requireNonNull(data, "Данные для проверки подписи не должны быть null");
requireLength(signature64, SIGNATURE_LEN, "подписи Ed25519");
requireLength(publicKey32, PUBLIC_KEY_LEN, "публичного ключа");
Ed25519PublicKeyParameters pub = new Ed25519PublicKeyParameters(publicKey32, 0);
Ed25519Signer verifier = new Ed25519Signer();
verifier.init(false, pub);
verifier.update(data, 0, data.length);
return verifier.verifySignature(signature64);
}
/**
* Преобразовать 32-байтный ключ (приватный seed или публичный key) в Base64-строку.
*/
public static String keyToBase64(byte[] key32) {
requireLength(key32, 32, "ключа (ожидалось 32 байта)");
return Base64.getEncoder().encodeToString(key32);
}
/**
* Из Base64-строки получить 32-байтный ключ.
* @throws IllegalArgumentException если после декодирования длина 32
*/
public static byte[] keyFromBase64(String base64) {
Objects.requireNonNull(base64, "Base64-строка не должна быть null");
byte[] raw = Base64.getDecoder().decode(base64);
requireLength(raw, 32, "ключа после декодирования Base64 (ожидалось 32 байта)");
return raw;
}
// =====================================================================
// ВСПОМОГАТЕЛЬНЫЕ
// =====================================================================
private static void requireLength(byte[] data, int expectedLen, String what) {
if (data == null) {
throw new IllegalArgumentException("Массив " + what + " не должен быть null.");
}
if (data.length != expectedLen) {
throw new IllegalArgumentException(
"Некорректная длина " + what + ": " + data.length + " байт(а). Ожидалось: " + expectedLen + "."
);
}
}
}

View File

@ -0,0 +1,53 @@
package utils.crypto;
import org.bouncycastle.crypto.digests.SHA256Digest;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
public final class HashSHA256Util {
private HashSHA256Util() {}
/** Посчитать SHA-256 от всего массива. */
public static byte[] sha256(byte[] data) {
if (data == null) throw new IllegalArgumentException("data == null");
SHA256Digest d = new SHA256Digest();
d.update(data, 0, data.length);
byte[] out = new byte[32];
d.doFinal(out, 0);
return out;
}
/** Получить loginId из строки логина.
* Алгоритм:
* - login -> UTF-8 bytes
* - SHA-256
* - берём последние 8 байт (справа)
* - интерпретируем как signed long (BigEndian)
*/
public static long loginToLoginId(String login) {
if (login == null || login.isBlank())
throw new IllegalArgumentException("login is null or empty");
byte[] hash = sha256(login.getBytes(StandardCharsets.UTF_8));
// последние 8 байт SHA-256
return ByteBuffer.wrap(hash, 24, 8)
.order(ByteOrder.BIG_ENDIAN)
.getLong();
}
/** Инкрементальный SHA-256 (если нужно будет кормить по кускам). */
public static final class Sha256 {
private final SHA256Digest d = new SHA256Digest();
public Sha256 update(byte[] part) {
if (part != null) d.update(part, 0, part.length);
return this;
}
public byte[] doFinal() {
byte[] out = new byte[32];
d.doFinal(out, 0);
return out;
}
}
}

View File

@ -0,0 +1,30 @@
# utils.crypto
Пакет отвечает за криптографию — подписи и хэши блоков.
Используется при создании и проверке целостности `.bch`-блоков.
---
## Классы
### **Ed25519Util**
Работает с подписями Ed25519.
Методы:
- `generatePrivateKey()` — создать приватный ключ (32 байта)
- `generatePrivateKeyFromString(String)` — детерминированный ключ из строки
- `derivePublicKey(byte[32])` — получить публичный ключ (32 байта)
- `sign(byte[], byte[32])` — подписать данные (64-байтная подпись)
- `verify(byte[], byte[64], byte[32])` — проверить подпись
### **HashUtil**
Вычисляет SHA-256.
Методы:
- `sha256(byte[])``[32]` — вернуть хэш массива
- `Sha256` — вложенный класс для пошагового хэширования
### **BchCryptoVerifier**
Проверяет подпись и хэш блока перед записью в блокчейн.
Методы:
- `verifyAll(userLogin, blockchainId, prevHash32, rawBytes, signature64, hash32, publicKey32)`
`true/false` — корректна ли подпись и хэш
- `buildPreimage(...)` — собирает байты, которые подписываются:

View File

@ -1,7 +1,5 @@
package shine.db;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession;
import utils.config.AppConfig;
import java.nio.file.Files;

View File

@ -1,7 +1,7 @@
package shine.db.dao;
import shine.db.SqliteDbController;
import shine.db.entities.ActiveSession;
import shine.db.entities.ActiveSessionEntry;
import java.sql.*;
import java.util.ArrayList;
@ -53,7 +53,7 @@ public final class ActiveSessionsDAO {
/**
* Вставка новой сессии.
*/
public void insert(ActiveSession session) throws SQLException {
public void insert(ActiveSessionEntry session) throws SQLException {
String sql = """
INSERT INTO active_sessions (
sessionId,
@ -94,7 +94,7 @@ public final class ActiveSessionsDAO {
/**
* Получить сессию по sessionId.
*/
public ActiveSession getBySessionId(String sessionId) throws SQLException {
public ActiveSessionEntry getBySessionId(String sessionId) throws SQLException {
String sql = """
SELECT
sessionId,
@ -128,7 +128,7 @@ public final class ActiveSessionsDAO {
/**
* Получить список всех активных сессий пользователя по loginId.
*/
public List<ActiveSession> getByLoginId(long loginId) throws SQLException {
public List<ActiveSessionEntry> getByLoginId(long loginId) throws SQLException {
String sql = """
SELECT
sessionId,
@ -148,7 +148,7 @@ public final class ActiveSessionsDAO {
WHERE loginId = ?
""";
List<ActiveSession> result = new ArrayList<>();
List<ActiveSessionEntry> result = new ArrayList<>();
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
ps.setLong(1, loginId);
@ -235,7 +235,7 @@ public final class ActiveSessionsDAO {
/**
* Маппинг ResultSet ActiveSession (все 13 полей).
*/
private ActiveSession mapRow(ResultSet rs) throws SQLException {
private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException {
String sessionId = rs.getString("sessionId");
long loginId = rs.getLong("loginId");
String sessionPwd = rs.getString("sessionPwd");
@ -250,7 +250,7 @@ public final class ActiveSessionsDAO {
String clientInfoFromRequest = rs.getString("clientInfoFromRequest");
String userLanguage = rs.getString("userLanguage");
return new ActiveSession(
return new ActiveSessionEntry(
sessionId,
loginId,
sessionPwd,

View File

@ -1,7 +1,7 @@
package shine.db.dao;
import shine.db.SqliteDbController;
import shine.db.entities.SolanaUser;
import shine.db.entities.SolanaUserEntry;
import java.sql.*;
import java.util.ArrayList;
@ -36,7 +36,7 @@ public final class SolanaUsersDAO {
return instance;
}
public void insert(SolanaUser user) throws SQLException {
public void insert(SolanaUserEntry user) throws SQLException {
String sql = """
INSERT INTO solana_users (login, loginId, bchId, loginKey, deviceKey, bchLimit)
VALUES (?, ?, ?, ?, ?, ?)
@ -59,7 +59,7 @@ public final class SolanaUsersDAO {
}
}
public SolanaUser getByLoginId(long loginId) throws SQLException {
public SolanaUserEntry getByLoginId(long loginId) throws SQLException {
String sql = """
SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users
@ -76,7 +76,7 @@ public final class SolanaUsersDAO {
}
}
public SolanaUser getByLogin(String login) throws SQLException {
public SolanaUserEntry getByLogin(String login) throws SQLException {
String sql = """
SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users
@ -93,7 +93,7 @@ public final class SolanaUsersDAO {
}
}
public List<SolanaUser> searchByLoginPrefix(String prefix) throws SQLException {
public List<SolanaUserEntry> searchByLoginPrefix(String prefix) throws SQLException {
String sql = """
SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users
@ -102,7 +102,7 @@ public final class SolanaUsersDAO {
LIMIT 5
""";
List<SolanaUser> result = new ArrayList<>();
List<SolanaUserEntry> result = new ArrayList<>();
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
ps.setString(1, prefix.toLowerCase() + "%");
@ -115,8 +115,8 @@ public final class SolanaUsersDAO {
return result;
}
private SolanaUser mapRow(ResultSet rs) throws SQLException {
return new SolanaUser(
private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
return new SolanaUserEntry(
rs.getLong("loginId"),
rs.getString("login"),
rs.getLong("bchId"),

View File

@ -1,7 +1,7 @@
package shine.db.dao;
import shine.db.SqliteDbController;
import shine.db.entities.UserParam;
import shine.db.entities.UserParamEntry;
import java.sql.*;
import java.util.ArrayList;
@ -33,7 +33,7 @@ public final class UserParamsDAO {
* Если запись существует -> обновляем поля.
* Если нет -> вставляем новую запись.
*/
public void upsert(UserParam param) throws SQLException {
public void upsert(UserParamEntry param) throws SQLException {
String sql = """
INSERT INTO users_params (
loginId,
@ -68,7 +68,7 @@ public final class UserParamsDAO {
/**
* Получить параметр по loginId + param.
*/
public UserParam getByUserIdAndParam(long loginId, String paramName) throws SQLException {
public UserParamEntry getByUserIdAndParam(long loginId, String paramName) throws SQLException {
String sql = """
SELECT
loginId,
@ -95,7 +95,7 @@ public final class UserParamsDAO {
/**
* Получить все параметры пользователя.
*/
public List<UserParam> getByUserId(long loginId) throws SQLException {
public List<UserParamEntry> getByUserId(long loginId) throws SQLException {
String sql = """
SELECT
loginId,
@ -110,7 +110,7 @@ public final class UserParamsDAO {
ORDER BY time_ms DESC
""";
List<UserParam> result = new ArrayList<>();
List<UserParamEntry> result = new ArrayList<>();
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
ps.setLong(1, loginId);
@ -122,8 +122,8 @@ public final class UserParamsDAO {
return result;
}
private UserParam mapRow(ResultSet rs) throws SQLException {
return new UserParam(
private UserParamEntry mapRow(ResultSet rs) throws SQLException {
return new UserParamEntry(
rs.getLong("loginId"),
rs.getString("param"),
rs.getLong("bch_channel_id"),

View File

@ -20,7 +20,7 @@ package shine.db.entities;
* FOREIGN KEY (loginId) REFERENCES solana_users(loginId)
* );
*/
public class ActiveSession {
public class ActiveSessionEntry {
private String sessionId; // TEXT base64(32 bytes)
private long loginId; // INTEGER
@ -38,22 +38,22 @@ public class ActiveSession {
private String clientInfoFromRequest; // строка, собранная на сервере
private String userLanguage; // prefer-language (например, "ru-RU")
public ActiveSession() {
public ActiveSessionEntry() {
}
public ActiveSession(String sessionId,
long loginId,
String sessionPwd,
String storagePwd,
long sessionCreatedAtMs,
long lastAuthirificatedAtMs,
String pushEndpoint,
String pushP256dhKey,
String pushAuthKey,
String clientIp,
String clientInfoFromClient,
String clientInfoFromRequest,
String userLanguage) {
public ActiveSessionEntry(String sessionId,
long loginId,
String sessionPwd,
String storagePwd,
long sessionCreatedAtMs,
long lastAuthirificatedAtMs,
String pushEndpoint,
String pushP256dhKey,
String pushAuthKey,
String clientIp,
String clientInfoFromClient,
String clientInfoFromRequest,
String userLanguage) {
this.sessionId = sessionId;
this.loginId = loginId;
this.sessionPwd = sessionPwd;

View File

@ -10,7 +10,7 @@ package shine.db.entities;
* - deviceKey публичный ключ устройства (второй ключ);
* - bchLimit лимит по количеству блоков / размеру цепочки (может быть null).
*/
public class SolanaUser {
public class SolanaUserEntry {
private long loginId;
private String login;
@ -19,15 +19,15 @@ public class SolanaUser {
private String deviceKey; // раньше pubkey1
private Integer bchLimit; // может быть null
public SolanaUser() {
public SolanaUserEntry() {
}
public SolanaUser(long loginId,
String login,
long bchId,
String loginKey,
String deviceKey,
Integer bchLimit) {
public SolanaUserEntry(long loginId,
String login,
long bchId,
String loginKey,
String deviceKey,
Integer bchLimit) {
this.loginId = loginId;
this.login = login;
this.bchId = bchId;

View File

@ -1,6 +1,6 @@
package shine.db.entities;
public class UserParam {
public class UserParamEntry {
private long loginId;
private String param;
@ -10,16 +10,16 @@ public class UserParam {
private short pubkeyNum;
private String signature;
public UserParam() {
public UserParamEntry() {
}
public UserParam(long loginId,
String param,
long bchChannelId,
String value,
long timeMs,
short pubkeyNum,
String signature) {
public UserParamEntry(long loginId,
String param,
long bchChannelId,
String value,
long timeMs,
short pubkeyNum,
String signature) {
this.loginId = loginId;
this.param = param;
this.bchChannelId = bchChannelId;

View File

@ -1,8 +1,8 @@
package server.logic.ws_protocol.JSON;
import org.eclipse.jetty.websocket.api.Session;
import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUserEntry;
import shine.db.entities.ActiveSessionEntry;
/**
* ConnectionContext контекст состояния одного WebSocket-соединения.
@ -16,10 +16,10 @@ public class ConnectionContext {
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
// Полный пользователь из БД (solana_users)
private SolanaUser solanaUser;
private SolanaUserEntry solanaUserEntry;
// Активная сессия из БД (active_sessions)
private ActiveSession activeSession;
private ActiveSessionEntry activeSessionEntry;
/**
* Идентификатор сессии base64-строка от 32 байт.
@ -61,30 +61,30 @@ public class ConnectionContext {
// --- SolanaUser / ActiveSession ---
public SolanaUser getSolanaUser() {
return solanaUser;
public SolanaUserEntry getSolanaUser() {
return solanaUserEntry;
}
public void setSolanaUser(SolanaUser solanaUser) {
this.solanaUser = solanaUser;
public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
this.solanaUserEntry = solanaUserEntry;
}
public ActiveSession getActiveSession() {
return activeSession;
public ActiveSessionEntry getActiveSession() {
return activeSessionEntry;
}
public void setActiveSession(ActiveSession activeSession) {
this.activeSession = activeSession;
public void setActiveSession(ActiveSessionEntry activeSessionEntry) {
this.activeSessionEntry = activeSessionEntry;
}
// --- Удобные геттеры для логина ---
public String getLogin() {
return solanaUser != null ? solanaUser.getLogin() : null;
return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
}
public Long getLoginId() {
return solanaUser != null ? solanaUser.getLoginId() : null;
return solanaUserEntry != null ? solanaUserEntry.getLoginId() : null;
}
// --- sessionId / sessionPwd ---
@ -134,8 +134,8 @@ public class ConnectionContext {
}
public void reset() {
solanaUser = null;
activeSession = null;
solanaUserEntry = null;
activeSessionEntry = null;
sessionId = null;
sessionPwd = null;

View File

@ -0,0 +1,38 @@
package server.logic.ws_protocol.JSON.entyties.Blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_AddBlock_new_Request extends Net_Request {
private long blockchainId;
private int globalNumber;
private String prevGlobalHash; // HEX(64) or ""
private int lineNumber; // 0..7
private int lineBlockNumber;
private String prevLineHash; // HEX(64) or ""
private String blockBase64; // base64url of raw .bch bytes
public long getBlockchainId() { return blockchainId; }
public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; }
public int getGlobalNumber() { return globalNumber; }
public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; }
public String getPrevGlobalHash() { return prevGlobalHash; }
public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; }
public int getLineNumber() { return lineNumber; }
public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; }
public int getLineBlockNumber() { return lineBlockNumber; }
public void setLineBlockNumber(int lineBlockNumber) { this.lineBlockNumber = lineBlockNumber; }
public String getPrevLineHash() { return prevLineHash; }
public void setPrevLineHash(String prevLineHash) { this.prevLineHash = prevLineHash; }
public String getBlockBase64() { return blockBase64; }
public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; }
}

View File

@ -0,0 +1,29 @@
package server.logic.ws_protocol.JSON.entyties.Blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public class Net_AddBlock_new_Response extends Net_Response {
private int serverLastGlobalNumber;
private String serverLastGlobalHash;
private int serverLastLineNumber;
private String serverLastLineHash;
private String reasonCode; // "OUT_OF_SEQUENCE", "HASH_MISMATCH", ...
public int getServerLastGlobalNumber() { return serverLastGlobalNumber; }
public void setServerLastGlobalNumber(int v) { this.serverLastGlobalNumber = v; }
public String getServerLastGlobalHash() { return serverLastGlobalHash; }
public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; }
public int getServerLastLineNumber() { return serverLastLineNumber; }
public void setServerLastLineNumber(int v) { this.serverLastLineNumber = v; }
public String getServerLastLineHash() { return serverLastLineHash; }
public void setServerLastLineHash(String v) { this.serverLastLineHash = v; }
public String getReasonCode() { return reasonCode; }
public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
}

View File

@ -8,7 +8,7 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUser;
import shine.db.entities.SolanaUserEntry;
import java.security.SecureRandom;
import java.util.Base64;
@ -49,9 +49,9 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
}
// 2) Ищем пользователя в локальной БД
SolanaUser solanaUser = SolanaUsersDAO.getInstance().getByLogin(login);
SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
if (solanaUser == null) {
if (solanaUserEntry == null) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.UNVERIFIED,
@ -61,7 +61,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
}
// 3) Заполняем контекст пользователем
ctx.setSolanaUser(solanaUser);
ctx.setSolanaUser(solanaUserEntry);
// 3.1) Отмечаем, что по этому соединению начата авторификация
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);

View File

@ -13,8 +13,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException;
@ -62,7 +62,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
);
}
SolanaUser user = ctx.getSolanaUser();
SolanaUserEntry user = ctx.getSolanaUser();
long currentLoginId = user.getLoginId();
int authStatus = ctx.getAuthenticationStatus();
@ -158,7 +158,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
}
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
ActiveSession targetSession;
ActiveSessionEntry targetSession;
try {
targetSession = sessionsDao.getBySessionId(targetSessionId);
} catch (SQLException e) {

View File

@ -14,8 +14,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import shine.geo.ClientInfoService;
import shine.geo.GeoLookupService;
import utils.crypto.Ed25519Util;
@ -72,7 +72,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
* @throws IllegalArgumentException при некорректном base64 ключа/подписи
*/
public static boolean verifyAuthorificatedSignature(
SolanaUser user,
SolanaUserEntry user,
String authNonce,
long timeMs,
String signatureB64
@ -108,7 +108,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
return err;
}
SolanaUser user = ctx.getSolanaUser();
SolanaUserEntry user = ctx.getSolanaUser();
Long loginId = user.getLoginId();
if (loginId == null) {
Net_Response err = NetExceptionResponseFactory.error(
@ -237,10 +237,10 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
// --- создаём запись ActiveSession и сохраняем в БД ---
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
ActiveSession activeSession;
ActiveSessionEntry activeSessionEntry;
try {
activeSession = new ActiveSession(
activeSessionEntry = new ActiveSessionEntry(
sessionId,
loginId,
newSessionPwd, // настоящий секрет сессии
@ -256,7 +256,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
userLanguage
);
dao.insert(activeSession);
dao.insert(activeSessionEntry);
} catch (SQLException e) {
log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e);
Net_Response err = NetExceptionResponseFactory.error(
@ -270,7 +270,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
}
// --- обновляем контекст ---
ctx.setActiveSession(activeSession);
ctx.setActiveSession(activeSessionEntry);
ctx.setSessionId(sessionId);
ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии
ctx.setAuthNonce(null); // одноразовый nonce больше не нужен

View File

@ -12,8 +12,8 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import shine.geo.GeoLookupService;
import java.sql.SQLException;
@ -50,7 +50,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
);
}
SolanaUser user = ctx.getSolanaUser();
SolanaUserEntry user = ctx.getSolanaUser();
long currentLoginId = user.getLoginId();
int authStatus = ctx.getAuthenticationStatus();
@ -128,7 +128,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
}
// 3) Тянем все активные сессии пользователя из БД
List<ActiveSession> sessions;
List<ActiveSessionEntry> sessions;
try {
sessions = ActiveSessionsDAO.getInstance().getByLoginId(currentLoginId);
} catch (SQLException e) {
@ -143,7 +143,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
// 4) Собираем DTO с геолокацией
List<SessionInfo> resultList = new ArrayList<>();
for (ActiveSession s : sessions) {
for (ActiveSessionEntry s : sessions) {
SessionInfo info = new SessionInfo();
info.setSessionId(s.getSessionId());
info.setClientInfoFromClient(s.getClientInfoFromClient());

View File

@ -13,8 +13,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import shine.geo.ClientInfoService;
import shine.geo.GeoLookupService;
@ -63,7 +63,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
}
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
ActiveSession session;
ActiveSessionEntry session;
try {
session = sessionsDao.getBySessionId(sessionId);
} catch (SQLException e) {
@ -96,11 +96,11 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
}
// --- вытаскиваем пользователя по loginId ---
SolanaUser solanaUser = null;
SolanaUserEntry solanaUserEntry = null;
long loginId = session.getLoginId();
try {
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
solanaUser = usersDao.getByLoginId(loginId);
solanaUserEntry = usersDao.getByLoginId(loginId);
} catch (SQLException e) {
log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e);
return NetExceptionResponseFactory.error(
@ -111,7 +111,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
);
}
if (solanaUser == null) {
if (solanaUserEntry == null) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.UNVERIFIED,
@ -171,7 +171,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
// --- обновляем контекст соединения ---
if (ctx != null) {
ctx.setActiveSession(session);
ctx.setSolanaUser(solanaUser);
ctx.setSolanaUser(solanaUserEntry);
ctx.setSessionId(sessionId);
ctx.setSessionPwd(sessionPwd);
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);

View File

@ -0,0 +1,190 @@
package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain.BchBlockEntry;
import blockchain.BodyRecordParser;
import blockchain.body.BodyRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Request;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.entities.BlockchainStateEntry;
import utils.crypto.BchCryptoVerifier;
import utils.files.FileStoreUtil;
import java.util.Base64;
public class Net_AddBlock_new_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_new_Handler.class);
@Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq;
// 0) базовые проверки
if (req.getBlockchainId() <= 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCKCHAIN_ID", "blockchainId <= 0");
}
if (req.getGlobalNumber() < 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_GLOBAL_NUMBER", "globalNumber < 0");
}
if (req.getLineNumber() < 0 || req.getLineNumber() > 7) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_NUMBER", "lineNumber must be 0..7");
}
if (req.getLineBlockNumber() < 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_BLOCK_NUMBER", "lineBlockNumber < 0");
}
if (req.getBlockBase64() == null || req.getBlockBase64().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_BLOCK", "blockBase64 is empty");
}
// 1) грузим состояние из БД
BlockchainStateDAO dao = BlockchainStateDAO.getInstance();
BlockchainStateEntry state = dao.getByBlockchainId(req.getBlockchainId());
if (state == null) {
// на MVP можно: запретить добавление, пока цепочка не создана отдельно
// либо разрешить только genesis/header как ты делал раньше
return NetExceptionResponseFactory.error(req, WireCodes.Status.CHAIN_NOT_FOUND, "CHAIN_NOT_FOUND", "chain not found in DB");
}
// 2) быстрые проверки на подходит ли блок
int expectedGlobal = state.getLastGlobalNumber() + 1;
int expectedLine = state.getLastLineNumber(req.getLineNumber()) + 1;
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
String dbPrevLineHash = nn(state.getLastLineHash(req.getLineNumber()));
if (req.getGlobalNumber() != expectedGlobal) {
return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_GLOBAL");
}
if (!eqHash(req.getPrevGlobalHash(), dbPrevGlobalHash)) {
return outOfSeq(req, state, req.getLineNumber(), "GLOBAL_HASH_MISMATCH");
}
if (req.getLineBlockNumber() != expectedLine) {
return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_LINE");
}
if (!eqHash(req.getPrevLineHash(), dbPrevLineHash)) {
return outOfSeq(req, state, req.getLineNumber(), "LINE_HASH_MISMATCH");
}
// 3) декодируем блок
byte[] fullBlockBytes;
try {
fullBlockBytes = Base64.getUrlDecoder().decode(req.getBlockBase64());
} catch (IllegalArgumentException e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "blockBase64 decode failed");
}
// 4) парсим .bch
BchBlockEntry block;
try {
block = new BchBlockEntry(fullBlockBytes);
} catch (Exception e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCK_FORMAT", "cannot parse BchBlockEntry");
}
// 5) ПОЛНАЯ валидация: подпись/хэш/тело
// ниже я оставляю общий вызов verifyAll как у тебя раньше,
// но теперь prevHash берём из БД, а publicKey из state (или из solana_users).
byte[] prevHashGlobal32 = hexToBytes32(dbPrevGlobalHash);
boolean verified = BchCryptoVerifier.verifyAll(
state.getUserLogin(),
req.getBlockchainId(),
prevHashGlobal32,
block.rawBytes,
block.getSignature64(),
block.getHash32(),
Base64.getDecoder().decode(state.getPublicKeyBase64())
);
if (!verified) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "UNVERIFIED", "signature/hash verification failed");
}
// Проверка тела блока
BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check();
// 6) TODO: извлечь lineNumber/lineBlockNumber/prevLineHash из body (если они реально в теле есть)
// и сверить с req + DB. Сейчас оставляю как крючок.
// BlockLineMeta meta = BlockLineMetaExtractor.extract(body);
// if (meta.lineNumber != req.getLineNumber()) ...
// if (meta.lineBlockNumber != req.getLineBlockNumber()) ...
// if (!eqHash(meta.prevLineHashHex, dbPrevLineHash)) ...
// 7) запись в файл (фактическое хранение блоков)
FileStoreUtil.getInstance().addDataToBlockchain(req.getBlockchainId(), fullBlockBytes);
// 8) TODO: обновление состояния в БД (вместо BchInfoManager)
// - state.sizeBytes += fullBlockBytes.length
// - state.lastGlobalNumber = req.globalNumber
// - state.lastGlobalHash = bytesToHex(block.getHash32())
// - state.lineX_last_number/hash обновить по lineNumber
// - state.updatedAtMs = now
// dao.upsert(state);
// 9) ответ OK
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
// можно вернуть новое состояние, но на MVP вернём хотя бы серверные lastы до апдейта/после апдейта
resp.setServerLastGlobalNumber(req.getGlobalNumber());
resp.setServerLastGlobalHash(bytesToHex(block.getHash32()));
resp.setServerLastLineNumber(req.getLineBlockNumber());
resp.setServerLastLineHash(resp.getServerLastGlobalHash());
resp.setReasonCode(null);
return resp;
}
private static Net_AddBlock_new_Response outOfSeq(Net_AddBlock_new_Request req, BlockchainStateEntry state, int line, String reason) {
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OUT_OF_SEQUENCE); // или свой статус
resp.setReasonCode(reason);
resp.setServerLastGlobalNumber(state.getLastGlobalNumber());
resp.setServerLastGlobalHash(nn(state.getLastGlobalHash()));
resp.setServerLastLineNumber(state.getLastLineNumber(line));
resp.setServerLastLineHash(nn(state.getLastLineHash(line)));
return resp;
}
private static boolean eqHash(String a, String b) {
return nn(a).equalsIgnoreCase(nn(b));
}
private static String nn(String s) { return s == null ? "" : s.trim(); }
private static byte[] hexToBytes32(String hex) {
hex = nn(hex);
if (hex.isEmpty()) return new byte[32];
int len = hex.length();
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
if (out.length == 32) return out;
byte[] full = new byte[32];
int copy = Math.min(out.length, 32);
System.arraycopy(out, out.length - copy, full, 32 - copy, copy);
return full;
}
private static String bytesToHex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte x : b) sb.append(String.format("%02x", x));
return sb.toString();
}
}

View File

@ -11,7 +11,7 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUser;
import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException;
@ -61,7 +61,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
try {
SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
SolanaUser user = new SolanaUser(
SolanaUserEntry user = new SolanaUserEntry(
req.getLoginId(),
req.getLogin(),
req.getBchId(),

View File

@ -5,8 +5,6 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUser;
import utils.config.AppConfig;
import java.time.Duration;