x10-python-trading-starknet 1.2.0__tar.gz → 1.3.0__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.
Files changed (57) hide show
  1. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/PKG-INFO +2 -2
  2. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/pyproject.toml +2 -2
  3. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/assets.py +29 -0
  4. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/balances.py +12 -0
  5. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/configuration.py +8 -3
  6. x10_python_trading_starknet-1.3.0/x10/perpetual/limit_order_object_settlement.py +72 -0
  7. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/order_object_settlement.py +35 -3
  8. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/orders.py +14 -0
  9. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/simple_client/simple_trading_client.py +7 -4
  10. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/trading_client/account_module.py +11 -1
  11. x10_python_trading_starknet-1.2.0/x10/perpetual/trading_client/markets_information_module.py → x10_python_trading_starknet-1.3.0/x10/perpetual/trading_client/info_markets_module.py +1 -1
  12. x10_python_trading_starknet-1.3.0/x10/perpetual/trading_client/info_module.py +29 -0
  13. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/trading_client/trading_client.py +38 -14
  14. x10_python_trading_starknet-1.3.0/x10/perpetual/trading_client/vault_module.py +177 -0
  15. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/transfer_object.py +2 -1
  16. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/utils/http.py +9 -1
  17. x10_python_trading_starknet-1.2.0/x10/perpetual/trading_client/info_module.py +0 -13
  18. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/LICENSE +0 -0
  19. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/README.md +0 -0
  20. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/__init__.py +0 -0
  21. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/config.py +0 -0
  22. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/errors.py +0 -0
  23. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/__init__.py +0 -0
  24. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/accounts.py +0 -0
  25. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/amounts.py +0 -0
  26. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/bridges.py +0 -0
  27. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/candles.py +0 -0
  28. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/clients.py +0 -0
  29. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/fees.py +0 -0
  30. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/funding_rates.py +0 -0
  31. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/markets.py +0 -0
  32. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/order_object.py +0 -0
  33. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/orderbook.py +0 -0
  34. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/orderbooks.py +0 -0
  35. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/positions.py +0 -0
  36. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/stream_client/__init__.py +0 -0
  37. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/stream_client/perpetual_stream_connection.py +0 -0
  38. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/stream_client/stream_client.py +0 -0
  39. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/trades.py +0 -0
  40. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/trading_client/__init__.py +0 -0
  41. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/trading_client/base_module.py +0 -0
  42. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/trading_client/order_management_module.py +0 -0
  43. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/trading_client/testnet_module.py +0 -0
  44. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/transfers.py +0 -0
  45. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/user_client/__init__.py +0 -0
  46. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/user_client/l1_signing.py +0 -0
  47. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/user_client/onboarding.py +0 -0
  48. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/user_client/user_client.py +0 -0
  49. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/withdrawal_object.py +0 -0
  50. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/perpetual/withdrawals.py +0 -0
  51. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/utils/__init__.py +0 -0
  52. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/utils/date.py +0 -0
  53. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/utils/log.py +0 -0
  54. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/utils/model.py +0 -0
  55. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/utils/nonce.py +0 -0
  56. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/utils/order.py +0 -0
  57. {x10_python_trading_starknet-1.2.0 → x10_python_trading_starknet-1.3.0}/x10/utils/string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: x10-python-trading-starknet
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Python client for X10 API
5
5
  Home-page: https://github.com/x10xchange/python_sdk
6
6
  Author: X10
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
17
  Requires-Dist: aiohttp (>=3.10.11)
18
18
  Requires-Dist: eth-account (>=0.12.0)
19
- Requires-Dist: fast-stark-crypto (==0.3.8)
19
+ Requires-Dist: fast-stark-crypto (==0.5.0)
20
20
  Requires-Dist: pydantic (>=2.9.0)
21
21
  Requires-Dist: pyyaml (>=6.0.1)
22
22
  Requires-Dist: sortedcontainers (>=2.4.0)
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
5
5
 
6
6
  [tool.poetry]
7
7
  name = "x10-python-trading-starknet"
8
- version = "1.2.0"
8
+ version = "1.3.0"
9
9
  description = "Python client for X10 API"
10
10
  authors = ["X10 <tech@ex10.org>"]
11
11
  repository = "https://github.com/x10xchange/python_sdk"
@@ -24,7 +24,7 @@ packages = [{ include = "x10" }]
24
24
  [tool.poetry.dependencies]
25
25
  aiohttp = ">=3.10.11"
26
26
  eth-account = ">=0.12.0"
27
- fast-stark-crypto = "==0.3.8"
27
+ fast-stark-crypto = "==0.5.0"
28
28
  pydantic = ">=2.9.0"
29
29
  python = "^3.10"
30
30
  pyyaml = ">=6.0.1"
@@ -7,6 +7,21 @@ from strenum import StrEnum
7
7
  from x10.utils.model import HexValue, X10BaseModel
8
8
 
9
9
 
10
+ class AssetModel(X10BaseModel):
11
+ id: int
12
+ name: str
13
+ symbol: str
14
+ precision: int
15
+ is_active: bool
16
+ is_collateral: bool
17
+ starkex_id: HexValue
18
+ starkex_resolution: int
19
+ l1_id: str
20
+ l1_resolution: int
21
+ version: int
22
+
23
+
24
+ # FIXME: Replace with AssetModel
10
25
  @dataclass
11
26
  class Asset:
12
27
  id: int
@@ -37,6 +52,20 @@ class Asset:
37
52
  raise ValueError("Only collateral assets have an L1 representation")
38
53
  return int(internal * Decimal(self.l1_resolution))
39
54
 
55
+ @staticmethod
56
+ def from_model(model: AssetModel):
57
+ return Asset(
58
+ id=model.id,
59
+ name=model.name,
60
+ precision=model.precision,
61
+ active=model.is_active,
62
+ is_collateral=model.is_collateral,
63
+ settlement_external_id=hex(model.starkex_id),
64
+ settlement_resolution=model.starkex_resolution,
65
+ l1_external_id=model.l1_id,
66
+ l1_resolution=model.l1_resolution,
67
+ )
68
+
40
69
 
41
70
  class AssetOperationType(StrEnum):
42
71
  CLAIM = "CLAIM"
@@ -13,3 +13,15 @@ class BalanceModel(X10BaseModel):
13
13
  initial_margin: Decimal
14
14
  margin_ratio: Decimal
15
15
  updated_time: int
16
+
17
+
18
+ class SpotBalanceModel(X10BaseModel):
19
+ account_id: int
20
+ asset: str
21
+ balance: Decimal
22
+ index_price: Decimal
23
+ notional_value: Decimal
24
+ contribution_factor: Decimal
25
+ equity_contribution: Decimal
26
+ available_to_withdraw: Decimal
27
+ updated_at: int
@@ -13,6 +13,7 @@ class StarknetDomain:
13
13
  class EndpointConfig:
14
14
  """
15
15
  Attributes:
16
+ chain_rpc_url (str): Field is deprecated and will be removed.
16
17
  asset_operations_contract (str): Field is deprecated and will be removed.
17
18
  collateral_asset_contract (str): Field is deprecated and will be removed.
18
19
  collateral_asset_on_chain_id (str): Field is deprecated and will be removed.
@@ -33,6 +34,8 @@ class EndpointConfig:
33
34
  collateral_decimals: int
34
35
  collateral_asset_id: str
35
36
 
37
+ vault_asset_name: str
38
+
36
39
 
37
40
  TESTNET_CONFIG = EndpointConfig(
38
41
  chain_rpc_url="https://rpc.sepolia.org",
@@ -40,12 +43,13 @@ TESTNET_CONFIG = EndpointConfig(
40
43
  stream_url="wss://api.starknet.sepolia.extended.exchange/stream.extended.exchange/v1",
41
44
  onboarding_url="https://api.starknet.sepolia.extended.exchange",
42
45
  signing_domain="starknet.sepolia.extended.exchange",
46
+ starknet_domain=StarknetDomain(name="Perpetuals", version="v0", chain_id="SN_SEPOLIA", revision="1"),
43
47
  asset_operations_contract="",
44
- collateral_asset_contract="0x31857064564ed0ff978e687456963cba09c2c6985d8f9300a1de4962fafa054",
48
+ collateral_asset_contract="0x05ba91db44b3e6a4485b5dbfcb17d791faa9cb6890a42731b66b3536b28b8ed5",
45
49
  collateral_asset_on_chain_id="0x1",
46
50
  collateral_decimals=6,
47
51
  collateral_asset_id="0x1",
48
- starknet_domain=StarknetDomain(name="Perpetuals", version="v0", chain_id="SN_SEPOLIA", revision="1"),
52
+ vault_asset_name="XVS",
49
53
  )
50
54
 
51
55
  MAINNET_CONFIG = EndpointConfig(
@@ -54,10 +58,11 @@ MAINNET_CONFIG = EndpointConfig(
54
58
  stream_url="wss://api.starknet.extended.exchange/stream.extended.exchange/v1",
55
59
  onboarding_url="https://api.starknet.extended.exchange",
56
60
  signing_domain="extended.exchange",
61
+ starknet_domain=StarknetDomain(name="Perpetuals", version="v0", chain_id="SN_MAIN", revision="1"),
57
62
  asset_operations_contract="",
58
63
  collateral_asset_contract="",
59
64
  collateral_asset_on_chain_id="0x1",
60
65
  collateral_decimals=6,
61
66
  collateral_asset_id="0x1",
62
- starknet_domain=StarknetDomain(name="Perpetuals", version="v0", chain_id="SN_MAIN", revision="1"),
67
+ vault_asset_name="XVS",
63
68
  )
@@ -0,0 +1,72 @@
1
+ import decimal
2
+ from datetime import timedelta
3
+
4
+ from x10.perpetual.accounts import StarkPerpetualAccount
5
+ from x10.perpetual.amounts import HumanReadableAmount, StarkAmount
6
+ from x10.perpetual.assets import Asset, AssetModel
7
+ from x10.perpetual.configuration import StarknetDomain
8
+ from x10.perpetual.order_object_settlement import (
9
+ calculate_order_settlement_expiration,
10
+ hash_limit_order,
11
+ )
12
+ from x10.perpetual.orders import LimitOrderSettlementModel
13
+ from x10.utils.date import utc_now
14
+ from x10.utils.model import SettlementSignatureModel
15
+ from x10.utils.nonce import generate_nonce
16
+
17
+
18
+ def create_order_settlement_data(
19
+ *,
20
+ quote_amount,
21
+ base_amount,
22
+ position_id,
23
+ quote_asset_model: AssetModel,
24
+ base_asset_model: AssetModel,
25
+ starknet_account: StarkPerpetualAccount,
26
+ starknet_domain: StarknetDomain,
27
+ is_buy: bool,
28
+ ):
29
+ quote_asset = Asset.from_model(quote_asset_model)
30
+ base_asset = Asset.from_model(base_asset_model)
31
+
32
+ quote_amount_human = HumanReadableAmount(
33
+ asset=quote_asset,
34
+ value=-quote_amount if is_buy else quote_amount,
35
+ )
36
+ base_amount_human = HumanReadableAmount(
37
+ asset=base_asset,
38
+ value=base_amount if is_buy else -base_amount,
39
+ )
40
+
41
+ quote_amount_stark = quote_amount_human.to_stark_amount(decimal.Context(rounding=decimal.ROUND_UP))
42
+ base_amount_stark = base_amount_human.to_stark_amount(decimal.Context(rounding=decimal.ROUND_UP))
43
+
44
+ nonce = generate_nonce()
45
+ expire_time = utc_now() + timedelta(hours=1)
46
+ order_hash = hash_limit_order(
47
+ amount_base=base_amount_stark,
48
+ amount_quote=quote_amount_stark,
49
+ max_fee=StarkAmount(0, quote_asset),
50
+ nonce=nonce,
51
+ position_id=position_id,
52
+ expiration_timestamp=expire_time,
53
+ public_key=starknet_account.public_key,
54
+ starknet_domain=starknet_domain,
55
+ )
56
+ order_signature = starknet_account.sign(order_hash)
57
+
58
+ settlement = LimitOrderSettlementModel(
59
+ base_amount=base_amount_stark.value,
60
+ quote_amount=quote_amount_stark.value,
61
+ fee_amount=0,
62
+ base_asset_id=int(base_asset.settlement_external_id, 16),
63
+ quote_asset_id=int(quote_asset.settlement_external_id, 16),
64
+ fee_asset_id=int(quote_asset.settlement_external_id, 16),
65
+ expiration_timestamp=calculate_order_settlement_expiration(expire_time),
66
+ nonce=nonce,
67
+ receiver_position_id=position_id,
68
+ sender_position_id=position_id,
69
+ signature=SettlementSignatureModel(r=order_signature[0], s=order_signature[1]),
70
+ )
71
+
72
+ return settlement, quote_amount_human, base_amount_human
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
4
4
  from decimal import Decimal
5
5
  from typing import Callable, Optional, Tuple
6
6
 
7
- from fast_stark_crypto import get_order_msg_hash
7
+ from fast_stark_crypto import get_limit_order_msg_hash, get_order_msg_hash
8
8
 
9
9
  from x10.perpetual.amounts import (
10
10
  ROUNDING_BUY_CONTEXT,
@@ -45,13 +45,45 @@ class SettlementDataCtx:
45
45
  starknet_domain: StarknetDomain
46
46
 
47
47
 
48
- def __calc_settlement_expiration(expiration_timestamp: datetime):
48
+ def calculate_order_settlement_expiration(expiration_timestamp: datetime):
49
49
  expire_time_with_buffer = expiration_timestamp + timedelta(days=14)
50
50
  expire_time_as_seconds = math.ceil(expire_time_with_buffer.timestamp())
51
51
 
52
52
  return expire_time_as_seconds
53
53
 
54
54
 
55
+ def hash_limit_order(
56
+ amount_base: StarkAmount,
57
+ amount_quote: StarkAmount,
58
+ max_fee: StarkAmount,
59
+ nonce: int,
60
+ position_id: int,
61
+ expiration_timestamp: datetime,
62
+ public_key: int,
63
+ starknet_domain: StarknetDomain,
64
+ ) -> int:
65
+ synthetic_asset = amount_base.asset
66
+ collateral_asset = amount_quote.asset
67
+
68
+ return get_limit_order_msg_hash(
69
+ source_position_id=position_id,
70
+ receive_position_id=position_id,
71
+ base_asset_id=int(synthetic_asset.settlement_external_id, 16),
72
+ base_amount=amount_base.value,
73
+ quote_asset_id=int(collateral_asset.settlement_external_id, 16),
74
+ quote_amount=amount_quote.value,
75
+ fee_amount=max_fee.value,
76
+ fee_asset_id=int(collateral_asset.settlement_external_id, 16),
77
+ expiration=calculate_order_settlement_expiration(expiration_timestamp),
78
+ salt=nonce,
79
+ user_public_key=public_key,
80
+ domain_name=starknet_domain.name,
81
+ domain_version=starknet_domain.version,
82
+ domain_chain_id=starknet_domain.chain_id,
83
+ domain_revision=starknet_domain.revision,
84
+ )
85
+
86
+
55
87
  def hash_order(
56
88
  amount_synthetic: StarkAmount,
57
89
  amount_collateral: StarkAmount,
@@ -73,7 +105,7 @@ def hash_order(
73
105
  quote_amount=amount_collateral.value,
74
106
  fee_amount=max_fee.value,
75
107
  fee_asset_id=int(collateral_asset.settlement_external_id, 16),
76
- expiration=__calc_settlement_expiration(expiration_timestamp),
108
+ expiration=calculate_order_settlement_expiration(expiration_timestamp),
77
109
  salt=nonce,
78
110
  user_public_key=public_key,
79
111
  domain_name=starknet_domain.name,
@@ -109,6 +109,20 @@ class StarkSettlementModel(X10BaseModel):
109
109
  collateral_position: Decimal
110
110
 
111
111
 
112
+ class LimitOrderSettlementModel(X10BaseModel):
113
+ base_amount: int
114
+ quote_amount: int
115
+ fee_amount: int
116
+ base_asset_id: HexValue
117
+ quote_asset_id: HexValue
118
+ fee_asset_id: HexValue
119
+ expiration_timestamp: int
120
+ nonce: int
121
+ receiver_position_id: int
122
+ sender_position_id: int
123
+ signature: SettlementSignatureModel
124
+
125
+
112
126
  class StarkDebuggingOrderAmountsModel(X10BaseModel):
113
127
  collateral_amount: Decimal
114
128
  fee_amount: Decimal
@@ -13,15 +13,14 @@ from x10.perpetual.orders import (
13
13
  OpenOrderModel,
14
14
  OrderSide,
15
15
  OrderStatus,
16
+ OrderType,
16
17
  TimeInForce,
17
18
  )
18
19
  from x10.perpetual.stream_client.perpetual_stream_connection import (
19
20
  PerpetualStreamConnection,
20
21
  )
21
22
  from x10.perpetual.stream_client.stream_client import PerpetualStreamClient
22
- from x10.perpetual.trading_client.markets_information_module import (
23
- MarketsInformationModule,
24
- )
23
+ from x10.perpetual.trading_client.info_markets_module import InfoMarketsModule
25
24
  from x10.perpetual.trading_client.order_management_module import OrderManagementModule
26
25
  from x10.utils.http import WrappedStreamResponse
27
26
 
@@ -81,7 +80,7 @@ class BlockingTradingClient:
81
80
  )
82
81
  self.__endpoint_config = endpoint_config
83
82
  self.__account = account
84
- self.__market_module = MarketsInformationModule(endpoint_config, api_key=account.api_key)
83
+ self.__market_module = InfoMarketsModule(endpoint_config, api_key=account.api_key)
85
84
  self.__orders_module = OrderManagementModule(endpoint_config, api_key=account.api_key)
86
85
  self.__markets: Union[None, Dict[str, MarketModel]] = None
87
86
  self.__stream_client: PerpetualStreamClient = PerpetualStreamClient(api_url=endpoint_config.stream_url)
@@ -203,6 +202,8 @@ class BlockingTradingClient:
203
202
  builder_fee: Decimal | None = None,
204
203
  builder_id: int | None = None,
205
204
  time_in_force: TimeInForce = TimeInForce.GTT,
205
+ reduce_only: bool = False,
206
+ order_type: OrderType = OrderType.LIMIT,
206
207
  ) -> TimedOpenOrderModel:
207
208
  market = (await self.get_markets()).get(market_name)
208
209
  if not market:
@@ -211,10 +212,12 @@ class BlockingTradingClient:
211
212
  order: NewOrderModel = create_order_object(
212
213
  account=self.__account,
213
214
  market=market,
215
+ order_type=order_type,
214
216
  amount_of_synthetic=amount_of_synthetic,
215
217
  price=price,
216
218
  side=side,
217
219
  post_only=post_only,
220
+ reduce_only=reduce_only,
218
221
  previous_order_external_id=previous_order_external_id,
219
222
  starknet_domain=self.__endpoint_config.starknet_domain,
220
223
  order_external_id=external_id,
@@ -7,7 +7,7 @@ from x10.perpetual.assets import (
7
7
  AssetOperationStatus,
8
8
  AssetOperationType,
9
9
  )
10
- from x10.perpetual.balances import BalanceModel
10
+ from x10.perpetual.balances import BalanceModel, SpotBalanceModel
11
11
  from x10.perpetual.bridges import BridgesConfig, Quote
12
12
  from x10.perpetual.clients import ClientModel
13
13
  from x10.perpetual.fees import TradingFeeModel
@@ -125,6 +125,16 @@ class AccountModule(BaseModule):
125
125
 
126
126
  return await send_get_request(await self.get_session(), url, list[OpenOrderModel], api_key=self._get_api_key())
127
127
 
128
+ async def get_spot_balances(self) -> WrappedApiResponse[List[SpotBalanceModel]]:
129
+ """
130
+ https://api.docs.extended.exchange/#get-spot-balance
131
+ """
132
+
133
+ url = self._get_url("/user/spot/balances")
134
+ return await send_get_request(
135
+ await self.get_session(), url, List[SpotBalanceModel], api_key=self._get_api_key()
136
+ )
137
+
128
138
  async def get_trades(
129
139
  self,
130
140
  market_names: Optional[List[str]] = None,
@@ -10,7 +10,7 @@ from x10.utils.date import to_epoch_millis
10
10
  from x10.utils.http import send_get_request
11
11
 
12
12
 
13
- class MarketsInformationModule(BaseModule):
13
+ class InfoMarketsModule(BaseModule):
14
14
  async def get_markets(self, *, market_names: Optional[List[str]] = None):
15
15
  """
16
16
  https://api.docs.extended.exchange/#get-markets
@@ -0,0 +1,29 @@
1
+ from decimal import Decimal
2
+ from typing import List
3
+
4
+ from x10.perpetual.assets import AssetModel
5
+ from x10.perpetual.trading_client.base_module import BaseModule
6
+ from x10.utils.http import send_get_request
7
+ from x10.utils.model import X10BaseModel
8
+
9
+
10
+ class _SettingsModel(X10BaseModel):
11
+ stark_ex_contract_address: str
12
+
13
+
14
+ class InfoModule(BaseModule):
15
+ async def get_settings(self):
16
+ url = self._get_url("/info/settings")
17
+ return await send_get_request(await self.get_session(), url, _SettingsModel)
18
+
19
+ async def get_assets(self):
20
+ url = self._get_url("/info/assets")
21
+ return await send_get_request(await self.get_session(), url, List[AssetModel])
22
+
23
+ async def get_assets_dict(self):
24
+ assets = await self.get_assets()
25
+ return {asset.name: asset for asset in assets.data}
26
+
27
+ async def get_asset_price(self, *, asset_name: str):
28
+ url = self._get_url("/info/assets/<asset_name>/price", asset_name=asset_name)
29
+ return await send_get_request(await self.get_session(), url, Decimal)
@@ -14,12 +14,11 @@ from x10.perpetual.orders import (
14
14
  TimeInForce,
15
15
  )
16
16
  from x10.perpetual.trading_client.account_module import AccountModule
17
+ from x10.perpetual.trading_client.info_markets_module import InfoMarketsModule
17
18
  from x10.perpetual.trading_client.info_module import InfoModule
18
- from x10.perpetual.trading_client.markets_information_module import (
19
- MarketsInformationModule,
20
- )
21
19
  from x10.perpetual.trading_client.order_management_module import OrderManagementModule
22
20
  from x10.perpetual.trading_client.testnet_module import TestnetModule
21
+ from x10.perpetual.trading_client.vault_module import VaultModule
23
22
  from x10.utils.date import utc_now
24
23
  from x10.utils.http import WrappedApiResponse
25
24
  from x10.utils.log import get_logger
@@ -33,14 +32,16 @@ class PerpetualTradingClient:
33
32
  """
34
33
 
35
34
  __markets: Dict[str, MarketModel] | None
36
- __stark_account: StarkPerpetualAccount
35
+
36
+ __config: EndpointConfig
37
+ __stark_account: StarkPerpetualAccount | None
37
38
 
38
39
  __info_module: InfoModule
39
- __markets_info_module: MarketsInformationModule
40
+ __info_markets_module: InfoMarketsModule
40
41
  __account_module: AccountModule
41
42
  __order_management_module: OrderManagementModule
43
+ __vault_module: VaultModule
42
44
  __testnet_module: TestnetModule
43
- __config: EndpointConfig
44
45
 
45
46
  async def place_order(
46
47
  self,
@@ -65,7 +66,7 @@ class PerpetualTradingClient:
65
66
  raise ValueError("Stark account is not set")
66
67
 
67
68
  if not self.__markets:
68
- self.__markets = await self.__markets_info_module.get_markets_dict()
69
+ self.__markets = await self.__info_markets_module.get_markets_dict()
69
70
 
70
71
  market = self.__markets.get(market_name)
71
72
 
@@ -98,24 +99,43 @@ class PerpetualTradingClient:
98
99
  return await self.__order_management_module.place_order(order)
99
100
 
100
101
  async def close(self):
101
- await self.__markets_info_module.close_session()
102
+ await self.__info_markets_module.close_session()
102
103
  await self.__account_module.close_session()
103
104
  await self.__order_management_module.close_session()
104
105
 
106
+ async def __aenter__(self):
107
+ return self
108
+
109
+ async def __aexit__(self, exc_type, exc_value, traceback):
110
+ await self.close()
111
+
105
112
  def __init__(self, endpoint_config: EndpointConfig, stark_account: StarkPerpetualAccount | None = None):
106
113
  api_key = stark_account.api_key if stark_account else None
107
114
 
115
+ self.__config = endpoint_config
108
116
  self.__markets = None
109
-
110
- if stark_account:
111
- self.__stark_account = stark_account
117
+ self.__stark_account = stark_account
112
118
 
113
119
  self.__info_module = InfoModule(endpoint_config)
114
- self.__markets_info_module = MarketsInformationModule(endpoint_config, api_key=api_key)
120
+ self.__info_markets_module = InfoMarketsModule(endpoint_config, api_key=api_key)
115
121
  self.__account_module = AccountModule(endpoint_config, api_key=api_key, stark_account=stark_account)
116
122
  self.__order_management_module = OrderManagementModule(endpoint_config, api_key=api_key)
123
+ self.__vault_module = VaultModule(
124
+ endpoint_config,
125
+ info_module=self.__info_module,
126
+ account_module=self.__account_module,
127
+ account=stark_account,
128
+ api_key=api_key,
129
+ )
117
130
  self.__testnet_module = TestnetModule(endpoint_config, api_key=api_key, account_module=self.__account_module)
118
- self.__config = endpoint_config
131
+
132
+ @property
133
+ def config(self):
134
+ return self.__config
135
+
136
+ @property
137
+ def stark_account(self):
138
+ return self.__stark_account
119
139
 
120
140
  @property
121
141
  def info(self):
@@ -123,7 +143,7 @@ class PerpetualTradingClient:
123
143
 
124
144
  @property
125
145
  def markets_info(self):
126
- return self.__markets_info_module
146
+ return self.__info_markets_module
127
147
 
128
148
  @property
129
149
  def account(self):
@@ -136,3 +156,7 @@ class PerpetualTradingClient:
136
156
  @property
137
157
  def testnet(self):
138
158
  return self.__testnet_module
159
+
160
+ @property
161
+ def vault(self):
162
+ return self.__vault_module
@@ -0,0 +1,177 @@
1
+ import decimal
2
+ from decimal import Decimal
3
+ from types import NoneType
4
+ from typing import Optional
5
+
6
+ from x10.errors import X10Error
7
+ from x10.perpetual.accounts import StarkPerpetualAccount
8
+ from x10.perpetual.configuration import EndpointConfig
9
+ from x10.perpetual.limit_order_object_settlement import create_order_settlement_data
10
+ from x10.perpetual.orders import LimitOrderSettlementModel
11
+ from x10.perpetual.trading_client.account_module import AccountModule
12
+ from x10.perpetual.trading_client.base_module import BaseModule
13
+ from x10.perpetual.trading_client.info_module import InfoModule
14
+ from x10.utils.http import send_post_request
15
+ from x10.utils.model import X10BaseModel
16
+
17
+ # Protects from an error on shares pricing fluctuations.
18
+ VAULT_SHARES_SLIPPAGE_PCT = Decimal("0.65")
19
+ COLLATERAL_ASSET_NAME = "USD"
20
+
21
+
22
+ class DepositRequestModel(X10BaseModel):
23
+ from_account_id: int
24
+ to_account_id: int
25
+ collateral: Decimal
26
+ shares: Decimal
27
+ settlement: LimitOrderSettlementModel
28
+
29
+
30
+ class WithdrawRequestModel(X10BaseModel):
31
+ from_account_id: int
32
+ to_account_id: int
33
+ collateral: Decimal
34
+ shares: Decimal
35
+ settlement: LimitOrderSettlementModel
36
+
37
+
38
+ class VaultModule(BaseModule):
39
+ def __init__(
40
+ self,
41
+ endpoint_config: EndpointConfig,
42
+ *,
43
+ info_module: InfoModule,
44
+ account_module: AccountModule,
45
+ account: Optional[StarkPerpetualAccount] = None,
46
+ api_key: Optional[str] = None,
47
+ ):
48
+ super().__init__(endpoint_config, api_key=api_key)
49
+
50
+ self._info_module = info_module
51
+ self._account_module = account_module
52
+ self._account = account
53
+
54
+ async def get_vault_share_balance(self) -> Decimal:
55
+ spot_balances = (await self._account_module.get_spot_balances()).data
56
+ if spot_balances is None:
57
+ raise X10Error("Failed to get spot balances")
58
+ vault_asset_balances = filter(lambda b: b.asset == self._get_endpoint_config().vault_asset_name, spot_balances)
59
+ total_vault_asset_balance = sum(map(lambda b: b.balance, vault_asset_balances), Decimal(0))
60
+ return total_vault_asset_balance
61
+
62
+ async def deposit_to_vault(self, *, collateral_amount: Decimal) -> None:
63
+ if self._account is None:
64
+ raise X10Error("Stark account is required for vault operations")
65
+
66
+ account_info = (await self._account_module.get_account()).data
67
+ assets = await self._info_module.get_assets_dict()
68
+ vault_asset_price = (
69
+ await self._info_module.get_asset_price(asset_name=self._get_endpoint_config().vault_asset_name)
70
+ ).data
71
+
72
+ assert account_info is not None
73
+ assert vault_asset_price is not None
74
+
75
+ position_id = account_info.l2_vault
76
+ collateral_asset = assets[COLLATERAL_ASSET_NAME]
77
+ vault_asset = assets[self._get_endpoint_config().vault_asset_name]
78
+ vault_shares_expected = self.__calc_vault_shares_expected(
79
+ collateral_amount,
80
+ vault_asset_price,
81
+ vault_asset.precision,
82
+ )
83
+
84
+ settlement, collateral_amount_human, shares_amount_human = create_order_settlement_data(
85
+ quote_amount=collateral_amount,
86
+ base_amount=vault_shares_expected,
87
+ position_id=position_id,
88
+ quote_asset_model=collateral_asset,
89
+ base_asset_model=vault_asset,
90
+ starknet_account=self._account,
91
+ starknet_domain=self._get_endpoint_config().starknet_domain,
92
+ is_buy=True,
93
+ )
94
+ deposit_request = DepositRequestModel(
95
+ from_account_id=account_info.id,
96
+ to_account_id=account_info.id,
97
+ collateral=abs(collateral_amount_human.value),
98
+ shares=abs(shares_amount_human.value),
99
+ settlement=settlement,
100
+ )
101
+
102
+ url = self._get_url("/vault/user/deposits")
103
+ resp = await send_post_request(
104
+ await self.get_session(),
105
+ url,
106
+ NoneType,
107
+ json=deposit_request.to_api_request_json(exclude_none=True),
108
+ api_key=self._get_api_key(),
109
+ )
110
+
111
+ if resp.error is not None:
112
+ raise X10Error(f"Deposit error: {resp.error}")
113
+
114
+ async def withdraw_from_vault(self, *, shares_amount: Decimal) -> None:
115
+ if self._account is None:
116
+ raise X10Error("Stark account is required for vault operations")
117
+
118
+ assets = await self._info_module.get_assets_dict()
119
+ account_info = (await self._account_module.get_account()).data
120
+ vault_asset_price = (
121
+ await self._info_module.get_asset_price(asset_name=self._get_endpoint_config().vault_asset_name)
122
+ ).data
123
+
124
+ assert account_info is not None
125
+ assert vault_asset_price is not None
126
+
127
+ position_id = account_info.l2_vault
128
+ collateral_asset = assets[COLLATERAL_ASSET_NAME]
129
+ vault_asset = assets[self._get_endpoint_config().vault_asset_name]
130
+ collateral_amount_expected = self.__calc_collateral_amount_expected(
131
+ shares_amount,
132
+ vault_asset_price,
133
+ vault_asset.precision,
134
+ )
135
+
136
+ settlement, collateral_amount_human, shares_amount_human = create_order_settlement_data(
137
+ quote_amount=collateral_amount_expected,
138
+ base_amount=shares_amount,
139
+ position_id=position_id,
140
+ quote_asset_model=collateral_asset,
141
+ base_asset_model=vault_asset,
142
+ starknet_account=self._account,
143
+ starknet_domain=self._get_endpoint_config().starknet_domain,
144
+ is_buy=False,
145
+ )
146
+ withdraw_request = WithdrawRequestModel(
147
+ from_account_id=account_info.id,
148
+ to_account_id=account_info.id,
149
+ collateral=abs(collateral_amount_human.value),
150
+ shares=abs(shares_amount_human.value),
151
+ settlement=settlement,
152
+ )
153
+ url = self._get_url("/vault/user/withdrawals")
154
+ resp = await send_post_request(
155
+ await self.get_session(),
156
+ url,
157
+ NoneType,
158
+ json=withdraw_request.to_api_request_json(exclude_none=True),
159
+ api_key=self._get_api_key(),
160
+ )
161
+
162
+ if resp.error is not None:
163
+ raise X10Error(f"Withdraw error: {resp.error}")
164
+
165
+ @staticmethod
166
+ def __calc_vault_shares_expected(
167
+ collateral_amount: Decimal, vault_asset_price: Decimal, vault_asset_precision: int
168
+ ) -> Decimal:
169
+ shares = collateral_amount / vault_asset_price * VAULT_SHARES_SLIPPAGE_PCT
170
+ return shares.quantize(Decimal("10") ** -vault_asset_precision, rounding=decimal.ROUND_FLOOR)
171
+
172
+ @staticmethod
173
+ def __calc_collateral_amount_expected(
174
+ shares_amount: Decimal, vault_asset_price: Decimal, collateral_asset_precision: int
175
+ ) -> Decimal:
176
+ collateral_amount = shares_amount * vault_asset_price * VAULT_SHARES_SLIPPAGE_PCT
177
+ return collateral_amount.quantize(Decimal("10") ** -collateral_asset_precision, rounding=decimal.ROUND_FLOOR)
@@ -29,6 +29,7 @@ def calc_expiration_timestamp():
29
29
  return expire_time_with_buffer_seconds
30
30
 
31
31
 
32
+ # FIXME: Transfers are broken
32
33
  def create_transfer_object(
33
34
  from_vault: int,
34
35
  to_vault: int,
@@ -76,5 +77,5 @@ def create_transfer_object(
76
77
  to_vault=to_vault,
77
78
  amount=amount,
78
79
  settlement=settlement,
79
- transferred_asset=config.collateral_asset_id,
80
+ transferred_asset=config.collateral_asset_on_chain_id,
80
81
  )
@@ -1,5 +1,6 @@
1
1
  import itertools
2
2
  import re
3
+ from types import NoneType
3
4
  from typing import Any, Dict, Generic, List, Optional, Sequence, Type, TypeVar, Union
4
5
 
5
6
  import aiohttp
@@ -16,7 +17,7 @@ from x10.utils.model import X10BaseModel
16
17
  LOGGER = get_logger(__name__)
17
18
  CLIENT_TIMEOUT = ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT_SECONDS)
18
19
 
19
- ApiResponseType = TypeVar("ApiResponseType", bound=Union[int, X10BaseModel, Sequence[X10BaseModel]])
20
+ ApiResponseType = TypeVar("ApiResponseType", bound=Union[int, X10BaseModel, Sequence[X10BaseModel], None])
20
21
 
21
22
 
22
23
  class RateLimitException(X10Error):
@@ -89,6 +90,13 @@ def parse_response_to_model(
89
90
  ) -> WrappedApiResponse[ApiResponseType]:
90
91
  # Read this to get more context re the type ignore:
91
92
  # https://github.com/python/mypy/issues/13619
93
+
94
+ if model_class == NoneType:
95
+ return WrappedApiResponse[None](
96
+ status=ResponseStatus.OK,
97
+ data=None,
98
+ ) # type: ignore
99
+
92
100
  return WrappedApiResponse[model_class].model_validate_json(response_text) # type: ignore[valid-type]
93
101
 
94
102
 
@@ -1,13 +0,0 @@
1
- from x10.perpetual.trading_client.base_module import BaseModule
2
- from x10.utils.http import send_get_request
3
- from x10.utils.model import X10BaseModel
4
-
5
-
6
- class _SettingsModel(X10BaseModel):
7
- stark_ex_contract_address: str
8
-
9
-
10
- class InfoModule(BaseModule):
11
- async def get_settings(self):
12
- url = self._get_url("/info/settings")
13
- return await send_get_request(await self.get_session(), url, _SettingsModel)