import { renderHeader } from '../components/header.js'; import { state } from '../state.js'; import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js'; export const pageMeta = { id: 'queue', title: 'Очередь билета' }; const SHINE_PAYMENTS_PROGRAM_ID = 'SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v'; const SHINE_PAYMENTS_ORACLE_ACCOUNT = '7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE'; const SHINE_PAYMENTS_SEEDS = { config: 'shine_payments_config', coef: 'shine_payments_coef_limit', queues: 'shine_payments_queues', q1: 'shine_payments_q1_ticket', q2: 'shine_payments_q2_ticket', q3: 'shine_payments_q3_ticket', }; function utf8Bytes(text) { return new TextEncoder().encode(String(text || '')); } function u64ToBytes(value) { const out = new Uint8Array(8); let current = BigInt(value || 0); for (let i = 0; i < 8; i += 1) { out[i] = Number(current & 0xffn); current >>= 8n; } return out; } function readU64(data, offset) { let value = 0n; for (let i = 0; i < 8; i += 1) { value |= BigInt(data[offset + i]) << (8n * BigInt(i)); } return value; } function readI64(data, offset) { let value = readU64(data, offset); if (value > 0x7fffffffffffffffn) { value -= 0x10000000000000000n; } return value; } function readI32(data, offset) { let value = Number(readU64(data, offset) & 0xffffffffn); if (value > 0x7fffffff) value -= 0x100000000; return value; } function formatQueuePrefix(queueId) { if (queueId === 2) return 'Q2'; if (queueId === 3) return 'Q3'; return 'Q1'; } function formatUsdCentsText(cents) { const value = Number(cents || 0n) / 100; return value.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } function parsePaymentsCoef(data) { let offset = 0; const version = data[offset]; offset += 1; const coefPpm = readU64(data, offset); offset += 8; const limitUsdCents = readU64(data, offset); offset += 8; const callRewardLamports = readU64(data, offset); return { version, coefPpm, limitUsdCents, callRewardLamports }; } function parsePaymentsQueues(data) { let offset = 0; const version = data[offset]; offset += 1; const q1TicketsTotal = readU64(data, offset); offset += 8; const q1TicketsPaid = readU64(data, offset); offset += 8; const q1SumTotalUsdCents = readU64(data, offset); offset += 8; const q1SumPaidUsdCents = readU64(data, offset); offset += 8; const q2TicketsTotal = readU64(data, offset); offset += 8; const q2TicketsPaid = readU64(data, offset); offset += 8; const q2SumTotalUsdCents = readU64(data, offset); offset += 8; const q2SumPaidUsdCents = readU64(data, offset); offset += 8; const q3TicketsTotal = readU64(data, offset); offset += 8; const q3TicketsPaid = readU64(data, offset); offset += 8; const q3SumTotalUsdCents = readU64(data, offset); offset += 8; const q3SumPaidUsdCents = readU64(data, offset); return { version, q1TicketsTotal, q1TicketsPaid, q1SumTotalUsdCents, q1SumPaidUsdCents, q2TicketsTotal, q2TicketsPaid, q2SumTotalUsdCents, q2SumPaidUsdCents, q3TicketsTotal, q3TicketsPaid, q3SumTotalUsdCents, q3SumPaidUsdCents, }; } function parsePaymentsTicket(data) { let offset = 0; const version = data[offset]; offset += 1; const queueId = data[offset]; offset += 1; const index = readU64(data, offset); offset += 8; const isPaid = Boolean(data[offset]); offset += 1; const solana = window.solanaWeb3; const recipientWallet = new solana.PublicKey(data.slice(offset, offset + 32)).toBase58(); offset += 32; const payoutUsdCents = readU64(data, offset); offset += 8; const debtBeforeUsdCents = readU64(data, offset); return { version, queueId, index, isPaid, recipientWallet, payoutUsdCents, debtBeforeUsdCents, }; } function parseSupportTicketInput(rawValue) { const clean = String(rawValue || '').trim(); if (!clean) throw new Error('Введите номер билета.'); if (/^\d+$/.test(clean)) { return { queueId: 1, index: BigInt(clean) }; } const match = clean.match(/^([123])(?:\s*[-_:\/#]+\s*|\s+)(\d+)$/); if (!match) { throw new Error('Формат: для Q1 просто номер, для Q2/Q3 - например 2-15 или 3 8.'); } return { queueId: Number(match[1]), index: BigInt(match[2]), }; } async function loadSupportPaymentsCore(endpoint) { const solana = await loadSolanaWeb3(); const rpc = String(endpoint || '').trim(); const connection = new solana.Connection(rpc, 'confirmed'); const programId = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID); const oracleAccount = new solana.PublicKey(SHINE_PAYMENTS_ORACLE_ACCOUNT); const [configPda] = solana.PublicKey.findProgramAddressSync([utf8Bytes(SHINE_PAYMENTS_SEEDS.config)], programId); const [coefPda] = solana.PublicKey.findProgramAddressSync([utf8Bytes(SHINE_PAYMENTS_SEEDS.coef)], programId); const [queuesPda] = solana.PublicKey.findProgramAddressSync([utf8Bytes(SHINE_PAYMENTS_SEEDS.queues)], programId); const [oracleAi, configAi, coefAi, queuesAi] = await Promise.all([ connection.getAccountInfo(oracleAccount, 'confirmed'), connection.getAccountInfo(configPda, 'confirmed'), connection.getAccountInfo(coefPda, 'confirmed'), connection.getAccountInfo(queuesPda, 'confirmed'), ]); if (!oracleAi) throw new Error('Не найден аккаунт оракула SOL/USD.'); if (!configAi || !coefAi || !queuesAi) throw new Error('PDA программы оплаты ещё не инициализированы.'); const parsePrice = (data) => { const price = readI64(data, 73); const exponent = readI32(data, 89); const publishTime = readI64(data, 93); if (price <= 0n) throw new Error('Оракул вернул некорректную цену.'); let num = price * 100n; let den = 1n; if (exponent >= 0) { num *= 10n ** BigInt(exponent); } else { den *= 10n ** BigInt(-exponent); } return { priceNum: num, priceDen: den, publishTime }; }; return { connection, programId, coef: parsePaymentsCoef(coefAi.data), queues: parsePaymentsQueues(queuesAi.data), pyth: parsePrice(oracleAi.data), }; } function queueStateView(queues, queueId) { if (queueId === 2) { return { ticketsTotal: queues.q2TicketsTotal, ticketsPaid: queues.q2TicketsPaid, sumTotalUsdCents: queues.q2SumTotalUsdCents, sumPaidUsdCents: queues.q2SumPaidUsdCents, }; } if (queueId === 3) { return { ticketsTotal: queues.q3TicketsTotal, ticketsPaid: queues.q3TicketsPaid, sumTotalUsdCents: queues.q3SumTotalUsdCents, sumPaidUsdCents: queues.q3SumPaidUsdCents, }; } return { ticketsTotal: queues.q1TicketsTotal, ticketsPaid: queues.q1TicketsPaid, sumTotalUsdCents: queues.q1SumTotalUsdCents, sumPaidUsdCents: queues.q1SumPaidUsdCents, }; } function queueSeedFor(queueId) { if (queueId === 2) return SHINE_PAYMENTS_SEEDS.q2; if (queueId === 3) return SHINE_PAYMENTS_SEEDS.q3; return SHINE_PAYMENTS_SEEDS.q1; } function styleInputField(field) { if (!field) return; field.style.color = '#111111'; field.style.webkitTextFillColor = '#111111'; field.style.caretColor = '#111111'; field.style.backgroundColor = '#ffffff'; } function readTicketFromUrl() { try { const params = new URLSearchParams(window.location.search || ''); return String(params.get('ticket') || '').trim(); } catch { return ''; } } export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; const content = document.createElement('div'); content.className = 'stack'; const status = document.createElement('p'); status.className = 'meta-muted'; status.textContent = 'Страница готова. Введите номер билета.'; const card = document.createElement('div'); card.className = 'card stack'; card.innerHTML = `