From e4f9d8c197e44aa1614ac69d6c9c1ea662a6901d Mon Sep 17 00:00:00 2001 From: Muhammet Selim Ferah Date: Fri, 19 Dec 2025 01:18:11 +0100 Subject: [PATCH] refactor: improve validator handling for L1 chains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Count unique validators by nodeId in /validators/network/total endpoint - Filter L1 validators to only include active ones (remainingBalance > 0) - Add weight and remainingBalance fields for L1 validators - Use weight field from /l1Validators, amountStaked from /validators - Remove dead fetchSnowPeerValidators method and unused constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/models/chain.js | 4 +- src/routes/validatorRoutes.js | 14 +++-- src/services/chainService.js | 112 ++++++++-------------------------- 3 files changed, 38 insertions(+), 92 deletions(-) diff --git a/src/models/chain.js b/src/models/chain.js index 1a9fdc7..a37cc6d 100644 --- a/src/models/chain.js +++ b/src/models/chain.js @@ -32,11 +32,13 @@ const chainSchema = new mongoose.Schema({ validators: [{ nodeId: String, txHash: String, - amountStaked: String, + amountStaked: String, // From /validators endpoint (traditional subnets) + weight: String, // From /l1Validators endpoint (L1 chains) startTimestamp: Number, endTimestamp: Number, validationStatus: String, uptimePerformance: Number, + remainingBalance: String, avalancheGoVersion: String }], lastUpdated: { type: Date, default: Date.now }, diff --git a/src/routes/validatorRoutes.js b/src/routes/validatorRoutes.js index 68357bb..e229d3d 100644 --- a/src/routes/validatorRoutes.js +++ b/src/routes/validatorRoutes.js @@ -62,13 +62,19 @@ router.get('/validators/network/total', async (req, res) => { validators: { $exists: true, $ne: [] } }).select('validators').lean(); - const totalValidators = chains.reduce((sum, chain) => - sum + (chain.validators ? chain.validators.length : 0), 0 - ); + // Collect unique validators by nodeId + const uniqueNodeIds = new Set(); + chains.forEach(chain => { + if (chain.validators) { + chain.validators.forEach(v => { + if (v.nodeId) uniqueNodeIds.add(v.nodeId); + }); + } + }); res.json({ success: true, - totalValidators, + totalValidators: uniqueNodeIds.size, chainsWithValidators: chains.length, timestamp: new Date().toISOString() }); diff --git a/src/services/chainService.js b/src/services/chainService.js index 8e8415e..99e97d5 100644 --- a/src/services/chainService.js +++ b/src/services/chainService.js @@ -4,8 +4,6 @@ const config = require('../config/config'); const cacheManager = require('../utils/cacheManager'); const logger = require('../utils/logger'); -const SNOWPEER_BASE_URL = 'https://api.snowpeer.io/v1'; -const DEFAULT_NETWORK = 'mainnet'; class ChainService { constructor() { @@ -285,16 +283,19 @@ class ChainService { const data = await response.json(); // L1Validators response format is different, so we need to transform it - const transformedValidators = (data.validators || []).map(v => ({ - nodeId: v.nodeId || '', - txHash: v.validationId || v.validationIdHex || '', - amountStaked: v.weight ? v.weight.toString() : '0', - startTimestamp: v.creationTimestamp || 0, - endTimestamp: 0, // End timestamp might not be available - validationStatus: 'active', - uptimePerformance: 100, // Default value - avalancheGoVersion: '' // Not available in this endpoint - })); + // Only include validators with remainingBalance > 0 (active validators) + const transformedValidators = (data.validators || []) + .filter(v => v.remainingBalance > 0) + .map(v => ({ + nodeId: v.nodeId || '', + txHash: v.validationId || v.validationIdHex || '', + weight: v.weight ? v.weight.toString() : '0', + startTimestamp: v.creationTimestamp || 0, + endTimestamp: 0, // End timestamp might not be available + validationStatus: 'active', + remainingBalance: v.remainingBalance.toString(), + avalancheGoVersion: '' // Not available in this endpoint + })); allValidators = transformedValidators; @@ -325,16 +326,18 @@ class ChainService { const nextPageData = await nextPageResponse.json(); - const nextPageValidators = (nextPageData.validators || []).map(v => ({ - nodeId: v.nodeId || '', - txHash: v.validationId || v.validationIdHex || '', - amountStaked: v.weight ? v.weight.toString() : '0', - startTimestamp: v.creationTimestamp || 0, - endTimestamp: 0, - validationStatus: 'active', - uptimePerformance: 100, - avalancheGoVersion: '' - })); + const nextPageValidators = (nextPageData.validators || []) + .filter(v => v.remainingBalance > 0) + .map(v => ({ + nodeId: v.nodeId || '', + txHash: v.validationId || v.validationIdHex || '', + weight: v.weight ? v.weight.toString() : '0', + startTimestamp: v.creationTimestamp || 0, + endTimestamp: 0, + validationStatus: 'active', + remainingBalance: v.remainingBalance.toString(), + avalancheGoVersion: '' + })); allValidators = [...allValidators, ...nextPageValidators]; l1NextPageToken = nextPageData.nextPageToken; @@ -386,71 +389,6 @@ class ChainService { } } - /** - * Fetch validators from SnowPeer API - */ - async fetchSnowPeerValidators(subnetId, chainId) { - if (!subnetId) return []; - - try { - logger.info(`Fetching validators from SnowPeer for subnet ${subnetId}`); - - const response = await this.retryRequest(async () => { - return await axios.get(`${SNOWPEER_BASE_URL}/blockchains`, { - params: { - network: DEFAULT_NETWORK, - subnetID: subnetId - }, - timeout: config.api?.snowpeer?.timeout || 30000, - }); - }); - - const apiResponse = response.data; - const blockchains = apiResponse.blockchains || []; - - // Find the blockchain matching our chainId or use the first one if there's only one - let blockchain = blockchains.find(b => b.blockchainID === chainId || b.chainID === chainId); - if (!blockchain && blockchains.length === 1) { - blockchain = blockchains[0]; - } - - if (!blockchain) { - logger.warn(`No blockchain found in SnowPeer response for subnet ${subnetId}`); - return []; - } - - // Extract validators from the blockchain object - const validators = blockchain.validators || []; - - if (validators.length === 0) { - logger.info(`No validators found in SnowPeer for subnet ${subnetId}`); - return []; - } - - // Transform SnowPeer validator format to our schema - const transformedValidators = validators.map(v => ({ - nodeId: v.nodeID || v.nodeId || '', - txHash: v.txID || v.txHash || '', - amountStaked: v.weight ? v.weight.toString() : (v.stake ? v.stake.toString() : '0'), - startTimestamp: v.startTime || v.startTimestamp || 0, - endTimestamp: v.endTime || v.endTimestamp || 0, - validationStatus: v.connected === false ? 'inactive' : 'active', - uptimePerformance: v.uptime || 100, - avalancheGoVersion: v.version || '' - })); - - logger.info(`Successfully fetched ${transformedValidators.length} validators from SnowPeer for subnet ${subnetId}`); - return transformedValidators; - - } catch (error) { - logger.error(`Error fetching SnowPeer validators for subnet ${subnetId}:`, { - error: error.message, - status: error.response?.status - }); - return []; - } - } - async fetchAlternativeValidators(chainId) { if (!chainId) return [];