- Organized 252 files across project - Root directory: 187 → 2 files (98.9% reduction) - Moved configuration guides to docs/04-configuration/ - Moved troubleshooting guides to docs/09-troubleshooting/ - Moved quick start guides to docs/01-getting-started/ - Moved reports to reports/ directory - Archived temporary files - Generated comprehensive reports and documentation - Created maintenance scripts and guides All files organized according to established standards.
285 lines
8.6 KiB
JavaScript
Executable File
285 lines
8.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* On-Chain Verification Script
|
|
* Verifies token list entries against on-chain contracts using RPC calls
|
|
*
|
|
* RPC endpoints (fallback order):
|
|
* 1. https://rpc-http-pub.d-bis.org (primary)
|
|
* 2. https://rpc-core.d-bis.org (fallback)
|
|
*/
|
|
|
|
import { readFileSync } from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, resolve } from 'path';
|
|
import { ethers } from 'ethers';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
const RPC_ENDPOINTS = [
|
|
'https://rpc-http-pub.d-bis.org',
|
|
'https://rpc-core.d-bis.org'
|
|
];
|
|
|
|
const REQUIRED_CHAIN_ID = 138;
|
|
const REQUIRED_CHAIN_ID_HEX = '0x8a';
|
|
|
|
// ERC-20 ABI (minimal)
|
|
const ERC20_ABI = [
|
|
'function decimals() view returns (uint8)',
|
|
'function symbol() view returns (string)',
|
|
'function name() view returns (string)',
|
|
'function totalSupply() view returns (uint256)'
|
|
];
|
|
|
|
// Oracle ABI (Chainlink-compatible)
|
|
const ORACLE_ABI = [
|
|
'function latestRoundData() view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)',
|
|
'function decimals() view returns (uint8)',
|
|
'function description() view returns (string)'
|
|
];
|
|
|
|
async function getProvider() {
|
|
let lastError;
|
|
|
|
for (const rpcUrl of RPC_ENDPOINTS) {
|
|
try {
|
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
|
|
// Verify chain ID
|
|
const network = await provider.getNetwork();
|
|
const chainId = Number(network.chainId);
|
|
|
|
if (chainId !== REQUIRED_CHAIN_ID) {
|
|
throw new Error(`Chain ID mismatch: expected ${REQUIRED_CHAIN_ID}, got ${chainId}`);
|
|
}
|
|
|
|
// Test connection
|
|
await provider.getBlockNumber();
|
|
|
|
console.log(`✅ Connected to RPC: ${rpcUrl} (Chain ID: ${chainId})\n`);
|
|
return provider;
|
|
} catch (error) {
|
|
lastError = error;
|
|
console.log(`⚠️ Failed to connect to ${rpcUrl}: ${error.message}`);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
throw new Error(`Failed to connect to any RPC endpoint. Last error: ${lastError?.message}`);
|
|
}
|
|
|
|
async function verifyERC20Token(provider, token, index) {
|
|
const results = [];
|
|
const prefix = `Token[${index}] ${token.symbol || token.name}`;
|
|
|
|
try {
|
|
// Check if contract exists
|
|
const code = await provider.getCode(token.address);
|
|
if (code === '0x') {
|
|
results.push({ type: 'error', message: `${prefix}: No contract code at address ${token.address}` });
|
|
return results;
|
|
}
|
|
|
|
const contract = new ethers.Contract(token.address, ERC20_ABI, provider);
|
|
|
|
// Verify decimals
|
|
try {
|
|
const onChainDecimals = await contract.decimals();
|
|
if (Number(onChainDecimals) !== token.decimals) {
|
|
results.push({
|
|
type: 'error',
|
|
message: `${prefix}: Decimals mismatch - list: ${token.decimals}, on-chain: ${onChainDecimals}`
|
|
});
|
|
} else {
|
|
results.push({ type: 'success', message: `${prefix}: Decimals verified (${token.decimals})` });
|
|
}
|
|
} catch (error) {
|
|
results.push({ type: 'warning', message: `${prefix}: Failed to read decimals: ${error.message}` });
|
|
}
|
|
|
|
// Verify symbol (warn if different)
|
|
try {
|
|
const onChainSymbol = await contract.symbol();
|
|
if (onChainSymbol !== token.symbol) {
|
|
results.push({
|
|
type: 'warning',
|
|
message: `${prefix}: Symbol mismatch - list: "${token.symbol}", on-chain: "${onChainSymbol}"`
|
|
});
|
|
}
|
|
} catch (error) {
|
|
results.push({ type: 'warning', message: `${prefix}: Failed to read symbol: ${error.message}` });
|
|
}
|
|
|
|
// Verify name (warn if different)
|
|
try {
|
|
const onChainName = await contract.name();
|
|
if (onChainName !== token.name) {
|
|
results.push({
|
|
type: 'warning',
|
|
message: `${prefix}: Name mismatch - list: "${token.name}", on-chain: "${onChainName}"`
|
|
});
|
|
}
|
|
} catch (error) {
|
|
results.push({ type: 'warning', message: `${prefix}: Failed to read name: ${error.message}` });
|
|
}
|
|
|
|
// Verify totalSupply (optional)
|
|
try {
|
|
await contract.totalSupply();
|
|
results.push({ type: 'success', message: `${prefix}: totalSupply() callable` });
|
|
} catch (error) {
|
|
results.push({ type: 'warning', message: `${prefix}: totalSupply() failed: ${error.message}` });
|
|
}
|
|
|
|
} catch (error) {
|
|
results.push({ type: 'error', message: `${prefix}: Verification failed: ${error.message}` });
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async function verifyOracleToken(provider, token, index) {
|
|
const results = [];
|
|
const prefix = `Token[${index}] ${token.symbol || token.name} (Oracle)`;
|
|
|
|
try {
|
|
// Check if contract exists
|
|
const code = await provider.getCode(token.address);
|
|
if (code === '0x') {
|
|
results.push({ type: 'error', message: `${prefix}: No contract code at address ${token.address}` });
|
|
return results;
|
|
}
|
|
|
|
const contract = new ethers.Contract(token.address, ORACLE_ABI, provider);
|
|
|
|
// Verify latestRoundData
|
|
try {
|
|
await contract.latestRoundData();
|
|
results.push({ type: 'success', message: `${prefix}: latestRoundData() callable` });
|
|
} catch (error) {
|
|
results.push({ type: 'error', message: `${prefix}: latestRoundData() failed: ${error.message}` });
|
|
}
|
|
|
|
// Verify decimals
|
|
try {
|
|
const onChainDecimals = await contract.decimals();
|
|
if (Number(onChainDecimals) !== token.decimals) {
|
|
results.push({
|
|
type: 'error',
|
|
message: `${prefix}: Decimals mismatch - list: ${token.decimals}, on-chain: ${onChainDecimals}`
|
|
});
|
|
} else {
|
|
results.push({ type: 'success', message: `${prefix}: Decimals verified (${token.decimals})` });
|
|
}
|
|
} catch (error) {
|
|
results.push({ type: 'warning', message: `${prefix}: Failed to read decimals: ${error.message}` });
|
|
}
|
|
|
|
} catch (error) {
|
|
results.push({ type: 'error', message: `${prefix}: Verification failed: ${error.message}` });
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function isOracleToken(token) {
|
|
return token.tags && (token.tags.includes('oracle') || token.tags.includes('pricefeed'));
|
|
}
|
|
|
|
async function verifyOnChain(filePath, required = false) {
|
|
console.log(`\n🔗 Verifying on-chain contracts: ${filePath}\n`);
|
|
|
|
// Read token list file
|
|
let tokenList;
|
|
try {
|
|
const fileContent = readFileSync(filePath, 'utf-8');
|
|
tokenList = JSON.parse(fileContent);
|
|
} catch (error) {
|
|
console.error('❌ Error reading or parsing token list file:');
|
|
console.error(` ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
let provider;
|
|
try {
|
|
provider = await getProvider();
|
|
} catch (error) {
|
|
if (required) {
|
|
console.error(`❌ ${error.message}`);
|
|
console.error('On-chain verification is required but RPC connection failed.');
|
|
process.exit(1);
|
|
} else {
|
|
console.log(`⚠️ ${error.message}`);
|
|
console.log('Skipping on-chain verification (optional mode)\n');
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (!tokenList.tokens || !Array.isArray(tokenList.tokens)) {
|
|
console.log('No tokens to verify.\n');
|
|
return 0;
|
|
}
|
|
|
|
const allResults = [];
|
|
|
|
for (const [index, token] of tokenList.tokens.entries()) {
|
|
let results;
|
|
if (isOracleToken(token)) {
|
|
results = await verifyOracleToken(provider, token, index);
|
|
} else {
|
|
results = await verifyERC20Token(provider, token, index);
|
|
}
|
|
allResults.push(...results);
|
|
}
|
|
|
|
// Report results
|
|
const errors = allResults.filter(r => r.type === 'error');
|
|
const warnings = allResults.filter(r => r.type === 'warning');
|
|
const successes = allResults.filter(r => r.type === 'success');
|
|
|
|
if (errors.length > 0) {
|
|
console.log('❌ Errors:');
|
|
errors.forEach(r => console.log(` ${r.message}`));
|
|
console.log('');
|
|
}
|
|
|
|
if (warnings.length > 0) {
|
|
console.log('⚠️ Warnings:');
|
|
warnings.forEach(r => console.log(` ${r.message}`));
|
|
console.log('');
|
|
}
|
|
|
|
if (successes.length > 0) {
|
|
console.log('✅ Verified:');
|
|
successes.forEach(r => console.log(` ${r.message}`));
|
|
console.log('');
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
console.log(`❌ Verification failed with ${errors.length} error(s)\n`);
|
|
return 1;
|
|
}
|
|
|
|
console.log('✅ All on-chain verifications passed!\n');
|
|
return 0;
|
|
}
|
|
|
|
// Main
|
|
const args = process.argv.slice(2);
|
|
const filePath = args.find(arg => !arg.startsWith('--')) || resolve(__dirname, '../lists/dbis-138.tokenlist.json');
|
|
const required = args.includes('--required');
|
|
|
|
if (!filePath) {
|
|
console.error('Usage: node verify-on-chain.js [path/to/token-list.json] [--required]');
|
|
process.exit(1);
|
|
}
|
|
|
|
verifyOnChain(filePath, required).then(exitCode => {
|
|
process.exit(exitCode);
|
|
}).catch(error => {
|
|
console.error('Unexpected error:', error);
|
|
process.exit(1);
|
|
});
|
|
|