cryptointerface 0.1.0__py3-none-any.whl
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.
- cryptointerface/__init__.py +5 -0
- cryptointerface/dex.py +892 -0
- cryptointerface/flashloan.py +307 -0
- cryptointerface/periphery/config.py +46 -0
- cryptointerface/periphery/db.py +136 -0
- cryptointerface/periphery/dex_architecture.json +119 -0
- cryptointerface/periphery/dex_contracts.json +301 -0
- cryptointerface/periphery/dex_contracts.py +56 -0
- cryptointerface/periphery/enums.py +31 -0
- cryptointerface/periphery/mapping.py +118 -0
- cryptointerface/periphery/utils.py +6 -0
- cryptointerface/providers/endpoints.json +25 -0
- cryptointerface/providers/infura.py +36 -0
- cryptointerface/routes.py +90 -0
- cryptointerface/token.py +264 -0
- cryptointerface/wallet.py +28 -0
- cryptointerface-0.1.0.dist-info/METADATA +354 -0
- cryptointerface-0.1.0.dist-info/RECORD +19 -0
- cryptointerface-0.1.0.dist-info/WHEEL +4 -0
cryptointerface/dex.py
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
from web3 import Web3
|
|
2
|
+
from .providers.infura import Infura
|
|
3
|
+
from .periphery.dex_contracts import get_dex_contracts, _load, get_dex_architecture
|
|
4
|
+
from .periphery.db import _init_tables, insert_data
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
# Minimal ABIs — only the functions needed for pool/pair lookups.
|
|
8
|
+
_V2_FACTORY_ABI = [
|
|
9
|
+
{
|
|
10
|
+
"inputs": [
|
|
11
|
+
{"internalType": "address", "name": "tokenA", "type": "address"},
|
|
12
|
+
{"internalType": "address", "name": "tokenB", "type": "address"},
|
|
13
|
+
],
|
|
14
|
+
"name": "getPair",
|
|
15
|
+
"outputs": [{"internalType": "address", "name": "pair", "type": "address"}],
|
|
16
|
+
"stateMutability": "view",
|
|
17
|
+
"type": "function",
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
_V3_FACTORY_ABI = [
|
|
22
|
+
{
|
|
23
|
+
"inputs": [
|
|
24
|
+
{"internalType": "address", "name": "tokenA", "type": "address"},
|
|
25
|
+
{"internalType": "address", "name": "tokenB", "type": "address"},
|
|
26
|
+
{"internalType": "uint24", "name": "fee", "type": "uint24"},
|
|
27
|
+
],
|
|
28
|
+
"name": "getPool",
|
|
29
|
+
"outputs": [{"internalType": "address", "name": "pool", "type": "address"}],
|
|
30
|
+
"stateMutability": "view",
|
|
31
|
+
"type": "function",
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
_ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
36
|
+
|
|
37
|
+
_V2_PAIR_ABI = [
|
|
38
|
+
{
|
|
39
|
+
"inputs": [],
|
|
40
|
+
"name": "getReserves",
|
|
41
|
+
"outputs": [
|
|
42
|
+
{"internalType": "uint112", "name": "reserve0", "type": "uint112"},
|
|
43
|
+
{"internalType": "uint112", "name": "reserve1", "type": "uint112"},
|
|
44
|
+
{"internalType": "uint32", "name": "blockTimestampLast", "type": "uint32"},
|
|
45
|
+
],
|
|
46
|
+
"stateMutability": "view",
|
|
47
|
+
"type": "function",
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
_V2_ROUTER_ABI = [
|
|
52
|
+
{
|
|
53
|
+
"inputs": [
|
|
54
|
+
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
|
55
|
+
{"internalType": "uint256", "name": "amountOutMin", "type": "uint256"},
|
|
56
|
+
{"internalType": "address[]", "name": "path", "type": "address[]"},
|
|
57
|
+
{"internalType": "address", "name": "to", "type": "address"},
|
|
58
|
+
{"internalType": "uint256", "name": "deadline", "type": "uint256"},
|
|
59
|
+
],
|
|
60
|
+
"name": "swapExactTokensForTokens",
|
|
61
|
+
"outputs": [
|
|
62
|
+
{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}
|
|
63
|
+
],
|
|
64
|
+
"stateMutability": "nonpayable",
|
|
65
|
+
"type": "function",
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
_V3_ROUTER_ABI = [
|
|
70
|
+
{
|
|
71
|
+
"inputs": [
|
|
72
|
+
{
|
|
73
|
+
"components": [
|
|
74
|
+
{"internalType": "address", "name": "tokenIn", "type": "address"},
|
|
75
|
+
{"internalType": "address", "name": "tokenOut", "type": "address"},
|
|
76
|
+
{"internalType": "uint24", "name": "fee", "type": "uint24"},
|
|
77
|
+
{"internalType": "address", "name": "recipient", "type": "address"},
|
|
78
|
+
{"internalType": "uint256", "name": "deadline", "type": "uint256"},
|
|
79
|
+
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
|
80
|
+
{
|
|
81
|
+
"internalType": "uint256",
|
|
82
|
+
"name": "amountOutMinimum",
|
|
83
|
+
"type": "uint256",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"internalType": "uint160",
|
|
87
|
+
"name": "sqrtPriceLimitX96",
|
|
88
|
+
"type": "uint160",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
"internalType": "struct ISwapRouter.ExactInputSingleParams",
|
|
92
|
+
"name": "params",
|
|
93
|
+
"type": "tuple",
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"name": "exactInputSingle",
|
|
97
|
+
"outputs": [
|
|
98
|
+
{"internalType": "uint256", "name": "amountOut", "type": "uint256"}
|
|
99
|
+
],
|
|
100
|
+
"stateMutability": "payable",
|
|
101
|
+
"type": "function",
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
_ALGEBRA_FACTORY_ABI = [
|
|
106
|
+
{
|
|
107
|
+
"inputs": [
|
|
108
|
+
{"internalType": "address", "name": "tokenA", "type": "address"},
|
|
109
|
+
{"internalType": "address", "name": "tokenB", "type": "address"},
|
|
110
|
+
],
|
|
111
|
+
"name": "poolByPair",
|
|
112
|
+
"outputs": [{"internalType": "address", "name": "pool", "type": "address"}],
|
|
113
|
+
"stateMutability": "view",
|
|
114
|
+
"type": "function",
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
_ALGEBRA_ROUTER_ABI = [
|
|
119
|
+
{
|
|
120
|
+
"inputs": [
|
|
121
|
+
{
|
|
122
|
+
"components": [
|
|
123
|
+
{"internalType": "address", "name": "tokenIn", "type": "address"},
|
|
124
|
+
{"internalType": "address", "name": "tokenOut", "type": "address"},
|
|
125
|
+
{"internalType": "address", "name": "recipient", "type": "address"},
|
|
126
|
+
{"internalType": "uint256", "name": "deadline", "type": "uint256"},
|
|
127
|
+
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
|
128
|
+
{
|
|
129
|
+
"internalType": "uint256",
|
|
130
|
+
"name": "amountOutMinimum",
|
|
131
|
+
"type": "uint256",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"internalType": "uint160",
|
|
135
|
+
"name": "limitSqrtPrice",
|
|
136
|
+
"type": "uint160",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
"internalType": "struct ISwapRouter.ExactInputSingleParams",
|
|
140
|
+
"name": "params",
|
|
141
|
+
"type": "tuple",
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"name": "exactInputSingle",
|
|
145
|
+
"outputs": [
|
|
146
|
+
{"internalType": "uint256", "name": "amountOut", "type": "uint256"}
|
|
147
|
+
],
|
|
148
|
+
"stateMutability": "payable",
|
|
149
|
+
"type": "function",
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
_SOLIDLY_FACTORY_ABI = [
|
|
154
|
+
{
|
|
155
|
+
"inputs": [
|
|
156
|
+
{"internalType": "address", "name": "tokenA", "type": "address"},
|
|
157
|
+
{"internalType": "address", "name": "tokenB", "type": "address"},
|
|
158
|
+
{"internalType": "bool", "name": "stable", "type": "bool"},
|
|
159
|
+
],
|
|
160
|
+
"name": "getPool",
|
|
161
|
+
"outputs": [{"internalType": "address", "name": "pool", "type": "address"}],
|
|
162
|
+
"stateMutability": "view",
|
|
163
|
+
"type": "function",
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
_SOLIDLY_ROUTER_ABI = [
|
|
168
|
+
{
|
|
169
|
+
"inputs": [
|
|
170
|
+
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
|
171
|
+
{"internalType": "uint256", "name": "amountOutMin", "type": "uint256"},
|
|
172
|
+
{
|
|
173
|
+
"components": [
|
|
174
|
+
{"internalType": "address", "name": "from", "type": "address"},
|
|
175
|
+
{"internalType": "address", "name": "to", "type": "address"},
|
|
176
|
+
{"internalType": "bool", "name": "stable", "type": "bool"},
|
|
177
|
+
],
|
|
178
|
+
"internalType": "struct IRouter.route[]",
|
|
179
|
+
"name": "routes",
|
|
180
|
+
"type": "tuple[]",
|
|
181
|
+
},
|
|
182
|
+
{"internalType": "address", "name": "to", "type": "address"},
|
|
183
|
+
{"internalType": "uint256", "name": "deadline", "type": "uint256"},
|
|
184
|
+
],
|
|
185
|
+
"name": "swapExactTokensForTokens",
|
|
186
|
+
"outputs": [
|
|
187
|
+
{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}
|
|
188
|
+
],
|
|
189
|
+
"stateMutability": "nonpayable",
|
|
190
|
+
"type": "function",
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
_V3_POOL_ABI = [
|
|
195
|
+
{
|
|
196
|
+
"inputs": [],
|
|
197
|
+
"name": "slot0",
|
|
198
|
+
"outputs": [
|
|
199
|
+
{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"},
|
|
200
|
+
{"internalType": "int24", "name": "tick", "type": "int24"},
|
|
201
|
+
{"internalType": "uint16", "name": "observationIndex", "type": "uint16"},
|
|
202
|
+
{
|
|
203
|
+
"internalType": "uint16",
|
|
204
|
+
"name": "observationCardinality",
|
|
205
|
+
"type": "uint16",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"internalType": "uint16",
|
|
209
|
+
"name": "observationCardinalityNext",
|
|
210
|
+
"type": "uint16",
|
|
211
|
+
},
|
|
212
|
+
{"internalType": "uint8", "name": "feeProtocol", "type": "uint8"},
|
|
213
|
+
{"internalType": "bool", "name": "unlocked", "type": "bool"},
|
|
214
|
+
],
|
|
215
|
+
"stateMutability": "view",
|
|
216
|
+
"type": "function",
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _get_protocol(dex_name: str) -> str:
|
|
222
|
+
"""Return the protocol string from dex_architecture.json, falling back to name-based inference."""
|
|
223
|
+
arch = get_dex_architecture(dex_name)
|
|
224
|
+
if arch:
|
|
225
|
+
return arch["protocol"]
|
|
226
|
+
# Fallback for DEXes not yet in dex_architecture.json
|
|
227
|
+
if "v3" in dex_name.lower():
|
|
228
|
+
return "uniswap_v3"
|
|
229
|
+
return "uniswap_v2"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _get_fee_tiers(dex_name: str) -> list[int]:
|
|
233
|
+
"""Return the fee tiers for a DEX, falling back to standard Uniswap V3 tiers."""
|
|
234
|
+
arch = get_dex_architecture(dex_name)
|
|
235
|
+
if arch and arch.get("fee_tiers"):
|
|
236
|
+
return arch["fee_tiers"]
|
|
237
|
+
return [500, 3000, 100, 10000]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _sort_tokens(token_a: str, token_b: str) -> tuple[str, str]:
|
|
241
|
+
"""
|
|
242
|
+
Return (token0, token1) in canonical Uniswap order (lower address first).
|
|
243
|
+
Both inputs are normalised to checksum addresses before comparison.
|
|
244
|
+
"""
|
|
245
|
+
a = Web3.to_checksum_address(token_a)
|
|
246
|
+
b = Web3.to_checksum_address(token_b)
|
|
247
|
+
return (a, b) if int(a, 16) < int(b, 16) else (b, a)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _resolve_pool_table(dex_name: str) -> str:
|
|
251
|
+
"""Return the pool table name for a given DEX based on its architecture version."""
|
|
252
|
+
arch = get_dex_architecture(dex_name)
|
|
253
|
+
if arch:
|
|
254
|
+
return "pools_v3" if arch["version"] == 3 else "pools_v2"
|
|
255
|
+
if "v3" in dex_name.lower():
|
|
256
|
+
return "pools_v3"
|
|
257
|
+
if "v2" in dex_name.lower():
|
|
258
|
+
return "pools_v2"
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"Cannot resolve pool table for '{dex_name}': not found in dex_architecture.json."
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def create_dex_mapping(dex_names: list[str], chain_ids: list[str], infura_obj) -> dict:
|
|
265
|
+
if isinstance(dex_names, str):
|
|
266
|
+
dex_names = [dex_names]
|
|
267
|
+
if isinstance(chain_ids, str):
|
|
268
|
+
chain_ids = [chain_ids]
|
|
269
|
+
mapping = {}
|
|
270
|
+
for d in dex_names:
|
|
271
|
+
for _id in chain_ids:
|
|
272
|
+
mapping[d] = Dex(d, _id, infura_obj.get_url(_id))
|
|
273
|
+
return mapping
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class Dex:
|
|
277
|
+
def __init__(self, dex_name: str, chain_id: str, rpc_url: str = None):
|
|
278
|
+
self.name = dex_name
|
|
279
|
+
self.chain_id = chain_id
|
|
280
|
+
self.rpc_url = rpc_url
|
|
281
|
+
self.interface = DexInterface(rpc_url=rpc_url)
|
|
282
|
+
self._factory = None
|
|
283
|
+
self._router = None
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def factory(self):
|
|
287
|
+
if self._factory is None:
|
|
288
|
+
self._factory = self.interface.get_factory(self.name, self.chain_id)
|
|
289
|
+
return self._factory
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def router(self):
|
|
293
|
+
if self._router is None:
|
|
294
|
+
self._router = self.interface.get_router(self.name, self.chain_id)
|
|
295
|
+
return self._router
|
|
296
|
+
|
|
297
|
+
def pool_address(
|
|
298
|
+
self, token_a_address: str, token_b_address: str, fee_tier: str = None
|
|
299
|
+
):
|
|
300
|
+
address = self.interface.get_pool_address(
|
|
301
|
+
token_a=token_a_address,
|
|
302
|
+
token_b=token_b_address,
|
|
303
|
+
dex_name=self.name,
|
|
304
|
+
chain_id=self.chain_id,
|
|
305
|
+
fee=fee_tier,
|
|
306
|
+
)
|
|
307
|
+
return address
|
|
308
|
+
|
|
309
|
+
def get_price(
|
|
310
|
+
self, token_a_address: str, token_b_address: str, fee_tier: int = None
|
|
311
|
+
):
|
|
312
|
+
price = self.interface.get_price(
|
|
313
|
+
token_a_address,
|
|
314
|
+
token_b_address,
|
|
315
|
+
dex_name=self.name,
|
|
316
|
+
chain_id=self.chain_id,
|
|
317
|
+
fee=fee_tier,
|
|
318
|
+
)
|
|
319
|
+
return price
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class DexInterface:
|
|
323
|
+
def __init__(self, rpc_url: str, debug: bool = True):
|
|
324
|
+
self.w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 10}))
|
|
325
|
+
self.dex_contracts = None
|
|
326
|
+
self.debug = debug
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def conn(self):
|
|
330
|
+
return _init_tables()
|
|
331
|
+
|
|
332
|
+
def set_dex_contracts(self):
|
|
333
|
+
self.dex_contracts = _load()
|
|
334
|
+
|
|
335
|
+
def get_dex_contracts(self):
|
|
336
|
+
if self.dex_contracts is None:
|
|
337
|
+
self.set_dex_contracts()
|
|
338
|
+
return self.dex_contracts
|
|
339
|
+
|
|
340
|
+
def get_router(self, dex_name: str, chain_id: str):
|
|
341
|
+
data = self.get_dex_contracts()
|
|
342
|
+
return self._get(
|
|
343
|
+
dex_name=dex_name, chain_id=chain_id, data=data, contract_type="router"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def get_factory(self, dex_name: str, chain_id: str):
|
|
347
|
+
data = self.get_dex_contracts()
|
|
348
|
+
return self._get(
|
|
349
|
+
dex_name=dex_name, chain_id=chain_id, data=data, contract_type="factory"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def get_pool_address(
|
|
353
|
+
self,
|
|
354
|
+
token_a: str,
|
|
355
|
+
token_b: str,
|
|
356
|
+
dex_name: str,
|
|
357
|
+
chain_id: int,
|
|
358
|
+
fee: int = None,
|
|
359
|
+
) -> str | None:
|
|
360
|
+
token_0, token_1 = _sort_tokens(token_a, token_b)
|
|
361
|
+
token_0 = Web3.to_checksum_address(token_0)
|
|
362
|
+
token_1 = Web3.to_checksum_address(token_1)
|
|
363
|
+
is_v3 = _get_protocol(dex_name) in ("uniswap_v3", "algebra_v3")
|
|
364
|
+
if is_v3:
|
|
365
|
+
cached = self.read_pool_v3(dex_name, chain_id, token_0, token_1, fee)
|
|
366
|
+
if not cached.is_empty():
|
|
367
|
+
return cached["address"][0]
|
|
368
|
+
result = self._fetch_pool_address(
|
|
369
|
+
token_0, token_1, dex_name, str(chain_id), fee
|
|
370
|
+
)
|
|
371
|
+
if result is not None:
|
|
372
|
+
address, matched_fee = result
|
|
373
|
+
self.insert_pool_v3(
|
|
374
|
+
dex_name, chain_id, token_0, token_1, matched_fee, address
|
|
375
|
+
)
|
|
376
|
+
return address
|
|
377
|
+
else:
|
|
378
|
+
cached = self.read_pool_v2(dex_name, chain_id, token_0, token_1)
|
|
379
|
+
if not cached.is_empty():
|
|
380
|
+
return cached["address"][0]
|
|
381
|
+
result = self._fetch_pool_address(token_0, token_1, dex_name, str(chain_id))
|
|
382
|
+
if result is not None:
|
|
383
|
+
address, _ = result
|
|
384
|
+
self.insert_pool_v2(dex_name, chain_id, token_0, token_1, address)
|
|
385
|
+
return address
|
|
386
|
+
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
def _fetch_pool_address(
|
|
390
|
+
self,
|
|
391
|
+
token_0: str,
|
|
392
|
+
token_1: str,
|
|
393
|
+
dex_name: str,
|
|
394
|
+
chain_id: str,
|
|
395
|
+
fee: int = None,
|
|
396
|
+
) -> tuple[str, int | None] | None:
|
|
397
|
+
"""
|
|
398
|
+
Fetch pool address from chain. Returns (address, fee) on success, None if not found.
|
|
399
|
+
Tokens must already be sorted before calling this method.
|
|
400
|
+
fee is unused for algebra_v3 and solidly_v2 (stored as 0).
|
|
401
|
+
"""
|
|
402
|
+
if self.debug:
|
|
403
|
+
print(f"Fetching pool address from: {dex_name}")
|
|
404
|
+
factory_address = self.get_factory(dex_name, chain_id)
|
|
405
|
+
if factory_address is None:
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
token_0 = Web3.to_checksum_address(token_0)
|
|
409
|
+
token_1 = Web3.to_checksum_address(token_1)
|
|
410
|
+
factory = Web3.to_checksum_address(factory_address)
|
|
411
|
+
protocol = _get_protocol(dex_name)
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
if protocol == "uniswap_v3":
|
|
415
|
+
contract = self.w3.eth.contract(address=factory, abi=_V3_FACTORY_ABI)
|
|
416
|
+
fee_tiers = [fee] if fee is not None else _get_fee_tiers(dex_name)
|
|
417
|
+
for tier in fee_tiers:
|
|
418
|
+
pool = contract.functions.getPool(token_0, token_1, tier).call()
|
|
419
|
+
if pool != _ZERO_ADDRESS:
|
|
420
|
+
if self.debug:
|
|
421
|
+
print(f"Found pool at fee tier {tier}: {pool}")
|
|
422
|
+
return pool, tier
|
|
423
|
+
if self.debug:
|
|
424
|
+
print(f"No pool found for {token_0}/{token_1} on {dex_name} (chain {chain_id})")
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
elif protocol == "algebra_v3":
|
|
428
|
+
contract = self.w3.eth.contract(address=factory, abi=_ALGEBRA_FACTORY_ABI)
|
|
429
|
+
pool = contract.functions.poolByPair(token_0, token_1).call()
|
|
430
|
+
if pool == _ZERO_ADDRESS:
|
|
431
|
+
if self.debug:
|
|
432
|
+
print(f"No pool found for {token_0}/{token_1} on {dex_name} (chain {chain_id})")
|
|
433
|
+
return None
|
|
434
|
+
if self.debug:
|
|
435
|
+
print(f"Found Algebra pool: {pool}")
|
|
436
|
+
return pool, 0
|
|
437
|
+
|
|
438
|
+
elif protocol == "solidly_v2":
|
|
439
|
+
contract = self.w3.eth.contract(address=factory, abi=_SOLIDLY_FACTORY_ABI)
|
|
440
|
+
for stable in (False, True):
|
|
441
|
+
pool = contract.functions.getPool(token_0, token_1, stable).call()
|
|
442
|
+
if pool != _ZERO_ADDRESS:
|
|
443
|
+
if self.debug:
|
|
444
|
+
print(f"Found Solidly pool (stable={stable}): {pool}")
|
|
445
|
+
return pool, None
|
|
446
|
+
if self.debug:
|
|
447
|
+
print(f"No pool found for {token_0}/{token_1} on {dex_name} (chain {chain_id})")
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
else: # uniswap_v2
|
|
451
|
+
contract = self.w3.eth.contract(address=factory, abi=_V2_FACTORY_ABI)
|
|
452
|
+
pair = contract.functions.getPair(token_0, token_1).call()
|
|
453
|
+
if pair == _ZERO_ADDRESS:
|
|
454
|
+
if self.debug:
|
|
455
|
+
print(f"No pair found for {token_0}/{token_1} on {dex_name} (chain {chain_id})")
|
|
456
|
+
return None
|
|
457
|
+
return pair, None
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
if self.debug:
|
|
461
|
+
print(f"Contract call failed for {dex_name} (chain {chain_id}): {e}")
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
################## Insert ##################
|
|
465
|
+
def insert_pool_v2(
|
|
466
|
+
self, dex: str, chain_id: int, token_0: str, token_1: str, address: str
|
|
467
|
+
) -> None:
|
|
468
|
+
df = pl.DataFrame(
|
|
469
|
+
{
|
|
470
|
+
"dex": [dex],
|
|
471
|
+
"chain_id": [chain_id],
|
|
472
|
+
"token_0": [token_0],
|
|
473
|
+
"token_1": [token_1],
|
|
474
|
+
"address": [address],
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
insert_data(
|
|
478
|
+
df,
|
|
479
|
+
db_cols=["dex", "chain_id", "token_0", "token_1", "address"],
|
|
480
|
+
table_name="pools_v2",
|
|
481
|
+
conn=self.conn,
|
|
482
|
+
pk_cols=["dex", "chain_id", "token_0", "token_1"],
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def insert_pool_v3(
|
|
486
|
+
self,
|
|
487
|
+
dex: str,
|
|
488
|
+
chain_id: int,
|
|
489
|
+
token_0: str,
|
|
490
|
+
token_1: str,
|
|
491
|
+
fee: int,
|
|
492
|
+
address: str,
|
|
493
|
+
) -> None:
|
|
494
|
+
df = pl.DataFrame(
|
|
495
|
+
{
|
|
496
|
+
"dex": [dex],
|
|
497
|
+
"chain_id": [chain_id],
|
|
498
|
+
"token_0": [token_0],
|
|
499
|
+
"token_1": [token_1],
|
|
500
|
+
"fee": [fee],
|
|
501
|
+
"address": [address],
|
|
502
|
+
}
|
|
503
|
+
)
|
|
504
|
+
insert_data(
|
|
505
|
+
df,
|
|
506
|
+
db_cols=["dex", "chain_id", "token_0", "token_1", "fee", "address"],
|
|
507
|
+
table_name="pools_v3",
|
|
508
|
+
conn=self.conn,
|
|
509
|
+
pk_cols=["dex", "chain_id", "token_0", "token_1", "fee"],
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
################## Read ##################
|
|
513
|
+
def read_pool_v2(
|
|
514
|
+
self, dex: str, chain_id: int, token_0: str, token_1: str
|
|
515
|
+
) -> pl.DataFrame:
|
|
516
|
+
query = """
|
|
517
|
+
SELECT * FROM pools_v2
|
|
518
|
+
WHERE dex = ? AND chain_id = ? AND token_0 = ? AND token_1 = ?
|
|
519
|
+
"""
|
|
520
|
+
return self.conn.execute(query, [dex, chain_id, token_0, token_1]).pl()
|
|
521
|
+
|
|
522
|
+
def read_pool_v3(
|
|
523
|
+
self, dex: str, chain_id: int, token_0: str, token_1: str, fee: int = None
|
|
524
|
+
) -> pl.DataFrame:
|
|
525
|
+
if fee is not None:
|
|
526
|
+
query = """
|
|
527
|
+
SELECT * FROM pools_v3
|
|
528
|
+
WHERE dex = ? AND chain_id = ? AND token_0 = ? AND token_1 = ? AND fee = ?
|
|
529
|
+
"""
|
|
530
|
+
return self.conn.execute(query, [dex, chain_id, token_0, token_1, fee]).pl()
|
|
531
|
+
else:
|
|
532
|
+
query = """
|
|
533
|
+
SELECT * FROM pools_v3
|
|
534
|
+
WHERE dex = ? AND chain_id = ? AND token_0 = ? AND token_1 = ?
|
|
535
|
+
"""
|
|
536
|
+
return self.conn.execute(query, [dex, chain_id, token_0, token_1]).pl()
|
|
537
|
+
|
|
538
|
+
################## Prices ##################
|
|
539
|
+
def get_price(
|
|
540
|
+
self,
|
|
541
|
+
token_a: str,
|
|
542
|
+
token_b: str,
|
|
543
|
+
dex_name: str,
|
|
544
|
+
chain_id: int,
|
|
545
|
+
fee: int = None,
|
|
546
|
+
) -> float | None:
|
|
547
|
+
"""
|
|
548
|
+
Return the price of token_a denominated in token_b.
|
|
549
|
+
(i.e. how much token_b you receive for 1 token_a)
|
|
550
|
+
"""
|
|
551
|
+
token_0, token_1 = _sort_tokens(token_a, token_b)
|
|
552
|
+
pool_address = self.get_pool_address(token_0, token_1, dex_name, chain_id, fee)
|
|
553
|
+
if pool_address is None:
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
decimals_0 = self._get_token_decimals(token_0, str(chain_id))
|
|
557
|
+
decimals_1 = self._get_token_decimals(token_1, str(chain_id))
|
|
558
|
+
|
|
559
|
+
protocol = _get_protocol(dex_name)
|
|
560
|
+
if protocol in ("uniswap_v3", "algebra_v3"):
|
|
561
|
+
price = self._get_v3_price(pool_address, decimals_0, decimals_1)
|
|
562
|
+
else:
|
|
563
|
+
price = self._get_v2_price(pool_address, decimals_0, decimals_1)
|
|
564
|
+
|
|
565
|
+
if price is None:
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
# If the caller passed tokens in reverse order, invert the price.
|
|
569
|
+
token_a_cs = Web3.to_checksum_address(token_a)
|
|
570
|
+
if token_a_cs != token_0:
|
|
571
|
+
return 1 / price
|
|
572
|
+
return price
|
|
573
|
+
|
|
574
|
+
def _get_v2_price(
|
|
575
|
+
self, pool_address: str, decimals_0: int, decimals_1: int
|
|
576
|
+
) -> float | None:
|
|
577
|
+
"""
|
|
578
|
+
Derive price from V2 pair reserves.
|
|
579
|
+
Returns price of token0 in terms of token1.
|
|
580
|
+
"""
|
|
581
|
+
contract = self.w3.eth.contract(
|
|
582
|
+
address=Web3.to_checksum_address(pool_address),
|
|
583
|
+
abi=_V2_PAIR_ABI,
|
|
584
|
+
)
|
|
585
|
+
reserve0, reserve1, _ = contract.functions.getReserves().call()
|
|
586
|
+
if reserve0 == 0:
|
|
587
|
+
return None
|
|
588
|
+
return (reserve1 / 10**decimals_1) / (reserve0 / 10**decimals_0)
|
|
589
|
+
|
|
590
|
+
def _get_v3_price(
|
|
591
|
+
self, pool_address: str, decimals_0: int, decimals_1: int
|
|
592
|
+
) -> float | None:
|
|
593
|
+
"""
|
|
594
|
+
Derive price from V3 pool sqrtPriceX96.
|
|
595
|
+
Returns price of token0 in terms of token1.
|
|
596
|
+
"""
|
|
597
|
+
contract = self.w3.eth.contract(
|
|
598
|
+
address=Web3.to_checksum_address(pool_address),
|
|
599
|
+
abi=_V3_POOL_ABI,
|
|
600
|
+
)
|
|
601
|
+
sqrt_price_x96 = contract.functions.slot0().call()[0]
|
|
602
|
+
if sqrt_price_x96 == 0:
|
|
603
|
+
return None
|
|
604
|
+
price = (sqrt_price_x96 / (2**96)) ** 2
|
|
605
|
+
return price * (10**decimals_0 / 10**decimals_1)
|
|
606
|
+
|
|
607
|
+
def _get_token_decimals(self, address: str, chain_id: str) -> int:
|
|
608
|
+
"""Look up token decimals from the tokens table. Defaults to 18 if not found."""
|
|
609
|
+
result = self.conn.execute(
|
|
610
|
+
"SELECT decimals FROM tokens WHERE LOWER(address) = ? AND chain_id = ?",
|
|
611
|
+
[address.lower(), chain_id],
|
|
612
|
+
).fetchone()
|
|
613
|
+
if result and result[0] is not None:
|
|
614
|
+
return result[0]
|
|
615
|
+
if self.debug:
|
|
616
|
+
print(
|
|
617
|
+
f"Decimals not found for {address} on chain {chain_id}, defaulting to 18"
|
|
618
|
+
)
|
|
619
|
+
return 18
|
|
620
|
+
|
|
621
|
+
################## Swaps ##################
|
|
622
|
+
def swap(
|
|
623
|
+
self,
|
|
624
|
+
token_in: str,
|
|
625
|
+
token_out: str,
|
|
626
|
+
amount_in: int,
|
|
627
|
+
dex_name: str,
|
|
628
|
+
chain_id: int,
|
|
629
|
+
sender: str,
|
|
630
|
+
fee: int = None,
|
|
631
|
+
amount_out_min: int = 0,
|
|
632
|
+
deadline: int = None,
|
|
633
|
+
) -> dict | None:
|
|
634
|
+
"""
|
|
635
|
+
Build a swap transaction for token_in -> token_out.
|
|
636
|
+
Returns an unsigned transaction dict ready for signing, or None if the pool is not found.
|
|
637
|
+
amount_in is in raw token units (i.e. already scaled by decimals).
|
|
638
|
+
"""
|
|
639
|
+
if deadline is None:
|
|
640
|
+
import time
|
|
641
|
+
|
|
642
|
+
deadline = int(time.time()) + 300
|
|
643
|
+
|
|
644
|
+
token_in_cs = Web3.to_checksum_address(token_in)
|
|
645
|
+
token_out_cs = Web3.to_checksum_address(token_out)
|
|
646
|
+
|
|
647
|
+
protocol = _get_protocol(dex_name)
|
|
648
|
+
|
|
649
|
+
if protocol == "uniswap_v3":
|
|
650
|
+
pool_address = self.get_pool_address(
|
|
651
|
+
token_in_cs, token_out_cs, dex_name, chain_id, fee
|
|
652
|
+
)
|
|
653
|
+
if pool_address is None:
|
|
654
|
+
return None
|
|
655
|
+
token_0, token_1 = _sort_tokens(token_in_cs, token_out_cs)
|
|
656
|
+
cached = self.read_pool_v3(dex_name, chain_id, token_0, token_1, fee)
|
|
657
|
+
resolved_fee = (
|
|
658
|
+
fee
|
|
659
|
+
if fee is not None
|
|
660
|
+
else (cached["fee"][0] if not cached.is_empty() else None)
|
|
661
|
+
)
|
|
662
|
+
if resolved_fee is None:
|
|
663
|
+
return None
|
|
664
|
+
return self._swap_v3(
|
|
665
|
+
token_in_cs,
|
|
666
|
+
token_out_cs,
|
|
667
|
+
amount_in,
|
|
668
|
+
dex_name,
|
|
669
|
+
str(chain_id),
|
|
670
|
+
sender,
|
|
671
|
+
resolved_fee,
|
|
672
|
+
amount_out_min,
|
|
673
|
+
deadline,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
elif protocol == "algebra_v3":
|
|
677
|
+
pool_address = self.get_pool_address(
|
|
678
|
+
token_in_cs, token_out_cs, dex_name, chain_id
|
|
679
|
+
)
|
|
680
|
+
if pool_address is None:
|
|
681
|
+
return None
|
|
682
|
+
return self._swap_algebra(
|
|
683
|
+
token_in_cs,
|
|
684
|
+
token_out_cs,
|
|
685
|
+
amount_in,
|
|
686
|
+
dex_name,
|
|
687
|
+
str(chain_id),
|
|
688
|
+
sender,
|
|
689
|
+
amount_out_min,
|
|
690
|
+
deadline,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
elif protocol == "solidly_v2":
|
|
694
|
+
pool_address = self.get_pool_address(
|
|
695
|
+
token_in_cs, token_out_cs, dex_name, chain_id
|
|
696
|
+
)
|
|
697
|
+
if pool_address is None:
|
|
698
|
+
return None
|
|
699
|
+
return self._swap_solidly(
|
|
700
|
+
token_in_cs,
|
|
701
|
+
token_out_cs,
|
|
702
|
+
amount_in,
|
|
703
|
+
dex_name,
|
|
704
|
+
str(chain_id),
|
|
705
|
+
sender,
|
|
706
|
+
amount_out_min,
|
|
707
|
+
deadline,
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
else: # uniswap_v2
|
|
711
|
+
pool_address = self.get_pool_address(
|
|
712
|
+
token_in_cs, token_out_cs, dex_name, chain_id
|
|
713
|
+
)
|
|
714
|
+
if pool_address is None:
|
|
715
|
+
return None
|
|
716
|
+
return self._swap_v2(
|
|
717
|
+
token_in_cs,
|
|
718
|
+
token_out_cs,
|
|
719
|
+
amount_in,
|
|
720
|
+
dex_name,
|
|
721
|
+
str(chain_id),
|
|
722
|
+
sender,
|
|
723
|
+
amount_out_min,
|
|
724
|
+
deadline,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
def _swap_v2(
|
|
728
|
+
self,
|
|
729
|
+
token_in: str,
|
|
730
|
+
token_out: str,
|
|
731
|
+
amount_in: int,
|
|
732
|
+
dex_name: str,
|
|
733
|
+
chain_id: str,
|
|
734
|
+
sender: str,
|
|
735
|
+
amount_out_min: int,
|
|
736
|
+
deadline: int,
|
|
737
|
+
) -> dict | None:
|
|
738
|
+
"""Build an unsigned V2 swapExactTokensForTokens transaction."""
|
|
739
|
+
router_address = self.get_router(dex_name, chain_id)
|
|
740
|
+
if router_address is None:
|
|
741
|
+
return None
|
|
742
|
+
router = self.w3.eth.contract(
|
|
743
|
+
address=Web3.to_checksum_address(router_address),
|
|
744
|
+
abi=_V2_ROUTER_ABI,
|
|
745
|
+
)
|
|
746
|
+
sender_cs = Web3.to_checksum_address(sender)
|
|
747
|
+
tx = router.functions.swapExactTokensForTokens(
|
|
748
|
+
amount_in,
|
|
749
|
+
amount_out_min,
|
|
750
|
+
[token_in, token_out],
|
|
751
|
+
sender_cs,
|
|
752
|
+
deadline,
|
|
753
|
+
).build_transaction({"from": sender_cs})
|
|
754
|
+
return tx
|
|
755
|
+
|
|
756
|
+
def _swap_v3(
|
|
757
|
+
self,
|
|
758
|
+
token_in: str,
|
|
759
|
+
token_out: str,
|
|
760
|
+
amount_in: int,
|
|
761
|
+
dex_name: str,
|
|
762
|
+
chain_id: str,
|
|
763
|
+
sender: str,
|
|
764
|
+
fee: int,
|
|
765
|
+
amount_out_min: int,
|
|
766
|
+
deadline: int,
|
|
767
|
+
) -> dict | None:
|
|
768
|
+
"""Build an unsigned V3 exactInputSingle transaction."""
|
|
769
|
+
router_address = self.get_router(dex_name, chain_id)
|
|
770
|
+
if router_address is None:
|
|
771
|
+
return None
|
|
772
|
+
router = self.w3.eth.contract(
|
|
773
|
+
address=Web3.to_checksum_address(router_address),
|
|
774
|
+
abi=_V3_ROUTER_ABI,
|
|
775
|
+
)
|
|
776
|
+
sender_cs = Web3.to_checksum_address(sender)
|
|
777
|
+
tx = router.functions.exactInputSingle(
|
|
778
|
+
{
|
|
779
|
+
"tokenIn": token_in,
|
|
780
|
+
"tokenOut": token_out,
|
|
781
|
+
"fee": fee,
|
|
782
|
+
"recipient": sender_cs,
|
|
783
|
+
"deadline": deadline,
|
|
784
|
+
"amountIn": amount_in,
|
|
785
|
+
"amountOutMinimum": amount_out_min,
|
|
786
|
+
"sqrtPriceLimitX96": 0,
|
|
787
|
+
}
|
|
788
|
+
).build_transaction({"from": sender_cs})
|
|
789
|
+
return tx
|
|
790
|
+
|
|
791
|
+
def _swap_algebra(
|
|
792
|
+
self,
|
|
793
|
+
token_in: str,
|
|
794
|
+
token_out: str,
|
|
795
|
+
amount_in: int,
|
|
796
|
+
dex_name: str,
|
|
797
|
+
chain_id: str,
|
|
798
|
+
sender: str,
|
|
799
|
+
amount_out_min: int,
|
|
800
|
+
deadline: int,
|
|
801
|
+
) -> dict | None:
|
|
802
|
+
"""Build an unsigned Algebra V3 exactInputSingle transaction (no fee field)."""
|
|
803
|
+
router_address = self.get_router(dex_name, chain_id)
|
|
804
|
+
if router_address is None:
|
|
805
|
+
return None
|
|
806
|
+
router = self.w3.eth.contract(
|
|
807
|
+
address=Web3.to_checksum_address(router_address),
|
|
808
|
+
abi=_ALGEBRA_ROUTER_ABI,
|
|
809
|
+
)
|
|
810
|
+
sender_cs = Web3.to_checksum_address(sender)
|
|
811
|
+
tx = router.functions.exactInputSingle(
|
|
812
|
+
{
|
|
813
|
+
"tokenIn": token_in,
|
|
814
|
+
"tokenOut": token_out,
|
|
815
|
+
"recipient": sender_cs,
|
|
816
|
+
"deadline": deadline,
|
|
817
|
+
"amountIn": amount_in,
|
|
818
|
+
"amountOutMinimum": amount_out_min,
|
|
819
|
+
"limitSqrtPrice": 0,
|
|
820
|
+
}
|
|
821
|
+
).build_transaction({"from": sender_cs})
|
|
822
|
+
return tx
|
|
823
|
+
|
|
824
|
+
def _swap_solidly(
|
|
825
|
+
self,
|
|
826
|
+
token_in: str,
|
|
827
|
+
token_out: str,
|
|
828
|
+
amount_in: int,
|
|
829
|
+
dex_name: str,
|
|
830
|
+
chain_id: str,
|
|
831
|
+
sender: str,
|
|
832
|
+
amount_out_min: int,
|
|
833
|
+
deadline: int,
|
|
834
|
+
) -> dict | None:
|
|
835
|
+
"""
|
|
836
|
+
Build an unsigned Solidly swapExactTokensForTokens transaction.
|
|
837
|
+
Tries volatile route first; if no pool exists falls back to stable.
|
|
838
|
+
"""
|
|
839
|
+
router_address = self.get_router(dex_name, chain_id)
|
|
840
|
+
if router_address is None:
|
|
841
|
+
return None
|
|
842
|
+
factory_address = self.get_factory(dex_name, chain_id)
|
|
843
|
+
factory = self.w3.eth.contract(
|
|
844
|
+
address=Web3.to_checksum_address(factory_address),
|
|
845
|
+
abi=_SOLIDLY_FACTORY_ABI,
|
|
846
|
+
)
|
|
847
|
+
# Determine whether the existing pool is stable or volatile.
|
|
848
|
+
stable = False
|
|
849
|
+
if (
|
|
850
|
+
factory.functions.getPool(token_in, token_out, False).call()
|
|
851
|
+
== _ZERO_ADDRESS
|
|
852
|
+
):
|
|
853
|
+
stable = True
|
|
854
|
+
|
|
855
|
+
router = self.w3.eth.contract(
|
|
856
|
+
address=Web3.to_checksum_address(router_address),
|
|
857
|
+
abi=_SOLIDLY_ROUTER_ABI,
|
|
858
|
+
)
|
|
859
|
+
sender_cs = Web3.to_checksum_address(sender)
|
|
860
|
+
tx = router.functions.swapExactTokensForTokens(
|
|
861
|
+
amount_in,
|
|
862
|
+
amount_out_min,
|
|
863
|
+
[{"from": token_in, "to": token_out, "stable": stable}],
|
|
864
|
+
sender_cs,
|
|
865
|
+
deadline,
|
|
866
|
+
).build_transaction({"from": sender_cs})
|
|
867
|
+
return tx
|
|
868
|
+
|
|
869
|
+
def _get(
|
|
870
|
+
self, dex_name: str, chain_id: str, data: dict, contract_type: str
|
|
871
|
+
) -> str | None:
|
|
872
|
+
dex_data = data.get(dex_name)
|
|
873
|
+
if dex_data is None:
|
|
874
|
+
if self.debug:
|
|
875
|
+
print(f"Couldn't find dex: {dex_name}...")
|
|
876
|
+
return None
|
|
877
|
+
|
|
878
|
+
chain_data = dex_data.get(chain_id)
|
|
879
|
+
if chain_data is None:
|
|
880
|
+
if self.debug:
|
|
881
|
+
print(
|
|
882
|
+
f"Couldn't find chain_id: {chain_id}. Available: {list(dex_data.keys())}"
|
|
883
|
+
)
|
|
884
|
+
return None
|
|
885
|
+
|
|
886
|
+
contract_address = chain_data.get(contract_type)
|
|
887
|
+
if contract_address is None and self.debug:
|
|
888
|
+
print(
|
|
889
|
+
f"Couldn't find contract type: {contract_type}. Available: {list(chain_data.keys())}"
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
return contract_address
|