SHA256
Добавить линии связей и параметров пользователя
This commit is contained in:
@@ -37,7 +37,7 @@ import {
|
||||
saveEncryptedUserSecrets,
|
||||
saveSessionMaterial,
|
||||
} from './key-vault.js';
|
||||
import { createAns104DataItem } from './ans104-data-item.js';
|
||||
import { createAns104DataItem, parseAns104DataItem } from './ans104-data-item.js';
|
||||
import { defaultServerWs } from '../deploy-config.js';
|
||||
|
||||
const BCH_SUFFIX = '001';
|
||||
@@ -48,10 +48,12 @@ const MSG_TYPE_TECH = 0;
|
||||
const MSG_TYPE_TEXT = 1;
|
||||
const MSG_TYPE_REACTION = 2;
|
||||
const MSG_TYPE_CONNECTION = 3;
|
||||
const MSG_TYPE_USER_PROFILE = 4;
|
||||
const MSG_TYPE_STATUS_ACTION = 5;
|
||||
|
||||
const MSG_SUBTYPE_TECH_HEADER = 0;
|
||||
const MSG_SUBTYPE_TECH_CREATE_CHANNEL = 1;
|
||||
const MSG_SUBTYPE_TECH_FORK = 2;
|
||||
const MSG_SUBTYPE_TEXT_POST = 10;
|
||||
const MSG_SUBTYPE_TEXT_EDIT_POST = 11;
|
||||
const MSG_SUBTYPE_TEXT_REPLY = 20;
|
||||
@@ -553,18 +555,19 @@ function makeHeaderBodyBytes({ login, initialBlockchainKey32 }) {
|
||||
);
|
||||
}
|
||||
|
||||
function makeUserParamBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, key, value }) {
|
||||
function makeUserParamBodyBytes({ prevProfileBlockNumber, prevProfileBlockHashHex, profileSequence, key, value }) {
|
||||
const keyBytes = utf8Bytes(String(key || ''));
|
||||
const valueBytes = utf8Bytes(String(value || ''));
|
||||
const prevHashBytes = hexToBytes(prevLineHashHex);
|
||||
if (!keyBytes.length || !valueBytes.length) throw new Error('Пустые key/value для блока параметра');
|
||||
if (prevHashBytes.length !== 32) throw new Error('prevLineHash должен быть 32 байта');
|
||||
const prevHashBytes = hexToBytes(prevProfileBlockHashHex);
|
||||
if (!keyBytes.length || !valueBytes.length) throw new Error('Пустые key/value для блока профиля');
|
||||
if (!Number.isInteger(Number(prevProfileBlockNumber)) || Number(prevProfileBlockNumber) < 0) throw new Error('Некорректный previous profile block number');
|
||||
if (!Number.isInteger(Number(profileSequence)) || Number(profileSequence) < 1) throw new Error('Некорректный profile sequence');
|
||||
if (prevHashBytes.length !== 32) throw new Error('prevProfileBlockHash должен быть 32 байта');
|
||||
|
||||
return concatBytes(
|
||||
int32Bytes(lineCode),
|
||||
int32Bytes(prevLineNumber),
|
||||
int32Bytes(prevProfileBlockNumber),
|
||||
prevHashBytes,
|
||||
int32Bytes(thisLineNumber),
|
||||
int32Bytes(profileSequence),
|
||||
int16Bytes(keyBytes.length),
|
||||
keyBytes,
|
||||
int16Bytes(valueBytes.length),
|
||||
@@ -653,15 +656,17 @@ function makeTextEditReplyBodyBytes({ toBlockNumber, toBlockHashHex, text }) {
|
||||
}
|
||||
|
||||
function makeConnectionBodyBytes({
|
||||
lineCode = 0, prevLineNumber = -1, prevLineHashHex = ZERO64, thisLineNumber = -1,
|
||||
prevConnectionBlockNumber, prevConnectionBlockHashHex, connectionSequence,
|
||||
toLogin, toForkNumber, toBlockNumber, toBlockHashHex,
|
||||
}) {
|
||||
const loginBytes = targetLoginBytes(toLogin);
|
||||
const forkNumber = normalizeTargetForkNumber(toForkNumber, 'connection');
|
||||
const blockNumber = normalizeTargetBlockNumber(toBlockNumber, 'connection');
|
||||
const hashBytes = nonZeroHash32Bytes(toBlockHashHex, 'connection');
|
||||
if (!Number.isInteger(Number(prevConnectionBlockNumber)) || Number(prevConnectionBlockNumber) < 0) throw new Error('Некорректный previous connection block number');
|
||||
if (!Number.isInteger(Number(connectionSequence)) || Number(connectionSequence) < 1) throw new Error('Некорректный connection sequence');
|
||||
return concatBytes(
|
||||
int32Bytes(lineCode), int32Bytes(prevLineNumber), hexToBytes(normalizeHex32(prevLineHashHex)), int32Bytes(thisLineNumber),
|
||||
int32Bytes(prevConnectionBlockNumber), hexToBytes(normalizeHex32(prevConnectionBlockHashHex)), int32Bytes(connectionSequence),
|
||||
int8Byte(loginBytes.length), loginBytes, int16Bytes(forkNumber), int32Bytes(blockNumber), hashBytes,
|
||||
);
|
||||
}
|
||||
@@ -674,7 +679,7 @@ function targetLoginBytes(value) {
|
||||
|
||||
function normalizeTargetForkNumber(value, actionName = 'target') {
|
||||
const n = Number(value);
|
||||
if (!Number.isInteger(n) || n < 1 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
|
||||
if (!Number.isInteger(n) || n < 0 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -884,6 +889,51 @@ function resolveLatestLineStep(message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function bytesToHexLower(bytes) {
|
||||
return Array.from(bytes || [], (value) => Number(value).toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function parseStoredLineMeta(blockBytesB64, { fixedLine = false } = {}) {
|
||||
const raw = base64ToBytes(String(blockBytesB64 || ''));
|
||||
const frame = parseAns104DataItem(raw).data;
|
||||
const bodyOffset = 56;
|
||||
const minLength = fixedLine ? bodyOffset + 40 : bodyOffset + 44;
|
||||
if (!(frame instanceof Uint8Array) || frame.length < minLength) throw new Error('Некорректный line-блок');
|
||||
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
||||
if (fixedLine) {
|
||||
return {
|
||||
lineCode: 0,
|
||||
prevLineNumber: view.getInt32(bodyOffset, false),
|
||||
prevLineHashHex: bytesToHexLower(frame.slice(bodyOffset + 4, bodyOffset + 36)),
|
||||
lineSequence: view.getInt32(bodyOffset + 36, false),
|
||||
};
|
||||
}
|
||||
return {
|
||||
lineCode: view.getInt32(bodyOffset, false),
|
||||
prevLineNumber: view.getInt32(bodyOffset + 4, false),
|
||||
prevLineHashHex: bytesToHexLower(frame.slice(bodyOffset + 8, bodyOffset + 40)),
|
||||
lineSequence: view.getInt32(bodyOffset + 40, false),
|
||||
};
|
||||
}
|
||||
|
||||
function typeIndexTagValue(msgType) {
|
||||
switch (Number(msgType)) {
|
||||
case MSG_TYPE_TECH: return 'tech';
|
||||
case MSG_TYPE_CONNECTION: return 'conn';
|
||||
case MSG_TYPE_USER_PROFILE: return 'prof';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isLineConflictError(error) {
|
||||
return new Set([
|
||||
'BAD_PREV_LINE_NUMBER',
|
||||
'BAD_PREV_LINE_HASH',
|
||||
'BAD_LINE_SEQUENCE',
|
||||
'BAD_LINE_STATE',
|
||||
]).has(String(error?.code || '').toUpperCase());
|
||||
}
|
||||
|
||||
function buildBlockPreimage({ prevBlockHashHex, blockNumber, msgType, msgSubType, msgVersion = 1, bodyBytes }) {
|
||||
const prevHashBytes = hexToBytes(normalizeHex32(prevBlockHashHex));
|
||||
const body = bodyBytes || new Uint8Array(0);
|
||||
@@ -919,6 +969,8 @@ export class AuthService {
|
||||
this.eventListeners = new Map();
|
||||
this.wsEventUnsubscribers = new Map();
|
||||
this.headerHashCache = new Map();
|
||||
this.lineTailCache = new Map();
|
||||
this.lineWriteQueues = new Map();
|
||||
this.writeLocks = new Map();
|
||||
this.passwordKeyBundleCache = new Map();
|
||||
this.passwordKeyBundleInFlight = new Map();
|
||||
@@ -951,6 +1003,8 @@ export class AuthService {
|
||||
this.serverUrl = normalized;
|
||||
this.ws = new WsJsonClient(this.serverUrl);
|
||||
this.headerHashCache = new Map();
|
||||
this.lineTailCache = new Map();
|
||||
this.lineWriteQueues = new Map();
|
||||
this.writeLocks.clear();
|
||||
this.bindRegisteredEventsToCurrentWs();
|
||||
if (clearSessionContext) this.clearActiveSessionContext();
|
||||
@@ -1142,10 +1196,8 @@ export class AuthService {
|
||||
async createSessionFromImportedSecrets(login, secrets) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('В QR-коде нет логина');
|
||||
const clientKey = String(secrets?.clientKey || '').trim();
|
||||
const blockchainKey = String(secrets?.blockchainKey || '').trim();
|
||||
if (!clientKey) throw new Error('В QR-коде нет ключа доступа для входа');
|
||||
if (!blockchainKey) throw new Error('В QR-коде нет ключа блокчейна');
|
||||
const clientKey = String(secrets?.clientKey || secrets?.clientKey || '').trim();
|
||||
if (!clientKey) throw new Error('В QR-коде нет client key для входа');
|
||||
|
||||
const privateKey = await importPkcs8Ed25519(clientKey);
|
||||
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(clientKey);
|
||||
@@ -1221,7 +1273,7 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async persistSelectedKeys(login, storagePwd, keyBundle) {
|
||||
async persistSelectedKeys(login, storagePwd, keyBundle, saveOptions = { saveRoot: true, saveBlockchain: true }) {
|
||||
let currentSecrets = {};
|
||||
try {
|
||||
const loaded = await loadEncryptedUserSecrets(login, storagePwd);
|
||||
@@ -1232,23 +1284,12 @@ export class AuthService {
|
||||
// Если контейнера ещё нет или пароль новый для этого логина — создадим новый ниже.
|
||||
}
|
||||
|
||||
const clientKey = String(keyBundle?.clientPair?.privatePkcs8B64 || '').trim();
|
||||
const blockchainKey = String(keyBundle?.blockchainPair?.privatePkcs8B64 || '').trim();
|
||||
if (!clientKey || !blockchainKey) {
|
||||
throw new Error('Для входа нужно сохранить client key и blockchain key');
|
||||
}
|
||||
|
||||
const secrets = {
|
||||
...currentSecrets,
|
||||
clientKey,
|
||||
blockchainKey,
|
||||
clientKey: keyBundle.clientPair.privatePkcs8B64,
|
||||
};
|
||||
// SAWD-v1 wallet зависит от blockchain key. При смене ключа старый кэш
|
||||
// нельзя переносить в новую identity: он будет лениво выведен заново.
|
||||
if (String(currentSecrets?.blockchainKey || '').trim() !== blockchainKey) {
|
||||
delete secrets.arweaveWallet;
|
||||
}
|
||||
delete secrets.rootKey;
|
||||
if (saveOptions.saveRoot) secrets.rootKey = keyBundle.rootPair.privatePkcs8B64;
|
||||
if (saveOptions.saveBlockchain) secrets.blockchainKey = keyBundle.blockchainPair.privatePkcs8B64;
|
||||
await saveEncryptedUserSecrets(login, storagePwd, secrets);
|
||||
}
|
||||
|
||||
@@ -1773,6 +1814,8 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const ansTags = [{ name: 'App', value: 'test5590' }];
|
||||
const typeIndexTag = typeIndexTagValue(msgType);
|
||||
if (typeIndexTag) ansTags.push({ name: 't', value: typeIndexTag });
|
||||
const cleanChannelSlug = String(channelSlug || '').trim();
|
||||
if (cleanChannelSlug) ansTags.push({ name: 'c_test5590', value: cleanChannelSlug });
|
||||
|
||||
@@ -1990,53 +2033,17 @@ export class AuthService {
|
||||
const selector = channel || {};
|
||||
const owner = String(selector?.ownerBlockchainName || '').trim();
|
||||
const root = Number(selector?.channelRootBlockNumber);
|
||||
const key = `repost:${cleanLogin}:${owner}:${root}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}:${cleanText}`;
|
||||
const key = `channel-line:${cleanLogin}:${owner}:${root}`;
|
||||
|
||||
return this.runWriteLocked(key, async () => {
|
||||
return this.runLineWriteLocked(key, async () => {
|
||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||
if (!owner || !Number.isFinite(root) || root < 0) throw new Error('Invalid channel selector');
|
||||
if (owner !== blockchainName) throw new Error('Repost is allowed only to your own channels');
|
||||
|
||||
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
|
||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64 || !channelSlug) {
|
||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
|
||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === root);
|
||||
if (!rootChannel) throw new Error('Channel root not found');
|
||||
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
||||
}
|
||||
|
||||
let prevLineNumber = root;
|
||||
let prevLineHashHex = rootHashHex;
|
||||
let thisLineNumber = 0;
|
||||
try {
|
||||
const latestPayload = await this.getChannelMessages({
|
||||
ownerBlockchainName: owner,
|
||||
channelRootBlockNumber: root,
|
||||
channelRootBlockHash: rootHashHex,
|
||||
}, 1, 'desc', cleanLogin);
|
||||
const latestMessage = Array.isArray(latestPayload?.messages) ? latestPayload.messages[0] : null;
|
||||
const latestBlockNumber = Number(latestMessage?.messageRef?.blockNumber);
|
||||
const latestBlockHash = normalizeHex32(latestMessage?.messageRef?.blockHash, '');
|
||||
const latestLineStep = resolveLatestLineStep(latestMessage);
|
||||
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
||||
prevLineNumber = latestBlockNumber;
|
||||
prevLineHashHex = latestBlockHash;
|
||||
thisLineNumber = Number.isFinite(latestLineStep)
|
||||
? Math.max(0, latestLineStep + 1)
|
||||
: 1;
|
||||
}
|
||||
} catch {
|
||||
// fallback to root anchor
|
||||
}
|
||||
|
||||
const tail = await this.resolveChannelLineTail({ login: cleanLogin, blockchainName, channel: selector });
|
||||
const bodyBytes = makeTextRepostBodyBytes({
|
||||
lineCode: root,
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
lineCode: tail.lineCode,
|
||||
prevLineNumber: tail.prevLineNumber,
|
||||
prevLineHashHex: tail.prevLineHashHex,
|
||||
thisLineNumber: tail.thisLineNumber,
|
||||
toLogin: target.login,
|
||||
toForkNumber: target.forkNumber,
|
||||
toBlockNumber: target.blockNumber,
|
||||
@@ -2044,15 +2051,17 @@ export class AuthService {
|
||||
text: cleanText,
|
||||
});
|
||||
|
||||
return this.addBlockSigned({
|
||||
const payload = await this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: MSG_TYPE_TEXT,
|
||||
msgSubType: MSG_SUBTYPE_TEXT_REPOST,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
channelSlug,
|
||||
channelSlug: tail.channelSlug,
|
||||
});
|
||||
this.rememberChannelLineTail(blockchainName, tail.lineCode, payload, tail.thisLineNumber);
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2067,82 +2076,57 @@ export class AuthService {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanText = String(text || '').trim();
|
||||
const target = normalizeMessageRefTarget(message, 'edit');
|
||||
const lockKey = `edit:${cleanLogin}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}:${cleanText}:${isChannelPost ? 'post' : 'reply'}`;
|
||||
|
||||
return this.runWriteLocked(lockKey, async () => {
|
||||
if (isChannelPost) {
|
||||
const selector = channel || {};
|
||||
const ownerBlockchainName = String(selector?.ownerBlockchainName || target.blockchainName || '').trim();
|
||||
const lineCode = Number(selector?.channelRootBlockNumber);
|
||||
if (!ownerBlockchainName || !Number.isFinite(lineCode) || lineCode < 0) {
|
||||
throw new Error('Invalid channel selector for edit');
|
||||
}
|
||||
|
||||
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
|
||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64 || !channelSlug) {
|
||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, ownerBlockchainName);
|
||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
|
||||
if (rootChannel) {
|
||||
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
||||
}
|
||||
}
|
||||
|
||||
let prevLineNumber = lineCode;
|
||||
let prevLineHashHex = rootHashHex;
|
||||
let thisLineNumber = 1;
|
||||
try {
|
||||
const latestPayload = await this.getChannelMessages({
|
||||
if (isChannelPost) {
|
||||
const selector = channel || {};
|
||||
const ownerBlockchainName = String(selector?.ownerBlockchainName || target.blockchainName || '').trim();
|
||||
const lineCode = Number(selector?.channelRootBlockNumber);
|
||||
if (!ownerBlockchainName || !Number.isInteger(lineCode) || lineCode < 0) {
|
||||
throw new Error('Invalid channel selector for edit');
|
||||
}
|
||||
const lockKey = `channel-line:${cleanLogin}:${ownerBlockchainName}:${lineCode}`;
|
||||
return this.runLineWriteLocked(lockKey, async () => {
|
||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||
const tail = await this.resolveChannelLineTail({
|
||||
login: cleanLogin,
|
||||
blockchainName,
|
||||
channel: {
|
||||
...selector,
|
||||
ownerBlockchainName,
|
||||
channelRootBlockNumber: lineCode,
|
||||
channelRootBlockHash: rootHashHex,
|
||||
}, 1, 'desc', cleanLogin);
|
||||
const latestMessage = Array.isArray(latestPayload?.messages) ? latestPayload.messages[0] : null;
|
||||
const latestVersions = Array.isArray(latestMessage?.versions) ? latestMessage.versions : [];
|
||||
const latestVersion = latestVersions[latestVersions.length - 1] || null;
|
||||
const latestBlockNumber = Number(latestVersion?.blockNumber ?? latestMessage?.messageRef?.blockNumber);
|
||||
const latestBlockHash = normalizeHex32(latestVersion?.blockHash ?? latestMessage?.messageRef?.blockHash, '');
|
||||
const latestLineStep = resolveLatestLineStep(latestMessage);
|
||||
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
||||
prevLineNumber = latestBlockNumber;
|
||||
prevLineHashHex = latestBlockHash;
|
||||
thisLineNumber = Number.isFinite(latestLineStep)
|
||||
? Math.max(0, latestLineStep)
|
||||
: 1;
|
||||
}
|
||||
} catch {
|
||||
// fallback to root anchor
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
const bodyBytes = makeTextEditPostBodyBytes({
|
||||
lineCode,
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
toLogin: target.login,
|
||||
lineCode: tail.lineCode,
|
||||
prevLineNumber: tail.prevLineNumber,
|
||||
prevLineHashHex: tail.prevLineHashHex,
|
||||
thisLineNumber: tail.thisLineNumber,
|
||||
toBlockNumber: target.blockNumber,
|
||||
toBlockHashHex: target.blockHash,
|
||||
text: cleanText,
|
||||
});
|
||||
|
||||
return this.addBlockSigned({
|
||||
const payload = await this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: MSG_TYPE_TEXT,
|
||||
msgSubType: MSG_SUBTYPE_TEXT_EDIT_POST,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
channelSlug,
|
||||
channelSlug: tail.channelSlug,
|
||||
});
|
||||
}
|
||||
this.rememberChannelLineTail(blockchainName, tail.lineCode, payload, tail.thisLineNumber);
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
|
||||
const lockKey = `edit-reply:${cleanLogin}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}`;
|
||||
return this.runWriteLocked(lockKey, async () => {
|
||||
const bodyBytes = makeTextEditReplyBodyBytes({
|
||||
toBlockNumber: target.blockNumber,
|
||||
toBlockHashHex: target.blockHash,
|
||||
text: cleanText,
|
||||
});
|
||||
|
||||
return this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
@@ -2196,35 +2180,21 @@ export class AuthService {
|
||||
throw new Error('Invalid target block number');
|
||||
}
|
||||
const seedHash = normalizeHex32(targetBlockHashHex, ZERO64);
|
||||
const key = `${unfollow ? 'unfollow-channel' : 'follow-channel'}:${cleanLogin}:${cleanTargetBch}:${cleanTargetBlockNumber}:${seedHash}`;
|
||||
let targetHashHex = seedHash;
|
||||
if (targetHashHex === ZERO64) {
|
||||
targetHashHex = cleanTargetBlockNumber === 0
|
||||
? await this.resolveHeaderHashForBlockchain(cleanTargetBch)
|
||||
: await this.getBlockHashByNumber(cleanTargetBch, cleanTargetBlockNumber);
|
||||
}
|
||||
|
||||
return this.runWriteLocked(key, async () => {
|
||||
let targetHashHex = seedHash;
|
||||
if (targetHashHex === ZERO64) {
|
||||
targetHashHex = cleanTargetBlockNumber === 0
|
||||
? await this.resolveHeaderHashForBlockchain(cleanTargetBch)
|
||||
: await this.getBlockHashByNumber(cleanTargetBch, cleanTargetBlockNumber);
|
||||
}
|
||||
|
||||
const bodyBytes = makeConnectionBodyBytes({
|
||||
lineCode: 0,
|
||||
prevLineNumber: -1,
|
||||
prevLineHashHex: ZERO64,
|
||||
thisLineNumber: -1,
|
||||
toLogin: cleanTargetLogin,
|
||||
toForkNumber: forkNumberFromBlockchainNameValue(cleanTargetBch),
|
||||
toBlockNumber: cleanTargetBlockNumber,
|
||||
toBlockHashHex: targetHashHex,
|
||||
});
|
||||
|
||||
return this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: MSG_TYPE_CONNECTION,
|
||||
msgSubType: unfollow ? MSG_SUBTYPE_CONNECTION_UNFOLLOW : MSG_SUBTYPE_CONNECTION_FOLLOW,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
});
|
||||
return this.addBlockConnectionTarget({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
subType: unfollow ? MSG_SUBTYPE_CONNECTION_UNFOLLOW : MSG_SUBTYPE_CONNECTION_FOLLOW,
|
||||
toLogin: cleanTargetLogin,
|
||||
toForkNumber: forkNumberFromBlockchainNameValue(cleanTargetBch),
|
||||
toBlockNumber: cleanTargetBlockNumber,
|
||||
toBlockHashHex: targetHashHex,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2270,6 +2240,123 @@ export class AuthService {
|
||||
return headerHash;
|
||||
}
|
||||
|
||||
async getMyBlockchainPage({ beforeBlock = null, limit = 100, includeBlockBytes = true } = {}) {
|
||||
const response = await this.ws.request('GetMyBlockchain', { beforeBlock, limit, includeBlockBytes });
|
||||
if (response.status !== 200) throw opError('GetMyBlockchain', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async findLatestOwnLineBlock({
|
||||
blockchainName,
|
||||
msgType,
|
||||
subTypes = null,
|
||||
lineCode = null,
|
||||
fixedLine = false,
|
||||
stopAtBlock = 0,
|
||||
}) {
|
||||
const cleanBch = String(blockchainName || '').trim();
|
||||
if (!cleanBch) throw new Error('Missing blockchainName for line lookup');
|
||||
const subtypeSet = subTypes == null ? null : new Set(Array.from(subTypes, (value) => Number(value)));
|
||||
const minBlock = Math.max(0, Number(stopAtBlock) || 0);
|
||||
let beforeBlock = null;
|
||||
|
||||
for (;;) {
|
||||
const page = await this.getMyBlockchainPage({ beforeBlock, limit: 100, includeBlockBytes: true });
|
||||
const pageBch = String(page?.blockchainName || '').trim();
|
||||
if (pageBch && pageBch !== cleanBch) throw new Error('GetMyBlockchain returned another blockchain');
|
||||
const blocks = Array.isArray(page?.blocks) ? page.blocks : [];
|
||||
for (const block of blocks) {
|
||||
const blockNumber = Number(block?.blockNumber);
|
||||
if (!Number.isInteger(blockNumber) || blockNumber <= minBlock) continue;
|
||||
if (Number(block?.msgType) !== Number(msgType)) continue;
|
||||
if (subtypeSet && !subtypeSet.has(Number(block?.msgSubType))) continue;
|
||||
if (!block?.blockBytesB64) continue;
|
||||
const meta = parseStoredLineMeta(block.blockBytesB64, { fixedLine });
|
||||
if (lineCode != null && Number(meta.lineCode) !== Number(lineCode)) continue;
|
||||
return {
|
||||
blockNumber,
|
||||
blockHash: normalizeHex32(block?.blockHash, ZERO64),
|
||||
lineSequence: Number(meta.lineSequence),
|
||||
lineCode: Number(meta.lineCode),
|
||||
};
|
||||
}
|
||||
const next = page?.nextBeforeBlock;
|
||||
if (next == null || !Number.isInteger(Number(next)) || Number(next) <= minBlock) return null;
|
||||
beforeBlock = Number(next);
|
||||
}
|
||||
}
|
||||
|
||||
async resolveFixedLineTail({ blockchainName, kind }) {
|
||||
const cleanBch = String(blockchainName || '').trim();
|
||||
const kindKey = String(kind || '').trim().toLowerCase();
|
||||
const config = kindKey === 'profile'
|
||||
? { msgType: MSG_TYPE_USER_PROFILE, tag: 'prof' }
|
||||
: kindKey === 'connections'
|
||||
? { msgType: MSG_TYPE_CONNECTION, tag: 'conn' }
|
||||
: kindKey === 'tech'
|
||||
? { msgType: MSG_TYPE_TECH, tag: 'tech', subTypes: [MSG_SUBTYPE_TECH_CREATE_CHANNEL, MSG_SUBTYPE_TECH_FORK] }
|
||||
: null;
|
||||
if (!config) throw new Error(`Unknown fixed line kind: ${kind}`);
|
||||
|
||||
const cacheKey = `${cleanBch}|${kindKey}`;
|
||||
const cached = this.lineTailCache.get(cacheKey);
|
||||
if (cached) return { ...cached };
|
||||
|
||||
const latest = await this.findLatestOwnLineBlock({
|
||||
blockchainName: cleanBch,
|
||||
msgType: config.msgType,
|
||||
subTypes: config.subTypes || null,
|
||||
lineCode: kindKey === 'tech' ? 0 : null,
|
||||
fixedLine: kindKey !== 'tech',
|
||||
stopAtBlock: 0,
|
||||
});
|
||||
if (latest) {
|
||||
const result = {
|
||||
prevLineNumber: latest.blockNumber,
|
||||
prevLineHashHex: latest.blockHash,
|
||||
thisLineNumber: latest.lineSequence + 1,
|
||||
};
|
||||
this.lineTailCache.set(cacheKey, result);
|
||||
return { ...result };
|
||||
}
|
||||
|
||||
const headerHash = await this.resolveHeaderHashForBlockchain(cleanBch);
|
||||
if (headerHash === ZERO64) throw new Error('HEADER блокчейна не найден');
|
||||
const result = { prevLineNumber: 0, prevLineHashHex: headerHash, thisLineNumber: 1 };
|
||||
this.lineTailCache.set(cacheKey, result);
|
||||
return { ...result };
|
||||
}
|
||||
|
||||
rememberLineTail(blockchainName, kind, payload, currentSequence) {
|
||||
const cleanBch = String(blockchainName || '').trim();
|
||||
const acceptedNumber = Number(payload?.serverLastGlobalNumber);
|
||||
const acceptedHash = normalizeHex32(payload?.serverLastGlobalHash, ZERO64);
|
||||
if (!cleanBch || !Number.isInteger(acceptedNumber) || acceptedNumber < 0 || acceptedHash === ZERO64) return;
|
||||
this.lineTailCache.set(`${cleanBch}|${String(kind || '').trim().toLowerCase()}`, {
|
||||
prevLineNumber: acceptedNumber,
|
||||
prevLineHashHex: acceptedHash,
|
||||
thisLineNumber: Number(currentSequence) + 1,
|
||||
});
|
||||
}
|
||||
|
||||
async runLineWriteLocked(lockKey, buildAndSend) {
|
||||
const key = String(lockKey || '').trim() || 'line-write';
|
||||
const previous = this.lineWriteQueues.get(key) || Promise.resolve();
|
||||
const task = previous.catch(() => {}).then(async () => {
|
||||
try {
|
||||
return await buildAndSend();
|
||||
} catch (error) {
|
||||
if (!isLineConflictError(error)) throw error;
|
||||
this.lineTailCache.clear();
|
||||
return buildAndSend();
|
||||
}
|
||||
});
|
||||
this.lineWriteQueues.set(key, task);
|
||||
return task.finally(() => {
|
||||
if (this.lineWriteQueues.get(key) === task) this.lineWriteQueues.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
async listOwnChannelsForBlockchain(login, blockchainName) {
|
||||
const feed = await this.listSubscriptionsFeed(login, 500);
|
||||
const own = feed?.ownedChannels || [];
|
||||
@@ -2314,34 +2401,11 @@ export class AuthService {
|
||||
: normalizeChannelDescription(channelDescription);
|
||||
const channelSlug = toCanonicalChannelSlug(cleanChannelName);
|
||||
const typeVersion = Number(channelTypeVersion);
|
||||
const key = `create-channel:${cleanLogin}:${typeCode}:${channelSlug || cleanChannelName.toLowerCase()}`;
|
||||
return this.runWriteLocked(key, async () => {
|
||||
const key = `tech-line:${cleanLogin}`;
|
||||
return this.runLineWriteLocked(key, async () => {
|
||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||
const userLastGlobalNumber = Number(user?.serverLastGlobalNumber);
|
||||
const userLastGlobalHash = normalizeHex32(user?.serverLastGlobalHash, ZERO64);
|
||||
|
||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
|
||||
const createdChannels = ownChannels
|
||||
.filter((item) => item.rootBlockNumber > 0)
|
||||
.sort((a, b) => a.rootBlockNumber - b.rootBlockNumber);
|
||||
|
||||
let prevLineNumber = 0;
|
||||
let prevLineHashHex = (
|
||||
Number.isFinite(userLastGlobalNumber) &&
|
||||
userLastGlobalNumber === 0 &&
|
||||
userLastGlobalHash !== ZERO64
|
||||
)
|
||||
? userLastGlobalHash
|
||||
: await this.resolveHeaderHashForBlockchain(blockchainName);
|
||||
let thisLineNumber = 1;
|
||||
|
||||
if (createdChannels.length > 0) {
|
||||
const last = createdChannels[createdChannels.length - 1];
|
||||
prevLineNumber = last.rootBlockNumber;
|
||||
prevLineHashHex = normalizeHex32(last.rootBlockHash, ZERO64);
|
||||
thisLineNumber = createdChannels.length + 1;
|
||||
}
|
||||
const tail = await this.resolveFixedLineTail({ blockchainName, kind: 'tech' });
|
||||
|
||||
const payload = await this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
@@ -2352,15 +2416,16 @@ export class AuthService {
|
||||
channelSlug,
|
||||
bodyBytes: makeCreateChannelBodyBytes({
|
||||
lineCode: 0,
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
prevLineNumber: tail.prevLineNumber,
|
||||
prevLineHashHex: tail.prevLineHashHex,
|
||||
thisLineNumber: tail.thisLineNumber,
|
||||
channelName: cleanChannelName,
|
||||
channelDescription: cleanChannelDescription,
|
||||
channelType: typeCode,
|
||||
channelTypeVersion: typeVersion,
|
||||
}),
|
||||
});
|
||||
this.rememberLineTail(blockchainName, 'tech', payload, tail.thisLineNumber);
|
||||
|
||||
const selector = {
|
||||
ownerBlockchainName: blockchainName,
|
||||
@@ -2381,7 +2446,7 @@ export class AuthService {
|
||||
const selector = channel || {};
|
||||
const ownerBlockchainName = String(selector?.ownerBlockchainName || '').trim();
|
||||
const lineCode = Number(selector?.channelRootBlockNumber);
|
||||
if (!ownerBlockchainName || !Number.isFinite(lineCode) || lineCode < 0) {
|
||||
if (!ownerBlockchainName || !Number.isInteger(lineCode) || lineCode < 0) {
|
||||
throw new Error('Invalid channel selector');
|
||||
}
|
||||
if (ownerBlockchainName !== blockchainName) {
|
||||
@@ -2398,52 +2463,70 @@ export class AuthService {
|
||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
||||
}
|
||||
if (!channelSlug) throw new Error('Cannot resolve canonical channel slug');
|
||||
if (rootHashHex === ZERO64) throw new Error('Cannot resolve channel root hash');
|
||||
|
||||
let prevLineNumber = lineCode;
|
||||
let prevLineHashHex = rootHashHex;
|
||||
let thisLineNumber = 0;
|
||||
try {
|
||||
const latestPayload = await this.getChannelMessages({
|
||||
const cacheKey = `${blockchainName}|channel:${lineCode}`;
|
||||
const cached = this.lineTailCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return {
|
||||
ownerBlockchainName,
|
||||
channelRootBlockNumber: lineCode,
|
||||
channelRootBlockHash: rootHashHex,
|
||||
}, 1, 'desc', login);
|
||||
const latestMessage = Array.isArray(latestPayload?.messages) ? latestPayload.messages[0] : null;
|
||||
const latestMeta = (Array.isArray(latestPayload?.metaEvents) ? latestPayload.metaEvents : [])
|
||||
.slice()
|
||||
.sort((a, b) => Number(b?.messageRef?.blockNumber || -1) - Number(a?.messageRef?.blockNumber || -1))[0] || null;
|
||||
const latestMessageBlock = Number(latestMessage?.messageRef?.blockNumber);
|
||||
const latestMetaBlock = Number(latestMeta?.messageRef?.blockNumber);
|
||||
const latest = Number.isFinite(latestMetaBlock) && (!Number.isFinite(latestMessageBlock) || latestMetaBlock > latestMessageBlock)
|
||||
? latestMeta
|
||||
: latestMessage;
|
||||
const latestBlockNumber = Number(latest?.messageRef?.blockNumber);
|
||||
const latestBlockHash = normalizeHex32(latest?.messageRef?.blockHash, '');
|
||||
const latestLineStep = resolveLatestLineStep(latest);
|
||||
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
||||
prevLineNumber = latestBlockNumber;
|
||||
prevLineHashHex = latestBlockHash;
|
||||
thisLineNumber = latest?.kind === 'created'
|
||||
? 0
|
||||
: Number.isFinite(latestLineStep)
|
||||
? Math.max(0, latestLineStep + 1)
|
||||
: (latestBlockNumber === lineCode ? 0 : 1);
|
||||
}
|
||||
} catch {
|
||||
// fallback to root anchor
|
||||
lineCode,
|
||||
rootHashHex,
|
||||
channelSlug,
|
||||
...cached,
|
||||
};
|
||||
}
|
||||
|
||||
const latest = await this.findLatestOwnLineBlock({
|
||||
blockchainName,
|
||||
msgType: MSG_TYPE_TEXT,
|
||||
subTypes: [
|
||||
MSG_SUBTYPE_TEXT_POST,
|
||||
MSG_SUBTYPE_TEXT_EDIT_POST,
|
||||
MSG_SUBTYPE_TEXT_REPOST,
|
||||
MSG_SUBTYPE_TEXT_CHANNEL_META,
|
||||
MSG_SUBTYPE_TEXT_ENTRYPOINT,
|
||||
MSG_SUBTYPE_TEXT_EXERCISE,
|
||||
MSG_SUBTYPE_TEXT_SERVICE,
|
||||
MSG_SUBTYPE_TEXT_COURSE,
|
||||
],
|
||||
lineCode,
|
||||
fixedLine: false,
|
||||
stopAtBlock: lineCode,
|
||||
});
|
||||
|
||||
const lineTail = latest
|
||||
? {
|
||||
prevLineNumber: latest.blockNumber,
|
||||
prevLineHashHex: latest.blockHash,
|
||||
thisLineNumber: latest.lineSequence + 1,
|
||||
}
|
||||
: {
|
||||
prevLineNumber: lineCode,
|
||||
prevLineHashHex: rootHashHex,
|
||||
thisLineNumber: 0,
|
||||
};
|
||||
this.lineTailCache.set(cacheKey, lineTail);
|
||||
return {
|
||||
ownerBlockchainName,
|
||||
lineCode,
|
||||
rootHashHex,
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
channelSlug,
|
||||
...lineTail,
|
||||
};
|
||||
}
|
||||
|
||||
rememberChannelLineTail(blockchainName, lineCode, payload, currentSequence) {
|
||||
const acceptedNumber = Number(payload?.serverLastGlobalNumber);
|
||||
const acceptedHash = normalizeHex32(payload?.serverLastGlobalHash, ZERO64);
|
||||
if (!Number.isInteger(acceptedNumber) || acceptedNumber < 0 || acceptedHash === ZERO64) return;
|
||||
this.lineTailCache.set(`${String(blockchainName || '').trim()}|channel:${Number(lineCode)}`, {
|
||||
prevLineNumber: acceptedNumber,
|
||||
prevLineHashHex: acceptedHash,
|
||||
thisLineNumber: Number(currentSequence) + 1,
|
||||
});
|
||||
}
|
||||
|
||||
async addBlockTextPost({ login, channel, text, storagePwd, msgSubType = MSG_SUBTYPE_TEXT_POST }) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Missing login');
|
||||
@@ -2462,9 +2545,9 @@ export class AuthService {
|
||||
const selector = channel || {};
|
||||
const owner = String(selector?.ownerBlockchainName || '').trim();
|
||||
const root = Number(selector?.channelRootBlockNumber);
|
||||
const key = `text-post:${cleanLogin}:${owner}:${root}:${cleanSubType}:${cleanText}`;
|
||||
const key = `channel-line:${cleanLogin}:${owner}:${root}`;
|
||||
|
||||
return this.runWriteLocked(key, async () => {
|
||||
return this.runLineWriteLocked(key, async () => {
|
||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||
|
||||
@@ -2487,6 +2570,7 @@ export class AuthService {
|
||||
bodyBytes,
|
||||
channelSlug: tail.channelSlug,
|
||||
});
|
||||
this.rememberChannelLineTail(blockchainName, tail.lineCode, payload, tail.thisLineNumber);
|
||||
|
||||
return {
|
||||
...payload,
|
||||
@@ -2506,9 +2590,9 @@ export class AuthService {
|
||||
const owner = String(selector?.ownerBlockchainName || '').trim();
|
||||
const root = Number(selector?.channelRootBlockNumber);
|
||||
const metaText = composeChannelMetaText({ title, description, avatar });
|
||||
const key = `channel-meta:${cleanLogin}:${owner}:${root}:${metaText}`;
|
||||
const key = `channel-line:${cleanLogin}:${owner}:${root}`;
|
||||
|
||||
return this.runWriteLocked(key, async () => {
|
||||
return this.runLineWriteLocked(key, async () => {
|
||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||
const tail = await this.resolveChannelLineTail({ login: cleanLogin, blockchainName, channel: selector });
|
||||
@@ -2529,6 +2613,7 @@ export class AuthService {
|
||||
bodyBytes,
|
||||
channelSlug: tail.channelSlug,
|
||||
});
|
||||
this.rememberChannelLineTail(blockchainName, tail.lineCode, payload, tail.thisLineNumber);
|
||||
|
||||
return {
|
||||
...payload,
|
||||
@@ -3161,21 +3246,65 @@ export class AuthService {
|
||||
if (!cleanLogin || !cleanParam) throw new Error('Не переданы login/param.');
|
||||
if (!cleanValue) throw new Error('Значение параметра не может быть пустым.');
|
||||
if (!storagePwd) throw new Error('Не передан storagePwd для подписи AddBlock.');
|
||||
const bodyBytes = makeUserParamBodyBytes({
|
||||
lineCode: 0,
|
||||
prevLineNumber: -1,
|
||||
prevLineHashHex: ZERO_HASH_HEX,
|
||||
thisLineNumber: -1,
|
||||
key: cleanParam,
|
||||
value: cleanValue,
|
||||
|
||||
return this.runLineWriteLocked(`profile-line:${cleanLogin}`, async () => {
|
||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||
const tail = await this.resolveFixedLineTail({ blockchainName, kind: 'profile' });
|
||||
const bodyBytes = makeUserParamBodyBytes({
|
||||
prevProfileBlockNumber: tail.prevLineNumber,
|
||||
prevProfileBlockHashHex: tail.prevLineHashHex,
|
||||
profileSequence: tail.thisLineNumber,
|
||||
key: cleanParam,
|
||||
value: cleanValue,
|
||||
});
|
||||
const payload = await this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: MSG_TYPE_USER_PROFILE,
|
||||
msgSubType: 1,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
});
|
||||
this.rememberLineTail(blockchainName, 'profile', payload, tail.thisLineNumber);
|
||||
return payload;
|
||||
});
|
||||
return this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: 4,
|
||||
msgSubType: 1,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
}
|
||||
|
||||
async addBlockConnectionTarget({
|
||||
login,
|
||||
storagePwd,
|
||||
subType,
|
||||
toLogin,
|
||||
toForkNumber,
|
||||
toBlockNumber,
|
||||
toBlockHashHex,
|
||||
}) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Missing login for CONNECTION');
|
||||
return this.runLineWriteLocked(`connections-line:${cleanLogin}`, async () => {
|
||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||
const tail = await this.resolveFixedLineTail({ blockchainName, kind: 'connections' });
|
||||
const bodyBytes = makeConnectionBodyBytes({
|
||||
prevConnectionBlockNumber: tail.prevLineNumber,
|
||||
prevConnectionBlockHashHex: tail.prevLineHashHex,
|
||||
connectionSequence: tail.thisLineNumber,
|
||||
toLogin,
|
||||
toForkNumber,
|
||||
toBlockNumber,
|
||||
toBlockHashHex,
|
||||
});
|
||||
const payload = await this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: MSG_TYPE_CONNECTION,
|
||||
msgSubType: Number(subType),
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
});
|
||||
this.rememberLineTail(blockchainName, 'connections', payload, tail.thisLineNumber);
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3190,9 +3319,6 @@ export class AuthService {
|
||||
throw new Error('Нельзя создать связь на самого себя.');
|
||||
}
|
||||
|
||||
const user = await this.getUser(cleanLogin);
|
||||
if (user?.exists === false) throw new Error('Текущий пользователь не найден.');
|
||||
|
||||
const targetUser = await this.getUser(cleanToLogin);
|
||||
if (!targetUser?.exists) throw new Error('Пользователь цели не найден.');
|
||||
const targetBlockchainName = String(targetUser.blockchainName || '').trim();
|
||||
@@ -3200,24 +3326,15 @@ export class AuthService {
|
||||
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,
|
||||
return this.addBlockConnectionTarget({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
subType: cleanSubType,
|
||||
toLogin: cleanToLogin,
|
||||
toForkNumber: targetForkNumber,
|
||||
toBlockNumber: 0,
|
||||
toBlockHashHex: targetHeaderHash,
|
||||
});
|
||||
return this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: 3,
|
||||
msgSubType: cleanSubType,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user