GPT: сгенерено и не проверено (смена ключей пользователя)

This commit is contained in:
AidarKC
2026-09-27 15:50:27 +03:00
parent 13693a0a53
commit 151a2c1754
94 changed files with 5327 additions and 815 deletions
+78
View File
@@ -123,3 +123,81 @@ export async function createAns104DataItem({ owner32, privateKey, data, tags = [
data,
);
}
function readUint64LE(bytes, offset) {
if (!(bytes instanceof Uint8Array) || offset < 0 || offset + 8 > bytes.length) throw new Error('ANS-104 truncated uint64');
let value = 0n;
for (let i = 7; i >= 0; i -= 1) value = (value << 8n) | BigInt(bytes[offset + i]);
if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('ANS-104 uint64 exceeds JS safe integer');
return Number(value);
}
/** Parse enough of an Ed25519 ANS-104 DataItem to re-sign its exact SHiNE payload. */
export function parseAns104DataItem(rawBytes) {
const raw = rawBytes instanceof Uint8Array ? rawBytes : Uint8Array.from(rawBytes || []);
let p = 0;
if (raw.length < 2 + 64 + 32 + 1 + 1 + 8 + 8) throw new Error('ANS-104 DataItem is too short');
const sigType = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint16(p, true); p += 2;
if (sigType !== ANS104_ED25519_SIGNATURE_TYPE) throw new Error(`Unsupported ANS-104 signature type: ${sigType}`);
const signature = raw.slice(p, p + 64); p += 64;
const owner32 = raw.slice(p, p + 32); p += 32;
const targetPresent = raw[p++] !== 0;
const target = targetPresent ? raw.slice(p, p + 32) : new Uint8Array(0);
if (targetPresent) p += 32;
const anchorPresent = raw[p++] !== 0;
const anchor = anchorPresent ? raw.slice(p, p + 32) : new Uint8Array(0);
if (anchorPresent) p += 32;
const tagsCount = readUint64LE(raw, p); p += 8;
const tagsBytesLength = readUint64LE(raw, p); p += 8;
if (p + tagsBytesLength > raw.length) throw new Error('ANS-104 tags are truncated');
const rawTags = raw.slice(p, p + tagsBytesLength); p += tagsBytesLength;
const data = raw.slice(p);
return { sigType, signature, owner32, target, anchor, tagsCount, rawTags, data, raw };
}
async function ans104SigningMessageRaw({ owner32, target = new Uint8Array(0), anchor = new Uint8Array(0), rawTags, data }) {
if (!(owner32 instanceof Uint8Array) || owner32.length !== 32) throw new Error('ANS-104 owner must be 32 bytes');
if (!(target instanceof Uint8Array) || (target.length !== 0 && target.length !== 32)) throw new Error('ANS-104 target must be empty or 32 bytes');
if (!(anchor instanceof Uint8Array) || (anchor.length !== 0 && anchor.length !== 32)) throw new Error('ANS-104 anchor must be empty or 32 bytes');
if (!(rawTags instanceof Uint8Array)) throw new Error('ANS-104 rawTags must be Uint8Array');
if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array');
return deepHash([
TE.encode('dataitem'),
TE.encode('1'),
TE.encode(String(ANS104_ED25519_SIGNATURE_TYPE)),
owner32,
target,
anchor,
rawTags,
data,
]);
}
/**
* Re-sign a DataItem while preserving exact raw tags/target/anchor/data bytes.
* Used by key rotation so SHA-256(SHiNE Frame) is unchanged across forks.
*/
export async function createAns104DataItemWithRawParts({
owner32,
privateKey,
data,
rawTags,
tagsCount,
target = new Uint8Array(0),
anchor = new Uint8Array(0),
}) {
const signingMessage = await ans104SigningMessageRaw({ owner32, target, anchor, rawTags, data });
const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', privateKey, signingMessage));
if (signature.length !== 64) throw new Error(`Unexpected Ed25519 signature length: ${signature.length}`);
return concatBytes(
uint16LE(ANS104_ED25519_SIGNATURE_TYPE),
signature,
owner32,
Uint8Array.of(target.length ? 1 : 0), target,
Uint8Array.of(anchor.length ? 1 : 0), anchor,
uint64LE(tagsCount),
uint64LE(rawTags.length),
rawTags,
data,
);
}
+90 -64
View File
@@ -553,31 +553,31 @@ function makeUserParamBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thi
);
}
function makeReactionLikeBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHex }) {
const cleanBch = String(toBlockchainName || '').trim();
if (!cleanBch) throw new Error('toBlockchainName is required for like');
function makeReactionLikeBodyBytes({ toLogin, 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 bchBytes = utf8Bytes(cleanBch);
if (bchBytes.length < 1 || bchBytes.length > 255) {
throw new Error('toBlockchainName must be 1..255 bytes');
const loginBytes = utf8Bytes(cleanLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
return concatBytes(
int8Byte(bchBytes.length),
bchBytes,
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex))
);
}
function makeTextReplyBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHex, text }) {
const cleanBch = String(toBlockchainName || '').trim();
if (!cleanBch) throw new Error('toBlockchainName is required for reply');
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) {
@@ -587,9 +587,9 @@ function makeTextReplyBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHe
const message = String(text || '').trim();
if (!message) throw new Error('Reply text is required');
const bchBytes = utf8Bytes(cleanBch);
if (bchBytes.length < 1 || bchBytes.length > 255) {
throw new Error('toBlockchainName must be 1..255 bytes');
const loginBytes = utf8Bytes(cleanLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
const textBytes = utf8Bytes(message);
@@ -598,8 +598,8 @@ function makeTextReplyBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHe
}
return concatBytes(
int8Byte(bchBytes.length),
bchBytes,
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
@@ -607,9 +607,9 @@ function makeTextReplyBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHe
);
}
function makeTextRatingBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHex, text }) {
const cleanBch = String(toBlockchainName || '').trim();
if (!cleanBch) throw new Error('toBlockchainName is required for rating');
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) {
@@ -619,9 +619,9 @@ function makeTextRatingBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashH
const message = String(text || '').trim();
if (!message) throw new Error('Rating text is required');
const bchBytes = utf8Bytes(cleanBch);
if (bchBytes.length < 1 || bchBytes.length > 255) {
throw new Error('toBlockchainName must be 1..255 bytes');
const loginBytes = utf8Bytes(cleanLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
const textBytes = utf8Bytes(message);
@@ -630,8 +630,8 @@ function makeTextRatingBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashH
}
return concatBytes(
int8Byte(bchBytes.length),
bchBytes,
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
@@ -639,18 +639,18 @@ function makeTextRatingBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashH
);
}
function makeStatusActionBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHex, text = '' }) {
const cleanBch = String(toBlockchainName || '').trim();
if (!cleanBch) throw new Error('toBlockchainName is required for status action');
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 bchBytes = utf8Bytes(cleanBch);
if (bchBytes.length < 1 || bchBytes.length > 255) {
throw new Error('toBlockchainName must be 1..255 bytes');
const loginBytes = utf8Bytes(cleanLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
const textBytes = utf8Bytes(String(text || '').trim());
@@ -659,8 +659,8 @@ function makeStatusActionBodyBytes({ toBlockchainName, toBlockNumber, toBlockHas
}
return concatBytes(
int8Byte(bchBytes.length),
bchBytes,
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
@@ -673,7 +673,7 @@ function makeTextRepostBodyBytes({
prevLineNumber,
prevLineHashHex,
thisLineNumber,
toBlockchainName,
toLogin,
toBlockNumber,
toBlockHashHex,
text,
@@ -681,11 +681,11 @@ function makeTextRepostBodyBytes({
const message = String(text || '').trim();
if (!message) throw new Error('Комментарий к репосту обязателен');
const bch = String(toBlockchainName || '').trim();
if (!bch) throw new Error('toBlockchainName is required for repost');
const bchBytes = utf8Bytes(bch);
if (bchBytes.length < 1 || bchBytes.length > 255) {
throw new Error('toBlockchainName must be 1..255 bytes');
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);
@@ -703,8 +703,8 @@ function makeTextRepostBodyBytes({
int32Bytes(prevLineNumber),
hexToBytes(normalizeHex32(prevLineHashHex)),
int32Bytes(thisLineNumber),
int8Byte(bchBytes.length),
bchBytes,
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
@@ -717,11 +717,16 @@ function makeTextEditPostBodyBytes({
prevLineNumber,
prevLineHashHex,
thisLineNumber,
toLogin,
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');
@@ -736,6 +741,8 @@ function makeTextEditPostBodyBytes({
int32Bytes(prevLineNumber),
hexToBytes(normalizeHex32(prevLineHashHex)),
int32Bytes(thisLineNumber),
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(targetBlockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
@@ -743,8 +750,12 @@ function makeTextEditPostBodyBytes({
);
}
function makeTextEditReplyBodyBytes({ toBlockNumber, toBlockHashHex, text }) {
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');
@@ -755,6 +766,8 @@ function makeTextEditReplyBodyBytes({ toBlockNumber, toBlockHashHex, text }) {
}
return concatBytes(
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(targetBlockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
@@ -767,21 +780,21 @@ function makeConnectionBodyBytes({
prevLineNumber = -1,
prevLineHashHex = ZERO64,
thisLineNumber = -1,
toBlockchainName,
toLogin,
toBlockNumber,
toBlockHashHex,
}) {
const cleanBch = String(toBlockchainName || '').trim();
if (!cleanBch) throw new Error('toBlockchainName is required for connection');
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 bchBytes = utf8Bytes(cleanBch);
if (bchBytes.length < 1 || bchBytes.length > 255) {
throw new Error('toBlockchainName must be 1..255 bytes');
const loginBytes = utf8Bytes(cleanTargetLogin);
if (loginBytes.length < 1 || loginBytes.length > 255) {
throw new Error('toLogin must be 1..255 bytes');
}
return concatBytes(
@@ -789,8 +802,8 @@ function makeConnectionBodyBytes({
int32Bytes(prevLineNumber),
hexToBytes(normalizeHex32(prevLineHashHex)),
int32Bytes(thisLineNumber),
int8Byte(bchBytes.length),
bchBytes,
int8Byte(loginBytes.length),
loginBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex))
);
@@ -938,13 +951,20 @@ function makeTextLineBodyBytesAllowEmpty({ lineCode, prevLineNumber, prevLineHas
);
}
function normalizeMessageRefTarget(target, actionName = 'action') {
const cleanBch = String(target?.blockchainName || '').trim();
const cleanBlockNumber = Number(target?.blockNumber);
const cleanBlockHash = String(target?.blockHash || '').trim().toLowerCase();
function loginFromBlockchainNameValue(value) {
const name = String(value || '').trim();
const match = name.match(/^(.*)-\d{3}$/u);
return match?.[1] ? match[1] : '';
}
if (!cleanBch) {
throw new Error(`Missing message target blockchain for ${actionName}`);
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();
const cleanBlockNumber = Number(target?.blockNumber ?? target?.messageRef?.blockNumber);
const cleanBlockHash = String(target?.blockHash ?? target?.messageRef?.blockHash ?? '').trim().toLowerCase();
if (!cleanLogin) {
throw new Error(`Missing message target login for ${actionName}`);
}
if (!Number.isFinite(cleanBlockNumber) || cleanBlockNumber < 0) {
throw new Error(`Invalid message target block number for ${actionName}`);
@@ -954,6 +974,7 @@ function normalizeMessageRefTarget(target, actionName = 'action') {
}
return {
login: cleanLogin,
blockchainName: cleanBch,
blockNumber: cleanBlockNumber,
blockHash: cleanBlockHash,
@@ -1892,7 +1913,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeReactionLikeBodyBytes({
toBlockchainName: target.blockchainName,
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
});
@@ -1915,7 +1936,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeReactionLikeBodyBytes({
toBlockchainName: target.blockchainName,
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
});
@@ -1939,7 +1960,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeTextReplyBodyBytes({
toBlockchainName: target.blockchainName,
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -1964,7 +1985,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeTextRatingBodyBytes({
toBlockchainName: target.blockchainName,
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2004,7 +2025,7 @@ export class AuthService {
return this.runWriteLocked(key, async () => {
const bodyBytes = makeStatusActionBodyBytes({
toBlockchainName: target.blockchainName,
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2077,7 +2098,7 @@ export class AuthService {
prevLineNumber,
prevLineHashHex,
thisLineNumber,
toBlockchainName: target.blockchainName,
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2159,6 +2180,7 @@ export class AuthService {
prevLineNumber,
prevLineHashHex,
thisLineNumber,
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2176,6 +2198,7 @@ export class AuthService {
}
const bodyBytes = makeTextEditReplyBodyBytes({
toLogin: target.login,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
@@ -2206,6 +2229,7 @@ export class AuthService {
return this.addBlockFollowChannel({
login: cleanLogin,
storagePwd,
targetLogin: cleanTargetLogin,
targetBlockchainName: targetUser.blockchainName,
targetBlockNumber: 0,
targetBlockHashHex: targetHeaderHash,
@@ -2217,6 +2241,7 @@ export class AuthService {
async addBlockFollowChannel({
login,
storagePwd,
targetLogin = '',
targetBlockchainName,
targetBlockNumber,
targetBlockHashHex,
@@ -2224,8 +2249,10 @@ export class AuthService {
}) {
const cleanLogin = String(login || '').trim();
const cleanTargetBch = String(targetBlockchainName || '').trim();
const cleanTargetLogin = String(targetLogin || loginFromBlockchainNameValue(cleanTargetBch)).trim();
const cleanTargetBlockNumber = Number(targetBlockNumber);
if (!cleanTargetBch) throw new Error('Target blockchain is required');
if (!cleanTargetLogin) throw new Error('Target login is required');
if (!Number.isFinite(cleanTargetBlockNumber) || cleanTargetBlockNumber < 0) {
throw new Error('Invalid target block number');
}
@@ -2245,7 +2272,7 @@ export class AuthService {
prevLineNumber: -1,
prevLineHashHex: ZERO64,
thisLineNumber: -1,
toBlockchainName: cleanTargetBch,
toLogin: cleanTargetLogin,
toBlockNumber: cleanTargetBlockNumber,
toBlockHashHex: targetHashHex,
});
@@ -3228,13 +3255,12 @@ export class AuthService {
const targetUser = await this.getUser(cleanToLogin);
if (!targetUser?.exists) throw new Error('Пользователь цели не найден.');
const toBlockchainName = String(targetUser?.blockchainName || `${cleanToLogin}-${BCH_SUFFIX}`).trim();
const bodyBytes = makeConnectionBodyBytes({
lineCode: 0,
prevLineNumber: -1,
prevLineHashHex: ZERO_HASH_HEX,
thisLineNumber: -1,
toBlockchainName,
toLogin: cleanToLogin,
toBlockNumber: 0,
toBlockHashHex: ZERO_HASH_HEX,
});
@@ -0,0 +1,168 @@
import { createAns104DataItem, createAns104DataItemWithRawParts, parseAns104DataItem } from './ans104-data-item.js';
import { base64ToBytes, bytesToBase64 } from './crypto-utils.js';
import { updateShineUserPdaOnSolana } from './shine-user-pda-service.js';
import { updateEncryptedUserSecrets } from './key-vault.js';
const TE = new TextEncoder();
const ZERO_HASH = '0'.repeat(64);
const TECH_FORK_SUBTYPE = 2;
function payload(resp) { return resp?.payload && typeof resp.payload === 'object' ? resp.payload : (resp || {}); }
function assertOk(resp, op) {
if (Number(resp?.status) >= 200 && Number(resp?.status) < 300) return payload(resp);
const p = payload(resp);
const message = p?.message || p?.error || p?.code || `${op} failed (${resp?.status ?? '?'})`;
const error = new Error(String(message));
error.code = p?.code || p?.errorCode || '';
error.response = resp;
throw error;
}
function concat(...chunks) {
const len = chunks.reduce((n, c) => n + c.length, 0);
const out = new Uint8Array(len); let p = 0;
for (const c of chunks) { out.set(c, p); p += c.length; }
return out;
}
function be16(v) { const a=new Uint8Array(2); new DataView(a.buffer).setUint16(0, Number(v), false); return a; }
function be32(v) { const a=new Uint8Array(4); new DataView(a.buffer).setUint32(0, Number(v), false); return a; }
function be64(v) { const a=new Uint8Array(8); new DataView(a.buffer).setBigInt64(0, BigInt(v), false); return a; }
function hexToBytes(hex) {
const s=String(hex||'').trim().toLowerCase(); if(!/^[0-9a-f]{64}$/.test(s)) throw new Error('Ожидался SHA-256 hash');
const out=new Uint8Array(32); for(let i=0;i<32;i++) out[i]=parseInt(s.slice(i*2,i*2+2),16); return out;
}
function bytesToHex(bytes) { return Array.from(bytes || [], (b)=>b.toString(16).padStart(2,'0')).join(''); }
function normalizeComment(value) { return String(value || '').trim().replace(/\r\n/g,'\n').replace(/\r/g,'\n'); }
function readFrameTimestampMs(frame) {
if (!(frame instanceof Uint8Array) || frame.length < 50) throw new Error('Некорректный SHiNE Frame');
const seconds = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getBigInt64(42, false);
return Number(seconds * 1000n);
}
function makeForkBody({ oldBlockchainKeyB64, forkPointBlock, forkPointHash, forkPointTimestampMs, parentTipBlock, parentTipHash, parentTipTimestampMs, reasonCode, comment }) {
const oldKey=base64ToBytes(oldBlockchainKeyB64); if(oldKey.length!==32) throw new Error('Некорректный старый blockchain key');
const c=TE.encode(normalizeComment(comment)); if(c.length>1024) throw new Error('Комментарий больше 1024 UTF-8 байт');
const discarded=Number(parentTipBlock)-Number(forkPointBlock);
if(discarded<0) throw new Error('Некорректная точка fork');
return concat(oldKey,be32(forkPointBlock),hexToBytes(forkPointHash),be64(forkPointTimestampMs),be32(parentTipBlock),hexToBytes(parentTipHash),be64(parentTipTimestampMs),be32(discarded),Uint8Array.of(Number(reasonCode)),be16(c.length),c);
}
function makeFrame({ prevHash, blockNumber, type, subType, version=1, body, timestampMs=Date.now() }) {
const bodyBytes=body || new Uint8Array(0);
const size=2+32+4+4+8+2+2+2+bodyBytes.length;
return concat(be16(1),hexToBytes(prevHash),be32(size),be32(blockNumber),be64(Math.floor(Number(timestampMs)/1000)),be16(type),be16(subType),be16(version),bodyBytes);
}
export const KEY_ROTATION_REASONS = Object.freeze([
{ code:1, label:'Обычная смена пароля / ключей' },
{ code:2, label:'Возможная компрометация ключей' },
{ code:3, label:'Подтверждённая компрометация — откат истории' },
{ code:4, label:'Восстановление доступа' },
]);
export class KeyRotationClient {
constructor(authService) { this.auth = authService; }
async status() { return assertOk(await this.auth.ws.request('KeyRotationStatus', {}), 'KeyRotationStatus'); }
async start(args) { return assertOk(await this.auth.ws.request('KeyRotationStart', args), 'KeyRotationStart'); }
async abort() { return assertOk(await this.auth.ws.request('KeyRotationAbort', {}), 'KeyRotationAbort'); }
async continuePlaceholder() { return assertOk(await this.auth.ws.request('KeyRotationContinue', {}), 'KeyRotationContinue'); }
async finishChain() { return assertOk(await this.auth.ws.request('KeyRotationFinishChain', {}), 'KeyRotationFinishChain'); }
async notifyPdaRotation(signature) { return assertOk(await this.auth.ws.request('KeyRotationRotatePda', { pdaRotationSignature: signature }), 'KeyRotationRotatePda'); }
async getMyBlockchain({ beforeBlock=null, limit=50, includeBlockBytes=false }={}) {
return assertOk(await this.auth.ws.request('GetMyBlockchain', { beforeBlock, limit, includeBlockBytes }), 'GetMyBlockchain');
}
async getBlock(blockchainName, blockNumber) {
return assertOk(await this.auth.ws.request('GetBlockchainBlock', { blockchainName, blockNumber }), 'GetBlockchainBlock');
}
async addCandidate({ blockNumber, prevBlockHash, blockBytesB64 }) {
return assertOk(await this.auth.ws.request('KeyRotationAddBlock', { blockNumber, prevBlockHash, blockBytesB64 }, 30000), 'KeyRotationAddBlock');
}
async copyCandidateChain({ rotation, newBundle, onProgress=()=>{} }) {
const cutoff=Number(rotation.forkFromBlock); const source=String(rotation.sourceBlockchainName||'');
if(!source || !Number.isInteger(cutoff) || cutoff<0) throw new Error('Некорректное состояние ротации');
if(String(newBundle?.blockchainPair?.publicKeyB64||'') !== String(rotation.newBlockchainKey||'')) throw new Error('Новый пароль выводит другой blockchain key');
const owner32=base64ToBytes(newBundle.blockchainPair.publicKeyB64);
const privateKey=newBundle.blockchainPair.privateKey;
let start=Math.max(0, Math.min(cutoff+1, Number(rotation.progressCurrent)||0));
// progressCurrent tracks published, while server may already have more pending blocks. Re-sends are idempotent.
for(let n=start;n<=cutoff;n++) {
const src=await this.getBlock(source,n);
const parsed=parseAns104DataItem(base64ToBytes(src.blockBytesB64));
const raw=await createAns104DataItemWithRawParts({ owner32, privateKey, data:parsed.data, rawTags:parsed.rawTags, tagsCount:parsed.tagsCount, target:parsed.target, anchor:parsed.anchor });
await this.addCandidate({ blockNumber:n, prevBlockHash:n===0?ZERO_HASH:nullIfEmpty(src.prevBlockHash)||await this.#sourcePrevHash(source,n), blockBytesB64:bytesToBase64(raw) });
onProgress({ phase:'copy', current:n+1, total:cutoff+2 });
}
const forkSource=await this.getBlock(source,cutoff);
const tipBlock=Number(rotation.sourceTipBlock);
const tipSource=tipBlock===cutoff ? forkSource : await this.getBlock(source,tipBlock);
const forkParsed=parseAns104DataItem(base64ToBytes(forkSource.blockBytesB64));
const tipParsed=parseAns104DataItem(base64ToBytes(tipSource.blockBytesB64));
const forkBody=makeForkBody({
oldBlockchainKeyB64:rotation.oldBlockchainKey,
forkPointBlock:cutoff,
forkPointHash:rotation.forkFromHash,
forkPointTimestampMs:readFrameTimestampMs(forkParsed.data),
parentTipBlock:tipBlock,
parentTipHash:rotation.sourceTipHash,
parentTipTimestampMs:readFrameTimestampMs(tipParsed.data),
reasonCode:rotation.reasonCode,
comment:rotation.comment,
});
const techFrame=makeFrame({ prevHash:rotation.forkFromHash, blockNumber:cutoff+1, type:0, subType:TECH_FORK_SUBTYPE, version:1, body:forkBody });
const techRaw=await createAns104DataItem({ owner32, privateKey, data:techFrame, tags:[{name:'App',value:'test5590'}] });
await this.addCandidate({ blockNumber:cutoff+1, prevBlockHash:rotation.forkFromHash, blockBytesB64:bytesToBase64(techRaw) });
onProgress({ phase:'copy', current:cutoff+2, total:cutoff+2 });
}
async #sourcePrevHash(source, blockNumber) {
if(blockNumber<=0) return ZERO_HASH;
const prev=await this.getBlock(source,blockNumber-1);
return String(prev.blockHash||'');
}
async waitUntilPublished({ timeoutMs=180000, onProgress=()=>{} }={}) {
const started=Date.now();
while(Date.now()-started<timeoutMs) {
const s=await this.status();
onProgress({ phase:'publish', current:Number(s.progressCurrent)||0, total:Number(s.progressTotal)||0, status:s.rotationStatus });
if(s.rotationStatus!=='COPYING_CHAIN') return s;
if(Number(s.progressTotal)>0 && Number(s.progressCurrent)>=Number(s.progressTotal)) return s;
await new Promise(r=>setTimeout(r,1500));
}
throw new Error('Публикация candidate-цепочки ещё не завершилась. Можно закрыть окно и продолжить позже.');
}
async rotatePda({ login, solanaEndpoint, oldBundle, newBundle, storagePwd }) {
const rotation=await this.status();
if(rotation.rotationStatus!=='CHAIN_READY' && rotation.rotationStatus!=='ROTATING_PDA') throw new Error('Новая цепочка ещё не готова к смене PDA');
const checks=[['root',oldBundle?.rootPair?.publicKeyB64,rotation.oldRootKey],['blockchain',oldBundle?.blockchainPair?.publicKeyB64,rotation.oldBlockchainKey],['client',oldBundle?.clientPair?.publicKeyB64,rotation.oldClientKey],['new root',newBundle?.rootPair?.publicKeyB64,rotation.newRootKey],['new blockchain',newBundle?.blockchainPair?.publicKeyB64,rotation.newBlockchainKey],['new client',newBundle?.clientPair?.publicKeyB64,rotation.newClientKey]];
for(const [name,actual,expected] of checks) if(String(actual||'')!==String(expected||'')) throw new Error(`Ключ «${name}» не соответствует начатой ротации`);
let signature=String(rotation.pdaRotationSignature||'');
if(!signature) {
const tx=await updateShineUserPdaOnSolana({
login, solanaEndpoint,
rootPrivatePkcs8B64:oldBundle.rootPair.privatePkcs8B64,
blockchainPrivatePkcs8B64:oldBundle.blockchainPair.privatePkcs8B64,
clientPrivatePkcs8B64:oldBundle.clientPair.privatePkcs8B64,
payerPrivatePkcs8B64:oldBundle.blockchainPair.privatePkcs8B64,
authorityMode:'root',
newRootPublicKey32:base64ToBytes(newBundle.rootPair.publicKeyB64),
nextClientPublicKey32:base64ToBytes(newBundle.clientPair.publicKeyB64),
newBlockchainPublicKey32:base64ToBytes(newBundle.blockchainPair.publicKeyB64),
newBlockchainPrivatePkcs8B64:newBundle.blockchainPair.privatePkcs8B64,
});
signature=String(tx.signature||'');
if(!signature) throw new Error('Solana не вернула signature ротации');
}
const state=await this.notifyPdaRotation(signature);
if(storagePwd) {
await updateEncryptedUserSecrets(login, storagePwd, (current)=>({
...(current||{}),
rootKey:newBundle.rootPair.privatePkcs8B64,
blockchainKey:newBundle.blockchainPair.privatePkcs8B64,
clientKey:newBundle.clientPair.privatePkcs8B64,
}));
}
return state;
}
}
function nullIfEmpty(value) { const s=String(value||'').trim(); return s || null; }
+33 -21
View File
@@ -557,7 +557,12 @@ async function createShineUserPdaOnSolana({
const clientKey32 = base64ToBytes(keyBundle.clientPair.publicKeyB64);
const rootPriv = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64);
const bchPriv = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64);
const ctx = await buildCommonContext({ login: cleanLogin, clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64, solanaEndpoint });
const ctx = await buildCommonContext({
login: cleanLogin,
clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64,
payerPrivatePkcs8B64: keyBundle.blockchainPair.privatePkcs8B64,
solanaEndpoint,
});
const ecoInfo = await ctx.connection.getAccountInfo(ctx.economyConfigPda, 'confirmed');
if (!ecoInfo?.data) throw new Error('Economy config не инициализирован.');
const startBonusLimit = parseUsersEconomyConfig(new Uint8Array(ecoInfo.data)).startBonusLimit;
@@ -603,7 +608,7 @@ async function createShineUserPdaOnSolana({
const createIx = new ctx.solana.TransactionInstruction({
programId: ctx.usersProgram,
keys: [
{ pubkey: ctx.clientKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: ctx.payerKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: ctx.userPda, isSigner: false, isWritable: true },
{ pubkey: ctx.solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: ctx.inflowVault, isSigner: false, isWritable: true },
@@ -618,7 +623,7 @@ async function createShineUserPdaOnSolana({
const tx = new ctx.solana.Transaction();
if (promoEdIx) tx.add(promoEdIx);
tx.add(rootIx, recordIx, createIx);
const signature = await ctx.solana.sendAndConfirmTransaction(ctx.connection, tx, [ctx.clientKeypair], { commitment: 'confirmed' });
const signature = await ctx.solana.sendAndConfirmTransaction(ctx.connection, tx, [ctx.payerKeypair], { commitment: 'confirmed' });
return { signature, userPda: ctx.userPda.toBase58(), pdaAddress: ctx.userPda.toBase58(), blockchainName: `${cleanLogin}-001` };
} catch (error) { throw await attachSolanaLogs(error, ctx.connection); }
}
@@ -678,15 +683,27 @@ export async function updateShineUserPdaOnSolana({
accessServers,
}) {
const current = await readShineUserPda({ login, solanaEndpoint });
const ctx = await buildCommonContext({ login: current.login, clientPrivatePkcs8B64, payerPrivatePkcs8B64, solanaEndpoint });
const ctx = await buildCommonContext({
login: current.login,
clientPrivatePkcs8B64,
payerPrivatePkcs8B64: String(payerPrivatePkcs8B64 || blockchainPrivatePkcs8B64 || ''),
solanaEndpoint,
});
const addLimit = BigInt(additionalLimitBytes || 0);
if (addLimit < 0n || addLimit % LIMIT_STEP !== 0n) throw new Error(`Лимит можно увеличивать только шагом ${LIMIT_STEP}`);
const authMode = authorityMode === 'blockchain' || authorityMode === AUTH_MODE_BLOCKCHAIN ? AUTH_MODE_BLOCKCHAIN : AUTH_MODE_ROOT;
const rootKey = newRootPublicKey32 ? toUint8Array(newRootPublicKey32, 32) : current.rootKey;
const clientKey = nextClientPublicKey32 ? toUint8Array(nextClientPublicKey32, 32) : current.clientKey;
const rootChanged = bytesToBase58(rootKey) !== bytesToBase58(current.rootKey);
if (authMode === AUTH_MODE_BLOCKCHAIN && rootChanged) throw new Error('Blockchain authority не может менять root key');
if (rootChanged && newBlockchainPublicKey32) throw new Error('Root rotation и blockchain fork нужно делать разными транзакциями');
const clientChanged = bytesToBase58(clientKey) !== bytesToBase58(current.clientKey);
const fullKeyRotation = rootChanged && clientChanged && Boolean(newBlockchainPublicKey32);
if ((rootChanged || clientChanged) && !fullKeyRotation) {
throw new Error('Root/client keys меняются только как полная ротация: новый root + новый client + новый blockchain fork');
}
const authMode = fullKeyRotation ? AUTH_MODE_ROOT : AUTH_MODE_BLOCKCHAIN;
if (authorityMode != null) {
const requestedMode = authorityMode === 'root' || authorityMode === AUTH_MODE_ROOT ? AUTH_MODE_ROOT : AUTH_MODE_BLOCKCHAIN;
if (requestedMode !== authMode) throw new Error(fullKeyRotation ? 'Полная ротация требует root authority' : 'Обычное обновление PDA выполняется blockchain authority');
}
const updatedAtMs = BigInt(Date.now());
const forks = current.forks.map((fork) => ({ blockchainKey: fork.blockchainKey, createdAtMs: BigInt(fork.createdAtMs), paidLimitBytes: BigInt(fork.paidLimitBytes) }));
@@ -695,7 +712,7 @@ export async function updateShineUserPdaOnSolana({
appendedKey = toUint8Array(newBlockchainPublicKey32, 32);
if (forks.some((f) => bytesToBase58(f.blockchainKey) === bytesToBase58(appendedKey))) throw new Error('Этот blockchain key уже был в истории fork');
const lastForkAt = BigInt(forks.at(-1).createdAtMs);
if (authMode === AUTH_MODE_BLOCKCHAIN && updatedAtMs - lastForkAt < FORK_COOLDOWN_MS) throw new Error('Новый fork обычным blockchain authority можно создавать не чаще одного раза в 72 часа');
if (!fullKeyRotation && updatedAtMs - lastForkAt < FORK_COOLDOWN_MS) throw new Error('Новый fork обычным blockchain authority можно создавать не чаще одного раза в 72 часа');
const newLimit = forks.at(-1).paidLimitBytes + addLimit;
if (newLimit > 0xffffffffn) throw new Error('paid_limit_bytes превышает u32');
forks.push({ blockchainKey: appendedKey, createdAtMs: updatedAtMs, paidLimitBytes: newLimit });
@@ -728,23 +745,17 @@ export async function updateShineUserPdaOnSolana({
const unsigned = serializeUnsignedRecordFromState(next);
const hash = await sha256Bytes(unsigned);
const oldAuthorityPub = authMode === AUTH_MODE_ROOT ? current.rootKey : current.forks.at(-1).blockchainKey;
const oldAuthorityPrivB64 = authMode === AUTH_MODE_ROOT ? rootPrivatePkcs8B64 : blockchainPrivatePkcs8B64;
if (!oldAuthorityPrivB64) throw new Error(authMode === AUTH_MODE_ROOT ? 'Нужен root private key' : 'Нужен blockchain private key');
const oldAuthorityPub = fullKeyRotation ? current.rootKey : current.forks.at(-1).blockchainKey;
const oldAuthorityPrivB64 = fullKeyRotation ? rootPrivatePkcs8B64 : blockchainPrivatePkcs8B64;
if (!oldAuthorityPrivB64) throw new Error(fullKeyRotation ? 'Для полной ротации нужен старый root private key' : 'Для обновления PDA нужен blockchain private key');
const oldAuthorityPriv = await importPkcs8Ed25519(oldAuthorityPrivB64);
const authSig = await signBytes(oldAuthorityPriv, hash);
let recordSignerPub;
let recordSignerPrivB64;
if (rootChanged) {
recordSignerPub = rootKey;
recordSignerPrivB64 = newRootPrivatePkcs8B64;
} else if (appendedKey) {
if (appendedKey) {
recordSignerPub = appendedKey;
recordSignerPrivB64 = newBlockchainPrivatePkcs8B64;
} else if (authMode === AUTH_MODE_ROOT) {
recordSignerPub = rootKey;
recordSignerPrivB64 = rootPrivatePkcs8B64;
} else {
recordSignerPub = current.forks.at(-1).blockchainKey;
recordSignerPrivB64 = blockchainPrivatePkcs8B64;
@@ -758,7 +769,7 @@ export async function updateShineUserPdaOnSolana({
const updateIx = new ctx.solana.TransactionInstruction({
programId: ctx.usersProgram,
keys: [
{ pubkey: ctx.clientKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: ctx.payerKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: ctx.userPda, isSigner: false, isWritable: true },
{ pubkey: ctx.solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: ctx.inflowVault, isSigner: false, isWritable: true },
@@ -768,7 +779,7 @@ export async function updateShineUserPdaOnSolana({
data: serializeUpdateUserPdaArgs({ login: current.login, rootKey32: rootKey, updatedAtMs, additionalLimitBytes: addLimit, clientKey32: clientKey, authMode, newBlockchainKey32: appendedKey, serverAddresses: addresses, accessServers: nextAccess, recordSignature64: recordSig }),
});
try {
const signature = await ctx.solana.sendAndConfirmTransaction(ctx.connection, new ctx.solana.Transaction().add(authIx, recordIx, updateIx), [ctx.clientKeypair], { commitment: 'confirmed' });
const signature = await ctx.solana.sendAndConfirmTransaction(ctx.connection, new ctx.solana.Transaction().add(authIx, recordIx, updateIx), [ctx.payerKeypair], { commitment: 'confirmed' });
return { signature, userPda: ctx.userPda.toBase58(), pdaAddress: ctx.userPda.toBase58(), paidLimitBytes: forks.at(-1).paidLimitBytes, forkCount: forks.length };
} catch (error) { throw await attachSolanaLogs(error, ctx.connection); }
}
@@ -778,8 +789,9 @@ export async function updateServerOnSolana({ login, keyBundle, serverAddress, se
login,
solanaEndpoint,
rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64,
blockchainPrivatePkcs8B64: keyBundle.blockchainPair.privatePkcs8B64,
clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64,
authorityMode: 'root',
authorityMode: 'blockchain',
serverAddresses: serverAddresses || [{ addressFormatType, addressFormatVersion, address: serverAddress }],
});
}
+30 -31
View File
@@ -180,6 +180,26 @@ export async function getWalletFromStoredClientKey({ login, storagePwd }) {
};
}
export async function getWalletFromStoredBlockchainKey({ login, storagePwd }) {
const cleanLogin = String(login || '').trim();
const cleanPwd = String(storagePwd || '').trim();
if (!cleanLogin || !cleanPwd) {
throw new Error('Нет активной сессии для доступа к blockchain.key');
}
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
const blockchainPrivate = String(secrets?.blockchainKey || '').trim();
if (!blockchainPrivate) {
throw new Error('На устройстве не найден blockchain.key');
}
const keypair = await keypairFromStoredSecret(blockchainPrivate);
return {
address: keypair.publicKey.toBase58(),
keypair,
blockchainPrivatePkcs8B64: blockchainPrivate,
};
}
export async function getWalletFromStoredRootKey({ login, storagePwd }) {
const cleanLogin = String(login || '').trim();
const cleanPwd = String(storagePwd || '').trim();
@@ -199,39 +219,18 @@ export async function getWalletFromStoredRootKey({ login, storagePwd }) {
};
}
export async function getStoredSolanaWalletChoices({ login, storagePwd, includeRoot = true } = {}) {
export async function getStoredSolanaWalletChoices({ login, storagePwd } = {}) {
const cleanLogin = String(login || '').trim();
const cleanPwd = String(storagePwd || '').trim();
if (!cleanLogin || !cleanPwd) {
throw new Error('Нет активной сессии для доступа к ключам');
}
const choices = [];
const clientWallet = await getWalletFromStoredClientKey({ login: cleanLogin, storagePwd: cleanPwd });
choices.push({
id: 'client-key',
keySource: 'client',
label: 'client key',
address: clientWallet.address,
keypair: clientWallet.keypair,
});
if (includeRoot) {
try {
const rootWallet = await getWalletFromStoredRootKey({ login: cleanLogin, storagePwd: cleanPwd });
choices.push({
id: 'root-key',
keySource: 'root',
label: 'root key',
address: rootWallet.address,
keypair: rootWallet.keypair,
});
} catch {
// root key на устройстве может отсутствовать, это допустимо
}
}
return choices;
if (!cleanLogin || !cleanPwd) throw new Error('Нет активной сессии для доступа к ключам');
const wallet = await getWalletFromStoredBlockchainKey({ login: cleanLogin, storagePwd: cleanPwd });
return [{
id: 'blockchain-key',
keySource: 'blockchain',
label: 'blockchain key',
address: wallet.address,
keypair: wallet.keypair,
}];
}
export async function encodeSolanaSecretKeyBase58(secretKey) {
+10 -12
View File
@@ -25,8 +25,8 @@ function parseSolToLamports(amountSol) {
return lamports.toString();
}
function normalizeKeySource(value) {
return String(value || '').trim().toLowerCase() === 'root' ? 'root' : 'client';
function normalizeKeySource() {
return 'blockchain';
}
async function loadTurboLib() {
@@ -36,14 +36,12 @@ async function loadTurboLib() {
return turboLibPromise;
}
async function getTurboWalletChoice({ login, storagePwd, keySource = 'client' } = {}) {
async function getTurboWalletChoice({ login, storagePwd, keySource = 'blockchain' } = {}) {
const normalizedKeySource = normalizeKeySource(keySource);
const choices = await getStoredSolanaWalletChoices({ login, storagePwd, includeRoot: true });
const choices = await getStoredSolanaWalletChoices({ login, storagePwd });
const selected = choices.find((item) => item.keySource === normalizedKeySource) || choices[0] || null;
if (!selected?.keypair) {
throw new Error(normalizedKeySource === 'root'
? 'На устройстве не найден root key для Turbo.'
: 'На устройстве не найден client key для Turbo.');
throw new Error('На устройстве не найден blockchain key для Turbo.');
}
return selected;
}
@@ -90,7 +88,7 @@ export function formatTurboCredits(value, digits = 6) {
});
}
export async function getTurboContextForStoredSolanaKey({ login, storagePwd, keySource = 'client' } = {}) {
export async function getTurboContextForStoredSolanaKey({ login, storagePwd, keySource = 'blockchain' } = {}) {
const choice = await getTurboWalletChoice({ login, storagePwd, keySource });
const turbo = await createTurboClientFromWalletChoice(choice);
return {
@@ -103,7 +101,7 @@ export async function getTurboContextForStoredSolanaKey({ login, storagePwd, key
};
}
export async function getTurboBalanceForStoredSolanaKey({ login, storagePwd, keySource = 'client' } = {}) {
export async function getTurboBalanceForStoredSolanaKey({ login, storagePwd, keySource = 'blockchain' } = {}) {
const ctx = await getTurboContextForStoredSolanaKey({ login, storagePwd, keySource });
const balance = await ctx.turbo.getBalance(ctx.address);
const solanaBalance = await getBalanceSol({
@@ -123,7 +121,7 @@ export async function getTurboBalanceForStoredSolanaKey({ login, storagePwd, key
};
}
export async function estimateTurboUploadPrice({ login, storagePwd, keySource = 'client', byteLength } = {}) {
export async function estimateTurboUploadPrice({ login, storagePwd, keySource = 'blockchain', byteLength } = {}) {
const bytes = Number(byteLength);
if (!Number.isInteger(bytes) || bytes <= 0) {
throw new Error('Некорректный размер файла для Turbo.');
@@ -147,7 +145,7 @@ export async function estimateTurboUploadPrice({ login, storagePwd, keySource =
export async function uploadFileWithTurbo({
login,
storagePwd,
keySource = 'client',
keySource = 'blockchain',
file,
tags = [],
shineType = 'attachment',
@@ -191,7 +189,7 @@ export async function uploadFileWithTurbo({
export async function topUpTurboWithSolana({
login,
storagePwd,
keySource = 'client',
keySource = 'blockchain',
amountSol,
turboCreditDestinationAddress = '',
} = {}) {