tristero 0.1.7__py3-none-any.whl → 0.3.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.
tristero/__init__.py CHANGED
@@ -1,12 +1,79 @@
1
- from tristero.client import OrderFailedException, StuckException, SwapException, TokenSpec, execute_swap, start_swap
2
- from tristero.config import set_config
1
+ from tristero.client import (
2
+ OrderFailedException,
3
+ StuckException,
4
+ SwapException,
5
+ MarginException,
6
+ FeatherException,
7
+ TokenSpec,
8
+ OrderType,
9
+ QuoteResult,
10
+ OrderResult,
11
+ MarginPosition,
12
+ FeatherSwapResult,
13
+ make_async_w3,
14
+ execute_permit2_swap,
15
+ execute_swap,
16
+ start_permit2_swap,
17
+ start_feather_swap,
18
+ wait_for_feather_completion,
19
+ wait_for_completion,
20
+ wait_for_completion_with_retry,
21
+ request_margin_quote,
22
+ sign_order,
23
+ submit_order,
24
+ open_margin_position,
25
+ close_margin_position,
26
+ list_margin_positions,
27
+ get_position,
28
+ )
29
+ from tristero.config import set_config, Config
30
+ from tristero.data import ChainID
31
+ from tristero.permit2 import get_erc20_contract
32
+ from tristero.api import APIException, QuoteException, get_quote, fill_order
3
33
 
4
34
  __all__ = [
35
+ # Enums and Types
36
+ "ChainID",
5
37
  "TokenSpec",
38
+ "OrderType",
39
+ "QuoteResult",
40
+ "OrderResult",
41
+ "MarginPosition",
42
+ "FeatherSwapResult",
43
+ "Config",
44
+ # Exceptions
6
45
  "StuckException",
7
46
  "OrderFailedException",
8
47
  "SwapException",
48
+ "MarginException",
49
+ "FeatherException",
50
+ # Swap Functions
51
+ "start_permit2_swap",
52
+ "start_feather_swap",
53
+ "execute_permit2_swap",
9
54
  "execute_swap",
10
- "start_swap",
55
+ "make_async_w3",
56
+ # Wait Functions
57
+ "wait_for_feather_completion",
58
+ "wait_for_completion",
59
+ "wait_for_completion_with_retry",
60
+ # Margin Functions (Quote Flow)
61
+ "request_margin_quote",
62
+ "sign_order",
63
+ "submit_order",
64
+ # Margin Functions (Direct Execution)
65
+ "open_margin_position",
66
+ "close_margin_position",
67
+ # Position Management
68
+ "list_margin_positions",
69
+ "get_position",
70
+ # Config
11
71
  "set_config",
72
+ # ERC20 Helpers
73
+ "get_erc20_contract",
74
+ # Low-level HTTP API
75
+ "APIException",
76
+ "QuoteException",
77
+ "get_quote",
78
+ "fill_order",
12
79
  ]
tristero/api.py CHANGED
@@ -1,80 +1,26 @@
1
- from enum import Enum
2
- from typing import Any, Optional
1
+ from typing import Any, Dict, List, Optional
2
+ import os
3
+ import ssl
3
4
  import httpx
4
5
  from websockets.asyncio.client import connect
6
+ from eth_utils import to_checksum_address
5
7
  from tristero.config import get_config
8
+ from tristero.data import get_gas_addr
9
+
10
+ try:
11
+ import certifi # type: ignore
12
+ except Exception: # pragma: no cover
13
+ certifi = None
6
14
 
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
- }
71
15
 
72
16
  class APIException(Exception):
73
17
  pass
74
18
 
19
+
75
20
  class QuoteException(Exception):
76
21
  pass
77
22
 
23
+
78
24
  def handle_resp(resp: httpx.Response):
79
25
  try:
80
26
  resp.raise_for_status()
@@ -90,6 +36,7 @@ def handle_resp(resp: httpx.Response):
90
36
  raise e
91
37
  raise APIException(data)
92
38
 
39
+
93
40
  async def t_get(client: httpx.AsyncClient, url: str):
94
41
  resp = await client.get(url, headers=get_config().headers, timeout=20)
95
42
  return handle_resp(resp)
@@ -100,59 +47,240 @@ async def t_post(client: httpx.AsyncClient, url: str, body: Optional[dict[str, A
100
47
  return handle_resp(resp)
101
48
 
102
49
 
103
- def native_address(chain_id: ChainID) -> str:
104
- return "0x0000000000000000000000000000000000000000"
50
+ def or_native(chain_id: str, address: str):
51
+ return get_gas_addr(chain_id) if address == "native" else address
52
+
53
+ def _env_truthy(name: str) -> bool:
54
+ v = os.getenv(name, "").strip().lower()
55
+ return v in {"1", "true", "yes", "y", "on"}
56
+
105
57
 
58
+ def _httpx_verify_setting():
59
+ if _env_truthy("TRISTERO_INSECURE_SSL"):
60
+ return False
61
+ if certifi is not None:
62
+ return certifi.where()
63
+ return True
106
64
 
107
- def or_native(chain_id: ChainID, address: str):
108
- return native_address(chain_id) if address == "native" else address
65
+
66
+ def _ws_ssl_context_for_url(url: str):
67
+ if not url.lower().startswith("wss://"):
68
+ return None
69
+ if _env_truthy("TRISTERO_INSECURE_SSL"):
70
+ return ssl._create_unverified_context()
71
+ if certifi is not None:
72
+ return ssl.create_default_context(cafile=certifi.where())
73
+ return ssl.create_default_context()
74
+
75
+
76
+ # Replace the default client with one that uses certifi (fixes CERTIFICATE_VERIFY_FAILED on some macOS setups)
77
+ c = httpx.AsyncClient(timeout=20, verify=_httpx_verify_setting())
109
78
 
110
79
 
111
80
  async def get_quote(
112
81
  from_wallet: str,
113
82
  to_wallet: str,
114
- from_chain_id: ChainID,
83
+ from_chain_id: str,
115
84
  from_address: str,
116
- to_chain_id: ChainID,
85
+ to_chain_id: str,
117
86
  to_address: str,
118
87
  amount: int,
119
88
  ):
120
- from_chain_id = from_chain_id
121
- to_chain_id = to_chain_id
122
-
123
89
  from_token_address = or_native(from_chain_id, from_address)
124
90
  to_token_address = or_native(to_chain_id, to_address)
125
91
 
126
- async with httpx.AsyncClient() as c:
127
- data = {
128
- "src_chain_id": from_chain_id.value,
129
- "src_token_address": from_token_address,
130
- "src_token_quantity": str(int(amount)),
131
- "src_wallet_address": from_wallet,
132
- "dst_chain_id": to_chain_id.value,
133
- "dst_token_address": to_token_address,
134
- "dst_wallet_address": to_wallet,
135
- }
136
- resp = await c.post(
137
- get_config().quoter_url,
138
- json=data,
139
- )
140
- # print(data, resp)
141
- try:
142
- return handle_resp(resp)
143
- except Exception as e:
144
- raise QuoteException(e, data) from e
92
+ data = {
93
+ "src_chain_id": from_chain_id,
94
+ "src_token_address": from_token_address,
95
+ "src_token_quantity": str(int(amount)),
96
+ "src_wallet_address": from_wallet,
97
+ "dst_chain_id": to_chain_id,
98
+ "dst_token_address": to_token_address,
99
+ "dst_wallet_address": to_wallet,
100
+ }
101
+ resp = await c.post(
102
+ get_config().quoter_url,
103
+ json=data,
104
+ headers=get_config().headers,
105
+ )
106
+ try:
107
+ return handle_resp(resp)
108
+ except Exception as e:
109
+ raise QuoteException(e, data) from e
145
110
 
146
111
 
147
112
  async def fill_order(signature: str, domain: dict[str, Any], message: dict[str, Any]):
148
- async with httpx.AsyncClient() as c:
149
- data = {"signature": signature, "domain": domain, "message": message}
150
- resp = await c.post(
151
- get_config().filler_url,
152
- json=data,
153
- )
154
- return handle_resp(resp)
113
+ data = {"signature": signature, "domain": domain, "message": message}
114
+ resp = await c.post(
115
+ get_config().filler_url,
116
+ json=data,
117
+ headers=get_config().headers,
118
+ )
119
+ return handle_resp(resp)
120
+
121
+
122
+ async def fill_order_feather(src_chain: str, dst_chain: str, dst_address: str, amount: int, client_id: str = ''):
123
+ data = {
124
+ "client_id": client_id,
125
+ "src_chain": src_chain,
126
+ "dst_chain": dst_chain,
127
+ "dst_address": dst_address,
128
+ "amount": amount
129
+ }
130
+ resp = await c.post(
131
+ get_config().filler_url,
132
+ json=data,
133
+ headers=get_config().headers,
134
+ )
135
+ return handle_resp(resp)
136
+
155
137
 
156
138
  async def poll_updates(order_id: str):
157
- ws = await connect(f"{get_config().ws_url}/{order_id}")
139
+ base = get_config().ws_url.rstrip("/")
140
+ ws = await connect(
141
+ f"{base}/{order_id}",
142
+ ssl=_ws_ssl_context_for_url(base),
143
+ )
158
144
  return ws
145
+
146
+
147
+ async def poll_updates_feather(order_id: str):
148
+ base = get_config().ws_url.rstrip("/")
149
+ ws = await connect(
150
+ f"{base}/feather/{order_id}",
151
+ ssl=_ws_ssl_context_for_url(base),
152
+ )
153
+ return ws
154
+
155
+
156
+ async def get_margin_quote(
157
+ chain_id: str,
158
+ wallet_address: str,
159
+ quote_currency: str,
160
+ base_currency: str,
161
+ leverage_ratio: int,
162
+ collateral_amount: str,
163
+ ) -> Dict[str, Any]:
164
+ """
165
+ Get a quote for opening a margin position.
166
+
167
+ Args:
168
+ chain_id: Chain ID (e.g., "42161" for Arbitrum)
169
+ wallet_address: User's wallet address
170
+ quote_currency: Quote currency token address (e.g., USDC)
171
+ base_currency: Base currency token address (e.g., WETH)
172
+ leverage_ratio: Leverage ratio (e.g., 2 for 2x)
173
+ collateral_amount: Collateral amount in raw units
174
+
175
+ Returns:
176
+ Quote response with order_data for signing
177
+ """
178
+ payload = {
179
+ "chain_id": chain_id,
180
+ "wallet_address": wallet_address,
181
+ "quote_currency": quote_currency,
182
+ "base_currency": base_currency,
183
+ "leverage_ratio": leverage_ratio,
184
+ "collateral_amount": collateral_amount,
185
+ }
186
+ resp = await c.post(
187
+ f"{get_config().margin_quoter_url.rstrip('/')}/margin",
188
+ json=payload,
189
+ headers=get_config().headers,
190
+ )
191
+ data = handle_resp(resp)
192
+ if not data.get("success"):
193
+ raise QuoteException(data.get("message") or "margin quote failed", payload)
194
+ return data
195
+
196
+
197
+ async def submit_margin_order(signed_order_payload: Dict[str, Any]) -> Dict[str, Any]:
198
+ """
199
+ Submit a signed margin order.
200
+
201
+ Args:
202
+ signed_order_payload: Signed order from sign_margin_order
203
+
204
+ Returns:
205
+ Submission response with order_id
206
+ """
207
+ resp = await c.post(
208
+ f"{get_config().margin_filler_url.rstrip('/')}/",
209
+ json=signed_order_payload,
210
+ headers=get_config().headers,
211
+ )
212
+ return handle_resp(resp)
213
+
214
+
215
+ async def submit_close_position(signed_close_payload: Dict[str, Any]) -> Dict[str, Any]:
216
+ """
217
+ Submit a signed close position request.
218
+
219
+ Args:
220
+ signed_close_payload: Signed close request from sign_close_position
221
+
222
+ Returns:
223
+ Close response with order_id
224
+ """
225
+ resp = await c.post(
226
+ f"{get_config().margin_filler_url.rstrip('/')}/close-margin-position",
227
+ json=signed_close_payload,
228
+ headers=get_config().headers,
229
+ )
230
+ return handle_resp(resp)
231
+
232
+
233
+ async def get_margin_positions(wallet_address: str) -> List[Dict[str, Any]]:
234
+ """
235
+ Get all margin positions for a wallet.
236
+
237
+ Args:
238
+ wallet_address: User's wallet address
239
+
240
+ Returns:
241
+ List of margin positions
242
+ """
243
+ wallet = to_checksum_address(wallet_address)
244
+ resp = await c.get(
245
+ f"{get_config().wallet_server_url}/{wallet}/margin-positions",
246
+ headers=get_config().headers,
247
+ )
248
+ return handle_resp(resp)
249
+
250
+
251
+ async def get_margin_position(position_id: str) -> Dict[str, Any]:
252
+ """
253
+ Get a specific margin position by ID.
254
+
255
+ Args:
256
+ position_id: Position ID
257
+
258
+ Returns:
259
+ Margin position details
260
+ """
261
+ resp = await c.get(
262
+ f"{get_config().wallet_server_url}/margin-positions/{position_id}",
263
+ headers=get_config().headers,
264
+ )
265
+ return handle_resp(resp)
266
+
267
+
268
+ async def poll_margin_updates(order_id: str):
269
+ """
270
+ Open a WebSocket connection to poll margin order updates.
271
+
272
+ Args:
273
+ order_id: Margin order ID
274
+
275
+ Returns:
276
+ WebSocket connection
277
+ """
278
+ base = get_config().ws_url.rstrip("/")
279
+ ssl_ctx = _ws_ssl_context_for_url(base)
280
+
281
+ # Some deployments expose margin order updates on the same channel as normal orders.
282
+ # Try the non-/margin path first (fixes local 403), then fall back to /margin.
283
+ try:
284
+ return await connect(f"{base}/{order_id}", ssl=ssl_ctx)
285
+ except Exception:
286
+ return await connect(f"{base}/margin/{order_id}", ssl=ssl_ctx)