Очень сильно переделать формат блоков

Внимание: версия ещё не проверена.
This commit is contained in:
AidarKC
2026-10-01 09:34:19 +03:00
parent 08c7fd7676
commit 3281da7f5c
65 changed files with 1863 additions and 2743 deletions
+172 -239
View File
@@ -50,6 +50,7 @@ const MSG_TYPE_REACTION = 2;
const MSG_TYPE_CONNECTION = 3;
const MSG_TYPE_STATUS_ACTION = 5;
const MSG_SUBTYPE_TECH_HEADER = 0;
const MSG_SUBTYPE_TECH_CREATE_CHANNEL = 1;
const MSG_SUBTYPE_TEXT_POST = 10;
const MSG_SUBTYPE_TEXT_EDIT_POST = 11;
@@ -534,6 +535,24 @@ function extractContactsFromDialogs(dialogs) {
return out;
}
function makeHeaderBodyBytes({ login, initialBlockchainKey32 }) {
const cleanLogin = String(login || '').trim();
const loginBytes = utf8Bytes(cleanLogin);
const keyBytes = initialBlockchainKey32 instanceof Uint8Array
? initialBlockchainKey32
: new Uint8Array(initialBlockchainKey32 || []);
if (loginBytes.length < 1 || loginBytes.length > 255) throw new Error('HEADER login must be 1..255 UTF-8 bytes');
if (keyBytes.length !== 32 || keyBytes.every((value) => value === 0)) {
throw new Error('HEADER initialBlockchainKey32 must be a non-zero 32-byte key');
}
return concatBytes(
utf8Bytes('SHiNE'),
int8Byte(loginBytes.length),
loginBytes,
keyBytes,
);
}
function makeUserParamBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, key, value }) {
const keyBytes = utf8Bytes(String(key || ''));
const valueBytes = utf8Bytes(String(value || ''));
@@ -553,262 +572,124 @@ function makeUserParamBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thi
);
}
function makeReactionLikeBodyBytes({ toLogin, toBlockNumber, toBlockHashHex }) {
function makeReactionLikeBodyBytes({ toLogin, toForkNumber, toBlockNumber, toBlockHashHex }) {
const cleanLogin = String(toLogin || '').trim();
if (!cleanLogin) throw new Error('toLogin is required for like');
const blockNumber = Number(toBlockNumber);
if (!Number.isFinite(blockNumber) || blockNumber < 0) {
throw new Error('Invalid toBlockNumber for like');
}
const loginBytes = utf8Bytes(cleanLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
return concatBytes(
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex))
);
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'like');
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'like');
const loginBytes = targetLoginBytes(cleanLogin);
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'like');
return concatBytes(int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes);
}
function makeTextReplyBodyBytes({ toLogin, toBlockNumber, toBlockHashHex, text }) {
const cleanLogin = String(toLogin || '').trim();
if (!cleanLogin) throw new Error('toLogin is required for reply');
const blockNumber = Number(toBlockNumber);
if (!Number.isFinite(blockNumber) || blockNumber < 0) {
throw new Error('Invalid toBlockNumber for reply');
}
const message = String(text || '').trim();
if (!message) throw new Error('Reply text is required');
const loginBytes = utf8Bytes(cleanLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
const textBytes = utf8Bytes(message);
if (textBytes.length < 1 || textBytes.length > 65535) {
throw new Error('Reply text must be 1..65535 UTF-8 bytes');
}
return concatBytes(
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
textBytes
);
}
function makeTextRatingBodyBytes({ toLogin, toBlockNumber, toBlockHashHex, text }) {
const cleanLogin = String(toLogin || '').trim();
if (!cleanLogin) throw new Error('toLogin is required for rating');
const blockNumber = Number(toBlockNumber);
if (!Number.isFinite(blockNumber) || blockNumber < 0) {
throw new Error('Invalid toBlockNumber for rating');
}
const message = String(text || '').trim();
if (!message) throw new Error('Rating text is required');
const loginBytes = utf8Bytes(cleanLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
const textBytes = utf8Bytes(message);
if (textBytes.length < 1 || textBytes.length > 65535) {
throw new Error('Rating text must be 1..65535 UTF-8 bytes');
}
return concatBytes(
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
textBytes
);
}
function makeStatusActionBodyBytes({ toLogin, toBlockNumber, toBlockHashHex, text = '' }) {
const cleanLogin = String(toLogin || '').trim();
if (!cleanLogin) throw new Error('toLogin is required for status action');
const blockNumber = Number(toBlockNumber);
if (!Number.isFinite(blockNumber) || blockNumber < 0) {
throw new Error('Invalid toBlockNumber for status action');
}
const loginBytes = utf8Bytes(cleanLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
function makeTextReplyBodyBytes({ toLogin, toForkNumber, toBlockNumber, toBlockHashHex, text }) {
const loginBytes = targetLoginBytes(toLogin);
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'reply');
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'reply');
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'reply');
const textBytes = utf8Bytes(String(text || '').trim());
if (textBytes.length > 65535) {
throw new Error('Status action text must be 0..65535 UTF-8 bytes');
}
if (textBytes.length < 1 || textBytes.length > 65535) throw new Error('Reply text must be 1..65535 UTF-8 bytes');
return concatBytes(int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes, int16Bytes(textBytes.length), textBytes);
}
return concatBytes(
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
textBytes
);
function makeTextRatingBodyBytes({ toLogin, toForkNumber, toBlockNumber, toBlockHashHex, text }) {
const loginBytes = targetLoginBytes(toLogin);
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'rating');
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'rating');
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'rating');
const textBytes = utf8Bytes(String(text || '').trim());
if (textBytes.length < 1 || textBytes.length > 65535) throw new Error('Rating text must be 1..65535 UTF-8 bytes');
return concatBytes(int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes, int16Bytes(textBytes.length), textBytes);
}
function makeStatusActionBodyBytes({ toLogin, toForkNumber, toBlockNumber, toBlockHashHex, text = '' }) {
const loginBytes = targetLoginBytes(toLogin);
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'status action');
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'status action');
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'status action');
const textBytes = utf8Bytes(String(text || '').trim());
if (textBytes.length > 65535) throw new Error('Status action text must be 0..65535 UTF-8 bytes');
return concatBytes(int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes, int16Bytes(textBytes.length), textBytes);
}
function makeTextRepostBodyBytes({
lineCode,
prevLineNumber,
prevLineHashHex,
thisLineNumber,
toLogin,
toBlockNumber,
toBlockHashHex,
text,
lineCode, prevLineNumber, prevLineHashHex, thisLineNumber,
toLogin, toForkNumber, toBlockNumber, toBlockHashHex, text,
}) {
const message = String(text || '').trim();
if (!message) throw new Error('Комментарий к репосту обязателен');
const cleanTargetLogin = String(toLogin || '').trim();
if (!cleanTargetLogin) throw new Error('toLogin is required for repost');
const loginBytes = utf8Bytes(cleanTargetLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
const blockNumber = Number(toBlockNumber);
if (!Number.isFinite(blockNumber) || blockNumber < 0) {
throw new Error('Invalid toBlockNumber for repost');
}
const loginBytes = targetLoginBytes(toLogin);
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'repost');
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'repost');
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'repost');
const textBytes = utf8Bytes(message);
if (textBytes.length < 1 || textBytes.length > 65535) {
throw new Error('Repost comment must be 1..65535 UTF-8 bytes');
}
if (textBytes.length > 65535) throw new Error('Repost comment too long');
return concatBytes(
int32Bytes(lineCode),
int32Bytes(prevLineNumber),
hexToBytes(normalizeHex32(prevLineHashHex)),
int32Bytes(thisLineNumber),
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
textBytes,
int32Bytes(lineCode), int32Bytes(prevLineNumber), hexToBytes(normalizeHex32(prevLineHashHex)), int32Bytes(thisLineNumber),
int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes,
int16Bytes(textBytes.length), textBytes,
);
}
function makeTextEditPostBodyBytes({
lineCode,
prevLineNumber,
prevLineHashHex,
thisLineNumber,
toLogin,
toBlockNumber,
toBlockHashHex,
text,
lineCode, prevLineNumber, prevLineHashHex, thisLineNumber,
toBlockNumber, toBlockHashHex, text,
}) {
const message = String(text || '').trim();
const cleanTargetLogin = String(toLogin || '').trim();
if (!cleanTargetLogin) throw new Error('toLogin is required for edit post');
const loginBytes = utf8Bytes(cleanTargetLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) throw new Error('toLogin must be 1..255 bytes');
const targetBlockNumber = Number(toBlockNumber);
if (!Number.isFinite(targetBlockNumber) || targetBlockNumber < 0) {
throw new Error('Invalid target block number for edit post');
}
const textBytes = utf8Bytes(message);
if (textBytes.length > 65535) {
throw new Error('Message text must be 0..65535 UTF-8 bytes');
}
const targetBlockNumber = normalizeTargetBlockNumber(toBlockNumber, 'edit post');
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'edit post');
const textBytes = utf8Bytes(String(text || '').trim());
if (textBytes.length > 65535) throw new Error('Message text must be 0..65535 UTF-8 bytes');
return concatBytes(
int32Bytes(lineCode),
int32Bytes(prevLineNumber),
hexToBytes(normalizeHex32(prevLineHashHex)),
int32Bytes(thisLineNumber),
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(targetBlockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
textBytes,
int32Bytes(lineCode), int32Bytes(prevLineNumber), hexToBytes(normalizeHex32(prevLineHashHex)), int32Bytes(thisLineNumber),
int32Bytes(targetBlockNumber), hashBytes, int16Bytes(textBytes.length), textBytes,
);
}
function makeTextEditReplyBodyBytes({ toLogin, toBlockNumber, toBlockHashHex, text }) {
const message = String(text || '').trim();
const cleanTargetLogin = String(toLogin || '').trim();
if (!cleanTargetLogin) throw new Error('toLogin is required for edit reply');
const loginBytes = utf8Bytes(cleanTargetLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) throw new Error('toLogin must be 1..255 bytes');
const targetBlockNumber = Number(toBlockNumber);
if (!Number.isFinite(targetBlockNumber) || targetBlockNumber < 0) {
throw new Error('Invalid target block number for edit reply');
}
const textBytes = utf8Bytes(message);
if (textBytes.length > 65535) {
throw new Error('Message text must be 0..65535 UTF-8 bytes');
}
return concatBytes(
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(targetBlockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
textBytes,
);
function makeTextEditReplyBodyBytes({ toBlockNumber, toBlockHashHex, text }) {
const targetBlockNumber = normalizeTargetBlockNumber(toBlockNumber, 'edit reply');
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'edit reply');
const textBytes = utf8Bytes(String(text || '').trim());
if (textBytes.length > 65535) throw new Error('Message text must be 0..65535 UTF-8 bytes');
return concatBytes(int32Bytes(targetBlockNumber), hashBytes, int16Bytes(textBytes.length), textBytes);
}
function makeConnectionBodyBytes({
lineCode = 0,
prevLineNumber = -1,
prevLineHashHex = ZERO64,
thisLineNumber = -1,
toLogin,
toBlockNumber,
toBlockHashHex,
lineCode = 0, prevLineNumber = -1, prevLineHashHex = ZERO64, thisLineNumber = -1,
toLogin, toForkNumber, toBlockNumber, toBlockHashHex,
}) {
const cleanTargetLogin = String(toLogin || '').trim();
if (!cleanTargetLogin) throw new Error('toLogin is required for connection');
const blockNumber = Number(toBlockNumber);
if (!Number.isFinite(blockNumber) || blockNumber < 0) {
throw new Error('Invalid toBlockNumber for connection');
}
const loginBytes = utf8Bytes(cleanTargetLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
const loginBytes = targetLoginBytes(toLogin);
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'connection');
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'connection');
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'connection');
return concatBytes(
int32Bytes(lineCode),
int32Bytes(prevLineNumber),
hexToBytes(normalizeHex32(prevLineHashHex)),
int32Bytes(thisLineNumber),
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex))
int32Bytes(lineCode), int32Bytes(prevLineNumber), hexToBytes(normalizeHex32(prevLineHashHex)), int32Bytes(thisLineNumber),
int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes,
);
}
function targetLoginBytes(value) {
const bytes = utf8Bytes(String(value || '').trim());
if (bytes.length < 1 || bytes.length > 255) throw new Error('toLogin must be 1..255 UTF-8 bytes');
return bytes;
}
function normalizeTargetForkNumber(value, actionName = 'target') {
const n = Number(value);
if (!Number.isInteger(n) || n < 0 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
return n;
}
function normalizeTargetBlockNumber(value, actionName = 'target') {
const n = Number(value);
if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid toBlockNumber for ${actionName}`);
return n;
}
function nonZeroHash32Bytes(value, actionName = 'target') {
const hex = normalizeHex32(value, ZERO64);
if (hex === ZERO64) throw new Error(`Zero target hash is forbidden for ${actionName}`);
return hexToBytes(hex);
}
function normalizeChannelDescription(value) {
const text = String(value == null ? '' : value).trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const bytes = utf8Bytes(text);
@@ -953,10 +834,18 @@ function makeTextLineBodyBytesAllowEmpty({ lineCode, prevLineNumber, prevLineHas
function loginFromBlockchainNameValue(value) {
const name = String(value || '').trim();
const match = name.match(/^(.*)-\d{3}$/u);
const match = name.match(/^(.*)-(\d{3})$/u);
return match?.[1] ? match[1] : '';
}
function forkNumberFromBlockchainNameValue(value) {
const name = String(value || '').trim();
const match = name.match(/^(.*)-(\d{3})$/u);
if (!match) return 0;
const forkNumber = Number(match[2]);
return Number.isInteger(forkNumber) && forkNumber >= 1 && forkNumber <= 999 ? forkNumber : 0;
}
function normalizeMessageRefTarget(target, actionName = 'action') {
const cleanBch = String(target?.blockchainName || target?.authorBlockchainName || '').trim();
const cleanLogin = String(target?.login || target?.authorLogin || target?.ownerLogin || loginFromBlockchainNameValue(cleanBch)).trim();
@@ -973,9 +862,15 @@ function normalizeMessageRefTarget(target, actionName = 'action') {
throw new Error(`Invalid message target hash for ${actionName}`);
}
const forkNumber = Number(target?.forkNumber ?? target?.toForkNumber ?? forkNumberFromBlockchainNameValue(cleanBch));
if (!Number.isInteger(forkNumber) || forkNumber < 1 || forkNumber > 999) {
throw new Error(`Missing/invalid message target fork for ${actionName}`);
}
return {
login: cleanLogin,
blockchainName: cleanBch,
forkNumber,
blockNumber: cleanBlockNumber,
blockHash: cleanBlockHash,
};
@@ -1839,11 +1734,16 @@ export class AuthService {
};
}
async addBlockSigned({ login, storagePwd, msgType, msgSubType, msgVersion = 1, bodyBytes, channelSlug = '' }) {
async addBlockSigned({ login, storagePwd, msgType, msgSubType, msgVersion = 1, bodyBytes, channelSlug = '', _skipHeaderEnsure = false }) {
const cleanLogin = (login || '').trim();
if (!cleanLogin) throw new Error('Missing login for AddBlock');
if (!storagePwd) throw new Error('Missing storagePwd for AddBlock signing');
const isHeader = Number(msgType) === MSG_TYPE_TECH && Number(msgSubType) === MSG_SUBTYPE_TECH_HEADER;
if (!_skipHeaderEnsure && !isHeader) {
await this.ensureBlockchainHeader(cleanLogin, storagePwd);
}
const keyBundle = await loadEncryptedUserSecrets(cleanLogin, storagePwd);
const blockchainPrivatePkcs8 = String(keyBundle?.blockchainKey || '').trim();
if (!blockchainPrivatePkcs8) {
@@ -1888,22 +1788,43 @@ export class AuthService {
return payload;
}
async ensureChainInitializedForLineOps(login, storagePwd) {
const current = await this.getUser(login);
async ensureBlockchainHeader(login, storagePwd) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) throw new Error('Missing login for HEADER bootstrap');
const current = await this.getUser(cleanLogin);
const lastNum = Number(current?.serverLastGlobalNumber);
if (Number.isFinite(lastNum) && lastNum >= 0) return current;
if (!(Number.isFinite(lastNum) && lastNum === -1)) return current;
// Bootstrap an empty chain with a minimal USER_PARAM block so line-based
// channel operations have a valid anchor at block #0.
await this.addBlockUserParam({
login,
const keyBundle = await loadEncryptedUserSecrets(cleanLogin, storagePwd);
const privatePkcs8 = String(keyBundle?.blockchainKey || '').trim();
let publicKeyB64 = '';
if (privatePkcs8) {
publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(privatePkcs8);
} else {
publicKeyB64 = String(current?.blockchainKey || '').trim();
}
const initialBlockchainKey32 = base64ToBytes(publicKeyB64);
if (initialBlockchainKey32.length !== 32 || initialBlockchainKey32.every((value) => value === 0)) {
throw new Error('Не удалось определить initial blockchain key для HEADER');
}
await this.addBlockSigned({
login: cleanLogin,
storagePwd,
param: 'shine',
value: 'yes',
msgType: MSG_TYPE_TECH,
msgSubType: MSG_SUBTYPE_TECH_HEADER,
msgVersion: 1,
bodyBytes: makeHeaderBodyBytes({ login: cleanLogin, initialBlockchainKey32 }),
_skipHeaderEnsure: true,
});
return this.getUser(login);
return this.getUser(cleanLogin);
}
async ensureChainInitializedForLineOps(login, storagePwd) {
return this.ensureBlockchainHeader(login, storagePwd);
}
async addBlockLike({ login, message, storagePwd }) {
@@ -1914,6 +1835,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeReactionLikeBodyBytes({
toLogin: target.login,
toForkNumber: target.forkNumber,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
});
@@ -1937,6 +1859,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeReactionLikeBodyBytes({
toLogin: target.login,
toForkNumber: target.forkNumber,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
});
@@ -1961,6 +1884,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeTextReplyBodyBytes({
toLogin: target.login,
toForkNumber: target.forkNumber,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -1986,6 +1910,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeTextRatingBodyBytes({
toLogin: target.login,
toForkNumber: target.forkNumber,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2026,6 +1951,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeStatusActionBodyBytes({
toLogin: target.login,
toForkNumber: target.forkNumber,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2099,6 +2025,7 @@ export class AuthService {
prevLineHashHex,
thisLineNumber,
toLogin: target.login,
toForkNumber: target.forkNumber,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2198,7 +2125,6 @@ export class AuthService {
}
const bodyBytes = makeTextEditReplyBodyBytes({
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2273,6 +2199,7 @@ export class AuthService {
prevLineHashHex: ZERO64,
thisLineNumber: -1,
toLogin: cleanTargetLogin,
toForkNumber: forkNumberFromBlockchainNameValue(cleanTargetBch),
toBlockNumber: cleanTargetBlockNumber,
toBlockHashHex: targetHashHex,
});
@@ -3255,14 +3182,20 @@ export class AuthService {
const targetUser = await this.getUser(cleanToLogin);
if (!targetUser?.exists) throw new Error('Пользователь цели не найден.');
const targetBlockchainName = String(targetUser.blockchainName || '').trim();
const targetForkNumber = forkNumberFromBlockchainNameValue(targetBlockchainName);
if (!targetForkNumber) throw new Error('Не удалось определить fork пользователя цели.');
const targetHeaderHash = await this.resolveHeaderHashForBlockchain(targetBlockchainName);
if (targetHeaderHash === ZERO64) throw new Error('HEADER пользователя цели не найден.');
const bodyBytes = makeConnectionBodyBytes({
lineCode: 0,
prevLineNumber: -1,
prevLineHashHex: ZERO_HASH_HEX,
thisLineNumber: -1,
toLogin: cleanToLogin,
toForkNumber: targetForkNumber,
toBlockNumber: 0,
toBlockHashHex: ZERO_HASH_HEX,
toBlockHashHex: targetHeaderHash,
});
return this.addBlockSigned({
login: cleanLogin,