Обновлён server UI и приватные ключи переведены в base58

This commit is contained in:
AidarKC
2026-06-02 15:52:22 +04:00
parent 67f882b9bc
commit a06b76b800
14 changed files with 2277 additions and 5 deletions
+468
View File
@@ -0,0 +1,468 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Регистрация сервера — SHiNE Server Admin</title>
<link rel="stylesheet" href="styles.css" />
<style>
.pwd-wrap { display: flex; }
.pwd-wrap input { flex: 1; border-radius: var(--radius) 0 0 var(--radius); }
.btn-eye { border: 1px solid var(--border); border-left: none; background: #0d0d0d;
color: var(--text-muted); border-radius: 0 var(--radius) var(--radius) 0;
padding: 0 16px; cursor: pointer; font-size: 13px; }
.btn-eye:hover { color: var(--accent); border-color: var(--accent); }
.gen-msg { font-size: 12px; margin-top: 8px; padding: 8px 12px; border-radius: var(--radius); display: none; }
.gen-msg.ok { display:block; background:#1a2e1a; border:1px solid #2a4a2a; color:#7dcc7d; }
.gen-msg.err { display:block; background:#2e1a1a; border:1px solid #5a2a2a; color:#f08080; }
.kp-title { font-size:11px; font-weight:700; color:var(--accent); text-transform:uppercase; letter-spacing:.06em; margin-bottom:8px; }
.kp-row { display:flex; gap:8px; align-items:flex-start; margin-bottom:6px; }
.kp-row:last-child { margin-bottom:0; }
.kp-lbl { font-size:11px; color:var(--text-muted); min-width:60px; padding-top:10px; }
.kp-inp { flex:1; font-size:11px; font-family:monospace; padding:8px 10px; }
.kp-block { margin-bottom:14px; padding-bottom:14px; border-bottom:1px solid var(--border); }
.kp-block:last-child { border-bottom:none; margin-bottom:0; padding-bottom:0; }
.sec-lbl { font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.06em; margin:16px 0 10px; }
.sol-box { margin-top:14px; background:#0d1a0d; border:1px solid #2a4a2a; border-radius:var(--radius); padding:10px 14px; display:none; }
.sol-box.show { display:block; }
.sol-ttl { font-size:12px; font-weight:600; color:#7dcc7d; }
.sol-adr { font-family:monospace; font-size:12px; word-break:break-all; margin-top:4px; }
.sol-ht { font-size:11px; color:var(--text-muted); margin-top:4px; }
</style>
</head>
<body>
<div class="container">
<div class="nav-links">
<a href="index.html">← Назад</a>
<a href="update-server-pda.html">Обновить PDA</a>
</div>
<h1>Регистрация серверного аккаунта</h1>
<p class="subtitle">Создаёт user_pda в Solana с флагом is_server=true</p>
<div class="card">
<h2>Параметры Solana</h2>
<div class="field">
<label>Solana Endpoint</label>
<input type="text" id="endpoint" value="https://api.devnet.solana.com" />
<div class="hint">devnet: https://api.devnet.solana.com · mainnet: https://api.mainnet-beta.solana.com</div>
</div>
</div>
<div class="card">
<h2>Данные сервера</h2>
<div class="field">
<label>Логин сервера</label>
<input type="text" id="login" placeholder="shineupme" maxlength="20" />
<div class="hint">Только a-z, 0-9, _ · без точки · макс. 20 символов</div>
</div>
<div class="field">
<label>Адрес сервера (URL)</label>
<input type="text" id="serverAddress" placeholder="https://shineup.me/ws" />
</div>
<div class="field">
<label>Серверы синхронизации (sync_servers)</label>
<textarea id="syncServers" placeholder="По одному логину на строку (можно оставить пустым)"></textarea>
</div>
<div class="field">
<label>Серверы доступа (access_servers, опционально)</label>
<textarea id="accessServers" placeholder="Обычно пусто для серверного PDA"></textarea>
</div>
</div>
<div class="card">
<h2>Ключи сервера</h2>
<div class="field">
<label>Пароль</label>
<div class="pwd-wrap">
<input type="password" id="password" placeholder="Пароль аккаунта сервера" autocomplete="new-password" />
<button class="btn-eye" id="btnEye" type="button">Показать</button>
</div>
<div class="hint">Нажмите «Сгенерировать» — поля ниже заполнятся из логина + пароля (Argon2id).<br/>Или введите ключи вручную.</div>
</div>
<div class="btn-row">
<button class="btn-secondary" id="btnGen" type="button">Сгенерировать ключи</button>
</div>
<div class="gen-msg" id="genMsg"></div>
<div class="sec-lbl">Секрет (master secret, base58)</div>
<div class="field" style="margin-bottom:0">
<input type="text" id="masterSecret" placeholder="32-байтовый master secret в base58 (~44 символа)" />
</div>
<div class="sec-lbl">Ключевые пары (base58)</div>
<div class="kp-block">
<div class="kp-title">Root Key — подпись PDA-записи</div>
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="rootPub" placeholder="base58, ~44 символа" /></div>
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="rootPriv" placeholder="seed base58, ~44 символа" /></div>
</div>
<div class="kp-block">
<div class="kp-title">Blockchain Key — подпись LastBlockState</div>
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="bchPub" placeholder="base58, ~44 символа" /></div>
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="bchPriv" placeholder="seed base58, ~44 символа" /></div>
</div>
<div class="kp-block">
<div class="kp-title">Device Key — оплата транзакции Solana</div>
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="devPub" placeholder="base58, ~44 символа (= Solana-адрес)" /></div>
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="devPriv" placeholder="seed base58, ~44 символа" /></div>
<div class="sol-box" id="solBox">
<div class="sol-ttl">Положите SOL на этот адрес перед регистрацией:</div>
<div class="sol-adr" id="solAdr"></div>
<div class="sol-ht">Это Solana-адрес device-ключа (public key в base58 = Solana-адрес). С него оплачивается создание PDA.</div>
</div>
</div>
</div>
<div class="btn-row">
<button class="btn-primary" id="btnCreate">Зарегистрировать сервер</button>
</div>
<div class="status" id="status"></div>
</div>
<script type="module">
// ===== CDN-загрузчики (единственные внешние зависимости) =====
let _sol = null, _arg = null;
const loadSol = async () => { if (!_sol) _sol = await import('https://esm.sh/@solana/web3.js@1.98.4?bundle'); return _sol; };
const loadArg = async () => { if (!_arg) _arg = await import('https://esm.sh/@noble/hashes@1.8.0/argon2.js'); return _arg; };
// ===== Константы =====
const USERS_PGM = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm';
const PAY_PGM = 'm48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR';
const GUARD_PGM = '3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo';
const ED25519_PGM = 'Ed25519SigVerify111111111111111111111111111';
const SYSVAR_IX = 'Sysvar1nstructions1111111111111111111111111';
const CREATE_DISC = new Uint8Array([139,157,13,41,142,174,226,214]);
// ===== Crypto =====
const sha256 = async b => new Uint8Array(await crypto.subtle.digest('SHA-256', b));
async function signEd25519(pkcs8B64, msg) {
const pkcs8 = b64ToBytes(pkcs8B64);
const key = await crypto.subtle.importKey('pkcs8', pkcs8, { name:'Ed25519' }, false, ['sign']);
return new Uint8Array(await crypto.subtle.sign({ name:'Ed25519' }, key, msg));
}
const b64ToBytes = b64 => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const bytesToB64 = b => btoa(String.fromCharCode(...b));
const b64urlToStd = s => { const n = s.replace(/-/g,'+').replace(/_/g,'/'); return n+'='.repeat((4-n.length%4)%4); };
const extractSeed32 = pkcs8B64 => b64ToBytes(pkcs8B64).slice(16, 48);
async function anchorDisc(name) {
return (await sha256(new TextEncoder().encode(`global:${name}`))).slice(0,8);
}
// ===== Base58 =====
function base58Enc(bytes) {
const A = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let n = 0n; for (const b of bytes) n = (n<<8n)|BigInt(b);
let r = ''; while (n>0n) { r = A[Number(n%58n)]+r; n/=58n; }
for (const b of bytes) { if (b!==0) break; r='1'+r; }
return r;
}
function b58Dec(s) {
const A = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let n = 0n;
for (const c of s) {
const i = A.indexOf(c);
if (i < 0) throw new Error('Недопустимый символ base58: ' + c);
n = n * 58n + BigInt(i);
}
let hex = n.toString(16); if (hex.length % 2) hex = '0' + hex;
const res = []; for (let i = 0; i < hex.length; i += 2) res.push(parseInt(hex.slice(i,i+2),16));
let zeros = 0; for (const c of s) { if (c !== '1') break; zeros++; }
return new Uint8Array(zeros + res.length).fill(0).map((_, i) => i < zeros ? 0 : res[i - zeros]);
}
// Обеспечивает ровно 32 байта (дополняет нулями слева при необходимости)
function to32(bytes) {
if (bytes.length === 32) return bytes;
if (bytes.length > 32) throw new Error(`Ожидалось 32 байта, получено ${bytes.length}`);
const out = new Uint8Array(32); out.set(bytes, 32 - bytes.length); return out;
}
// Подпись с использованием 32-байтового seed в base58
async function signWithSeedB58(seedB58, msg) {
const seed32 = to32(b58Dec(seedB58));
const pkcs8 = pkcs8FromSeed(seed32);
const key = await crypto.subtle.importKey('pkcs8', pkcs8, { name:'Ed25519' }, false, ['sign']);
return new Uint8Array(await crypto.subtle.sign({ name:'Ed25519' }, key, msg));
}
// ===== Borsh =====
const p32le = (buf,v) => { const n=v>>>0; buf.push(n&0xFF,(n>>8)&0xFF,(n>>16)&0xFF,(n>>24)&0xFF); };
const p64le = (buf,v) => { const b=typeof v==='bigint'?v:BigInt(v); p32le(buf,Number(b&0xFFFFFFFFn)>>>0); p32le(buf,Number((b>>32n)&0xFFFFFFFFn)>>>0); };
class BB {
constructor(){ this._b=[]; }
u8(v){this._b.push(v&0xFF);}
u32(v){p32le(this._b,v);}
u64(v){p64le(this._b,v);}
bool(v){this.u8(v?1:0);}
b32(b){for(const x of b)this._b.push(x);}
vu8(b){this.u32(b.length);for(const x of b)this._b.push(x);}
str(s){const e=new TextEncoder().encode(s);this.u32(e.length);for(const x of e)this._b.push(x);}
vstr(a){this.u32(a.length);for(const s of a)this.str(s);}
raw(b){for(const x of b)this._b.push(x);}
done(){return new Uint8Array(this._b);}
}
// ===== Бинарный формат PDA =====
function lbsBytes(login, bchName, blockNum, blockHash32, usedBytes) {
const enc = new TextEncoder();
const buf = [...enc.encode('SHiNE_LAST_BLOCK')];
const lb = enc.encode(login); buf.push(lb.length,...lb);
const bb = enc.encode(bchName); buf.push(bb.length,...bb);
p32le(buf,blockNum);
for (const x of blockHash32) buf.push(x);
p64le(buf,usedBytes);
return new Uint8Array(buf);
}
function buildServerRecord({ login,createdAtMs,updatedAtMs,recordNum,prevHash32,rootKey32,devKey32,bchKey32,bchName,paidLimit,usedBytes,blockNum,blockHash32,blockSig64,arweaveId,srvAddr,fmtType,fmtVer,syncSrvs,accSrvs,trusted }) {
const enc = new TextEncoder();
const lB = enc.encode(login), bB = enc.encode(bchName);
const buf = [0x53,0x48,0x69,0x4E,0x45,1,0,0,0];
p64le(buf,createdAtMs); p64le(buf,updatedAtMs); p32le(buf,recordNum);
for (const x of prevHash32) buf.push(x);
buf.push(lB.length,...lB);
buf.push(6); // 6 блоков (сервер)
buf.push(1,0,...rootKey32); // RootKeyBlock
buf.push(2,0,...devKey32); // DeviceKeyBlock
buf.push(3,0,1,1,bB.length,...bB,...bchKey32); // BlockchainRegistryBlock
p64le(buf,paidLimit); p64le(buf,usedBytes); p32le(buf,blockNum);
for (const x of blockHash32) buf.push(x);
for (const x of blockSig64) buf.push(x);
if (arweaveId) { buf.push(1); const a=enc.encode(arweaveId); buf.push(a.length,...a); } else buf.push(0);
// ServerProfileBlock
buf.push(30,0,1,fmtType&0xFF,fmtVer&0xFF);
const sB=enc.encode(srvAddr); buf.push(sB.length,...sB);
buf.push(syncSrvs.length); for (const s of syncSrvs){const x=enc.encode(s);buf.push(x.length,...x);}
// AccessServersBlock
buf.push(40,0,accSrvs.length); for (const s of accSrvs){const x=enc.encode(s);buf.push(x.length,...x);}
// TrustedStateBlock
buf.push(50,0,trusted&0xFF);
const recLen=buf.length+64; buf[7]=recLen&0xFF; buf[8]=(recLen>>8)&0xFF;
return new Uint8Array(buf);
}
function serializeCreateArgs({ login,rootKey32,createdAtMs,devKey32,bchKey32,bchName,usedBytes,blockNum,blockHash32,blockSig64,arweaveId,srvAddr,fmtType,fmtVer,syncSrvs,accSrvs,trusted,rootSig64 }) {
const b = new BB();
b.raw(CREATE_DISC); b.str(login); b.b32(rootKey32); b.u64(createdAtMs); b.u64(0n);
b.b32(devKey32); b.b32(bchKey32); b.str(bchName);
b.u64(usedBytes); b.u32(blockNum); b.vu8(blockHash32); b.vu8(blockSig64); b.str(arweaveId);
b.bool(true); b.u8(fmtType); b.u8(fmtVer); b.str(srvAddr);
b.vstr(syncSrvs); b.vstr(accSrvs); b.u8(trusted); b.vu8(rootSig64);
return b.done();
}
function buildEd25519Ix(sig64, pub32, msgHash32) {
const d = new Uint8Array(144); const v = new DataView(d.buffer);
d[0]=1; d[1]=0;
v.setUint16(2,16,true); v.setUint16(4,0xFFFF,true);
v.setUint16(6,80,true); v.setUint16(8,0xFFFF,true);
v.setUint16(10,112,true); v.setUint16(12,32,true); v.setUint16(14,0xFFFF,true);
d.set(sig64,16); d.set(pub32,80); d.set(msgHash32,112);
return d;
}
function readEcoLimit(data) {
return new DataView(data.buffer,data.byteOffset,data.byteLength).getBigUint64(17,true);
}
// ===== Деривация ключей из пароля =====
function pkcs8FromSeed(seed32) {
const p = new Uint8Array([0x30,0x2e,0x02,0x01,0x00,0x30,0x05,0x06,0x03,0x2b,0x65,0x70,0x04,0x22,0x04,0x20]);
const out = new Uint8Array(48); out.set(p); out.set(seed32,16); return out;
}
async function deriveEd25519FromMaster(master32, suffix) {
const material = `${bytesToB64(master32)}|${suffix}`;
const seed32 = await sha256(new TextEncoder().encode(material)); // 32-байтовый seed
const pkcs8 = pkcs8FromSeed(seed32);
const key = await crypto.subtle.importKey('pkcs8', pkcs8, { name:'Ed25519' }, true, ['sign']);
const jwk = await crypto.subtle.exportKey('jwk', key);
if (!jwk.x) throw new Error(`Нет публичного ключа для ${suffix}`);
const pubBytes = b64ToBytes(b64urlToStd(jwk.x)); // 32 байта raw Ed25519 public key
return { publicKeyB58: base58Enc(pubBytes), privateSeedB58: base58Enc(seed32) };
}
async function deriveKeyBundle(login, password) {
const { argon2idAsync } = await loadArg();
const enc = new TextEncoder();
const loginNorm = login.trim().toLowerCase();
const saltSrc = `shine-auth-v2|login=${loginNorm}|suffix=master.secret`;
const salt = (await sha256(enc.encode(saltSrc))).slice(0,16);
const pass = enc.encode(`${loginNorm}\n${password}`);
const raw = await argon2idAsync(pass, salt, { t:2, m:65536, p:1, dkLen:32 });
const master32 = new Uint8Array(raw);
const [rootPair, blockchainPair, devicePair] = await Promise.all([
deriveEd25519FromMaster(master32,'root.key'),
deriveEd25519FromMaster(master32,'bch.key'),
deriveEd25519FromMaster(master32,'dev.key'),
]);
return { masterSecretB58: base58Enc(master32), rootPair, blockchainPair, devicePair };
}
// ===== Регистрация сервера =====
async function registerServer({ login, rootPub, rootPriv, bchPub, bchPriv, devPub, devPriv, srvAddr, syncSrvs, accSrvs, endpoint }) {
const sol = await loadSol();
const conn = new sol.Connection(endpoint, 'confirmed');
const enc = new TextEncoder();
const loginNorm = login.trim().toLowerCase();
const bchName = `${loginNorm}-001`;
const zero32 = new Uint8Array(32);
const uPgm = new sol.PublicKey(USERS_PGM);
const [pda] = sol.PublicKey.findProgramAddressSync([enc.encode('login='),enc.encode(loginNorm)],uPgm);
const [eco] = sol.PublicKey.findProgramAddressSync([enc.encode('shine_users_economy_config')],uPgm);
const [infl] = sol.PublicKey.findProgramAddressSync([enc.encode('shine_payments_inflow_vault')],new sol.PublicKey(PAY_PGM));
const guard = new sol.PublicKey(GUARD_PGM);
const ed25519= new sol.PublicKey(ED25519_PGM);
const sysIx = new sol.PublicKey(SYSVAR_IX);
const rootKey32 = to32(b58Dec(rootPub)); // pub: base58 → 32 bytes
const bchKey32 = to32(b58Dec(bchPub));
const devKey32 = to32(b58Dec(devPub));
const devKp = sol.Keypair.fromSeed(to32(b58Dec(devPriv))); // priv: seed base58 → 32 bytes
const ecoAcc = await conn.getAccountInfo(eco);
if (!ecoAcc) throw new Error('Economy config не инициализирован — запустите init_users_economy_config');
const paidLimit = readEcoLimit(ecoAcc.data);
const createdAtMs = BigInt(Date.now());
const lbsMsg = lbsBytes(loginNorm, bchName, 0, zero32, 0n);
const lbsHash = await sha256(lbsMsg);
const bchSig = await signWithSeedB58(bchPriv, lbsHash);
const unsignedRec = buildServerRecord({
login:loginNorm, createdAtMs, updatedAtMs:createdAtMs, recordNum:0, prevHash32:zero32,
rootKey32, devKey32, bchKey32, bchName,
paidLimit, usedBytes:0n, blockNum:0, blockHash32:zero32, blockSig64:bchSig, arweaveId:'',
srvAddr, fmtType:1, fmtVer:0, syncSrvs, accSrvs, trusted:0,
});
const recHash = await sha256(unsignedRec);
const rootSig = await signWithSeedB58(rootPriv, recHash);
const ixData = serializeCreateArgs({
login:loginNorm, rootKey32, createdAtMs, devKey32, bchKey32, bchName,
usedBytes:0n, blockNum:0, blockHash32:zero32, blockSig64:bchSig, arweaveId:'',
srvAddr, fmtType:1, fmtVer:0, syncSrvs, accSrvs, trusted:0, rootSig64:rootSig,
});
const tx = new sol.Transaction().add(
new sol.TransactionInstruction({ programId:ed25519, keys:[], data:buildEd25519Ix(rootSig,rootKey32,recHash) }),
new sol.TransactionInstruction({ programId:ed25519, keys:[], data:buildEd25519Ix(bchSig,bchKey32,lbsHash) }),
new sol.TransactionInstruction({
programId: uPgm,
keys:[
{pubkey:devKp.publicKey,isSigner:true,isWritable:true},
{pubkey:pda,isSigner:false,isWritable:true},
{pubkey:sol.SystemProgram.programId,isSigner:false,isWritable:false},
{pubkey:infl,isSigner:false,isWritable:true},
{pubkey:sysIx,isSigner:false,isWritable:false},
{pubkey:eco,isSigner:false,isWritable:false},
{pubkey:guard,isSigner:false,isWritable:false},
],
data:ixData,
}),
);
const sig = await sol.sendAndConfirmTransaction(conn, tx, [devKp], { commitment:'confirmed' });
return { sig, pdaAddr: pda.toBase58(), bchName };
}
// ===== UI =====
const $ = id => document.getElementById(id);
const statusEl = $('status');
const genMsgEl = $('genMsg');
function setStatus(txt, cls) { statusEl.className=`status ${cls}`; statusEl.textContent=txt; }
function setGenMsg(txt, cls) { genMsgEl.className=`gen-msg ${cls}`; genMsgEl.textContent=txt; }
// Показать/скрыть пароль
$('btnEye').addEventListener('click', () => {
const inp = $('password');
const vis = inp.type === 'password';
inp.type = vis ? 'text' : 'password';
$('btnEye').textContent = vis ? 'Скрыть' : 'Показать';
});
// Solana-адрес = device public key в base58 (это одно и то же)
function refreshSolAddr() {
const b58 = $('devPub').value.trim();
if (!b58) { $('solBox').classList.remove('show'); return; }
try {
to32(b58Dec(b58)); // проверяем что это корректный 32-байтовый base58
$('solAdr').textContent = b58; // device pub key в base58 = Solana адрес
$('solBox').classList.add('show');
} catch { $('solBox').classList.remove('show'); }
}
$('devPub').addEventListener('input', refreshSolAddr);
// Генерация ключей
$('btnGen').addEventListener('click', async () => {
const login = $('login').value.trim().toLowerCase();
const pwd = $('password').value;
if (!login) { setGenMsg('Сначала введите логин сервера', 'err'); return; }
if (!pwd) { setGenMsg('Введите пароль', 'err'); return; }
$('btnGen').disabled = true;
genMsgEl.className = 'gen-msg';
try {
const r = await deriveKeyBundle(login, pwd);
$('masterSecret').value = r.masterSecretB58;
$('rootPub').value = r.rootPair.publicKeyB58;
$('rootPriv').value = r.rootPair.privateSeedB58;
$('bchPub').value = r.blockchainPair.publicKeyB58;
$('bchPriv').value = r.blockchainPair.privateSeedB58;
$('devPub').value = r.devicePair.publicKeyB58;
$('devPriv').value = r.devicePair.privateSeedB58;
refreshSolAddr();
setGenMsg('✓ Ключи сгенерированы успешно', 'ok');
} catch(e) {
setGenMsg('Ошибка: ' + (e.message || String(e)), 'err');
} finally {
$('btnGen').disabled = false;
}
});
// Регистрация
const parseLogins = raw => raw.split('\n').map(s=>s.trim().toLowerCase()).filter(Boolean);
$('btnCreate').addEventListener('click', async () => {
const btn = $('btnCreate');
btn.disabled = true;
setStatus('Подготовка...', 'info');
try {
const login = $('login').value.trim().toLowerCase();
if (!login) throw new Error('Введите логин сервера');
const srvAddr = $('serverAddress').value.trim();
if (!srvAddr) throw new Error('Введите адрес сервера');
if (!$('rootPriv').value.trim()) throw new Error('Root Key приватный не заполнен');
if (!$('bchPriv').value.trim()) throw new Error('Blockchain Key приватный не заполнен');
if (!$('devPriv').value.trim()) throw new Error('Device Key приватный не заполнен');
setStatus('Загрузка Solana SDK и отправка транзакции...', 'info');
const res = await registerServer({
login,
rootPub: $('rootPub').value.trim(), rootPriv: $('rootPriv').value.trim(),
bchPub: $('bchPub').value.trim(), bchPriv: $('bchPriv').value.trim(),
devPub: $('devPub').value.trim(), devPriv: $('devPriv').value.trim(),
srvAddr,
syncSrvs: parseLogins($('syncServers').value),
accSrvs: parseLogins($('accessServers').value),
endpoint: $('endpoint').value.trim(),
});
setStatus(`✓ Сервер зарегистрирован!\n\nЛогин: ${login}\nPDA: ${res.pdaAddr}\nBlockchain: ${res.bchName}\nТранзакция: ${res.sig}`, 'success');
} catch(e) {
setStatus('Ошибка: ' + (e.message || String(e)), 'error');
} finally {
btn.disabled = false;
}
});
</script>
</body>
</html>