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 +70 -3
- tristero/api.py +230 -102
- tristero/client.py +568 -47
- tristero/config.py +16 -0
- tristero/data.py +56 -0
- tristero/eip712/__init__.py +48 -0
- tristero/eip712/eip712_auto.py +212 -0
- tristero/eip712/eip712_struct.py +73 -0
- tristero/eip712/escrow_utils.py +32 -0
- tristero/eip712/nested_types.py +85 -0
- tristero/eip712/simple_types.py +392 -0
- tristero/files/chains.json +6557 -0
- tristero/permit2.py +250 -48
- tristero-0.3.0.dist-info/METADATA +198 -0
- tristero-0.3.0.dist-info/RECORD +22 -0
- tristero-0.3.0.dist-info/WHEEL +5 -0
- tristero-0.3.0.dist-info/licenses/LICENSE +201 -0
- tristero-0.3.0.dist-info/top_level.txt +1 -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/WHEEL +0 -4
tristero/__init__.py
CHANGED
|
@@ -1,12 +1,79 @@
|
|
|
1
|
-
from tristero.client import
|
|
2
|
-
|
|
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
|
-
"
|
|
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
|
|
2
|
-
|
|
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
|
|
104
|
-
return "
|
|
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
|
-
|
|
108
|
-
|
|
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:
|
|
83
|
+
from_chain_id: str,
|
|
115
84
|
from_address: str,
|
|
116
|
-
to_chain_id:
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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)
|