Skip to content

Commit 945cbe3

Browse files
committed
Initial commit
0 parents  commit 945cbe3

26 files changed

Lines changed: 2682 additions & 0 deletions
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { executeArbitrageAction } from '../../src/actions/arbitrageAction';
3+
import { ServiceType } from '@elizaos/core';
4+
import { ArbitrageService } from '../../src/services/ArbitrageService';
5+
6+
describe('executeArbitrageAction', () => {
7+
const mockRuntime = {
8+
getSetting: vi.fn(),
9+
getService: vi.fn()
10+
};
11+
12+
const mockMessage = {
13+
userId: 'test-user',
14+
content: {
15+
text: 'Execute arbitrage'
16+
}
17+
};
18+
19+
const mockArbitrageService = {
20+
evaluateMarkets: vi.fn(),
21+
executeArbitrage: vi.fn()
22+
};
23+
24+
beforeEach(() => {
25+
vi.clearAllMocks();
26+
mockRuntime.getService.mockReturnValue(mockArbitrageService);
27+
});
28+
29+
describe('metadata', () => {
30+
it('should have correct name and description', () => {
31+
expect(executeArbitrageAction.name).toBe('EXECUTE_ARBITRAGE');
32+
expect(executeArbitrageAction.description).toContain('Execute arbitrage trades');
33+
});
34+
35+
it('should have valid examples', () => {
36+
expect(Array.isArray(executeArbitrageAction.examples)).toBe(true);
37+
executeArbitrageAction.examples.forEach(example => {
38+
expect(Array.isArray(example)).toBe(true);
39+
expect(example.length).toBe(2);
40+
expect(example[1].content.action).toBe('EXECUTE_ARBITRAGE');
41+
});
42+
});
43+
});
44+
45+
describe('validation', () => {
46+
it('should validate required settings', async () => {
47+
mockRuntime.getSetting.mockReturnValue('test-key');
48+
const isValid = await executeArbitrageAction.validate(mockRuntime, mockMessage);
49+
expect(isValid).toBe(true);
50+
});
51+
52+
it('should fail validation when settings are missing', async () => {
53+
mockRuntime.getSetting.mockReturnValue(undefined);
54+
const isValid = await executeArbitrageAction.validate(mockRuntime, mockMessage);
55+
expect(isValid).toBe(false);
56+
});
57+
});
58+
59+
describe('handler', () => {
60+
it('should execute arbitrage when opportunities exist', async () => {
61+
const mockOpportunities = [
62+
{
63+
buyFromMarket: { id: 'market1' },
64+
sellToMarket: { id: 'market2' },
65+
profit: '100'
66+
}
67+
];
68+
69+
mockArbitrageService.evaluateMarkets.mockResolvedValue(mockOpportunities);
70+
mockArbitrageService.executeArbitrage.mockResolvedValue(true);
71+
72+
const result = await executeArbitrageAction.handler(mockRuntime, mockMessage);
73+
expect(result).toBe(true);
74+
expect(mockArbitrageService.evaluateMarkets).toHaveBeenCalled();
75+
expect(mockArbitrageService.executeArbitrage).toHaveBeenCalledWith(mockOpportunities);
76+
});
77+
78+
it('should handle case when no opportunities exist', async () => {
79+
mockArbitrageService.evaluateMarkets.mockResolvedValue([]);
80+
81+
const result = await executeArbitrageAction.handler(mockRuntime, mockMessage);
82+
expect(result).toBe(true);
83+
expect(mockArbitrageService.evaluateMarkets).toHaveBeenCalled();
84+
expect(mockArbitrageService.executeArbitrage).not.toHaveBeenCalled();
85+
});
86+
87+
it('should handle evaluation errors', async () => {
88+
mockArbitrageService.evaluateMarkets.mockRejectedValue(new Error('Evaluation failed'));
89+
90+
await expect(executeArbitrageAction.handler(mockRuntime, mockMessage))
91+
.rejects.toThrow('Evaluation failed');
92+
});
93+
94+
it('should handle execution errors', async () => {
95+
const mockOpportunities = [
96+
{
97+
buyFromMarket: { id: 'market1' },
98+
sellToMarket: { id: 'market2' },
99+
profit: '100'
100+
}
101+
];
102+
103+
mockArbitrageService.evaluateMarkets.mockResolvedValue(mockOpportunities);
104+
mockArbitrageService.executeArbitrage.mockRejectedValue(new Error('Execution failed'));
105+
106+
await expect(executeArbitrageAction.handler(mockRuntime, mockMessage))
107+
.rejects.toThrow('Execution failed');
108+
});
109+
});
110+
});

__tests__/core/Arbitrage.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { Arbitrage } from '../../src/core/Arbitrage';
3+
import { BigNumber } from '@ethersproject/bignumber';
4+
import { TestMarket } from '../utils/TestMarket';
5+
import { CrossedMarketDetails } from '../../src/type';
6+
7+
describe('Arbitrage', () => {
8+
let arbitrage: Arbitrage;
9+
let mockProvider: any;
10+
let mockWallet: any;
11+
12+
beforeEach(() => {
13+
mockProvider = {
14+
getGasPrice: vi.fn().mockResolvedValue(BigNumber.from('50000000000')),
15+
getBlock: vi.fn().mockResolvedValue({ number: 1 })
16+
};
17+
18+
mockWallet = {
19+
provider: mockProvider,
20+
address: '0xmockaddress'
21+
};
22+
23+
arbitrage = new Arbitrage(mockWallet, mockProvider);
24+
});
25+
26+
describe('market evaluation', () => {
27+
it('should filter out markets with insufficient liquidity', async () => {
28+
const mockMarkets = {
29+
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2': [
30+
new TestMarket('0xmarket1', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'),
31+
new TestMarket('0xmarket2', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
32+
]
33+
};
34+
35+
// Mock insufficient liquidity
36+
vi.spyOn(mockMarkets['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'][0], 'getReserves').mockResolvedValue(BigNumber.from('100'));
37+
vi.spyOn(mockMarkets['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'][1], 'getReserves').mockResolvedValue(BigNumber.from('100'));
38+
39+
const opportunities = await arbitrage.evaluateMarkets(mockMarkets);
40+
expect(opportunities.length).toBe(0);
41+
});
42+
});
43+
44+
describe('bundle execution', () => {
45+
it('should handle simulation success', async () => {
46+
const mockMarket = new TestMarket('0xmarket1', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
47+
const mockOpportunities: CrossedMarketDetails[] = [{
48+
marketPairs: [{
49+
buyFromMarket: mockMarket,
50+
sellToMarket: mockMarket
51+
}],
52+
profit: BigNumber.from('1000000'),
53+
volume: BigNumber.from('1000000'),
54+
tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
55+
buyFromMarket: mockMarket,
56+
sellToMarket: mockMarket
57+
}];
58+
59+
await expect(arbitrage.takeCrossedMarkets(mockOpportunities, 1, 1)).resolves.not.toThrow();
60+
});
61+
62+
it('should handle simulation failure', async () => {
63+
const mockMarket = new TestMarket('0xmarket1', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
64+
vi.spyOn(mockMarket, 'sellTokensToNextMarket').mockRejectedValue(new Error('Simulation failed'));
65+
66+
const mockOpportunities: CrossedMarketDetails[] = [{
67+
marketPairs: [{
68+
buyFromMarket: mockMarket,
69+
sellToMarket: mockMarket
70+
}],
71+
profit: BigNumber.from('1000000'),
72+
volume: BigNumber.from('1000000'),
73+
tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
74+
buyFromMarket: mockMarket,
75+
sellToMarket: mockMarket
76+
}];
77+
78+
await expect(arbitrage.takeCrossedMarkets(mockOpportunities, 1, 1)).resolves.not.toThrow();
79+
});
80+
});
81+
});

__tests__/index.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, it, expect } from 'vitest';
2+
import arbitragePlugin from '../src/index';
3+
import { executeArbitrageAction } from '../src/actions/arbitrageAction';
4+
import { marketProvider } from '../src/providers/marketProvider';
5+
import { ArbitrageService } from '../src/services/ArbitrageService';
6+
7+
describe('arbitragePlugin', () => {
8+
it('should have correct name and description', () => {
9+
expect(arbitragePlugin.name).toBe('arbitrage-plugin');
10+
expect(arbitragePlugin.description).toBe('Automated arbitrage trading plugin');
11+
});
12+
13+
it('should register the correct action', () => {
14+
expect(arbitragePlugin.actions).toContain(executeArbitrageAction);
15+
});
16+
17+
it('should register the correct provider', () => {
18+
expect(arbitragePlugin.providers).toContain(marketProvider);
19+
});
20+
21+
it('should register the arbitrage service', () => {
22+
expect(arbitragePlugin.services.length).toBe(1);
23+
expect(arbitragePlugin.services[0]).toBeInstanceOf(ArbitrageService);
24+
});
25+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { ArbitrageService } from '../../src/services/ArbitrageService';
3+
import { ServiceType, IAgentRuntime } from '@elizaos/core';
4+
5+
describe('ArbitrageService', () => {
6+
let arbitrageService: ArbitrageService;
7+
let mockRuntime: IAgentRuntime;
8+
9+
beforeEach(() => {
10+
mockRuntime = {
11+
getSetting: vi.fn((key: string) => {
12+
switch (key) {
13+
case 'ARBITRAGE_ETHEREUM_WS_URL':
14+
return 'ws://test.com';
15+
case 'ARBITRAGE_EVM_PROVIDER_URL':
16+
return 'http://test.com';
17+
case 'ARBITRAGE_EVM_PRIVATE_KEY':
18+
return '0x1234567890123456789012345678901234567890123456789012345678901234';
19+
case 'FLASHBOTS_RELAY_SIGNING_KEY':
20+
return '0x1234567890123456789012345678901234567890123456789012345678901234';
21+
default:
22+
return undefined;
23+
}
24+
}),
25+
getLogger: vi.fn().mockReturnValue({
26+
log: vi.fn(),
27+
error: vi.fn(),
28+
warn: vi.fn()
29+
}),
30+
getBlocksApi: vi.fn().mockReturnValue({
31+
getRecentBlocks: vi.fn().mockResolvedValue([])
32+
})
33+
} as unknown as IAgentRuntime;
34+
35+
arbitrageService = new ArbitrageService();
36+
});
37+
38+
describe('basic functionality', () => {
39+
it('should have correct service type', () => {
40+
expect(arbitrageService.serviceType).toBe(ServiceType.ARBITRAGE);
41+
});
42+
43+
it('should throw error if required settings are missing', async () => {
44+
mockRuntime.getSetting = vi.fn().mockReturnValue(undefined);
45+
await expect(arbitrageService.initialize(mockRuntime)).rejects.toThrow();
46+
});
47+
});
48+
});

__tests__/setup.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { vi } from 'vitest';
2+
import { WebSocket } from 'ws';
3+
import { FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle';
4+
5+
// Mock WebSocket
6+
vi.mock('ws', () => ({
7+
WebSocket: vi.fn().mockImplementation(() => ({
8+
on: vi.fn(),
9+
close: vi.fn(),
10+
send: vi.fn()
11+
}))
12+
}));
13+
14+
// Mock ethers providers
15+
vi.mock('@ethersproject/providers', () => ({
16+
WebSocketProvider: vi.fn().mockImplementation(() => ({
17+
on: vi.fn(),
18+
getGasPrice: vi.fn().mockResolvedValue('1000000000'),
19+
getBlock: vi.fn().mockResolvedValue({ number: 1 })
20+
}))
21+
}));
22+
23+
// Mock Flashbots provider
24+
vi.mock('@flashbots/ethers-provider-bundle', () => ({
25+
FlashbotsBundleProvider: {
26+
create: vi.fn().mockResolvedValue({
27+
sendBundle: vi.fn().mockResolvedValue({
28+
wait: vi.fn().mockResolvedValue(true)
29+
}),
30+
simulate: vi.fn().mockResolvedValue({
31+
success: true,
32+
profit: '1000000000000000'
33+
})
34+
})
35+
}
36+
}));
37+
38+
// Mock @elizaos/core
39+
vi.mock('@elizaos/core', () => ({
40+
Service: class {},
41+
ServiceType: {
42+
ARBITRAGE: 'arbitrage'
43+
},
44+
elizaLogger: {
45+
info: vi.fn(),
46+
error: vi.fn(),
47+
log: vi.fn()
48+
}
49+
}));

__tests__/utils/TestMarket.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { EthMarket } from '../../src/core/EthMarket';
2+
import { BigNumber } from '@ethersproject/bignumber';
3+
4+
export class TestMarket extends EthMarket {
5+
constructor(address: string, tokenAddress: string) {
6+
super(address, tokenAddress, [tokenAddress], {});
7+
}
8+
9+
receiveDirectly(tokenAddress: string): boolean {
10+
return true;
11+
}
12+
13+
async getReserves(tokenAddress: string): Promise<BigNumber> {
14+
return BigNumber.from('1000000');
15+
}
16+
17+
async getTokensOut(tokenIn: string, tokenOut: string, amountIn: BigNumber): Promise<BigNumber> {
18+
return amountIn.mul(95).div(100); // 5% slippage
19+
}
20+
21+
async sellTokens(tokenAddress: string, volume: BigNumber, recipient: string): Promise<string> {
22+
return '0xmocktx';
23+
}
24+
25+
async sellTokensToNextMarket(tokenAddress: string, volume: BigNumber, nextMarket: EthMarket): Promise<{ targets: string[], data: string[] }> {
26+
return {
27+
targets: ['0xmocktarget'],
28+
data: ['0xmockdata']
29+
};
30+
}
31+
}

0 commit comments

Comments
 (0)