SHA256
Автоопределение SHiNE-сервера по логину через PDA
This commit is contained in:
@@ -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,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
|
||||
export { PublicKey };
|
||||
Reference in New Issue
Block a user