import crypto from 'node:crypto' const TIMEOUT = 10_000 function getConfig() { const url = process.env.BTCPAY_URL const apiKey = process.env.BTCPAY_API_KEY const storeId = process.env.BTCPAY_STORE_ID if (!url || !apiKey || !storeId) throw new Error('BTCPay configuration missing: BTCPAY_URL, BTCPAY_API_KEY, BTCPAY_STORE_ID required') return { url: url.replace(/\/$/, ''), apiKey, storeId } } async function btcpayFetch(path: string, options: RequestInit = {}): Promise { const { url, apiKey } = getConfig() const controller = new AbortController() const timer = setTimeout(() => controller.abort(), TIMEOUT) try { return await fetch(`${url}${path}`, { ...options, signal: controller.signal, headers: { Authorization: `token ${apiKey}`, 'Content-Type': 'application/json', ...options.headers } }) } finally { clearTimeout(timer) } } export interface CreateInvoiceParams { amountSats: number; orderId: string; redirectUrl: string } export interface BtcPayInvoice { id: string; checkoutLink: string; status: string; amount: string; currency: string } export async function createInvoice(params: CreateInvoiceParams): Promise { const { storeId } = getConfig() const res = await btcpayFetch(`/api/v1/stores/${storeId}/invoices`, { method: 'POST', body: JSON.stringify({ amount: String(params.amountSats), currency: 'SATS', metadata: { orderId: params.orderId }, checkout: { redirectURL: params.redirectUrl, redirectAutomatically: true } }), }) if (!res.ok) { const text = await res.text(); throw new Error(`BTCPay invoice creation failed: ${res.status} ${text}`) } return res.json() as Promise } export async function getInvoice(invoiceId: string): Promise { const { storeId } = getConfig() const res = await btcpayFetch(`/api/v1/stores/${storeId}/invoices/${invoiceId}`) if (!res.ok) throw new Error(`BTCPay invoice fetch failed: ${res.status}`) return res.json() as Promise } export function validateWebhookSignature(body: string, signature: string): boolean { const secret = process.env.BTCPAY_WEBHOOK_SECRET if (!secret) return false const expected = crypto.createHmac('sha256', secret).update(body).digest('hex') const sigBuf = Buffer.from(signature.replace('sha256=', ''), 'hex') const expectedBuf = Buffer.from(expected, 'hex') if (sigBuf.length !== expectedBuf.length) return false return crypto.timingSafeEqual(sigBuf, expectedBuf) }