import { sha256Bytes, signBytes, importPkcs8Ed25519, base64ToBytes } from './crypto-utils.js'; import { extractSeed32FromPkcs8B64 } from './device-key-utils.js'; import { SHINE_USERS_PROGRAM_ID, SHINE_PAYMENTS_PROGRAM_ID, SHINE_LOGIN_GUARD_PROGRAM_ID, } from '../solana-programs.js'; const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]); const CLASSIFY_LOGIN_DISCRIMINATOR = new Uint8Array([112, 97, 152, 32, 255, 73, 108, 86]); const PRECHECK_SIM_PAYER = 'FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P'; const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111'; const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111'; 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 = 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); } } // Matches Rust serialize_last_block_state (initial zero state) function buildLastBlockStateBytes(login, blockchainName) { const enc = new TextEncoder(); const prefix = enc.encode('SHiNE_LAST_BLOCK'); const loginB = enc.encode(login); const bchB = enc.encode(blockchainName); const buf = []; for (const x of prefix) buf.push(x); buf.push(loginB.length); for (const x of loginB) buf.push(x); buf.push(bchB.length); for (const x of bchB) buf.push(x); pushU32LE(buf, 0); // last_block_number = 0 for (let i = 0; i < 32; i++) buf.push(0); // last_block_hash = [0;32] pushU64LE(buf, 0n); // used_bytes = 0 return new Uint8Array(buf); } // Matches Rust serialize_unsigned_record for initial registration function buildUnsignedRecordBytes( login, createdAtMs, rootKey32, deviceKey32, blockchainKey32, blockchainName, paidLimitBytes, lastBlockSig64, ) { const enc = new TextEncoder(); const loginB = enc.encode(login); const bchB = enc.encode(blockchainName); const accessB = enc.encode('shineup.me'); const buf = []; // Fixed header: MAGIC(5) + FORMAT_MAJOR(1) + FORMAT_MINOR(1) + record_len_placeholder(2) buf.push(0x53, 0x48, 0x69, 0x4E, 0x45, 1, 0, 0, 0); // indices 0..8 pushU64LE(buf, createdAtMs); // created_at_ms pushU64LE(buf, createdAtMs); // updated_at_ms = same pushU32LE(buf, 0); // record_number = 0 for (let i = 0; i < 32; i++) buf.push(0); // prev_record_hash = [0;32] buf.push(loginB.length); for (const x of loginB) buf.push(x); buf.push(5); // blocks_count (non-server) // 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) buf.push(3, 0, 1, 1); // type, ver, count=1, blockchain_type=1(MAIN_USER) buf.push(bchB.length); for (const x of bchB) buf.push(x); for (const x of blockchainKey32) buf.push(x); pushU64LE(buf, paidLimitBytes); // paid_limit_bytes pushU64LE(buf, 0n); // used_bytes = 0 pushU32LE(buf, 0); // last_block_number = 0 for (let i = 0; i < 32; i++) buf.push(0); // last_block_hash = [0;32] for (const x of lastBlockSig64) buf.push(x); // last_block_signature buf.push(0); // arweave_present = 0 // AccessServersBlock (type=40, ver=0) buf.push(40, 0, 1, accessB.length); for (const x of accessB) buf.push(x); // TrustedStateBlock (type=50, ver=0, trusted_count=0) buf.push(50, 0, 0); // Patch record_len at indices 7-8: total = buf.length + 64 (signature) const recLen = buf.length + 64; buf[7] = recLen & 0xFF; buf[8] = (recLen >> 8) & 0xFF; return new Uint8Array(buf); } // Builds Ed25519 program instruction data for one signature function buildEd25519IxData(sig64, pubkey32, msgHash32) { const sigOff = 16; const pkOff = sigOff + 64; // 80 const msgOff = pkOff + 32; // 112 const data = new Uint8Array(msgOff + 32); // 144 bytes total const v = new DataView(data.buffer); data[0] = 1; data[1] = 0; // num_signatures=1, padding v.setUint16(2, sigOff, true); v.setUint16(4, 0xFFFF, true); // same instruction v.setUint16(6, pkOff, true); v.setUint16(8, 0xFFFF, true); v.setUint16(10, msgOff, true); v.setUint16(12, 32, true); // message_data_size = 32 v.setUint16(14, 0xFFFF, true); data.set(sig64, sigOff); data.set(pubkey32, pkOff); data.set(msgHash32, msgOff); return data; } function readStartBonusLimit(data) { // Borsh: version(u8=1) + reg_fee(u64=8) + lamports_per_step(u64=8) + start_bonus_limit(u64=8) const v = new DataView(data.buffer, data.byteOffset, data.byteLength); return v.getBigUint64(17, true); } function serializeCreateUserPdaArgs( login, rootKey32, createdAtMs, deviceKey32, blockchainKey32, blockchainName, lastBlockSig64, 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(0n); // used_bytes b.u32(0); // last_block_number b.vecU8(new Uint8Array(32)); // last_block_hash b.vecU8(lastBlockSig64); // last_block_signature b.str(''); // arweave_tx_id b.bool(false); // is_server b.bytes32(new Uint8Array(32)); // server_key (default) b.str(''); // server_address b.vecStr([]); // sync_servers b.vecStr(['shineup.me']); // access_servers b.u8(0); // trusted_count b.vecU8(rootSig64); // signature return b.result(); } function serializeClassifyLoginArgs(login) { const b = new BorshBuf(); b.raw(CLASSIFY_LOGIN_DISCRIMINATOR); b.str(String(login || '')); return b.result(); } function decodeU32FromB64(rawB64) { const bytes = Uint8Array.from(atob(rawB64), (ch) => ch.charCodeAt(0)); if (bytes.length < 4) throw new Error('LOGIN_GUARD_BAD_RETURN_DATA'); return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true); } function safeJson(value) { try { return JSON.stringify(value); } catch { return String(value); } } export function formatSolanaErrorDetails(error) { const parts = []; const msg = String(error?.message || error || '').trim(); if (msg) parts.push(msg); const logs = error?.logs || error?.transactionLogs || error?.simulationLogs || error?.data?.logs; if (Array.isArray(logs) && logs.length) { parts.push(`Logs: ${logs.join(' | ')}`); } const errObj = error?.value?.err || error?.err || error?.data?.err; if (errObj) { parts.push(`Err: ${safeJson(errObj)}`); } if (!parts.length) return 'unknown'; return parts.join(' :: '); } export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) { const solana = await loadSolanaLib(); const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed'); const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID); const payer = new solana.PublicKey(PRECHECK_SIM_PAYER); const ix = new solana.TransactionInstruction({ programId: loginGuardProgram, keys: [{ pubkey: payer, isSigner: true, isWritable: false }], data: serializeClassifyLoginArgs(String(login || '').toLowerCase()), }); const { blockhash } = await connection.getLatestBlockhash('confirmed'); const v0Message = new solana.TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions: [ix], }).compileToV0Message(); const tx = new solana.VersionedTransaction(v0Message); const sim = await connection.simulateTransaction(tx, { commitment: 'confirmed', sigVerify: false, replaceRecentBlockhash: true, }); if (sim?.value?.err) { const simErr = new Error(`LOGIN_GUARD_SIMULATION_FAILED: ${safeJson(sim.value.err)}`); simErr.logs = sim?.value?.logs || []; simErr.err = sim?.value?.err; throw simErr; } const returnData = sim?.value?.returnData; if (!returnData || returnData.programId !== SHINE_LOGIN_GUARD_PROGRAM_ID) { throw new Error('LOGIN_GUARD_BAD_RETURN_DATA'); } const classValue = decodeU32FromB64(returnData.data?.[0] || ''); if (classValue === 0) return { classCode: 0, className: 'free' }; if (classValue === 1) return { classCode: 1, className: 'premium' }; if (classValue === 2) return { classCode: 2, className: 'company' }; return { classCode: classValue, className: 'unknown' }; } export async function checkLoginExistsOnSolana({ login, solanaEndpoint }) { const solana = await loadSolanaLib(); const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed'); const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID); const enc = new TextEncoder(); const loginNorm = String(login || '').trim().toLowerCase(); if (!loginNorm) { throw new Error('EMPTY_LOGIN'); } const [userPda] = solana.PublicKey.findProgramAddressSync( [enc.encode('login='), enc.encode(loginNorm)], usersProgram, ); const ai = await connection.getAccountInfo(userPda, 'confirmed'); return { exists: !!ai, userPda: userPda.toBase58() }; } export async function registerUserOnSolana({ login, keyBundle, 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 = login.toLowerCase(); const blockchainName = `${loginNorm}-001`; 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 rootPrivKey = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64); const bchPrivKey = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64); const deviceSeed32 = extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64); const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32); const ecoAccount = await connection.getAccountInfo(economyConfigPda); if (!ecoAccount) { throw new Error('Economy config не инициализирован. Запустите init_users_economy_config.'); } const startBonusLimit = readStartBonusLimit(ecoAccount.data); const createdAtMs = BigInt(Date.now()); // Sign LastBlockState with blockchain key const lbsBytes = buildLastBlockStateBytes(loginNorm, blockchainName); const lbsHash = await sha256Bytes(lbsBytes); const lastBlockSig64 = await signBytes(bchPrivKey, lbsHash); // Build and sign unsigned PDA record with root key const unsignedRecord = buildUnsignedRecordBytes( loginNorm, createdAtMs, rootKey32, deviceKey32, blockchainKey32, blockchainName, startBonusLimit, lastBlockSig64, ); const unsignedHash = await sha256Bytes(unsignedRecord); const rootSig64 = await signBytes(rootPrivKey, unsignedHash); const ixData = serializeCreateUserPdaArgs( loginNorm, rootKey32, createdAtMs, deviceKey32, blockchainKey32, blockchainName, lastBlockSig64, rootSig64, ); // Ed25519 instructions must precede create_user_pda const ed25519RootIx = new solana.TransactionInstruction({ programId: ed25519Program, keys: [], data: buildEd25519IxData(rootSig64, rootKey32, unsignedHash), }); const ed25519BchIx = new solana.TransactionInstruction({ programId: ed25519Program, keys: [], data: buildEd25519IxData(lastBlockSig64, blockchainKey32, lbsHash), }); const createUserIx = 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 sig = await solana.sendAndConfirmTransaction( connection, new solana.Transaction().add(ed25519RootIx, ed25519BchIx, createUserIx), [deviceKeypair], { commitment: 'confirmed' }, ); return { signature: sig, blockchainName }; }