SHiNE-server/SHiNE-browser-plugin-wallet/js/lib/shine-server-resolver.js

187 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { base64ToBytes } from './crypto-utils.js';
import { PublicKey } from './vendor/solana-publickey-bundle.js';
const SOLANA_ENDPOINT_DEFAULT = 'https://api.devnet.solana.com';
const SHINE_USERS_PROGRAM_ID = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm';
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
const DEFAULT_SHINE_SERVER_LOGIN = 'shineupme';
const DEFAULT_SHINE_SERVER_ADDRESS = 'shineup.me';
function normalizeHostLike(value) {
const raw = String(value || '').trim();
if (!raw) return '';
try {
const withScheme = /^[a-z]+:\/\//i.test(raw) ? raw : `https://${raw}`;
const parsed = new URL(withScheme);
return String(parsed.host || '').trim().toLowerCase();
} catch {
return raw.replace(/^https?:\/\//i, '').replace(/^wss?:\/\//i, '').replace(/\/.*$/, '').trim().toLowerCase();
}
}
function normalizeServerLogin(value) {
return String(value || '').trim().toLowerCase();
}
function buildHttpBase(address) {
const host = normalizeHostLike(address) || DEFAULT_SHINE_SERVER_ADDRESS;
return `https://${host}`;
}
function buildWsUrl(address) {
const host = normalizeHostLike(address) || DEFAULT_SHINE_SERVER_ADDRESS;
return `wss://${host}/ws`;
}
function readU8(bytes, cursorRef) {
if (cursorRef.value >= bytes.length) throw new Error('Повреждённый формат PDA');
return bytes[cursorRef.value++];
}
function readBytes(bytes, cursorRef, length) {
if (cursorRef.value + length > bytes.length) throw new Error('Повреждённый формат PDA');
const out = bytes.slice(cursorRef.value, cursorRef.value + length);
cursorRef.value += length;
return out;
}
function readStrU8(bytes, cursorRef) {
const length = readU8(bytes, cursorRef);
return new TextDecoder().decode(readBytes(bytes, cursorRef, length));
}
function parseServerFieldsFromUserPda(dataBytes) {
const bytes = dataBytes instanceof Uint8Array ? dataBytes : new Uint8Array(dataBytes || []);
if (bytes.length < 5) throw new Error('Некорректный формат PDA');
const cursorRef = { value: 0 };
const magic = new TextDecoder().decode(readBytes(bytes, cursorRef, 5));
if (magic !== 'SHiNE') throw new Error('Некорректный формат PDA');
cursorRef.value += 1; // format_major
cursorRef.value += 1; // format_minor
cursorRef.value += 2; // record_len
cursorRef.value += 8; // created_at_ms
cursorRef.value += 8; // updated_at_ms
cursorRef.value += 4; // record_number
cursorRef.value += 32; // prev_record_hash
readStrU8(bytes, cursorRef); // login
const blocksCount = readU8(bytes, cursorRef);
let isServer = false;
let serverAddress = '';
let accessServers = [];
for (let i = 0; i < blocksCount; i += 1) {
const blockType = readU8(bytes, cursorRef);
cursorRef.value += 1; // block_version
if (blockType === 1 || blockType === 2) {
cursorRef.value += 32;
continue;
}
if (blockType === 3) {
const count = readU8(bytes, cursorRef);
for (let j = 0; j < count; j += 1) {
cursorRef.value += 1;
readStrU8(bytes, cursorRef);
cursorRef.value += 32;
cursorRef.value += 8 + 8 + 4 + 32 + 64;
const arPresent = readU8(bytes, cursorRef);
if (arPresent === 1) readStrU8(bytes, cursorRef);
}
continue;
}
if (blockType === 30) {
isServer = readU8(bytes, cursorRef) === 1;
if (isServer) {
cursorRef.value += 1; // address_format_type
cursorRef.value += 1; // address_format_version
serverAddress = readStrU8(bytes, cursorRef);
const syncCount = readU8(bytes, cursorRef);
for (let j = 0; j < syncCount; j += 1) readStrU8(bytes, cursorRef);
}
continue;
}
if (blockType === 40) {
const accessCount = readU8(bytes, cursorRef);
accessServers = [];
for (let j = 0; j < accessCount; j += 1) accessServers.push(readStrU8(bytes, cursorRef));
continue;
}
if (blockType === 50) {
cursorRef.value += 1;
const sessionsCount = readU8(bytes, cursorRef);
for (let j = 0; j < sessionsCount; j += 1) {
cursorRef.value += 1 + 1;
readStrU8(bytes, cursorRef);
cursorRef.value += 32;
}
continue;
}
if (blockType === 70) {
cursorRef.value += 1;
continue;
}
throw new Error(`Неизвестный блок PDA: ${blockType}`);
}
return {
isServer,
serverAddress: normalizeHostLike(serverAddress),
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
};
}
async function fetchUserPda(login, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) {
const cleanLogin = normalizeServerLogin(login);
if (!cleanLogin) throw new Error('Не указан логин для чтения PDA.');
const usersProgram = new PublicKey(SHINE_USERS_PROGRAM_ID);
const enc = new TextEncoder();
const [userPda] = PublicKey.findProgramAddressSync(
[enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)],
usersProgram,
);
const response = await fetch(String(solanaEndpoint || SOLANA_ENDPOINT_DEFAULT), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getAccountInfo',
params: [userPda.toBase58(), { encoding: 'base64', commitment: 'confirmed' }],
}),
});
if (!response.ok) throw new Error('Не удалось прочитать Solana RPC.');
const json = await response.json();
const dataB64 = json?.result?.value?.data?.[0];
if (!dataB64) throw new Error(`PDA не найдена для логина @${cleanLogin}.`);
return parseServerFieldsFromUserPda(base64ToBytes(dataB64));
}
export async function resolveShineServerByServerLogin(serverLogin, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) {
const cleanServerLogin = normalizeServerLogin(serverLogin) || DEFAULT_SHINE_SERVER_LOGIN;
const parsed = await fetchUserPda(cleanServerLogin, solanaEndpoint);
if (!parsed.isServer) {
throw new Error(`Логин @${cleanServerLogin} не опубликован как сервер SHiNE.`);
}
if (!parsed.serverAddress) {
throw new Error(`У server PDA пользователя @${cleanServerLogin} не задан server_address.`);
}
return {
serverLogin: cleanServerLogin,
serverAddress: parsed.serverAddress,
serverHttp: buildHttpBase(parsed.serverAddress),
serverUrl: buildWsUrl(parsed.serverAddress),
};
}
export {
DEFAULT_SHINE_SERVER_ADDRESS,
DEFAULT_SHINE_SERVER_LOGIN,
SOLANA_ENDPOINT_DEFAULT,
buildHttpBase,
buildWsUrl,
normalizeServerLogin,
};