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

134 lines
4.6 KiB
JavaScript

import { registerUserOnSolana as registerUserOnSolanaShared } from './shine-user-pda-service.js';
import {
SHINE_USERS_PROGRAM_ID,
SHINE_LOGIN_GUARD_PROGRAM_ID,
} from '../solana-programs.js';
const CLASSIFY_LOGIN_DISCRIMINATOR = new Uint8Array([112, 97, 152, 32, 255, 73, 108, 86]);
const PRECHECK_SIM_PAYER = 'FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P';
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);
}
class BorshBuf {
constructor() { this._b = []; }
u8(v) { this._b.push(v & 0xFF); }
u32(v) { pushU32LE(this._b, v); }
str(s) {
const enc = new TextEncoder().encode(s);
this.u32(enc.length);
for (const x of enc) this._b.push(x);
}
raw(bytes) { for (const x of bytes) this._b.push(x); }
result() { return new Uint8Array(this._b); }
}
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 }) {
return registerUserOnSolanaShared({ login, keyBundle, solanaEndpoint });
}