Автоопределение SHiNE-сервера по логину через PDA

This commit is contained in:
AidarKC
2026-06-16 16:32:33 +04:00
parent 3efa8bb7ee
commit f8a76bcd7f
19 changed files with 17426 additions and 48 deletions
@@ -0,0 +1,186 @@
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,
};