import { readShineUserPda, updateServerOnSolana } from '../../js/services/shine-user-pda-service.js'; import { $, buildKeyBundleFromForm, clearGenMessage, clearStatus, compareExpectedPublicKeys, deriveKeyBundleFromPassword, fillKeyFields, formatBigInt, formatTimestamp, openDevnetTopup, parseLoginList, publicKeyBytesToBase58, refreshDeviceBalance, setGenMessage, setStatus, setText, setupPasswordEye, summarizeKeyComparison, updateSolAddress, validateLoginOrThrow, wireDeviceAddressPreview, } from './server-ui-shared.js'; import { defaultServerAddress, defaultServerLogin } from '../../js/deploy-config.js'; const fieldMap = { masterSecret: 'masterSecret', recoveryPub: 'recoveryPub', recoveryPriv: 'recoveryPriv', rootPub: 'rootPub', rootPriv: 'rootPriv', bchPub: 'bchPub', bchPriv: 'bchPriv', devPub: 'devPub', devPriv: 'devPriv', solBox: 'solBox', solAdr: 'solAdr', }; let currentPda = null; function resetExpectedKeysUi() { $('expectedKeysBox').style.display = 'none'; setText('expectedRecoveryPub', ''); setText('expectedRootPub', ''); setText('expectedBchPub', ''); setText('expectedDevPub', ''); clearGenMessage($('expectedKeysStatus')); setText('deviceBalance', 'Баланс device ещё не запрашивался.'); } function renderExpectedKeys(parsed) { $('expectedKeysBox').style.display = 'block'; setText('expectedRecoveryPub', publicKeyBytesToBase58(parsed.recoveryKey)); setText('expectedRootPub', publicKeyBytesToBase58(parsed.rootKey)); setText('expectedBchPub', publicKeyBytesToBase58(parsed.blockchain.blockchainPublicKey)); setText('expectedDevPub', publicKeyBytesToBase58(parsed.clientKey)); setGenMessage($('expectedKeysStatus'), 'После генерации ключей этот блок покажет, совпадают ли они с уже записанной PDA.', 'warn'); } function compareCurrentFormKeysWithPda() { if (!currentPda) throw new Error('Сначала загрузите PDA'); const blockchainActual = String($('bchPub').value || '').trim(); return { resultMap: { recovery: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.recoveryKey), $('recoveryPub').value), root: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.rootKey), $('rootPub').value), blockchain: blockchainActual ? compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), blockchainActual) : { matches: true, expected: publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), actual: '' }, device: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.clientKey), $('devPub').value), }, }; } function renderComparisonStatus() { if (!currentPda) return false; try { const { resultMap } = compareCurrentFormKeysWithPda(); const summary = summarizeKeyComparison(resultMap); if (summary.allMatch) { setGenMessage($('expectedKeysStatus'), 'Ключи совпадают с загруженной PDA. Похоже, пароль верный.', 'ok'); return true; } setGenMessage( $('expectedKeysStatus'), `Не совпали ключи: ${summary.mismatches.join(', ')}. Похоже, пароль неверный или введены не те ключи.`, 'err', ); return false; } catch (error) { setGenMessage($('expectedKeysStatus'), error?.message || String(error), 'err'); return false; } } setupPasswordEye($('btnEye'), $('password')); wireDeviceAddressPreview(fieldMap); $('password').value = ''; $('login').placeholder = defaultServerLogin; $('serverAddress').placeholder = defaultServerAddress; resetExpectedKeysUi(); $('btnTopupDevnet').addEventListener('click', () => { try { openDevnetTopup($('devPub').value); } catch (error) { setStatus($('status'), error?.message || String(error), 'error'); } }); $('btnRefreshBalance').addEventListener('click', async () => { try { $('btnRefreshBalance').disabled = true; await refreshDeviceBalance({ endpoint: $('endpoint').value, deviceAddress: $('devPub').value, targetNode: $('deviceBalance'), }); } catch (error) { setStatus($('status'), error?.message || String(error), 'error'); } finally { $('btnRefreshBalance').disabled = false; } }); $('btnGen').addEventListener('click', async () => { clearGenMessage($('genMsg')); clearStatus($('status')); $('btnGen').disabled = true; try { const login = validateLoginOrThrow($('login').value); const password = $('password').value; const { masterSecret32, keyBundle } = await deriveKeyBundleFromPassword({ login, password }); fillKeyFields(fieldMap, keyBundle, masterSecret32); updateSolAddress(fieldMap); $('deviceBalance').textContent = 'Теперь можно проверить баланс device-аккаунта.'; const keysOk = renderComparisonStatus(); setGenMessage( $('genMsg'), keysOk ? 'Ключи и master secret сгенерированы из логина и пароля. Device-ключ оплатит транзакцию.' : 'Ключи сгенерированы, но не совпали с уже загруженной PDA. Скорее всего, пароль неверный.', keysOk ? 'ok' : 'err', ); } catch (error) { setGenMessage($('genMsg'), error?.message || String(error), 'err'); } finally { $('btnGen').disabled = false; } }); $('btnLoad').addEventListener('click', async () => { clearStatus($('status')); clearGenMessage($('genMsg')); $('btnLoad').disabled = true; currentPda = null; $('pdaInfo').style.display = 'none'; $('updateForm').style.display = 'none'; resetExpectedKeysUi(); try { const login = validateLoginOrThrow($('login').value); const endpoint = String($('endpoint').value || '').trim(); if (!endpoint) throw new Error('Укажите Solana endpoint'); setStatus($('status'), 'Загрузка PDA из Solana...', 'info'); const parsed = await readShineUserPda({ login, solanaEndpoint: endpoint }); if (!parsed.isServer) throw new Error('Эта PDA не является серверной'); currentPda = parsed; $('iAddr').textContent = parsed.pdaAddress; $('iVer').textContent = `#${parsed.recordNumber}`; $('iCreated').textContent = formatTimestamp(parsed.createdAtMs); $('iUpdated').textContent = formatTimestamp(parsed.updatedAtMs); $('iSrvAddr').textContent = parsed.serverAddress || '—'; $('iSync').textContent = parsed.syncServers.length ? parsed.syncServers.join(', ') : '—'; $('iBch').textContent = parsed.blockchain.blockchainName; $('iLimit').textContent = formatBigInt(parsed.blockchain.paidLimitBytes); $('password').value = ''; $('serverAddress').value = parsed.serverAddress || ''; $('syncServers').value = parsed.syncServers.join('\n'); $('pdaInfo').style.display = 'block'; $('updateForm').style.display = 'block'; renderExpectedKeys(parsed); setStatus($('status'), 'PDA загружена. Можно менять адрес или sync_servers.', 'success'); } catch (error) { setStatus($('status'), error?.message || String(error), 'error'); } finally { $('btnLoad').disabled = false; } }); $('btnUpdate').addEventListener('click', async () => { clearStatus($('status')); clearGenMessage($('genMsg')); $('btnUpdate').disabled = true; try { if (!currentPda) throw new Error('Сначала загрузите PDA'); const endpoint = String($('endpoint').value || '').trim(); if (!endpoint) throw new Error('Укажите Solana endpoint'); const serverAddress = String($('serverAddress').value || '').trim(); if (!serverAddress) throw new Error('Укажите адрес сервера'); setStatus($('status'), 'Проверка и сборка keyBundle...', 'info'); const { keyBundle, normalized } = await buildKeyBundleFromForm(fieldMap, { requireBlockchain: false }); $('rootPub').value = normalized.rootPubB58; $('rootPriv').value = normalized.rootPrivB58; $('bchPub').value = normalized.bchPubB58; $('bchPriv').value = normalized.bchPrivB58; $('devPub').value = normalized.devPubB58; $('devPriv').value = normalized.devPrivB58; updateSolAddress(fieldMap); if (!renderComparisonStatus()) { throw new Error('Ключи не совпадают с загруженной PDA. Проверьте пароль или введённые ключи.'); } setStatus($('status'), 'Отправка update_user_pda в Solana...', 'info'); const result = await updateServerOnSolana({ login: currentPda.login, keyBundle, serverAddress, addressFormatType: currentPda.addressFormatType ?? 1, addressFormatVersion: currentPda.addressFormatVersion ?? 0, syncServers: parseLoginList($('syncServers').value), solanaEndpoint: endpoint, }); setStatus( $('status'), `✓ PDA обновлена!\n\nЛогин: ${currentPda.login}\nPDA: ${result.pdaAddress}\nТранзакция: ${result.signature}`, 'success', ); currentPda = null; $('pdaInfo').style.display = 'none'; $('updateForm').style.display = 'none'; } catch (error) { setStatus($('status'), error?.message || String(error), 'error'); } finally { $('btnUpdate').disabled = false; } }); document.body.dataset.ready = '1';