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 = '3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ'; 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 = []; let recoveryKey32 = null; let rootKey32 = null; let clientKey32 = null; let blockchainKey32 = null; let blockchainName = ''; let homeserverSessions = []; for (let i = 0; i < blocksCount; i += 1) { const blockType = readU8(bytes, cursorRef); cursorRef.value += 1; // block_version if (blockType === 0 || blockType === 1 || blockType === 2) { const key32 = readBytes(bytes, cursorRef, 32); if (blockType === 0) recoveryKey32 = key32; if (blockType === 1) rootKey32 = key32; if (blockType === 2) clientKey32 = key32; continue; } if (blockType === 3) { const count = readU8(bytes, cursorRef); for (let j = 0; j < count; j += 1) { cursorRef.value += 1; const currentBlockchainName = readStrU8(bytes, cursorRef); const currentBlockchainKey32 = readBytes(bytes, cursorRef, 32); if (!blockchainKey32) { blockchainKey32 = currentBlockchainKey32; blockchainName = currentBlockchainName; } 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) { const sessionType = readU8(bytes, cursorRef); const sessionVersion = readU8(bytes, cursorRef); const sessionName = readStrU8(bytes, cursorRef); const sessionPubKey32 = readBytes(bytes, cursorRef, 32); if (sessionType === 100) { homeserverSessions.push({ sessionType, sessionVersion, sessionName, sessionPubKeyBase58: new PublicKey(sessionPubKey32).toBase58(), sessionPubKeyB64: `ed25519/${btoa(String.fromCharCode(...sessionPubKey32))}`, }); } } 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), publicKeys: { recoveryKeyBase58: recoveryKey32 ? new PublicKey(recoveryKey32).toBase58() : '', rootKeyBase58: rootKey32 ? new PublicKey(rootKey32).toBase58() : '', clientKeyBase58: clientKey32 ? new PublicKey(clientKey32).toBase58() : '', blockchainKeyBase58: blockchainKey32 ? new PublicKey(blockchainKey32).toBase58() : '', blockchainName, }, homeserverSessions, }; } 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 async function resolveShineServerByUserLogin(login, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) { const cleanLogin = normalizeServerLogin(login); if (!cleanLogin) throw new Error('Не указан логин пользователя.'); const parsed = await fetchUserPda(cleanLogin, solanaEndpoint); const serverLogin = normalizeServerLogin(parsed.accessServers?.[0] || ''); if (!serverLogin) { throw new Error(`У пользователя @${cleanLogin} в PDA не найден первый сервер доступа.`); } const resolved = await resolveShineServerByServerLogin(serverLogin, solanaEndpoint); return { login: cleanLogin, accessServers: parsed.accessServers, serverLogin: resolved.serverLogin, serverAddress: resolved.serverAddress, serverHttp: resolved.serverHttp, serverUrl: resolved.serverUrl, }; } export async function readWalletProfileByLogin(login, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) { const cleanLogin = normalizeServerLogin(login); const parsed = await fetchUserPda(cleanLogin, solanaEndpoint); return { login: cleanLogin, accessServers: parsed.accessServers, publicKeys: parsed.publicKeys, homeserverSessions: parsed.homeserverSessions, }; } export { DEFAULT_SHINE_SERVER_ADDRESS, DEFAULT_SHINE_SERVER_LOGIN, SOLANA_ENDPOINT_DEFAULT, buildHttpBase, buildWsUrl, normalizeServerLogin, };