490 lines
19 KiB
JavaScript
490 lines
19 KiB
JavaScript
import { importPkcs8Ed25519, sha256Bytes, signBytes } from './crypto-utils.js';
|
||
import { extractSeed32FromPkcs8B64 } from './device-key-utils.js';
|
||
import { SHINE_PAYMENTS_PROGRAM_ID, SHINE_USERS_ECONOMY_CONFIG_SEED, SHINE_USERS_PROGRAM_ID } from '../solana-programs.js';
|
||
|
||
const MAGIC = 'SHiNE';
|
||
const LAST_BLOCK_STATE_PREFIX = 'SHiNE_LAST_BLOCK';
|
||
const SHINE_PAYMENTS_INFLOW_VAULT_SEED = 'shine_payments_inflow_vault';
|
||
const LIMIT_STEP = 10_000n;
|
||
const UPDATE_USER_PDA_DISCRIMINATOR = new Uint8Array([42, 133, 114, 232, 38, 245, 167, 234]);
|
||
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
|
||
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
|
||
|
||
const BLOCK_TYPE_ROOT_KEY = 1;
|
||
const BLOCK_TYPE_DEVICE_KEY = 2;
|
||
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
|
||
const BLOCK_TYPE_SERVER_PROFILE = 30;
|
||
const BLOCK_TYPE_ACCESS_SERVERS = 40;
|
||
const BLOCK_TYPE_TRUSTED_STATE = 50;
|
||
|
||
let solanaLibPromise = null;
|
||
function loadSolanaLib() {
|
||
if (!solanaLibPromise) solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
|
||
return solanaLibPromise;
|
||
}
|
||
|
||
function pushU32LE(buf, v) {
|
||
const n = Number(v) >>> 0;
|
||
buf.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff);
|
||
}
|
||
function pushU64LE(buf, v) {
|
||
const b = BigInt(v);
|
||
const lo = Number(b & 0xffffffffn) >>> 0;
|
||
const hi = Number((b >> 32n) & 0xffffffffn) >>> 0;
|
||
pushU32LE(buf, lo);
|
||
pushU32LE(buf, hi);
|
||
}
|
||
function pushStrU8(buf, value) {
|
||
const bytes = new TextEncoder().encode(String(value || ''));
|
||
if (bytes.length > 255) throw new Error('Слишком длинная строка для формата U8');
|
||
buf.push(bytes.length);
|
||
for (const x of bytes) buf.push(x);
|
||
}
|
||
function pushStrU32(buf, value) {
|
||
const bytes = new TextEncoder().encode(String(value || ''));
|
||
pushU32LE(buf, bytes.length);
|
||
for (const x of bytes) buf.push(x);
|
||
}
|
||
function pushVecU8(buf, bytes) {
|
||
const data = bytes || new Uint8Array();
|
||
pushU32LE(buf, data.length);
|
||
for (const x of data) buf.push(x);
|
||
}
|
||
function pushVecStrU32(buf, values) {
|
||
const arr = Array.isArray(values) ? values : [];
|
||
pushU32LE(buf, arr.length);
|
||
for (const s of arr) pushStrU32(buf, s);
|
||
}
|
||
|
||
function makeReader(bytes) {
|
||
let o = 0;
|
||
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||
const ensure = (n) => { if (o + n > bytes.length) throw new Error('Повреждённый формат PDA'); };
|
||
const readU8 = () => { ensure(1); const v = dv.getUint8(o); o += 1; return v; };
|
||
const readU16 = () => { ensure(2); const v = dv.getUint16(o, true); o += 2; return v; };
|
||
const readU32 = () => { ensure(4); const v = dv.getUint32(o, true); o += 4; return v; };
|
||
const readU64 = () => { ensure(8); const v = dv.getBigUint64(o, true); o += 8; return v; };
|
||
const readBytes = (n) => { ensure(n); const out = bytes.slice(o, o + n); o += n; return out; };
|
||
const readStrU8 = () => {
|
||
const len = readU8();
|
||
return new TextDecoder().decode(readBytes(len));
|
||
};
|
||
return { readU8, readU16, readU32, readU64, readBytes, readStrU8 };
|
||
}
|
||
|
||
function parseShineUserPda(dataBytes) {
|
||
const r = makeReader(dataBytes);
|
||
const magic = new TextDecoder().decode(r.readBytes(5));
|
||
if (magic !== MAGIC) throw new Error('Некорректный формат PDA');
|
||
r.readU8();
|
||
r.readU8();
|
||
r.readU16();
|
||
const createdAtMs = r.readU64();
|
||
const updatedAtMs = r.readU64();
|
||
const recordNumber = r.readU32();
|
||
const prevRecordHash = r.readBytes(32);
|
||
const login = r.readStrU8();
|
||
const blocksCount = r.readU8();
|
||
|
||
const out = {
|
||
createdAtMs,
|
||
updatedAtMs,
|
||
recordNumber,
|
||
prevRecordHash,
|
||
login,
|
||
rootKey: null,
|
||
deviceKey: null,
|
||
blockchain: null,
|
||
isServer: false,
|
||
serverKey: new Uint8Array(32),
|
||
serverAddress: '',
|
||
syncServers: [],
|
||
accessServers: [],
|
||
trustedCount: 0,
|
||
};
|
||
|
||
for (let i = 0; i < blocksCount; i += 1) {
|
||
const type = r.readU8();
|
||
r.readU8();
|
||
if (type === BLOCK_TYPE_ROOT_KEY) { out.rootKey = r.readBytes(32); continue; }
|
||
if (type === BLOCK_TYPE_DEVICE_KEY) { out.deviceKey = r.readBytes(32); continue; }
|
||
if (type === BLOCK_TYPE_BLOCKCHAIN_REGISTRY) {
|
||
const count = r.readU8();
|
||
for (let j = 0; j < count; j += 1) {
|
||
const blockchainType = r.readU8();
|
||
const blockchainName = r.readStrU8();
|
||
const blockchainPublicKey = r.readBytes(32);
|
||
const paidLimitBytes = r.readU64();
|
||
const usedBytes = r.readU64();
|
||
const lastBlockNumber = r.readU32();
|
||
const lastBlockHash = r.readBytes(32);
|
||
const lastBlockSignature = r.readBytes(64);
|
||
const arPresent = r.readU8();
|
||
const arweaveTxId = arPresent ? r.readStrU8() : '';
|
||
if (!out.blockchain) {
|
||
out.blockchain = {
|
||
blockchainType,
|
||
blockchainName,
|
||
blockchainPublicKey,
|
||
paidLimitBytes,
|
||
usedBytes,
|
||
lastBlockNumber,
|
||
lastBlockHash,
|
||
lastBlockSignature,
|
||
arweaveTxId,
|
||
};
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
if (type === BLOCK_TYPE_SERVER_PROFILE) {
|
||
out.isServer = r.readU8() === 1;
|
||
out.serverKey = r.readBytes(32);
|
||
out.serverAddress = r.readStrU8();
|
||
const syncCount = r.readU8();
|
||
out.syncServers = [];
|
||
for (let k = 0; k < syncCount; k += 1) out.syncServers.push(r.readStrU8());
|
||
continue;
|
||
}
|
||
if (type === BLOCK_TYPE_ACCESS_SERVERS) {
|
||
const accessCount = r.readU8();
|
||
out.accessServers = [];
|
||
for (let k = 0; k < accessCount; k += 1) out.accessServers.push(r.readStrU8());
|
||
continue;
|
||
}
|
||
if (type === BLOCK_TYPE_TRUSTED_STATE) {
|
||
out.trustedCount = r.readU8();
|
||
continue;
|
||
}
|
||
throw new Error(`Неизвестный блок PDA: ${type}`);
|
||
}
|
||
|
||
if (!out.rootKey || !out.deviceKey || !out.blockchain) {
|
||
throw new Error('В PDA отсутствуют обязательные блоки');
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function serializeUnsignedRecordFromState(stateLike) {
|
||
const buf = [];
|
||
const login = String(stateLike.login || '');
|
||
const bch = stateLike.blockchain;
|
||
buf.push(0x53, 0x48, 0x69, 0x4e, 0x45, 1, 0, 0, 0);
|
||
pushU64LE(buf, stateLike.createdAtMs);
|
||
pushU64LE(buf, stateLike.updatedAtMs);
|
||
pushU32LE(buf, stateLike.recordNumber);
|
||
for (const x of stateLike.prevRecordHash) buf.push(x);
|
||
pushStrU8(buf, login);
|
||
const blocksCount = stateLike.isServer ? 6 : 5;
|
||
buf.push(blocksCount);
|
||
|
||
buf.push(BLOCK_TYPE_ROOT_KEY, 0);
|
||
for (const x of stateLike.rootKey) buf.push(x);
|
||
buf.push(BLOCK_TYPE_DEVICE_KEY, 0);
|
||
for (const x of stateLike.deviceKey) buf.push(x);
|
||
|
||
buf.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY, 0, 1);
|
||
buf.push(bch.blockchainType);
|
||
pushStrU8(buf, bch.blockchainName);
|
||
for (const x of bch.blockchainPublicKey) buf.push(x);
|
||
pushU64LE(buf, bch.paidLimitBytes);
|
||
pushU64LE(buf, bch.usedBytes);
|
||
pushU32LE(buf, bch.lastBlockNumber);
|
||
for (const x of bch.lastBlockHash) buf.push(x);
|
||
for (const x of bch.lastBlockSignature) buf.push(x);
|
||
if (String(bch.arweaveTxId || '').trim()) {
|
||
buf.push(1);
|
||
pushStrU8(buf, bch.arweaveTxId);
|
||
} else {
|
||
buf.push(0);
|
||
}
|
||
|
||
if (stateLike.isServer) {
|
||
buf.push(BLOCK_TYPE_SERVER_PROFILE, 0, 1);
|
||
for (const x of stateLike.serverKey) buf.push(x);
|
||
pushStrU8(buf, stateLike.serverAddress);
|
||
const sync = Array.isArray(stateLike.syncServers) ? stateLike.syncServers : [];
|
||
buf.push(sync.length & 0xff);
|
||
for (const s of sync) pushStrU8(buf, s);
|
||
}
|
||
|
||
buf.push(BLOCK_TYPE_ACCESS_SERVERS, 0);
|
||
const access = Array.isArray(stateLike.accessServers) ? stateLike.accessServers : [];
|
||
buf.push(access.length & 0xff);
|
||
for (const s of access) pushStrU8(buf, s);
|
||
|
||
buf.push(BLOCK_TYPE_TRUSTED_STATE, 0, Number(stateLike.trustedCount || 0) & 0xff);
|
||
const recLen = buf.length + 64;
|
||
buf[7] = recLen & 0xff;
|
||
buf[8] = (recLen >>> 8) & 0xff;
|
||
return new Uint8Array(buf);
|
||
}
|
||
|
||
function buildLastBlockStateBytes(login, blockchainName, lastBlockNumber, lastBlockHash32, usedBytes) {
|
||
const buf = [];
|
||
for (const x of new TextEncoder().encode(LAST_BLOCK_STATE_PREFIX)) buf.push(x);
|
||
pushStrU8(buf, login);
|
||
pushStrU8(buf, blockchainName);
|
||
pushU32LE(buf, lastBlockNumber);
|
||
for (const x of lastBlockHash32) buf.push(x);
|
||
pushU64LE(buf, usedBytes);
|
||
return new Uint8Array(buf);
|
||
}
|
||
|
||
function buildEd25519IxData(sig64, pubkey32, msgHash32) {
|
||
const sigOff = 16;
|
||
const pkOff = sigOff + 64;
|
||
const msgOff = pkOff + 32;
|
||
const data = new Uint8Array(msgOff + 32);
|
||
const v = new DataView(data.buffer);
|
||
data[0] = 1;
|
||
data[1] = 0;
|
||
v.setUint16(2, sigOff, true);
|
||
v.setUint16(4, 0xffff, true);
|
||
v.setUint16(6, pkOff, true);
|
||
v.setUint16(8, 0xffff, true);
|
||
v.setUint16(10, msgOff, true);
|
||
v.setUint16(12, 32, true);
|
||
v.setUint16(14, 0xffff, true);
|
||
data.set(sig64, sigOff);
|
||
data.set(pubkey32, pkOff);
|
||
data.set(msgHash32, msgOff);
|
||
return data;
|
||
}
|
||
|
||
function serializeUpdateUserPdaArgs(args) {
|
||
const b = [];
|
||
for (const x of UPDATE_USER_PDA_DISCRIMINATOR) b.push(x);
|
||
pushStrU32(b, args.login);
|
||
for (const x of args.rootKey32) b.push(x);
|
||
pushU64LE(b, args.createdAtMs);
|
||
pushU64LE(b, args.updatedAtMs);
|
||
pushU32LE(b, args.version);
|
||
pushVecU8(b, args.prevHash32);
|
||
pushU64LE(b, args.additionalLimitBytes);
|
||
for (const x of args.deviceKey32) b.push(x);
|
||
for (const x of args.blockchainPublicKey32) b.push(x);
|
||
pushStrU32(b, args.blockchainName);
|
||
pushU64LE(b, args.usedBytes);
|
||
pushU32LE(b, args.lastBlockNumber);
|
||
pushVecU8(b, args.lastBlockHash32);
|
||
pushVecU8(b, args.lastBlockSignature64);
|
||
pushStrU32(b, args.arweaveTxId);
|
||
b.push(args.isServer ? 1 : 0);
|
||
for (const x of args.serverKey32) b.push(x);
|
||
pushStrU32(b, args.serverAddress);
|
||
pushVecStrU32(b, args.syncServers);
|
||
pushVecStrU32(b, args.accessServers);
|
||
b.push(Number(args.trustedCount || 0) & 0xff);
|
||
pushVecU8(b, args.rootSignature64);
|
||
return new Uint8Array(b);
|
||
}
|
||
|
||
function parseUsersEconomyConfig(dataBytes) {
|
||
const v = new DataView(dataBytes.buffer, dataBytes.byteOffset, dataBytes.byteLength);
|
||
if (dataBytes.byteLength < 25) throw new Error('Некорректный economy config');
|
||
return {
|
||
version: v.getUint8(0),
|
||
registrationFeeLamports: v.getBigUint64(1, true),
|
||
lamportsPerLimitStep: v.getBigUint64(9, true),
|
||
startBonusLimit: v.getBigUint64(17, true),
|
||
};
|
||
}
|
||
|
||
export async function getShineUsersEconomyConfig({ solanaEndpoint }) {
|
||
const endpoint = String(solanaEndpoint || '').trim();
|
||
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
|
||
const solana = await loadSolanaLib();
|
||
const connection = new solana.Connection(endpoint, 'confirmed');
|
||
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
|
||
const [economyPda] = solana.PublicKey.findProgramAddressSync(
|
||
[new TextEncoder().encode(SHINE_USERS_ECONOMY_CONFIG_SEED)],
|
||
usersProgram,
|
||
);
|
||
const ai = await connection.getAccountInfo(economyPda, 'confirmed');
|
||
if (!ai?.data) throw new Error('Economy config PDA не найден');
|
||
const economy = parseUsersEconomyConfig(ai.data);
|
||
return { endpoint, economyPda: economyPda.toBase58(), ...economy };
|
||
}
|
||
|
||
export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
|
||
const cleanLogin = String(login || '').trim().toLowerCase();
|
||
const endpoint = String(solanaEndpoint || '').trim();
|
||
if (!cleanLogin) throw new Error('Не указан логин');
|
||
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
|
||
const solana = await loadSolanaLib();
|
||
const connection = new solana.Connection(endpoint, 'confirmed');
|
||
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
|
||
const enc = new TextEncoder();
|
||
const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode('login='), enc.encode(cleanLogin)], usersProgram);
|
||
const ai = await connection.getAccountInfo(userPda, 'confirmed');
|
||
if (!ai?.data) throw new Error('Пользовательский PDA не найден в Solana');
|
||
const parsed = parseShineUserPda(ai.data);
|
||
const bch = parsed.blockchain;
|
||
const leftBytes = bch.paidLimitBytes > bch.usedBytes ? (bch.paidLimitBytes - bch.usedBytes) : 0n;
|
||
return {
|
||
endpoint,
|
||
userPda: userPda.toBase58(),
|
||
login: parsed.login,
|
||
recordNumber: parsed.recordNumber,
|
||
paidLimitBytes: bch.paidLimitBytes,
|
||
usedBytes: bch.usedBytes,
|
||
leftBytes,
|
||
lastBlockNumber: bch.lastBlockNumber,
|
||
lastBlockHashHex: Array.from(bch.lastBlockHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
|
||
};
|
||
}
|
||
|
||
export async function updateShineUserPdaOnSolana({
|
||
login,
|
||
solanaEndpoint,
|
||
rootPrivatePkcs8B64,
|
||
devicePrivatePkcs8B64,
|
||
blockchainPrivatePkcs8B64,
|
||
additionalLimitBytes = 0n,
|
||
nextUsedBytes,
|
||
nextLastBlockNumber,
|
||
nextLastBlockHashHex,
|
||
}) {
|
||
const cleanLogin = String(login || '').trim().toLowerCase();
|
||
if (!cleanLogin) throw new Error('Не указан логин');
|
||
const endpoint = String(solanaEndpoint || '').trim();
|
||
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
|
||
|
||
const solana = await loadSolanaLib();
|
||
const connection = new solana.Connection(endpoint, 'confirmed');
|
||
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
|
||
const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
|
||
const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID);
|
||
const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID);
|
||
const enc = new TextEncoder();
|
||
const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode('login='), enc.encode(cleanLogin)], usersProgram);
|
||
const [economyPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
|
||
const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);
|
||
|
||
const userAi = await connection.getAccountInfo(userPda, 'confirmed');
|
||
if (!userAi?.data) throw new Error('PDA пользователя не найден');
|
||
const current = parseShineUserPda(userAi.data);
|
||
const currentBch = current.blockchain;
|
||
|
||
const effectiveUsed = nextUsedBytes == null ? currentBch.usedBytes : BigInt(nextUsedBytes);
|
||
const effectiveLastNum = nextLastBlockNumber == null ? currentBch.lastBlockNumber : Number(nextLastBlockNumber);
|
||
const effectiveLastHash = nextLastBlockHashHex
|
||
? Uint8Array.from(String(nextLastBlockHashHex).match(/.{1,2}/g).map((h) => parseInt(h, 16)))
|
||
: currentBch.lastBlockHash;
|
||
|
||
if (effectiveLastHash.length !== 32) throw new Error('last block hash должен быть 32 байта');
|
||
const addLimit = BigInt(additionalLimitBytes || 0);
|
||
if (addLimit < 0n) throw new Error('Нельзя уменьшать лимит');
|
||
if (addLimit % LIMIT_STEP !== 0n) throw new Error(`Лимит можно увеличивать только шагом ${LIMIT_STEP}`);
|
||
|
||
const rootPriv = await importPkcs8Ed25519(rootPrivatePkcs8B64);
|
||
const bchPriv = await importPkcs8Ed25519(blockchainPrivatePkcs8B64);
|
||
const deviceSeed32 = extractSeed32FromPkcs8B64(devicePrivatePkcs8B64);
|
||
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
|
||
|
||
const updatedAtMs = BigInt(Date.now());
|
||
const newPaid = currentBch.paidLimitBytes + addLimit;
|
||
const newRecordNumber = current.recordNumber + 1;
|
||
const prevHash = await sha256Bytes(serializeUnsignedRecordFromState(current));
|
||
|
||
const lastBlockStateBytes = buildLastBlockStateBytes(cleanLogin, currentBch.blockchainName, effectiveLastNum, effectiveLastHash, effectiveUsed);
|
||
const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes);
|
||
const lastBlockSig64 = await signBytes(bchPriv, lastBlockStateHash);
|
||
|
||
const nextState = {
|
||
...current,
|
||
updatedAtMs,
|
||
recordNumber: newRecordNumber,
|
||
prevRecordHash: prevHash,
|
||
blockchain: {
|
||
...currentBch,
|
||
paidLimitBytes: newPaid,
|
||
usedBytes: effectiveUsed,
|
||
lastBlockNumber: effectiveLastNum,
|
||
lastBlockHash: effectiveLastHash,
|
||
lastBlockSignature: lastBlockSig64,
|
||
},
|
||
};
|
||
const unsignedNext = serializeUnsignedRecordFromState(nextState);
|
||
const unsignedNextHash = await sha256Bytes(unsignedNext);
|
||
const rootSig64 = await signBytes(rootPriv, unsignedNextHash);
|
||
|
||
const ixData = serializeUpdateUserPdaArgs({
|
||
login: cleanLogin,
|
||
rootKey32: current.rootKey,
|
||
createdAtMs: current.createdAtMs,
|
||
updatedAtMs,
|
||
version: newRecordNumber,
|
||
prevHash32: prevHash,
|
||
additionalLimitBytes: addLimit,
|
||
deviceKey32: current.deviceKey,
|
||
blockchainPublicKey32: currentBch.blockchainPublicKey,
|
||
blockchainName: currentBch.blockchainName,
|
||
usedBytes: effectiveUsed,
|
||
lastBlockNumber: effectiveLastNum,
|
||
lastBlockHash32: effectiveLastHash,
|
||
lastBlockSignature64: lastBlockSig64,
|
||
arweaveTxId: currentBch.arweaveTxId,
|
||
isServer: current.isServer,
|
||
serverKey32: current.serverKey,
|
||
serverAddress: current.serverAddress,
|
||
syncServers: current.syncServers,
|
||
accessServers: current.accessServers,
|
||
trustedCount: current.trustedCount,
|
||
rootSignature64: rootSig64,
|
||
});
|
||
|
||
const edIxRoot = new solana.TransactionInstruction({
|
||
programId: ed25519Program,
|
||
keys: [],
|
||
data: buildEd25519IxData(rootSig64, current.rootKey, unsignedNextHash),
|
||
});
|
||
const edIxBch = new solana.TransactionInstruction({
|
||
programId: ed25519Program,
|
||
keys: [],
|
||
data: buildEd25519IxData(lastBlockSig64, currentBch.blockchainPublicKey, lastBlockStateHash),
|
||
});
|
||
const updIx = new solana.TransactionInstruction({
|
||
programId: usersProgram,
|
||
keys: [
|
||
{ pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true },
|
||
{ pubkey: userPda, isSigner: false, isWritable: true },
|
||
{ pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
|
||
{ pubkey: inflowVault, isSigner: false, isWritable: true },
|
||
{ pubkey: sysvarInstructions, isSigner: false, isWritable: false },
|
||
{ pubkey: economyPda, isSigner: false, isWritable: false },
|
||
],
|
||
data: ixData,
|
||
});
|
||
|
||
const signature = await solana.sendAndConfirmTransaction(
|
||
connection,
|
||
new solana.Transaction().add(edIxRoot, edIxBch, updIx),
|
||
[deviceKeypair],
|
||
{ commitment: 'confirmed' },
|
||
);
|
||
|
||
return {
|
||
signature,
|
||
userPda: userPda.toBase58(),
|
||
paidLimitBytes: newPaid,
|
||
usedBytes: effectiveUsed,
|
||
leftBytes: newPaid > effectiveUsed ? (newPaid - effectiveUsed) : 0n,
|
||
lastBlockNumber: effectiveLastNum,
|
||
lastBlockHashHex: Array.from(effectiveLastHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
|
||
};
|
||
}
|
||
|
||
export function calcLimitTopupPriceLamports(additionalLimitBytes, lamportsPerLimitStep) {
|
||
const add = BigInt(additionalLimitBytes || 0);
|
||
const pricePerStep = BigInt(lamportsPerLimitStep || 0);
|
||
if (add < 0n) throw new Error('Некорректный размер увеличения лимита');
|
||
if (add % LIMIT_STEP !== 0n) throw new Error(`Увеличение лимита должно быть кратно ${LIMIT_STEP} байт`);
|
||
return (add / LIMIT_STEP) * pricePerStep;
|
||
}
|
||
|
||
export function getLimitStepBytes() {
|
||
return LIMIT_STEP;
|
||
}
|