469 lines
23 KiB
HTML
469 lines
23 KiB
HTML
<!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>
|