Files
wownerogue/src/rpc/rpcService.js
T
2025-09-24 14:06:42 -04:00

267 lines
8.3 KiB
JavaScript

/**
* Enhanced RPC Service with Failover Support
* Handles blockchain RPC calls with automatic failover and monitoring
*/
const axios = require('axios');
const CONSOLE_LOGGING = true; // Temporarily enable for debugging
class RPCService {
constructor() {
this.primaryEndpoint = process.env.PRIMARY_RPC_ENDPOINT || 'http://jw-nodes:18081';
this.fallbackEndpoint = process.env.FALLBACK_RPC_ENDPOINT || 'http://jw-nodes:18081';
this.currentEndpoint = this.primaryEndpoint;
this.failoverActive = false;
this.lastBlockHeight = 0;
this.consecutiveFailures = 0;
this.maxFailures = 3;
if (CONSOLE_LOGGING) {
console.log(`🔗 RPC Service initialized`);
console.log(`Primary: ${this.primaryEndpoint}`);
console.log(`Fallback: ${this.fallbackEndpoint}`);
}
}
/**
* Make RPC call with automatic failover
*/
async makeRPCCall(method, params = {}) {
const request = {
jsonrpc: '2.0',
id: '0',
method: method,
params: params
};
try {
const response = await axios.post(`${this.currentEndpoint}/json_rpc`, request, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000
});
if (response.data.error) {
throw new Error(`RPC Error: ${response.data.error.message}`);
}
// Reset failure counter on success
this.consecutiveFailures = 0;
// Switch back to primary if we were using fallback
if (this.failoverActive && this.currentEndpoint !== this.primaryEndpoint) {
if (CONSOLE_LOGGING) {
console.log('🔄 Switching back to primary RPC endpoint');
}
this.currentEndpoint = this.primaryEndpoint;
this.failoverActive = false;
}
return response.data.result;
} catch (error) {
this.consecutiveFailures++;
if (CONSOLE_LOGGING) {
console.error(`❌ RPC call failed (${this.consecutiveFailures}/${this.maxFailures}):`, error.message);
}
// Try failover if we haven't exceeded max failures
if (this.consecutiveFailures >= this.maxFailures && !this.failoverActive) {
return await this.tryFailover(method, params);
}
throw error;
}
}
/**
* Attempt failover to backup endpoint
*/
async tryFailover(method, params) {
if (this.currentEndpoint === this.fallbackEndpoint) {
throw new Error('Both primary and fallback RPC endpoints failed');
}
if (CONSOLE_LOGGING) {
console.log('🔄 Attempting RPC failover to backup endpoint');
}
this.currentEndpoint = this.fallbackEndpoint;
this.failoverActive = true;
this.consecutiveFailures = 0;
try {
const request = {
jsonrpc: '2.0',
id: '0',
method: method,
params: params
};
const response = await axios.post(`${this.currentEndpoint}/json_rpc`, request, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000
});
if (response.data.error) {
throw new Error(`RPC Error: ${response.data.error.message}`);
}
if (CONSOLE_LOGGING) {
console.log('✅ Failover successful');
}
return response.data.result;
} catch (error) {
if (CONSOLE_LOGGING) {
console.error('❌ Failover also failed:', error.message);
}
throw new Error('Both primary and fallback RPC endpoints failed');
}
}
/**
* Get current block height
*/
async getBlockHeight() {
try {
const result = await this.makeRPCCall('getblockcount');
const blockHeight = result.count;
if (blockHeight !== this.lastBlockHeight) {
this.lastBlockHeight = blockHeight;
if (CONSOLE_LOGGING) {
console.log(`📊 Block height updated: ${blockHeight}`);
}
}
return blockHeight;
} catch (error) {
if (CONSOLE_LOGGING) {
console.error('❌ Failed to get block height:', error.message);
}
return this.lastBlockHeight; // Return last known height on failure
}
}
/**
* Get block information by height
*/
async getBlockByHeight(height) {
try {
return await this.makeRPCCall('get_block', { height });
} catch (error) {
if (CONSOLE_LOGGING) {
console.error(`❌ Failed to get block ${height}:`, error.message);
}
throw error;
}
}
/**
* Get network info
*/
async getNetworkInfo() {
try {
const [blockCount, networkInfo] = await Promise.all([
this.makeRPCCall('getblockcount'),
this.makeRPCCall('get_info')
]);
return {
blockHeight: blockCount.count,
difficulty: networkInfo.difficulty,
hashRate: networkInfo.difficulty / 120, // Approximate hash rate
networkType: networkInfo.mainnet ? 'mainnet' : 'testnet',
status: networkInfo.status
};
} catch (error) {
if (CONSOLE_LOGGING) {
console.error('❌ Failed to get network info:', error.message);
}
throw error;
}
}
/**
* Health check for RPC endpoints
*/
async healthCheck() {
const checks = {
primary: { endpoint: this.primaryEndpoint, status: 'unknown', responseTime: 0 },
fallback: { endpoint: this.fallbackEndpoint, status: 'unknown', responseTime: 0 },
current: this.currentEndpoint,
failoverActive: this.failoverActive,
consecutiveFailures: this.consecutiveFailures
};
// Test primary endpoint
try {
const start = Date.now();
await axios.post(`${this.primaryEndpoint}/json_rpc`, {
jsonrpc: '2.0',
id: '0',
method: 'getblockcount'
}, { timeout: 5000 });
checks.primary.status = 'healthy';
checks.primary.responseTime = Date.now() - start;
} catch (error) {
checks.primary.status = 'unhealthy';
checks.primary.error = error.message;
}
// Test fallback endpoint (if different)
if (this.fallbackEndpoint !== this.primaryEndpoint) {
try {
const start = Date.now();
await axios.post(`${this.fallbackEndpoint}/json_rpc`, {
jsonrpc: '2.0',
id: '0',
method: 'getblockcount'
}, { timeout: 5000 });
checks.fallback.status = 'healthy';
checks.fallback.responseTime = Date.now() - start;
} catch (error) {
checks.fallback.status = 'unhealthy';
checks.fallback.error = error.message;
}
} else {
checks.fallback = checks.primary; // Same endpoint
}
return checks;
}
/**
* Get current endpoint status
*/
getStatus() {
return {
currentEndpoint: this.currentEndpoint,
failoverActive: this.failoverActive,
consecutiveFailures: this.consecutiveFailures,
lastBlockHeight: this.lastBlockHeight
};
}
// Legacy methods for backward compatibility
async daemonCall(method, params, callback) {
try {
const result = await this.makeRPCCall(method, params);
if (callback) callback(result);
return result;
} catch (error) {
if (callback) callback(null);
throw error;
}
}
getBlockHeightLegacy() {
return this.getBlockHeight();
}
}
module.exports = RPCService;