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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upside-python-sdk
3
- Version: 0.1.0
4
- Summary: Python SDK for the Upside decentralized perpetuals exchange (REST + WebSocket).
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) decentralized
32
- perpetuals exchange — REST reads (`POST /info`), signed writes (`POST /exchange`),
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 **QA testnet** (`https://dev.upsidemax.xyz`).
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.QA_API_URL)
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.QA_API_URL)
68
+ exchange = Exchange("0x<private-key>", base_url=constants.UAT_API_URL)
68
69
 
69
- # Register (QA requires an invite code from the Upside team). A 10,000 USDC
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, is_hedge=True)
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", sl_price="80000")
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
- ### Order placement is asynchronous
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
- A batch returns `{"status": "accepted", "response": {"type": "order", "data": {"count": n}}}`
125
- — **not** the resting order id. Read the resulting state from
126
- `Info.user_orders` / `orders_by_cloids`, or the `orderUpdates` / `userFills`
127
- WebSocket channels. Cancels, modifies, and margin actions respond synchronously.
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.QA_API_URL, account_id=master.account_id)
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.QA_API_URL) # WS starts automatically
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` (public) and
163
- `orderUpdates`, `openOrders`, `userFills` (per-address). The client pings every
164
- 30s and auto-reconnects, replaying subscriptions. WebSocket does **not** push
165
- position or balance changes poll `userAccount` for those.
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`). See
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) decentralized
4
- perpetuals exchange — REST reads (`POST /info`), signed writes (`POST /exchange`),
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 **QA testnet** (`https://dev.upsidemax.xyz`).
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.QA_API_URL)
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.QA_API_URL)
40
+ exchange = Exchange("0x<private-key>", base_url=constants.UAT_API_URL)
40
41
 
41
- # Register (QA requires an invite code from the Upside team). A 10,000 USDC
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, is_hedge=True)
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", sl_price="80000")
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
- ### Order placement is asynchronous
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
- A batch returns `{"status": "accepted", "response": {"type": "order", "data": {"count": n}}}`
97
- — **not** the resting order id. Read the resulting state from
98
- `Info.user_orders` / `orders_by_cloids`, or the `orderUpdates` / `userFills`
99
- WebSocket channels. Cancels, modifies, and margin actions respond synchronously.
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.QA_API_URL, account_id=master.account_id)
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.QA_API_URL) # WS starts automatically
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` (public) and
135
- `orderUpdates`, `openOrders`, `userFills` (per-address). The client pings every
136
- 30s and auto-reconnects, replaying subscriptions. WebSocket does **not** push
137
- position or balance changes poll `userAccount` for those.
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`). See
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.0"
4
- description = "Python SDK for the Upside decentralized perpetuals exchange (REST + WebSocket)."
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.QA_API_URL)
8
+ info = Info(base_url=constants.UAT_API_URL)
9
9
  print(info.configs())
10
10
 
11
- exchange = Exchange(private_key, base_url=constants.QA_API_URL)
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.0"
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.QA_API_URL).rstrip("/")
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()