// Логика управления серверной PDA в Solana (shine_users) // Автономный модуль для панели администратора сервера SHiNE const SHINE_USERS_PROGRAM_ID = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm'; const SHINE_PAYMENTS_PROGRAM_ID = 'm48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR'; const SHINE_LOGIN_GUARD_PROGRAM_ID = '3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo'; const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111'; const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111'; // Discriminator create_user_pda (sha256("global:create_user_pda")[0..8]) const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]); let _solanaLib = null; async function loadSolanaLib() { if (!_solanaLib) _solanaLib = await import('https://esm.sh/@solana/web3.js@1.98.4?bundle'); return _solanaLib; } let _argon2Lib = null; async function loadArgon2() { if (!_argon2Lib) _argon2Lib = await import('https://esm.sh/@noble/hashes@1.8.0/argon2.js'); return _argon2Lib; } // ------------------------------------------------------------------- // Crypto (WebCrypto, Ed25519) // ------------------------------------------------------------------- async function sha256Bytes(bytes) { const buf = await crypto.subtle.digest('SHA-256', bytes); return new Uint8Array(buf); } async function signEd25519(pkcs8B64, messageBytes) { const pkcs8 = Uint8Array.from(atob(pkcs8B64), c => c.charCodeAt(0)); const key = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, false, ['sign']); const sig = await crypto.subtle.sign({ name: 'Ed25519' }, key, messageBytes); return new Uint8Array(sig); } function base64ToBytes(b64) { return Uint8Array.from(atob(b64), c => c.charCodeAt(0)); } function extractSeed32FromPkcs8B64(pkcs8B64) { // Ed25519 PKCS8 (48 байт): seed расположен начиная с байта 16 return base64ToBytes(pkcs8B64).slice(16, 48); } async function anchorDiscriminator(name) { const hash = await sha256Bytes(new TextEncoder().encode(`global:${name}`)); return hash.slice(0, 8); } // ------------------------------------------------------------------- // Borsh-кодирование (Anchor-совместимое) // ------------------------------------------------------------------- function pushU32LE(buf, v) { const n = v >>> 0; buf.push(n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF); } function pushU64LE(buf, bigV) { const b = typeof bigV === 'bigint' ? bigV : BigInt(bigV); const lo = Number(b & 0xFFFFFFFFn) >>> 0; const hi = Number((b >> 32n) & 0xFFFFFFFFn) >>> 0; pushU32LE(buf, lo); pushU32LE(buf, hi); } class BorshBuf { constructor() { this._b = []; } u8(v) { this._b.push(v & 0xFF); } u32(v) { pushU32LE(this._b, v); } u64(v) { pushU64LE(this._b, v); } bool(v) { this.u8(v ? 1 : 0); } bytes32(b) { for (const x of b) this._b.push(x); } vecU8(b) { this.u32(b.length); for (const x of b) this._b.push(x); } str(s) { const enc = new TextEncoder().encode(s); this.u32(enc.length); for (const x of enc) this._b.push(x); } vecStr(arr) { this.u32(arr.length); for (const s of arr) this.str(s); } raw(bytes) { for (const x of bytes) this._b.push(x); } result() { return new Uint8Array(this._b); } } // ------------------------------------------------------------------- // Построение бинарного формата PDA (matches Rust serialize_unsigned_record) // ------------------------------------------------------------------- function buildLastBlockStateBytes(login, blockchainName, lastBlockNumber, lastBlockHash32, usedBytes) { const enc = new TextEncoder(); const buf = []; for (const x of enc.encode('SHiNE_LAST_BLOCK')) buf.push(x); const loginB = enc.encode(login); buf.push(loginB.length); for (const x of loginB) buf.push(x); const bchB = enc.encode(blockchainName); buf.push(bchB.length); for (const x of bchB) buf.push(x); pushU32LE(buf, lastBlockNumber); for (const x of lastBlockHash32) buf.push(x); pushU64LE(buf, usedBytes); return new Uint8Array(buf); } function buildUnsignedRecordBytesServer({ login, createdAtMs, updatedAtMs, recordNumber, prevHash32, rootKey32, deviceKey32, blockchainKey32, blockchainName, paidLimitBytes, usedBytes, lastBlockNumber, lastBlockHash32, lastBlockSig64, arweaveTxId, serverAddress, addressFormatType, addressFormatVersion, syncServers, accessServers, trustedCount, }) { const enc = new TextEncoder(); const loginB = enc.encode(login); const bchB = enc.encode(blockchainName); const buf = []; // Заголовок: MAGIC(5) + FORMAT_MAJOR(1) + FORMAT_MINOR(1) + record_len_placeholder(2) buf.push(0x53, 0x48, 0x69, 0x4E, 0x45, 1, 0, 0, 0); // байты 0..8 pushU64LE(buf, createdAtMs); pushU64LE(buf, updatedAtMs); pushU32LE(buf, recordNumber); for (const x of prevHash32) buf.push(x); buf.push(loginB.length); for (const x of loginB) buf.push(x); buf.push(6); // blocks_count = 6 (сервер) // RootKeyBlock (type=1, ver=0) buf.push(1, 0); for (const x of rootKey32) buf.push(x); // DeviceKeyBlock (type=2, ver=0) buf.push(2, 0); for (const x of deviceKey32) buf.push(x); // BlockchainRegistryBlock (type=3, ver=0, count=1, blockchain_type=1) buf.push(3, 0, 1, 1); buf.push(bchB.length); for (const x of bchB) buf.push(x); for (const x of blockchainKey32) buf.push(x); pushU64LE(buf, paidLimitBytes); pushU64LE(buf, usedBytes); pushU32LE(buf, lastBlockNumber); for (const x of lastBlockHash32) buf.push(x); for (const x of lastBlockSig64) buf.push(x); if (arweaveTxId) { buf.push(1); const aTxB = enc.encode(arweaveTxId); buf.push(aTxB.length); for (const x of aTxB) buf.push(x); } else { buf.push(0); } // ServerProfileBlock (type=30, ver=0) buf.push(30, 0); buf.push(1); // is_server = 1 buf.push(addressFormatType & 0xFF); buf.push(addressFormatVersion & 0xFF); const srvB = enc.encode(serverAddress); buf.push(srvB.length); for (const x of srvB) buf.push(x); buf.push(syncServers.length); for (const srv of syncServers) { const sB = enc.encode(srv); buf.push(sB.length); for (const x of sB) buf.push(x); } // AccessServersBlock (type=40, ver=0) buf.push(40, 0, accessServers.length); for (const srv of accessServers) { const sB = enc.encode(srv); buf.push(sB.length); for (const x of sB) buf.push(x); } // TrustedStateBlock (type=50, ver=0) buf.push(50, 0, trustedCount & 0xFF); // Записываем record_len: (длина буфера + 64 байта подписи) const recLen = buf.length + 64; buf[7] = recLen & 0xFF; buf[8] = (recLen >> 8) & 0xFF; return new Uint8Array(buf); } // ------------------------------------------------------------------- // Borsh-сериализация Anchor-инструкций // ------------------------------------------------------------------- function serializeCreateServerPdaArgs({ login, rootKey32, createdAtMs, deviceKey32, blockchainKey32, blockchainName, usedBytes, lastBlockNumber, lastBlockHash32, lastBlockSig64, arweaveTxId, serverAddress, addressFormatType, addressFormatVersion, syncServers, accessServers, trustedCount, rootSig64, }) { const b = new BorshBuf(); b.raw(CREATE_USER_PDA_DISCRIMINATOR); b.str(login); b.bytes32(rootKey32); b.u64(createdAtMs); b.u64(0n); // additional_limit // UserMutableFields: b.bytes32(deviceKey32); b.bytes32(blockchainKey32); b.str(blockchainName); b.u64(usedBytes); b.u32(lastBlockNumber); b.vecU8(lastBlockHash32); b.vecU8(lastBlockSig64); b.str(arweaveTxId); b.bool(true); // is_server b.u8(addressFormatType); b.u8(addressFormatVersion); b.str(serverAddress); b.vecStr(syncServers); b.vecStr(accessServers); b.u8(trustedCount); b.vecU8(rootSig64); return b.result(); } async function serializeUpdateServerPdaArgs({ login, rootKey32, createdAtMs, updatedAtMs, version, prevHash32, deviceKey32, blockchainKey32, blockchainName, usedBytes, lastBlockNumber, lastBlockHash32, lastBlockSig64, arweaveTxId, serverAddress, addressFormatType, addressFormatVersion, syncServers, accessServers, trustedCount, rootSig64, }) { const discriminator = await anchorDiscriminator('update_user_pda'); const b = new BorshBuf(); b.raw(discriminator); b.str(login); b.bytes32(rootKey32); b.u64(createdAtMs); b.u64(updatedAtMs); b.u32(version); b.vecU8(prevHash32); b.u64(0n); // additional_limit // UserMutableFields: b.bytes32(deviceKey32); b.bytes32(blockchainKey32); b.str(blockchainName); b.u64(usedBytes); b.u32(lastBlockNumber); b.vecU8(lastBlockHash32); b.vecU8(lastBlockSig64); b.str(arweaveTxId); b.bool(true); // is_server b.u8(addressFormatType); b.u8(addressFormatVersion); b.str(serverAddress); b.vecStr(syncServers); b.vecStr(accessServers); b.u8(trustedCount); b.vecU8(rootSig64); return b.result(); } // ------------------------------------------------------------------- // Построитель Ed25519-инструкции Solana // ------------------------------------------------------------------- function buildEd25519IxData(sig64, pubkey32, msgHash32) { const sigOff = 16, pkOff = 80, msgOff = 112; 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; } // ------------------------------------------------------------------- // Парсер бинарных данных PDA (matches Rust deserialize_record_from_pda) // ------------------------------------------------------------------- export function parsePdaData(raw) { const d = raw instanceof Uint8Array ? raw : new Uint8Array(raw); if (d.length < 9) throw new Error('PDA слишком короткая'); if (String.fromCharCode(d[0], d[1], d[2], d[3], d[4]) !== 'SHiNE') { throw new Error('Неверный magic в PDA'); } const view = new DataView(d.buffer, d.byteOffset); const recordLen = view.getUint16(7, true); if (recordLen < 9 + 64 || recordLen > d.length) throw new Error('Неверный record_len'); // Подписанная часть = байты [0 .. recordLen-64) const unsignedBytes = d.slice(0, recordLen - 64); let cur = 9; const ru8 = () => d[cur++]; const ru32 = () => { const v = view.getUint32(cur, true); cur += 4; return v; }; const ru64 = () => { const v = view.getBigUint64(cur, true); cur += 8; return v; }; const rBytes = n => { const s = d.slice(cur, cur + n); cur += n; return s; }; const rStr = () => { const len = ru8(); return new TextDecoder().decode(rBytes(len)); }; const createdAtMs = ru64(); const updatedAtMs = ru64(); const recordNumber = ru32(); const prevRecordHash = rBytes(32); const login = rStr(); const blocksCount = ru8(); let rootKey32 = null, deviceKey32 = null, blockchainData = null; let isServer = false, serverData = null; let accessServers = [], trustedCount = 0; for (let i = 0; i < blocksCount; i++) { const blockType = ru8(); ru8(); // block_version if (blockType === 1) { rootKey32 = rBytes(32); } else if (blockType === 2) { deviceKey32 = rBytes(32); } else if (blockType === 3) { const count = ru8(); const blockchainType = ru8(); const blockchainName = rStr(); const blockchainPublicKey = rBytes(32); const paidLimitBytes = ru64(); const usedBytes = ru64(); const lastBlockNumber = ru32(); const lastBlockHash = rBytes(32); const lastBlockSignature = rBytes(64); const arweavePresent = ru8(); const arweaveTxId = arweavePresent === 1 ? rStr() : ''; blockchainData = { blockchainType, blockchainName, blockchainPublicKey, paidLimitBytes, usedBytes, lastBlockNumber, lastBlockHash, lastBlockSignature, arweaveTxId, }; } else if (blockType === 30) { if (ru8() === 1) { isServer = true; const addressFormatType = ru8(); const addressFormatVersion = ru8(); const serverAddress = rStr(); const syncCount = ru8(); const syncServers = []; for (let j = 0; j < syncCount; j++) syncServers.push(rStr()); serverData = { addressFormatType, addressFormatVersion, serverAddress, syncServers }; } } else if (blockType === 40) { const cnt = ru8(); for (let j = 0; j < cnt; j++) accessServers.push(rStr()); } else if (blockType === 50) { trustedCount = ru8(); } } const signature = d.slice(cur, cur + 64); return { recordLen, unsignedBytes, createdAtMs, updatedAtMs, recordNumber, prevRecordHash, login, rootKey32, deviceKey32, blockchainData, isServer, serverData, accessServers, trustedCount, signature, }; } // ------------------------------------------------------------------- // Вспомогательная: читает start_bonus_limit из economy config PDA // ------------------------------------------------------------------- function readStartBonusLimit(data) { // Borsh: version(u8=1) + reg_fee(u64) + lamports_per_step(u64) = 17 байт до start_bonus_limit return new DataView(data.buffer, data.byteOffset, data.byteLength).getBigUint64(17, true); } // ------------------------------------------------------------------- // Читает и парсит существующую PDA с блокчейна // ------------------------------------------------------------------- export async function readServerPdaData({ login, solanaEndpoint }) { const solana = await loadSolanaLib(); const connection = new solana.Connection(String(solanaEndpoint), 'confirmed'); const loginNorm = String(login).trim().toLowerCase(); const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID); const enc = new TextEncoder(); const [userPda] = solana.PublicKey.findProgramAddressSync( [enc.encode('login='), enc.encode(loginNorm)], usersProgram, ); const ai = await connection.getAccountInfo(userPda, 'confirmed'); if (!ai) throw new Error(`PDA не найдена для логина «${loginNorm}»`); const parsed = parsePdaData(ai.data); parsed.pdaAddress = userPda.toBase58(); return parsed; } // ------------------------------------------------------------------- // Регистрация нового серверного аккаунта в Solana // ------------------------------------------------------------------- export async function registerServerOnSolana({ login, keyBundle, serverAddress, addressFormatType = 1, addressFormatVersion = 0, syncServers = [], accessServers = [], solanaEndpoint, }) { const solana = await loadSolanaLib(); const connection = new solana.Connection(String(solanaEndpoint), '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 loginNorm = String(login).trim().toLowerCase(); const blockchainName = `${loginNorm}-001`; const zeroHash32 = new Uint8Array(32); const [userPda] = solana.PublicKey.findProgramAddressSync( [enc.encode('login='), enc.encode(loginNorm)], usersProgram); const [economyConfigPda] = solana.PublicKey.findProgramAddressSync( [enc.encode('shine_users_economy_config')], usersProgram); const [inflowVault] = solana.PublicKey.findProgramAddressSync( [enc.encode('shine_payments_inflow_vault')], paymentsProgram); const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64); const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64); const deviceKey32 = base64ToBytes(keyBundle.devicePair.publicKeyB64); const deviceKeypair = solana.Keypair.fromSeed( extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64)); const ecoAccount = await connection.getAccountInfo(economyConfigPda); if (!ecoAccount) throw new Error('Economy config не инициализирован'); const paidLimitBytes = readStartBonusLimit(ecoAccount.data); // additional_limit = 0 const createdAtMs = BigInt(Date.now()); // Подписываем LastBlockState ключом блокчейна (начальное состояние: всё нули) const lbsBytes = buildLastBlockStateBytes(loginNorm, blockchainName, 0, zeroHash32, 0n); const lbsHash = await sha256Bytes(lbsBytes); const lastBlockSig64 = await signEd25519(keyBundle.blockchainPair.privatePkcs8B64, lbsHash); // Строим и подписываем беззнаковую запись PDA корневым ключом const unsignedRecord = buildUnsignedRecordBytesServer({ login: loginNorm, createdAtMs, updatedAtMs: createdAtMs, recordNumber: 0, prevHash32: zeroHash32, rootKey32, deviceKey32, blockchainKey32, blockchainName, paidLimitBytes, usedBytes: 0n, lastBlockNumber: 0, lastBlockHash32: zeroHash32, lastBlockSig64, arweaveTxId: '', serverAddress, addressFormatType, addressFormatVersion, syncServers, accessServers, trustedCount: 0, }); const unsignedHash = await sha256Bytes(unsignedRecord); const rootSig64 = await signEd25519(keyBundle.rootPair.privatePkcs8B64, unsignedHash); const ixData = serializeCreateServerPdaArgs({ login: loginNorm, rootKey32, createdAtMs, deviceKey32, blockchainKey32, blockchainName, usedBytes: 0n, lastBlockNumber: 0, lastBlockHash32: zeroHash32, lastBlockSig64, arweaveTxId: '', serverAddress, addressFormatType, addressFormatVersion, syncServers, accessServers, trustedCount: 0, rootSig64, }); const tx = new solana.Transaction().add( new solana.TransactionInstruction({ programId: ed25519Program, keys: [], data: buildEd25519IxData(rootSig64, rootKey32, unsignedHash), }), new solana.TransactionInstruction({ programId: ed25519Program, keys: [], data: buildEd25519IxData(lastBlockSig64, blockchainKey32, lbsHash), }), 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: economyConfigPda, isSigner: false, isWritable: false }, { pubkey: loginGuardProgram, isSigner: false, isWritable: false }, ], data: ixData, }), ); const signature = await solana.sendAndConfirmTransaction( connection, tx, [deviceKeypair], { commitment: 'confirmed' }); return { signature, pdaAddress: userPda.toBase58(), blockchainName }; } // ------------------------------------------------------------------- // Обновление серверного профиля в существующей PDA // Для обновления нужен только root-ключ (подпись записи) + device-ключ (оплата). // Blockchain-ключ не нужен — переиспользуем существующую подпись LastBlockState из PDA. // ------------------------------------------------------------------- export async function updateServerOnSolana({ login, keyBundle, serverAddress, addressFormatType, addressFormatVersion, syncServers, solanaEndpoint, }) { const solana = await loadSolanaLib(); const connection = new solana.Connection(String(solanaEndpoint), '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 loginNorm = String(login).trim().toLowerCase(); const [userPda] = solana.PublicKey.findProgramAddressSync( [enc.encode('login='), enc.encode(loginNorm)], usersProgram); const [economyConfigPda] = solana.PublicKey.findProgramAddressSync( [enc.encode('shine_users_economy_config')], usersProgram); const [inflowVault] = solana.PublicKey.findProgramAddressSync( [enc.encode('shine_payments_inflow_vault')], paymentsProgram); // Читаем существующую PDA const ai = await connection.getAccountInfo(userPda, 'confirmed'); if (!ai) throw new Error(`PDA не найдена для логина «${loginNorm}»`); const pda = parsePdaData(ai.data); if (!pda.isServer) throw new Error('Эта PDA не является серверной (is_server = false)'); const bch = pda.blockchainData; const deviceKeypair = solana.Keypair.fromSeed( extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64)); // Формат адреса: берём из аргументов или из существующей PDA const fmtType = addressFormatType ?? pda.serverData?.addressFormatType ?? 1; const fmtVersion = addressFormatVersion ?? pda.serverData?.addressFormatVersion ?? 0; // prev_hash = sha256(unsigned_bytes предыдущей записи) const prevHash32 = await sha256Bytes(pda.unsignedBytes); const updatedAtMs = BigInt(Date.now()); const newVersion = pda.recordNumber + 1; // Строим новую беззнаковую запись const unsignedRecord = buildUnsignedRecordBytesServer({ login: loginNorm, createdAtMs: pda.createdAtMs, updatedAtMs, recordNumber: newVersion, prevHash32, rootKey32: pda.rootKey32, deviceKey32: pda.deviceKey32, blockchainKey32: bch.blockchainPublicKey, blockchainName: bch.blockchainName, paidLimitBytes: bch.paidLimitBytes, usedBytes: bch.usedBytes, lastBlockNumber: bch.lastBlockNumber, lastBlockHash32: bch.lastBlockHash, lastBlockSig64: bch.lastBlockSignature, arweaveTxId: bch.arweaveTxId, serverAddress, addressFormatType: fmtType, addressFormatVersion: fmtVersion, syncServers, accessServers: pda.accessServers, trustedCount: pda.trustedCount, }); const unsignedHash = await sha256Bytes(unsignedRecord); const rootSig64 = await signEd25519(keyBundle.rootPair.privatePkcs8B64, unsignedHash); // Хэш LastBlockState из существующей PDA (те же данные — та же подпись) const lbsBytes = buildLastBlockStateBytes( loginNorm, bch.blockchainName, bch.lastBlockNumber, bch.lastBlockHash, bch.usedBytes); const lbsHash = await sha256Bytes(lbsBytes); const ixData = await serializeUpdateServerPdaArgs({ login: loginNorm, rootKey32: pda.rootKey32, createdAtMs: pda.createdAtMs, updatedAtMs, version: newVersion, prevHash32, deviceKey32: pda.deviceKey32, blockchainKey32: bch.blockchainPublicKey, blockchainName: bch.blockchainName, usedBytes: bch.usedBytes, lastBlockNumber: bch.lastBlockNumber, lastBlockHash32: bch.lastBlockHash, lastBlockSig64: bch.lastBlockSignature, arweaveTxId: bch.arweaveTxId, serverAddress, addressFormatType: fmtType, addressFormatVersion: fmtVersion, syncServers, accessServers: pda.accessServers, trustedCount: pda.trustedCount, rootSig64, }); const tx = new solana.Transaction().add( // Ed25519: подпись новой записи корневым ключом new solana.TransactionInstruction({ programId: ed25519Program, keys: [], data: buildEd25519IxData(rootSig64, pda.rootKey32, unsignedHash), }), // Ed25519: переиспользуем существующую подпись LastBlockState из PDA new solana.TransactionInstruction({ programId: ed25519Program, keys: [], data: buildEd25519IxData(bch.lastBlockSignature, bch.blockchainPublicKey, lbsHash), }), 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: economyConfigPda, isSigner: false, isWritable: false }, ], data: ixData, }), ); const signature = await solana.sendAndConfirmTransaction( connection, tx, [deviceKeypair], { commitment: 'confirmed' }); return { signature, pdaAddress: userPda.toBase58() }; } // ------------------------------------------------------------------- // Деривация keyBundle из логина + пароля // Идентична логике SHiNE-клиента (crypto-utils.js): // masterSecret = Argon2id(login+"\n"+password, salt=sha256("shine-auth-v2|login=...|suffix=master.secret")) // rootPair = Ed25519(sha256(base64(master) + "|root.key")) // blockchainPair = Ed25519(sha256(base64(master) + "|bch.key")) // devicePair = Ed25519(sha256(base64(master) + "|dev.key")) // ------------------------------------------------------------------- function _b64urlToStd(s) { const n = s.replace(/-/g, '+').replace(/_/g, '/'); return n + '='.repeat((4 - n.length % 4) % 4); } function _ed25519Pkcs8FromSeed(seed32) { const prefix = new Uint8Array([ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20, ]); const out = new Uint8Array(prefix.length + 32); out.set(prefix); out.set(seed32, prefix.length); return out; } async function _deriveEd25519PairFromMasterSecret(masterSecret32, suffix) { const enc = new TextEncoder(); const material = `${btoa(String.fromCharCode(...masterSecret32))}|${suffix}`; const seed = await sha256Bytes(enc.encode(material)); const pkcs8 = _ed25519Pkcs8FromSeed(seed); const privateKey = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']); const jwk = await crypto.subtle.exportKey('jwk', privateKey); if (!jwk.x) throw new Error(`Не удалось получить публичный ключ (suffix=${suffix})`); const pubBytes = base64ToBytes(_b64urlToStd(jwk.x)); return { publicKeyB64: btoa(String.fromCharCode(...pubBytes)), privatePkcs8B64: btoa(String.fromCharCode(...pkcs8)), }; } /** * Выводит полный keyBundle из логина и пароля. * Та же самая логика, что используется в SHiNE-клиенте при регистрации. * * @param {string} login — логин сервера (нормализуется в нижний регистр) * @param {string} password — пароль * @param {function} [onProgress] — коллбэк(0..1) прогресса Argon2id * @returns {{ rootPair, blockchainPair, devicePair }} */ export async function deriveKeyBundleFromPassword({ login, password, onProgress }) { const { argon2idAsync } = await loadArgon2(); const enc = new TextEncoder(); const loginNorm = String(login || '').trim().toLowerCase(); const pwd = String(password ?? ''); // Salt для master secret = sha256("shine-auth-v2|login=...|suffix=master.secret")[0..16] const saltSource = `shine-auth-v2|login=${loginNorm}|suffix=master.secret`; const saltFull = await sha256Bytes(enc.encode(saltSource)); const salt = saltFull.slice(0, 16); const passBytes = enc.encode(`${loginNorm}\n${pwd}`); const masterRaw = await argon2idAsync(passBytes, salt, { t: 2, m: 65536, p: 1, dkLen: 32, onProgress, }); const masterSecret32 = new Uint8Array(masterRaw); const [rootPair, blockchainPair, devicePair] = await Promise.all([ _deriveEd25519PairFromMasterSecret(masterSecret32, 'root.key'), _deriveEd25519PairFromMasterSecret(masterSecret32, 'bch.key'), _deriveEd25519PairFromMasterSecret(masterSecret32, 'dev.key'), ]); const masterSecretB64 = btoa(String.fromCharCode(...masterSecret32)); return { masterSecretB64, rootPair, blockchainPair, devicePair }; } // ------------------------------------------------------------------- // Кодирование байт в base58 (для отображения Solana-адреса) // ------------------------------------------------------------------- export function base58Encode(bytes) { const ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; let num = 0n; for (const b of bytes) num = (num << 8n) | BigInt(b); let result = ''; while (num > 0n) { result = ALPHA[Number(num % 58n)] + result; num /= 58n; } for (const b of bytes) { if (b !== 0) break; result = '1' + result; } return result; }