Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "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

**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)
Expand Down
225 changes: 225 additions & 0 deletions dataService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 "Devnet" but identifying as Mainnet
const fullName = chain.theGraph?.fullName || chain.name || '';
const nameLower = fullName.toLowerCase();

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 "Devnet" 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
};
}
11 changes: 10 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
*/
Expand All @@ -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: [
Expand Down