import { deriveEd25519FromPassword } from './crypto-utils.js'; import { extractDeviceKey32FromStoredValue } from './device-key-utils.js'; import { loadEncryptedUserSecrets } from './key-vault.js'; import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js'; const DEFAULT_SOLANA_ENDPOINT = SOLANA_ENDPOINT_DEFAULT; const TOPUP_SITE_URL = '/devnet-topup-view'; let solanaLibPromise = null; const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]+$/; const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; const BASE58_MAP = (() => { const out = Object.create(null); for (let i = 0; i < BASE58_ALPHABET.length; i += 1) { out[BASE58_ALPHABET[i]] = i; } return out; })(); function normalizeEndpoint(url) { const raw = String(url || '').trim(); if (!raw) return DEFAULT_SOLANA_ENDPOINT; return raw; } async function loadSolanaLib() { if (!solanaLibPromise) { solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle'); } return solanaLibPromise; } function decodeBase58(input) { const text = String(input || '').trim(); if (!text) return new Uint8Array(0); const bytes = [0]; for (let i = 0; i < text.length; i += 1) { const ch = text[i]; const value = BASE58_MAP[ch]; if (value == null) { throw new Error('Недопустимый символ Base58'); } let carry = value; for (let j = 0; j < bytes.length; j += 1) { const x = (bytes[j] * 58) + carry; bytes[j] = x & 0xff; carry = x >> 8; } while (carry > 0) { bytes.push(carry & 0xff); carry >>= 8; } } for (let i = 0; i < text.length && text[i] === '1'; i += 1) { bytes.push(0); } bytes.reverse(); return Uint8Array.from(bytes); } async function keypairFromPkcs8(pkcs8B64) { const solana = await loadSolanaLib(); const seed32 = extractDeviceKey32FromStoredValue(pkcs8B64); return solana.Keypair.fromSeed(seed32); } export async function deriveWalletFromPassword(password) { const keyBundle = await deriveEd25519FromPassword(String(password ?? ''), 'dev.key'); const keypair = await keypairFromPkcs8(keyBundle.privatePkcs8B64); return { address: keypair.publicKey.toBase58(), keypair, devicePublicKeyB64: keyBundle.publicKeyB64, devicePrivatePkcs8B64: keyBundle.privatePkcs8B64, }; } export async function createRandomSolanaWallet() { const solana = await loadSolanaLib(); const keypair = solana.Keypair.generate(); const privateKey32Base58 = solana.bs58.encode(keypair.secretKey.slice(0, 32)); return { address: keypair.publicKey.toBase58(), privateKey32Base58, keypair, }; } export async function createSolanaWalletFromPrivateBase58(privateKey32Base58) { const solana = await loadSolanaLib(); const clean = String(privateKey32Base58 || '').trim(); if (!clean) throw new Error('Введите приватный ключ'); if (!BASE58_RE.test(clean)) throw new Error('Разрешены только символы Base58'); const privateBytes = decodeBase58(clean); if (privateBytes.length !== 32) { throw new Error('Приватный ключ должен быть ровно 32 байта в Base58'); } const keypair = solana.Keypair.fromSeed(privateBytes); return { address: keypair.publicKey.toBase58(), privateKey32Base58: clean, keypair, }; } export async function getWalletFromStoredDeviceKey({ login, storagePwd }) { const cleanLogin = String(login || '').trim(); const cleanPwd = String(storagePwd || '').trim(); if (!cleanLogin || !cleanPwd) { throw new Error('Нет активной сессии для доступа к wallet.key'); } const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd); const devicePrivate = String(secrets?.deviceKey || '').trim(); if (!devicePrivate) { throw new Error('На устройстве не найден device.key (wallet.key)'); } const keypair = await keypairFromPkcs8(devicePrivate); return { address: keypair.publicKey.toBase58(), keypair, devicePrivatePkcs8B64: devicePrivate, }; } export async function getBalanceSol({ endpoint, address }) { const solana = await loadSolanaLib(); const rpc = normalizeEndpoint(endpoint); const conn = new solana.Connection(rpc, 'confirmed'); const pubkey = new solana.PublicKey(String(address || '').trim()); const lamports = await conn.getBalance(pubkey, 'confirmed'); return { endpoint: rpc, lamports, sol: lamports / solana.LAMPORTS_PER_SOL, }; } export async function requestAirdropSol({ endpoint, address, amountSol = 1 }) { const solana = await loadSolanaLib(); const rpc = normalizeEndpoint(endpoint); const conn = new solana.Connection(rpc, 'confirmed'); const pubkey = new solana.PublicKey(String(address || '').trim()); const lamports = Math.max(1, Math.floor(Number(amountSol) * solana.LAMPORTS_PER_SOL)); const signature = await conn.requestAirdrop(pubkey, lamports); await conn.confirmTransaction(signature, 'confirmed'); return { endpoint: rpc, signature, lamports }; } export async function transferSol({ endpoint, fromKeypair, toAddress, amountSol }) { const solana = await loadSolanaLib(); const rpc = normalizeEndpoint(endpoint); const cleanTo = String(toAddress || '').trim(); const amount = Number(amountSol); if (!cleanTo) throw new Error('Не указан адрес получателя'); if (!Number.isFinite(amount) || amount <= 0) throw new Error('Сумма перевода должна быть больше 0'); const conn = new solana.Connection(rpc, 'confirmed'); const lamports = Math.floor(amount * solana.LAMPORTS_PER_SOL); if (lamports <= 0) throw new Error('Сумма слишком мала'); const tx = new solana.Transaction().add( solana.SystemProgram.transfer({ fromPubkey: fromKeypair.publicKey, toPubkey: new solana.PublicKey(cleanTo), lamports, }), ); const signature = await solana.sendAndConfirmTransaction(conn, tx, [fromKeypair], { commitment: 'confirmed', }); return { endpoint: rpc, signature, lamports }; } export function formatSol(value, digits = 6) { const n = Number(value); if (!Number.isFinite(n)) return '0'; return n.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: digits, }); } export function getTopupSiteUrl(walletAddress = '') { const cleanWallet = String(walletAddress || '').trim(); if (!cleanWallet) return TOPUP_SITE_URL; try { const base = typeof window !== 'undefined' && window.location?.origin ? window.location.origin : 'http://localhost:8088'; const url = new URL(TOPUP_SITE_URL, base); url.searchParams.set('wallet', cleanWallet); return url.toString(); } catch { return `${TOPUP_SITE_URL}?wallet=${encodeURIComponent(cleanWallet)}`; } }