Initial commit: add .gitignore and README
This commit is contained in:
63
tests/e2e/cross-chain.test.ts
Normal file
63
tests/e2e/cross-chain.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { CrossChainOrchestrator } from "../../src/xchain/orchestrator.js";
|
||||
import { BridgeConfig } from "../../src/xchain/orchestrator.js";
|
||||
|
||||
describe("Cross-Chain E2E", () => {
|
||||
// These tests require actual bridge setup
|
||||
const TEST_RPC = process.env.RPC_MAINNET || "";
|
||||
|
||||
it.skipIf(!TEST_RPC)("should send CCIP message", async () => {
|
||||
const orchestrator = new CrossChainOrchestrator("mainnet", "arbitrum");
|
||||
|
||||
const bridge: BridgeConfig = {
|
||||
type: "ccip",
|
||||
sourceChain: "mainnet",
|
||||
destinationChain: "arbitrum",
|
||||
};
|
||||
|
||||
// Mock execution - would need actual bridge setup
|
||||
const result = await orchestrator.executeCrossChain(
|
||||
bridge,
|
||||
{ steps: [] } as any,
|
||||
"0x123"
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it.skipIf(!TEST_RPC)("should check message status", async () => {
|
||||
const orchestrator = new CrossChainOrchestrator("mainnet", "arbitrum");
|
||||
|
||||
const bridge: BridgeConfig = {
|
||||
type: "ccip",
|
||||
sourceChain: "mainnet",
|
||||
destinationChain: "arbitrum",
|
||||
};
|
||||
|
||||
const status = await orchestrator.checkMessageStatus(
|
||||
bridge,
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234"
|
||||
);
|
||||
|
||||
expect(["pending", "delivered", "failed"]).toContain(status);
|
||||
});
|
||||
|
||||
it.skipIf(!TEST_RPC)("should send LayerZero message", async () => {
|
||||
const orchestrator = new CrossChainOrchestrator("mainnet", "arbitrum");
|
||||
|
||||
const bridge: BridgeConfig = {
|
||||
type: "layerzero",
|
||||
sourceChain: "mainnet",
|
||||
destinationChain: "arbitrum",
|
||||
};
|
||||
|
||||
const result = await orchestrator.executeCrossChain(
|
||||
bridge,
|
||||
{ steps: [] } as any,
|
||||
"0x123"
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
126
tests/e2e/fork-simulation.test.ts
Normal file
126
tests/e2e/fork-simulation.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { executeStrategy } from "../../src/engine.js";
|
||||
import { loadStrategy } from "../../src/strategy.js";
|
||||
import { Strategy } from "../../src/strategy.schema.js";
|
||||
|
||||
describe("Fork Simulation E2E", () => {
|
||||
// These tests require a fork RPC endpoint
|
||||
const FORK_RPC = process.env.FORK_RPC || "";
|
||||
|
||||
it.skipIf(!FORK_RPC)("should execute strategy on mainnet fork", async () => {
|
||||
const strategy: Strategy = {
|
||||
name: "Fork Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "supply",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const result = await executeStrategy(strategy, {
|
||||
simulate: true,
|
||||
dry: false,
|
||||
explain: false,
|
||||
fork: FORK_RPC,
|
||||
});
|
||||
|
||||
expect(result.success).toBeDefined();
|
||||
});
|
||||
|
||||
it.skipIf(!FORK_RPC)("should execute flash loan on fork", async () => {
|
||||
const strategy: Strategy = {
|
||||
name: "Flash Loan Fork",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "flashLoan",
|
||||
action: {
|
||||
type: "aaveV3.flashLoan",
|
||||
assets: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
amounts: ["1000000"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swap",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: "1000000",
|
||||
exactInput: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = await executeStrategy(strategy, {
|
||||
simulate: true,
|
||||
dry: false,
|
||||
explain: false,
|
||||
fork: FORK_RPC,
|
||||
});
|
||||
|
||||
expect(result.plan?.requiresFlashLoan).toBe(true);
|
||||
});
|
||||
|
||||
it.skipIf(!FORK_RPC)("should evaluate guards on fork", async () => {
|
||||
const strategy: Strategy = {
|
||||
name: "Guard Fork Test",
|
||||
chain: "mainnet",
|
||||
guards: [{
|
||||
type: "maxGas",
|
||||
params: {
|
||||
maxGasLimit: "5000000",
|
||||
},
|
||||
}],
|
||||
steps: [{
|
||||
id: "supply",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const result = await executeStrategy(strategy, {
|
||||
simulate: true,
|
||||
dry: false,
|
||||
explain: false,
|
||||
fork: FORK_RPC,
|
||||
});
|
||||
|
||||
expect(result.guardResults).toBeDefined();
|
||||
});
|
||||
|
||||
it.skipIf(!FORK_RPC)("should track state changes after execution", async () => {
|
||||
const strategy: Strategy = {
|
||||
name: "State Change Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "supply",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const result = await executeStrategy(strategy, {
|
||||
simulate: true,
|
||||
dry: false,
|
||||
explain: false,
|
||||
fork: FORK_RPC,
|
||||
});
|
||||
|
||||
// State changes would be tracked in simulation
|
||||
expect(result.success).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
27
tests/fixtures/strategies/flash-loan-swap.json
vendored
Normal file
27
tests/fixtures/strategies/flash-loan-swap.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Flash Loan Swap",
|
||||
"description": "Flash loan with swap for testing",
|
||||
"chain": "mainnet",
|
||||
"steps": [
|
||||
{
|
||||
"id": "flashLoan",
|
||||
"action": {
|
||||
"type": "aaveV3.flashLoan",
|
||||
"assets": ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
"amounts": ["1000000"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "swap",
|
||||
"action": {
|
||||
"type": "uniswapV3.swap",
|
||||
"tokenIn": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"tokenOut": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
"fee": 3000,
|
||||
"amountIn": "1000000",
|
||||
"exactInput": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
16
tests/fixtures/strategies/simple-supply.json
vendored
Normal file
16
tests/fixtures/strategies/simple-supply.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Simple Supply",
|
||||
"description": "Simple Aave supply strategy for testing",
|
||||
"chain": "mainnet",
|
||||
"steps": [
|
||||
{
|
||||
"id": "supply",
|
||||
"action": {
|
||||
"type": "aaveV3.supply",
|
||||
"asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": "1000000"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
125
tests/integration/errors.test.ts
Normal file
125
tests/integration/errors.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { validateStrategy, loadStrategy } from "../../src/strategy.js";
|
||||
import { StrategyCompiler } from "../../src/planner/compiler.js";
|
||||
import { writeFileSync, unlinkSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle invalid strategy JSON", () => {
|
||||
const invalidStrategy = {
|
||||
name: "Invalid",
|
||||
// Missing required fields
|
||||
};
|
||||
|
||||
const validation = validateStrategy(invalidStrategy as any);
|
||||
expect(validation.valid).toBe(false);
|
||||
expect(validation.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should handle missing blind values", () => {
|
||||
const strategy = {
|
||||
name: "Missing Blinds",
|
||||
chain: "mainnet",
|
||||
blinds: [
|
||||
{
|
||||
name: "amount",
|
||||
type: "uint256",
|
||||
},
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
id: "step1",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: { blind: "amount" },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Strategy should be valid but execution would fail without blind values
|
||||
const validation = validateStrategy(strategy as any);
|
||||
expect(validation.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle protocol adapter failures gracefully", async () => {
|
||||
const strategy = {
|
||||
name: "Invalid Protocol",
|
||||
chain: "invalid-chain",
|
||||
steps: [
|
||||
{
|
||||
id: "step1",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const compiler = new StrategyCompiler("invalid-chain");
|
||||
|
||||
// Should handle missing adapter gracefully
|
||||
await expect(compiler.compile(strategy as any)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should handle guard failures", async () => {
|
||||
const strategy = {
|
||||
name: "Guard Failure",
|
||||
chain: "mainnet",
|
||||
guards: [
|
||||
{
|
||||
type: "maxGas",
|
||||
params: {
|
||||
maxGasLimit: "1000", // Very low limit
|
||||
},
|
||||
onFailure: "revert",
|
||||
},
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
id: "step1",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Guard should fail and strategy should not execute
|
||||
const validation = validateStrategy(strategy as any);
|
||||
expect(validation.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle unsupported action types", async () => {
|
||||
const strategy = {
|
||||
name: "Unsupported Action",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "step1",
|
||||
action: {
|
||||
type: "unsupported.action",
|
||||
// Invalid action
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
await expect(compiler.compile(strategy as any)).rejects.toThrow(
|
||||
"Unsupported action type"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle execution failures gracefully", async () => {
|
||||
// This would require a mock execution environment
|
||||
// For now, just verify error handling structure exists
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
60
tests/integration/execution.test.ts
Normal file
60
tests/integration/execution.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { StrategyCompiler } from "../../src/planner/compiler.js";
|
||||
|
||||
describe("Execution Integration", () => {
|
||||
it("should compile a simple strategy", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "supply",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy as any);
|
||||
expect(plan.calls.length).toBeGreaterThan(0);
|
||||
expect(plan.requiresFlashLoan).toBe(false);
|
||||
});
|
||||
|
||||
it("should compile flash loan strategy", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy = {
|
||||
name: "Flash Loan Test",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "flashLoan",
|
||||
action: {
|
||||
type: "aaveV3.flashLoan",
|
||||
assets: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
amounts: ["1000000"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swap",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: "1000000",
|
||||
exactInput: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy as any, "0x1234567890123456789012345678901234567890");
|
||||
expect(plan.requiresFlashLoan).toBe(true);
|
||||
expect(plan.flashLoanAsset).toBe("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
});
|
||||
});
|
||||
|
||||
135
tests/integration/flash-loan.test.ts
Normal file
135
tests/integration/flash-loan.test.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { StrategyCompiler } from "../../src/planner/compiler.js";
|
||||
|
||||
describe("Flash Loan Integration", () => {
|
||||
it("should compile flash loan with swap", async () => {
|
||||
const strategy = {
|
||||
name: "Flash Loan Swap",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "flashLoan",
|
||||
action: {
|
||||
type: "aaveV3.flashLoan",
|
||||
assets: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
amounts: ["1000000"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swap",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: "1000000",
|
||||
exactInput: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const plan = await compiler.compile(
|
||||
strategy as any,
|
||||
"0x1234567890123456789012345678901234567890"
|
||||
);
|
||||
|
||||
expect(plan.requiresFlashLoan).toBe(true);
|
||||
expect(plan.flashLoanAsset).toBe(
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
);
|
||||
expect(plan.flashLoanAmount).toBe(1000000n);
|
||||
});
|
||||
|
||||
it("should compile flash loan with multiple operations", async () => {
|
||||
const strategy = {
|
||||
name: "Flash Loan Multi-Op",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "flashLoan",
|
||||
action: {
|
||||
type: "aaveV3.flashLoan",
|
||||
assets: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
amounts: ["1000000"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swap1",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: "500000",
|
||||
exactInput: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swap2",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
fee: 3000,
|
||||
amountIn: "500000",
|
||||
exactInput: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const plan = await compiler.compile(
|
||||
strategy as any,
|
||||
"0x1234567890123456789012345678901234567890"
|
||||
);
|
||||
|
||||
expect(plan.requiresFlashLoan).toBe(true);
|
||||
// Both swaps should be in the callback
|
||||
expect(plan.calls.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should validate flash loan repayment requirements", async () => {
|
||||
const strategy = {
|
||||
name: "Flash Loan Validation",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "flashLoan",
|
||||
action: {
|
||||
type: "aaveV3.flashLoan",
|
||||
assets: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
amounts: ["1000000"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swap",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: "1000000",
|
||||
exactInput: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const plan = await compiler.compile(
|
||||
strategy as any,
|
||||
"0x1234567890123456789012345678901234567890"
|
||||
);
|
||||
|
||||
// Flash loan should require repayment
|
||||
expect(plan.requiresFlashLoan).toBe(true);
|
||||
// Should have executeFlashLoan call
|
||||
expect(plan.calls.some((c) => c.description.includes("flash loan"))).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
144
tests/integration/full-execution.test.ts
Normal file
144
tests/integration/full-execution.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { StrategyCompiler } from "../../src/planner/compiler.js";
|
||||
import { executeStrategy } from "../../src/engine.js";
|
||||
import { loadStrategy, substituteBlinds } from "../../src/strategy.js";
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
describe("Full Strategy Execution", () => {
|
||||
it("should compile and execute recursive leverage strategy", async () => {
|
||||
const strategyPath = join(
|
||||
process.cwd(),
|
||||
"strategies",
|
||||
"sample.recursive.json"
|
||||
);
|
||||
|
||||
if (!require("fs").existsSync(strategyPath)) {
|
||||
// Skip if strategy file doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
const strategy = loadStrategy(strategyPath);
|
||||
|
||||
// Substitute blind values for testing
|
||||
const blindValues = {
|
||||
collateralAmount: "1000000", // 1 USDC (6 decimals)
|
||||
leverageFactor: "500000", // 0.5 USDC
|
||||
};
|
||||
const resolvedStrategy = substituteBlinds(strategy, blindValues);
|
||||
|
||||
const compiler = new StrategyCompiler(resolvedStrategy.chain);
|
||||
|
||||
const plan = await compiler.compile(resolvedStrategy);
|
||||
expect(plan.calls.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should compile liquidation helper strategy", async () => {
|
||||
const strategyPath = join(
|
||||
process.cwd(),
|
||||
"strategies",
|
||||
"sample.liquidation.json"
|
||||
);
|
||||
|
||||
if (!require("fs").existsSync(strategyPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strategy = loadStrategy(strategyPath);
|
||||
const compiler = new StrategyCompiler(strategy.chain);
|
||||
|
||||
const plan = await compiler.compile(strategy);
|
||||
expect(plan.requiresFlashLoan).toBe(true);
|
||||
});
|
||||
|
||||
it("should compile stablecoin hedge strategy", async () => {
|
||||
const strategyPath = join(
|
||||
process.cwd(),
|
||||
"strategies",
|
||||
"sample.stablecoin-hedge.json"
|
||||
);
|
||||
|
||||
if (!require("fs").existsSync(strategyPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strategy = loadStrategy(strategyPath);
|
||||
const compiler = new StrategyCompiler(strategy.chain);
|
||||
|
||||
const plan = await compiler.compile(strategy);
|
||||
expect(plan.calls.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should compile multi-protocol strategy", async () => {
|
||||
const strategy = {
|
||||
name: "Multi-Protocol",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "supply",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swap",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: "500000",
|
||||
exactInput: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const plan = await compiler.compile(strategy as any);
|
||||
expect(plan.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should compile strategy with all guard types", async () => {
|
||||
const strategy = {
|
||||
name: "All Guards",
|
||||
chain: "mainnet",
|
||||
guards: [
|
||||
{
|
||||
type: "maxGas",
|
||||
params: { maxGasLimit: "5000000" },
|
||||
},
|
||||
{
|
||||
type: "slippage",
|
||||
params: { maxBps: 50 },
|
||||
},
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
id: "step1",
|
||||
guards: [
|
||||
{
|
||||
type: "oracleSanity",
|
||||
params: {
|
||||
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
maxDeviationBps: 500,
|
||||
},
|
||||
},
|
||||
],
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const plan = await compiler.compile(strategy as any);
|
||||
expect(plan.calls.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
101
tests/integration/guards.test.ts
Normal file
101
tests/integration/guards.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { evaluateGuard } from "../../src/planner/guards.js";
|
||||
import { GuardContext } from "../../src/planner/guards.js";
|
||||
import { Guard } from "../../src/strategy.schema.js";
|
||||
import { PriceOracle } from "../../src/pricing/index.js";
|
||||
import { AaveV3Adapter } from "../../src/adapters/aaveV3.js";
|
||||
|
||||
describe("Guard Integration", () => {
|
||||
it("should evaluate multiple guards in sequence", async () => {
|
||||
const guards: Guard[] = [
|
||||
{
|
||||
type: "maxGas",
|
||||
params: {
|
||||
maxGasLimit: "5000000",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "slippage",
|
||||
params: {
|
||||
maxBps: 50,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const context: GuardContext = {
|
||||
chainName: "mainnet",
|
||||
gasEstimate: {
|
||||
total: 2000000n,
|
||||
perCall: [1000000n],
|
||||
},
|
||||
};
|
||||
|
||||
for (const guard of guards) {
|
||||
const result = await evaluateGuard(guard, context);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.passed).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle guard failure with revert action", async () => {
|
||||
const guard: Guard = {
|
||||
type: "maxGas",
|
||||
params: {
|
||||
maxGasLimit: "1000000", // Very low limit
|
||||
},
|
||||
onFailure: "revert",
|
||||
};
|
||||
|
||||
const context: GuardContext = {
|
||||
chainName: "mainnet",
|
||||
gasEstimate: {
|
||||
total: 2000000n, // Exceeds limit
|
||||
perCall: [2000000n],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await evaluateGuard(guard, context);
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.guard.onFailure).toBe("revert");
|
||||
});
|
||||
|
||||
it("should handle guard failure with warn action", async () => {
|
||||
const guard: Guard = {
|
||||
type: "slippage",
|
||||
params: {
|
||||
maxBps: 10, // Very tight slippage
|
||||
},
|
||||
onFailure: "warn",
|
||||
};
|
||||
|
||||
const context: GuardContext = {
|
||||
chainName: "mainnet",
|
||||
amountIn: 1000000n,
|
||||
amountOut: 900000n, // 10% slippage
|
||||
};
|
||||
|
||||
const result = await evaluateGuard(guard, context);
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.guard.onFailure).toBe("warn");
|
||||
});
|
||||
|
||||
it("should handle missing context gracefully", async () => {
|
||||
const guard: Guard = {
|
||||
type: "oracleSanity",
|
||||
params: {
|
||||
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
maxDeviationBps: 500,
|
||||
},
|
||||
};
|
||||
|
||||
const context: GuardContext = {
|
||||
chainName: "mainnet",
|
||||
// Missing oracle
|
||||
};
|
||||
|
||||
const result = await evaluateGuard(guard, context);
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("not available");
|
||||
});
|
||||
});
|
||||
|
||||
234
tests/integration/protocol-integration.test.ts
Normal file
234
tests/integration/protocol-integration.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { StrategyCompiler } from "../../src/planner/compiler.js";
|
||||
import { loadStrategy, substituteBlinds } from "../../src/strategy.js";
|
||||
import { getChainConfig } from "../../src/config/chains.js";
|
||||
import { JsonRpcProvider, Contract, getAddress } from "ethers";
|
||||
import { join } from "path";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
describe("Protocol Integration Tests - Recursive Leverage Strategy", () => {
|
||||
const RPC_MAINNET = process.env.RPC_MAINNET;
|
||||
const RPC_POLYGON = process.env.RPC_POLYGON;
|
||||
const RPC_BASE = process.env.RPC_BASE;
|
||||
const RPC_OPTIMISM = process.env.RPC_OPTIMISM;
|
||||
|
||||
// Aave v3 Pool contract ABI (minimal for testing)
|
||||
const AAVE_POOL_ABI = [
|
||||
"function getReserveData(address asset) external view returns (tuple(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint40))",
|
||||
"function getReservesList() external view returns (address[])",
|
||||
];
|
||||
|
||||
describe("Mainnet Protocol Contracts", () => {
|
||||
it.skipIf(!RPC_MAINNET)("should connect to Aave v3 Pool on mainnet", async () => {
|
||||
const provider = new JsonRpcProvider(RPC_MAINNET);
|
||||
const chainConfig = getChainConfig("mainnet");
|
||||
|
||||
if (!chainConfig.protocols.aaveV3?.pool) {
|
||||
throw new Error("Aave v3 pool address not configured");
|
||||
}
|
||||
|
||||
const poolAddress = getAddress(chainConfig.protocols.aaveV3.pool);
|
||||
const poolContract = new Contract(poolAddress, AAVE_POOL_ABI, provider);
|
||||
|
||||
// Test connection by calling getReservesList
|
||||
const reserves = await poolContract.getReservesList();
|
||||
expect(reserves).toBeDefined();
|
||||
expect(Array.isArray(reserves)).toBe(true);
|
||||
expect(reserves.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.skipIf(!RPC_MAINNET)("should verify Aave v3 Pool address is correct", async () => {
|
||||
const provider = new JsonRpcProvider(RPC_MAINNET);
|
||||
const chainConfig = getChainConfig("mainnet");
|
||||
|
||||
if (!chainConfig.protocols.aaveV3?.pool) {
|
||||
throw new Error("Aave v3 pool address not configured");
|
||||
}
|
||||
|
||||
const poolAddress = getAddress(chainConfig.protocols.aaveV3.pool);
|
||||
|
||||
// Verify contract exists by checking code
|
||||
const code = await provider.getCode(poolAddress);
|
||||
expect(code).not.toBe("0x");
|
||||
expect(code.length).toBeGreaterThan(2);
|
||||
});
|
||||
|
||||
it.skipIf(!RPC_MAINNET)("should compile recursive leverage strategy with real protocol addresses", async () => {
|
||||
const strategyPath = join(
|
||||
process.cwd(),
|
||||
"strategies",
|
||||
"sample.recursive.json"
|
||||
);
|
||||
|
||||
if (!require("fs").existsSync(strategyPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strategy = loadStrategy(strategyPath);
|
||||
|
||||
// Substitute blind values for testing
|
||||
const blindValues = {
|
||||
collateralAmount: "1000000", // 1 USDC (6 decimals)
|
||||
leverageFactor: "500000", // 0.5 USDC
|
||||
};
|
||||
const resolvedStrategy = substituteBlinds(strategy, blindValues);
|
||||
|
||||
const compiler = new StrategyCompiler(resolvedStrategy.chain);
|
||||
const plan = await compiler.compile(resolvedStrategy);
|
||||
|
||||
expect(plan.calls.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify the compiled calls target the correct Aave pool address
|
||||
const chainConfig = getChainConfig("mainnet");
|
||||
const aavePoolAddress = chainConfig.protocols.aaveV3?.pool;
|
||||
|
||||
if (aavePoolAddress) {
|
||||
const checksummedPoolAddress = getAddress(aavePoolAddress);
|
||||
const supplyCall = plan.calls.find(call =>
|
||||
getAddress(call.to).toLowerCase() === checksummedPoolAddress.toLowerCase()
|
||||
);
|
||||
expect(supplyCall).toBeDefined();
|
||||
expect(supplyCall?.description).toContain("Aave");
|
||||
}
|
||||
});
|
||||
|
||||
it.skipIf(!RPC_MAINNET)("should verify USDC is a valid reserve in Aave v3", async () => {
|
||||
const provider = new JsonRpcProvider(RPC_MAINNET);
|
||||
const chainConfig = getChainConfig("mainnet");
|
||||
|
||||
if (!chainConfig.protocols.aaveV3?.pool) {
|
||||
throw new Error("Aave v3 pool address not configured");
|
||||
}
|
||||
|
||||
const poolAddress = getAddress(chainConfig.protocols.aaveV3.pool);
|
||||
const poolContract = new Contract(poolAddress, AAVE_POOL_ABI, provider);
|
||||
|
||||
// USDC address on mainnet
|
||||
const USDC_ADDRESS = getAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
|
||||
// Get reserve data for USDC
|
||||
const reserveData = await poolContract.getReserveData(USDC_ADDRESS);
|
||||
expect(reserveData).toBeDefined();
|
||||
|
||||
// Verify it's a valid reserve (liquidityIndex should be > 0)
|
||||
const liquidityIndex = reserveData[0];
|
||||
expect(liquidityIndex).toBeDefined();
|
||||
expect(Number(liquidityIndex)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.skipIf(!RPC_MAINNET)("should verify Uniswap V3 Router address", async () => {
|
||||
const provider = new JsonRpcProvider(RPC_MAINNET);
|
||||
const chainConfig = getChainConfig("mainnet");
|
||||
|
||||
if (!chainConfig.protocols.uniswapV3?.router) {
|
||||
throw new Error("Uniswap V3 router address not configured");
|
||||
}
|
||||
|
||||
const routerAddress = getAddress(chainConfig.protocols.uniswapV3.router);
|
||||
|
||||
// Verify contract exists
|
||||
const code = await provider.getCode(routerAddress);
|
||||
expect(code).not.toBe("0x");
|
||||
expect(code.length).toBeGreaterThan(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Polygon Protocol Contracts", () => {
|
||||
it.skipIf(!RPC_POLYGON)("should connect to Polygon RPC and verify chain config", async () => {
|
||||
const provider = new JsonRpcProvider(RPC_POLYGON);
|
||||
// Note: Polygon chain config may not be implemented yet
|
||||
// This test verifies RPC connectivity only
|
||||
|
||||
// Verify we can connect
|
||||
const blockNumber = await provider.getBlockNumber();
|
||||
expect(blockNumber).toBeGreaterThan(0);
|
||||
|
||||
// Verify chain ID matches Polygon
|
||||
const network = await provider.getNetwork();
|
||||
expect(network.chainId).toBe(BigInt(137));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Base Protocol Contracts", () => {
|
||||
it.skipIf(!RPC_BASE)("should connect to Base RPC and verify chain config", async () => {
|
||||
const provider = new JsonRpcProvider(RPC_BASE);
|
||||
const chainConfig = getChainConfig("base");
|
||||
|
||||
// Verify we can connect
|
||||
const blockNumber = await provider.getBlockNumber();
|
||||
expect(blockNumber).toBeGreaterThan(0);
|
||||
|
||||
// Verify chain ID matches
|
||||
const network = await provider.getNetwork();
|
||||
expect(network.chainId).toBe(BigInt(8453));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Optimism Protocol Contracts", () => {
|
||||
it.skipIf(!RPC_OPTIMISM)("should connect to Optimism RPC and verify chain config", async () => {
|
||||
const provider = new JsonRpcProvider(RPC_OPTIMISM);
|
||||
const chainConfig = getChainConfig("optimism");
|
||||
|
||||
// Verify we can connect
|
||||
const blockNumber = await provider.getBlockNumber();
|
||||
expect(blockNumber).toBeGreaterThan(0);
|
||||
|
||||
// Verify chain ID matches
|
||||
const network = await provider.getNetwork();
|
||||
expect(network.chainId).toBe(BigInt(10));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Recursive Leverage Strategy - Full Integration", () => {
|
||||
it.skipIf(!RPC_MAINNET)("should compile and validate recursive leverage strategy against real contracts", async () => {
|
||||
const strategyPath = join(
|
||||
process.cwd(),
|
||||
"strategies",
|
||||
"sample.recursive.json"
|
||||
);
|
||||
|
||||
if (!require("fs").existsSync(strategyPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = new JsonRpcProvider(RPC_MAINNET);
|
||||
const strategy = loadStrategy(strategyPath);
|
||||
const chainConfig = getChainConfig(strategy.chain);
|
||||
|
||||
// Substitute blind values
|
||||
const blindValues = {
|
||||
collateralAmount: "1000000", // 1 USDC
|
||||
leverageFactor: "500000", // 0.5 USDC
|
||||
};
|
||||
const resolvedStrategy = substituteBlinds(strategy, blindValues);
|
||||
|
||||
// Compile strategy
|
||||
const compiler = new StrategyCompiler(resolvedStrategy.chain);
|
||||
const plan = await compiler.compile(resolvedStrategy);
|
||||
|
||||
expect(plan.calls.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify all calls target valid contract addresses
|
||||
for (const call of plan.calls) {
|
||||
const callAddress = getAddress(call.to);
|
||||
const code = await provider.getCode(callAddress);
|
||||
expect(code).not.toBe("0x");
|
||||
expect(code.length).toBeGreaterThan(2);
|
||||
}
|
||||
|
||||
// Verify Aave pool address matches configuration
|
||||
const aavePoolAddress = chainConfig.protocols.aaveV3?.pool;
|
||||
if (aavePoolAddress) {
|
||||
const checksummedPoolAddress = getAddress(aavePoolAddress);
|
||||
const aaveCalls = plan.calls.filter(call =>
|
||||
getAddress(call.to).toLowerCase() === checksummedPoolAddress.toLowerCase()
|
||||
);
|
||||
expect(aaveCalls.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
87
tests/unit/adapters/aaveV3.test.ts
Normal file
87
tests/unit/adapters/aaveV3.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { AaveV3Adapter } from "../../../src/adapters/aaveV3.js";
|
||||
|
||||
describe("Aave V3 Adapter", () => {
|
||||
let adapter: AaveV3Adapter;
|
||||
let mockProvider: any;
|
||||
let mockSigner: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
mockSigner = {
|
||||
getAddress: vi.fn().mockResolvedValue("0x1234567890123456789012345678901234567890"),
|
||||
};
|
||||
|
||||
// Mock the adapter constructor
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
supply: vi.fn(),
|
||||
withdraw: vi.fn(),
|
||||
borrow: vi.fn(),
|
||||
repay: vi.fn(),
|
||||
flashLoanSimple: vi.fn(),
|
||||
setUserEMode: vi.fn(),
|
||||
setUserUseReserveAsCollateral: vi.fn(),
|
||||
getUserAccountData: vi.fn(),
|
||||
interface: {
|
||||
parseLog: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("should validate asset address on supply", async () => {
|
||||
adapter = new AaveV3Adapter("mainnet", mockSigner as any);
|
||||
|
||||
await expect(
|
||||
adapter.supply("0x0000000000000000000000000000000000000000", 1000n)
|
||||
).rejects.toThrow("Invalid asset address");
|
||||
});
|
||||
|
||||
it("should validate asset address on withdraw", async () => {
|
||||
adapter = new AaveV3Adapter("mainnet", mockSigner as any);
|
||||
|
||||
await expect(
|
||||
adapter.withdraw("0x0000000000000000000000000000000000000000", 1000n)
|
||||
).rejects.toThrow("Invalid asset address");
|
||||
});
|
||||
|
||||
it("should calculate health factor correctly", async () => {
|
||||
adapter = new AaveV3Adapter("mainnet");
|
||||
|
||||
const mockData = {
|
||||
totalCollateralBase: 2000000n,
|
||||
totalDebtBase: 1000000n,
|
||||
availableBorrowsBase: 500000n,
|
||||
currentLiquidationThreshold: 8000n, // 80%
|
||||
ltv: 7500n, // 75%
|
||||
healthFactor: 2000000000000000000n, // 2.0
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
adapter.dataProvider.getUserAccountData = vi.fn().mockResolvedValue([
|
||||
mockData.totalCollateralBase,
|
||||
mockData.totalDebtBase,
|
||||
mockData.availableBorrowsBase,
|
||||
mockData.currentLiquidationThreshold,
|
||||
mockData.ltv,
|
||||
mockData.healthFactor,
|
||||
]);
|
||||
|
||||
const hf = await adapter.getUserHealthFactor("0x123");
|
||||
expect(hf).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should handle different interest rate modes", async () => {
|
||||
adapter = new AaveV3Adapter("mainnet", mockSigner as any);
|
||||
|
||||
// Test variable rate mode (default)
|
||||
// Test stable rate mode
|
||||
// These would require mocking the contract calls
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
79
tests/unit/adapters/aggregators.test.ts
Normal file
79
tests/unit/adapters/aggregators.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { AggregatorAdapter } from "../../../src/adapters/aggregators.js";
|
||||
|
||||
describe("Aggregator Adapter", () => {
|
||||
let adapter: AggregatorAdapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
swap: vi.fn(),
|
||||
transformERC20: vi.fn(),
|
||||
interface: {
|
||||
encodeFunctionData: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
});
|
||||
|
||||
it("should get 1inch quote", async () => {
|
||||
adapter = new AggregatorAdapter("mainnet");
|
||||
|
||||
// Mock 1inch API response
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
toAmount: "1000000",
|
||||
}),
|
||||
});
|
||||
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
tx: {
|
||||
data: "0x1234",
|
||||
to: "0x1111111254EEB25477B68fb85Ed929f73A960582",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const quote = await adapter.get1InchQuote(
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
1000000n,
|
||||
50
|
||||
);
|
||||
|
||||
expect(quote).toBeDefined();
|
||||
expect(quote?.amountOut).toBeGreaterThan(0n);
|
||||
expect(quote?.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fallback when 1inch API fails", async () => {
|
||||
adapter = new AggregatorAdapter("mainnet");
|
||||
|
||||
// Mock API failure
|
||||
(global.fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
});
|
||||
|
||||
const quote = await adapter.get1InchQuote(
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
1000000n,
|
||||
50
|
||||
);
|
||||
|
||||
// Should return fallback quote
|
||||
expect(quote).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
74
tests/unit/adapters/balancer.test.ts
Normal file
74
tests/unit/adapters/balancer.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { BalancerAdapter } from "../../../src/adapters/balancer.js";
|
||||
|
||||
describe("Balancer Adapter", () => {
|
||||
let adapter: BalancerAdapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
swap: vi.fn(),
|
||||
batchSwap: vi.fn(),
|
||||
interface: {
|
||||
encodeFunctionData: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("should encode single swap", async () => {
|
||||
adapter = new BalancerAdapter("mainnet");
|
||||
|
||||
const swapParams = {
|
||||
poolId: "0x...",
|
||||
kind: 0, // GIVEN_IN
|
||||
assetIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
assetOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
amount: 1000000n,
|
||||
userData: "0x",
|
||||
};
|
||||
|
||||
const data = await adapter.swap(swapParams);
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode batch swap", async () => {
|
||||
adapter = new BalancerAdapter("mainnet");
|
||||
|
||||
const batchSwapParams = {
|
||||
kind: 0,
|
||||
swaps: [{
|
||||
poolId: "0x...",
|
||||
assetInIndex: 0,
|
||||
assetOutIndex: 1,
|
||||
amount: 1000000n,
|
||||
userData: "0x",
|
||||
}],
|
||||
assets: [
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
],
|
||||
funds: {
|
||||
sender: "0x123",
|
||||
fromInternalBalance: false,
|
||||
recipient: "0x123",
|
||||
toInternalBalance: false,
|
||||
},
|
||||
limits: [0n, 0n],
|
||||
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
|
||||
};
|
||||
|
||||
const data = await adapter.batchSwap(batchSwapParams);
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
70
tests/unit/adapters/compoundV3.test.ts
Normal file
70
tests/unit/adapters/compoundV3.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { CompoundV3Adapter } from "../../../src/adapters/compoundV3.js";
|
||||
|
||||
describe("Compound V3 Adapter", () => {
|
||||
let adapter: CompoundV3Adapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
supply: vi.fn(),
|
||||
withdraw: vi.fn(),
|
||||
borrow: vi.fn(),
|
||||
repay: vi.fn(),
|
||||
allow: vi.fn(),
|
||||
getAccountLiquidity: vi.fn(),
|
||||
interface: {
|
||||
parseLog: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("should supply assets", async () => {
|
||||
adapter = new CompoundV3Adapter("mainnet");
|
||||
// Test would verify supply call encoding
|
||||
expect(adapter).toBeDefined();
|
||||
});
|
||||
|
||||
it("should withdraw assets", async () => {
|
||||
adapter = new CompoundV3Adapter("mainnet");
|
||||
// Test would verify withdraw call encoding
|
||||
expect(adapter).toBeDefined();
|
||||
});
|
||||
|
||||
it("should borrow assets", async () => {
|
||||
adapter = new CompoundV3Adapter("mainnet");
|
||||
// Test would verify borrow call encoding
|
||||
expect(adapter).toBeDefined();
|
||||
});
|
||||
|
||||
it("should repay assets", async () => {
|
||||
adapter = new CompoundV3Adapter("mainnet");
|
||||
// Test would verify repay call encoding
|
||||
expect(adapter).toBeDefined();
|
||||
});
|
||||
|
||||
it("should calculate account liquidity", async () => {
|
||||
adapter = new CompoundV3Adapter("mainnet");
|
||||
|
||||
const mockLiquidity = {
|
||||
isLiquid: true,
|
||||
shortfall: 0n,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
adapter.comet.getAccountLiquidity = vi.fn().mockResolvedValue([
|
||||
true,
|
||||
0n,
|
||||
]);
|
||||
|
||||
const liquidity = await adapter.getAccountLiquidity("0x123");
|
||||
expect(liquidity).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
69
tests/unit/adapters/curve.test.ts
Normal file
69
tests/unit/adapters/curve.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { CurveAdapter } from "../../../src/adapters/curve.js";
|
||||
|
||||
describe("Curve Adapter", () => {
|
||||
let adapter: CurveAdapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
exchange: vi.fn(),
|
||||
exchange_underlying: vi.fn(),
|
||||
get_pool_from_lp_token: vi.fn(),
|
||||
interface: {
|
||||
encodeFunctionData: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("should find pool for LP token", async () => {
|
||||
adapter = new CurveAdapter("mainnet");
|
||||
|
||||
// Mock registry response
|
||||
mockProvider.call = vi.fn().mockResolvedValue(
|
||||
"0x000000000000000000000000bebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
|
||||
);
|
||||
|
||||
const pool = await adapter.findPool("0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490");
|
||||
expect(pool).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode exchange", async () => {
|
||||
adapter = new CurveAdapter("mainnet");
|
||||
|
||||
const data = await adapter.exchange(
|
||||
"0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
|
||||
0,
|
||||
1,
|
||||
1000000n,
|
||||
990000n
|
||||
);
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode exchange_underlying", async () => {
|
||||
adapter = new CurveAdapter("mainnet");
|
||||
|
||||
const data = await adapter.exchangeUnderlying(
|
||||
"0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
|
||||
0,
|
||||
1,
|
||||
1000000n,
|
||||
990000n
|
||||
);
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
42
tests/unit/adapters/lido.test.ts
Normal file
42
tests/unit/adapters/lido.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { LidoAdapter } from "../../../src/adapters/lido.js";
|
||||
|
||||
describe("Lido Adapter", () => {
|
||||
let adapter: LidoAdapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
wrap: vi.fn(),
|
||||
unwrap: vi.fn(),
|
||||
interface: {
|
||||
encodeFunctionData: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("should encode wrap operation", async () => {
|
||||
adapter = new LidoAdapter("mainnet");
|
||||
|
||||
const data = await adapter.wrap(1000000000000000000n);
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode unwrap operation", async () => {
|
||||
adapter = new LidoAdapter("mainnet");
|
||||
|
||||
const data = await adapter.unwrap(1000000000000000000n);
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
77
tests/unit/adapters/maker.test.ts
Normal file
77
tests/unit/adapters/maker.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { MakerAdapter } from "../../../src/adapters/maker.js";
|
||||
|
||||
describe("MakerDAO Adapter", () => {
|
||||
let adapter: MakerAdapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
open: vi.fn(),
|
||||
frob: vi.fn(),
|
||||
join: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
interface: {
|
||||
parseLog: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("should open vault and parse CDP ID", async () => {
|
||||
adapter = new MakerAdapter("mainnet");
|
||||
|
||||
// Mock transaction with NewCdp event
|
||||
const mockReceipt = {
|
||||
logs: [{
|
||||
topics: ["0x...", "0x0000000000000000000000000000000000000000000000000000000000000123"],
|
||||
data: "0x",
|
||||
}],
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
adapter.cdpManager.open = vi.fn().mockResolvedValue({
|
||||
wait: vi.fn().mockResolvedValue(mockReceipt),
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
adapter.cdpManager.interface.parseLog = vi.fn().mockReturnValue({
|
||||
name: "NewCdp",
|
||||
args: {
|
||||
usr: "0x123",
|
||||
own: "0x456",
|
||||
cdp: 291n, // CDP ID
|
||||
},
|
||||
});
|
||||
|
||||
const cdpId = await adapter.openVault("ETH-A");
|
||||
expect(cdpId).toBe(291n);
|
||||
});
|
||||
|
||||
it("should encode frob operation", async () => {
|
||||
adapter = new MakerAdapter("mainnet");
|
||||
|
||||
const data = await adapter.frob(291n, 1000000000000000000n, 1000n);
|
||||
expect(data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode join operation", async () => {
|
||||
adapter = new MakerAdapter("mainnet");
|
||||
|
||||
const data = await adapter.join(1000000n);
|
||||
expect(data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode exit operation", async () => {
|
||||
adapter = new MakerAdapter("mainnet");
|
||||
|
||||
const data = await adapter.exit(1000000n);
|
||||
expect(data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
62
tests/unit/adapters/perps.test.ts
Normal file
62
tests/unit/adapters/perps.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { PerpsAdapter } from "../../../src/adapters/perps.js";
|
||||
|
||||
describe("Perps Adapter", () => {
|
||||
let adapter: PerpsAdapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
increasePosition: vi.fn(),
|
||||
decreasePosition: vi.fn(),
|
||||
interface: {
|
||||
encodeFunctionData: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("should encode increase position", async () => {
|
||||
adapter = new PerpsAdapter("mainnet");
|
||||
|
||||
const params = {
|
||||
path: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
indexToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
amountIn: 1000000n,
|
||||
minOut: 990000n,
|
||||
sizeDelta: 2000000n,
|
||||
isLong: true,
|
||||
acceptablePrice: 1000000n,
|
||||
};
|
||||
|
||||
const data = await adapter.increasePosition(params);
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode decrease position", async () => {
|
||||
adapter = new PerpsAdapter("mainnet");
|
||||
|
||||
const params = {
|
||||
path: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
indexToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
collateralDelta: 500000n,
|
||||
sizeDelta: 1000000n,
|
||||
isLong: true,
|
||||
receiver: "0x123",
|
||||
acceptablePrice: 1000000n,
|
||||
};
|
||||
|
||||
const data = await adapter.decreasePosition(params);
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
104
tests/unit/adapters/uniswapV3.test.ts
Normal file
104
tests/unit/adapters/uniswapV3.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { UniswapV3Adapter } from "../../../src/adapters/uniswapV3.js";
|
||||
|
||||
describe("Uniswap V3 Adapter", () => {
|
||||
let adapter: UniswapV3Adapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
|
||||
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
|
||||
exactInputSingle: vi.fn(),
|
||||
exactOutputSingle: vi.fn(),
|
||||
quoteExactInputSingle: vi.fn(),
|
||||
interface: {
|
||||
encodeFunctionData: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("should encode exact input swap", async () => {
|
||||
adapter = new UniswapV3Adapter("mainnet");
|
||||
|
||||
const params = {
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: 1000000n,
|
||||
amountOutMinimum: 990000n,
|
||||
recipient: "0x123",
|
||||
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
|
||||
};
|
||||
|
||||
const data = await adapter.swapExactInput(params);
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode exact output swap", async () => {
|
||||
adapter = new UniswapV3Adapter("mainnet");
|
||||
|
||||
const params = {
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountOut: 1000000n,
|
||||
amountInMaximum: 1010000n,
|
||||
recipient: "0x123",
|
||||
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
|
||||
};
|
||||
|
||||
const data = await adapter.swapExactOutput(params);
|
||||
expect(data).toBeDefined();
|
||||
expect(data.to).toBeDefined();
|
||||
expect(data.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("should encode path correctly", () => {
|
||||
adapter = new UniswapV3Adapter("mainnet");
|
||||
|
||||
const path = adapter.encodePath(
|
||||
["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xdAC17F958D2ee523a2206206994597C13D831ec7"],
|
||||
[3000]
|
||||
);
|
||||
|
||||
expect(path).toBeDefined();
|
||||
expect(path.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should get quote for exact input", async () => {
|
||||
adapter = new UniswapV3Adapter("mainnet");
|
||||
|
||||
// Mock quoter response
|
||||
mockProvider.call = vi.fn().mockResolvedValue(
|
||||
"0x00000000000000000000000000000000000000000000000000000000000f4240" // 1000000
|
||||
);
|
||||
|
||||
const quote = await adapter.quoteExactInput(
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
3000,
|
||||
1000000n
|
||||
);
|
||||
|
||||
expect(quote).toBeGreaterThan(0n);
|
||||
});
|
||||
|
||||
it("should validate fee tiers", () => {
|
||||
adapter = new UniswapV3Adapter("mainnet");
|
||||
|
||||
const validFees = [100, 500, 3000, 10000];
|
||||
validFees.forEach(fee => {
|
||||
expect(adapter.isValidFee(fee)).toBe(true);
|
||||
});
|
||||
|
||||
expect(adapter.isValidFee(999)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
83
tests/unit/guards/maxGas.test.ts
Normal file
83
tests/unit/guards/maxGas.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { evaluateMaxGas } from "../../../src/guards/maxGas.js";
|
||||
import { Guard } from "../../../src/strategy.schema.js";
|
||||
import { GasEstimate } from "../../../src/utils/gas.js";
|
||||
|
||||
describe("Max Gas Guard", () => {
|
||||
it("should pass when gas is below limit", () => {
|
||||
const guard: Guard = {
|
||||
type: "maxGas",
|
||||
params: {
|
||||
maxGasLimit: "5000000",
|
||||
},
|
||||
};
|
||||
|
||||
const gasEstimate: GasEstimate = {
|
||||
gasLimit: 2000000n,
|
||||
maxFeePerGas: 100000000000n,
|
||||
maxPriorityFeePerGas: 2000000000n,
|
||||
};
|
||||
|
||||
const result = evaluateMaxGas(guard, gasEstimate, "mainnet");
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when gas exceeds limit", () => {
|
||||
const guard: Guard = {
|
||||
type: "maxGas",
|
||||
params: {
|
||||
maxGasLimit: "2000000",
|
||||
},
|
||||
};
|
||||
|
||||
const gasEstimate: GasEstimate = {
|
||||
gasLimit: 3000000n,
|
||||
maxFeePerGas: 100000000000n,
|
||||
maxPriorityFeePerGas: 2000000000n,
|
||||
};
|
||||
|
||||
const result = evaluateMaxGas(guard, gasEstimate, "mainnet");
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("exceeds");
|
||||
});
|
||||
|
||||
it("should handle gas price limits", () => {
|
||||
const guard: Guard = {
|
||||
type: "maxGas",
|
||||
params: {
|
||||
maxGasLimit: "5000000",
|
||||
maxGasPrice: "100000000000", // 100 gwei
|
||||
},
|
||||
};
|
||||
|
||||
const gasEstimate: GasEstimate = {
|
||||
gasLimit: 2000000n,
|
||||
maxFeePerGas: 50000000000n, // 50 gwei
|
||||
maxPriorityFeePerGas: 2000000000n,
|
||||
};
|
||||
|
||||
const result = evaluateMaxGas(guard, gasEstimate, "mainnet");
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when gas price exceeds limit", () => {
|
||||
const guard: Guard = {
|
||||
type: "maxGas",
|
||||
params: {
|
||||
maxGasLimit: "5000000",
|
||||
maxGasPrice: "100000000000", // 100 gwei
|
||||
},
|
||||
};
|
||||
|
||||
const gasEstimate: GasEstimate = {
|
||||
gasLimit: 2000000n,
|
||||
maxFeePerGas: 150000000000n, // 150 gwei
|
||||
maxPriorityFeePerGas: 2000000000n,
|
||||
};
|
||||
|
||||
const result = evaluateMaxGas(guard, gasEstimate, "mainnet");
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("gas price");
|
||||
});
|
||||
});
|
||||
|
||||
70
tests/unit/guards/minHealthFactor.test.ts
Normal file
70
tests/unit/guards/minHealthFactor.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { evaluateMinHealthFactor } from "../../../src/guards/minHealthFactor.js";
|
||||
import { AaveV3Adapter } from "../../../src/adapters/aaveV3.js";
|
||||
import { Guard } from "../../../src/strategy.schema.js";
|
||||
|
||||
describe("Min Health Factor Guard", () => {
|
||||
let mockAave: AaveV3Adapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
};
|
||||
|
||||
mockAave = new AaveV3Adapter("mainnet");
|
||||
// @ts-ignore - access private property for testing
|
||||
mockAave.provider = mockProvider;
|
||||
});
|
||||
|
||||
it("should pass when health factor is above minimum", async () => {
|
||||
const guard: Guard = {
|
||||
type: "minHealthFactor",
|
||||
params: {
|
||||
minHF: 1.2,
|
||||
user: "0x1234567890123456789012345678901234567890",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock health factor calculation
|
||||
vi.spyOn(mockAave, "getUserHealthFactor").mockResolvedValue(1.5);
|
||||
|
||||
const result = await evaluateMinHealthFactor(guard, mockAave, {});
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when health factor is below minimum", async () => {
|
||||
const guard: Guard = {
|
||||
type: "minHealthFactor",
|
||||
params: {
|
||||
minHF: 1.2,
|
||||
user: "0x1234567890123456789012345678901234567890",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock health factor below minimum
|
||||
vi.spyOn(mockAave, "getUserHealthFactor").mockResolvedValue(1.1);
|
||||
|
||||
const result = await evaluateMinHealthFactor(guard, mockAave, {});
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("health factor");
|
||||
});
|
||||
|
||||
it("should handle missing user position", async () => {
|
||||
const guard: Guard = {
|
||||
type: "minHealthFactor",
|
||||
params: {
|
||||
minHF: 1.2,
|
||||
user: "0x0000000000000000000000000000000000000000",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock no position
|
||||
vi.spyOn(mockAave, "getUserHealthFactor").mockResolvedValue(0);
|
||||
|
||||
const result = await evaluateMinHealthFactor(guard, mockAave, {});
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
107
tests/unit/guards/oracleSanity.test.ts
Normal file
107
tests/unit/guards/oracleSanity.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { evaluateOracleSanity } from "../../../src/guards/oracleSanity.js";
|
||||
import { PriceOracle } from "../../../src/pricing/index.js";
|
||||
import { Guard } from "../../../src/strategy.schema.js";
|
||||
|
||||
describe("Oracle Sanity Guard", () => {
|
||||
let mockOracle: PriceOracle;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
mockOracle = new PriceOracle("mainnet");
|
||||
// @ts-ignore - access private property for testing
|
||||
mockOracle.provider = mockProvider;
|
||||
});
|
||||
|
||||
it("should pass when price is within bounds", async () => {
|
||||
const guard: Guard = {
|
||||
type: "oracleSanity",
|
||||
params: {
|
||||
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
maxDeviationBps: 500, // 5%
|
||||
},
|
||||
};
|
||||
|
||||
// Mock price fetch
|
||||
vi.spyOn(mockOracle, "getPrice").mockResolvedValue({
|
||||
name: "chainlink",
|
||||
price: 1000000n, // $1.00
|
||||
decimals: 8,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await evaluateOracleSanity(guard, mockOracle, {});
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when price deviation is too high", async () => {
|
||||
const guard: Guard = {
|
||||
type: "oracleSanity",
|
||||
params: {
|
||||
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
maxDeviationBps: 100, // 1%
|
||||
expectedPrice: "1000000", // $1.00
|
||||
},
|
||||
};
|
||||
|
||||
// Mock price that's 2% off
|
||||
vi.spyOn(mockOracle, "getPrice").mockResolvedValue({
|
||||
name: "chainlink",
|
||||
price: 1020000n, // $1.02 (2% deviation)
|
||||
decimals: 8,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await evaluateOracleSanity(guard, mockOracle, {});
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("deviation");
|
||||
});
|
||||
|
||||
it("should handle missing oracle gracefully", async () => {
|
||||
const guard: Guard = {
|
||||
type: "oracleSanity",
|
||||
params: {
|
||||
token: "0xInvalid",
|
||||
maxDeviationBps: 500,
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(mockOracle, "getPrice").mockRejectedValue(
|
||||
new Error("Oracle not found")
|
||||
);
|
||||
|
||||
const result = await evaluateOracleSanity(guard, mockOracle, {});
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toBeDefined();
|
||||
});
|
||||
|
||||
it("should check for stale price data", async () => {
|
||||
const guard: Guard = {
|
||||
type: "oracleSanity",
|
||||
params: {
|
||||
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
maxDeviationBps: 500,
|
||||
maxAgeSeconds: 3600, // 1 hour
|
||||
},
|
||||
};
|
||||
|
||||
// Mock stale price (2 hours old)
|
||||
const staleTimestamp = Date.now() - 2 * 60 * 60 * 1000;
|
||||
vi.spyOn(mockOracle, "getPrice").mockResolvedValue({
|
||||
name: "chainlink",
|
||||
price: 1000000n,
|
||||
decimals: 8,
|
||||
timestamp: staleTimestamp,
|
||||
});
|
||||
|
||||
const result = await evaluateOracleSanity(guard, mockOracle, {});
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("stale");
|
||||
});
|
||||
});
|
||||
|
||||
57
tests/unit/guards/positionDeltaLimit.test.ts
Normal file
57
tests/unit/guards/positionDeltaLimit.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { evaluatePositionDeltaLimit } from "../../../src/guards/positionDeltaLimit.js";
|
||||
import { Guard } from "../../../src/strategy.schema.js";
|
||||
|
||||
describe("Position Delta Limit Guard", () => {
|
||||
it("should pass when position delta is within limit", () => {
|
||||
const guard: Guard = {
|
||||
type: "positionDeltaLimit",
|
||||
params: {
|
||||
maxDelta: "1000000",
|
||||
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
positionDelta: 500000n,
|
||||
};
|
||||
|
||||
const result = evaluatePositionDeltaLimit(guard, "mainnet", context);
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when position delta exceeds limit", () => {
|
||||
const guard: Guard = {
|
||||
type: "positionDeltaLimit",
|
||||
params: {
|
||||
maxDelta: "1000000",
|
||||
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
positionDelta: 2000000n,
|
||||
};
|
||||
|
||||
const result = evaluatePositionDeltaLimit(guard, "mainnet", context);
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("exceeds");
|
||||
});
|
||||
|
||||
it("should handle missing position delta", () => {
|
||||
const guard: Guard = {
|
||||
type: "positionDeltaLimit",
|
||||
params: {
|
||||
maxDelta: "1000000",
|
||||
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
},
|
||||
};
|
||||
|
||||
const context = {};
|
||||
|
||||
const result = evaluatePositionDeltaLimit(guard, "mainnet", context);
|
||||
// Should pass if no delta to check
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
60
tests/unit/guards/slippage.test.ts
Normal file
60
tests/unit/guards/slippage.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { evaluateSlippage } from "../../../src/guards/slippage.js";
|
||||
import { Guard } from "../../../src/strategy.schema.js";
|
||||
|
||||
describe("Slippage Guard", () => {
|
||||
it("should pass when slippage is within limit", () => {
|
||||
const guard: Guard = {
|
||||
type: "slippage",
|
||||
params: {
|
||||
maxBps: 50, // 0.5%
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
expectedAmount: 1000000n,
|
||||
actualAmount: 995000n, // 0.5% slippage
|
||||
};
|
||||
|
||||
const result = evaluateSlippage(guard, context);
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when slippage exceeds limit", () => {
|
||||
const guard: Guard = {
|
||||
type: "slippage",
|
||||
params: {
|
||||
maxBps: 10, // 0.1%
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
expectedAmount: 1000000n,
|
||||
actualAmount: 980000n, // 2% slippage
|
||||
};
|
||||
|
||||
const result = evaluateSlippage(guard, context);
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("slippage");
|
||||
});
|
||||
|
||||
it("should handle zero expected amount", () => {
|
||||
const guard: Guard = {
|
||||
type: "slippage",
|
||||
params: {
|
||||
maxBps: 50,
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
expectedAmount: 0n,
|
||||
actualAmount: 995000n,
|
||||
};
|
||||
|
||||
const result = evaluateSlippage(guard, context);
|
||||
// Should fail if expected amount is zero
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("zero");
|
||||
});
|
||||
});
|
||||
|
||||
88
tests/unit/guards/twapSanity.test.ts
Normal file
88
tests/unit/guards/twapSanity.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { evaluateTWAPSanity } from "../../../src/guards/twapSanity.js";
|
||||
import { UniswapV3Adapter } from "../../../src/adapters/uniswapV3.js";
|
||||
import { Guard } from "../../../src/strategy.schema.js";
|
||||
|
||||
describe("TWAP Sanity Guard", () => {
|
||||
let mockUniswap: UniswapV3Adapter;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
mockUniswap = new UniswapV3Adapter("mainnet");
|
||||
// @ts-ignore - access private property for testing
|
||||
mockUniswap.provider = mockProvider;
|
||||
});
|
||||
|
||||
it("should pass when TWAP is within deviation", async () => {
|
||||
const guard: Guard = {
|
||||
type: "twapSanity",
|
||||
params: {
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
maxDeviationBps: 500, // 5%
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
amountIn: 1000000n,
|
||||
expectedAmountOut: 1000000n,
|
||||
};
|
||||
|
||||
// Mock quote that's within deviation
|
||||
vi.spyOn(mockUniswap, "quoteExactInput").mockResolvedValue(1010000n); // 1% deviation
|
||||
|
||||
const result = await evaluateTWAPSanity(guard, mockUniswap, context);
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when TWAP deviation is too high", async () => {
|
||||
const guard: Guard = {
|
||||
type: "twapSanity",
|
||||
params: {
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
maxDeviationBps: 100, // 1%
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
amountIn: 1000000n,
|
||||
expectedAmountOut: 1000000n,
|
||||
};
|
||||
|
||||
// Mock quote that's 2% off
|
||||
vi.spyOn(mockUniswap, "quoteExactInput").mockResolvedValue(980000n); // 2% deviation
|
||||
|
||||
const result = await evaluateTWAPSanity(guard, mockUniswap, context);
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toContain("deviation");
|
||||
});
|
||||
|
||||
it("should handle missing pool gracefully", async () => {
|
||||
const guard: Guard = {
|
||||
type: "twapSanity",
|
||||
params: {
|
||||
tokenIn: "0xInvalid",
|
||||
tokenOut: "0xInvalid",
|
||||
fee: 3000,
|
||||
maxDeviationBps: 500,
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(mockUniswap, "quoteExactInput").mockRejectedValue(
|
||||
new Error("Pool not found")
|
||||
);
|
||||
|
||||
const result = await evaluateTWAPSanity(guard, mockUniswap, {});
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reason).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
222
tests/unit/planner/compiler.test.ts
Normal file
222
tests/unit/planner/compiler.test.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { StrategyCompiler } from "../../../src/planner/compiler.js";
|
||||
import { Strategy } from "../../../src/strategy.schema.js";
|
||||
|
||||
describe("Strategy Compiler", () => {
|
||||
const executorAddr = "0x1234567890123456789012345678901234567890";
|
||||
|
||||
it("should compile aaveV3.supply", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "supply",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
expect(plan.calls.length).toBe(1);
|
||||
expect(plan.calls[0].description).toContain("Aave v3 supply");
|
||||
});
|
||||
|
||||
it("should compile aaveV3.setUserEMode", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "setEMode",
|
||||
action: {
|
||||
type: "aaveV3.setUserEMode",
|
||||
categoryId: 1,
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
expect(plan.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should compile maker.openVault", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "openVault",
|
||||
action: {
|
||||
type: "maker.openVault",
|
||||
ilk: "ETH-A",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
expect(plan.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should compile balancer.batchSwap", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "batchSwap",
|
||||
action: {
|
||||
type: "balancer.batchSwap",
|
||||
kind: "givenIn",
|
||||
swaps: [{
|
||||
poolId: "0x...",
|
||||
assetInIndex: 0,
|
||||
assetOutIndex: 1,
|
||||
amount: "1000000",
|
||||
}],
|
||||
assets: [
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
],
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
expect(plan.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should compile curve.exchange_underlying", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "exchange",
|
||||
action: {
|
||||
type: "curve.exchange_underlying",
|
||||
pool: "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
|
||||
i: 0,
|
||||
j: 1,
|
||||
dx: "1000000",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
expect(plan.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should compile aggregators.swap1Inch", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "swap",
|
||||
action: {
|
||||
type: "aggregators.swap1Inch",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
amountIn: "1000000",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
// Mock 1inch API
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
toAmount: "1000000",
|
||||
tx: { data: "0x1234" },
|
||||
}),
|
||||
});
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
expect(plan.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should compile perps.increasePosition", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "increase",
|
||||
action: {
|
||||
type: "perps.increasePosition",
|
||||
path: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
indexToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
amountIn: "1000000",
|
||||
sizeDelta: "2000000",
|
||||
isLong: true,
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
expect(plan.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should wrap flash loan with subsequent operations", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Flash Loan",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "flashLoan",
|
||||
action: {
|
||||
type: "aaveV3.flashLoan",
|
||||
assets: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
|
||||
amounts: ["1000000"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swap",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: "1000000",
|
||||
exactInput: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
expect(plan.requiresFlashLoan).toBe(true);
|
||||
expect(plan.flashLoanAsset).toBe("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
// Should have executeFlashLoan call
|
||||
expect(plan.calls.some(c => c.description.includes("flash loan"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should substitute executor address in swaps", async () => {
|
||||
const compiler = new StrategyCompiler("mainnet");
|
||||
const strategy: Strategy = {
|
||||
name: "Test",
|
||||
chain: "mainnet",
|
||||
steps: [{
|
||||
id: "swap",
|
||||
action: {
|
||||
type: "uniswapV3.swap",
|
||||
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
fee: 3000,
|
||||
amountIn: "1000000",
|
||||
exactInput: true,
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const plan = await compiler.compile(strategy, executorAddr);
|
||||
// Recipient should be executor address, not zero
|
||||
expect(plan.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
71
tests/unit/pricing/index.test.ts
Normal file
71
tests/unit/pricing/index.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { PriceOracle } from "../../../src/pricing/index.js";
|
||||
|
||||
describe("Price Oracle", () => {
|
||||
let oracle: PriceOracle;
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
call: vi.fn(),
|
||||
};
|
||||
|
||||
oracle = new PriceOracle("mainnet");
|
||||
// @ts-ignore - access private property for testing
|
||||
oracle.provider = mockProvider;
|
||||
});
|
||||
|
||||
it("should fetch Chainlink price", async () => {
|
||||
// Mock Chainlink aggregator response
|
||||
mockProvider.call = vi.fn().mockResolvedValue(
|
||||
"0x0000000000000000000000000000000000000000000000000000000005f5e100" // 100000000 = $1.00 with 8 decimals
|
||||
);
|
||||
|
||||
const price = await oracle.getPrice(
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // USDC
|
||||
);
|
||||
|
||||
expect(price).toBeDefined();
|
||||
expect(price.name).toBe("chainlink");
|
||||
expect(price.price).toBeGreaterThan(0n);
|
||||
});
|
||||
|
||||
it("should calculate weighted average with quorum", async () => {
|
||||
const sources = [
|
||||
{
|
||||
name: "chainlink",
|
||||
price: 1000000n,
|
||||
decimals: 8,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
name: "uniswap-twap",
|
||||
price: 1010000n,
|
||||
decimals: 8,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
];
|
||||
|
||||
const result = await oracle.getPriceWithQuorum(
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
sources
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.price).toBeGreaterThan(0n);
|
||||
// Weighted average should be between the two prices
|
||||
expect(result.price).toBeGreaterThanOrEqual(1000000n);
|
||||
expect(result.price).toBeLessThanOrEqual(1010000n);
|
||||
});
|
||||
|
||||
it("should handle missing token decimals gracefully", async () => {
|
||||
mockProvider.call = vi.fn().mockRejectedValue(new Error("Not found"));
|
||||
|
||||
// Should not throw, should use default decimals
|
||||
await expect(
|
||||
oracle.getPrice("0xInvalidToken")
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
78
tests/unit/strategy.test.ts
Normal file
78
tests/unit/strategy.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { loadStrategy, validateStrategy, substituteBlinds } from "../../src/strategy.js";
|
||||
import { BlindValues } from "../../src/strategy.js";
|
||||
|
||||
describe("Strategy", () => {
|
||||
const sampleStrategy = {
|
||||
name: "Test Strategy",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "step1",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("should load a valid strategy", () => {
|
||||
// Write to temp file for testing
|
||||
const fs = await import("fs");
|
||||
const path = await import("path");
|
||||
const tempFile = path.join(process.cwd(), "temp-strategy.json");
|
||||
fs.writeFileSync(tempFile, JSON.stringify(sampleStrategy));
|
||||
|
||||
const strategy = loadStrategy(tempFile);
|
||||
expect(strategy.name).toBe("Test Strategy");
|
||||
expect(strategy.steps.length).toBe(1);
|
||||
|
||||
// Cleanup
|
||||
fs.unlinkSync(tempFile);
|
||||
});
|
||||
|
||||
it("should validate a strategy", () => {
|
||||
const validation = validateStrategy(sampleStrategy as any);
|
||||
expect(validation.valid).toBe(true);
|
||||
expect(validation.errors.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should detect duplicate step IDs", () => {
|
||||
const invalidStrategy = {
|
||||
...sampleStrategy,
|
||||
steps: [
|
||||
{ id: "step1", action: sampleStrategy.steps[0].action },
|
||||
{ id: "step1", action: sampleStrategy.steps[0].action },
|
||||
],
|
||||
};
|
||||
const validation = validateStrategy(invalidStrategy as any);
|
||||
expect(validation.valid).toBe(false);
|
||||
expect(validation.errors).toContain("Duplicate step ID: step1");
|
||||
});
|
||||
|
||||
it("should substitute blind values", () => {
|
||||
const strategyWithBlinds = {
|
||||
...sampleStrategy,
|
||||
blinds: [
|
||||
{ name: "amount", type: "uint256", description: "Amount to supply" },
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
id: "step1",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: { blind: "amount" },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const blindValues: BlindValues = { amount: "1000000" };
|
||||
const substituted = substituteBlinds(strategyWithBlinds as any, blindValues);
|
||||
expect(substituted.steps[0].action.amount).toBe("1000000");
|
||||
});
|
||||
});
|
||||
|
||||
105
tests/unit/utils/gas.test.ts
Normal file
105
tests/unit/utils/gas.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { estimateGasForCalls } from "../../../src/utils/gas.js";
|
||||
import { CompiledCall } from "../../../src/planner/compiler.js";
|
||||
import { JsonRpcProvider } from "ethers";
|
||||
|
||||
describe("Gas Estimation", () => {
|
||||
let mockProvider: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProvider = {
|
||||
estimateGas: vi.fn(),
|
||||
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
|
||||
};
|
||||
});
|
||||
|
||||
it("should estimate gas for single call", async () => {
|
||||
const calls: CompiledCall[] = [
|
||||
{
|
||||
to: "0x1234567890123456789012345678901234567890",
|
||||
data: "0x1234",
|
||||
description: "Test call",
|
||||
},
|
||||
];
|
||||
|
||||
mockProvider.estimateGas = vi.fn().mockResolvedValue(100000n);
|
||||
|
||||
const estimate = await estimateGasForCalls(
|
||||
mockProvider as any,
|
||||
calls,
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
);
|
||||
|
||||
expect(estimate).toBeGreaterThan(0n);
|
||||
});
|
||||
|
||||
it("should estimate gas for multiple calls", async () => {
|
||||
const calls: CompiledCall[] = [
|
||||
{
|
||||
to: "0x1234567890123456789012345678901234567890",
|
||||
data: "0x1234",
|
||||
description: "Call 1",
|
||||
},
|
||||
{
|
||||
to: "0x1234567890123456789012345678901234567890",
|
||||
data: "0x5678",
|
||||
description: "Call 2",
|
||||
},
|
||||
];
|
||||
|
||||
mockProvider.estimateGas = vi.fn().mockResolvedValue(100000n);
|
||||
|
||||
const estimate = await estimateGasForCalls(
|
||||
mockProvider as any,
|
||||
calls,
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
);
|
||||
|
||||
expect(estimate).toBeGreaterThan(0n);
|
||||
});
|
||||
|
||||
it("should add safety buffer to estimates", async () => {
|
||||
const calls: CompiledCall[] = [
|
||||
{
|
||||
to: "0x1234567890123456789012345678901234567890",
|
||||
data: "0x1234",
|
||||
description: "Test call",
|
||||
},
|
||||
];
|
||||
|
||||
mockProvider.estimateGas = vi.fn().mockResolvedValue(100000n);
|
||||
|
||||
const estimate = await estimateGasForCalls(
|
||||
mockProvider as any,
|
||||
calls,
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
);
|
||||
|
||||
// Should have safety buffer (typically 20%)
|
||||
expect(estimate).toBeGreaterThan(100000n);
|
||||
});
|
||||
|
||||
it("should handle estimation failures gracefully", async () => {
|
||||
const calls: CompiledCall[] = [
|
||||
{
|
||||
to: "0x1234567890123456789012345678901234567890",
|
||||
data: "0x1234",
|
||||
description: "Test call",
|
||||
},
|
||||
];
|
||||
|
||||
mockProvider.estimateGas = vi.fn().mockRejectedValue(
|
||||
new Error("Estimation failed")
|
||||
);
|
||||
|
||||
// Should fall back to rough estimate
|
||||
await expect(
|
||||
estimateGasForCalls(
|
||||
mockProvider as any,
|
||||
calls,
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
94
tests/utils/test-helpers.ts
Normal file
94
tests/utils/test-helpers.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { JsonRpcProvider, Wallet } from "ethers";
|
||||
import { Strategy } from "../../src/strategy.schema.js";
|
||||
|
||||
/**
|
||||
* Create a mock JSON-RPC provider for testing
|
||||
*/
|
||||
export function createMockProvider(): JsonRpcProvider {
|
||||
const provider = new JsonRpcProvider("http://localhost:8545");
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock wallet for testing
|
||||
*/
|
||||
export function createMockSigner(privateKey?: string): Wallet {
|
||||
const key = privateKey || "0x" + "1".repeat(64);
|
||||
return new Wallet(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock strategy for testing
|
||||
*/
|
||||
export function createMockStrategy(overrides?: Partial<Strategy>): Strategy {
|
||||
return {
|
||||
name: "Test Strategy",
|
||||
chain: "mainnet",
|
||||
steps: [
|
||||
{
|
||||
id: "step1",
|
||||
action: {
|
||||
type: "aaveV3.supply",
|
||||
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
amount: "1000000",
|
||||
},
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock adapter (generic helper)
|
||||
*/
|
||||
export function createMockAdapter(adapterName: string): any {
|
||||
return {
|
||||
name: adapterName,
|
||||
provider: createMockProvider(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a fork for testing (requires Anvil or similar)
|
||||
*/
|
||||
export async function setupFork(
|
||||
rpcUrl: string,
|
||||
blockNumber?: number
|
||||
): Promise<JsonRpcProvider> {
|
||||
const provider = new JsonRpcProvider(rpcUrl);
|
||||
|
||||
if (blockNumber) {
|
||||
// In a real implementation, you'd use anvil_reset or similar
|
||||
// For now, just return the provider
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific number of blocks
|
||||
*/
|
||||
export async function waitForBlocks(
|
||||
provider: JsonRpcProvider,
|
||||
blocks: number
|
||||
): Promise<void> {
|
||||
const currentBlock = await provider.getBlockNumber();
|
||||
const targetBlock = currentBlock + blocks;
|
||||
|
||||
while ((await provider.getBlockNumber()) < targetBlock) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test addresses
|
||||
*/
|
||||
export const TEST_ADDRESSES = {
|
||||
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
DAI: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
WETH: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
EXECUTOR: "0x1234567890123456789012345678901234567890",
|
||||
USER: "0x1111111111111111111111111111111111111111",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user