文件
capay/app/src/watchers/evmWatcher.js
2026-02-01 14:52:46 +08:00

77 行
2.3 KiB
JavaScript

const { JsonRpcProvider } = require('ethers');
const { readDb, writeDb } = require('../storage');
const { getEthereumRpcUrls, createRoundRobinPicker } = require('../utils/rpc');
function startEvmWatcher({ notifyMerchant }) {
const rpcUrls = getEthereumRpcUrls();
if (!rpcUrls.length) {
console.warn('[evmWatcher] No Ethereum 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 === 'evm:ethereum' && tx.status === 'pending');
if (pending.length === 0) {
running = false;
return;
}
const rpcUrl = pickRpcUrl();
const provider = new JsonRpcProvider(rpcUrl);
const latestBlock = await provider.getBlockNumber();
const now = new Date().toISOString();
const confirmedEvents = [];
for (const tx of pending.slice(0, maxPerCycle)) {
const receipt = await provider.getTransactionReceipt(tx.txHash);
tx.lastCheckedAt = now;
if (!receipt) continue;
const confirmations = latestBlock - receipt.blockNumber + 1;
tx.confirmations = confirmations;
tx.updatedAt = now;
if (receipt.status === 1 && 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 });
}
} else if (receipt.status === 0) {
tx.status = 'failed';
}
}
await writeDb(db);
for (const event of confirmedEvents) {
await notifyMerchant(event.order, event.tx);
}
} catch (error) {
console.error('[evmWatcher] poll error', error.message);
} finally {
running = false;
}
}
poll();
setInterval(poll, pollInterval);
console.log(`[evmWatcher] started with interval ${pollInterval}ms`);
}
module.exports = { startEvmWatcher };