183 lines
6.6 KiB
TypeScript
183 lines
6.6 KiB
TypeScript
|
|
#!/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<string, ProtocolAdapter>();
|
||
|
|
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();
|
||
|
|
|