@darksol/terminal 0.12.0 → 0.13.0

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.
@@ -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;