02 01 25
Доделал тесты и названия линий сделал в константы Дальше делать: Описание форматов. Запросы клиент-сервер. Промт на клиента. --- Потом в сервак дописать Синхронизацию серверов.
This commit is contained in:
parent
be7a3ab7a6
commit
c3d20ba338
@ -0,0 +1,17 @@
|
|||||||
|
package blockchain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LineIndex — канонические номера линий блокчейна.
|
||||||
|
*
|
||||||
|
* Линия = независимая последовательность блоков внутри одного блокчейна.
|
||||||
|
*/
|
||||||
|
public final class LineIndex {
|
||||||
|
|
||||||
|
private LineIndex() {}
|
||||||
|
|
||||||
|
public static final short HEADER = 0; // genesis / идентификация
|
||||||
|
public static final short TEXT = 1; // сообщения
|
||||||
|
public static final short REACTION = 2; // реакции
|
||||||
|
public static final short CONNECTION = 3; // связи (friend/contact/follow)
|
||||||
|
public static final short USER_PARAM = 4; // параметры профиля
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.LineIndex;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -173,7 +175,7 @@ public final class ConnectionBody implements BodyRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public short expectedLineIndex() {
|
public short expectedLineIndex() {
|
||||||
return 3;
|
return LineIndex.CONNECTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.LineIndex;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -93,8 +95,7 @@ public final class HeaderBody implements BodyRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public short expectedLineIndex() {
|
public short expectedLineIndex() {
|
||||||
return 0;
|
return LineIndex.HEADER; }
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderBody check() {
|
public HeaderBody check() {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.LineIndex;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -114,7 +116,7 @@ public final class ReactionBody implements BodyRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public short expectedLineIndex() {
|
public short expectedLineIndex() {
|
||||||
return 2;
|
return LineIndex.REACTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.LineIndex;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.CharacterCodingException;
|
import java.nio.charset.CharacterCodingException;
|
||||||
@ -223,7 +225,7 @@ public final class TextBody implements BodyRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public short expectedLineIndex() {
|
public short expectedLineIndex() {
|
||||||
return 1;
|
return LineIndex.TEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.LineIndex;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.CharacterCodingException;
|
import java.nio.charset.CharacterCodingException;
|
||||||
@ -138,7 +140,7 @@ public final class UserParamBody implements BodyRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public short expectedLineIndex() {
|
public short expectedLineIndex() {
|
||||||
return 4;
|
return LineIndex.USER_PARAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -12,5 +12,9 @@ find . -type f -name "*.java" | sort | while read -r f; do
|
|||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Готово! Все .java файлы собраны в $OUTFILE"
|
# скопировать весь файл в буфер обмена (Wayland)
|
||||||
|
wl-copy < "$OUTFILE"
|
||||||
|
|
||||||
|
echo "Готово!"
|
||||||
|
echo "Все .java файлы собраны в $OUTFILE"
|
||||||
|
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||||
|
|||||||
196
src/test/java/test/it/addBlockUtils/AddBlockSender.java
Normal file
196
src/test/java/test/it/addBlockUtils/AddBlockSender.java
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package test.it.addBlockUtils;
|
||||||
|
|
||||||
|
import blockchain.BchBlockEntry;
|
||||||
|
import blockchain.BchCryptoVerifier;
|
||||||
|
import blockchain.body.BodyRecord;
|
||||||
|
import test.it.utils.TestConfig;
|
||||||
|
import test.it.utils.TestLog;
|
||||||
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AddBlockSender — "одна кнопка":
|
||||||
|
* - принимает ГОТОВЫЙ Body (HeaderBody/TextBody/ReactionBody)
|
||||||
|
* - сам берёт номера/prev-hash из ChainState
|
||||||
|
* - строит raw/hash/signature
|
||||||
|
* - собирает BchBlockEntry (старый, без изменений)
|
||||||
|
* - отправляет AddBlock
|
||||||
|
* - проверяет serverLastGlobalHash == localHash
|
||||||
|
* - обновляет ChainState
|
||||||
|
*
|
||||||
|
* В тестах:
|
||||||
|
* sender.send(body, timeout);
|
||||||
|
*/
|
||||||
|
public final class AddBlockSender {
|
||||||
|
|
||||||
|
private static final byte[] ZERO32 = new byte[32];
|
||||||
|
private static final String ZERO64 = "0".repeat(64);
|
||||||
|
|
||||||
|
private final ChainState state;
|
||||||
|
|
||||||
|
public AddBlockSender(ChainState state) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChainState state() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить следующий блок по body.expectedLineIndex().
|
||||||
|
* Ничего не возвращает — состояние хранится в ChainState.
|
||||||
|
*/
|
||||||
|
public void send(BodyRecord body, Duration timeout) {
|
||||||
|
if (body == null) throw new IllegalArgumentException("body == null");
|
||||||
|
|
||||||
|
short lineIndex = body.expectedLineIndex();
|
||||||
|
|
||||||
|
// header должен быть первым
|
||||||
|
if (lineIndex == 0) {
|
||||||
|
if (state.globalLastNumber() != -1) {
|
||||||
|
throw new IllegalStateException("HEADER должен быть первым: globalLastNumber уже " + state.globalLastNumber());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!state.hasHeader()) {
|
||||||
|
throw new IllegalStateException("Нельзя слать line=" + lineIndex + " до HEADER (нет headerHash32)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int globalNumber = state.nextGlobalNumber();
|
||||||
|
int lineNumber = state.nextLineNumber(lineIndex);
|
||||||
|
|
||||||
|
byte[] prevGlobalHash32 = (lineIndex == 0) ? ZERO32 : state.prevGlobalHash32ForNext(lineIndex);
|
||||||
|
byte[] prevLineHash32 = (lineIndex == 0) ? ZERO32 : state.prevLineHash32ForNext(lineIndex);
|
||||||
|
|
||||||
|
long ts = System.currentTimeMillis() / 1000L;
|
||||||
|
|
||||||
|
byte[] bodyBytes = body.toBytes();
|
||||||
|
|
||||||
|
// RAW bytes (ровно то, что подписываем/хэшируем)
|
||||||
|
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
|
||||||
|
|
||||||
|
byte[] rawBytes = ByteBuffer.allocate(recordSize)
|
||||||
|
.order(ByteOrder.BIG_ENDIAN)
|
||||||
|
.putInt(recordSize)
|
||||||
|
.putInt(globalNumber)
|
||||||
|
.putLong(ts)
|
||||||
|
.putShort(lineIndex)
|
||||||
|
.putInt(lineNumber)
|
||||||
|
.put(bodyBytes)
|
||||||
|
.array();
|
||||||
|
|
||||||
|
// preimage -> sha256 -> signature
|
||||||
|
byte[] preimage = BchCryptoVerifier.buildPreimage(
|
||||||
|
TestConfig.LOGIN(),
|
||||||
|
prevGlobalHash32,
|
||||||
|
prevLineHash32,
|
||||||
|
rawBytes
|
||||||
|
);
|
||||||
|
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
|
||||||
|
byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY());
|
||||||
|
|
||||||
|
// Собираем полный блок (BchBlockEntry не меняем)
|
||||||
|
BchBlockEntry entry = new BchBlockEntry(
|
||||||
|
globalNumber,
|
||||||
|
ts,
|
||||||
|
lineIndex,
|
||||||
|
lineNumber,
|
||||||
|
bodyBytes,
|
||||||
|
signature64,
|
||||||
|
hash32
|
||||||
|
);
|
||||||
|
|
||||||
|
// отправляем JSON
|
||||||
|
String prevGlobalHashHex = (globalNumber == 0) ? ZERO64 : state.globalLastHashHex();
|
||||||
|
|
||||||
|
String req = buildAddBlockJson(
|
||||||
|
TestConfig.BCH_NAME(),
|
||||||
|
globalNumber,
|
||||||
|
prevGlobalHashHex,
|
||||||
|
base64(entry.toBytes())
|
||||||
|
);
|
||||||
|
|
||||||
|
String op = "AddBlock (global=" + globalNumber + ", line=" + lineIndex + ", lineNum=" + lineNumber + ")";
|
||||||
|
String resp = WsJsonOneShot.request(op, req, timeout);
|
||||||
|
|
||||||
|
assert200(op, resp);
|
||||||
|
|
||||||
|
String serverLastGlobalHash = extractPayloadString(resp, "serverLastGlobalHash");
|
||||||
|
assertNotNull(serverLastGlobalHash, op + ": payload.serverLastGlobalHash must not be null");
|
||||||
|
assertEquals(64, serverLastGlobalHash.trim().length(), op + ": serverLastGlobalHash must be 64 hex chars");
|
||||||
|
|
||||||
|
String localHashHex = bytesToHex64(hash32);
|
||||||
|
|
||||||
|
if (TestConfig.DEBUG()) {
|
||||||
|
TestLog.ok(op + ": localHash=" + localHashHex);
|
||||||
|
TestLog.ok(op + ": serverLastGlobalHash=" + serverLastGlobalHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(localHashHex, serverLastGlobalHash, op + ": serverLastGlobalHash must match local hash");
|
||||||
|
|
||||||
|
// обновляем ChainState
|
||||||
|
state.applyAppendedBlock(globalNumber, lineIndex, lineNumber, hash32);
|
||||||
|
|
||||||
|
if (TestConfig.DEBUG()) {
|
||||||
|
TestLog.ok(op + ": state updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- json helpers --------------------
|
||||||
|
|
||||||
|
private static String buildAddBlockJson(String blockchainName,
|
||||||
|
int globalNumber,
|
||||||
|
String prevGlobalHashHex,
|
||||||
|
String blockBytesB64) {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"op": "AddBlock",
|
||||||
|
"requestId": "%s",
|
||||||
|
"payload": {
|
||||||
|
"blockchainName": "%s",
|
||||||
|
"globalNumber": %d,
|
||||||
|
"prevGlobalHash": "%s",
|
||||||
|
"blockBytesB64": "%s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(WsJsonOneShot.FIXED_REQUEST_ID, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assert200(String op, String resp) {
|
||||||
|
int st = test.it.utils.JsonParsers.status(resp);
|
||||||
|
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
|
||||||
|
if (TestConfig.DEBUG()) TestLog.ok(op + ": status=200");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractPayloadString(String json, String field) {
|
||||||
|
try {
|
||||||
|
com.fasterxml.jackson.databind.JsonNode root =
|
||||||
|
new com.fasterxml.jackson.databind.ObjectMapper().readTree(json);
|
||||||
|
com.fasterxml.jackson.databind.JsonNode payload = root.get("payload");
|
||||||
|
if (payload != null && payload.has(field)) return payload.get(field).asText();
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String base64(byte[] bytes) {
|
||||||
|
return Base64.getEncoder().encodeToString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex64(byte[] b32) {
|
||||||
|
char[] out = new char[64];
|
||||||
|
final char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
int v = b32[i] & 0xFF;
|
||||||
|
out[i * 2] = HEX[v >>> 4];
|
||||||
|
out[i * 2 + 1] = HEX[v & 0x0F];
|
||||||
|
}
|
||||||
|
return new String(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
182
src/test/java/test/it/addBlockUtils/ChainState.java
Normal file
182
src/test/java/test/it/addBlockUtils/ChainState.java
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package test.it.addBlockUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChainState — только состояние цепочки (номера/хэши).
|
||||||
|
*
|
||||||
|
* Хранит:
|
||||||
|
* - last globalNumber / last globalHash
|
||||||
|
* - last lineNum / last lineHash по каждой линии
|
||||||
|
* - hash32 нулевого блока (headerHash32) — нужен как prevLineHash для первого блока каждой линии
|
||||||
|
* - map globalNumber -> hash32 (для ссылок reply/reaction на старые блоки)
|
||||||
|
*/
|
||||||
|
public final class ChainState {
|
||||||
|
|
||||||
|
public static final int LINES_MAX = 8;
|
||||||
|
|
||||||
|
private static final byte[] ZERO32 = new byte[32];
|
||||||
|
private static final String ZERO64 = "0".repeat(64);
|
||||||
|
|
||||||
|
private final int[] lineLastNumber = new int[LINES_MAX];
|
||||||
|
private final String[] lineLastHashHex = new String[LINES_MAX];
|
||||||
|
|
||||||
|
private int globalLastNumber = -1;
|
||||||
|
private String globalLastHashHex = ZERO64;
|
||||||
|
|
||||||
|
private byte[] headerHash32 = null;
|
||||||
|
|
||||||
|
// Для удобства тестов: чтобы можно было делать reply/like на любой уже отправленный globalNumber
|
||||||
|
private final Map<Integer, byte[]> globalHash32ByNumber = new HashMap<>();
|
||||||
|
|
||||||
|
public ChainState() {
|
||||||
|
Arrays.fill(lineLastHashHex, "");
|
||||||
|
// lineLastNumber по умолчанию = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- getters --------------------
|
||||||
|
|
||||||
|
public int globalLastNumber() { return globalLastNumber; }
|
||||||
|
public String globalLastHashHex() { return globalLastHashHex; }
|
||||||
|
|
||||||
|
public int lineLastNumber(short line) { return lineLastNumber[line]; }
|
||||||
|
public String lineLastHashHex(short line) { return lineLastHashHex[line]; }
|
||||||
|
|
||||||
|
public byte[] headerHash32() { return headerHash32 == null ? null : headerHash32.clone(); }
|
||||||
|
|
||||||
|
public byte[] getGlobalHash32(int globalNumber) {
|
||||||
|
byte[] h = globalHash32ByNumber.get(globalNumber);
|
||||||
|
return h == null ? null : h.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- state helpers --------------------
|
||||||
|
|
||||||
|
public boolean hasHeader() {
|
||||||
|
return headerHash32 != null && headerHash32.length == 32 && globalLastNumber >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Следующий globalNumber. */
|
||||||
|
public int nextGlobalNumber() {
|
||||||
|
return globalLastNumber + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Следующий lineNumber: для line>0 — last+1. Для line0 — всегда 0 (header). */
|
||||||
|
public int nextLineNumber(short lineIndex) {
|
||||||
|
checkLine(lineIndex);
|
||||||
|
if (lineIndex == 0) return 0;
|
||||||
|
return lineLastNumber[lineIndex] + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** prevGlobalHash32: для header это ZERO32, иначе hash последнего глобального блока. */
|
||||||
|
public byte[] prevGlobalHash32ForNext(short nextLineIndex) {
|
||||||
|
// Для genesis/header prevGlobalHash = ZERO32
|
||||||
|
if (globalLastNumber < 0) return ZERO32;
|
||||||
|
return hexToBytes32(globalLastHashHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* prevLineHash32 по твоему правилу:
|
||||||
|
* - для line0 (header) — ZERO32
|
||||||
|
* - для первого блока линии (lineLastNumber[line]==0) — hash нулевого блока (headerHash32)
|
||||||
|
* - иначе — hash последнего блока этой линии
|
||||||
|
*/
|
||||||
|
public byte[] prevLineHash32ForNext(short lineIndex) {
|
||||||
|
checkLine(lineIndex);
|
||||||
|
if (lineIndex == 0) return ZERO32;
|
||||||
|
|
||||||
|
if (lineLastNumber[lineIndex] == 0) {
|
||||||
|
if (headerHash32 == null) {
|
||||||
|
throw new IllegalStateException("headerHash32 is not set but required for first block of line " + lineIndex);
|
||||||
|
}
|
||||||
|
return headerHash32.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
String lastHex = lineLastHashHex[lineIndex];
|
||||||
|
if (lastHex == null || lastHex.isBlank()) {
|
||||||
|
throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] is blank but lineLastNumber>0");
|
||||||
|
}
|
||||||
|
return hexToBytes32(lastHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Применить факт успешного добавления блока:
|
||||||
|
* - обновить global last
|
||||||
|
* - обновить line last
|
||||||
|
* - сохранить globalNumber->hash32
|
||||||
|
* - если это header: сохранить headerHash32
|
||||||
|
*/
|
||||||
|
public void applyAppendedBlock(int globalNumber,
|
||||||
|
short lineIndex,
|
||||||
|
int lineNumber,
|
||||||
|
byte[] hash32) {
|
||||||
|
|
||||||
|
if (hash32 == null || hash32.length != 32) {
|
||||||
|
throw new IllegalArgumentException("hash32 must be 32 bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// базовые ожидания по номерам (для тестов строго)
|
||||||
|
if (globalNumber != globalLastNumber + 1) {
|
||||||
|
throw new IllegalStateException("globalNumber sequence broken: expected=" + (globalLastNumber + 1) + " got=" + globalNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkLine(lineIndex);
|
||||||
|
|
||||||
|
if (lineIndex == 0) {
|
||||||
|
if (globalNumber != 0 || lineNumber != 0) {
|
||||||
|
throw new IllegalStateException("Header must be global=0 line=0 lineNum=0");
|
||||||
|
}
|
||||||
|
headerHash32 = hash32.clone();
|
||||||
|
} else {
|
||||||
|
int expectedLineNum = lineLastNumber[lineIndex] + 1;
|
||||||
|
if (lineNumber != expectedLineNum) {
|
||||||
|
throw new IllegalStateException("lineNumber sequence broken for line=" + lineIndex +
|
||||||
|
": expected=" + expectedLineNum + " got=" + lineNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String hex64 = bytesToHex64(hash32);
|
||||||
|
|
||||||
|
globalLastNumber = globalNumber;
|
||||||
|
globalLastHashHex = hex64;
|
||||||
|
|
||||||
|
lineLastNumber[lineIndex] = lineNumber;
|
||||||
|
lineLastHashHex[lineIndex] = hex64;
|
||||||
|
|
||||||
|
globalHash32ByNumber.put(globalNumber, hash32.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- utils --------------------
|
||||||
|
|
||||||
|
private static void checkLine(short lineIndex) {
|
||||||
|
if (lineIndex < 0 || lineIndex >= LINES_MAX) {
|
||||||
|
throw new IllegalArgumentException("lineIndex must be 0.." + (LINES_MAX - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] hexToBytes32(String hex) {
|
||||||
|
if (hex == null) throw new IllegalArgumentException("hex is null");
|
||||||
|
String s = hex.trim();
|
||||||
|
if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
|
||||||
|
byte[] out = new byte[32];
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
int hi = Character.digit(s.charAt(i * 2), 16);
|
||||||
|
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
|
||||||
|
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
|
||||||
|
out[i] = (byte) ((hi << 4) | lo);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex64(byte[] b32) {
|
||||||
|
char[] out = new char[64];
|
||||||
|
final char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
int v = b32[i] & 0xFF;
|
||||||
|
out[i * 2] = HEX[v >>> 4];
|
||||||
|
out[i * 2 + 1] = HEX[v & 0x0F];
|
||||||
|
}
|
||||||
|
return new String(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/test/java/test/it/addBlockUtils/JsonMini.java
Normal file
24
src/test/java/test/it/addBlockUtils/JsonMini.java
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package test.it.addBlockUtils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JsonMini — маленькие утилиты, чтобы не раздувать зависимости.
|
||||||
|
*/
|
||||||
|
final class JsonMini {
|
||||||
|
private static final ObjectMapper M = new ObjectMapper();
|
||||||
|
private JsonMini() {}
|
||||||
|
|
||||||
|
static String extractPayloadString(String json, String field) {
|
||||||
|
try {
|
||||||
|
JsonNode root = M.readTree(json);
|
||||||
|
JsonNode payload = root.get("payload");
|
||||||
|
if (payload != null && payload.has(field)) {
|
||||||
|
JsonNode v = payload.get(field);
|
||||||
|
return (v == null || v.isNull()) ? null : v.asText();
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,7 +27,7 @@ public final class TestConfig {
|
|||||||
public static final String WS_URI = "ws://localhost:7070/ws";
|
public static final String WS_URI = "ws://localhost:7070/ws";
|
||||||
|
|
||||||
// ======= По умолчанию (можно поменять под свою среду) =======
|
// ======= По умолчанию (можно поменять под свою среду) =======
|
||||||
public static final String DEFAULT_LOGIN = "anya24";
|
public static final String DEFAULT_LOGIN = "Anya";
|
||||||
|
|
||||||
// Суффикс блокчейна по твоему правилу: login + 3 цифры
|
// Суффикс блокчейна по твоему правилу: login + 3 цифры
|
||||||
public static final String DEFAULT_BCH_SUFFIX_3 = "001";
|
public static final String DEFAULT_BCH_SUFFIX_3 = "001";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user