485 lines
25 KiB
HTML
485 lines
25 KiB
HTML
<!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>
|