From 89ca6c0f86e34874544a5720f3892cefe3661b91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:50:23 +0000 Subject: [PATCH 1/4] Initial plan From b463d5d9adac8846949740c51cea7b9a8bfae425 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:55:14 +0000 Subject: [PATCH 2/4] Add chain data validation function with 6 error detection rules Co-authored-by: Johnaverse <110527930+Johnaverse@users.noreply.github.com> --- README.md | 50 +++++++++++ dataService.js | 225 +++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 11 ++- 3 files changed, 285 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9548cf0..284967f 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,56 @@ Reload data from all sources. } ``` +### `GET /validate` +Validate chain data for potential human errors across all three data sources. + +This endpoint analyzes the chain data and identifies potential inconsistencies or errors based on the following rules: + +1. **Rule 1 - Relation Conflicts**: Assumes graph relations are always true and finds conflicts with other sources +2. **Rule 2 - slip44/Testnet Mismatch**: Chains with slip44=1 but isTestnet=false +3. **Rule 3 - Name/Tag Mismatch**: Chain full names containing "Testnet" or "Denver" but not tagged as Testnet +4. **Rule 4 - Sepolia/Hoodie Networks**: Chains containing "sepolia" or "hoodie" keywords but not identifying as L2 or having no relations +5. **Rule 5 - Status Conflicts**: Deprecated status conflicts across different sources +6. **Rule 6 - Goerli Deprecation**: Chains containing "Goerli" keyword but not marked as deprecated + +**Response:** +```json +{ + "totalErrors": 85, + "summary": { + "rule1": 3, + "rule2": 57, + "rule3": 16, + "rule4": 1, + "rule5": 1, + "rule6": 7 + }, + "errorsByRule": { + "rule1_relation_conflicts": [...], + "rule2_slip44_testnet_mismatch": [...], + "rule3_name_testnet_mismatch": [...], + "rule4_sepolia_hoodie_issues": [...], + "rule5_status_conflicts": [...], + "rule6_goerli_not_deprecated": [...] + }, + "allErrors": [...] +} +``` + +**Example Error Object:** +```json +{ + "rule": 6, + "chainId": 5, + "chainName": "Goerli", + "type": "goerli_not_deprecated", + "message": "Chain 5 (Goerli) contains \"Goerli\" but is not marked as deprecated", + "fullName": "Goerli", + "status": "active", + "statusInSources": [] +} +``` + ## Data Structure ### Chain Object (from `/chains` endpoints) diff --git a/dataService.js b/dataService.js index 8e605da..083b1af 100644 --- a/dataService.js +++ b/dataService.js @@ -823,3 +823,228 @@ export function getAllEndpoints() { return cachedData.indexed.all.map(extractEndpoints); } + +/** + * Validate chain data for potential human errors + * Returns an object with validation results categorized by error type + */ +export function validateChainData() { + if (!cachedData.indexed || !cachedData.theGraph || !cachedData.chainlist || !cachedData.chains) { + return { + error: 'Data not loaded. Please reload data sources first.', + errors: [] + }; + } + + const errors = []; + + // Helper function to get chain from different sources + const getChainFromSource = (chainId, source) => { + if (source === 'theGraph') { + return cachedData.theGraph.networks?.find(n => { + if (n.caip2Id) { + const match = n.caip2Id.match(/^eip155:(\d+)$/); + return match && parseInt(match[1]) === chainId; + } + return false; + }); + } else if (source === 'chainlist') { + return cachedData.chainlist?.find(c => c.chainId === chainId); + } else if (source === 'chains') { + return cachedData.chains?.find(c => c.chainId === chainId); + } + return null; + }; + + // Build network ID to chain ID map for relation checking + const networkIdToChainId = buildNetworkIdToChainIdMap(cachedData.theGraph); + + // Iterate through all indexed chains + Object.values(cachedData.indexed.byChainId).forEach(chain => { + const chainId = chain.chainId; + + // Rule 1: Conflicts between graph relations and other sources + // Assume graph relations are always true, check if other sources conflict + if (chain.relations && chain.relations.length > 0) { + const graphRelations = chain.relations.filter(r => r.source === 'theGraph'); + + graphRelations.forEach(graphRel => { + // Check testnetOf relation + if (graphRel.kind === 'testnetOf' && graphRel.chainId) { + // Check if chain is marked as Testnet + if (!chain.tags.includes('Testnet')) { + errors.push({ + rule: 1, + chainId: chainId, + chainName: chain.name, + type: 'relation_tag_conflict', + message: `Chain ${chainId} (${chain.name}) has testnetOf relation but is not tagged as Testnet`, + graphRelation: graphRel + }); + } + + // Check if other sources have conflicting data + const chainlistChain = getChainFromSource(chainId, 'chainlist'); + if (chainlistChain && chainlistChain.isTestnet === false) { + errors.push({ + rule: 1, + chainId: chainId, + chainName: chain.name, + type: 'relation_source_conflict', + message: `Chain ${chainId} (${chain.name}) has testnetOf relation in theGraph but isTestnet=false in chainlist`, + graphRelation: graphRel, + chainlistData: { isTestnet: chainlistChain.isTestnet } + }); + } + } + + // Check l2Of relation + if (graphRel.kind === 'l2Of' && graphRel.chainId) { + // Check if chain is marked as L2 + if (!chain.tags.includes('L2')) { + errors.push({ + rule: 1, + chainId: chainId, + chainName: chain.name, + type: 'relation_tag_conflict', + message: `Chain ${chainId} (${chain.name}) has l2Of relation but is not tagged as L2`, + graphRelation: graphRel + }); + } + } + }); + } + + // Rule 2: slip44 = 1 but isTestnet = false + const chainlistChain = getChainFromSource(chainId, 'chainlist'); + const chainsChain = getChainFromSource(chainId, 'chains'); + + if (chainlistChain && chainlistChain.slip44 === 1 && chainlistChain.isTestnet === false) { + errors.push({ + rule: 2, + chainId: chainId, + chainName: chain.name, + type: 'slip44_testnet_mismatch', + message: `Chain ${chainId} (${chain.name}) has slip44=1 (testnet indicator) but isTestnet=false in chainlist`, + slip44: chainlistChain.slip44, + isTestnet: chainlistChain.isTestnet + }); + } + + if (chainsChain && chainsChain.slip44 === 1 && !chain.tags.includes('Testnet')) { + errors.push({ + rule: 2, + chainId: chainId, + chainName: chain.name, + type: 'slip44_testnet_mismatch', + message: `Chain ${chainId} (${chain.name}) has slip44=1 (testnet indicator) in chains.json but not tagged as Testnet`, + slip44: chainsChain.slip44, + tags: chain.tags + }); + } + + // Rule 3: Chain full name includes "Testnet" or "Denver" but identifying as Mainnet + const fullName = chain.theGraph?.fullName || chain.name || ''; + const nameLower = fullName.toLowerCase(); + + if ((nameLower.includes('testnet') || nameLower.includes('denver')) && !chain.tags.includes('Testnet')) { + errors.push({ + rule: 3, + chainId: chainId, + chainName: chain.name, + type: 'name_testnet_mismatch', + message: `Chain ${chainId} (${chain.name}) has "Testnet" or "Denver" in full name "${fullName}" but not tagged as Testnet`, + fullName: fullName, + tags: chain.tags + }); + } + + // Rule 4: Chain name contains "sepolia" or "hoodie" but not identifying as L2 or no relations with other networks + if (nameLower.includes('sepolia') || nameLower.includes('hoodie')) { + const hasL2Tag = chain.tags.includes('L2'); + const hasRelations = chain.relations && chain.relations.length > 0; + + if (!hasL2Tag && !hasRelations) { + errors.push({ + rule: 4, + chainId: chainId, + chainName: chain.name, + type: 'sepolia_hoodie_no_l2_or_relations', + message: `Chain ${chainId} (${chain.name}) contains "sepolia" or "hoodie" but not tagged as L2 and has no relations`, + fullName: fullName, + tags: chain.tags, + relations: chain.relations + }); + } + } + + // Rule 5: Status "deprecated" conflicts in different sources + const statuses = []; + + if (chainlistChain && chainlistChain.status) { + statuses.push({ source: 'chainlist', status: chainlistChain.status }); + } + if (chainsChain && chainsChain.status) { + statuses.push({ source: 'chains', status: chainsChain.status }); + } + + // Check for conflicts + const deprecatedInSources = statuses.filter(s => s.status === 'deprecated'); + const activeInSources = statuses.filter(s => s.status === 'active'); + + if (deprecatedInSources.length > 0 && activeInSources.length > 0) { + errors.push({ + rule: 5, + chainId: chainId, + chainName: chain.name, + type: 'status_conflict', + message: `Chain ${chainId} (${chain.name}) has conflicting status across sources`, + statuses: statuses + }); + } + + // Rule 6: Chains containing "Goerli" not marked as deprecated + if (nameLower.includes('goerli')) { + const isDeprecated = chain.status === 'deprecated' || + (chainlistChain && chainlistChain.status === 'deprecated') || + (chainsChain && chainsChain.status === 'deprecated'); + + if (!isDeprecated) { + errors.push({ + rule: 6, + chainId: chainId, + chainName: chain.name, + type: 'goerli_not_deprecated', + message: `Chain ${chainId} (${chain.name}) contains "Goerli" but is not marked as deprecated`, + fullName: fullName, + status: chain.status, + statusInSources: statuses + }); + } + } + }); + + // Group errors by rule + const errorsByRule = { + rule1_relation_conflicts: errors.filter(e => e.rule === 1), + rule2_slip44_testnet_mismatch: errors.filter(e => e.rule === 2), + rule3_name_testnet_mismatch: errors.filter(e => e.rule === 3), + rule4_sepolia_hoodie_issues: errors.filter(e => e.rule === 4), + rule5_status_conflicts: errors.filter(e => e.rule === 5), + rule6_goerli_not_deprecated: errors.filter(e => e.rule === 6) + }; + + return { + totalErrors: errors.length, + errorsByRule: errorsByRule, + summary: { + rule1: errorsByRule.rule1_relation_conflicts.length, + rule2: errorsByRule.rule2_slip44_testnet_mismatch.length, + rule3: errorsByRule.rule3_name_testnet_mismatch.length, + rule4: errorsByRule.rule4_sepolia_hoodie_issues.length, + rule5: errorsByRule.rule5_status_conflicts.length, + rule6: errorsByRule.rule6_goerli_not_deprecated.length + }, + allErrors: errors + }; +} diff --git a/index.js b/index.js index 5410f61..b12d052 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ import Fastify from 'fastify'; -import { loadData, getCachedData, searchChains, getChainById, getAllChains, getAllRelations, getRelationsById, getEndpointsById, getAllEndpoints } from './dataService.js'; +import { loadData, getCachedData, searchChains, getChainById, getAllChains, getAllRelations, getRelationsById, getEndpointsById, getAllEndpoints, validateChainData } from './dataService.js'; const fastify = Fastify({ logger: true @@ -204,6 +204,14 @@ fastify.post('/reload', async (request, reply) => { } }); +/** + * Validate chain data for potential human errors + */ +fastify.get('/validate', async (request, reply) => { + const validationResults = validateChainData(); + return validationResults; +}); + /** * Root endpoint with API information */ @@ -224,6 +232,7 @@ fastify.get('/', async (request, reply) => { '/sources': 'Get data sources status', '/slip44': 'Get all SLIP-0044 coin types as JSON', '/slip44/:coinType': 'Get specific SLIP-0044 coin type by ID', + '/validate': 'Validate chain data for potential human errors', '/reload': 'Reload data from sources (POST)' }, dataSources: [ From 35157d7c456436b8045317f342d691fc33462dbb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:57:09 +0000 Subject: [PATCH 3/4] Add clarifying comment for Denver keyword check Co-authored-by: Johnaverse <110527930+Johnaverse@users.noreply.github.com> --- dataService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/dataService.js b/dataService.js index 083b1af..5cbb3b3 100644 --- a/dataService.js +++ b/dataService.js @@ -944,6 +944,7 @@ export function validateChainData() { } // Rule 3: Chain full name includes "Testnet" or "Denver" but identifying as Mainnet + // Note: "Denver" is as specified in requirements (may refer to development networks) const fullName = chain.theGraph?.fullName || chain.name || ''; const nameLower = fullName.toLowerCase(); From 80fece88e9f99169da98cb6dae4de369526e9b91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:15:23 +0000 Subject: [PATCH 4/4] Fix Rule 3: Change "Denver" to "Devnet" for validation keyword Co-authored-by: Johnaverse <110527930+Johnaverse@users.noreply.github.com> --- README.md | 2 +- dataService.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 284967f..3d833bd 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ This endpoint analyzes the chain data and identifies potential inconsistencies o 1. **Rule 1 - Relation Conflicts**: Assumes graph relations are always true and finds conflicts with other sources 2. **Rule 2 - slip44/Testnet Mismatch**: Chains with slip44=1 but isTestnet=false -3. **Rule 3 - Name/Tag Mismatch**: Chain full names containing "Testnet" or "Denver" but not tagged as Testnet +3. **Rule 3 - Name/Tag Mismatch**: Chain full names containing "Testnet" or "Devnet" but not tagged as Testnet 4. **Rule 4 - Sepolia/Hoodie Networks**: Chains containing "sepolia" or "hoodie" keywords but not identifying as L2 or having no relations 5. **Rule 5 - Status Conflicts**: Deprecated status conflicts across different sources 6. **Rule 6 - Goerli Deprecation**: Chains containing "Goerli" keyword but not marked as deprecated diff --git a/dataService.js b/dataService.js index 5cbb3b3..654ace1 100644 --- a/dataService.js +++ b/dataService.js @@ -943,18 +943,17 @@ export function validateChainData() { }); } - // Rule 3: Chain full name includes "Testnet" or "Denver" but identifying as Mainnet - // Note: "Denver" is as specified in requirements (may refer to development networks) + // Rule 3: Chain full name includes "Testnet" or "Devnet" but identifying as Mainnet const fullName = chain.theGraph?.fullName || chain.name || ''; const nameLower = fullName.toLowerCase(); - if ((nameLower.includes('testnet') || nameLower.includes('denver')) && !chain.tags.includes('Testnet')) { + if ((nameLower.includes('testnet') || nameLower.includes('devnet')) && !chain.tags.includes('Testnet')) { errors.push({ rule: 3, chainId: chainId, chainName: chain.name, type: 'name_testnet_mismatch', - message: `Chain ${chainId} (${chain.name}) has "Testnet" or "Denver" in full name "${fullName}" but not tagged as Testnet`, + message: `Chain ${chainId} (${chain.name}) has "Testnet" or "Devnet" in full name "${fullName}" but not tagged as Testnet`, fullName: fullName, tags: chain.tags });