SHiNE-server/shine-server-UI/create-server-pda.html

469 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>