import { base58ToBytes, base64ToBytes, bytesToBase58, bytesToBase64, deriveEd25519FromMasterSecret, deriveMasterSecretFromPassword, publicKeyB64FromPkcs8Ed25519, } from '../../js/services/crypto-utils.js'; const LOGIN_RE = /^[a-z0-9_]{1,20}$/; const ED25519_PKCS8_PREFIX = new Uint8Array([ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20, ]); export function $(id) { return document.getElementById(id); } export function normalizeLogin(login) { return String(login || '').trim().toLowerCase(); } export function validateLoginOrThrow(login) { const clean = normalizeLogin(login); if (!LOGIN_RE.test(clean)) { throw new Error('Логин должен содержать только a-z, 0-9, _ и быть длиной 1..20 символов'); } return clean; } export function parseLoginList(text) { return String(text || '') .split(/\r?\n/) .map((value) => value.trim().toLowerCase()) .filter(Boolean); } export function formatBigInt(value) { return BigInt(value || 0n).toString(10); } export function formatTimestamp(value) { const ts = Number(BigInt(value || 0n)); if (!Number.isFinite(ts) || ts <= 0) return '—'; return new Date(ts).toLocaleString('ru-RU'); } export function setStatus(node, text, kind = 'info') { node.className = `status ${kind}`; node.textContent = String(text || ''); } export function clearStatus(node) { node.className = 'status'; node.textContent = ''; } export function setGenMessage(node, text, kind) { node.className = `gen-msg ${kind}`; node.textContent = String(text || ''); } export function clearGenMessage(node) { node.className = 'gen-msg'; node.textContent = ''; } export function setupPasswordEye(button, input) { button.addEventListener('click', () => { const nextType = input.type === 'password' ? 'text' : 'password'; input.type = nextType; button.textContent = nextType === 'password' ? 'Показать' : 'Скрыть'; }); } function ensure32Bytes(bytes) { const input = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []); if (input.length > 32) throw new Error(`Ожидалось максимум 32 байта, получено ${input.length}`); if (input.length === 32) return input; const out = new Uint8Array(32); out.set(input, 32 - input.length); return out; } function pkcs8FromSeed32(seed32) { const seed = ensure32Bytes(seed32); const out = new Uint8Array(ED25519_PKCS8_PREFIX.length + seed.length); out.set(ED25519_PKCS8_PREFIX, 0); out.set(seed, ED25519_PKCS8_PREFIX.length); return out; } async function pairFromSeedBase58(seedB58, explicitPubB58) { const seed32 = ensure32Bytes(base58ToBytes(seedB58)); const privatePkcs8B64 = bytesToBase64(pkcs8FromSeed32(seed32)); const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(privatePkcs8B64); const actualPubB58 = bytesToBase58(base64ToBytes(publicKeyB64)); const expectedPubB58 = String(explicitPubB58 || '').trim(); if (expectedPubB58 && actualPubB58 !== expectedPubB58) { throw new Error(`Публичный ключ не совпадает с приватным seed: ${expectedPubB58}`); } return { publicKeyB64, privatePkcs8B64, publicKeyB58: actualPubB58, privateSeedB58: bytesToBase58(seed32), }; } export async function buildKeyBundleFromForm(fieldMap, options = {}) { const requireBlockchain = options.requireBlockchain !== false; const root = await pairFromSeedBase58($(fieldMap.rootPriv).value, $(fieldMap.rootPub).value); const device = await pairFromSeedBase58($(fieldMap.devPriv).value, $(fieldMap.devPub).value); const blockchainPriv = String($(fieldMap.bchPriv).value || '').trim(); const blockchainPub = String($(fieldMap.bchPub).value || '').trim(); const hasBlockchainInput = Boolean(blockchainPriv || blockchainPub); let blockchain = null; if (requireBlockchain || hasBlockchainInput) { blockchain = await pairFromSeedBase58(blockchainPriv, blockchainPub); } return { keyBundle: { rootPair: { publicKeyB64: root.publicKeyB64, privatePkcs8B64: root.privatePkcs8B64 }, blockchainPair: blockchain ? { publicKeyB64: blockchain.publicKeyB64, privatePkcs8B64: blockchain.privatePkcs8B64 } : null, devicePair: { publicKeyB64: device.publicKeyB64, privatePkcs8B64: device.privatePkcs8B64 }, }, normalized: { rootPubB58: root.publicKeyB58, rootPrivB58: root.privateSeedB58, bchPubB58: blockchain?.publicKeyB58 || '', bchPrivB58: blockchain?.privateSeedB58 || '', devPubB58: device.publicKeyB58, devPrivB58: device.privateSeedB58, }, }; } export async function deriveKeyBundleFromPassword({ login, password, onProgress }) { const cleanLogin = validateLoginOrThrow(login); const cleanPassword = String(password ?? ''); if (!cleanPassword) throw new Error('Введите пароль'); const masterSecret32 = await deriveMasterSecretFromPassword(cleanPassword, { login: cleanLogin, onProgress, }); const [rootPair, blockchainPair, devicePair] = await Promise.all([ deriveEd25519FromMasterSecret(masterSecret32, 'root.key'), deriveEd25519FromMasterSecret(masterSecret32, 'bch.key'), deriveEd25519FromMasterSecret(masterSecret32, 'dev.key'), ]); return { masterSecret32, keyBundle: { rootPair, blockchainPair, devicePair }, }; } export function fillKeyFields(fieldMap, keyBundle, masterSecret32) { if (masterSecret32) { $(fieldMap.masterSecret).value = bytesToBase58(masterSecret32); } $(fieldMap.rootPub).value = bytesToBase58(base64ToBytes(keyBundle.rootPair.publicKeyB64)); $(fieldMap.rootPriv).value = bytesToBase58(base64ToBytes(keyBundle.rootPair.privatePkcs8B64).slice(-32)); $(fieldMap.bchPub).value = bytesToBase58(base64ToBytes(keyBundle.blockchainPair.publicKeyB64)); $(fieldMap.bchPriv).value = bytesToBase58(base64ToBytes(keyBundle.blockchainPair.privatePkcs8B64).slice(-32)); $(fieldMap.devPub).value = bytesToBase58(base64ToBytes(keyBundle.devicePair.publicKeyB64)); $(fieldMap.devPriv).value = bytesToBase58(base64ToBytes(keyBundle.devicePair.privatePkcs8B64).slice(-32)); } export function updateSolAddress(fieldMap) { const box = $(fieldMap.solBox); const label = $(fieldMap.solAdr); const pubB58 = String($(fieldMap.devPub).value || '').trim(); if (!pubB58) { box.classList.remove('show'); label.textContent = ''; return; } try { ensure32Bytes(base58ToBytes(pubB58)); label.textContent = pubB58; box.classList.add('show'); } catch { box.classList.remove('show'); label.textContent = ''; } } export function wireDeviceAddressPreview(fieldMap) { const update = () => updateSolAddress(fieldMap); $(fieldMap.devPub).addEventListener('input', update); update(); } export function buildDevnetTopupUrl(walletAddress) { const cleanWallet = String(walletAddress || '').trim(); const url = new URL('../devnet-topup-view', window.location.href); if (cleanWallet) { url.searchParams.set('wallet', cleanWallet); } return url.toString(); } export function openDevnetTopup(walletAddress) { const cleanWallet = String(walletAddress || '').trim(); if (!cleanWallet) { throw new Error('Сначала укажите device-адрес'); } window.open(buildDevnetTopupUrl(cleanWallet), '_blank', 'noopener'); }