tristero 0.2.1__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,18 +1,79 @@
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
- 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
3
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
4
33
 
5
34
  __all__ = [
35
+ # Enums and Types
6
36
  "ChainID",
7
37
  "TokenSpec",
38
+ "OrderType",
39
+ "QuoteResult",
40
+ "OrderResult",
41
+ "MarginPosition",
42
+ "FeatherSwapResult",
43
+ "Config",
44
+ # Exceptions
8
45
  "StuckException",
9
46
  "OrderFailedException",
10
47
  "SwapException",
48
+ "MarginException",
49
+ "FeatherException",
50
+ # Swap Functions
11
51
  "start_permit2_swap",
12
52
  "start_feather_swap",
13
53
  "execute_permit2_swap",
54
+ "execute_swap",
55
+ "make_async_w3",
56
+ # Wait Functions
14
57
  "wait_for_feather_completion",
15
58
  "wait_for_completion",
16
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
17
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",
18
79
  ]
tristero/api.py CHANGED
@@ -1,15 +1,26 @@
1
- from typing import Any, Optional
1
+ from typing import Any, Dict, List, Optional
2
+ import os
3
+ import ssl
2
4
  import httpx
3
5
  from websockets.asyncio.client import connect
6
+ from eth_utils import to_checksum_address
4
7
  from tristero.config import get_config
5
8
  from tristero.data import get_gas_addr
6
9
 
10
+ try:
11
+ import certifi # type: ignore
12
+ except Exception: # pragma: no cover
13
+ certifi = None
14
+
15
+
7
16
  class APIException(Exception):
8
17
  pass
9
18
 
19
+
10
20
  class QuoteException(Exception):
11
21
  pass
12
22
 
23
+
13
24
  def handle_resp(resp: httpx.Response):
14
25
  try:
15
26
  resp.raise_for_status()
@@ -25,6 +36,7 @@ def handle_resp(resp: httpx.Response):
25
36
  raise e
26
37
  raise APIException(data)
27
38
 
39
+
28
40
  async def t_get(client: httpx.AsyncClient, url: str):
29
41
  resp = await client.get(url, headers=get_config().headers, timeout=20)
30
42
  return handle_resp(resp)
@@ -34,10 +46,36 @@ async def t_post(client: httpx.AsyncClient, url: str, body: Optional[dict[str, A
34
46
  resp = await client.post(url, headers=get_config().headers, json=body, timeout=20)
35
47
  return handle_resp(resp)
36
48
 
49
+
37
50
  def or_native(chain_id: str, address: str):
38
51
  return get_gas_addr(chain_id) if address == "native" else address
39
52
 
40
- c = httpx.AsyncClient()
53
+ def _env_truthy(name: str) -> bool:
54
+ v = os.getenv(name, "").strip().lower()
55
+ return v in {"1", "true", "yes", "y", "on"}
56
+
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
64
+
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())
78
+
41
79
 
42
80
  async def get_quote(
43
81
  from_wallet: str,
@@ -48,9 +86,6 @@ async def get_quote(
48
86
  to_address: str,
49
87
  amount: int,
50
88
  ):
51
- from_chain_id = from_chain_id
52
- to_chain_id = to_chain_id
53
-
54
89
  from_token_address = or_native(from_chain_id, from_address)
55
90
  to_token_address = or_native(to_chain_id, to_address)
56
91
 
@@ -66,21 +101,24 @@ async def get_quote(
66
101
  resp = await c.post(
67
102
  get_config().quoter_url,
68
103
  json=data,
104
+ headers=get_config().headers,
69
105
  )
70
- # print(data, resp)
71
106
  try:
72
107
  return handle_resp(resp)
73
108
  except Exception as e:
74
109
  raise QuoteException(e, data) from e
75
110
 
111
+
76
112
  async def fill_order(signature: str, domain: dict[str, Any], message: dict[str, Any]):
77
113
  data = {"signature": signature, "domain": domain, "message": message}
78
114
  resp = await c.post(
79
115
  get_config().filler_url,
80
116
  json=data,
117
+ headers=get_config().headers,
81
118
  )
82
119
  return handle_resp(resp)
83
120
 
121
+
84
122
  async def fill_order_feather(src_chain: str, dst_chain: str, dst_address: str, amount: int, client_id: str = ''):
85
123
  data = {
86
124
  "client_id": client_id,
@@ -92,13 +130,157 @@ async def fill_order_feather(src_chain: str, dst_chain: str, dst_address: str, a
92
130
  resp = await c.post(
93
131
  get_config().filler_url,
94
132
  json=data,
133
+ headers=get_config().headers,
95
134
  )
96
135
  return handle_resp(resp)
97
136
 
137
+
98
138
  async def poll_updates(order_id: str):
99
- 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
+ )
100
144
  return ws
101
145
 
146
+
102
147
  async def poll_updates_feather(order_id: str):
103
- ws = await connect(f"{get_config().ws_url}/feather/{order_id}")
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
+ )
104
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)