Initial capay platform scaffold
这个提交包含在:
76
app/src/watchers/evmWatcher.js
普通文件
76
app/src/watchers/evmWatcher.js
普通文件
@@ -0,0 +1,76 @@
|
||||
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 };
|
||||
在新工单中引用
屏蔽一个用户