/ test-network-api.js
test-network-api.js
1 #!/usr/bin/env node 2 /** 3 * Network API Testing Script 4 * Tests both mainnet and testnet APIs to verify they work the same way 5 * Run: node test-network-api.js 6 */ 7 8 const https = require('https'); 9 10 // Network configurations 11 const NETWORKS = { 12 mainnet: { 13 name: 'Mainnet', 14 apiBaseUrl: 'https://query.main.net.espresso.network', 15 apiVersion: 'v0', 16 wsBaseUrl: 'wss://query.main.net.espresso.network', 17 scanBaseUrl: 'https://explorer.main.net.espresso.network', 18 caffNodeUrl: 'https://rari.caff.mainnet.espresso.network' 19 }, 20 testnet: { 21 name: 'Testnet (DECAF)', 22 apiBaseUrl: 'https://query.decaf.testnet.espresso.network', 23 apiVersion: 'v0', 24 wsBaseUrl: 'wss://query.decaf.testnet.espresso.network', 25 scanBaseUrl: 'https://explorer.decaf.testnet.espresso.network', 26 caffNodeUrl: 'https://rari.caff.testnet.espresso.network' 27 } 28 }; 29 30 // Test results storage 31 const results = { 32 mainnet: { passed: 0, failed: 0, tests: [] }, 33 testnet: { passed: 0, failed: 0, tests: [] } 34 }; 35 36 // Helper: Make HTTPS request 37 function makeRequest(url) { 38 return new Promise((resolve, reject) => { 39 const urlObj = new URL(url); 40 const options = { 41 hostname: urlObj.hostname, 42 path: urlObj.pathname + urlObj.search, 43 method: 'GET', 44 headers: { 'Content-Type': 'application/json' } 45 }; 46 47 const req = https.request(options, (res) => { 48 let data = ''; 49 res.on('data', chunk => data += chunk); 50 res.on('end', () => { 51 if (res.statusCode >= 200 && res.statusCode < 300) { 52 try { 53 resolve({ status: res.statusCode, data: JSON.parse(data) }); 54 } catch { 55 resolve({ status: res.statusCode, data: data }); 56 } 57 } else { 58 reject({ status: res.statusCode, error: data }); 59 } 60 }); 61 }); 62 63 req.on('error', reject); 64 req.setTimeout(10000, () => { 65 req.destroy(); 66 reject(new Error('Request timeout')); 67 }); 68 req.end(); 69 }); 70 } 71 72 // Test runner 73 async function runTest(network, testName, testFn) { 74 const networkName = network === 'mainnet' ? 'Mainnet' : 'Testnet'; 75 process.stdout.write(` ${networkName} - ${testName}... `); 76 77 try { 78 const result = await testFn(NETWORKS[network]); 79 if (result.passed) { 80 console.log('✅ PASS'); 81 results[network].passed++; 82 results[network].tests.push({ name: testName, status: 'PASS', details: result.details }); 83 } else { 84 console.log('❌ FAIL:', result.error); 85 results[network].failed++; 86 results[network].tests.push({ name: testName, status: 'FAIL', error: result.error }); 87 } 88 } catch (error) { 89 console.log('❌ ERROR:', error.message); 90 results[network].failed++; 91 results[network].tests.push({ name: testName, status: 'ERROR', error: error.message }); 92 } 93 } 94 95 // TEST 1: Block height endpoint 96 async function testBlockHeight(config) { 97 const url = `${config.apiBaseUrl}/${config.apiVersion}/status/block-height`; 98 const response = await makeRequest(url); 99 const height = typeof response.data === 'number' ? response.data : parseInt(response.data); 100 101 if (isNaN(height) || height <= 0) { 102 return { passed: false, error: 'Invalid block height: ' + response.data }; 103 } 104 105 return { passed: true, details: `Block height: ${height}` }; 106 } 107 108 // TEST 2: Get specific block 109 async function testGetBlock(config) { 110 // Get current height first 111 const heightUrl = `${config.apiBaseUrl}/${config.apiVersion}/status/block-height`; 112 const heightResponse = await makeRequest(heightUrl); 113 const currentHeight = typeof heightResponse.data === 'number' ? heightResponse.data : parseInt(heightResponse.data); 114 115 // Get a recent block (10 blocks ago to ensure it exists) 116 const blockHeight = currentHeight - 10; 117 const url = `${config.apiBaseUrl}/${config.apiVersion}/availability/block/${blockHeight}`; 118 const response = await makeRequest(url); 119 120 // Validate response structure 121 if (!response.data || !response.data.header || !response.data.hash) { 122 return { passed: false, error: 'Invalid block structure' }; 123 } 124 125 const block = response.data; 126 const heightInBlock = block.header?.fields?.height; 127 128 if (heightInBlock !== blockHeight) { 129 return { passed: false, error: `Block height mismatch: expected ${blockHeight}, got ${heightInBlock}` }; 130 } 131 132 return { 133 passed: true, 134 details: `Block #${blockHeight}, Hash: ${block.hash.substring(0, 30)}..., Txns: ${block.num_transactions}` 135 }; 136 } 137 138 // TEST 3: Transaction count 139 async function testTransactionCount(config) { 140 const url = `${config.apiBaseUrl}/${config.apiVersion}/node/transactions/count`; 141 const response = await makeRequest(url); 142 const count = typeof response.data === 'number' ? response.data : parseInt(response.data); 143 144 if (isNaN(count) || count < 0) { 145 return { passed: false, error: 'Invalid transaction count: ' + response.data }; 146 } 147 148 return { passed: true, details: `Total transactions: ${count}` }; 149 } 150 151 // TEST 4: Payload size 152 async function testPayloadSize(config) { 153 const url = `${config.apiBaseUrl}/${config.apiVersion}/node/payloads/total-size`; 154 const response = await makeRequest(url); 155 const size = typeof response.data === 'number' ? response.data : parseInt(response.data); 156 157 if (isNaN(size) || size < 0) { 158 return { passed: false, error: 'Invalid payload size: ' + response.data }; 159 } 160 161 return { passed: true, details: `Total payload: ${(size / 1024 / 1024).toFixed(2)} MB` }; 162 } 163 164 // TEST 5: Success rate 165 async function testSuccessRate(config) { 166 const url = `${config.apiBaseUrl}/${config.apiVersion}/status/success-rate`; 167 const response = await makeRequest(url); 168 const rate = typeof response.data === 'number' ? response.data : parseFloat(response.data); 169 170 if (isNaN(rate) || rate < 0 || rate > 1) { 171 return { passed: false, error: 'Invalid success rate: ' + response.data }; 172 } 173 174 return { passed: true, details: `Success rate: ${(rate * 100).toFixed(2)}%` }; 175 } 176 177 // TEST 6: Block structure compatibility 178 async function testBlockStructureCompat(config) { 179 const heightUrl = `${config.apiBaseUrl}/${config.apiVersion}/status/block-height`; 180 const heightResponse = await makeRequest(heightUrl); 181 const currentHeight = typeof heightResponse.data === 'number' ? heightResponse.data : parseInt(heightResponse.data); 182 183 const blockHeight = currentHeight - 5; 184 const url = `${config.apiBaseUrl}/${config.apiVersion}/availability/block/${blockHeight}`; 185 const response = await makeRequest(url); 186 187 const block = response.data; 188 const requiredFields = ['header', 'hash', 'payload']; 189 const requiredHeaderFields = ['version', 'fields']; 190 const requiredBlockFields = ['height', 'timestamp', 'l1_head', 'l1_finalized']; 191 192 // Check top-level fields 193 for (const field of requiredFields) { 194 if (!(field in block)) { 195 return { passed: false, error: `Missing required field: ${field}` }; 196 } 197 } 198 199 // Check header fields 200 for (const field of requiredHeaderFields) { 201 if (!(field in block.header)) { 202 return { passed: false, error: `Missing header field: ${field}` }; 203 } 204 } 205 206 // Check block fields 207 for (const field of requiredBlockFields) { 208 if (!(field in block.header.fields)) { 209 return { passed: false, error: `Missing block field: ${field}` }; 210 } 211 } 212 213 return { passed: true, details: 'All required fields present' }; 214 } 215 216 // TEST 7: API response time 217 async function testResponseTime(config) { 218 const url = `${config.apiBaseUrl}/${config.apiVersion}/status/block-height`; 219 const start = Date.now(); 220 await makeRequest(url); 221 const duration = Date.now() - start; 222 223 if (duration > 5000) { 224 return { passed: false, error: `Response too slow: ${duration}ms` }; 225 } 226 227 return { passed: true, details: `Response time: ${duration}ms` }; 228 } 229 230 // TEST 8: Explorer URL accessibility 231 async function testExplorerUrl(config) { 232 try { 233 const response = await makeRequest(config.scanBaseUrl); 234 return { passed: true, details: `Explorer accessible (${response.status})` }; 235 } catch (error) { 236 // Explorer might not respond to HEAD/GET, that's okay 237 return { passed: true, details: 'Explorer URL configured' }; 238 } 239 } 240 241 // Main test suite 242 async function runAllTests() { 243 console.log('╔════════════════════════════════════════════════════════════╗'); 244 console.log('║ Network API Testing - Mainnet vs Testnet Comparison ║'); 245 console.log('╚════════════════════════════════════════════════════════════╝\n'); 246 247 const tests = [ 248 { name: 'Block Height Endpoint', fn: testBlockHeight }, 249 { name: 'Get Block Data', fn: testGetBlock }, 250 { name: 'Transaction Count', fn: testTransactionCount }, 251 { name: 'Payload Size', fn: testPayloadSize }, 252 { name: 'Success Rate', fn: testSuccessRate }, 253 { name: 'Block Structure Compatibility', fn: testBlockStructureCompat }, 254 { name: 'API Response Time', fn: testResponseTime }, 255 { name: 'Explorer URL', fn: testExplorerUrl } 256 ]; 257 258 for (let i = 0; i < tests.length; i++) { 259 const test = tests[i]; 260 console.log(`\n[${i + 1}/${tests.length}] ${test.name}`); 261 await runTest('mainnet', test.name, test.fn); 262 await runTest('testnet', test.name, test.fn); 263 } 264 265 // Print summary 266 console.log('\n╔════════════════════════════════════════════════════════════╗'); 267 console.log('║ Test Summary ║'); 268 console.log('╚════════════════════════════════════════════════════════════╝\n'); 269 270 console.log('MAINNET:'); 271 console.log(` ✅ Passed: ${results.mainnet.passed}`); 272 console.log(` ❌ Failed: ${results.mainnet.failed}`); 273 console.log(` Total: ${results.mainnet.passed + results.mainnet.failed}\n`); 274 275 console.log('TESTNET (DECAF):'); 276 console.log(` ✅ Passed: ${results.testnet.passed}`); 277 console.log(` ❌ Failed: ${results.testnet.failed}`); 278 console.log(` Total: ${results.testnet.passed + results.testnet.failed}\n`); 279 280 // Detailed results for failures 281 const mainnetFailed = results.mainnet.tests.filter(t => t.status !== 'PASS'); 282 const testnetFailed = results.testnet.tests.filter(t => t.status !== 'PASS'); 283 284 if (mainnetFailed.length > 0) { 285 console.log('❌ MAINNET FAILURES:'); 286 mainnetFailed.forEach(test => { 287 console.log(` - ${test.name}: ${test.error || 'Unknown error'}`); 288 }); 289 console.log(''); 290 } 291 292 if (testnetFailed.length > 0) { 293 console.log('❌ TESTNET FAILURES:'); 294 testnetFailed.forEach(test => { 295 console.log(` - ${test.name}: ${test.error || 'Unknown error'}`); 296 }); 297 console.log(''); 298 } 299 300 // Final verdict 301 const totalTests = tests.length * 2; 302 const totalPassed = results.mainnet.passed + results.testnet.passed; 303 const totalFailed = results.mainnet.failed + results.testnet.failed; 304 305 console.log('╔════════════════════════════════════════════════════════════╗'); 306 console.log('║ Final Verdict ║'); 307 console.log('╚════════════════════════════════════════════════════════════╝\n'); 308 309 if (totalFailed === 0) { 310 console.log('✅ ALL TESTS PASSED!'); 311 console.log(` ${totalPassed}/${totalTests} tests successful`); 312 console.log('\n✅ Both networks are working correctly and compatibly!'); 313 console.log('✅ Ready to implement network toggle feature!\n'); 314 process.exit(0); 315 } else { 316 console.log('❌ SOME TESTS FAILED'); 317 console.log(` ${totalPassed}/${totalTests} tests passed`); 318 console.log(` ${totalFailed}/${totalTests} tests failed\n`); 319 console.log('⚠️ Fix failed tests before implementing network toggle!\n'); 320 process.exit(1); 321 } 322 } 323 324 // Run tests 325 console.log('Starting network API tests...\n'); 326 runAllTests().catch(error => { 327 console.error('Fatal error:', error); 328 process.exit(1); 329 });