SHiNE-server/shine-UI/js/services/solana-register-service.js

382 lines
14 KiB
JavaScript

import { sha256Bytes, signBytes, importPkcs8Ed25519, base64ToBytes } from './crypto-utils.js';
import { extractSeed32FromPkcs8B64 } from './device-key-utils.js';
import {
SHINE_USERS_PROGRAM_ID,
SHINE_PAYMENTS_PROGRAM_ID,
SHINE_LOGIN_GUARD_PROGRAM_ID,
} from '../solana-programs.js';
const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]);
const CLASSIFY_LOGIN_DISCRIMINATOR = new Uint8Array([112, 97, 152, 32, 255, 73, 108, 86]);
const PRECHECK_SIM_PAYER = 'FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P';
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
let solanaLibPromise = null;
function loadSolanaLib() {
if (!solanaLibPromise) solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
return solanaLibPromise;
}
function pushU32LE(buf, v) {
const n = v >>> 0;
buf.push(n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF);
}
function pushU64LE(buf, bigV) {
const b = typeof bigV === 'bigint' ? bigV : BigInt(bigV);
const lo = Number(b & 0xFFFFFFFFn) >>> 0;
const hi = Number((b >> 32n) & 0xFFFFFFFFn) >>> 0;
pushU32LE(buf, lo);
pushU32LE(buf, hi);
}
class BorshBuf {
constructor() { this._b = []; }
u8(v) { this._b.push(v & 0xFF); }
u32(v) { pushU32LE(this._b, v); }
u64(v) { pushU64LE(this._b, v); }
bool(v) { this.u8(v ? 1 : 0); }
bytes32(b) { for (const x of b) this._b.push(x); }
vecU8(b) { this.u32(b.length); for (const x of b) this._b.push(x); }
str(s) {
const enc = new TextEncoder().encode(s);
this.u32(enc.length);
for (const x of enc) this._b.push(x);
}
vecStr(arr) {
this.u32(arr.length);
for (const s of arr) this.str(s);
}
raw(bytes) { for (const x of bytes) this._b.push(x); }
result() { return new Uint8Array(this._b); }
}
// Matches Rust serialize_last_block_state (initial zero state)
function buildLastBlockStateBytes(login, blockchainName) {
const enc = new TextEncoder();
const prefix = enc.encode('SHiNE_LAST_BLOCK');
const loginB = enc.encode(login);
const bchB = enc.encode(blockchainName);
const buf = [];
for (const x of prefix) buf.push(x);
buf.push(loginB.length);
for (const x of loginB) buf.push(x);
buf.push(bchB.length);
for (const x of bchB) buf.push(x);
pushU32LE(buf, 0); // last_block_number = 0
for (let i = 0; i < 32; i++) buf.push(0); // last_block_hash = [0;32]
pushU64LE(buf, 0n); // used_bytes = 0
return new Uint8Array(buf);
}
// Matches Rust serialize_unsigned_record for initial registration
function buildUnsignedRecordBytes(
login, createdAtMs, rootKey32, deviceKey32, blockchainKey32,
blockchainName, paidLimitBytes, lastBlockSig64,
) {
const enc = new TextEncoder();
const loginB = enc.encode(login);
const bchB = enc.encode(blockchainName);
const accessB = enc.encode('shineup.me');
const buf = [];
// Fixed header: MAGIC(5) + FORMAT_MAJOR(1) + FORMAT_MINOR(1) + record_len_placeholder(2)
buf.push(0x53, 0x48, 0x69, 0x4E, 0x45, 1, 0, 0, 0); // indices 0..8
pushU64LE(buf, createdAtMs); // created_at_ms
pushU64LE(buf, createdAtMs); // updated_at_ms = same
pushU32LE(buf, 0); // record_number = 0
for (let i = 0; i < 32; i++) buf.push(0); // prev_record_hash = [0;32]
buf.push(loginB.length);
for (const x of loginB) buf.push(x);
buf.push(5); // blocks_count (non-server)
// RootKeyBlock (type=1, ver=0)
buf.push(1, 0);
for (const x of rootKey32) buf.push(x);
// DeviceKeyBlock (type=2, ver=0)
buf.push(2, 0);
for (const x of deviceKey32) buf.push(x);
// BlockchainRegistryBlock (type=3, ver=0, count=1)
buf.push(3, 0, 1, 1); // type, ver, count=1, blockchain_type=1(MAIN_USER)
buf.push(bchB.length);
for (const x of bchB) buf.push(x);
for (const x of blockchainKey32) buf.push(x);
pushU64LE(buf, paidLimitBytes); // paid_limit_bytes
pushU64LE(buf, 0n); // used_bytes = 0
pushU32LE(buf, 0); // last_block_number = 0
for (let i = 0; i < 32; i++) buf.push(0); // last_block_hash = [0;32]
for (const x of lastBlockSig64) buf.push(x); // last_block_signature
buf.push(0); // arweave_present = 0
// AccessServersBlock (type=40, ver=0)
buf.push(40, 0, 1, accessB.length);
for (const x of accessB) buf.push(x);
// TrustedStateBlock (type=50, ver=0, trusted_count=0)
buf.push(50, 0, 0);
// Patch record_len at indices 7-8: total = buf.length + 64 (signature)
const recLen = buf.length + 64;
buf[7] = recLen & 0xFF;
buf[8] = (recLen >> 8) & 0xFF;
return new Uint8Array(buf);
}
// Builds Ed25519 program instruction data for one signature
function buildEd25519IxData(sig64, pubkey32, msgHash32) {
const sigOff = 16;
const pkOff = sigOff + 64; // 80
const msgOff = pkOff + 32; // 112
const data = new Uint8Array(msgOff + 32); // 144 bytes total
const v = new DataView(data.buffer);
data[0] = 1; data[1] = 0; // num_signatures=1, padding
v.setUint16(2, sigOff, true);
v.setUint16(4, 0xFFFF, true); // same instruction
v.setUint16(6, pkOff, true);
v.setUint16(8, 0xFFFF, true);
v.setUint16(10, msgOff, true);
v.setUint16(12, 32, true); // message_data_size = 32
v.setUint16(14, 0xFFFF, true);
data.set(sig64, sigOff);
data.set(pubkey32, pkOff);
data.set(msgHash32, msgOff);
return data;
}
function readStartBonusLimit(data) {
// Borsh: version(u8=1) + reg_fee(u64=8) + lamports_per_step(u64=8) + start_bonus_limit(u64=8)
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
return v.getBigUint64(17, true);
}
function serializeCreateUserPdaArgs(
login, rootKey32, createdAtMs, deviceKey32, blockchainKey32,
blockchainName, lastBlockSig64, rootSig64,
) {
const b = new BorshBuf();
b.raw(CREATE_USER_PDA_DISCRIMINATOR);
b.str(login);
b.bytes32(rootKey32);
b.u64(createdAtMs);
b.u64(0n); // additional_limit
// UserMutableFields:
b.bytes32(deviceKey32);
b.bytes32(blockchainKey32);
b.str(blockchainName);
b.u64(0n); // used_bytes
b.u32(0); // last_block_number
b.vecU8(new Uint8Array(32)); // last_block_hash
b.vecU8(lastBlockSig64); // last_block_signature
b.str(''); // arweave_tx_id
b.bool(false); // is_server
b.u8(0); // address_format_type
b.u8(0); // address_format_version
b.str(''); // server_address
b.vecStr([]); // sync_servers
b.vecStr(['shineup.me']); // access_servers
b.u8(0); // trusted_count
b.vecU8(rootSig64); // signature
return b.result();
}
function serializeClassifyLoginArgs(login) {
const b = new BorshBuf();
b.raw(CLASSIFY_LOGIN_DISCRIMINATOR);
b.str(String(login || ''));
return b.result();
}
function decodeU32FromB64(rawB64) {
const bytes = Uint8Array.from(atob(rawB64), (ch) => ch.charCodeAt(0));
if (bytes.length < 4) throw new Error('LOGIN_GUARD_BAD_RETURN_DATA');
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true);
}
function safeJson(value) {
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
export function formatSolanaErrorDetails(error) {
const parts = [];
const msg = String(error?.message || error || '').trim();
if (msg) parts.push(msg);
const logs = error?.logs || error?.transactionLogs || error?.simulationLogs || error?.data?.logs;
if (Array.isArray(logs) && logs.length) {
parts.push(`Logs: ${logs.join(' | ')}`);
}
const errObj = error?.value?.err || error?.err || error?.data?.err;
if (errObj) {
parts.push(`Err: ${safeJson(errObj)}`);
}
if (!parts.length) return 'unknown';
return parts.join(' :: ');
}
export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) {
const solana = await loadSolanaLib();
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID);
const payer = new solana.PublicKey(PRECHECK_SIM_PAYER);
const ix = new solana.TransactionInstruction({
programId: loginGuardProgram,
keys: [{ pubkey: payer, isSigner: true, isWritable: false }],
data: serializeClassifyLoginArgs(String(login || '').toLowerCase()),
});
const { blockhash } = await connection.getLatestBlockhash('confirmed');
const v0Message = new solana.TransactionMessage({
payerKey: payer,
recentBlockhash: blockhash,
instructions: [ix],
}).compileToV0Message();
const tx = new solana.VersionedTransaction(v0Message);
const sim = await connection.simulateTransaction(tx, {
commitment: 'confirmed',
sigVerify: false,
replaceRecentBlockhash: true,
});
if (sim?.value?.err) {
const simErr = new Error(`LOGIN_GUARD_SIMULATION_FAILED: ${safeJson(sim.value.err)}`);
simErr.logs = sim?.value?.logs || [];
simErr.err = sim?.value?.err;
throw simErr;
}
const returnData = sim?.value?.returnData;
if (!returnData || returnData.programId !== SHINE_LOGIN_GUARD_PROGRAM_ID) {
throw new Error('LOGIN_GUARD_BAD_RETURN_DATA');
}
const classValue = decodeU32FromB64(returnData.data?.[0] || '');
if (classValue === 0) return { classCode: 0, className: 'free' };
if (classValue === 1) return { classCode: 1, className: 'premium' };
if (classValue === 2) return { classCode: 2, className: 'company' };
return { classCode: classValue, className: 'unknown' };
}
export async function checkLoginExistsOnSolana({ login, solanaEndpoint }) {
const solana = await loadSolanaLib();
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const enc = new TextEncoder();
const loginNorm = String(login || '').trim().toLowerCase();
if (!loginNorm) {
throw new Error('EMPTY_LOGIN');
}
const [userPda] = solana.PublicKey.findProgramAddressSync(
[enc.encode('login='), enc.encode(loginNorm)],
usersProgram,
);
const ai = await connection.getAccountInfo(userPda, 'confirmed');
return { exists: !!ai, userPda: userPda.toBase58() };
}
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint }) {
const solana = await loadSolanaLib();
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID);
const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID);
const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID);
const enc = new TextEncoder();
const loginNorm = login.toLowerCase();
const blockchainName = `${loginNorm}-001`;
const [userPda] = solana.PublicKey.findProgramAddressSync(
[enc.encode('login='), enc.encode(loginNorm)],
usersProgram,
);
const [economyConfigPda] = solana.PublicKey.findProgramAddressSync(
[enc.encode('shine_users_economy_config')],
usersProgram,
);
const [inflowVault] = solana.PublicKey.findProgramAddressSync(
[enc.encode('shine_payments_inflow_vault')],
paymentsProgram,
);
const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64);
const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64);
const deviceKey32 = base64ToBytes(keyBundle.devicePair.publicKeyB64);
const rootPrivKey = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64);
const bchPrivKey = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64);
const deviceSeed32 = extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64);
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
const ecoAccount = await connection.getAccountInfo(economyConfigPda);
if (!ecoAccount) {
throw new Error('Economy config не инициализирован. Запустите init_users_economy_config.');
}
const startBonusLimit = readStartBonusLimit(ecoAccount.data);
const createdAtMs = BigInt(Date.now());
// Sign LastBlockState with blockchain key
const lbsBytes = buildLastBlockStateBytes(loginNorm, blockchainName);
const lbsHash = await sha256Bytes(lbsBytes);
const lastBlockSig64 = await signBytes(bchPrivKey, lbsHash);
// Build and sign unsigned PDA record with root key
const unsignedRecord = buildUnsignedRecordBytes(
loginNorm, createdAtMs, rootKey32, deviceKey32, blockchainKey32,
blockchainName, startBonusLimit, lastBlockSig64,
);
const unsignedHash = await sha256Bytes(unsignedRecord);
const rootSig64 = await signBytes(rootPrivKey, unsignedHash);
const ixData = serializeCreateUserPdaArgs(
loginNorm, rootKey32, createdAtMs, deviceKey32, blockchainKey32,
blockchainName, lastBlockSig64, rootSig64,
);
// Ed25519 instructions must precede create_user_pda
const ed25519RootIx = new solana.TransactionInstruction({
programId: ed25519Program,
keys: [],
data: buildEd25519IxData(rootSig64, rootKey32, unsignedHash),
});
const ed25519BchIx = new solana.TransactionInstruction({
programId: ed25519Program,
keys: [],
data: buildEd25519IxData(lastBlockSig64, blockchainKey32, lbsHash),
});
const createUserIx = new solana.TransactionInstruction({
programId: usersProgram,
keys: [
{ pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: inflowVault, isSigner: false, isWritable: true },
{ pubkey: sysvarInstructions, isSigner: false, isWritable: false },
{ pubkey: economyConfigPda, isSigner: false, isWritable: false },
{ pubkey: loginGuardProgram, isSigner: false, isWritable: false },
],
data: ixData,
});
const sig = await solana.sendAndConfirmTransaction(
connection,
new solana.Transaction().add(ed25519RootIx, ed25519BchIx, createUserIx),
[deviceKeypair],
{ commitment: 'confirmed' },
);
return { signature: sig, blockchainName };
}