hotstuff-python-sdk 0.0.1b2__tar.gz → 0.0.1b4__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.
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/PKG-INFO +564 -158
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/README.md +563 -157
- hotstuff_python_sdk-0.0.1b4/hotstuff/__init__.py +123 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/apis/exchange.py +15 -14
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/apis/info.py +7 -7
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/apis/subscription.py +9 -22
- hotstuff_python_sdk-0.0.1b4/hotstuff/exceptions.py +72 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/exchange/account.py +1 -23
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/exchange/collateral.py +2 -24
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/exchange/trading.py +47 -16
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/exchange/vault.py +2 -24
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/info/account.py +54 -32
- hotstuff_python_sdk-0.0.1b4/hotstuff/methods/info/global.py +65 -0
- hotstuff_python_sdk-0.0.1b2/hotstuff/methods/info/global.py → hotstuff_python_sdk-0.0.1b4/hotstuff/methods/info/market.py +42 -35
- hotstuff_python_sdk-0.0.1b2/hotstuff/methods/subscription/global.py → hotstuff_python_sdk-0.0.1b4/hotstuff/methods/subscription/channels.py +39 -39
- hotstuff_python_sdk-0.0.1b4/hotstuff/methods/subscription/global.py +55 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/transports/http.py +36 -6
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/utils/__init__.py +4 -1
- hotstuff_python_sdk-0.0.1b4/hotstuff/utils/signing.py +113 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/pyproject.toml +8 -4
- hotstuff_python_sdk-0.0.1b2/hotstuff/__init__.py +0 -53
- hotstuff_python_sdk-0.0.1b2/hotstuff/utils/signing.py +0 -76
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/LICENSE +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/apis/__init__.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/__init__.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/exchange/__init__.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/exchange/op_codes.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/info/__init__.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/info/explorer.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/info/vault.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/methods/subscription/__init__.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/transports/__init__.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/transports/websocket.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/types/__init__.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/types/clients.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/types/exchange.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/types/transports.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/utils/address.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/utils/endpoints.py +0 -0
- {hotstuff_python_sdk-0.0.1b2 → hotstuff_python_sdk-0.0.1b4}/hotstuff/utils/nonce.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hotstuff-python-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.1b4
|
|
4
4
|
Summary: Python SDK for interacting with Hotstuff L1
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: hotstuff,trading,blockchain,defi,exchange
|
|
@@ -48,6 +48,7 @@ Description-Content-Type: text/markdown
|
|
|
48
48
|
- [HttpTransport](#httptransport)
|
|
49
49
|
- [WebSocketTransport](#websockettransport)
|
|
50
50
|
- [Advanced Usage](#advanced-usage)
|
|
51
|
+
- [Signing](#signing)
|
|
51
52
|
- [Error Handling](#error-handling)
|
|
52
53
|
- [Examples](#examples)
|
|
53
54
|
|
|
@@ -152,8 +153,16 @@ async def setup():
|
|
|
152
153
|
#### Market Data Methods
|
|
153
154
|
|
|
154
155
|
```python
|
|
156
|
+
import importlib
|
|
157
|
+
|
|
158
|
+
global_methods = importlib.import_module("hotstuff.methods.info.global")
|
|
159
|
+
InstrumentsParams = global_methods.InstrumentsParams
|
|
160
|
+
TickerParams = global_methods.TickerParams
|
|
161
|
+
OrderbookParams = global_methods.OrderbookParams
|
|
162
|
+
TradesParams = global_methods.TradesParams
|
|
163
|
+
|
|
155
164
|
# Get all instruments (perps, spot, options)
|
|
156
|
-
instruments = await info.instruments(
|
|
165
|
+
instruments = await info.instruments(InstrumentsParams(type="all"))
|
|
157
166
|
|
|
158
167
|
# Get supported collateral
|
|
159
168
|
collateral = await info.supported_collateral({})
|
|
@@ -162,13 +171,13 @@ collateral = await info.supported_collateral({})
|
|
|
162
171
|
oracle = await info.oracle({})
|
|
163
172
|
|
|
164
173
|
# Get ticker for a specific symbol
|
|
165
|
-
ticker = await info.ticker(
|
|
174
|
+
ticker = await info.ticker(TickerParams(symbol="BTC-PERP"))
|
|
166
175
|
|
|
167
176
|
# Get orderbook with depth
|
|
168
|
-
orderbook = await info.orderbook(
|
|
177
|
+
orderbook = await info.orderbook(OrderbookParams(symbol="BTC-PERP", depth=20))
|
|
169
178
|
|
|
170
179
|
# Get recent trades
|
|
171
|
-
trades = await info.trades(
|
|
180
|
+
trades = await info.trades(TradesParams(symbol="BTC-PERP", limit=50))
|
|
172
181
|
|
|
173
182
|
# Get mid prices for all instruments
|
|
174
183
|
mids = await info.mids({})
|
|
@@ -187,65 +196,84 @@ chart = await info.chart({
|
|
|
187
196
|
#### Account Methods
|
|
188
197
|
|
|
189
198
|
```python
|
|
199
|
+
from hotstuff.methods.info.account import (
|
|
200
|
+
AccountSummaryParams,
|
|
201
|
+
AccountInfoParams,
|
|
202
|
+
UserBalanceParams,
|
|
203
|
+
OpenOrdersParams,
|
|
204
|
+
PositionsParams,
|
|
205
|
+
OrderHistoryParams,
|
|
206
|
+
TradeHistoryParams,
|
|
207
|
+
FundingHistoryParams,
|
|
208
|
+
TransferHistoryParams,
|
|
209
|
+
AccountHistoryParams,
|
|
210
|
+
UserFeeInfoParams,
|
|
211
|
+
InstrumentLeverageParams,
|
|
212
|
+
ReferralInfoParams,
|
|
213
|
+
ReferralSummaryParams,
|
|
214
|
+
SubAccountsListParams,
|
|
215
|
+
AgentsParams,
|
|
216
|
+
)
|
|
217
|
+
|
|
190
218
|
user_address = "0x1234..."
|
|
191
219
|
|
|
192
220
|
# Get account summary
|
|
193
|
-
summary = await info.account_summary(
|
|
221
|
+
summary = await info.account_summary(AccountSummaryParams(user=user_address))
|
|
194
222
|
|
|
195
223
|
# Get account info
|
|
196
|
-
account_info = await info.account_info(
|
|
224
|
+
account_info = await info.account_info(AccountInfoParams(user=user_address))
|
|
197
225
|
|
|
198
226
|
# Get user balance
|
|
199
|
-
balance = await info.user_balance(
|
|
227
|
+
balance = await info.user_balance(UserBalanceParams(user=user_address))
|
|
200
228
|
|
|
201
229
|
# Get open orders
|
|
202
|
-
open_orders = await info.open_orders(
|
|
230
|
+
open_orders = await info.open_orders(OpenOrdersParams(user=user_address))
|
|
203
231
|
|
|
204
232
|
# Get current positions
|
|
205
|
-
positions = await info.positions(
|
|
233
|
+
positions = await info.positions(PositionsParams(user=user_address))
|
|
206
234
|
|
|
207
235
|
# Get order history
|
|
208
|
-
order_history = await info.order_history(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
236
|
+
order_history = await info.order_history(OrderHistoryParams(
|
|
237
|
+
user=user_address,
|
|
238
|
+
limit=100,
|
|
239
|
+
))
|
|
212
240
|
|
|
213
241
|
# Get trade history (fills)
|
|
214
|
-
trade_history = await info.trade_history(
|
|
242
|
+
trade_history = await info.trade_history(TradeHistoryParams(user=user_address))
|
|
215
243
|
|
|
216
244
|
# Get funding history
|
|
217
|
-
funding_history = await info.funding_history(
|
|
245
|
+
funding_history = await info.funding_history(FundingHistoryParams(user=user_address))
|
|
218
246
|
|
|
219
247
|
# Get transfer history
|
|
220
|
-
transfer_history = await info.transfer_history(
|
|
248
|
+
transfer_history = await info.transfer_history(TransferHistoryParams(user=user_address))
|
|
221
249
|
|
|
222
250
|
# Get account history with time range
|
|
223
|
-
account_history = await info.account_history(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
251
|
+
account_history = await info.account_history(AccountHistoryParams(
|
|
252
|
+
user=user_address,
|
|
253
|
+
from_time=int(time.time()) - 86400, # 24h ago
|
|
254
|
+
to_time=int(time.time()),
|
|
255
|
+
))
|
|
228
256
|
|
|
229
257
|
# Get user fee information
|
|
230
|
-
fee_info = await info.user_fee_info(
|
|
258
|
+
fee_info = await info.user_fee_info(UserFeeInfoParams(user=user_address))
|
|
231
259
|
|
|
232
260
|
# Get instrument leverage settings
|
|
233
|
-
leverage = await info.instrument_leverage(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
leverage = await info.instrument_leverage(InstrumentLeverageParams(
|
|
262
|
+
user=user_address,
|
|
263
|
+
instrumentId=1,
|
|
264
|
+
))
|
|
237
265
|
|
|
238
266
|
# Get referral info
|
|
239
|
-
referral_info = await info.get_referral_info(
|
|
267
|
+
referral_info = await info.get_referral_info(ReferralInfoParams(user=user_address))
|
|
240
268
|
|
|
241
269
|
# Get referral summary
|
|
242
|
-
referral_summary = await info.referral_summary(
|
|
270
|
+
referral_summary = await info.referral_summary(ReferralSummaryParams(user=user_address))
|
|
243
271
|
|
|
244
272
|
# Get sub-accounts list
|
|
245
|
-
sub_accounts = await info.sub_accounts_list(
|
|
273
|
+
sub_accounts = await info.sub_accounts_list(SubAccountsListParams(user=user_address))
|
|
246
274
|
|
|
247
275
|
# Get agents
|
|
248
|
-
agents = await info.agents(
|
|
276
|
+
agents = await info.agents(AgentsParams(user=user_address))
|
|
249
277
|
```
|
|
250
278
|
|
|
251
279
|
#### Vault Methods
|
|
@@ -306,155 +334,222 @@ async def setup():
|
|
|
306
334
|
|
|
307
335
|
```python
|
|
308
336
|
import time
|
|
337
|
+
from hotstuff.methods.exchange.trading import (
|
|
338
|
+
PlaceOrderParams,
|
|
339
|
+
UnitOrder,
|
|
340
|
+
BrokerConfig,
|
|
341
|
+
CancelByOidParams,
|
|
342
|
+
CancelByCloidParams,
|
|
343
|
+
CancelAllParams,
|
|
344
|
+
)
|
|
309
345
|
|
|
310
346
|
# Place order(s)
|
|
311
|
-
await exchange.place_order(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
347
|
+
await exchange.place_order(
|
|
348
|
+
PlaceOrderParams(
|
|
349
|
+
orders=[
|
|
350
|
+
UnitOrder(
|
|
351
|
+
instrument_id=1,
|
|
352
|
+
side="b", # 'b' for buy, 's' for sell
|
|
353
|
+
position_side="BOTH", # 'LONG', 'SHORT', or 'BOTH'
|
|
354
|
+
price="50000.00",
|
|
355
|
+
size="0.1",
|
|
356
|
+
tif="GTC", # 'GTC', 'IOC', or 'FOK'
|
|
357
|
+
ro=False, # reduce-only
|
|
358
|
+
po=False, # post-only
|
|
359
|
+
cloid="my-order-123", # client order ID
|
|
360
|
+
trigger_px="51000.00", # optional trigger price
|
|
361
|
+
is_market=False, # optional market order flag
|
|
362
|
+
tpsl="", # optional: 'tp', 'sl', or ''
|
|
363
|
+
grouping="normal", # optional: 'position', 'normal', or ''
|
|
364
|
+
),
|
|
365
|
+
],
|
|
366
|
+
broker_config=BrokerConfig( # optional broker configuration
|
|
367
|
+
broker="0x0000000000000000000000000000000000000000",
|
|
368
|
+
fee="0.001",
|
|
369
|
+
),
|
|
370
|
+
expires_after=int(time.time() * 1000) + 3600000, # 1 hour from now (in milliseconds)
|
|
371
|
+
)
|
|
372
|
+
)
|
|
335
373
|
|
|
336
374
|
# Cancel order by order ID
|
|
337
|
-
await exchange.cancel_by_oid(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
375
|
+
await exchange.cancel_by_oid(
|
|
376
|
+
CancelByOidParams(
|
|
377
|
+
cancels=[
|
|
378
|
+
{"oid": 123456, "instrumentId": 1},
|
|
379
|
+
{"oid": 123457, "instrumentId": 1},
|
|
380
|
+
],
|
|
381
|
+
expires_after=int(time.time() * 1000) + 3600000,
|
|
382
|
+
)
|
|
383
|
+
)
|
|
344
384
|
|
|
345
385
|
# Cancel order by client order ID
|
|
346
|
-
await exchange.cancel_by_cloid(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
386
|
+
await exchange.cancel_by_cloid(
|
|
387
|
+
CancelByCloidParams(
|
|
388
|
+
cancels=[{"cloid": "my-order-123", "instrumentId": 1}],
|
|
389
|
+
expires_after=int(time.time() * 1000) + 3600000,
|
|
390
|
+
)
|
|
391
|
+
)
|
|
350
392
|
|
|
351
393
|
# Cancel all orders
|
|
352
|
-
await exchange.cancel_all(
|
|
353
|
-
|
|
354
|
-
|
|
394
|
+
await exchange.cancel_all(
|
|
395
|
+
CancelAllParams(
|
|
396
|
+
expires_after=int(time.time() * 1000) + 3600000,
|
|
397
|
+
)
|
|
398
|
+
)
|
|
355
399
|
```
|
|
356
400
|
|
|
357
401
|
#### Account Management
|
|
358
402
|
|
|
359
403
|
```python
|
|
404
|
+
from hotstuff import AddAgentParams
|
|
405
|
+
from hotstuff.methods.exchange.account import (
|
|
406
|
+
RevokeAgentParams,
|
|
407
|
+
ApproveBrokerFeeParams,
|
|
408
|
+
UpdatePerpInstrumentLeverageParams,
|
|
409
|
+
CreateReferralCodeParams,
|
|
410
|
+
SetReferrerParams,
|
|
411
|
+
ClaimReferralRewardsParams,
|
|
412
|
+
)
|
|
413
|
+
|
|
360
414
|
# Add an agent (requires agent private key)
|
|
361
|
-
await exchange.add_agent(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
415
|
+
await exchange.add_agent(
|
|
416
|
+
AddAgentParams(
|
|
417
|
+
agent_name="my-trading-bot",
|
|
418
|
+
agent="0xagent...",
|
|
419
|
+
for_account="",
|
|
420
|
+
agent_private_key="0xprivatekey...",
|
|
421
|
+
signer="0xsigner...",
|
|
422
|
+
valid_until=int(time.time() * 1000) + 86400000, # 24 hours (in milliseconds)
|
|
423
|
+
)
|
|
424
|
+
)
|
|
369
425
|
|
|
370
426
|
# Revoke an agent
|
|
371
|
-
await exchange.revoke_agent(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
427
|
+
await exchange.revoke_agent(
|
|
428
|
+
RevokeAgentParams(
|
|
429
|
+
agent="0xagent...",
|
|
430
|
+
for_account="", # optional: sub-account address
|
|
431
|
+
)
|
|
432
|
+
)
|
|
375
433
|
|
|
376
434
|
# Update leverage for a perpetual instrument
|
|
377
|
-
await exchange.update_perp_instrument_leverage(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
435
|
+
await exchange.update_perp_instrument_leverage(
|
|
436
|
+
UpdatePerpInstrumentLeverageParams(
|
|
437
|
+
instrument_id=1,
|
|
438
|
+
leverage=10, # 10x leverage
|
|
439
|
+
)
|
|
440
|
+
)
|
|
381
441
|
|
|
382
442
|
# Approve broker fee
|
|
383
|
-
await exchange.approve_broker_fee(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
443
|
+
await exchange.approve_broker_fee(
|
|
444
|
+
ApproveBrokerFeeParams(
|
|
445
|
+
broker="0xbroker...",
|
|
446
|
+
max_fee_rate="0.001", # 0.1% max fee
|
|
447
|
+
)
|
|
448
|
+
)
|
|
387
449
|
|
|
388
450
|
# Create a referral code
|
|
389
|
-
await exchange.create_referral_code(
|
|
390
|
-
|
|
391
|
-
|
|
451
|
+
await exchange.create_referral_code(
|
|
452
|
+
CreateReferralCodeParams(
|
|
453
|
+
code="MY_REFERRAL_CODE",
|
|
454
|
+
)
|
|
455
|
+
)
|
|
392
456
|
|
|
393
457
|
# Set referrer using a referral code
|
|
394
|
-
await exchange.set_referrer(
|
|
395
|
-
|
|
396
|
-
|
|
458
|
+
await exchange.set_referrer(
|
|
459
|
+
SetReferrerParams(
|
|
460
|
+
code="FRIEND_REFERRAL_CODE",
|
|
461
|
+
)
|
|
462
|
+
)
|
|
397
463
|
|
|
398
464
|
# Claim referral rewards
|
|
399
|
-
await exchange.claim_referral_rewards(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
465
|
+
await exchange.claim_referral_rewards(
|
|
466
|
+
ClaimReferralRewardsParams(
|
|
467
|
+
collateral_id=1,
|
|
468
|
+
spot=True, # True for spot account, False for derivatives
|
|
469
|
+
)
|
|
470
|
+
)
|
|
403
471
|
```
|
|
404
472
|
|
|
405
473
|
#### Collateral Transfer Methods
|
|
406
474
|
|
|
407
475
|
```python
|
|
476
|
+
from hotstuff.methods.exchange.collateral import (
|
|
477
|
+
AccountSpotWithdrawRequestParams,
|
|
478
|
+
AccountDerivativeWithdrawRequestParams,
|
|
479
|
+
AccountSpotBalanceTransferRequestParams,
|
|
480
|
+
AccountDerivativeBalanceTransferRequestParams,
|
|
481
|
+
AccountInternalBalanceTransferRequestParams,
|
|
482
|
+
)
|
|
483
|
+
|
|
408
484
|
# Request spot collateral withdrawal to external chain
|
|
409
|
-
await exchange.account_spot_withdraw_request(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
485
|
+
await exchange.account_spot_withdraw_request(
|
|
486
|
+
AccountSpotWithdrawRequestParams(
|
|
487
|
+
collateral_id=1,
|
|
488
|
+
amount="100.0",
|
|
489
|
+
chain_id=1, # Ethereum mainnet
|
|
490
|
+
)
|
|
491
|
+
)
|
|
414
492
|
|
|
415
493
|
# Request derivative collateral withdrawal to external chain
|
|
416
|
-
await exchange.account_derivative_withdraw_request(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
494
|
+
await exchange.account_derivative_withdraw_request(
|
|
495
|
+
AccountDerivativeWithdrawRequestParams(
|
|
496
|
+
collateral_id=1,
|
|
497
|
+
amount="100.0",
|
|
498
|
+
chain_id=1,
|
|
499
|
+
)
|
|
500
|
+
)
|
|
421
501
|
|
|
422
502
|
# Transfer spot balance to another address on Hotstuff
|
|
423
|
-
await exchange.account_spot_balance_transfer_request(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
503
|
+
await exchange.account_spot_balance_transfer_request(
|
|
504
|
+
AccountSpotBalanceTransferRequestParams(
|
|
505
|
+
collateral_id=1,
|
|
506
|
+
amount="50.0",
|
|
507
|
+
destination="0xrecipient...",
|
|
508
|
+
)
|
|
509
|
+
)
|
|
428
510
|
|
|
429
511
|
# Transfer derivative balance to another address on Hotstuff
|
|
430
|
-
await exchange.account_derivative_balance_transfer_request(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
512
|
+
await exchange.account_derivative_balance_transfer_request(
|
|
513
|
+
AccountDerivativeBalanceTransferRequestParams(
|
|
514
|
+
collateral_id=1,
|
|
515
|
+
amount="50.0",
|
|
516
|
+
destination="0xrecipient...",
|
|
517
|
+
)
|
|
518
|
+
)
|
|
435
519
|
|
|
436
520
|
# Transfer balance between spot and derivatives accounts
|
|
437
|
-
await exchange.account_internal_balance_transfer_request(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
521
|
+
await exchange.account_internal_balance_transfer_request(
|
|
522
|
+
AccountInternalBalanceTransferRequestParams(
|
|
523
|
+
collateral_id=1,
|
|
524
|
+
amount="25.0",
|
|
525
|
+
to_derivatives_account=True, # True: spot -> derivatives, False: derivatives -> spot
|
|
526
|
+
)
|
|
527
|
+
)
|
|
442
528
|
```
|
|
443
529
|
|
|
444
530
|
#### Vault Methods
|
|
445
531
|
|
|
446
532
|
```python
|
|
533
|
+
from hotstuff.methods.exchange.vault import (
|
|
534
|
+
DepositToVaultParams,
|
|
535
|
+
RedeemFromVaultParams,
|
|
536
|
+
)
|
|
537
|
+
|
|
447
538
|
# Deposit to a vault
|
|
448
|
-
await exchange.deposit_to_vault(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
539
|
+
await exchange.deposit_to_vault(
|
|
540
|
+
DepositToVaultParams(
|
|
541
|
+
vault_address="0xvault...",
|
|
542
|
+
amount="1000.0",
|
|
543
|
+
)
|
|
544
|
+
)
|
|
452
545
|
|
|
453
546
|
# Redeem shares from a vault
|
|
454
|
-
await exchange.redeem_from_vault(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
547
|
+
await exchange.redeem_from_vault(
|
|
548
|
+
RedeemFromVaultParams(
|
|
549
|
+
vault_address="0xvault...",
|
|
550
|
+
shares="500.0",
|
|
551
|
+
)
|
|
552
|
+
)
|
|
458
553
|
```
|
|
459
554
|
|
|
460
555
|
---
|
|
@@ -477,12 +572,18 @@ async def setup():
|
|
|
477
572
|
#### Market Subscriptions
|
|
478
573
|
|
|
479
574
|
```python
|
|
575
|
+
import importlib
|
|
576
|
+
|
|
577
|
+
subscription_methods = importlib.import_module("hotstuff.methods.subscription.global")
|
|
578
|
+
TickerSubscriptionParams = subscription_methods.TickerSubscriptionParams
|
|
579
|
+
TradeSubscriptionParams = subscription_methods.TradeSubscriptionParams
|
|
580
|
+
|
|
480
581
|
# Subscribe to ticker updates
|
|
481
582
|
def handle_ticker(data):
|
|
482
583
|
print(f"Ticker: {data.data}")
|
|
483
584
|
|
|
484
585
|
ticker_sub = await subscriptions.ticker(
|
|
485
|
-
|
|
586
|
+
TickerSubscriptionParams(symbol="BTC-PERP"),
|
|
486
587
|
handle_ticker
|
|
487
588
|
)
|
|
488
589
|
|
|
@@ -506,7 +607,7 @@ orderbook_sub = await subscriptions.orderbook(
|
|
|
506
607
|
|
|
507
608
|
# Subscribe to trades
|
|
508
609
|
trade_sub = await subscriptions.trade(
|
|
509
|
-
|
|
610
|
+
TradeSubscriptionParams(instrument_id="BTC-PERP"),
|
|
510
611
|
lambda data: print(f"Trade: {data.data}")
|
|
511
612
|
)
|
|
512
613
|
|
|
@@ -773,6 +874,90 @@ ws_transport = WebSocketTransport(
|
|
|
773
874
|
|
|
774
875
|
---
|
|
775
876
|
|
|
877
|
+
## Signing
|
|
878
|
+
|
|
879
|
+
### How Signing Works
|
|
880
|
+
|
|
881
|
+
The SDK uses EIP-712 typed data signing for all exchange actions. Here's what happens under the hood:
|
|
882
|
+
|
|
883
|
+
1. **Action Encoding**: The action payload is encoded using MessagePack
|
|
884
|
+
2. **Hashing**: The encoded bytes are hashed with keccak256
|
|
885
|
+
3. **EIP-712 Signing**: The hash is signed using EIP-712 typed data with the following structure:
|
|
886
|
+
|
|
887
|
+
```python
|
|
888
|
+
from eth_account import Account
|
|
889
|
+
from eth_account.messages import encode_structured_data
|
|
890
|
+
from eth_utils import keccak
|
|
891
|
+
import msgpack
|
|
892
|
+
|
|
893
|
+
# EIP-712 Domain
|
|
894
|
+
domain = {
|
|
895
|
+
"name": "HotstuffCore",
|
|
896
|
+
"version": "1",
|
|
897
|
+
"chainId": 1,
|
|
898
|
+
"verifyingContract": "0x1234567890123456789012345678901234567890",
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
# EIP-712 Types
|
|
902
|
+
types = {
|
|
903
|
+
"EIP712Domain": [
|
|
904
|
+
{"name": "name", "type": "string"},
|
|
905
|
+
{"name": "version", "type": "string"},
|
|
906
|
+
{"name": "chainId", "type": "uint256"},
|
|
907
|
+
{"name": "verifyingContract", "type": "address"},
|
|
908
|
+
],
|
|
909
|
+
"Action": [
|
|
910
|
+
{"name": "source", "type": "string"}, # "Testnet" or "Mainnet"
|
|
911
|
+
{"name": "hash", "type": "bytes32"}, # keccak256 of msgpack-encoded action
|
|
912
|
+
{"name": "txType", "type": "uint16"}, # transaction type identifier
|
|
913
|
+
],
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
# Encode action to msgpack
|
|
917
|
+
action_bytes = msgpack.packb(action)
|
|
918
|
+
|
|
919
|
+
# Hash the payload
|
|
920
|
+
payload_hash = keccak(action_bytes)
|
|
921
|
+
|
|
922
|
+
# Message
|
|
923
|
+
message = {
|
|
924
|
+
"source": "Testnet", # or "Mainnet"
|
|
925
|
+
"hash": payload_hash,
|
|
926
|
+
"txType": tx_type,
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
# Create structured data
|
|
930
|
+
structured_data = {
|
|
931
|
+
"types": types,
|
|
932
|
+
"primaryType": "Action",
|
|
933
|
+
"domain": domain,
|
|
934
|
+
"message": message,
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
# Encode and sign
|
|
938
|
+
encoded_data = encode_structured_data(structured_data)
|
|
939
|
+
signed_message = wallet.sign_message(encoded_data)
|
|
940
|
+
signature = signed_message.signature.hex()
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
### Debugging Signature Issues
|
|
944
|
+
|
|
945
|
+
It is recommended to use an existing SDK instead of manually generating signatures. There are many potential ways in which signatures can be wrong. An incorrect signature results in recovering a different signer based on the signature and payload and results in one of the following errors:
|
|
946
|
+
|
|
947
|
+
```
|
|
948
|
+
"Error: account does not exist."
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
```
|
|
952
|
+
"invalid order signer"
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
where the returned address does not match the public address of the wallet you are signing with. The returned address also changes for different inputs.
|
|
956
|
+
|
|
957
|
+
An incorrect signature does not indicate why it is incorrect which makes debugging more challenging. To debug this it is recommended to read through the SDK carefully and make sure the implementation matches exactly. If that doesn't work, add logging to find where the output diverges.
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
776
961
|
## Error Handling
|
|
777
962
|
|
|
778
963
|
### HTTP Errors
|
|
@@ -781,9 +966,11 @@ HTTP transport raises exceptions with descriptive messages from the server:
|
|
|
781
966
|
|
|
782
967
|
```python
|
|
783
968
|
try:
|
|
784
|
-
await exchange.place_order(
|
|
785
|
-
|
|
786
|
-
|
|
969
|
+
await exchange.place_order(
|
|
970
|
+
PlaceOrderParams(
|
|
971
|
+
# ... order params
|
|
972
|
+
)
|
|
973
|
+
)
|
|
787
974
|
except Exception as e:
|
|
788
975
|
print(f"Failed to place order: {e}")
|
|
789
976
|
```
|
|
@@ -811,6 +998,7 @@ except Exception as e:
|
|
|
811
998
|
```python
|
|
812
999
|
import asyncio
|
|
813
1000
|
import time
|
|
1001
|
+
import os
|
|
814
1002
|
from hotstuff import (
|
|
815
1003
|
HttpTransport,
|
|
816
1004
|
WebSocketTransport,
|
|
@@ -821,47 +1009,69 @@ from hotstuff import (
|
|
|
821
1009
|
WebSocketTransportOptions,
|
|
822
1010
|
)
|
|
823
1011
|
from eth_account import Account
|
|
1012
|
+
import importlib
|
|
1013
|
+
|
|
1014
|
+
global_methods = importlib.import_module("hotstuff.methods.info.global")
|
|
1015
|
+
TickerParams = global_methods.TickerParams
|
|
1016
|
+
|
|
1017
|
+
from hotstuff.methods.exchange.trading import (
|
|
1018
|
+
PlaceOrderParams,
|
|
1019
|
+
UnitOrder,
|
|
1020
|
+
BrokerConfig,
|
|
1021
|
+
)
|
|
824
1022
|
|
|
825
1023
|
async def main():
|
|
826
1024
|
# Setup
|
|
827
1025
|
http_transport = HttpTransport(HttpTransportOptions(is_testnet=True))
|
|
828
1026
|
ws_transport = WebSocketTransport(WebSocketTransportOptions(is_testnet=True))
|
|
829
1027
|
|
|
830
|
-
account = Account.from_key("
|
|
1028
|
+
account = Account.from_key(os.getenv("PRIVATE_KEY"))
|
|
831
1029
|
|
|
832
1030
|
info = InfoClient(transport=http_transport)
|
|
833
1031
|
exchange = ExchangeClient(transport=http_transport, wallet=account)
|
|
834
1032
|
subscriptions = SubscriptionClient(transport=ws_transport)
|
|
835
1033
|
|
|
836
1034
|
# Get current market data
|
|
837
|
-
ticker = await info.ticker(
|
|
1035
|
+
ticker = await info.ticker(TickerParams(symbol="BTC-PERP"))
|
|
838
1036
|
print(f"Current price: {ticker}")
|
|
839
1037
|
|
|
840
1038
|
# Subscribe to live updates
|
|
841
|
-
|
|
1039
|
+
def handle_ticker(data):
|
|
842
1040
|
price = data.data.get("last")
|
|
843
1041
|
print(f"Live price: {price}")
|
|
844
1042
|
|
|
845
1043
|
# Simple trading logic
|
|
846
1044
|
if price and price < 50000:
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1045
|
+
asyncio.create_task(place_order(exchange, price))
|
|
1046
|
+
|
|
1047
|
+
async def place_order(exchange, price):
|
|
1048
|
+
try:
|
|
1049
|
+
await exchange.place_order(
|
|
1050
|
+
PlaceOrderParams(
|
|
1051
|
+
orders=[
|
|
1052
|
+
UnitOrder(
|
|
1053
|
+
instrument_id=1,
|
|
1054
|
+
side="b",
|
|
1055
|
+
position_side="BOTH",
|
|
1056
|
+
price=str(price),
|
|
1057
|
+
size="0.1",
|
|
1058
|
+
tif="GTC",
|
|
1059
|
+
ro=False,
|
|
1060
|
+
po=False,
|
|
1061
|
+
cloid=f"order-{int(time.time())}",
|
|
1062
|
+
trigger_px=None,
|
|
1063
|
+
is_market=False,
|
|
1064
|
+
tpsl="",
|
|
1065
|
+
grouping="",
|
|
1066
|
+
)
|
|
1067
|
+
],
|
|
1068
|
+
broker_config=BrokerConfig(broker="", fee=""),
|
|
1069
|
+
expires_after=int(time.time() * 1000) + 3600000,
|
|
1070
|
+
)
|
|
1071
|
+
)
|
|
1072
|
+
print("Order placed!")
|
|
1073
|
+
except Exception as e:
|
|
1074
|
+
print(f"Order failed: {e}")
|
|
865
1075
|
|
|
866
1076
|
ticker_sub = await subscriptions.ticker(
|
|
867
1077
|
{"symbol": "BTC-PERP"},
|
|
@@ -983,3 +1193,199 @@ if __name__ == "__main__":
|
|
|
983
1193
|
asyncio.run(broker_agent_trading_example())
|
|
984
1194
|
```
|
|
985
1195
|
|
|
1196
|
+
### WebSocket Subscriptions Example
|
|
1197
|
+
|
|
1198
|
+
```python
|
|
1199
|
+
import asyncio
|
|
1200
|
+
import importlib
|
|
1201
|
+
from hotstuff import (
|
|
1202
|
+
WebSocketTransport,
|
|
1203
|
+
SubscriptionClient,
|
|
1204
|
+
WebSocketTransportOptions,
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
subscription_methods = importlib.import_module("hotstuff.methods.subscription.global")
|
|
1208
|
+
TickerSubscriptionParams = subscription_methods.TickerSubscriptionParams
|
|
1209
|
+
TradeSubscriptionParams = subscription_methods.TradeSubscriptionParams
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
async def main():
|
|
1213
|
+
# Create WebSocket transport for testnet
|
|
1214
|
+
transport = WebSocketTransport(
|
|
1215
|
+
WebSocketTransportOptions(is_testnet=True)
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
# Create SubscriptionClient
|
|
1219
|
+
subscriptions = SubscriptionClient(transport=transport)
|
|
1220
|
+
|
|
1221
|
+
try:
|
|
1222
|
+
# Subscribe to ticker updates
|
|
1223
|
+
def handle_ticker(data):
|
|
1224
|
+
print(f"Ticker update: {data.data}")
|
|
1225
|
+
|
|
1226
|
+
print("Subscribing to BTC-PERP ticker...")
|
|
1227
|
+
ticker_sub = await subscriptions.ticker(
|
|
1228
|
+
TickerSubscriptionParams(symbol="BTC-PERP"),
|
|
1229
|
+
handle_ticker
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
# Subscribe to trades
|
|
1233
|
+
def handle_trade(data):
|
|
1234
|
+
print(f"Trade: {data.data}")
|
|
1235
|
+
|
|
1236
|
+
print("Subscribing to BTC-PERP trades...")
|
|
1237
|
+
trade_sub = await subscriptions.trade(
|
|
1238
|
+
TradeSubscriptionParams(instrument_id="BTC-PERP"),
|
|
1239
|
+
handle_trade
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
# Run for 30 seconds
|
|
1243
|
+
print("\nListening to updates for 30 seconds...\n")
|
|
1244
|
+
await asyncio.sleep(30)
|
|
1245
|
+
|
|
1246
|
+
# Unsubscribe
|
|
1247
|
+
print("\nUnsubscribing...")
|
|
1248
|
+
await ticker_sub["unsubscribe"]()
|
|
1249
|
+
await trade_sub["unsubscribe"]()
|
|
1250
|
+
|
|
1251
|
+
finally:
|
|
1252
|
+
# Clean up
|
|
1253
|
+
await transport.disconnect()
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
if __name__ == "__main__":
|
|
1257
|
+
asyncio.run(main())
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
### Collateral Transfer Example
|
|
1261
|
+
|
|
1262
|
+
```python
|
|
1263
|
+
import asyncio
|
|
1264
|
+
import os
|
|
1265
|
+
from hotstuff import (
|
|
1266
|
+
HttpTransport,
|
|
1267
|
+
ExchangeClient,
|
|
1268
|
+
HttpTransportOptions,
|
|
1269
|
+
)
|
|
1270
|
+
from eth_account import Account
|
|
1271
|
+
from hotstuff.methods.exchange.collateral import (
|
|
1272
|
+
AccountSpotWithdrawRequestParams,
|
|
1273
|
+
AccountDerivativeWithdrawRequestParams,
|
|
1274
|
+
AccountSpotBalanceTransferRequestParams,
|
|
1275
|
+
AccountDerivativeBalanceTransferRequestParams,
|
|
1276
|
+
AccountInternalBalanceTransferRequestParams,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
async def main():
|
|
1281
|
+
transport = HttpTransport(HttpTransportOptions(is_testnet=True))
|
|
1282
|
+
account = Account.from_key(os.getenv("PRIVATE_KEY"))
|
|
1283
|
+
exchange = ExchangeClient(transport=transport, wallet=account)
|
|
1284
|
+
|
|
1285
|
+
try:
|
|
1286
|
+
# Request spot collateral withdrawal to external chain
|
|
1287
|
+
result = await exchange.account_spot_withdraw_request(
|
|
1288
|
+
AccountSpotWithdrawRequestParams(
|
|
1289
|
+
collateral_id=1, # USDC
|
|
1290
|
+
amount="100.0",
|
|
1291
|
+
chain_id=1, # Ethereum mainnet
|
|
1292
|
+
)
|
|
1293
|
+
)
|
|
1294
|
+
print(f"Spot withdraw request result: {result}")
|
|
1295
|
+
|
|
1296
|
+
# Request derivative collateral withdrawal to external chain
|
|
1297
|
+
result = await exchange.account_derivative_withdraw_request(
|
|
1298
|
+
AccountDerivativeWithdrawRequestParams(
|
|
1299
|
+
collateral_id=1, # USDC
|
|
1300
|
+
amount="50.0",
|
|
1301
|
+
chain_id=1, # Ethereum mainnet
|
|
1302
|
+
)
|
|
1303
|
+
)
|
|
1304
|
+
print(f"Derivative withdraw request result: {result}")
|
|
1305
|
+
|
|
1306
|
+
# Transfer spot balance to another address on Hotstuff
|
|
1307
|
+
recipient_address = "0x1234567890123456789012345678901234567890"
|
|
1308
|
+
result = await exchange.account_spot_balance_transfer_request(
|
|
1309
|
+
AccountSpotBalanceTransferRequestParams(
|
|
1310
|
+
collateral_id=1, # USDC
|
|
1311
|
+
amount="25.0",
|
|
1312
|
+
destination=recipient_address,
|
|
1313
|
+
)
|
|
1314
|
+
)
|
|
1315
|
+
print(f"Spot balance transfer result: {result}")
|
|
1316
|
+
|
|
1317
|
+
# Internal transfer between spot and derivatives accounts
|
|
1318
|
+
result = await exchange.account_internal_balance_transfer_request(
|
|
1319
|
+
AccountInternalBalanceTransferRequestParams(
|
|
1320
|
+
collateral_id=1, # USDC
|
|
1321
|
+
amount="10.0",
|
|
1322
|
+
to_derivatives_account=True, # Transfer from spot to derivatives
|
|
1323
|
+
)
|
|
1324
|
+
)
|
|
1325
|
+
print(f"Internal transfer result: {result}")
|
|
1326
|
+
|
|
1327
|
+
except Exception as e:
|
|
1328
|
+
print(f"Error: {e}")
|
|
1329
|
+
|
|
1330
|
+
finally:
|
|
1331
|
+
await transport.close()
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
if __name__ == "__main__":
|
|
1335
|
+
asyncio.run(main())
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
### Vault Operations Example
|
|
1339
|
+
|
|
1340
|
+
```python
|
|
1341
|
+
import asyncio
|
|
1342
|
+
import os
|
|
1343
|
+
from hotstuff import (
|
|
1344
|
+
HttpTransport,
|
|
1345
|
+
ExchangeClient,
|
|
1346
|
+
HttpTransportOptions,
|
|
1347
|
+
)
|
|
1348
|
+
from eth_account import Account
|
|
1349
|
+
from hotstuff.methods.exchange.vault import (
|
|
1350
|
+
DepositToVaultParams,
|
|
1351
|
+
RedeemFromVaultParams,
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
async def main():
|
|
1356
|
+
transport = HttpTransport(HttpTransportOptions(is_testnet=True))
|
|
1357
|
+
account = Account.from_key(os.getenv("PRIVATE_KEY"))
|
|
1358
|
+
exchange = ExchangeClient(transport=transport, wallet=account)
|
|
1359
|
+
|
|
1360
|
+
vault_address = "0x1234567890123456789012345678901234567890"
|
|
1361
|
+
|
|
1362
|
+
try:
|
|
1363
|
+
# Deposit to a vault
|
|
1364
|
+
result = await exchange.deposit_to_vault(
|
|
1365
|
+
DepositToVaultParams(
|
|
1366
|
+
vault_address=vault_address,
|
|
1367
|
+
amount="1000.0",
|
|
1368
|
+
)
|
|
1369
|
+
)
|
|
1370
|
+
print(f"Deposit result: {result}")
|
|
1371
|
+
|
|
1372
|
+
# Redeem shares from a vault
|
|
1373
|
+
result = await exchange.redeem_from_vault(
|
|
1374
|
+
RedeemFromVaultParams(
|
|
1375
|
+
vault_address=vault_address,
|
|
1376
|
+
shares="500.0",
|
|
1377
|
+
)
|
|
1378
|
+
)
|
|
1379
|
+
print(f"Redeem result: {result}")
|
|
1380
|
+
|
|
1381
|
+
except Exception as e:
|
|
1382
|
+
print(f"Error: {e}")
|
|
1383
|
+
|
|
1384
|
+
finally:
|
|
1385
|
+
await transport.close()
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
if __name__ == "__main__":
|
|
1389
|
+
asyncio.run(main())
|
|
1390
|
+
```
|
|
1391
|
+
|