import { base64ToBytes } from './crypto-utils.js'; import { PublicKey } from './vendor/solana-publickey-bundle.js'; const SOLANA_ENDPOINT_DEFAULT = 'https://solana-rpc.publicnode.com'; const SHINE_USERS_PROGRAM_ID = 'SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6'; 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 < 9) throw new Error('Некорректный формат PDA'); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const cursorRef = { value: 0 }; const readU16 = () => { if (cursorRef.value + 2 > bytes.length) throw new Error('Повреждённый формат PDA'); const v = view.getUint16(cursorRef.value, true); cursorRef.value += 2; return v; }; const readU32 = () => { if (cursorRef.value + 4 > bytes.length) throw new Error('Повреждённый формат PDA'); const v = view.getUint32(cursorRef.value, true); cursorRef.value += 4; return v; }; const readU64 = () => { if (cursorRef.value + 8 > bytes.length) throw new Error('Повреждённый формат PDA'); const v = view.getBigUint64(cursorRef.value, true); cursorRef.value += 8; return v; }; const magic = new TextDecoder().decode(readBytes(bytes, cursorRef, 5)); if (magic !== 'SHiNE') throw new Error('Некорректный формат PDA'); const formatMajor = readU8(bytes, cursorRef); const formatMinor = readU8(bytes, cursorRef); if (formatMajor !== 1 || formatMinor !== 2) throw new Error(`Неподдерживаемый формат PDA ${formatMajor}.${formatMinor}`); const recordLen = readU16(); if (recordLen < 73 || recordLen > bytes.length) throw new Error('Некорректный record_len PDA'); cursorRef.value += 8; // created_at_ms cursorRef.value += 8; // updated_at_ms cursorRef.value += 4; // record_number cursorRef.value += 32; // prev_record_hash const login = readStrU8(bytes, cursorRef); const blocksCount = readU8(bytes, cursorRef); let serverAddress = ''; let accessServers = []; let rootKey32 = null; let clientKey32 = null; let blockchainKey32 = null; let forkCount = 0; for (let i = 0; i < blocksCount; i += 1) { const blockType = readU8(bytes, cursorRef); const blockVersion = readU8(bytes, cursorRef); if (blockVersion !== 0) throw new Error('Неподдерживаемая версия блока PDA'); if (blockType === 1 || blockType === 2) { const key32 = readBytes(bytes, cursorRef, 32); if (blockType === 1) rootKey32 = key32; else clientKey32 = key32; continue; } const payloadLen = readU16(); const payloadEnd = cursorRef.value + payloadLen; if (payloadEnd > recordLen - 64) throw new Error('Повреждённый variable block PDA'); if (blockType === 3) { forkCount = readU16(); if (forkCount < 1) throw new Error('BlockchainRegistry пуст'); for (let j = 0; j < forkCount; j += 1) { blockchainKey32 = readBytes(bytes, cursorRef, 32); readU64(); // created_at_ms fork readU32(); // paid_limit_bytes } } else if (blockType === 30) { const addressCount = readU8(bytes, cursorRef); if (addressCount !== 1) throw new Error('PDA 1.2 допускает ровно один адрес сервера'); readU8(bytes, cursorRef); // address_format_type readU8(bytes, cursorRef); // address_format_version serverAddress = readStrU8(bytes, cursorRef); } else if (blockType === 40) { const accessCount = readU8(bytes, cursorRef); if (accessCount > 1) throw new Error('PDA 1.2 допускает максимум один access server'); accessServers = []; for (let j = 0; j < accessCount; j += 1) accessServers.push(readStrU8(bytes, cursorRef)); } cursorRef.value = payloadEnd; } if (!rootKey32 || !clientKey32 || !blockchainKey32) throw new Error('PDA 1.2 не содержит обязательные ключи'); return { isServer: Boolean(serverAddress), serverAddress: normalizeHostLike(serverAddress), accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean), publicKeys: { recoveryKeyBase58: '', rootKeyBase58: new PublicKey(rootKey32).toBase58(), clientKeyBase58: new PublicKey(clientKey32).toBase58(), blockchainKeyBase58: new PublicKey(blockchainKey32).toBase58(), blockchainName: `${normalizeServerLogin(login)}-${String(forkCount).padStart(3, '0')}`, }, 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, };