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

485 lines
25 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>Обновление PDA сервера — 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; }
.muted { font-size:12px; color:var(--text-muted); margin-bottom:14px; line-height:1.6; }
</style>
</head>
<body>
<div class="container">
<div class="nav-links">
<a href="index.html">← Назад</a>
<a href="create-server-pda.html">Создать PDA</a>
</div>
<h1>Обновление PDA сервера</h1>
<p class="subtitle">Меняет адрес сервера или список серверов синхронизации</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>
</div>
<div class="card">
<h2>Загрузить существующую PDA</h2>
<div class="field">
<label>Логин сервера</label>
<input type="text" id="login" placeholder="shineupme" maxlength="20" />
</div>
<div class="btn-row">
<button class="btn-secondary" id="btnLoad">Загрузить PDA</button>
</div>
<div class="pda-info" id="pdaInfo">
<hr class="section-divider" />
<div class="pda-row"><span class="pda-key">PDA адрес</span><span class="pda-value" id="iAddr"></span></div>
<div class="pda-row"><span class="pda-key">Версия</span><span class="pda-value" id="iVer"></span></div>
<div class="pda-row"><span class="pda-key">Создан</span><span class="pda-value" id="iCreated"></span></div>
<div class="pda-row"><span class="pda-key">Обновлён</span><span class="pda-value" id="iUpdated"></span></div>
<div class="pda-row"><span class="pda-key">Адрес сервера</span><span class="pda-value" id="iSrvAddr"></span></div>
<div class="pda-row"><span class="pda-key">sync_servers</span><span class="pda-value" id="iSync"></span></div>
<div class="pda-row"><span class="pda-key">Blockchain</span><span class="pda-value" id="iBch"></span></div>
<div class="pda-row"><span class="pda-key">Paid limit</span><span class="pda-value" id="iLimit"></span></div>
</div>
</div>
<div id="updateForm" style="display:none">
<div class="card">
<h2>Новые параметры сервера</h2>
<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>
<div class="card">
<h2>Ключи для подписи и оплаты</h2>
<p class="muted">Root-ключ подписывает новую PDA-запись. Device-ключ оплачивает транзакцию.<br/>Blockchain-ключ не нужен — подпись LastBlockState из PDA переиспользуется автоматически.</p>
<div class="field">
<label>Пароль</label>
<div class="pwd-wrap">
<input type="password" id="password" placeholder="Пароль аккаунта сервера" autocomplete="current-password" />
<button class="btn-eye" id="btnEye" type="button">Показать</button>
</div>
<div class="hint">Нажмите «Сгенерировать» — поля ниже заполнятся из логина + пароля.<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 — справочно, при обновлении не используется</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-адрес (base58) device-ключа. С него оплачивается транзакция.</div>
</div>
</div>
</div>
<div class="btn-row">
<button class="btn-primary" id="btnUpdate">Обновить PDA</button>
</div>
</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 ED25519_PGM='Ed25519SigVerify111111111111111111111111111';
const SYSVAR_IX='Sysvar1nstructions1111111111111111111111111';
// ===== Crypto =====
const sha256 = async b => new Uint8Array(await crypto.subtle.digest('SHA-256',b));
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); };
async function anchorDisc(name) {
return (await sha256(new TextEncoder().encode(`global:${name}`))).slice(0,8);
}
// ===== Base58 =====
function b58Enc(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 bytes = [];
for (let i = 0; i < hex.length; i += 2) bytes.push(parseInt(hex.slice(i, i + 2), 16));
let zeros = 0;
for (const c of s) {
if (c !== '1') break;
zeros++;
}
return Uint8Array.from([...new Array(zeros).fill(0), ...bytes]);
}
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;
}
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 signWithSeedB58(seedB58, msg) {
const seed32 = to32(b58Dec(seedB58));
const key = await crypto.subtle.importKey('pkcs8',pkcs8FromSeed(seed32),{name:'Ed25519'},false,['sign']);
return new Uint8Array(await crypto.subtle.sign({name:'Ed25519'},key,msg));
}
// ===== Borsh =====
const p32=(buf,v)=>{const n=v>>>0;buf.push(n&0xFF,(n>>8)&0xFF,(n>>16)&0xFF,(n>>24)&0xFF);};
const p64=(buf,v)=>{const b=typeof v==='bigint'?v:BigInt(v);p32(buf,Number(b&0xFFFFFFFFn)>>>0);p32(buf,Number((b>>32n)&0xFFFFFFFFn)>>>0);};
class BB{
constructor(){this._b=[];}
u8(v){this._b.push(v&0xFF);}u32(v){p32(this._b,v);}u64(v){p64(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 builder =====
function lbsBytes(login,bchName,blockNum,blockHash,usedBytes){
const enc=new TextEncoder(),buf=[...enc.encode('SHiNE_LAST_BLOCK')];
const l=enc.encode(login);buf.push(l.length,...l);
const b=enc.encode(bchName);buf.push(b.length,...b);
p32(buf,blockNum);for(const x of blockHash)buf.push(x);p64(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(),lB=enc.encode(login),bB=enc.encode(bchName);
const buf=[0x53,0x48,0x69,0x4E,0x45,1,0,0,0];
p64(buf,createdAtMs);p64(buf,updatedAtMs);p32(buf,recordNum);
for(const x of prevHash32)buf.push(x);
buf.push(lB.length,...lB);buf.push(6);
buf.push(1,0,...rootKey32);buf.push(2,0,...devKey32);
buf.push(3,0,1,1,bB.length,...bB,...bchKey32);
p64(buf,paidLimit);p64(buf,usedBytes);p32(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);
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);}
buf.push(40,0,accSrvs.length);for(const s of accSrvs){const x=enc.encode(s);buf.push(x.length,...x);}
buf.push(50,0,trusted&0xFF);
const rl=buf.length+64;buf[7]=rl&0xFF;buf[8]=(rl>>8)&0xFF;
return new Uint8Array(buf);
}
function buildEd25519Ix(sig64,pub32,msgHash){
const d=new Uint8Array(144),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(msgHash,112);return d;
}
// ===== PDA parser =====
function parsePda(raw){
const d=raw instanceof Uint8Array?raw:new Uint8Array(raw);
if(d.length<9||String.fromCharCode(d[0],d[1],d[2],d[3],d[4])!=='SHiNE')throw new Error('Неверный magic в PDA');
const view=new DataView(d.buffer,d.byteOffset);
const recLen=view.getUint16(7,true);
if(recLen<73||recLen>d.length)throw new Error('Неверный record_len');
const unsignedBytes=d.slice(0,recLen-64);
let cur=9;
const ru8=()=>d[cur++];
const ru32=()=>{const v=view.getUint32(cur,true);cur+=4;return v;};
const ru64=()=>{const v=view.getBigUint64(cur,true);cur+=8;return v;};
const rB=n=>{const s=d.slice(cur,cur+n);cur+=n;return s;};
const rStr=()=>{const l=ru8();return new TextDecoder().decode(rB(l));};
const createdAtMs=ru64(),updatedAtMs=ru64(),recordNumber=ru32();
rB(32); // prevHash
const login=rStr(),blocksCount=ru8();
let rootKey32=null,devKey32=null,bchData=null,srvData=null,accSrvs=[],trusted=0,isServer=false;
for(let i=0;i<blocksCount;i++){
const bt=ru8();ru8();
if(bt===1)rootKey32=rB(32);
else if(bt===2)devKey32=rB(32);
else if(bt===3){
ru8();const bt2=ru8(),bchName=rStr(),bchPub=rB(32);
const paidLimit=ru64(),usedBytes=ru64(),blockNum=ru32();
const blockHash=rB(32),blockSig=rB(64),ap=ru8();
const arweaveId=ap===1?rStr():'';
bchData={bchName,bchPub,paidLimit,usedBytes,blockNum,blockHash,blockSig,arweaveId};
}else if(bt===30){
if(ru8()===1){isServer=true;const ft=ru8(),fv=ru8(),sa=rStr(),sc=ru8();
const ss=[];for(let j=0;j<sc;j++)ss.push(rStr());
srvData={fmtType:ft,fmtVer:fv,srvAddr:sa,syncSrvs:ss};}
}else if(bt===40){const c=ru8();for(let j=0;j<c;j++)accSrvs.push(rStr());}
else if(bt===50)trusted=ru8();
}
const sig=d.slice(cur,cur+64);
return{recLen,unsignedBytes,createdAtMs,updatedAtMs,recordNumber,login,rootKey32,devKey32,bchData,isServer,srvData,accSrvs,trusted,sig};
}
// ===== Деривация ключей =====
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 deriveEd25519(master32,suffix){
const material=`${bytesToB64(master32)}|${suffix}`;
const seed=await sha256(new TextEncoder().encode(material));
const pkcs8=pkcs8FromSeed(seed);
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(`Нет pub для ${suffix}`);
return{publicKeyB58:b58Enc(b64ToBytes(b64urlToStd(jwk.x))),privateSeedB58:b58Enc(seed)};
}
async function deriveKeyBundle(login,pwd){
const{argon2idAsync}=await loadArg();
const enc=new TextEncoder(),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 raw=await argon2idAsync(enc.encode(`${loginNorm}\n${pwd}`),salt,{t:2,m:65536,p:1,dkLen:32});
const m32=new Uint8Array(raw);
const[rootPair,blockchainPair,devicePair]=await Promise.all([
deriveEd25519(m32,'root.key'),deriveEd25519(m32,'bch.key'),deriveEd25519(m32,'dev.key'),
]);
return{masterSecretB58:b58Enc(m32),rootPair,blockchainPair,devicePair};
}
// ===== Обновление PDA =====
async function updateServer({login,rootPriv,bchPriv,devPriv,srvAddr,syncSrvs,fmtType,fmtVer,endpoint}){
const sol=await loadSol();
const conn=new sol.Connection(endpoint,'confirmed'),enc=new TextEncoder();
const loginNorm=login.trim().toLowerCase();
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 ed25519=new sol.PublicKey(ED25519_PGM),sysIx=new sol.PublicKey(SYSVAR_IX);
const ai=await conn.getAccountInfo(pda,'confirmed');
if(!ai)throw new Error(`PDA не найдена для логина «${loginNorm}»`);
const p=parsePda(ai.data);
if(!p.isServer)throw new Error('Эта PDA не является серверной');
const bch=p.bchData;
const devSeed=to32(b58Dec(devPriv)),devKp=sol.Keypair.fromSeed(devSeed);
const prevHash=await sha256(p.unsignedBytes);
const updatedAtMs=BigInt(Date.now()),newVer=p.recordNumber+1;
const lbs=lbsBytes(loginNorm,bch.bchName,bch.blockNum,bch.blockHash,bch.usedBytes);
const lbsHash=await sha256(lbs);
const bchSig=String(bchPriv || '').trim() ? await signWithSeedB58(bchPriv.trim(), lbsHash) : bch.blockSig;
const unsignedRec=buildServerRecord({
login:loginNorm,createdAtMs:p.createdAtMs,updatedAtMs,recordNum:newVer,prevHash32:prevHash,
rootKey32:p.rootKey32,devKey32:p.devKey32,bchKey32:bch.bchPub,bchName:bch.bchName,
paidLimit:bch.paidLimit,usedBytes:bch.usedBytes,blockNum:bch.blockNum,
blockHash32:bch.blockHash,blockSig64:bchSig,arweaveId:bch.arweaveId,
srvAddr,fmtType,fmtVer,syncSrvs,accSrvs:p.accSrvs,trusted:p.trusted,
});
const recHash=await sha256(unsignedRec);
const rootSig=await signWithSeedB58(rootPriv,recHash);
const disc=await anchorDisc('update_user_pda');
const b=new BB();
b.raw(disc);b.str(loginNorm);b.b32(p.rootKey32);b.u64(p.createdAtMs);b.u64(updatedAtMs);
b.u32(newVer);b.vu8(prevHash);b.u64(0n);
b.b32(p.devKey32);b.b32(bch.bchPub);b.str(bch.bchName);
b.u64(bch.usedBytes);b.u32(bch.blockNum);b.vu8(bch.blockHash);b.vu8(bchSig);b.str(bch.arweaveId);
b.bool(true);b.u8(fmtType);b.u8(fmtVer);b.str(srvAddr);
b.vstr(syncSrvs);b.vstr(p.accSrvs);b.u8(p.trusted);b.vu8(rootSig);
const ixData=b.done();
const tx=new sol.Transaction().add(
new sol.TransactionInstruction({programId:ed25519,keys:[],data:buildEd25519Ix(rootSig,p.rootKey32,recHash)}),
new sol.TransactionInstruction({programId:ed25519,keys:[],data:buildEd25519Ix(bchSig,bch.bchPub,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},
],
data:ixData,
}),
);
const sig=await sol.sendAndConfirmTransaction(conn,tx,[devKp],{commitment:'confirmed'});
return{sig,pdaAddr:pda.toBase58()};
}
// ===== UI =====
const $=id=>document.getElementById(id);
const statusEl=$('status'),genMsgEl=$('genMsg');
let _pda=null;
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 i=$('password'),v=i.type==='password';
i.type=v?'text':'password';$('btnEye').textContent=v?'Скрыть':'Показать';
});
function refreshSolAddr(){
const b58=$('devPub').value.trim();
if(!b58){$('solBox').classList.remove('show');return;}
try{to32(b58Dec(b58));$('solAdr').textContent=b58;$('solBox').classList.add('show');}
catch{$('solBox').classList.remove('show');}
}
$('devPub').addEventListener('input',refreshSolAddr);
$('btnLoad').addEventListener('click',async()=>{
const btn=$('btnLoad');btn.disabled=true;
setStatus('Загрузка PDA из Solana...','info');
_pda=null;$('pdaInfo').style.display='none';$('updateForm').style.display='none';
try{
const login=$('login').value.trim().toLowerCase();
if(!login)throw new Error('Введите логин сервера');
const sol=await loadSol();
const conn=new sol.Connection($('endpoint').value.trim(),'confirmed');
const enc=new TextEncoder();
const uPgm=new sol.PublicKey(USERS_PGM);
const[pda]=sol.PublicKey.findProgramAddressSync([enc.encode('login='),enc.encode(login)],uPgm);
const ai=await conn.getAccountInfo(pda,'confirmed');
if(!ai)throw new Error(`PDA не найдена для «${login}»`);
const p=parsePda(ai.data);
if(!p.isServer)throw new Error('Эта PDA не является серверной');
_pda={...p,pdaAddr:pda.toBase58()};
const fmtTs=ms=>ms?new Date(Number(ms)).toLocaleString('ru'):'—';
const fmtB=n=>{const v=Number(n||0);return v<1024?`${v} B`:v<1048576?`${(v/1024).toFixed(1)} KB`:`${(v/1048576).toFixed(2)} MB`;};
$('iAddr').textContent=_pda.pdaAddr;$('iVer').textContent=`#${p.recordNumber}`;
$('iCreated').textContent=fmtTs(p.createdAtMs);$('iUpdated').textContent=fmtTs(p.updatedAtMs);
$('iSrvAddr').textContent=p.srvData?.srvAddr||'—';
$('iSync').textContent=p.srvData?.syncSrvs?.length?p.srvData.syncSrvs.join(', '):'(пусто)';
$('iBch').textContent=p.bchData?.bchName||'—';$('iLimit').textContent=fmtB(p.bchData?.paidLimit);
$('pdaInfo').style.display='block';
$('serverAddress').value=p.srvData?.srvAddr||'';
$('syncServers').value=(p.srvData?.syncSrvs||[]).join('\n');
$('updateForm').style.display='block';
statusEl.className='status';statusEl.textContent='';
}catch(e){setStatus('Ошибка загрузки: '+(e.message||String(e)),'error');}
finally{btn.disabled=false;}
});
$('btnGen').addEventListener('click',async()=>{
const login=$('login').value.trim().toLowerCase(),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);
$('btnUpdate').addEventListener('click',async()=>{
if(!_pda){setStatus('Сначала загрузите PDA','error');return;}
const btn=$('btnUpdate');btn.disabled=true;
setStatus('Подготовка...','info');
try{
const login=$('login').value.trim().toLowerCase();
const srvAddr=$('serverAddress').value.trim();
if(!srvAddr)throw new Error('Введите новый адрес сервера');
if(!$('rootPriv').value.trim())throw new Error('Root Key приватный не заполнен');
if(!$('devPriv').value.trim())throw new Error('Device Key приватный не заполнен');
setStatus('Отправка транзакции в Solana...','info');
const res=await updateServer({
login,rootPriv:$('rootPriv').value.trim(),bchPriv:$('bchPriv').value.trim(),devPriv:$('devPriv').value.trim(),
srvAddr,syncSrvs:parseLogins($('syncServers').value),
fmtType:_pda.srvData?.fmtType??1,fmtVer:_pda.srvData?.fmtVer??0,
endpoint:$('endpoint').value.trim(),
});
setStatus(`✓ PDA обновлена!\n\nЛогин: ${login}\nPDA: ${res.pdaAddr}\nТранзакция: ${res.sig}`,'success');
_pda=null;
}catch(e){setStatus('Ошибка: '+(e.message||String(e)),'error');}
finally{btn.disabled=false;}
});
</script>
</body>
</html>