const { readDb, writeDb } = require('../storage'); const { getBitcoinRpcUrls, createRoundRobinPicker } = require('../utils/rpc'); const BTC_NETWORK = 'btc:mainnet'; async function callRpc(url, method, params = []) { const body = JSON.stringify({ jsonrpc: '1.0', id: 'capay', method, params }); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }); if (!response.ok) { throw new Error(`rpc ${method} failed (${response.status})`); } const payload = await response.json(); if (payload.error) { const message = payload.error.message || JSON.stringify(payload.error); throw new Error(`rpc ${method} error: ${message}`); } return payload.result; } function startBtcWatcher({ notifyMerchant }) { const rpcUrls = getBitcoinRpcUrls(); if (!rpcUrls.length) { console.warn('[btcWatcher] No Bitcoin RPC URLs configured. Skipping watcher.'); return; } const pollInterval = Number(process.env.TX_POLL_INTERVAL_MS || 180000); const maxPerCycle = Number(process.env.MAX_TX_CHECK_PER_CYCLE || 20); const minConfirmations = Number(process.env.MIN_CONFIRMATIONS || 1); const pickRpcUrl = createRoundRobinPicker(rpcUrls); let running = false; async function poll() { if (running) return; running = true; try { const db = await readDb(); const pending = db.txs.filter((tx) => tx.network === BTC_NETWORK && tx.status === 'pending'); if (pending.length === 0) { running = false; return; } const rpcUrl = pickRpcUrl(); const now = new Date().toISOString(); const confirmedEvents = []; for (const tx of pending.slice(0, maxPerCycle)) { try { const result = await callRpc(rpcUrl, 'getrawtransaction', [tx.txHash, true]); const confirmations = Number(result?.confirmations || 0); tx.confirmations = confirmations; tx.lastCheckedAt = now; tx.updatedAt = now; if (confirmations >= minConfirmations) { tx.status = 'confirmed'; const order = db.orders.find((item) => item.id === tx.orderId); if (order) { order.status = 'paid'; order.updatedAt = now; confirmedEvents.push({ order, tx }); } } } catch (error) { tx.lastCheckedAt = now; console.warn('[btcWatcher] rpc error', tx.txHash, error.message); } } await writeDb(db); for (const event of confirmedEvents) { await notifyMerchant(event.order, event.tx); } } catch (error) { console.error('[btcWatcher] poll error', error.message); } finally { running = false; } } poll(); setInterval(poll, pollInterval); console.log(`[btcWatcher] started with interval ${pollInterval}ms`); } module.exports = { startBtcWatcher };