145 lines
5.0 KiB
JavaScript
145 lines
5.0 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';
|
|
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
|
|
|
|
const CLASSIFY_LOGIN_INSTRUCTION_TAG = 1;
|
|
const PRECHECK_SIM_PAYER = 'FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P';
|
|
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
|
|
|
|
let solanaLibPromise = null;
|
|
function loadSolanaLib() {
|
|
if (!solanaLibPromise) solanaLibPromise = loadSolanaWeb3();
|
|
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.u8(CLASSIFY_LOGIN_INSTRUCTION_TAG);
|
|
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 function isUserAlreadyExistsSolanaError(error) {
|
|
const details = formatSolanaErrorDetails(error);
|
|
return details.includes('UserAlreadyExists')
|
|
|| details.includes('custom program error: 0x4')
|
|
|| details.includes('"Custom":4')
|
|
|| details.includes('"InstructionError":[2,{"Custom":4}]')
|
|
|| details.includes('Instruction 2: custom program error: 0x4');
|
|
}
|
|
|
|
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: [],
|
|
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(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(loginNorm)],
|
|
usersProgram,
|
|
);
|
|
const ai = await connection.getAccountInfo(userPda, 'confirmed');
|
|
return { exists: !!ai, userPda: userPda.toBase58() };
|
|
}
|
|
|
|
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint, accessServers }) {
|
|
return registerUserOnSolanaShared({ login, keyBundle, solanaEndpoint, accessServers });
|
|
}
|