upside-python-sdk 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/PKG-INFO +74 -24
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/README.md +72 -22
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/pyproject.toml +2 -2
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/__init__.py +3 -3
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/api.py +9 -2
- upside_python_sdk-0.1.1/src/upside/exchange.py +733 -0
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/info.py +93 -0
- upside_python_sdk-0.1.1/src/upside/utils/constants.py +81 -0
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/utils/error.py +13 -3
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/utils/signing.py +37 -13
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/utils/types.py +44 -5
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/websocket_manager.py +54 -13
- upside_python_sdk-0.1.0/src/upside/exchange.py +0 -426
- upside_python_sdk-0.1.0/src/upside/utils/constants.py +0 -49
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/LICENSE +0 -0
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/py.typed +0 -0
- {upside_python_sdk-0.1.0 → upside_python_sdk-0.1.1}/src/upside/utils/__init__.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upside-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Python SDK for the Upside
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for the Upside perpetuals exchange (REST + WebSocket).
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Keywords: upside,perpetuals,dex,trading,eip712,websocket
|
|
@@ -28,16 +28,17 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
# Upside Python SDK
|
|
30
30
|
|
|
31
|
-
A Python client for the [Upside](https://docs.upsidemax.xyz)
|
|
32
|
-
|
|
33
|
-
and realtime WebSocket streams.
|
|
31
|
+
A Python client for the [Upside](https://docs.upsidemax.xyz) perpetuals
|
|
32
|
+
exchange — REST reads (`POST /info`), signed writes (`POST /exchange`),
|
|
33
|
+
and realtime WebSocket streams. Authentication is EIP-712 wallet-signed
|
|
34
|
+
(secp256k1) rather than API keys.
|
|
34
35
|
|
|
35
36
|
- **EIP-712 request signing** (secp256k1) with the Agent and Typed paths — no API keys.
|
|
36
37
|
- **Synchronous REST** over `requests`, **threaded WebSocket** over `websocket-client`.
|
|
37
38
|
- Raw-dict responses, `TypedDict` inputs, full type hints (ships `py.typed`).
|
|
38
39
|
- Agent (API-wallet) delegation, TP/SL, leverage/margin, and collateral actions.
|
|
39
40
|
|
|
40
|
-
> The default environment is the **
|
|
41
|
+
> The default environment is the **UAT testnet** (`https://dev.upsidemax.xyz`).
|
|
41
42
|
> Contract IDs, scales, and tick/step sizes are server-assigned — always read
|
|
42
43
|
> them from `configs`, never hardcode.
|
|
43
44
|
|
|
@@ -57,16 +58,16 @@ from upside import Info, Exchange
|
|
|
57
58
|
from upside.utils import constants
|
|
58
59
|
|
|
59
60
|
# --- reads (no signing) ---
|
|
60
|
-
info = Info(base_url=constants.
|
|
61
|
+
info = Info(base_url=constants.UAT_API_URL)
|
|
61
62
|
cfg = info.configs()
|
|
62
63
|
contract = next(c for c in cfg["contracts"] if c["status"] == "Active")
|
|
63
64
|
asset = contract["contractId"]
|
|
64
65
|
print(info.market_state(asset))
|
|
65
66
|
|
|
66
67
|
# --- writes (EIP-712 signed) ---
|
|
67
|
-
exchange = Exchange("0x<private-key>", base_url=constants.
|
|
68
|
+
exchange = Exchange("0x<private-key>", base_url=constants.UAT_API_URL)
|
|
68
69
|
|
|
69
|
-
# Register (
|
|
70
|
+
# Register (UAT requires an invite code from the Upside team). A 10,000 USDC
|
|
70
71
|
# test airdrop lands within ~10s.
|
|
71
72
|
exchange.register_account(invite_code="<invite-code>")
|
|
72
73
|
|
|
@@ -89,9 +90,16 @@ info.user_account(account_id, market_deployer_id)
|
|
|
89
90
|
info.user_orders(account_id, market_deployer_id, contract_id=0)
|
|
90
91
|
info.orders_by_ids(market_deployer_id, ["8280"])
|
|
91
92
|
info.orders_by_cloids(account_id, market_deployer_id, ["1778844423064"])
|
|
93
|
+
info.ticker(asset) # 24h rolling stats (omit asset = all markets)
|
|
92
94
|
info.user_agents(account_id)
|
|
93
95
|
info.user_market_deployers(account_id)
|
|
96
|
+
info.account_by_address("0x<address>") # master or agent address -> accountId
|
|
94
97
|
info.share_group_state()
|
|
98
|
+
|
|
99
|
+
# history (ascending, paginate on the last row's time; limit <= 1000)
|
|
100
|
+
info.user_fills(account_id, contract_id=0, start_time=None, end_time=None, limit=None)
|
|
101
|
+
info.order_history(account_id) # terminal orders only; active ones are in user_orders
|
|
102
|
+
info.user_funding_flows(account_id)
|
|
95
103
|
```
|
|
96
104
|
|
|
97
105
|
## Trading — `Exchange`
|
|
@@ -100,31 +108,59 @@ info.share_group_state()
|
|
|
100
108
|
from upside import Cloid
|
|
101
109
|
|
|
102
110
|
exchange.order(asset=1, is_buy=True, size="10", price="50", cloid=Cloid.from_int(1001))
|
|
103
|
-
exchange.market_order(asset=1, is_buy=False, size="5")
|
|
111
|
+
exchange.market_order(asset=1, is_buy=False, size="5", price="61000") # price = execution price to cross to
|
|
104
112
|
exchange.bulk_orders([...]) # up to 10 orders, one signature
|
|
105
113
|
exchange.cancel(asset=1, oid=15)
|
|
106
114
|
exchange.cancel_by_cloid(asset=1, cloid=1001)
|
|
107
115
|
exchange.cancel_all(asset=1)
|
|
108
116
|
exchange.modify(asset=1, oid=15, price="151", size="8")
|
|
109
117
|
|
|
118
|
+
# conditional order: fires when the mark price crosses trigger_px
|
|
119
|
+
exchange.trigger_order(asset=1, is_buy=False, size="10", price="79000", trigger_px="80000", tpsl="sl")
|
|
120
|
+
|
|
121
|
+
# entry-inline TP/SL: promoted to position TP/SL once this order fills completely
|
|
122
|
+
exchange.order(asset=1, is_buy=True, size="10", price="100",
|
|
123
|
+
tp_price="120", tp_limit_price="119", tp_order_type=1, # 1 = limit, 2 = market
|
|
124
|
+
sl_price="90", sl_limit_price="89", sl_order_type=2)
|
|
125
|
+
|
|
110
126
|
exchange.update_leverage(asset=1, leverage=20)
|
|
111
|
-
exchange.update_margin_mode(asset=1, is_cross=False
|
|
127
|
+
exchange.update_margin_mode(asset=1, is_cross=False) # HEDGE is disabled server-side; ONE_WAY only
|
|
112
128
|
exchange.update_isolated_margin(asset=1, ntli=5000)
|
|
129
|
+
exchange.update_slippage_setting(market_deployer_id=1, market_slippage_bps=500)
|
|
113
130
|
|
|
114
|
-
exchange.tp_sl(asset=1, tp_price="90000",
|
|
131
|
+
exchange.tp_sl(asset=1, tp_price="90000", tp_limit_price="90000", tp_order_type=1)
|
|
115
132
|
exchange.cancel_tp_sl(asset=1)
|
|
116
133
|
exchange.cancel_conditional(oid=123)
|
|
117
134
|
|
|
118
135
|
exchange.lock_collateral(market_deployer_id=1, coin_id=1, amount="1000")
|
|
136
|
+
exchange.unlock_collateral(market_deployer_id=1, coin_id=1, amount="1000")
|
|
119
137
|
exchange.transfer_between_deployers(1, 2, coin_id=1, amount="1000")
|
|
138
|
+
|
|
139
|
+
# portfolio (shared) margin
|
|
140
|
+
exchange.set_margin_share_type(1) # 0 = UNIFIED, 1 = PORTFOLIO
|
|
141
|
+
exchange.transfer_md_to_share_group(1, group_id=3, coin_id=1, amount="1000")
|
|
142
|
+
exchange.transfer_share_group_to_md(3, market_deployer_id=1, coin_id=1, amount="1000")
|
|
143
|
+
exchange.lock_into_share_group(group_id=3, coin_id=1, amount="1000")
|
|
144
|
+
exchange.unlock_from_share_group(group_id=3, coin_id=1, amount="1000")
|
|
120
145
|
```
|
|
121
146
|
|
|
122
|
-
|
|
147
|
+
Market orders and market TP/SL legs carry an **execution price** you compute
|
|
148
|
+
yourself (`mark price ± marketSlippageBps/1e4`) — the server derives none. Read
|
|
149
|
+
your account's cap from `Info.user_account`'s `marketSlippageBps` and set it
|
|
150
|
+
with `update_slippage_setting`.
|
|
123
151
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
152
|
+
### Orders and cancels answer 200 *or* 202
|
|
153
|
+
|
|
154
|
+
The order/cancel family (`order`, `cancel`, `cancelByCloid`, `cancelAll`,
|
|
155
|
+
`modify`) returns **either** HTTP 200 with a `statuses[]` entry per submitted
|
|
156
|
+
item (`resting` / `filled` / `error`), **or** HTTP 202 with
|
|
157
|
+
`{"status": "accepted", "response": {"type": "accepted", "data": {"count": n}}}`
|
|
158
|
+
— where `type` is the literal `"accepted"`, not the action name. Handle both.
|
|
159
|
+
On 202 the per-item outcome arrives on the `orderUpdates` channel; correlate by
|
|
160
|
+
`cloid`, or by `n` (your nonce) + `si` (index within the batch). A trigger order
|
|
161
|
+
is the exception: it answers with the TP/SL receipt
|
|
162
|
+
`{"type": "tpSl", "data": {"tpOrderId": n, "slOrderId": n}}`. Every other action
|
|
163
|
+
responds synchronously.
|
|
128
164
|
|
|
129
165
|
### HTTP 200 ≠ success
|
|
130
166
|
|
|
@@ -140,7 +176,7 @@ server routes agent-signed actions to the master account.
|
|
|
140
176
|
|
|
141
177
|
```python
|
|
142
178
|
response, agent_key = master.approve_agent(agent_name="bot1") # generates a fresh key
|
|
143
|
-
agent = Exchange(agent_key, base_url=constants.
|
|
179
|
+
agent = Exchange(agent_key, base_url=constants.UAT_API_URL, account_id=master.account_id)
|
|
144
180
|
agent.order(asset=1, is_buy=True, size="10", price="50")
|
|
145
181
|
master.revoke_agent(agent.address)
|
|
146
182
|
```
|
|
@@ -148,21 +184,30 @@ master.revoke_agent(agent.address)
|
|
|
148
184
|
## WebSocket streams
|
|
149
185
|
|
|
150
186
|
```python
|
|
151
|
-
info = Info(base_url=constants.
|
|
187
|
+
info = Info(base_url=constants.UAT_API_URL) # WS starts automatically
|
|
152
188
|
|
|
153
189
|
sid = info.subscribe({"type": "l2Book", "asset": "1"}, lambda m: print(m["data"]["bookVersion"]))
|
|
154
190
|
info.subscribe({"type": "trades", "asset": "1"}, print)
|
|
155
191
|
info.subscribe({"type": "orderUpdates", "user": "0x<address>"}, print) # private: pass the wallet address
|
|
156
192
|
info.subscribe({"type": "userFills", "user": "0x<address>"}, print)
|
|
193
|
+
info.subscribe({"type": "userAccount", "user": "0x<address>", "marketDeployerId": 1}, print) # every 3s
|
|
157
194
|
|
|
158
195
|
info.unsubscribe({"type": "l2Book", "asset": "1"}, sid)
|
|
159
196
|
info.close()
|
|
160
197
|
```
|
|
161
198
|
|
|
162
|
-
Channels: `l2Book`, `bbo`, `trades`, `candle`, `config`
|
|
163
|
-
`orderUpdates`, `openOrders`, `userFills`
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
Channels: `l2Book`, `bbo`, `trades`, `candle`, `ticker`, `allMarkets`, `config`
|
|
200
|
+
(public) and `orderUpdates`, `openOrders`, `userFills`, `userAccount`
|
|
201
|
+
(per-address; `userAccount` also takes `marketDeployerId`, since the account view
|
|
202
|
+
differs per deployer). The client pings every 30s and auto-reconnects, replaying
|
|
203
|
+
subscriptions.
|
|
204
|
+
|
|
205
|
+
Several channels open with a snapshot frame whose shape differs from the
|
|
206
|
+
increments that follow — `candle` (a batch of bars under `asset`/`interval`),
|
|
207
|
+
`openOrders` (`userOrders`'s response body), and `orderUpdates` / `userFills`
|
|
208
|
+
(the last 10 history rows under `data.rows`, in REST's long field names rather
|
|
209
|
+
than the compact wire ones). Dispatch handles the routing; your callback still
|
|
210
|
+
has to read both shapes.
|
|
166
211
|
|
|
167
212
|
## Signing
|
|
168
213
|
|
|
@@ -175,7 +220,12 @@ handles both paths automatically:
|
|
|
175
220
|
- **Agent path** — every other action (canonical-JSON `actionHash`).
|
|
176
221
|
|
|
177
222
|
Nonces are strictly increasing millisecond timestamps managed per `Exchange`
|
|
178
|
-
instance (`NonceManager`).
|
|
223
|
+
instance (`NonceManager`). Browser-extension wallets sign typed structs with
|
|
224
|
+
their active chain instead of 9767; pass `Exchange(..., signature_chain_id=...)`
|
|
225
|
+
to match that and the SDK sends the unsigned top-level `signatureChainId` the
|
|
226
|
+
server needs to rebuild the domain.
|
|
227
|
+
|
|
228
|
+
See
|
|
179
229
|
[docs.upsidemax.xyz/guide/authentication](https://docs.upsidemax.xyz/guide/authentication).
|
|
180
230
|
|
|
181
231
|
## Examples
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# Upside Python SDK
|
|
2
2
|
|
|
3
|
-
A Python client for the [Upside](https://docs.upsidemax.xyz)
|
|
4
|
-
|
|
5
|
-
and realtime WebSocket streams.
|
|
3
|
+
A Python client for the [Upside](https://docs.upsidemax.xyz) perpetuals
|
|
4
|
+
exchange — REST reads (`POST /info`), signed writes (`POST /exchange`),
|
|
5
|
+
and realtime WebSocket streams. Authentication is EIP-712 wallet-signed
|
|
6
|
+
(secp256k1) rather than API keys.
|
|
6
7
|
|
|
7
8
|
- **EIP-712 request signing** (secp256k1) with the Agent and Typed paths — no API keys.
|
|
8
9
|
- **Synchronous REST** over `requests`, **threaded WebSocket** over `websocket-client`.
|
|
9
10
|
- Raw-dict responses, `TypedDict` inputs, full type hints (ships `py.typed`).
|
|
10
11
|
- Agent (API-wallet) delegation, TP/SL, leverage/margin, and collateral actions.
|
|
11
12
|
|
|
12
|
-
> The default environment is the **
|
|
13
|
+
> The default environment is the **UAT testnet** (`https://dev.upsidemax.xyz`).
|
|
13
14
|
> Contract IDs, scales, and tick/step sizes are server-assigned — always read
|
|
14
15
|
> them from `configs`, never hardcode.
|
|
15
16
|
|
|
@@ -29,16 +30,16 @@ from upside import Info, Exchange
|
|
|
29
30
|
from upside.utils import constants
|
|
30
31
|
|
|
31
32
|
# --- reads (no signing) ---
|
|
32
|
-
info = Info(base_url=constants.
|
|
33
|
+
info = Info(base_url=constants.UAT_API_URL)
|
|
33
34
|
cfg = info.configs()
|
|
34
35
|
contract = next(c for c in cfg["contracts"] if c["status"] == "Active")
|
|
35
36
|
asset = contract["contractId"]
|
|
36
37
|
print(info.market_state(asset))
|
|
37
38
|
|
|
38
39
|
# --- writes (EIP-712 signed) ---
|
|
39
|
-
exchange = Exchange("0x<private-key>", base_url=constants.
|
|
40
|
+
exchange = Exchange("0x<private-key>", base_url=constants.UAT_API_URL)
|
|
40
41
|
|
|
41
|
-
# Register (
|
|
42
|
+
# Register (UAT requires an invite code from the Upside team). A 10,000 USDC
|
|
42
43
|
# test airdrop lands within ~10s.
|
|
43
44
|
exchange.register_account(invite_code="<invite-code>")
|
|
44
45
|
|
|
@@ -61,9 +62,16 @@ info.user_account(account_id, market_deployer_id)
|
|
|
61
62
|
info.user_orders(account_id, market_deployer_id, contract_id=0)
|
|
62
63
|
info.orders_by_ids(market_deployer_id, ["8280"])
|
|
63
64
|
info.orders_by_cloids(account_id, market_deployer_id, ["1778844423064"])
|
|
65
|
+
info.ticker(asset) # 24h rolling stats (omit asset = all markets)
|
|
64
66
|
info.user_agents(account_id)
|
|
65
67
|
info.user_market_deployers(account_id)
|
|
68
|
+
info.account_by_address("0x<address>") # master or agent address -> accountId
|
|
66
69
|
info.share_group_state()
|
|
70
|
+
|
|
71
|
+
# history (ascending, paginate on the last row's time; limit <= 1000)
|
|
72
|
+
info.user_fills(account_id, contract_id=0, start_time=None, end_time=None, limit=None)
|
|
73
|
+
info.order_history(account_id) # terminal orders only; active ones are in user_orders
|
|
74
|
+
info.user_funding_flows(account_id)
|
|
67
75
|
```
|
|
68
76
|
|
|
69
77
|
## Trading — `Exchange`
|
|
@@ -72,31 +80,59 @@ info.share_group_state()
|
|
|
72
80
|
from upside import Cloid
|
|
73
81
|
|
|
74
82
|
exchange.order(asset=1, is_buy=True, size="10", price="50", cloid=Cloid.from_int(1001))
|
|
75
|
-
exchange.market_order(asset=1, is_buy=False, size="5")
|
|
83
|
+
exchange.market_order(asset=1, is_buy=False, size="5", price="61000") # price = execution price to cross to
|
|
76
84
|
exchange.bulk_orders([...]) # up to 10 orders, one signature
|
|
77
85
|
exchange.cancel(asset=1, oid=15)
|
|
78
86
|
exchange.cancel_by_cloid(asset=1, cloid=1001)
|
|
79
87
|
exchange.cancel_all(asset=1)
|
|
80
88
|
exchange.modify(asset=1, oid=15, price="151", size="8")
|
|
81
89
|
|
|
90
|
+
# conditional order: fires when the mark price crosses trigger_px
|
|
91
|
+
exchange.trigger_order(asset=1, is_buy=False, size="10", price="79000", trigger_px="80000", tpsl="sl")
|
|
92
|
+
|
|
93
|
+
# entry-inline TP/SL: promoted to position TP/SL once this order fills completely
|
|
94
|
+
exchange.order(asset=1, is_buy=True, size="10", price="100",
|
|
95
|
+
tp_price="120", tp_limit_price="119", tp_order_type=1, # 1 = limit, 2 = market
|
|
96
|
+
sl_price="90", sl_limit_price="89", sl_order_type=2)
|
|
97
|
+
|
|
82
98
|
exchange.update_leverage(asset=1, leverage=20)
|
|
83
|
-
exchange.update_margin_mode(asset=1, is_cross=False
|
|
99
|
+
exchange.update_margin_mode(asset=1, is_cross=False) # HEDGE is disabled server-side; ONE_WAY only
|
|
84
100
|
exchange.update_isolated_margin(asset=1, ntli=5000)
|
|
101
|
+
exchange.update_slippage_setting(market_deployer_id=1, market_slippage_bps=500)
|
|
85
102
|
|
|
86
|
-
exchange.tp_sl(asset=1, tp_price="90000",
|
|
103
|
+
exchange.tp_sl(asset=1, tp_price="90000", tp_limit_price="90000", tp_order_type=1)
|
|
87
104
|
exchange.cancel_tp_sl(asset=1)
|
|
88
105
|
exchange.cancel_conditional(oid=123)
|
|
89
106
|
|
|
90
107
|
exchange.lock_collateral(market_deployer_id=1, coin_id=1, amount="1000")
|
|
108
|
+
exchange.unlock_collateral(market_deployer_id=1, coin_id=1, amount="1000")
|
|
91
109
|
exchange.transfer_between_deployers(1, 2, coin_id=1, amount="1000")
|
|
110
|
+
|
|
111
|
+
# portfolio (shared) margin
|
|
112
|
+
exchange.set_margin_share_type(1) # 0 = UNIFIED, 1 = PORTFOLIO
|
|
113
|
+
exchange.transfer_md_to_share_group(1, group_id=3, coin_id=1, amount="1000")
|
|
114
|
+
exchange.transfer_share_group_to_md(3, market_deployer_id=1, coin_id=1, amount="1000")
|
|
115
|
+
exchange.lock_into_share_group(group_id=3, coin_id=1, amount="1000")
|
|
116
|
+
exchange.unlock_from_share_group(group_id=3, coin_id=1, amount="1000")
|
|
92
117
|
```
|
|
93
118
|
|
|
94
|
-
|
|
119
|
+
Market orders and market TP/SL legs carry an **execution price** you compute
|
|
120
|
+
yourself (`mark price ± marketSlippageBps/1e4`) — the server derives none. Read
|
|
121
|
+
your account's cap from `Info.user_account`'s `marketSlippageBps` and set it
|
|
122
|
+
with `update_slippage_setting`.
|
|
95
123
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
### Orders and cancels answer 200 *or* 202
|
|
125
|
+
|
|
126
|
+
The order/cancel family (`order`, `cancel`, `cancelByCloid`, `cancelAll`,
|
|
127
|
+
`modify`) returns **either** HTTP 200 with a `statuses[]` entry per submitted
|
|
128
|
+
item (`resting` / `filled` / `error`), **or** HTTP 202 with
|
|
129
|
+
`{"status": "accepted", "response": {"type": "accepted", "data": {"count": n}}}`
|
|
130
|
+
— where `type` is the literal `"accepted"`, not the action name. Handle both.
|
|
131
|
+
On 202 the per-item outcome arrives on the `orderUpdates` channel; correlate by
|
|
132
|
+
`cloid`, or by `n` (your nonce) + `si` (index within the batch). A trigger order
|
|
133
|
+
is the exception: it answers with the TP/SL receipt
|
|
134
|
+
`{"type": "tpSl", "data": {"tpOrderId": n, "slOrderId": n}}`. Every other action
|
|
135
|
+
responds synchronously.
|
|
100
136
|
|
|
101
137
|
### HTTP 200 ≠ success
|
|
102
138
|
|
|
@@ -112,7 +148,7 @@ server routes agent-signed actions to the master account.
|
|
|
112
148
|
|
|
113
149
|
```python
|
|
114
150
|
response, agent_key = master.approve_agent(agent_name="bot1") # generates a fresh key
|
|
115
|
-
agent = Exchange(agent_key, base_url=constants.
|
|
151
|
+
agent = Exchange(agent_key, base_url=constants.UAT_API_URL, account_id=master.account_id)
|
|
116
152
|
agent.order(asset=1, is_buy=True, size="10", price="50")
|
|
117
153
|
master.revoke_agent(agent.address)
|
|
118
154
|
```
|
|
@@ -120,21 +156,30 @@ master.revoke_agent(agent.address)
|
|
|
120
156
|
## WebSocket streams
|
|
121
157
|
|
|
122
158
|
```python
|
|
123
|
-
info = Info(base_url=constants.
|
|
159
|
+
info = Info(base_url=constants.UAT_API_URL) # WS starts automatically
|
|
124
160
|
|
|
125
161
|
sid = info.subscribe({"type": "l2Book", "asset": "1"}, lambda m: print(m["data"]["bookVersion"]))
|
|
126
162
|
info.subscribe({"type": "trades", "asset": "1"}, print)
|
|
127
163
|
info.subscribe({"type": "orderUpdates", "user": "0x<address>"}, print) # private: pass the wallet address
|
|
128
164
|
info.subscribe({"type": "userFills", "user": "0x<address>"}, print)
|
|
165
|
+
info.subscribe({"type": "userAccount", "user": "0x<address>", "marketDeployerId": 1}, print) # every 3s
|
|
129
166
|
|
|
130
167
|
info.unsubscribe({"type": "l2Book", "asset": "1"}, sid)
|
|
131
168
|
info.close()
|
|
132
169
|
```
|
|
133
170
|
|
|
134
|
-
Channels: `l2Book`, `bbo`, `trades`, `candle`, `config`
|
|
135
|
-
`orderUpdates`, `openOrders`, `userFills`
|
|
136
|
-
|
|
137
|
-
|
|
171
|
+
Channels: `l2Book`, `bbo`, `trades`, `candle`, `ticker`, `allMarkets`, `config`
|
|
172
|
+
(public) and `orderUpdates`, `openOrders`, `userFills`, `userAccount`
|
|
173
|
+
(per-address; `userAccount` also takes `marketDeployerId`, since the account view
|
|
174
|
+
differs per deployer). The client pings every 30s and auto-reconnects, replaying
|
|
175
|
+
subscriptions.
|
|
176
|
+
|
|
177
|
+
Several channels open with a snapshot frame whose shape differs from the
|
|
178
|
+
increments that follow — `candle` (a batch of bars under `asset`/`interval`),
|
|
179
|
+
`openOrders` (`userOrders`'s response body), and `orderUpdates` / `userFills`
|
|
180
|
+
(the last 10 history rows under `data.rows`, in REST's long field names rather
|
|
181
|
+
than the compact wire ones). Dispatch handles the routing; your callback still
|
|
182
|
+
has to read both shapes.
|
|
138
183
|
|
|
139
184
|
## Signing
|
|
140
185
|
|
|
@@ -147,7 +192,12 @@ handles both paths automatically:
|
|
|
147
192
|
- **Agent path** — every other action (canonical-JSON `actionHash`).
|
|
148
193
|
|
|
149
194
|
Nonces are strictly increasing millisecond timestamps managed per `Exchange`
|
|
150
|
-
instance (`NonceManager`).
|
|
195
|
+
instance (`NonceManager`). Browser-extension wallets sign typed structs with
|
|
196
|
+
their active chain instead of 9767; pass `Exchange(..., signature_chain_id=...)`
|
|
197
|
+
to match that and the SDK sends the unsigned top-level `signatureChainId` the
|
|
198
|
+
server needs to rebuild the domain.
|
|
199
|
+
|
|
200
|
+
See
|
|
151
201
|
[docs.upsidemax.xyz/guide/authentication](https://docs.upsidemax.xyz/guide/authentication).
|
|
152
202
|
|
|
153
203
|
## Examples
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "upside-python-sdk"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "Python SDK for the Upside
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Python SDK for the Upside perpetuals exchange (REST + WebSocket)."
|
|
5
5
|
authors = ["Upside <dev@upsidemax.xyz>"]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
@@ -5,10 +5,10 @@ Quick start::
|
|
|
5
5
|
from upside import Info, Exchange
|
|
6
6
|
from upside.utils import constants
|
|
7
7
|
|
|
8
|
-
info = Info(base_url=constants.
|
|
8
|
+
info = Info(base_url=constants.UAT_API_URL)
|
|
9
9
|
print(info.configs())
|
|
10
10
|
|
|
11
|
-
exchange = Exchange(private_key, base_url=constants.
|
|
11
|
+
exchange = Exchange(private_key, base_url=constants.UAT_API_URL)
|
|
12
12
|
exchange.order(asset=1, is_buy=True, size="10", price="50")
|
|
13
13
|
|
|
14
14
|
See https://docs.upsidemax.xyz for the full API reference.
|
|
@@ -23,7 +23,7 @@ from .utils.signing import NonceManager
|
|
|
23
23
|
from .utils.types import Cloid
|
|
24
24
|
from .websocket_manager import WebsocketManager
|
|
25
25
|
|
|
26
|
-
__version__ = "0.1.
|
|
26
|
+
__version__ = "0.1.1"
|
|
27
27
|
|
|
28
28
|
__all__ = [
|
|
29
29
|
"API",
|
|
@@ -23,7 +23,7 @@ class API:
|
|
|
23
23
|
"""Thin POST-only client with status-based exception mapping."""
|
|
24
24
|
|
|
25
25
|
def __init__(self, base_url: Optional[str] = None, timeout: Optional[float] = None) -> None:
|
|
26
|
-
self.base_url = (base_url or constants.
|
|
26
|
+
self.base_url = (base_url or constants.UAT_API_URL).rstrip("/")
|
|
27
27
|
self.timeout = timeout
|
|
28
28
|
self.session = requests.Session()
|
|
29
29
|
self.session.headers.update({"Content-Type": "application/json"})
|
|
@@ -47,15 +47,22 @@ class API:
|
|
|
47
47
|
return cast(Json, body)
|
|
48
48
|
|
|
49
49
|
code = message = request_id = None
|
|
50
|
+
errors = None
|
|
50
51
|
if isinstance(body, dict):
|
|
51
52
|
code = body.get("code")
|
|
52
53
|
message = body.get("message")
|
|
53
54
|
request_id = body.get("requestId")
|
|
55
|
+
raw_errors = body.get("errors")
|
|
56
|
+
# Field-level detail, sent with INVALID_PARAM rejections. Keep only
|
|
57
|
+
# the object entries: a bare string here must not cost the caller
|
|
58
|
+
# the whole error envelope.
|
|
59
|
+
if isinstance(raw_errors, list):
|
|
60
|
+
errors = [e for e in raw_errors if isinstance(e, dict)]
|
|
54
61
|
else:
|
|
55
62
|
message = response.text[:500] or None
|
|
56
63
|
|
|
57
64
|
error_cls = ClientError if 400 <= response.status_code < 500 else ServerError
|
|
58
|
-
raise error_cls(response.status_code, code=code, message=message, request_id=request_id)
|
|
65
|
+
raise error_cls(response.status_code, code=code, message=message, request_id=request_id, errors=errors)
|
|
59
66
|
|
|
60
67
|
def close(self) -> None:
|
|
61
68
|
self.session.close()
|