Some checks failed
Verify Deployment / Verify Deployment (push) Has been cancelled
CI/CD Pipeline / Solidity Contracts (push) Has been cancelled
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
212 lines
7.3 KiB
JavaScript
Executable File
212 lines
7.3 KiB
JavaScript
Executable File
#!/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 || '30') * 1000, // Convert to milliseconds
|
|
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;
|
|
|