/ 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  });