#!/usr/bin/env tsx /** * Test script for DeFi strategy testing * * This script can be used to test the strategy framework with a real fork * * Usage: * tsx scripts/test-strategy.ts * * Environment variables: * MAINNET_RPC_URL - RPC URL for mainnet fork (required) * TEST_SCENARIO - Path to scenario file (default: scenarios/aave/leveraged-long.yml) * TEST_NETWORK - Network name (default: mainnet) */ // Load environment variables FIRST, before any other imports that might use them import dotenv from 'dotenv'; dotenv.config(); import { readFileSync } from 'fs'; import { join } from 'path'; import { ForkOrchestrator } from '../src/strat/core/fork-orchestrator.js'; import { ScenarioRunner } from '../src/strat/core/scenario-runner.js'; import { loadScenario } from '../src/strat/dsl/scenario-loader.js'; import { AaveV3Adapter } from '../src/strat/adapters/aave-v3-adapter.js'; import { UniswapV3Adapter } from '../src/strat/adapters/uniswap-v3-adapter.js'; import { CompoundV3Adapter } from '../src/strat/adapters/compound-v3-adapter.js'; import { Erc20Adapter } from '../src/strat/adapters/erc20-adapter.js'; import { FailureInjector } from '../src/strat/core/failure-injector.js'; import { JsonReporter } from '../src/strat/reporters/json-reporter.js'; import { HtmlReporter } from '../src/strat/reporters/html-reporter.js'; import { getNetwork } from '../src/strat/config/networks.js'; import type { ProtocolAdapter } from '../src/strat/types.js'; async function main() { const scenarioPath = process.env.TEST_SCENARIO || 'scenarios/aave/leveraged-long.yml'; const networkName = process.env.TEST_NETWORK || 'mainnet'; // Get RPC URL from env - try network-specific first, then MAINNET_RPC_URL const networkEnvVar = `${networkName.toUpperCase()}_RPC_URL`; let rpcUrl = process.env[networkEnvVar] || process.env.MAINNET_RPC_URL; if (!rpcUrl) { console.error('ERROR: RPC URL not found'); console.error(` Please set ${networkEnvVar} or MAINNET_RPC_URL in your .env file`); console.error(' Or create .env from .env.example and fill in your RPC URLs'); process.exit(1); } if (rpcUrl.includes('YOUR_KEY') || rpcUrl.includes('YOUR_INFURA_KEY')) { console.error('ERROR: RPC URL contains placeholder'); console.error(' Please set a real RPC URL in your .env file'); console.error(` Current: ${rpcUrl.substring(0, 50)}...`); process.exit(1); } console.log('='.repeat(60)); console.log('DeFi Strategy Testing - Test Script'); console.log('='.repeat(60)); console.log(`Scenario: ${scenarioPath}`); console.log(`Network: ${networkName}`); console.log(`RPC: ${rpcUrl.substring(0, 30)}...`); console.log(''); try { // Load scenario console.log('Loading scenario...'); const scenario = loadScenario(scenarioPath); console.log(`✓ Loaded scenario with ${scenario.steps.length} steps`); // Setup network const network = getNetwork(networkName); network.rpcUrl = rpcUrl; // Start fork console.log('Starting fork...'); const fork = new ForkOrchestrator(network, rpcUrl); await fork.start(); console.log('✓ Fork started'); // Register adapters console.log('Registering adapters...'); const adapters = new Map(); adapters.set('erc20', new Erc20Adapter()); adapters.set('aave-v3', new AaveV3Adapter()); adapters.set('uniswap-v3', new UniswapV3Adapter()); adapters.set('compound-v3', new CompoundV3Adapter()); // Register failure injector const failureInjector = new FailureInjector(fork); adapters.set('failure', { name: 'failure', discover: async () => ({}), actions: { oracleShock: (ctx, args) => failureInjector.oracleShock(ctx, args), timeTravel: (ctx, args) => failureInjector.timeTravel(ctx, args), setTimestamp: (ctx, args) => failureInjector.setTimestamp(ctx, args), liquidityShock: (ctx, args) => failureInjector.liquidityShock(ctx, args), setBaseFee: (ctx, args) => failureInjector.setBaseFee(ctx, args), pauseReserve: (ctx, args) => failureInjector.pauseReserve(ctx, args), capExhaustion: (ctx, args) => failureInjector.capExhaustion(ctx, args), }, views: {}, }); console.log('✓ Adapters registered'); // Create snapshot console.log('Creating snapshot...'); const snapshotId = await fork.snapshot('test_start'); console.log(`✓ Snapshot created: ${snapshotId}`); // Run scenario console.log(''); console.log('Running scenario...'); console.log('-'.repeat(60)); const runner = new ScenarioRunner(fork, adapters, network); const report = await runner.run(scenario); console.log('-'.repeat(60)); // Print summary console.log(''); console.log('='.repeat(60)); console.log('Run Summary'); console.log('='.repeat(60)); console.log(`Status: ${report.passed ? '✓ PASSED' : '✗ FAILED'}`); console.log(`Steps: ${report.steps.length}`); console.log(`Duration: ${((report.endTime! - report.startTime) / 1000).toFixed(2)}s`); console.log(`Total Gas: ${report.metadata.totalGas.toString()}`); if (report.error) { console.log(`Error: ${report.error}`); } // Generate reports const outputDir = 'out'; const timestamp = Date.now(); const jsonPath = join(outputDir, `test-run-${timestamp}.json`); const htmlPath = join(outputDir, `test-report-${timestamp}.html`); console.log(''); console.log('Generating reports...'); JsonReporter.generate(report, jsonPath); HtmlReporter.generate(report, htmlPath); console.log(`✓ JSON report: ${jsonPath}`); console.log(`✓ HTML report: ${htmlPath}`); // Print step details console.log(''); console.log('Step Results:'); for (const step of report.steps) { const status = step.result.success ? '✓' : '✗'; const duration = (step.duration / 1000).toFixed(2); console.log(` ${status} ${step.stepName} (${duration}s)`); if (!step.result.success) { console.log(` Error: ${step.result.error}`); } if (step.assertions && step.assertions.length > 0) { const passed = step.assertions.filter(a => a.passed).length; const total = step.assertions.length; console.log(` Assertions: ${passed}/${total} passed`); } } // Cleanup await fork.revert(snapshotId); await fork.stop(); console.log(''); console.log('='.repeat(60)); console.log('Test completed'); process.exit(report.passed ? 0 : 1); } catch (error: any) { console.error(''); console.error('ERROR:', error.message); console.error(error.stack); process.exit(1); } } main();