#!/usr/bin/env node /** * Price Feed Keeper Service * Automatically updates price feeds at regular intervals * * Usage: * node scripts/reserve/keeper-service.js * * Environment Variables: * RPC_URL_138 - ChainID 138 RPC endpoint * KEEPER_PRIVATE_KEY - Keeper wallet private key * PRICE_FEED_KEEPER_ADDRESS - PriceFeedKeeper contract address * UPDATE_INTERVAL - Update interval in seconds (default: 30) */ const { ethers } = require('ethers'); const fs = require('fs'); const path = require('path'); // Load environment variables require('dotenv').config(); // Configuration const CONFIG = { rpcUrl: process.env.RPC_URL_138 || 'http://localhost:8545', keeperPrivateKey: process.env.KEEPER_PRIVATE_KEY, keeperAddress: process.env.PRICE_FEED_KEEPER_ADDRESS, updateInterval: parseInt(process.env.UPDATE_INTERVAL || '6') * 1000, // PMM mesh default cadence maxRetries: 3, retryDelay: 5000, // 5 seconds }; // ABI for PriceFeedKeeper const KEEPER_ABI = [ "function checkUpkeep() external view returns (bool needsUpdate, address[] memory assets)", "function performUpkeep() external returns (bool success, address[] memory updatedAssets)", "function updateAssets(address[] calldata assets) external", "function getTrackedAssets() external view returns (address[] memory)", "function needsUpdate(address asset) external view returns (bool)", "function updateInterval() external view returns (uint256)", "function maxUpdatesPerCall() external view returns (uint256)", "event PriceFeedsUpdated(address[] assets, uint256 timestamp)" ]; class PriceFeedKeeperService { constructor() { if (!CONFIG.keeperPrivateKey) { throw new Error('KEEPER_PRIVATE_KEY environment variable is required'); } if (!CONFIG.keeperAddress) { throw new Error('PRICE_FEED_KEEPER_ADDRESS environment variable is required'); } this.provider = new ethers.JsonRpcProvider(CONFIG.rpcUrl); this.wallet = new ethers.Wallet(CONFIG.keeperPrivateKey, this.provider); this.keeper = new ethers.Contract(CONFIG.keeperAddress, KEEPER_ABI, this.wallet); this.isRunning = false; this.updateCount = 0; this.errorCount = 0; } async start() { console.log('=== Price Feed Keeper Service ==='); console.log(`RPC URL: ${CONFIG.rpcUrl}`); console.log(`Keeper Address: ${CONFIG.keeperAddress}`); console.log(`Wallet Address: ${this.wallet.address}`); console.log(`Update Interval: ${CONFIG.updateInterval / 1000} seconds`); console.log(''); // Verify keeper contract try { const trackedAssets = await this.keeper.getTrackedAssets(); const updateInterval = await this.keeper.updateInterval(); const maxUpdates = await this.keeper.maxUpdatesPerCall(); console.log(`Tracked Assets: ${trackedAssets.length}`); trackedAssets.forEach((asset, i) => { console.log(` ${i + 1}. ${asset}`); }); console.log(`Update Interval: ${updateInterval.toString()} seconds`); console.log(`Max Updates Per Call: ${maxUpdates.toString()}`); console.log(''); } catch (error) { console.error('Error verifying keeper contract:', error.message); process.exit(1); } this.isRunning = true; console.log('Keeper service started. Press Ctrl+C to stop.'); console.log(''); // Start update loop this.updateLoop(); } async updateLoop() { while (this.isRunning) { try { await this.performUpkeep(); } catch (error) { console.error(`Error in update loop: ${error.message}`); this.errorCount++; } // Wait for next interval await this.sleep(CONFIG.updateInterval); } } async performUpkeep() { try { // Check if upkeep is needed const [needsUpdate, assets] = await this.keeper.checkUpkeep(); if (!needsUpdate || assets.length === 0) { console.log(`[${new Date().toISOString()}] No updates needed`); return; } console.log(`[${new Date().toISOString()}] Updating ${assets.length} asset(s)...`); // Perform upkeep let retries = 0; let success = false; while (retries < CONFIG.maxRetries && !success) { try { const tx = await this.keeper.performUpkeep(); console.log(` Transaction sent: ${tx.hash}`); const receipt = await tx.wait(); if (receipt.status === 1) { success = true; this.updateCount++; // Parse events const events = receipt.logs.filter(log => { try { const parsed = this.keeper.interface.parseLog(log); return parsed.name === 'PriceFeedsUpdated'; } catch { return false; } }); if (events.length > 0) { const event = this.keeper.interface.parseLog(events[0]); console.log(` Updated assets: ${event.args.assets.length}`); console.log(` Timestamp: ${event.args.timestamp.toString()}`); } console.log(` ✓ Update successful (Gas: ${receipt.gasUsed.toString()})`); } else { throw new Error('Transaction failed'); } } catch (error) { retries++; if (retries < CONFIG.maxRetries) { console.log(` Retry ${retries}/${CONFIG.maxRetries}...`); await this.sleep(CONFIG.retryDelay); } else { throw error; } } } if (!success) { throw new Error('Failed after retries'); } } catch (error) { console.error(` ✗ Update failed: ${error.message}`); this.errorCount++; throw error; } } async stop() { console.log('\nStopping keeper service...'); this.isRunning = false; console.log('\n=== Statistics ==='); console.log(`Total Updates: ${this.updateCount}`); console.log(`Total Errors: ${this.errorCount}`); console.log(`Success Rate: ${this.updateCount > 0 ? ((this.updateCount / (this.updateCount + this.errorCount)) * 100).toFixed(2) : 0}%`); process.exit(0); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // Main execution if (require.main === module) { const keeper = new PriceFeedKeeperService(); // Handle graceful shutdown process.on('SIGINT', () => keeper.stop()); process.on('SIGTERM', () => keeper.stop()); // Start keeper keeper.start().catch(error => { console.error('Fatal error:', error); process.exit(1); }); } module.exports = PriceFeedKeeperService;