104 行
2.9 KiB
JavaScript
104 行
2.9 KiB
JavaScript
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 };
|