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/token.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import polars as pl
|
|
3
|
+
from web3 import Web3
|
|
4
|
+
from .periphery.db import _init_tables, insert_data
|
|
5
|
+
from .periphery.mapping import COINGECKO_CHAIN_IDS
|
|
6
|
+
|
|
7
|
+
_ERC20_APPROVE_ABI = [
|
|
8
|
+
{
|
|
9
|
+
"inputs": [
|
|
10
|
+
{"internalType": "address", "name": "spender", "type": "address"},
|
|
11
|
+
{"internalType": "uint256", "name": "amount", "type": "uint256"},
|
|
12
|
+
],
|
|
13
|
+
"name": "approve",
|
|
14
|
+
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
|
|
15
|
+
"stateMutability": "nonpayable",
|
|
16
|
+
"type": "function",
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
_ERC20_ALLOWANCE_ABI = [
|
|
21
|
+
{
|
|
22
|
+
"inputs": [
|
|
23
|
+
{"internalType": "address", "name": "owner", "type": "address"},
|
|
24
|
+
{"internalType": "address", "name": "spender", "type": "address"},
|
|
25
|
+
],
|
|
26
|
+
"name": "allowance",
|
|
27
|
+
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
|
|
28
|
+
"stateMutability": "view",
|
|
29
|
+
"type": "function",
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Token:
|
|
35
|
+
def __init__(self, symbol: str, chain_id: str):
|
|
36
|
+
self.symbol = symbol
|
|
37
|
+
self.chain_id = chain_id
|
|
38
|
+
self.interface = TokenInterface()
|
|
39
|
+
|
|
40
|
+
self._address = None
|
|
41
|
+
self._decimals = None
|
|
42
|
+
self._info = None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def address(self):
|
|
46
|
+
if self._address is None:
|
|
47
|
+
self._address = self.interface.get_token_address(self.symbol, self.chain_id)
|
|
48
|
+
return self._address
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def decimals(self) -> int:
|
|
52
|
+
if self._decimals is None:
|
|
53
|
+
self._decimals = self.interface.get_token_decimals(
|
|
54
|
+
self.symbol, self.chain_id
|
|
55
|
+
)
|
|
56
|
+
return self._decimals
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def info(self):
|
|
60
|
+
if self._info is None:
|
|
61
|
+
self._info = self.interface.get_token_info(self.symbol, self.chain_id)
|
|
62
|
+
return self._info
|
|
63
|
+
|
|
64
|
+
def approve(
|
|
65
|
+
self, spender_address: str, amount: int, owner_address: str, rpc_url: str
|
|
66
|
+
) -> dict:
|
|
67
|
+
"""
|
|
68
|
+
Build an unsigned ERC-20 approve transaction.
|
|
69
|
+
amount is in raw token units — use to_wei() to scale beforehand.
|
|
70
|
+
Pass 2**256 - 1 as amount for an unlimited approval.
|
|
71
|
+
"""
|
|
72
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
73
|
+
contract = w3.eth.contract(
|
|
74
|
+
address=Web3.to_checksum_address(self.address),
|
|
75
|
+
abi=_ERC20_APPROVE_ABI,
|
|
76
|
+
)
|
|
77
|
+
owner_cs = Web3.to_checksum_address(owner_address)
|
|
78
|
+
return contract.functions.approve(
|
|
79
|
+
Web3.to_checksum_address(spender_address), amount
|
|
80
|
+
).build_transaction({"from": owner_cs})
|
|
81
|
+
|
|
82
|
+
def check_allowance(
|
|
83
|
+
self, owner_address: str, spender_address: str, rpc_url: str
|
|
84
|
+
) -> int:
|
|
85
|
+
"""
|
|
86
|
+
Return the current ERC-20 allowance (in raw token units) that
|
|
87
|
+
`spender_address` is approved to spend on behalf of `owner_address`.
|
|
88
|
+
"""
|
|
89
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
90
|
+
contract = w3.eth.contract(
|
|
91
|
+
address=Web3.to_checksum_address(self.address),
|
|
92
|
+
abi=_ERC20_ALLOWANCE_ABI,
|
|
93
|
+
)
|
|
94
|
+
return contract.functions.allowance(
|
|
95
|
+
Web3.to_checksum_address(owner_address),
|
|
96
|
+
Web3.to_checksum_address(spender_address),
|
|
97
|
+
).call()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TokenInterface:
|
|
101
|
+
def __init__(self):
|
|
102
|
+
self.table_name = "tokens"
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def conn(self):
|
|
106
|
+
return _init_tables()
|
|
107
|
+
|
|
108
|
+
def get_token_info(self, symbol: str, chain_id: str) -> pl.DataFrame:
|
|
109
|
+
symbol = symbol.upper()
|
|
110
|
+
chain_id = str(chain_id)
|
|
111
|
+
gen = self._read_token_info(symbol)
|
|
112
|
+
print(f"GEN: {gen}")
|
|
113
|
+
df = self._read_token_info(symbol, chain_id)
|
|
114
|
+
print(f"DF: {df}")
|
|
115
|
+
if not df.is_empty():
|
|
116
|
+
return df
|
|
117
|
+
|
|
118
|
+
# Cache miss — download all chains and persist them.
|
|
119
|
+
downloaded = self._download_token_info(symbol)
|
|
120
|
+
if not downloaded.is_empty():
|
|
121
|
+
self._insert_token_info(downloaded)
|
|
122
|
+
|
|
123
|
+
# Re-read so we only return the requested chain.
|
|
124
|
+
df = self._read_token_info(symbol, chain_id)
|
|
125
|
+
return df
|
|
126
|
+
|
|
127
|
+
def get_token_address(self, symbol: str, chain_id: str):
|
|
128
|
+
df = self.get_token_info(symbol=symbol, chain_id=chain_id)
|
|
129
|
+
return df["address"][0]
|
|
130
|
+
|
|
131
|
+
def get_token_decimals(self, symbol: str, chain_id: str):
|
|
132
|
+
df = self.get_token_info(symbol=symbol, chain_id=chain_id)
|
|
133
|
+
return df["decimals"][0]
|
|
134
|
+
|
|
135
|
+
def batch_save(self, symbols: list):
|
|
136
|
+
if isinstance(symbols, str):
|
|
137
|
+
symbols = [symbols]
|
|
138
|
+
|
|
139
|
+
coin_ids = _find_coin_ids()
|
|
140
|
+
|
|
141
|
+
for s in symbols:
|
|
142
|
+
try:
|
|
143
|
+
df = self._download_token_info(s, coin_ids=coin_ids)
|
|
144
|
+
self._insert_token_info(df)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
break
|
|
147
|
+
except KeyboardInterrupt:
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
def _download_token_info(self, symbol: str, coin_ids: list = None) -> dict:
|
|
151
|
+
if coin_ids is None:
|
|
152
|
+
coin_ids = _find_coin_ids(symbol=symbol)
|
|
153
|
+
else:
|
|
154
|
+
coin_ids = _match_coin_ids(coins=coin_ids, symbol=symbol)
|
|
155
|
+
addresses = _fetch_token_addresses(symbol, coin_ids=coin_ids)
|
|
156
|
+
decimals_map = _fetch_token_decimals(symbol, coin_ids=coin_ids)
|
|
157
|
+
decimals = next(iter(decimals_map.values())) if decimals_map else None
|
|
158
|
+
|
|
159
|
+
data = {
|
|
160
|
+
"chain_id": [],
|
|
161
|
+
"address": [],
|
|
162
|
+
}
|
|
163
|
+
for k, v in addresses.items():
|
|
164
|
+
cid = COINGECKO_CHAIN_IDS.get(k, None)
|
|
165
|
+
data["chain_id"].append(str(cid) if cid is not None else None)
|
|
166
|
+
try:
|
|
167
|
+
address = Web3.to_checksum_address(v)
|
|
168
|
+
except ValueError:
|
|
169
|
+
address = v
|
|
170
|
+
data["address"].append(address)
|
|
171
|
+
|
|
172
|
+
df = pl.DataFrame(data)
|
|
173
|
+
df = df.filter(pl.col("chain_id").is_not_null())
|
|
174
|
+
df = df.with_columns(
|
|
175
|
+
pl.lit(symbol).alias("symbol"), pl.lit(decimals).alias("decimals")
|
|
176
|
+
)
|
|
177
|
+
df = df[["symbol", "chain_id", "address", "decimals"]]
|
|
178
|
+
return df
|
|
179
|
+
|
|
180
|
+
def update_decimals(self, symbol: str, decimals: int) -> None:
|
|
181
|
+
self.conn.execute(
|
|
182
|
+
"UPDATE tokens SET decimals = ? WHERE symbol = ?",
|
|
183
|
+
[decimals, symbol],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _insert_token_info(self, df: pl.DataFrame):
|
|
187
|
+
insert_data(
|
|
188
|
+
df,
|
|
189
|
+
db_cols=["symbol", "chain_id", "address", "decimals"],
|
|
190
|
+
table_name=self.table_name,
|
|
191
|
+
conn=self.conn,
|
|
192
|
+
pk_cols=["chain_id", "address"],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _read_token_info(self, symbol: str, chain_id: str = None) -> pl.DataFrame:
|
|
196
|
+
if chain_id:
|
|
197
|
+
query = f"SELECT * FROM {self.table_name} WHERE symbol = '{symbol}' AND chain_id = '{chain_id}'"
|
|
198
|
+
else:
|
|
199
|
+
query = f"SELECT * FROM {self.table_name} WHERE symbol = '{symbol}'"
|
|
200
|
+
return self.conn.execute(query).pl()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _find_coin_ids(symbol: str = None) -> list[dict]:
|
|
204
|
+
"""Return all CoinGecko coin entries matching the given symbol."""
|
|
205
|
+
coins = requests.get(
|
|
206
|
+
"https://api.coingecko.com/api/v3/coins/list",
|
|
207
|
+
params={"include_platform": "true"},
|
|
208
|
+
timeout=15,
|
|
209
|
+
).json()
|
|
210
|
+
if symbol:
|
|
211
|
+
return [c for c in coins if c.get("symbol", "").upper() == symbol]
|
|
212
|
+
else:
|
|
213
|
+
return coins
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _match_coin_ids(coins: list, symbol: str):
|
|
217
|
+
return [c for c in coins if c.get("symbol", "").upper() == symbol]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _fetch_token_addresses(symbol: str, coin_ids: list = None) -> dict[str, str]:
|
|
221
|
+
"""
|
|
222
|
+
Given a token symbol (e.g. "WETH"), return {network: address}
|
|
223
|
+
for all networks known to CoinGecko.
|
|
224
|
+
"""
|
|
225
|
+
addresses: dict[str, str] = {}
|
|
226
|
+
if coin_ids is None:
|
|
227
|
+
coin_ids = _find_coin_ids(symbol)
|
|
228
|
+
for coin in coin_ids:
|
|
229
|
+
for network, address in coin.get("platforms", {}).items():
|
|
230
|
+
if address:
|
|
231
|
+
addresses[network] = address
|
|
232
|
+
return addresses
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _fetch_token_decimals(symbol: str, coin_ids: list = None) -> dict[str, int]:
|
|
236
|
+
"""
|
|
237
|
+
Given a token symbol (e.g. "WETH"), return {network: decimals}
|
|
238
|
+
for every network where CoinGecko reports a decimal_place value.
|
|
239
|
+
"""
|
|
240
|
+
if coin_ids is None:
|
|
241
|
+
matches = _find_coin_ids(symbol)
|
|
242
|
+
else:
|
|
243
|
+
matches = coin_ids
|
|
244
|
+
if not matches:
|
|
245
|
+
return {}
|
|
246
|
+
|
|
247
|
+
decimals: dict[str, int] = {}
|
|
248
|
+
for coin in matches:
|
|
249
|
+
detail = requests.get(
|
|
250
|
+
f"https://api.coingecko.com/api/v3/coins/{coin['id']}",
|
|
251
|
+
params={
|
|
252
|
+
"localization": "false",
|
|
253
|
+
"tickers": "false",
|
|
254
|
+
"market_data": "false",
|
|
255
|
+
"community_data": "false",
|
|
256
|
+
"developer_data": "false",
|
|
257
|
+
},
|
|
258
|
+
timeout=15,
|
|
259
|
+
).json()
|
|
260
|
+
for network, info in detail.get("detail_platforms", {}).items():
|
|
261
|
+
dp = info.get("decimal_place")
|
|
262
|
+
if dp is not None:
|
|
263
|
+
decimals[network] = dp
|
|
264
|
+
return decimals
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from web3 import Web3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Wallet:
|
|
5
|
+
def __init__(self, private_key: str, rpc_url: str):
|
|
6
|
+
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
7
|
+
self.account = self.w3.eth.account.from_key(private_key)
|
|
8
|
+
self.address = self.account.address
|
|
9
|
+
|
|
10
|
+
def sign_and_send(self, tx: dict) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Fill in nonce and gas if missing, sign the transaction, broadcast it,
|
|
13
|
+
and return the transaction hash as a hex string.
|
|
14
|
+
"""
|
|
15
|
+
if "nonce" not in tx:
|
|
16
|
+
tx["nonce"] = self.w3.eth.get_transaction_count(self.address)
|
|
17
|
+
if "gas" not in tx:
|
|
18
|
+
tx["gas"] = self.w3.eth.estimate_gas({**tx, "from": self.address})
|
|
19
|
+
if "gasPrice" not in tx and "maxFeePerGas" not in tx:
|
|
20
|
+
tx["gasPrice"] = self.w3.eth.gas_price
|
|
21
|
+
|
|
22
|
+
signed = self.account.sign_transaction(tx)
|
|
23
|
+
tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
24
|
+
return tx_hash.hex()
|
|
25
|
+
|
|
26
|
+
def wait(self, tx_hash: str, timeout: int = 120) -> dict:
|
|
27
|
+
"""Block until the transaction is mined and return the receipt."""
|
|
28
|
+
return self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cryptointerface
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interface with cryptocurrency and DEX contracts. Store data locally for future use.
|
|
5
|
+
Project-URL: Source, https://github.com/William-Kruta/CryptoInterface
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: aave,arbitrage,crypto,defi,dex,uniswap,web3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Requires-Dist: duckdb>=1.5
|
|
15
|
+
Requires-Dist: polars>=1.0
|
|
16
|
+
Requires-Dist: pyarrow>=14.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0
|
|
18
|
+
Requires-Dist: requests>=2.33
|
|
19
|
+
Requires-Dist: web3>=7.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# CryptoInterface
|
|
23
|
+
|
|
24
|
+
A Python library for interacting with EVM-compatible DEXes — price fetching, swapping.
|
|
25
|
+
|
|
26
|
+
I have provided some starter data to enter in your database.
|
|
27
|
+
|
|
28
|
+
To do this, just write this code:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
from cryptointerface.periphery.db import insert_tokens_csv_to_db, insert_pools_csv_to_db
|
|
32
|
+
|
|
33
|
+
insert_tokens_csv_to_db()
|
|
34
|
+
insert_pools_csv_to_db()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install cryptointerface
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv add cryptointerface
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Requirements:** Python 3.12+
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from cryptointerface import Dex, Token, Wallet, arbitrage_route
|
|
57
|
+
from cryptointerface.providers.infura import Infura
|
|
58
|
+
from cryptointerface.periphery.utils import to_wei, from_wei
|
|
59
|
+
from cryptointerface.periphery.enums import Network, Platform
|
|
60
|
+
|
|
61
|
+
infura = Infura(api_key="YOUR_INFURA_KEY")
|
|
62
|
+
|
|
63
|
+
# Look up a token
|
|
64
|
+
token = Token("WETH", chain_id=Network.ARBITRUM.value)
|
|
65
|
+
print(token.address, token.decimals)
|
|
66
|
+
|
|
67
|
+
# Fetch a price
|
|
68
|
+
dex = Dex("uniswap_v3", chain_id="42161", rpc_url=infura.get_url("42161"))
|
|
69
|
+
price = dex.get_price(weth_address, usdc_address)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Core Modules
|
|
75
|
+
|
|
76
|
+
### `Token` / `TokenInterface`
|
|
77
|
+
|
|
78
|
+
Resolves token metadata from a local DuckDB cache, downloading from CoinGecko on first use.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from cryptointerface import Token, TokenInterface
|
|
82
|
+
|
|
83
|
+
token = Token("USDC", chain_id="42161")
|
|
84
|
+
print(token.address) # checksummed address
|
|
85
|
+
print(token.decimals) # 6
|
|
86
|
+
print(token.info) # polars DataFrame
|
|
87
|
+
|
|
88
|
+
# Build an ERC-20 approve transaction (unsigned)
|
|
89
|
+
tx = token.approve(
|
|
90
|
+
spender_address="0xRouter...",
|
|
91
|
+
amount=to_wei(1000, decimals=6),
|
|
92
|
+
owner_address="0xYourWallet",
|
|
93
|
+
rpc_url="https://...",
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`TokenInterface` provides the lower-level DB methods (`get_token_info`, `get_token_address`, `get_token_decimals`, `batch_save`, `update_decimals`).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### `Dex` / `DexInterface`
|
|
102
|
+
|
|
103
|
+
Fetches pool addresses and prices, and builds swap transactions for all supported DEX protocols.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from cryptointerface import Dex
|
|
107
|
+
|
|
108
|
+
dex = Dex("uniswap_v3", chain_id="42161", rpc_url="https://...")
|
|
109
|
+
|
|
110
|
+
# Pool address (cached in DuckDB on first call)
|
|
111
|
+
pool = dex.pool_address(token_a_address, token_b_address)
|
|
112
|
+
|
|
113
|
+
# Spot price — token_a denominated in token_b
|
|
114
|
+
price = dex.get_price(token_a_address, token_b_address)
|
|
115
|
+
|
|
116
|
+
# Build an unsigned swap transaction
|
|
117
|
+
tx = dex.interface.swap(
|
|
118
|
+
token_in=token_a_address,
|
|
119
|
+
token_out=token_b_address,
|
|
120
|
+
amount_in=to_wei(1.0),
|
|
121
|
+
dex_name="uniswap_v3",
|
|
122
|
+
chain_id="42161",
|
|
123
|
+
sender="0xYourWallet",
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Supported DEXes
|
|
128
|
+
|
|
129
|
+
| Name | Protocol | Chains |
|
|
130
|
+
| ---------------- | --------------- | ---------------------------------------------------- |
|
|
131
|
+
| `uniswap_v2` | Uniswap V2 | Ethereum, Sepolia |
|
|
132
|
+
| `uniswap_v3` | Uniswap V3 | Ethereum, Arbitrum, Polygon, Optimism, Base, Sepolia |
|
|
133
|
+
| `sushiswap_v2` | Uniswap V2 fork | Ethereum, Arbitrum, Polygon, Sepolia |
|
|
134
|
+
| `sushiswap_v3` | Uniswap V3 fork | Ethereum, Arbitrum, Polygon, Sepolia |
|
|
135
|
+
| `pancakeswap_v2` | Uniswap V2 fork | BSC, Ethereum |
|
|
136
|
+
| `pancakeswap_v3` | Uniswap V3 fork | BSC, Ethereum |
|
|
137
|
+
| `quickswap_v2` | Uniswap V2 fork | Polygon |
|
|
138
|
+
| `quickswap_v3` | Algebra V3 | Polygon |
|
|
139
|
+
| `camelot_v2` | Uniswap V2 fork | Arbitrum |
|
|
140
|
+
| `camelot_v3` | Algebra V3 | Arbitrum |
|
|
141
|
+
| `traderjoe_v1` | Uniswap V2 fork | Avalanche, Arbitrum |
|
|
142
|
+
| `aerodrome` | Solidly V2 | Base |
|
|
143
|
+
| `velodrome` | Solidly V2 | Optimism |
|
|
144
|
+
|
|
145
|
+
#### Protocols
|
|
146
|
+
|
|
147
|
+
- **Uniswap V2** — `getPair` factory, fixed fee per DEX, `getReserves` pricing
|
|
148
|
+
- **Uniswap V3** — `getPool` factory, per-pool fee tiers, `sqrtPriceX96` pricing
|
|
149
|
+
- **Algebra V3** — `poolByPair` factory, dynamic fees, no fee argument (QuickSwap, Camelot V3)
|
|
150
|
+
- **Solidly V2** — `getPool(stable: bool)` factory, stable/volatile pools (Aerodrome, Velodrome)
|
|
151
|
+
|
|
152
|
+
#### `create_dex_mapping`
|
|
153
|
+
|
|
154
|
+
Build a multi-DEX dict for use with `get_prices` / `arbitrage_route`:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from cryptointerface.dex import create_dex_mapping
|
|
158
|
+
|
|
159
|
+
dex_mapping = create_dex_mapping(
|
|
160
|
+
dex_names=["uniswap_v3", "sushiswap_v3", "camelot_v3"],
|
|
161
|
+
chain_ids=["42161"],
|
|
162
|
+
infura_obj=infura,
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### `routes` — Prices & Arbitrage
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from cryptointerface import get_prices, arbitrage_route
|
|
172
|
+
|
|
173
|
+
# Parallel price fetch across all DEXes
|
|
174
|
+
prices = get_prices(dex_mapping, token_a_address, token_b_address)
|
|
175
|
+
# {"uniswap_v3": {"price": 1823.4, "pool": "0x...", "fee_bps": 5}, ...}
|
|
176
|
+
|
|
177
|
+
# Arbitrage analysis
|
|
178
|
+
route = arbitrage_route(dex_mapping, token_a_address, token_b_address)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`arbitrage_route` returns:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
{
|
|
185
|
+
"buy": {"dex": "camelot_v3", "price": 1820.1, "pool": "0x...", "fee_bps": 5},
|
|
186
|
+
"sell": {"dex": "uniswap_v3", "price": 1825.8, "pool": "0x...", "fee_bps": 5},
|
|
187
|
+
"spread_abs": 5.7,
|
|
188
|
+
"spread_pct": 0.3132,
|
|
189
|
+
"total_fee_pct": 0.1, # both legs combined
|
|
190
|
+
"net_spread_pct": 0.2132, # spread after fees
|
|
191
|
+
"profitable": True,
|
|
192
|
+
"all_prices": {...},
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Fees are sourced from `dex_architecture.json` — V3 fee tiers use the matched pool tier; V2 uses the fixed DEX fee.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### `Wallet`
|
|
201
|
+
|
|
202
|
+
Signs and broadcasts unsigned transactions built by `Dex`, `Token`, or `AaveFlashLoan`.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from cryptointerface import Wallet
|
|
206
|
+
|
|
207
|
+
wallet = Wallet(private_key="0x...", rpc_url="https://...")
|
|
208
|
+
|
|
209
|
+
tx_hash = wallet.sign_and_send(tx) # auto-fills nonce, gas, gasPrice
|
|
210
|
+
receipt = wallet.wait(tx_hash) # blocks until mined
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
### `AaveFlashLoan`
|
|
216
|
+
|
|
217
|
+
Builds Aave V3 flash loan transactions. Supported chains: Ethereum, Optimism, BSC, Polygon, Base, Arbitrum, Avalanche, Sepolia.
|
|
218
|
+
|
|
219
|
+
Flash loan fee: **0.09%** (9 bps).
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from cryptointerface import AaveFlashLoan
|
|
223
|
+
from cryptointerface.periphery.utils import to_wei
|
|
224
|
+
|
|
225
|
+
fl = AaveFlashLoan(chain_id="42161", rpc_url="https://...")
|
|
226
|
+
|
|
227
|
+
# Single-asset flash loan (lower gas)
|
|
228
|
+
tx = fl.flash_loan_simple_tx(
|
|
229
|
+
receiver_contract="0xYourDeployedExecutor",
|
|
230
|
+
asset=usdc_address,
|
|
231
|
+
amount=to_wei(10_000, decimals=6),
|
|
232
|
+
sender=wallet.address,
|
|
233
|
+
)
|
|
234
|
+
tx_hash = wallet.sign_and_send(tx)
|
|
235
|
+
|
|
236
|
+
# Multi-asset flash loan
|
|
237
|
+
tx = fl.flash_loan_tx(
|
|
238
|
+
receiver_contract="0xYourDeployedExecutor",
|
|
239
|
+
assets=[weth_address, usdc_address],
|
|
240
|
+
amounts=[to_wei(5.0), to_wei(10_000, decimals=6)],
|
|
241
|
+
sender=wallet.address,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Check profitability after the flash loan fee
|
|
245
|
+
result = AaveFlashLoan.net_profit(
|
|
246
|
+
amount=to_wei(10_000, decimals=6),
|
|
247
|
+
gross_profit_wei=to_wei(15, decimals=6),
|
|
248
|
+
token_decimals=6,
|
|
249
|
+
)
|
|
250
|
+
# {"fee_wei": 9000, "net_profit_wei": 6000, "net_profit_human": 0.006, "is_profitable": True}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Solidity Executor Contract
|
|
254
|
+
|
|
255
|
+
The flash loan callback (`executeOperation`) must be implemented in Solidity. A full template is available as:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
print(AaveFlashLoan.EXECUTOR_TEMPLATE)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Deploy this contract once, paste your swap logic inside `executeOperation`, then pass its address as `receiver_contract`.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
### `Infura`
|
|
266
|
+
|
|
267
|
+
RPC URL builder for Infura endpoints.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from cryptointerface.providers.infura import Infura
|
|
271
|
+
|
|
272
|
+
infura = Infura(api_key="YOUR_KEY")
|
|
273
|
+
rpc_url = infura.get_url("42161") # Arbitrum
|
|
274
|
+
|
|
275
|
+
# Add a custom endpoint
|
|
276
|
+
infura.add_endpoint(chain_id="10", endpoint="https://optimism-mainnet.infura.io/v3/")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Local Database (DuckDB)
|
|
282
|
+
|
|
283
|
+
Token and pool data is cached locally in a DuckDB file (`~/.cryptointerface/data.db` by default). All reads/writes are thread-safe via thread-local connections and a write lock.
|
|
284
|
+
|
|
285
|
+
### Schema
|
|
286
|
+
|
|
287
|
+
**`tokens`** — `symbol`, `chain_id`, `address`, `decimals`
|
|
288
|
+
|
|
289
|
+
**`pools_v2`** — `dex`, `chain_id`, `token_0`, `token_1`, `address`
|
|
290
|
+
|
|
291
|
+
**`pools_v3`** — `dex`, `chain_id`, `token_0`, `token_1`, `fee`, `address`
|
|
292
|
+
|
|
293
|
+
### CSV Import / Export
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
from cryptointerface.periphery.db import (
|
|
297
|
+
export_tokens_to_csv,
|
|
298
|
+
export_pools_to_csv,
|
|
299
|
+
insert_tokens_csv_to_db,
|
|
300
|
+
insert_pools_csv_to_db,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
export_tokens_to_csv("tokens.csv")
|
|
304
|
+
export_pools_to_csv("pools_v2.csv", "pools_v3.csv")
|
|
305
|
+
|
|
306
|
+
insert_tokens_csv_to_db("tokens.csv")
|
|
307
|
+
insert_pools_csv_to_db("pools_v2.csv", "pools_v3.csv")
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Utilities
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from cryptointerface.periphery.utils import to_wei, from_wei
|
|
316
|
+
|
|
317
|
+
to_wei(1.5, decimals=18) # 1500000000000000000
|
|
318
|
+
from_wei(1_000_000, decimals=6) # 1.0
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Enums
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from cryptointerface.periphery.enums import Network, Platform
|
|
325
|
+
|
|
326
|
+
Network.ARBITRUM.value # "42161"
|
|
327
|
+
Platform.UNISWAP_V3.value # "uniswap_v3"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Networks:** `ETHEREUM`, `OPTIMISM`, `BSC`, `POLYGON`, `BASE`, `ARBITRUM`, `CELO`, `AVALANCHE`, `BLAST`, `ZORA`, `SEPOLIA`
|
|
331
|
+
|
|
332
|
+
**Platforms:** `UNISWAP_V2`, `UNISWAP_V3`, `SUSHISWAP_V2`, `SUSHISWAP_V3`, `PANCAKESWAP_V2`, `PANCAKESWAP_V3`, `QUICKSWAP_V2`, `QUICKSWAP_V3`, `CAMELOT_V2`, `CAMELOT_V3`, `TRADERJOE_V1`, `AERODROME`, `VELODROME`
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Running Tests
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
uv run pytest
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Network tests (require a live RPC) are skipped by default unless `SEPOLIA_RPC_URL` is set in your environment.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Dependencies
|
|
347
|
+
|
|
348
|
+
| Package | Purpose |
|
|
349
|
+
| --------------- | ---------------------------- |
|
|
350
|
+
| `web3` | EVM RPC interaction |
|
|
351
|
+
| `duckdb` | Local persistent storage |
|
|
352
|
+
| `polars` | DataFrame operations |
|
|
353
|
+
| `requests` | CoinGecko API calls |
|
|
354
|
+
| `python-dotenv` | Environment variable loading |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
cryptointerface/__init__.py,sha256=rCrUO5e1Jt-TnAKz49Tdvi0vTxGoTBFO8i7hG4gn6DM,208
|
|
2
|
+
cryptointerface/dex.py,sha256=XZjUrG_rSD42gR6HPh2L8zauXFBg9YoxMxYyhs7c2Lg,30980
|
|
3
|
+
cryptointerface/flashloan.py,sha256=hKwbI45GgVX2e6zrDiNOEQI70MtnWjq0Tnk6jIkXgwg,11673
|
|
4
|
+
cryptointerface/routes.py,sha256=cuwv1z5MuOQ1L_-ck6gBoLT_bnRZNpduP4rLqUMhJSo,3643
|
|
5
|
+
cryptointerface/token.py,sha256=SszyrMdM_USYHrkP6lmi96_3hwmLfmzZikoDMV9xms4,8808
|
|
6
|
+
cryptointerface/wallet.py,sha256=HdSOt-6-LabM8R7cpj-cDm9kl3l6uZ5QJcozLI0RpqA,1160
|
|
7
|
+
cryptointerface/periphery/config.py,sha256=Pyu54RZSiafOefQ-h-5r0ubkWq3J_plTeE3iyYgFOik,1248
|
|
8
|
+
cryptointerface/periphery/db.py,sha256=NlqywCHZK-JqFCb4OiwxK9JLQCY5ZCoCBpjbq1JuY94,4307
|
|
9
|
+
cryptointerface/periphery/dex_architecture.json,sha256=nShSIgX2ZOhRQi1sV0GzBLW3SNsZ4UihamrQafQIX1g,3838
|
|
10
|
+
cryptointerface/periphery/dex_contracts.json,sha256=nN1dXSY53d0YBfREggbJRPxGkWOK-CJDMOCvRziHneY,13288
|
|
11
|
+
cryptointerface/periphery/dex_contracts.py,sha256=QROcJYi9Jfr9zqTaF3L06bszCPUAacQDIJKvS5aIlNU,1541
|
|
12
|
+
cryptointerface/periphery/enums.py,sha256=1uWJb9xYPDTUnTap4UukHWK4XNwVQszWfebIH2edCt0,783
|
|
13
|
+
cryptointerface/periphery/mapping.py,sha256=RnX7kH76IcsVJhLS54pIyeeID69QZQRNnmCKHgqssN4,2780
|
|
14
|
+
cryptointerface/periphery/utils.py,sha256=ag9tMjJ7ugTL7Cs4PgyYxr5DoiiH31cLEt4NNcEzPak,203
|
|
15
|
+
cryptointerface/providers/endpoints.json,sha256=MJi4KJu73_0As3Nza-vF0XCUr5D_SsQ0zy22T6Vig9Q,1186
|
|
16
|
+
cryptointerface/providers/infura.py,sha256=3o4Ru4FLJXcHvWbrx5fgOVnWJjXElSl5cQvK4p6fcMs,1011
|
|
17
|
+
cryptointerface-0.1.0.dist-info/METADATA,sha256=s6XOY2KzDcFpWnp6PoZyNbtVEylN7PgIK5PU9L0Sln0,10506
|
|
18
|
+
cryptointerface-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
19
|
+
cryptointerface-0.1.0.dist-info/RECORD,,
|