@darksol/terminal 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT-2026-03-14.md +241 -0
- package/README.md +16 -0
- package/package.json +1 -1
- package/src/cli.js +93 -0
- package/src/config/store.js +28 -0
- package/src/llm/intent.js +2 -0
- package/src/trading/arb-ai.js +827 -0
- package/src/trading/arb-dexes.js +291 -0
- package/src/trading/arb.js +971 -0
- package/src/trading/index.js +2 -0
- package/src/web/commands.js +99 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* arb-dexes.js — DEX Adapter Registry
|
|
3
|
+
* Each adapter implements: getQuote(tokenIn, tokenOut, amountIn, chain, provider)
|
|
4
|
+
* Returns: { amountOut: bigint, fee: number, gasEstimate: bigint }
|
|
5
|
+
*/
|
|
6
|
+
import { ethers } from 'ethers';
|
|
7
|
+
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════
|
|
9
|
+
// VERIFIED CONTRACT ADDRESSES
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════
|
|
11
|
+
|
|
12
|
+
export const DEX_ADDRESSES = {
|
|
13
|
+
uniswapV3: {
|
|
14
|
+
base: {
|
|
15
|
+
router: '0x2626664c2603336E57B271c5C0b26F421741e481', // SwapRouter02
|
|
16
|
+
quoter: '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a', // QuoterV2
|
|
17
|
+
},
|
|
18
|
+
ethereum: {
|
|
19
|
+
router: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // SwapRouter V1
|
|
20
|
+
quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', // QuoterV2
|
|
21
|
+
},
|
|
22
|
+
arbitrum: {
|
|
23
|
+
router: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
|
|
24
|
+
quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
|
|
25
|
+
},
|
|
26
|
+
optimism: {
|
|
27
|
+
router: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
|
|
28
|
+
quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
|
|
29
|
+
},
|
|
30
|
+
polygon: {
|
|
31
|
+
router: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
|
|
32
|
+
quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
aerodrome: {
|
|
36
|
+
base: {
|
|
37
|
+
router: '0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43',
|
|
38
|
+
factory: '0x420DD381b31aEf6683db6B902084cB0FFECe40Da',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
sushiswap: {
|
|
42
|
+
base: {
|
|
43
|
+
router: '0xFB7eF66a7e61224DD6FcD0D7d9C3Ae5362F52e76', // SushiSwap V3 RouteProcessor3
|
|
44
|
+
quoter: '0xb1E835Dc2785b52265711e17fCCb0fd018226a6e', // SushiSwap QuoterV2
|
|
45
|
+
},
|
|
46
|
+
ethereum: {
|
|
47
|
+
router: '0x2c9E897Ed5A48BbB2da7A4EF68BC9FC1CD12Bb7B',
|
|
48
|
+
quoter: '0x64e829B4fE5ef4dF9E74E44c0d1ABb4E7d253E96',
|
|
49
|
+
},
|
|
50
|
+
arbitrum: {
|
|
51
|
+
router: '0xb590D17D71E7Ff2F332F77fb85Fc45A03D4DAf40',
|
|
52
|
+
quoter: '0x0524E833cCD057e4d7A296e3aaAb9f7675964Ce1',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
velodrome: {
|
|
56
|
+
optimism: {
|
|
57
|
+
router: '0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
quickswap: {
|
|
61
|
+
polygon: {
|
|
62
|
+
router: '0xf5b509bB0909a69B1c207E495f687a596C168E12', // QuickSwap V3
|
|
63
|
+
quoter: '0xa15F0D7377B2A0C0c10db057f641beD21028FC89',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
camelot: {
|
|
67
|
+
arbitrum: {
|
|
68
|
+
router: '0xc873fEcbd354f5A56E00E710B90EF4201db2448d', // Camelot V2 Router
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════
|
|
74
|
+
// ABIs
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════
|
|
76
|
+
|
|
77
|
+
const QUOTER_V2_ABI = [
|
|
78
|
+
'function quoteExactInputSingle(tuple(address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96) params) external returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const AERODROME_ROUTER_ABI = [
|
|
82
|
+
'function getAmountsOut(uint256 amountIn, tuple(address from, address to, bool stable, address factory)[] routes) external view returns (uint256[] amounts)',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const VELODROME_ROUTER_ABI = [
|
|
86
|
+
'function getAmountsOut(uint256 amountIn, tuple(address from, address to, bool stable)[] routes) external view returns (uint256[] amounts)',
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const CAMELOT_ROUTER_ABI = [
|
|
90
|
+
'function getAmountsOut(uint amountIn, address[] path) external view returns (uint[] amounts)',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const QUICKSWAP_QUOTER_ABI = [
|
|
94
|
+
'function quoteExactInputSingle(address tokenIn, address tokenOut, uint256 amountIn, uint160 limitSqrtPrice) external returns (uint256 amountOut, uint16 fee)',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════
|
|
98
|
+
// ADAPTER IMPLEMENTATIONS
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
async function getUniswapV3Quote(tokenIn, tokenOut, amountIn, chain, provider, overrideAddrs = {}) {
|
|
102
|
+
const addrs = (overrideAddrs[chain]?.uniswapV3) || DEX_ADDRESSES.uniswapV3[chain];
|
|
103
|
+
if (!addrs?.quoter) throw new Error(`No Uniswap V3 quoter for chain: ${chain}`);
|
|
104
|
+
|
|
105
|
+
const quoter = new ethers.Contract(addrs.quoter, QUOTER_V2_ABI, provider);
|
|
106
|
+
const feeTiers = [500, 3000, 10000];
|
|
107
|
+
|
|
108
|
+
let bestOut = 0n;
|
|
109
|
+
let bestFee = 3000;
|
|
110
|
+
let bestGas = 180000n;
|
|
111
|
+
|
|
112
|
+
for (const fee of feeTiers) {
|
|
113
|
+
try {
|
|
114
|
+
const result = await quoter.quoteExactInputSingle.staticCall({
|
|
115
|
+
tokenIn, tokenOut, amountIn, fee, sqrtPriceLimitX96: 0,
|
|
116
|
+
});
|
|
117
|
+
if (result[0] > bestOut) {
|
|
118
|
+
bestOut = result[0];
|
|
119
|
+
bestFee = fee;
|
|
120
|
+
bestGas = result[3] || 180000n;
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// fee tier may not have liquidity — skip
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (bestOut === 0n) throw new Error(`No Uniswap V3 liquidity for pair on ${chain}`);
|
|
128
|
+
return { amountOut: bestOut, fee: bestFee, gasEstimate: bestGas };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function getAerodromeQuote(tokenIn, tokenOut, amountIn, provider) {
|
|
132
|
+
const { router, factory } = DEX_ADDRESSES.aerodrome.base;
|
|
133
|
+
const routerContract = new ethers.Contract(router, AERODROME_ROUTER_ABI, provider);
|
|
134
|
+
|
|
135
|
+
let bestOut = 0n;
|
|
136
|
+
|
|
137
|
+
for (const stable of [false, true]) {
|
|
138
|
+
try {
|
|
139
|
+
const routes = [{ from: tokenIn, to: tokenOut, stable, factory }];
|
|
140
|
+
const amounts = await routerContract.getAmountsOut(amountIn, routes);
|
|
141
|
+
const out = amounts[amounts.length - 1];
|
|
142
|
+
if (out > bestOut) bestOut = out;
|
|
143
|
+
} catch {
|
|
144
|
+
// pool may not exist
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (bestOut === 0n) throw new Error('No Aerodrome liquidity for pair');
|
|
149
|
+
return { amountOut: bestOut, fee: 0, gasEstimate: 200000n };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function getSushiswapQuote(tokenIn, tokenOut, amountIn, chain, provider) {
|
|
153
|
+
const addrs = DEX_ADDRESSES.sushiswap[chain];
|
|
154
|
+
if (!addrs?.quoter) throw new Error(`No SushiSwap quoter for chain: ${chain}`);
|
|
155
|
+
|
|
156
|
+
// SushiSwap V3 quoter shares the same interface as Uniswap V3 QuoterV2
|
|
157
|
+
const quoter = new ethers.Contract(addrs.quoter, QUOTER_V2_ABI, provider);
|
|
158
|
+
const feeTiers = [500, 3000, 10000];
|
|
159
|
+
|
|
160
|
+
let bestOut = 0n;
|
|
161
|
+
let bestFee = 3000;
|
|
162
|
+
let bestGas = 180000n;
|
|
163
|
+
|
|
164
|
+
for (const fee of feeTiers) {
|
|
165
|
+
try {
|
|
166
|
+
const result = await quoter.quoteExactInputSingle.staticCall({
|
|
167
|
+
tokenIn, tokenOut, amountIn, fee, sqrtPriceLimitX96: 0,
|
|
168
|
+
});
|
|
169
|
+
if (result[0] > bestOut) {
|
|
170
|
+
bestOut = result[0];
|
|
171
|
+
bestFee = fee;
|
|
172
|
+
bestGas = result[3] || 180000n;
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (bestOut === 0n) throw new Error(`No SushiSwap liquidity for pair on ${chain}`);
|
|
178
|
+
return { amountOut: bestOut, fee: bestFee, gasEstimate: bestGas };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function getVelodromeQuote(tokenIn, tokenOut, amountIn, provider) {
|
|
182
|
+
const { router } = DEX_ADDRESSES.velodrome.optimism;
|
|
183
|
+
const routerContract = new ethers.Contract(router, VELODROME_ROUTER_ABI, provider);
|
|
184
|
+
|
|
185
|
+
let bestOut = 0n;
|
|
186
|
+
|
|
187
|
+
for (const stable of [false, true]) {
|
|
188
|
+
try {
|
|
189
|
+
const amounts = await routerContract.getAmountsOut(amountIn, [{ from: tokenIn, to: tokenOut, stable }]);
|
|
190
|
+
const out = amounts[amounts.length - 1];
|
|
191
|
+
if (out > bestOut) bestOut = out;
|
|
192
|
+
} catch {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (bestOut === 0n) throw new Error('No Velodrome liquidity for pair');
|
|
196
|
+
return { amountOut: bestOut, fee: 0, gasEstimate: 180000n };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function getQuickswapQuote(tokenIn, tokenOut, amountIn, provider) {
|
|
200
|
+
const { quoter } = DEX_ADDRESSES.quickswap.polygon;
|
|
201
|
+
const quoterContract = new ethers.Contract(quoter, QUICKSWAP_QUOTER_ABI, provider);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const result = await quoterContract.quoteExactInputSingle.staticCall(tokenIn, tokenOut, amountIn, 0);
|
|
205
|
+
return { amountOut: result[0], fee: Number(result[1]), gasEstimate: 200000n };
|
|
206
|
+
} catch (e) {
|
|
207
|
+
throw new Error(`No QuickSwap liquidity: ${e.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function getCamelotQuote(tokenIn, tokenOut, amountIn, provider) {
|
|
212
|
+
const { router } = DEX_ADDRESSES.camelot.arbitrum;
|
|
213
|
+
const routerContract = new ethers.Contract(router, CAMELOT_ROUTER_ABI, provider);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const amounts = await routerContract.getAmountsOut(amountIn, [tokenIn, tokenOut]);
|
|
217
|
+
return { amountOut: amounts[1], fee: 0, gasEstimate: 150000n };
|
|
218
|
+
} catch (e) {
|
|
219
|
+
throw new Error(`No Camelot liquidity: ${e.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ═══════════════════════════════════════════════════════════════
|
|
224
|
+
// DEX ADAPTER REGISTRY
|
|
225
|
+
// ═══════════════════════════════════════════════════════════════
|
|
226
|
+
|
|
227
|
+
export const DEX_ADAPTERS = {
|
|
228
|
+
uniswapV3: {
|
|
229
|
+
name: 'Uniswap V3',
|
|
230
|
+
chains: ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'],
|
|
231
|
+
async getQuote(tokenIn, tokenOut, amountIn, chain, provider, opts = {}) {
|
|
232
|
+
return getUniswapV3Quote(tokenIn, tokenOut, amountIn, chain, provider, opts.overrideAddrs);
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
aerodrome: {
|
|
236
|
+
name: 'Aerodrome',
|
|
237
|
+
chains: ['base'],
|
|
238
|
+
async getQuote(tokenIn, tokenOut, amountIn, chain, provider) {
|
|
239
|
+
if (chain !== 'base') throw new Error('Aerodrome is Base-only');
|
|
240
|
+
return getAerodromeQuote(tokenIn, tokenOut, amountIn, provider);
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
sushiswap: {
|
|
244
|
+
name: 'SushiSwap V3',
|
|
245
|
+
chains: ['base', 'ethereum', 'arbitrum'],
|
|
246
|
+
async getQuote(tokenIn, tokenOut, amountIn, chain, provider) {
|
|
247
|
+
return getSushiswapQuote(tokenIn, tokenOut, amountIn, chain, provider);
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
velodrome: {
|
|
251
|
+
name: 'Velodrome',
|
|
252
|
+
chains: ['optimism'],
|
|
253
|
+
async getQuote(tokenIn, tokenOut, amountIn, chain, provider) {
|
|
254
|
+
if (chain !== 'optimism') throw new Error('Velodrome is Optimism-only');
|
|
255
|
+
return getVelodromeQuote(tokenIn, tokenOut, amountIn, provider);
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
quickswap: {
|
|
259
|
+
name: 'QuickSwap V3',
|
|
260
|
+
chains: ['polygon'],
|
|
261
|
+
async getQuote(tokenIn, tokenOut, amountIn, chain, provider) {
|
|
262
|
+
if (chain !== 'polygon') throw new Error('QuickSwap is Polygon-only');
|
|
263
|
+
return getQuickswapQuote(tokenIn, tokenOut, amountIn, provider);
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
camelot: {
|
|
267
|
+
name: 'Camelot',
|
|
268
|
+
chains: ['arbitrum'],
|
|
269
|
+
async getQuote(tokenIn, tokenOut, amountIn, chain, provider) {
|
|
270
|
+
if (chain !== 'arbitrum') throw new Error('Camelot is Arbitrum-only');
|
|
271
|
+
return getCamelotQuote(tokenIn, tokenOut, amountIn, provider);
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get all enabled adapters for a chain
|
|
278
|
+
* @param {string} chain
|
|
279
|
+
* @param {string[]} [enabledKeys] - whitelist of dex keys; null = all
|
|
280
|
+
* @returns {{ key: string, name: string, getQuote: Function }[]}
|
|
281
|
+
*/
|
|
282
|
+
export function getDexesForChain(chain, enabledKeys = null) {
|
|
283
|
+
return Object.entries(DEX_ADAPTERS)
|
|
284
|
+
.filter(([key, adapter]) => {
|
|
285
|
+
if (enabledKeys && !enabledKeys.includes(key)) return false;
|
|
286
|
+
return adapter.chains.includes(chain);
|
|
287
|
+
})
|
|
288
|
+
.map(([key, adapter]) => ({ key, ...adapter }));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default DEX_ADAPTERS;
|