import { base64ToBytes, importPkcs8Ed25519, sha256Bytes, signBytes } from './crypto-utils.js'; import { extractSeed32FromPkcs8B64 } from './client-key-utils.js'; import { SHINE_LOGIN_GUARD_PROGRAM_ID, SHINE_PAYMENTS_PROGRAM_ID, SHINE_USERS_ECONOMY_CONFIG_SEED, SHINE_USERS_PROGRAM_ID, } from '../solana-programs.js'; import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js'; const MAGIC = 'SHiNE'; const LAST_BLOCK_STATE_PREFIX = 'SHiNE_LAST_BLOCK'; const SHINE_PAYMENTS_INFLOW_VAULT_SEED = 'shine_payments_inflow_vault'; const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login='; const LIMIT_STEP = 10_000n; const BLOCKCHAIN_TYPE_MAIN_USER = 1; const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111'; const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111'; const BLOCK_TYPE_RECOVERY_KEY = 0; const BLOCK_TYPE_ROOT_KEY = 1; const BLOCK_TYPE_CLIENT_KEY = 2; const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3; const BLOCK_TYPE_SERVER_PROFILE = 30; const BLOCK_TYPE_ACCESS_SERVERS = 40; const BLOCK_TYPE_SESSIONS = 50; const BLOCK_TYPE_TRUSTED_STATE = 70; const SESSIONS_MODE_MIXED = 1; const SESSION_TYPE_USER = 1; const SESSION_TYPE_HOMESERVER = 100; let solanaLibPromise = null; function loadSolanaLib() { if (!solanaLibPromise) solanaLibPromise = loadSolanaWeb3(); return solanaLibPromise; } function normalizeLogin(login) { return String(login || '').trim().toLowerCase(); } function pushU32LE(buf, value) { const n = Number(value) >>> 0; buf.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff); } function pushU64LE(buf, value) { const b = BigInt(value); 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 value of arr) pushStrU32(buf, value); } function pushSessionRecordsU32(buf, sessions) { const arr = Array.isArray(sessions) ? sessions : []; pushU32LE(buf, arr.length); for (const session of arr) { buf.push(Number(session?.sessionType || 0) & 0xff); buf.push(Number(session?.sessionVersion || 0) & 0xff); pushStrU32(buf, String(session?.sessionName || '')); const key = session?.sessionPubKey32 || new Uint8Array(32); for (const x of key) buf.push(x); } } function makeReader(bytes) { let offset = 0; const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const ensure = (len) => { if (offset + len > bytes.length) throw new Error('Повреждённый формат PDA'); }; const readU8 = () => { ensure(1); const value = view.getUint8(offset); offset += 1; return value; }; const readU16 = () => { ensure(2); const value = view.getUint16(offset, true); offset += 2; return value; }; const readU32 = () => { ensure(4); const value = view.getUint32(offset, true); offset += 4; return value; }; const readU64 = () => { ensure(8); const value = view.getBigUint64(offset, true); offset += 8; return value; }; const readBytes = (len) => { ensure(len); const out = bytes.slice(offset, offset + len); offset += len; return out; }; const readStrU8 = () => { const len = readU8(); return new TextDecoder().decode(readBytes(len)); }; return { readU8, readU16, readU32, readU64, readBytes, readStrU8, get offset() { return offset; }, }; } function parseUsersEconomyConfig(dataBytes) { const view = new DataView(dataBytes.buffer, dataBytes.byteOffset, dataBytes.byteLength); if (dataBytes.byteLength < 25) throw new Error('Некорректный economy config'); return { version: view.getUint8(0), registrationFeeLamports: view.getBigUint64(1, true), lamportsPerLimitStep: view.getBigUint64(9, true), startBonusLimit: view.getBigUint64(17, true), }; } function buildEd25519IxData(sig64, pubkey32, msgHash32) { const sigOff = 16; const pkOff = sigOff + 64; const msgOff = pkOff + 32; const data = new Uint8Array(msgOff + 32); const view = new DataView(data.buffer); data[0] = 1; data[1] = 0; view.setUint16(2, sigOff, true); view.setUint16(4, 0xffff, true); view.setUint16(6, pkOff, true); view.setUint16(8, 0xffff, true); view.setUint16(10, msgOff, true); view.setUint16(12, 32, true); view.setUint16(14, 0xffff, true); data.set(sig64, sigOff); data.set(pubkey32, pkOff); data.set(msgHash32, msgOff); return data; } function serializeCreateUserPdaArgs(args) { const buf = []; buf.push(3); pushStrU8(buf, args.login); for (const x of args.recoveryKey32) buf.push(x); for (const x of args.rootKey32) buf.push(x); pushU64LE(buf, args.createdAtMs); pushU64LE(buf, BigInt(args.additionalLimitBytes || 0n)); for (const x of args.clientKey32) buf.push(x); for (const x of args.blockchainPublicKey32) buf.push(x); pushStrU8(buf, args.blockchainName); pushU64LE(buf, args.usedBytes); pushU32LE(buf, args.lastBlockNumber); for (const x of args.lastBlockHash32) buf.push(x); for (const x of args.lastBlockSignature64) buf.push(x); pushStrU8(buf, args.arweaveTxId); buf.push(args.isServer ? 1 : 0); if (args.isServer) { buf.push(Number(args.addressFormatType || 0) & 0xff); buf.push(Number(args.addressFormatVersion || 0) & 0xff); pushStrU8(buf, args.serverAddress); const syncServers = Array.isArray(args.syncServers) ? args.syncServers : []; buf.push(syncServers.length & 0xff); for (const value of syncServers) pushStrU8(buf, value); } const accessServers = Array.isArray(args.accessServers) ? args.accessServers : []; buf.push(accessServers.length & 0xff); for (const value of accessServers) pushStrU8(buf, value); buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff); const sessions = Array.isArray(args.sessions) ? args.sessions : []; buf.push(sessions.length & 0xff); for (const session of sessions) { buf.push(Number(session?.sessionType || 0) & 0xff); buf.push(Number(session?.sessionVersion || 0) & 0xff); pushStrU8(buf, String(session?.sessionName || '')); const key = session?.sessionPubKey32 || new Uint8Array(32); for (const x of key) buf.push(x); } buf.push(Number(args.trustedCount || 0) & 0xff); for (const x of args.rootSignature64) buf.push(x); return new Uint8Array(buf); } function serializeUpdateUserPdaArgs(args) { const buf = []; buf.push(4); pushStrU8(buf, args.login); for (const x of args.recoveryKey32) buf.push(x); for (const x of args.rootKey32) buf.push(x); pushU64LE(buf, args.createdAtMs); pushU64LE(buf, args.updatedAtMs); pushU32LE(buf, args.version); for (const x of args.prevHash32) buf.push(x); pushU64LE(buf, args.additionalLimitBytes); for (const x of args.clientKey32) buf.push(x); for (const x of args.blockchainPublicKey32) buf.push(x); pushStrU8(buf, args.blockchainName); pushU64LE(buf, args.usedBytes); pushU32LE(buf, args.lastBlockNumber); for (const x of args.lastBlockHash32) buf.push(x); for (const x of args.lastBlockSignature64) buf.push(x); pushStrU8(buf, args.arweaveTxId); buf.push(args.isServer ? 1 : 0); if (args.isServer) { buf.push(Number(args.addressFormatType || 0) & 0xff); buf.push(Number(args.addressFormatVersion || 0) & 0xff); pushStrU8(buf, args.serverAddress); const syncServers = Array.isArray(args.syncServers) ? args.syncServers : []; buf.push(syncServers.length & 0xff); for (const value of syncServers) pushStrU8(buf, value); } const accessServers = Array.isArray(args.accessServers) ? args.accessServers : []; buf.push(accessServers.length & 0xff); for (const value of accessServers) pushStrU8(buf, value); buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff); const sessions = Array.isArray(args.sessions) ? args.sessions : []; buf.push(sessions.length & 0xff); for (const session of sessions) { buf.push(Number(session?.sessionType || 0) & 0xff); buf.push(Number(session?.sessionVersion || 0) & 0xff); pushStrU8(buf, String(session?.sessionName || '')); const key = session?.sessionPubKey32 || new Uint8Array(32); for (const x of key) buf.push(x); } buf.push(Number(args.trustedCount || 0) & 0xff); for (const x of args.rootSignature64) buf.push(x); return new Uint8Array(buf); } function createBlockchainState({ blockchainName, blockchainPublicKey, paidLimitBytes, usedBytes, lastBlockNumber, lastBlockHash, lastBlockSignature, arweaveTxId, }) { return { blockchainType: BLOCKCHAIN_TYPE_MAIN_USER, blockchainName, blockchainPublicKey, paidLimitBytes, usedBytes, lastBlockNumber, lastBlockHash, lastBlockSignature, arweaveTxId, }; } function createPdaState({ login, createdAtMs, updatedAtMs, recordNumber, prevRecordHash, recoveryKey, rootKey, clientKey, blockchain, isServer, addressFormatType, addressFormatVersion, serverAddress, syncServers, accessServers, sessionsMode, sessions, trustedCount, }) { const serverProfile = isServer ? { addressFormatType: Number(addressFormatType || 0), addressFormatVersion: Number(addressFormatVersion || 0), serverAddress: String(serverAddress || ''), syncServers: Array.isArray(syncServers) ? [...syncServers] : [], } : null; return { createdAtMs, updatedAtMs, recordNumber, prevRecordHash, login, recoveryKey, rootKey, clientKey, clientKey: clientKey, blockchain, isServer: Boolean(isServer), serverProfile, serverData: serverProfile, addressFormatType: serverProfile?.addressFormatType ?? 0, addressFormatVersion: serverProfile?.addressFormatVersion ?? 0, serverAddress: serverProfile?.serverAddress ?? '', syncServers: serverProfile?.syncServers ? [...serverProfile.syncServers] : [], accessServers: Array.isArray(accessServers) ? [...accessServers] : [], sessionsMode: Number(sessionsMode || SESSIONS_MODE_MIXED) & 0xff, sessions: Array.isArray(sessions) ? sessions.map((x) => ({ sessionType: Number(x?.sessionType || 0) & 0xff, sessionVersion: Number(x?.sessionVersion || 0) & 0xff, sessionName: String(x?.sessionName || ''), sessionPubKey32: x?.sessionPubKey32 instanceof Uint8Array ? x.sessionPubKey32 : new Uint8Array(x?.sessionPubKey32 || 32), })) : [], trustedCount: Number(trustedCount || 0) & 0xff, }; } function encodeOptionalArweave(buf, arweaveTxId) { const value = String(arweaveTxId || '').trim(); if (value) { buf.push(1); pushStrU8(buf, value); } else { buf.push(0); } } export function buildLastBlockStateBytes(login, blockchainName, lastBlockNumber = 0, lastBlockHash32 = new Uint8Array(32), usedBytes = 0n) { 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); } export function parseShineUserPda(dataBytes) { const bytes = dataBytes instanceof Uint8Array ? dataBytes : new Uint8Array(dataBytes || []); const reader = makeReader(bytes); const magic = new TextDecoder().decode(reader.readBytes(5)); if (magic !== MAGIC) throw new Error('Некорректный формат PDA'); reader.readU8(); reader.readU8(); const recordLen = reader.readU16(); if (recordLen < 9 + 64 || recordLen > bytes.length) throw new Error('Некорректный record_len'); const createdAtMs = reader.readU64(); const updatedAtMs = reader.readU64(); const recordNumber = reader.readU32(); const prevRecordHash = reader.readBytes(32); const login = reader.readStrU8(); const blocksCount = reader.readU8(); let recoveryKey = null; let rootKey = null; let clientKey = null; let blockchain = null; let isServer = false; let addressFormatType = 0; let addressFormatVersion = 0; let serverAddress = ''; let syncServers = []; let accessServers = []; let sessionsMode = SESSIONS_MODE_MIXED; let sessions = []; let trustedCount = 0; for (let i = 0; i < blocksCount; i += 1) { const blockType = reader.readU8(); reader.readU8(); if (blockType === BLOCK_TYPE_RECOVERY_KEY) { recoveryKey = reader.readBytes(32); continue; } if (blockType === BLOCK_TYPE_ROOT_KEY) { rootKey = reader.readBytes(32); continue; } if (blockType === BLOCK_TYPE_CLIENT_KEY) { clientKey = reader.readBytes(32); continue; } if (blockType === BLOCK_TYPE_BLOCKCHAIN_REGISTRY) { const count = reader.readU8(); for (let j = 0; j < count; j += 1) { const blockchainType = reader.readU8(); const blockchainName = reader.readStrU8(); const blockchainPublicKey = reader.readBytes(32); const paidLimitBytes = reader.readU64(); const usedBytes = reader.readU64(); const lastBlockNumber = reader.readU32(); const lastBlockHash = reader.readBytes(32); const lastBlockSignature = reader.readBytes(64); const arweavePresent = reader.readU8(); const arweaveTxId = arweavePresent === 1 ? reader.readStrU8() : ''; if (!blockchain) { blockchain = { blockchainType, blockchainName, blockchainPublicKey, paidLimitBytes, usedBytes, lastBlockNumber, lastBlockHash, lastBlockSignature, arweaveTxId, }; } } continue; } if (blockType === BLOCK_TYPE_SERVER_PROFILE) { isServer = reader.readU8() === 1; if (isServer) { addressFormatType = reader.readU8(); addressFormatVersion = reader.readU8(); serverAddress = reader.readStrU8(); const syncCount = reader.readU8(); syncServers = []; for (let j = 0; j < syncCount; j += 1) syncServers.push(reader.readStrU8()); } continue; } if (blockType === BLOCK_TYPE_ACCESS_SERVERS) { const accessCount = reader.readU8(); accessServers = []; for (let j = 0; j < accessCount; j += 1) accessServers.push(reader.readStrU8()); continue; } if (blockType === BLOCK_TYPE_SESSIONS) { sessionsMode = reader.readU8(); const sessionsCount = reader.readU8(); sessions = []; for (let j = 0; j < sessionsCount; j += 1) { sessions.push({ sessionType: reader.readU8(), sessionVersion: reader.readU8(), sessionName: reader.readStrU8(), sessionPubKey32: reader.readBytes(32), }); } continue; } if (blockType === BLOCK_TYPE_TRUSTED_STATE) { trustedCount = reader.readU8(); continue; } throw new Error(`Неизвестный блок PDA: ${blockType}`); } if (!recoveryKey || !rootKey || !clientKey || !blockchain) { throw new Error('В PDA отсутствуют обязательные блоки'); } const signature = bytes.slice(reader.offset, reader.offset + 64); const unsignedBytes = bytes.slice(0, recordLen - 64); const state = createPdaState({ login, createdAtMs, updatedAtMs, recordNumber, prevRecordHash, recoveryKey, rootKey, clientKey, blockchain, isServer, addressFormatType, addressFormatVersion, serverAddress, syncServers, accessServers, sessionsMode, sessions, trustedCount, }); return { ...state, recordLen, unsignedBytes, signature, }; } export function serializeUnsignedRecordFromState(stateLike) { const state = createPdaState({ login: stateLike.login, createdAtMs: stateLike.createdAtMs, updatedAtMs: stateLike.updatedAtMs, recordNumber: stateLike.recordNumber, prevRecordHash: stateLike.prevRecordHash, recoveryKey: stateLike.recoveryKey, rootKey: stateLike.rootKey, clientKey: stateLike.clientKey ?? stateLike.clientKey, blockchain: stateLike.blockchain, isServer: stateLike.isServer, addressFormatType: stateLike.addressFormatType ?? stateLike.serverProfile?.addressFormatType, addressFormatVersion: stateLike.addressFormatVersion ?? stateLike.serverProfile?.addressFormatVersion, serverAddress: stateLike.serverAddress ?? stateLike.serverProfile?.serverAddress, syncServers: stateLike.syncServers ?? stateLike.serverProfile?.syncServers, accessServers: stateLike.accessServers, sessionsMode: stateLike.sessionsMode, sessions: stateLike.sessions, trustedCount: stateLike.trustedCount, }); const buf = [0x53, 0x48, 0x69, 0x4e, 0x45, 1, 0, 0, 0]; pushU64LE(buf, state.createdAtMs); pushU64LE(buf, state.updatedAtMs); pushU32LE(buf, state.recordNumber); for (const x of state.prevRecordHash) buf.push(x); pushStrU8(buf, state.login); buf.push(state.isServer ? 8 : 7); buf.push(BLOCK_TYPE_RECOVERY_KEY, 0); for (const x of state.recoveryKey) buf.push(x); buf.push(BLOCK_TYPE_ROOT_KEY, 0); for (const x of state.rootKey) buf.push(x); buf.push(BLOCK_TYPE_CLIENT_KEY, 0); for (const x of state.clientKey) buf.push(x); buf.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY, 0, 1, state.blockchain.blockchainType); pushStrU8(buf, state.blockchain.blockchainName); for (const x of state.blockchain.blockchainPublicKey) buf.push(x); pushU64LE(buf, state.blockchain.paidLimitBytes); pushU64LE(buf, state.blockchain.usedBytes); pushU32LE(buf, state.blockchain.lastBlockNumber); for (const x of state.blockchain.lastBlockHash) buf.push(x); for (const x of state.blockchain.lastBlockSignature) buf.push(x); encodeOptionalArweave(buf, state.blockchain.arweaveTxId); if (state.isServer) { buf.push(BLOCK_TYPE_SERVER_PROFILE, 0, 1); buf.push(state.addressFormatType & 0xff); buf.push(state.addressFormatVersion & 0xff); pushStrU8(buf, state.serverAddress); buf.push(state.syncServers.length & 0xff); for (const loginValue of state.syncServers) pushStrU8(buf, loginValue); } buf.push(BLOCK_TYPE_ACCESS_SERVERS, 0, state.accessServers.length & 0xff); for (const loginValue of state.accessServers) pushStrU8(buf, loginValue); buf.push(BLOCK_TYPE_SESSIONS, 0, state.sessionsMode & 0xff, state.sessions.length & 0xff); for (const session of state.sessions) { buf.push(session.sessionType & 0xff, session.sessionVersion & 0xff); pushStrU8(buf, session.sessionName); for (const x of session.sessionPubKey32) buf.push(x); } buf.push(BLOCK_TYPE_TRUSTED_STATE, 0, state.trustedCount & 0xff); const recordLen = buf.length + 64; buf[7] = recordLen & 0xff; buf[8] = (recordLen >>> 8) & 0xff; return new Uint8Array(buf); } 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 accountInfo = await connection.getAccountInfo(economyPda, 'confirmed'); if (!accountInfo?.data) throw new Error('Economy config PDA не найден'); return { endpoint, economyPda: economyPda.toBase58(), ...parseUsersEconomyConfig(accountInfo.data) }; } export async function readShineUserPda({ login, solanaEndpoint }) { const cleanLogin = normalizeLogin(login); 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(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram); const accountInfo = await connection.getAccountInfo(userPda, 'confirmed'); if (!accountInfo?.data) throw new Error(`PDA не найдена для логина «${cleanLogin}»`); return { ...parseShineUserPda(accountInfo.data), userPda: userPda.toBase58(), pdaAddress: userPda.toBase58(), endpoint, }; } export async function readShineUserPdaByAddress({ pdaAddress, solanaEndpoint }) { const address = String(pdaAddress || '').trim(); const endpoint = String(solanaEndpoint || '').trim(); if (!address) throw new Error('Не указан адрес PDA'); if (!endpoint) throw new Error('Не указан Solana RPC endpoint'); const solana = await loadSolanaLib(); const connection = new solana.Connection(endpoint, 'confirmed'); const accountAddress = new solana.PublicKey(address); const accountInfo = await connection.getAccountInfo(accountAddress, 'confirmed'); if (!accountInfo?.data) throw new Error(`PDA не найдена: ${address}`); return { ...parseShineUserPda(accountInfo.data), userPda: accountAddress.toBase58(), pdaAddress: accountAddress.toBase58(), endpoint, }; } export async function readShineUserPdaByRef({ value, solanaEndpoint }) { const ref = String(value || '').trim(); if (!ref) throw new Error('Не указан логин или адрес PDA'); try { const bytes = base58ToBytes(ref); if (bytes.length === 32) { return await readShineUserPdaByAddress({ pdaAddress: ref, solanaEndpoint }); } } catch { // Если это не адрес, читаем как логин. } return readShineUserPda({ login: ref, solanaEndpoint }); } export async function getShineBlockchainUsage({ login, solanaEndpoint }) { const parsed = await readShineUserPda({ login, solanaEndpoint }); const bch = parsed.blockchain; const leftBytes = bch.paidLimitBytes > bch.usedBytes ? (bch.paidLimitBytes - bch.usedBytes) : 0n; return { endpoint: parsed.endpoint, userPda: parsed.userPda, 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(''), }; } function parseHex32(value) { const clean = String(value || '').trim().toLowerCase(); if (!clean) return null; if (!/^[0-9a-f]+$/.test(clean) || clean.length !== 64) { throw new Error('last block hash должен быть 32 байта в hex'); } const out = new Uint8Array(32); for (let i = 0; i < 32; i += 1) { out[i] = parseInt(clean.slice(i * 2, (i * 2) + 2), 16); } return out; } async function attachSolanaLogs(error, connection) { if (!error || typeof error.getLogs !== 'function' || !connection) { return error; } try { const logs = await error.getLogs(connection); if (Array.isArray(logs) && logs.length) { error.logs = logs; error.transactionLogs = logs; error.simulationLogs = logs; if (!String(error.message || '').includes('Logs:')) { error.message = `${String(error.message || 'Solana transaction failed')} :: Logs: ${logs.join(' | ')}`; } } } catch { // Если RPC не вернул логи, оставляем исходную ошибку как есть. } return error; } async function buildCreateContext({ login, keyBundle, solanaEndpoint }) { const cleanLogin = normalizeLogin(login); 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 paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID); const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_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(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram); const [economyConfigPda] = 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 recoveryKey32 = base64ToBytes(keyBundle.recoveryPair.publicKeyB64); const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64); const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64); const clientKey32 = base64ToBytes(keyBundle.clientPair.publicKeyB64); const rootPrivKey = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64); const bchPrivKey = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64); const clientSeed32 = extractSeed32FromPkcs8B64(keyBundle.clientPair.privatePkcs8B64); const clientKeypair = solana.Keypair.fromSeed(clientSeed32); return { cleanLogin, endpoint, solana, connection, usersProgram, paymentsProgram, loginGuardProgram, ed25519Program, sysvarInstructions, userPda, economyConfigPda, inflowVault, recoveryKey32, rootKey32, blockchainKey32, clientKey32, rootPrivKey, bchPrivKey, clientKeypair, }; } async function createShineUserPdaOnSolana({ login, keyBundle, solanaEndpoint, isServer = false, addressFormatType = 0, addressFormatVersion = 0, serverAddress = '', syncServers = [], accessServers = [], }) { const ctx = await buildCreateContext({ login, keyBundle, solanaEndpoint }); const ecoAccount = await ctx.connection.getAccountInfo(ctx.economyConfigPda); if (!ecoAccount?.data) { throw new Error('Economy config не инициализирован. Запустите init_users_economy_config.'); } const cleanLogin = ctx.cleanLogin; const blockchainName = `${cleanLogin}-001`; const zeroHash32 = new Uint8Array(32); const createdAtMs = BigInt(Date.now()); const startBonusLimit = parseUsersEconomyConfig(ecoAccount.data).startBonusLimit; const lastBlockStateBytes = buildLastBlockStateBytes(cleanLogin, blockchainName, 0, zeroHash32, 0n); const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes); const lastBlockSig64 = await signBytes(ctx.bchPrivKey, lastBlockStateHash); const initialState = createPdaState({ login: cleanLogin, createdAtMs, updatedAtMs: createdAtMs, recordNumber: 0, prevRecordHash: zeroHash32, recoveryKey: ctx.recoveryKey32, rootKey: ctx.rootKey32, clientKey: ctx.clientKey32, blockchain: createBlockchainState({ blockchainName, blockchainPublicKey: ctx.blockchainKey32, paidLimitBytes: startBonusLimit, usedBytes: 0n, lastBlockNumber: 0, lastBlockHash: zeroHash32, lastBlockSignature: lastBlockSig64, arweaveTxId: '', }), isServer, addressFormatType, addressFormatVersion, serverAddress, syncServers, accessServers, sessionsMode: SESSIONS_MODE_MIXED, sessions: [], trustedCount: 0, }); const unsignedRecord = serializeUnsignedRecordFromState(initialState); const unsignedHash = await sha256Bytes(unsignedRecord); const rootSig64 = await signBytes(ctx.rootPrivKey, unsignedHash); const ixData = serializeCreateUserPdaArgs({ login: cleanLogin, recoveryKey32: ctx.recoveryKey32, rootKey32: ctx.rootKey32, createdAtMs, additionalLimitBytes: 0n, clientKey32: ctx.clientKey32, blockchainPublicKey32: ctx.blockchainKey32, blockchainName, usedBytes: 0n, lastBlockNumber: 0, lastBlockHash32: zeroHash32, lastBlockSignature64: lastBlockSig64, arweaveTxId: '', isServer, addressFormatType: isServer ? addressFormatType : 0, addressFormatVersion: isServer ? addressFormatVersion : 0, serverAddress: isServer ? serverAddress : '', syncServers: isServer ? syncServers : [], accessServers, sessionsMode: SESSIONS_MODE_MIXED, sessions: [], trustedCount: 0, rootSignature64: rootSig64, }); const ed25519RootIx = new ctx.solana.TransactionInstruction({ programId: ctx.ed25519Program, keys: [], data: buildEd25519IxData(rootSig64, ctx.rootKey32, unsignedHash), }); const ed25519BchIx = new ctx.solana.TransactionInstruction({ programId: ctx.ed25519Program, keys: [], data: buildEd25519IxData(lastBlockSig64, ctx.blockchainKey32, lastBlockStateHash), }); const createIx = new ctx.solana.TransactionInstruction({ programId: ctx.usersProgram, keys: [ { pubkey: ctx.clientKeypair.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 }, { pubkey: ctx.sysvarInstructions, isSigner: false, isWritable: false }, { pubkey: ctx.economyConfigPda, isSigner: false, isWritable: false }, { pubkey: ctx.loginGuardProgram, isSigner: false, isWritable: false }, ], data: ixData, }); let signature; try { signature = await ctx.solana.sendAndConfirmTransaction( ctx.connection, new ctx.solana.Transaction().add(ed25519RootIx, ed25519BchIx, createIx), [ctx.clientKeypair], { commitment: 'confirmed' }, ); } catch (error) { throw await attachSolanaLogs(error, ctx.connection); } return { signature, userPda: ctx.userPda.toBase58(), pdaAddress: ctx.userPda.toBase58(), blockchainName, }; } export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint, accessServers = [] }) { return createShineUserPdaOnSolana({ login, keyBundle, solanaEndpoint, isServer: false, accessServers: Array.isArray(accessServers) ? accessServers : [], }); } export async function registerServerOnSolana({ login, keyBundle, serverAddress, addressFormatType = 1, addressFormatVersion = 0, syncServers = [], accessServers = [], solanaEndpoint, }) { return createShineUserPdaOnSolana({ login, keyBundle, solanaEndpoint, isServer: true, addressFormatType, addressFormatVersion, serverAddress, syncServers, accessServers, }); } export async function updateShineUserPdaOnSolana({ login, solanaEndpoint, rootPrivatePkcs8B64, clientPrivatePkcs8B64, blockchainPrivatePkcs8B64, additionalLimitBytes = 0n, nextUsedBytes, nextLastBlockNumber, nextLastBlockHashHex, serverProfile, accessServers, trustedCount, }) { const current = await readShineUserPda({ login, solanaEndpoint }); const cleanLogin = current.login; const endpoint = current.endpoint; const currentBch = current.blockchain; const addLimit = BigInt(additionalLimitBytes || 0); if (addLimit < 0n) throw new Error('Нельзя уменьшать лимит'); if (addLimit % LIMIT_STEP !== 0n) throw new Error(`Лимит можно увеличивать только шагом ${LIMIT_STEP}`); const effectiveUsed = nextUsedBytes == null ? currentBch.usedBytes : BigInt(nextUsedBytes); const effectiveLastNum = nextLastBlockNumber == null ? currentBch.lastBlockNumber : Number(nextLastBlockNumber); const effectiveLastHash = parseHex32(nextLastBlockHashHex) || currentBch.lastBlockHash; if (effectiveLastHash.length !== 32) throw new Error('last block hash должен быть 32 байта'); const rootPriv = await importPkcs8Ed25519(rootPrivatePkcs8B64); 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(SHINE_USERS_USER_PDA_SEED_PREFIX), 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 clientSeed32 = extractSeed32FromPkcs8B64(clientPrivatePkcs8B64); const clientKeypair = solana.Keypair.fromSeed(clientSeed32); const lastBlockStateBytes = buildLastBlockStateBytes( cleanLogin, currentBch.blockchainName, effectiveLastNum, effectiveLastHash, effectiveUsed, ); const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes); const blockchainStateWillChange = ( nextUsedBytes != null || nextLastBlockNumber != null || nextLastBlockHashHex != null ); let lastBlockSig64 = currentBch.lastBlockSignature; if (blockchainPrivatePkcs8B64) { const bchPriv = await importPkcs8Ed25519(blockchainPrivatePkcs8B64); lastBlockSig64 = await signBytes(bchPriv, lastBlockStateHash); } else if (blockchainStateWillChange) { throw new Error('Для изменения last block state нужен blockchain-ключ'); } const updatedAtMs = BigInt(Date.now()); const newPaid = currentBch.paidLimitBytes + addLimit; const newRecordNumber = current.recordNumber + 1; const prevHash = await sha256Bytes(serializeUnsignedRecordFromState(current)); const nextServerProfile = serverProfile ? { addressFormatType: Number(serverProfile.addressFormatType ?? current.addressFormatType ?? 0), addressFormatVersion: Number(serverProfile.addressFormatVersion ?? current.addressFormatVersion ?? 0), serverAddress: String(serverProfile.serverAddress ?? current.serverAddress ?? ''), syncServers: Array.isArray(serverProfile.syncServers) ? [...serverProfile.syncServers] : [...current.syncServers], } : current.serverProfile; const nextState = createPdaState({ login: cleanLogin, createdAtMs: current.createdAtMs, updatedAtMs, recordNumber: newRecordNumber, prevRecordHash: prevHash, recoveryKey: current.recoveryKey, rootKey: current.rootKey, clientKey: current.clientKey ?? current.clientKey, blockchain: createBlockchainState({ blockchainName: currentBch.blockchainName, blockchainPublicKey: currentBch.blockchainPublicKey, paidLimitBytes: newPaid, usedBytes: effectiveUsed, lastBlockNumber: effectiveLastNum, lastBlockHash: effectiveLastHash, lastBlockSignature: lastBlockSig64, arweaveTxId: currentBch.arweaveTxId, }), isServer: Boolean(nextServerProfile), addressFormatType: nextServerProfile?.addressFormatType ?? 0, addressFormatVersion: nextServerProfile?.addressFormatVersion ?? 0, serverAddress: nextServerProfile?.serverAddress ?? '', syncServers: nextServerProfile?.syncServers ?? [], accessServers: accessServers == null ? current.accessServers : accessServers, sessionsMode: current.sessionsMode, sessions: current.sessions, trustedCount: trustedCount == null ? current.trustedCount : trustedCount, }); const unsignedNext = serializeUnsignedRecordFromState(nextState); const unsignedNextHash = await sha256Bytes(unsignedNext); const rootSig64 = await signBytes(rootPriv, unsignedNextHash); const ixData = serializeUpdateUserPdaArgs({ login: cleanLogin, recoveryKey32: current.recoveryKey, rootKey32: current.rootKey, createdAtMs: current.createdAtMs, updatedAtMs, version: newRecordNumber, prevHash32: prevHash, additionalLimitBytes: addLimit, clientKey32: current.clientKey, blockchainPublicKey32: currentBch.blockchainPublicKey, blockchainName: currentBch.blockchainName, usedBytes: effectiveUsed, lastBlockNumber: effectiveLastNum, lastBlockHash32: effectiveLastHash, lastBlockSignature64: lastBlockSig64, arweaveTxId: currentBch.arweaveTxId, isServer: nextState.isServer, addressFormatType: nextState.addressFormatType, addressFormatVersion: nextState.addressFormatVersion, serverAddress: nextState.serverAddress, syncServers: nextState.syncServers, accessServers: nextState.accessServers, sessionsMode: nextState.sessionsMode, sessions: nextState.sessions, trustedCount: nextState.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 updateIx = new solana.TransactionInstruction({ programId: usersProgram, keys: [ { pubkey: clientKeypair.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 computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 800_000 }); const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 262_144 }); let signature; try { signature = await solana.sendAndConfirmTransaction( connection, new solana.Transaction().add(computeIx, heapIx, edIxRoot, edIxBch, updateIx), [clientKeypair], { commitment: 'confirmed' }, ); } catch (error) { throw await attachSolanaLogs(error, connection); } return { signature, userPda: userPda.toBase58(), pdaAddress: 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 async function updateServerOnSolana({ login, keyBundle, serverAddress, addressFormatType, addressFormatVersion, syncServers, solanaEndpoint, }) { return updateShineUserPdaOnSolana({ login, solanaEndpoint, rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64, clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64, serverProfile: { addressFormatType, addressFormatVersion, serverAddress, syncServers, }, }); } 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; }