tristero 0.1.7__py3-none-any.whl → 0.2.1__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.
- tristero/__init__.py +9 -3
- tristero/api.py +48 -102
- tristero/client.py +88 -43
- tristero/data.py +35 -0
- tristero/files/chains.json +6557 -0
- tristero/permit2.py +30 -27
- tristero-0.2.1.dist-info/METADATA +284 -0
- tristero-0.2.1.dist-info/RECORD +14 -0
- tristero-0.1.7.dist-info/METADATA +0 -157
- tristero-0.1.7.dist-info/RECORD +0 -12
- {tristero-0.1.7.dist-info → tristero-0.2.1.dist-info}/WHEEL +0 -0
tristero/__init__.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
from tristero.client import OrderFailedException, StuckException, SwapException, TokenSpec,
|
|
1
|
+
from tristero.client import OrderFailedException, StuckException, SwapException, TokenSpec, execute_permit2_swap, start_permit2_swap, start_feather_swap, wait_for_feather_completion, wait_for_completion, wait_for_completion_with_retry
|
|
2
2
|
from tristero.config import set_config
|
|
3
|
+
from tristero.data import ChainID
|
|
3
4
|
|
|
4
5
|
__all__ = [
|
|
6
|
+
"ChainID",
|
|
5
7
|
"TokenSpec",
|
|
6
8
|
"StuckException",
|
|
7
9
|
"OrderFailedException",
|
|
8
10
|
"SwapException",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
+
"start_permit2_swap",
|
|
12
|
+
"start_feather_swap",
|
|
13
|
+
"execute_permit2_swap",
|
|
14
|
+
"wait_for_feather_completion",
|
|
15
|
+
"wait_for_completion",
|
|
16
|
+
"wait_for_completion_with_retry",
|
|
11
17
|
"set_config",
|
|
12
18
|
]
|
tristero/api.py
CHANGED
|
@@ -1,73 +1,8 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
1
|
from typing import Any, Optional
|
|
3
2
|
import httpx
|
|
4
3
|
from websockets.asyncio.client import connect
|
|
5
4
|
from tristero.config import get_config
|
|
6
|
-
|
|
7
|
-
class ChainID(str, Enum):
|
|
8
|
-
arbitrum = "42161"
|
|
9
|
-
avalanche = "43114"
|
|
10
|
-
base = "8453"
|
|
11
|
-
blast = "81457"
|
|
12
|
-
bsc = "56"
|
|
13
|
-
celo = "42220"
|
|
14
|
-
ethereum = "1"
|
|
15
|
-
mantle = "5000"
|
|
16
|
-
mode = "34443"
|
|
17
|
-
opbnb = "204"
|
|
18
|
-
optimism = "10"
|
|
19
|
-
polygon = "137"
|
|
20
|
-
scroll = "534352"
|
|
21
|
-
solana = "1151111081099710"
|
|
22
|
-
tron = "728126428"
|
|
23
|
-
unichain = "130"
|
|
24
|
-
sei = "1329"
|
|
25
|
-
sonic = "146"
|
|
26
|
-
linea = "59144"
|
|
27
|
-
worldchain = "480"
|
|
28
|
-
codex = "81224"
|
|
29
|
-
plume = "98866"
|
|
30
|
-
hyperevm = "999"
|
|
31
|
-
|
|
32
|
-
_WRAPPED_GAS_ADDRESSES = {
|
|
33
|
-
ChainID.ethereum: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
34
|
-
ChainID.unichain: "0x4200000000000000000000000000000000000006",
|
|
35
|
-
ChainID.optimism: "0x4200000000000000000000000000000000000006",
|
|
36
|
-
ChainID.arbitrum: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
37
|
-
ChainID.avalanche: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7",
|
|
38
|
-
ChainID.polygon: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
39
|
-
ChainID.base: "0x4200000000000000000000000000000000000006",
|
|
40
|
-
ChainID.bsc: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
|
41
|
-
ChainID.blast: "0x4300000000000000000000000000000000000004",
|
|
42
|
-
ChainID.mode: "0x4200000000000000000000000000000000000006",
|
|
43
|
-
ChainID.solana: "So11111111111111111111111111111111111111112",
|
|
44
|
-
ChainID.sonic: "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38",
|
|
45
|
-
ChainID.linea: "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f",
|
|
46
|
-
ChainID.sei: "0xE30feDd158A2e3b13e9badaeABaFc5516e95e8C7",
|
|
47
|
-
ChainID.worldchain: "0x4200000000000000000000000000000000000006",
|
|
48
|
-
ChainID.plume: "0xEa237441c92CAe6FC17Caaf9a7acB3f953be4bd1",
|
|
49
|
-
ChainID.hyperevm: "0x5555555555555555555555555555555555555555",
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
_PERMIT2_CONTRACT_ADDRESSES = {
|
|
53
|
-
ChainID.ethereum: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
54
|
-
ChainID.avalanche: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
55
|
-
ChainID.arbitrum: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
56
|
-
ChainID.base: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
57
|
-
ChainID.bsc: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
58
|
-
ChainID.optimism: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
59
|
-
ChainID.polygon: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
60
|
-
ChainID.unichain: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
61
|
-
ChainID.linea: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
62
|
-
ChainID.worldchain: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
63
|
-
ChainID.sonic: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
64
|
-
ChainID.sei: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
65
|
-
ChainID.blast: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
66
|
-
ChainID.mode: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
67
|
-
ChainID.scroll: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
68
|
-
ChainID.plume: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
69
|
-
ChainID.hyperevm: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
|
|
70
|
-
}
|
|
5
|
+
from tristero.data import get_gas_addr
|
|
71
6
|
|
|
72
7
|
class APIException(Exception):
|
|
73
8
|
pass
|
|
@@ -99,21 +34,17 @@ async def t_post(client: httpx.AsyncClient, url: str, body: Optional[dict[str, A
|
|
|
99
34
|
resp = await client.post(url, headers=get_config().headers, json=body, timeout=20)
|
|
100
35
|
return handle_resp(resp)
|
|
101
36
|
|
|
37
|
+
def or_native(chain_id: str, address: str):
|
|
38
|
+
return get_gas_addr(chain_id) if address == "native" else address
|
|
102
39
|
|
|
103
|
-
|
|
104
|
-
return "0x0000000000000000000000000000000000000000"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def or_native(chain_id: ChainID, address: str):
|
|
108
|
-
return native_address(chain_id) if address == "native" else address
|
|
109
|
-
|
|
40
|
+
c = httpx.AsyncClient()
|
|
110
41
|
|
|
111
42
|
async def get_quote(
|
|
112
43
|
from_wallet: str,
|
|
113
44
|
to_wallet: str,
|
|
114
|
-
from_chain_id:
|
|
45
|
+
from_chain_id: str,
|
|
115
46
|
from_address: str,
|
|
116
|
-
to_chain_id:
|
|
47
|
+
to_chain_id: str,
|
|
117
48
|
to_address: str,
|
|
118
49
|
amount: int,
|
|
119
50
|
):
|
|
@@ -123,36 +54,51 @@ async def get_quote(
|
|
|
123
54
|
from_token_address = or_native(from_chain_id, from_address)
|
|
124
55
|
to_token_address = or_native(to_chain_id, to_address)
|
|
125
56
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
raise QuoteException(e, data) from e
|
|
145
|
-
|
|
57
|
+
data = {
|
|
58
|
+
"src_chain_id": from_chain_id,
|
|
59
|
+
"src_token_address": from_token_address,
|
|
60
|
+
"src_token_quantity": str(int(amount)),
|
|
61
|
+
"src_wallet_address": from_wallet,
|
|
62
|
+
"dst_chain_id": to_chain_id,
|
|
63
|
+
"dst_token_address": to_token_address,
|
|
64
|
+
"dst_wallet_address": to_wallet,
|
|
65
|
+
}
|
|
66
|
+
resp = await c.post(
|
|
67
|
+
get_config().quoter_url,
|
|
68
|
+
json=data,
|
|
69
|
+
)
|
|
70
|
+
# print(data, resp)
|
|
71
|
+
try:
|
|
72
|
+
return handle_resp(resp)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
raise QuoteException(e, data) from e
|
|
146
75
|
|
|
147
76
|
async def fill_order(signature: str, domain: dict[str, Any], message: dict[str, Any]):
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
77
|
+
data = {"signature": signature, "domain": domain, "message": message}
|
|
78
|
+
resp = await c.post(
|
|
79
|
+
get_config().filler_url,
|
|
80
|
+
json=data,
|
|
81
|
+
)
|
|
82
|
+
return handle_resp(resp)
|
|
83
|
+
|
|
84
|
+
async def fill_order_feather(src_chain: str, dst_chain: str, dst_address: str, amount: int, client_id: str = ''):
|
|
85
|
+
data = {
|
|
86
|
+
"client_id": client_id,
|
|
87
|
+
"src_chain": src_chain,
|
|
88
|
+
"dst_chain": dst_chain,
|
|
89
|
+
"dst_address": dst_address,
|
|
90
|
+
"amount": amount
|
|
91
|
+
}
|
|
92
|
+
resp = await c.post(
|
|
93
|
+
get_config().filler_url,
|
|
94
|
+
json=data,
|
|
95
|
+
)
|
|
96
|
+
return handle_resp(resp)
|
|
155
97
|
|
|
156
98
|
async def poll_updates(order_id: str):
|
|
157
99
|
ws = await connect(f"{get_config().ws_url}/{order_id}")
|
|
158
100
|
return ws
|
|
101
|
+
|
|
102
|
+
async def poll_updates_feather(order_id: str):
|
|
103
|
+
ws = await connect(f"{get_config().ws_url}/feather/{order_id}")
|
|
104
|
+
return ws
|
tristero/client.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
import json
|
|
3
4
|
import logging
|
|
4
5
|
from typing import Any, Optional, TypeVar
|
|
@@ -6,10 +7,10 @@ from typing import Any, Optional, TypeVar
|
|
|
6
7
|
from eth_account.signers.local import LocalAccount
|
|
7
8
|
from pydantic import BaseModel
|
|
8
9
|
|
|
9
|
-
from tristero.api import
|
|
10
|
-
from .permit2 import
|
|
10
|
+
from tristero.api import fill_order, fill_order_feather, get_quote, poll_updates, poll_updates_feather
|
|
11
|
+
from .permit2 import Permit2Order, create_permit2_order
|
|
12
|
+
from .data import ChainID
|
|
11
13
|
from web3 import AsyncBaseProvider, AsyncWeb3
|
|
12
|
-
import logging
|
|
13
14
|
from web3 import AsyncWeb3
|
|
14
15
|
from tenacity import (
|
|
15
16
|
retry,
|
|
@@ -45,7 +46,36 @@ class TokenSpec(BaseModel, frozen=True):
|
|
|
45
46
|
chain_id: ChainID
|
|
46
47
|
token_address: str
|
|
47
48
|
|
|
48
|
-
async def
|
|
49
|
+
async def wait_for_feather_completion(order_id: str):
|
|
50
|
+
ws = await poll_updates_feather(order_id)
|
|
51
|
+
try:
|
|
52
|
+
async for msg in ws:
|
|
53
|
+
if not msg:
|
|
54
|
+
continue
|
|
55
|
+
msg = json.loads(msg)
|
|
56
|
+
status = msg['status']
|
|
57
|
+
logger.info(
|
|
58
|
+
{
|
|
59
|
+
"message": f"status={status}",
|
|
60
|
+
"id": "order_update",
|
|
61
|
+
"payload": msg,
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
if status in ['Expired']:
|
|
65
|
+
await ws.close()
|
|
66
|
+
raise Exception(f"Swap failed: {ws.close_reason} {msg}")
|
|
67
|
+
elif status in ['Finalized']:
|
|
68
|
+
await ws.close()
|
|
69
|
+
return msg
|
|
70
|
+
# If we exit the loop without completed/failed, raise to retry
|
|
71
|
+
raise WebSocketClosedError("WebSocket closed without completion status")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
# Close cleanly if still open
|
|
74
|
+
if not ws.close_code:
|
|
75
|
+
await ws.close()
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
async def wait_for_permit2_completion(order_id: str):
|
|
49
79
|
ws = await poll_updates(order_id)
|
|
50
80
|
try:
|
|
51
81
|
async for msg in ws:
|
|
@@ -72,49 +102,69 @@ async def wait_for_completion(order_id: str):
|
|
|
72
102
|
await ws.close()
|
|
73
103
|
raise
|
|
74
104
|
|
|
105
|
+
async def wait_for_completion(order_id: str, feather: bool):
|
|
106
|
+
if feather:
|
|
107
|
+
return await wait_for_feather_completion(order_id)
|
|
108
|
+
else:
|
|
109
|
+
return await wait_for_permit2_completion(order_id)
|
|
110
|
+
|
|
75
111
|
@retry(
|
|
76
112
|
stop=stop_after_attempt(3),
|
|
77
113
|
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
78
114
|
retry=retry_if_exception_type(WebSocketClosedError)
|
|
79
115
|
)
|
|
80
|
-
async def wait_for_completion_with_retry(order_id: str):
|
|
81
|
-
return await wait_for_completion(order_id)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
async def wait_for_completion_with_retry(order_id: str, feather: bool = False):
|
|
117
|
+
return await wait_for_completion(order_id, feather)
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class FeatherSwapResult:
|
|
121
|
+
deposit_address: str
|
|
122
|
+
data: Any
|
|
123
|
+
|
|
124
|
+
class FeatherException(Exception):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
async def start_feather_swap(
|
|
128
|
+
src_t: TokenSpec,
|
|
129
|
+
dst_t: TokenSpec,
|
|
130
|
+
dst_addr: str,
|
|
131
|
+
raw_amount: int,
|
|
132
|
+
client_id: str = ''
|
|
133
|
+
):
|
|
134
|
+
resp = await fill_order_feather(client_id, str(src_t.chain_id.value), str(dst_t.chain_id), dst_addr, raw_amount)
|
|
135
|
+
if resp['detail']:
|
|
136
|
+
raise FeatherException(resp)
|
|
137
|
+
else:
|
|
138
|
+
return FeatherSwapResult(
|
|
139
|
+
deposit_address = resp['deposit_address'],
|
|
140
|
+
data = resp
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
async def start_permit2_swap(
|
|
144
|
+
w3: AsyncWeb3[P],
|
|
145
|
+
account: LocalAccount,
|
|
146
|
+
src_t: TokenSpec,
|
|
147
|
+
dst_t: TokenSpec,
|
|
148
|
+
raw_amount: int,
|
|
149
|
+
):
|
|
150
|
+
order = await create_permit2_order(
|
|
101
151
|
w3,
|
|
102
152
|
account,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
153
|
+
str(src_t.chain_id.value),
|
|
154
|
+
src_t.token_address,
|
|
155
|
+
str(dst_t.chain_id.value),
|
|
156
|
+
dst_t.token_address,
|
|
107
157
|
raw_amount,
|
|
108
158
|
)
|
|
109
159
|
response = await fill_order(
|
|
110
|
-
str(sig.signature.to_0x_hex()),
|
|
111
|
-
|
|
112
|
-
|
|
160
|
+
str(order.sig.signature.to_0x_hex()),
|
|
161
|
+
order.msg.domain.model_dump(by_alias=True, mode="json"),
|
|
162
|
+
order.msg.message.model_dump(by_alias=True, mode="json"),
|
|
113
163
|
)
|
|
164
|
+
order_id = response['id']
|
|
165
|
+
return order_id
|
|
114
166
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
async def execute_swap(
|
|
167
|
+
async def execute_permit2_swap(
|
|
118
168
|
w3: AsyncWeb3[P],
|
|
119
169
|
account: LocalAccount,
|
|
120
170
|
src_t: TokenSpec,
|
|
@@ -124,20 +174,14 @@ async def execute_swap(
|
|
|
124
174
|
timeout: Optional[float] = None
|
|
125
175
|
) -> dict[str, Any]:
|
|
126
176
|
"""Execute and wait for swap completion."""
|
|
127
|
-
order_id = await
|
|
128
|
-
w3,
|
|
129
|
-
account,
|
|
130
|
-
src_t,
|
|
131
|
-
dst_t,
|
|
132
|
-
raw_amount
|
|
133
|
-
)
|
|
177
|
+
order_id = await start_permit2_swap(w3, account, src_t, dst_t, raw_amount)
|
|
134
178
|
logger.info(f"Swap order placed: {order_id}")
|
|
135
179
|
|
|
136
180
|
waiter = wait_for_completion_with_retry if retry else wait_for_completion
|
|
137
181
|
|
|
138
182
|
try:
|
|
139
183
|
if timeout is None:
|
|
140
|
-
return await waiter(order_id)
|
|
184
|
+
return await waiter(order_id, False)
|
|
141
185
|
|
|
142
186
|
return await asyncio.wait_for(
|
|
143
187
|
waiter(order_id),
|
|
@@ -145,3 +189,4 @@ async def execute_swap(
|
|
|
145
189
|
)
|
|
146
190
|
except asyncio.TimeoutError as exc:
|
|
147
191
|
raise StuckException(f"Swap {order_id} timed out after {timeout}s") from exc
|
|
192
|
+
|
tristero/data.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from importlib import resources as impresources
|
|
2
|
+
import json
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
import enum
|
|
5
|
+
|
|
6
|
+
CHAINS_FILE = impresources.files("tristero.files") / "chains.json"
|
|
7
|
+
CHAINS = json.loads(CHAINS_FILE.read_text())
|
|
8
|
+
|
|
9
|
+
def get_address(chain: str, id: str) -> str | None:
|
|
10
|
+
return CHAINS[chain].get('addresses').get(id)
|
|
11
|
+
|
|
12
|
+
def get_permit2_addr(chain: str):
|
|
13
|
+
return get_address(chain, 'permit2')
|
|
14
|
+
|
|
15
|
+
def get_wrapped_gas_addr(chain: str):
|
|
16
|
+
return get_address(chain, 'wrappedGasToken')
|
|
17
|
+
|
|
18
|
+
def get_gas_addr(chain: str):
|
|
19
|
+
return get_address(chain, 'gasToken')
|
|
20
|
+
|
|
21
|
+
def chain(id: str) -> str | None:
|
|
22
|
+
return CHAINS[str(id)].get('chainId')
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from enum import IntEnum
|
|
26
|
+
class ChainID(IntEnum):
|
|
27
|
+
_value_: int
|
|
28
|
+
else:
|
|
29
|
+
ChainID = enum.Enum(
|
|
30
|
+
'ChainID',
|
|
31
|
+
{
|
|
32
|
+
name: data['chainId']
|
|
33
|
+
for name, data in CHAINS.items()
|
|
34
|
+
}
|
|
35
|
+
)
|