afp-sdk 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,6 +53,11 @@ class MarginAccount:
53
53
  abi=ABI,
54
54
  )
55
55
 
56
+ @property
57
+ def Deposit(self) -> contract.ContractEvent:
58
+ """Binding for `event Deposit` on the MarginAccount contract."""
59
+ return self._contract.events.Deposit
60
+
56
61
  @property
57
62
  def FeeCollected(self) -> contract.ContractEvent:
58
63
  """Binding for `event FeeCollected` on the MarginAccount contract."""
@@ -68,11 +73,26 @@ class MarginAccount:
68
73
  """Binding for `event Initialized` on the MarginAccount contract."""
69
74
  return self._contract.events.Initialized
70
75
 
76
+ @property
77
+ def IntentAuthorized(self) -> contract.ContractEvent:
78
+ """Binding for `event IntentAuthorized` on the MarginAccount contract."""
79
+ return self._contract.events.IntentAuthorized
80
+
81
+ @property
82
+ def IntentRevoked(self) -> contract.ContractEvent:
83
+ """Binding for `event IntentRevoked` on the MarginAccount contract."""
84
+ return self._contract.events.IntentRevoked
85
+
71
86
  @property
72
87
  def PositionUpdated(self) -> contract.ContractEvent:
73
88
  """Binding for `event PositionUpdated` on the MarginAccount contract."""
74
89
  return self._contract.events.PositionUpdated
75
90
 
91
+ @property
92
+ def Withdraw(self) -> contract.ContractEvent:
93
+ """Binding for `event Withdraw` on the MarginAccount contract."""
94
+ return self._contract.events.Withdraw
95
+
76
96
  def authorize(
77
97
  self,
78
98
  intent_account: eth_typing.ChecksumAddress,
@@ -789,6 +809,25 @@ ABI = typing.cast(
789
809
  "name": "Unauthorized",
790
810
  "type": "error",
791
811
  },
812
+ {
813
+ "anonymous": False,
814
+ "inputs": [
815
+ {
816
+ "indexed": True,
817
+ "internalType": "address",
818
+ "name": "user",
819
+ "type": "address",
820
+ },
821
+ {
822
+ "indexed": False,
823
+ "internalType": "uint256",
824
+ "name": "amount",
825
+ "type": "uint256",
826
+ },
827
+ ],
828
+ "name": "Deposit",
829
+ "type": "event",
830
+ },
792
831
  {
793
832
  "anonymous": False,
794
833
  "inputs": [
@@ -840,6 +879,44 @@ ABI = typing.cast(
840
879
  "name": "Initialized",
841
880
  "type": "event",
842
881
  },
882
+ {
883
+ "anonymous": False,
884
+ "inputs": [
885
+ {
886
+ "indexed": True,
887
+ "internalType": "address",
888
+ "name": "marginAccountID",
889
+ "type": "address",
890
+ },
891
+ {
892
+ "indexed": True,
893
+ "internalType": "address",
894
+ "name": "intentAccount",
895
+ "type": "address",
896
+ },
897
+ ],
898
+ "name": "IntentAuthorized",
899
+ "type": "event",
900
+ },
901
+ {
902
+ "anonymous": False,
903
+ "inputs": [
904
+ {
905
+ "indexed": True,
906
+ "internalType": "address",
907
+ "name": "marginAccountID",
908
+ "type": "address",
909
+ },
910
+ {
911
+ "indexed": True,
912
+ "internalType": "address",
913
+ "name": "intentAccount",
914
+ "type": "address",
915
+ },
916
+ ],
917
+ "name": "IntentRevoked",
918
+ "type": "event",
919
+ },
843
920
  {
844
921
  "anonymous": False,
845
922
  "inputs": [
@@ -867,10 +944,41 @@ ABI = typing.cast(
867
944
  "name": "costBasis",
868
945
  "type": "int256",
869
946
  },
947
+ {
948
+ "indexed": False,
949
+ "internalType": "uint256",
950
+ "name": "price",
951
+ "type": "uint256",
952
+ },
953
+ {
954
+ "indexed": False,
955
+ "internalType": "int256",
956
+ "name": "quantity",
957
+ "type": "int256",
958
+ },
870
959
  ],
871
960
  "name": "PositionUpdated",
872
961
  "type": "event",
873
962
  },
963
+ {
964
+ "anonymous": False,
965
+ "inputs": [
966
+ {
967
+ "indexed": True,
968
+ "internalType": "address",
969
+ "name": "user",
970
+ "type": "address",
971
+ },
972
+ {
973
+ "indexed": False,
974
+ "internalType": "uint256",
975
+ "name": "amount",
976
+ "type": "uint256",
977
+ },
978
+ ],
979
+ "name": "Withdraw",
980
+ "type": "event",
981
+ },
874
982
  {
875
983
  "inputs": [
876
984
  {"internalType": "address", "name": "intentAccount", "type": "address"}
@@ -56,7 +56,6 @@ class Product:
56
56
  unit_value: int
57
57
  initial_margin_requirement: int
58
58
  maintenance_margin_requirement: int
59
- offer_price_buffer: int
60
59
  auction_bounty: int
61
60
  tradeout_interval: int
62
61
  tick_size: int
@@ -221,7 +220,6 @@ class ProductRegistry:
221
220
  product.unit_value,
222
221
  product.initial_margin_requirement,
223
222
  product.maintenance_margin_requirement,
224
- product.offer_price_buffer,
225
223
  product.auction_bounty,
226
224
  product.tradeout_interval,
227
225
  product.tick_size,
@@ -280,25 +278,6 @@ class ProductRegistry:
280
278
  ).call()
281
279
  return int(return_value)
282
280
 
283
- def offer_price_buffer(
284
- self,
285
- product_id: hexbytes.HexBytes,
286
- ) -> int:
287
- """Binding for `offerPriceBuffer` on the ProductRegistry contract.
288
-
289
- Parameters
290
- ----------
291
- product_id : hexbytes.HexBytes
292
-
293
- Returns
294
- -------
295
- int
296
- """
297
- return_value = self._contract.functions.offerPriceBuffer(
298
- product_id,
299
- ).call()
300
- return int(return_value)
301
-
302
281
  def oracle_specification(
303
282
  self,
304
283
  product_id: hexbytes.HexBytes,
@@ -395,8 +374,7 @@ class ProductRegistry:
395
374
  int(return_value[9]),
396
375
  int(return_value[10]),
397
376
  int(return_value[11]),
398
- int(return_value[12]),
399
- str(return_value[13]),
377
+ str(return_value[12]),
400
378
  )
401
379
 
402
380
  def proxiable_uuid(
@@ -447,7 +425,6 @@ class ProductRegistry:
447
425
  product.unit_value,
448
426
  product.initial_margin_requirement,
449
427
  product.maintenance_margin_requirement,
450
- product.offer_price_buffer,
451
428
  product.auction_bounty,
452
429
  product.tradeout_interval,
453
430
  product.tick_size,
@@ -856,11 +833,6 @@ ABI = typing.cast(
856
833
  "name": "maintenanceMarginRequirement",
857
834
  "type": "uint16",
858
835
  },
859
- {
860
- "internalType": "uint64",
861
- "name": "offerPriceBuffer",
862
- "type": "uint64",
863
- },
864
836
  {
865
837
  "internalType": "uint64",
866
838
  "name": "auctionBounty",
@@ -913,15 +885,6 @@ ABI = typing.cast(
913
885
  "stateMutability": "view",
914
886
  "type": "function",
915
887
  },
916
- {
917
- "inputs": [
918
- {"internalType": "bytes32", "name": "productId", "type": "bytes32"}
919
- ],
920
- "name": "offerPriceBuffer",
921
- "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
922
- "stateMutability": "view",
923
- "type": "function",
924
- },
925
888
  {
926
889
  "inputs": [
927
890
  {"internalType": "bytes32", "name": "productId", "type": "bytes32"}
@@ -1073,11 +1036,6 @@ ABI = typing.cast(
1073
1036
  "name": "maintenanceMarginRequirement",
1074
1037
  "type": "uint16",
1075
1038
  },
1076
- {
1077
- "internalType": "uint64",
1078
- "name": "offerPriceBuffer",
1079
- "type": "uint64",
1080
- },
1081
1039
  {
1082
1040
  "internalType": "uint64",
1083
1041
  "name": "auctionBounty",
@@ -1203,11 +1161,6 @@ ABI = typing.cast(
1203
1161
  "name": "maintenanceMarginRequirement",
1204
1162
  "type": "uint16",
1205
1163
  },
1206
- {
1207
- "internalType": "uint64",
1208
- "name": "offerPriceBuffer",
1209
- "type": "uint64",
1210
- },
1211
1164
  {
1212
1165
  "internalType": "uint64",
1213
1166
  "name": "auctionBounty",
@@ -324,8 +324,7 @@ class SystemViewer:
324
324
  int(return_value_elem[9]),
325
325
  int(return_value_elem[10]),
326
326
  int(return_value_elem[11]),
327
- int(return_value_elem[12]),
328
- str(return_value_elem[13]),
327
+ str(return_value_elem[12]),
329
328
  )
330
329
  for return_value_elem in return_value
331
330
  ]
@@ -929,11 +928,6 @@ ABI = typing.cast(
929
928
  "name": "maintenanceMarginRequirement",
930
929
  "type": "uint16",
931
930
  },
932
- {
933
- "internalType": "uint64",
934
- "name": "offerPriceBuffer",
935
- "type": "uint64",
936
- },
937
931
  {
938
932
  "internalType": "uint64",
939
933
  "name": "auctionBounty",
afp/config.py CHANGED
@@ -1,42 +1,28 @@
1
- import os
1
+ from dataclasses import dataclass
2
2
 
3
- from web3 import Web3
3
+ from eth_typing.evm import ChecksumAddress
4
4
 
5
+ from .auth import Authenticator
5
6
 
6
- # Constants from clearing/contracts/lib/constants.sol
7
- RATE_MULTIPLIER = 10**4
8
- FEE_RATE_MULTIPLIER = 10**6
9
- FULL_PRECISION_MULTIPLIER = 10**18
10
7
 
11
- USER_AGENT = "afp-sdk"
12
- DEFAULT_EXCHANGE_API_VERSION = 1
13
- EXCHANGE_URL = os.getenv(
14
- "AFP_EXCHANGE_URL", "https://afp-exchange-stable.up.railway.app"
15
- )
8
+ @dataclass(frozen=True)
9
+ class Config:
10
+ authenticator: Authenticator | None
16
11
 
17
- CHAIN_ID = int(os.getenv("AFP_CHAIN_ID", 65000000))
12
+ # Venue parameters
13
+ exchange_url: str
18
14
 
19
- CLEARING_DIAMOND_ADDRESS = Web3.to_checksum_address(
20
- os.getenv(
21
- "AFP_CLEARING_DIAMOND_ADDRESS", "0x5B5411F1548254d25360d71FE40cFc1cC983B2A2"
22
- )
23
- )
24
- MARGIN_ACCOUNT_REGISTRY_ADDRESS = Web3.to_checksum_address(
25
- os.getenv(
26
- "AFP_MARGIN_ACCOUNT_REGISTRY_ADDRESS",
27
- "0x99f4FA9Cdce7AD227eB84907936a8FeF2095D846",
28
- )
29
- )
30
- ORACLE_PROVIDER_ADDRESS = Web3.to_checksum_address(
31
- os.getenv(
32
- "AFP_ORACLE_PROVIDER_ADDRESS", "0xF2A2A27da33D30B4BF38D7e186E7B0b1e964e55c"
33
- )
34
- )
35
- PRODUCT_REGISTRY_ADDRESS = Web3.to_checksum_address(
36
- os.getenv(
37
- "AFP_PRODUCT_REGISTRY_ADDRESS", "0x86B3829471929B115367DA0958f56A6AB844b08e"
38
- )
39
- )
40
- SYSTEM_VIEWER_ADDRESS = Web3.to_checksum_address(
41
- os.getenv("AFP_SYSTEM_VIEWER_ADDRESS", "0xfF2DFcC44a95cce96E03EfC33C65c8Be671Bae5B")
42
- )
15
+ # Blockchain parameters
16
+ rpc_url: str | None
17
+ chain_id: int
18
+ gas_limit: int | None
19
+ max_fee_per_gas: int | None
20
+ max_priority_fee_per_gas: int | None
21
+ timeout_seconds: int
22
+
23
+ # Clearing System parameters
24
+ clearing_diamond_address: ChecksumAddress
25
+ margin_account_registry_address: ChecksumAddress
26
+ oracle_provider_address: ChecksumAddress
27
+ product_registry_address: ChecksumAddress
28
+ system_viewer_address: ChecksumAddress
afp/constants.py ADDED
@@ -0,0 +1,52 @@
1
+ import os
2
+ from types import SimpleNamespace
3
+
4
+
5
+ def _int_or_none(value: str | None) -> int | None:
6
+ return int(value) if value is not None else None
7
+
8
+
9
+ USER_AGENT = "afp-sdk"
10
+ DEFAULT_EXCHANGE_API_VERSION = 1
11
+
12
+ # Constants from clearing/contracts/lib/constants.sol
13
+ RATE_MULTIPLIER = 10**4
14
+ FEE_RATE_MULTIPLIER = 10**6
15
+ FULL_PRECISION_MULTIPLIER = 10**18
16
+
17
+ defaults = SimpleNamespace(
18
+ # Authentication parameters
19
+ KEYFILE=os.getenv("AFP_KEYFILE", None),
20
+ KEYFILE_PASSWORD=os.getenv("AFP_KEYFILE_PASSWORD", ""),
21
+ PRIVATE_KEY=os.getenv("AFP_PRIVATE_KEY", None),
22
+ # Venue parameters
23
+ EXCHANGE_URL=os.getenv(
24
+ "AFP_EXCHANGE_URL", "https://afp-exchange-stable.up.railway.app"
25
+ ),
26
+ # Blockchain parameters
27
+ RPC_URL=os.getenv("AFP_RPC_URL", None),
28
+ CHAIN_ID=int(os.getenv("AFP_CHAIN_ID", 65000000)),
29
+ GAS_LIMIT=_int_or_none(os.getenv("AFP_GAS_LIMIT", None)),
30
+ MAX_FEE_PER_GAS=_int_or_none(os.getenv("AFP_MAX_FEE_PER_GAS", None)),
31
+ MAX_PRIORITY_FEE_PER_GAS=_int_or_none(
32
+ os.getenv("AFP_MAX_PRIORITY_FEE_PER_GAS", None)
33
+ ),
34
+ TIMEOUT_SECONDS=int(os.getenv("AFP_TIMEOUT_SECONDS", 10)),
35
+ # Clearing System parameters
36
+ CLEARING_DIAMOND_ADDRESS=os.getenv(
37
+ "AFP_CLEARING_DIAMOND_ADDRESS", "0x5B5411F1548254d25360d71FE40cFc1cC983B2A2"
38
+ ),
39
+ MARGIN_ACCOUNT_REGISTRY_ADDRESS=os.getenv(
40
+ "AFP_MARGIN_ACCOUNT_REGISTRY_ADDRESS",
41
+ "0x99f4FA9Cdce7AD227eB84907936a8FeF2095D846",
42
+ ),
43
+ ORACLE_PROVIDER_ADDRESS=os.getenv(
44
+ "AFP_ORACLE_PROVIDER_ADDRESS", "0xF2A2A27da33D30B4BF38D7e186E7B0b1e964e55c"
45
+ ),
46
+ PRODUCT_REGISTRY_ADDRESS=os.getenv(
47
+ "AFP_PRODUCT_REGISTRY_ADDRESS", "0x86B3829471929B115367DA0958f56A6AB844b08e"
48
+ ),
49
+ SYSTEM_VIEWER_ADDRESS=os.getenv(
50
+ "AFP_SYSTEM_VIEWER_ADDRESS", "0xfF2DFcC44a95cce96E03EfC33C65c8Be671Bae5B"
51
+ ),
52
+ )
afp/decorators.py CHANGED
@@ -45,8 +45,10 @@ def convert_web3_error(*contract_abis: ABI) -> Callable[..., Any]:
45
45
  )
46
46
  if reason == "no data":
47
47
  reason = "Unspecified reason"
48
+ if reason is None:
49
+ reason = "Unknown error"
48
50
  raise ClearingSystemError(
49
- "Contract call reverted" + f": {reason}" if reason else ""
51
+ "Contract call reverted" + (f": {reason}" if reason else "")
50
52
  ) from contract_error
51
53
  except Web3RPCError as rpc_error:
52
54
  reason = None
afp/exceptions.py CHANGED
@@ -1,12 +1,16 @@
1
- class BaseException(Exception):
1
+ class AFPException(Exception):
2
2
  pass
3
3
 
4
4
 
5
- class ClearingSystemError(BaseException):
5
+ class ConfigurationError(AFPException):
6
6
  pass
7
7
 
8
8
 
9
- class ExchangeError(BaseException):
9
+ class ClearingSystemError(AFPException):
10
+ pass
11
+
12
+
13
+ class ExchangeError(AFPException):
10
14
  pass
11
15
 
12
16
 
afp/exchange.py CHANGED
@@ -1,10 +1,11 @@
1
1
  import json
2
+ import re
2
3
  from typing import Any, Generator
3
4
 
4
5
  import requests
5
6
  from requests import Response, Session
6
7
 
7
- from . import config
8
+ from . import constants
8
9
  from .exceptions import (
9
10
  AuthenticationError,
10
11
  AuthorizationError,
@@ -26,11 +27,16 @@ from .schemas import (
26
27
 
27
28
 
28
29
  class ExchangeClient:
30
+ _base_url: str
29
31
  _session: Session
30
32
 
31
- def __init__(self):
33
+ def __init__(self, base_url: str):
34
+ self._base_url = re.sub(r"/$", "", base_url)
32
35
  self._session = Session()
33
36
 
37
+ def __repr__(self) -> str:
38
+ return f"{self.__class__.__name__}(base_url={self._base_url})"
39
+
34
40
  # POST /nonce
35
41
  def generate_login_nonce(self) -> str:
36
42
  response = self._send_request("GET", "/nonce")
@@ -71,8 +77,10 @@ class ExchangeClient:
71
77
  return Order(**response.json())
72
78
 
73
79
  # GET /orders
74
- def get_open_orders(self) -> list[Order]:
75
- response = self._send_request("GET", "/orders")
80
+ def get_open_orders(self, product_id: str | None = None) -> list[Order]:
81
+ response = self._send_request(
82
+ "GET", "/orders", params=({"product_id": product_id} if product_id else {})
83
+ )
76
84
  return [Order(**item) for item in response.json()["orders"]]
77
85
 
78
86
  # GET /orders/{order_id}
@@ -121,19 +129,19 @@ class ExchangeClient:
121
129
  endpoint: str,
122
130
  *,
123
131
  stream: bool = False,
124
- api_version: int = config.DEFAULT_EXCHANGE_API_VERSION,
132
+ api_version: int = constants.DEFAULT_EXCHANGE_API_VERSION,
125
133
  **kwargs: Any,
126
134
  ) -> Response:
127
135
  kwargs["headers"] = {
128
136
  "Content-Type": "application/json",
129
137
  "Accept": "application/x-ndjson" if stream else "application/json",
130
- "User-Agent": config.USER_AGENT,
138
+ "User-Agent": constants.USER_AGENT,
131
139
  }
132
140
 
133
141
  try:
134
142
  response = self._session.request(
135
143
  method,
136
- f"{config.EXCHANGE_URL}/v{api_version}{endpoint}",
144
+ f"{self._base_url}/v{api_version}{endpoint}",
137
145
  stream=stream,
138
146
  **kwargs,
139
147
  )
@@ -1,13 +1,11 @@
1
1
  from typing import Any, cast
2
2
 
3
3
  from eth_abi.packed import encode_packed
4
- from eth_account import messages
5
4
  from eth_typing.evm import ChecksumAddress
6
- from eth_account.signers.local import LocalAccount
7
5
  from hexbytes import HexBytes
8
6
  from web3 import Web3
9
7
 
10
- from . import config
8
+ from . import constants
11
9
  from .bindings import clearing_facet
12
10
  from .schemas import IntentData, OrderSide
13
11
 
@@ -44,7 +42,7 @@ def generate_intent_hash(
44
42
  HexBytes(intent_data.product_id),
45
43
  int(intent_data.limit_price * 10**tick_size),
46
44
  intent_data.quantity,
47
- int(intent_data.max_trading_fee_rate * config.FEE_RATE_MULTIPLIER),
45
+ int(intent_data.max_trading_fee_rate * constants.FEE_RATE_MULTIPLIER),
48
46
  int(intent_data.good_until_time.timestamp()),
49
47
  ORDER_SIDE_MAPPING[intent_data.side],
50
48
  ]
@@ -57,13 +55,7 @@ def generate_order_cancellation_hash(nonce: int, intent_hash: str) -> HexBytes:
57
55
  return Web3.keccak(encode_packed(types, values))
58
56
 
59
57
 
60
- def generate_product_id(builder: LocalAccount, symbol: str) -> HexBytes:
58
+ def generate_product_id(builder_address: ChecksumAddress, symbol: str) -> HexBytes:
61
59
  types = ["address", "string"]
62
- values: list[Any] = [builder.address, symbol]
60
+ values: list[Any] = [builder_address, symbol]
63
61
  return Web3.keccak(encode_packed(types, values))
64
-
65
-
66
- def sign_message(account: LocalAccount, message: bytes) -> HexBytes:
67
- eip191_message = messages.encode_defunct(message)
68
- signed_message = account.sign_message(eip191_message)
69
- return signed_message.signature
afp/schemas.py CHANGED
@@ -66,6 +66,9 @@ class ExchangeProduct(Model):
66
66
  tick_size: int
67
67
  collateral_asset: str
68
68
 
69
+ def __str__(self) -> str:
70
+ return self.id
71
+
69
72
 
70
73
  class IntentData(Model):
71
74
  trading_protocol_id: str
@@ -152,6 +155,12 @@ class MarketDepthData(Model):
152
155
  # Clearing API
153
156
 
154
157
 
158
+ class Transaction(Model):
159
+ hash: str
160
+ data: dict[str, Any]
161
+ receipt: dict[str, Any]
162
+
163
+
155
164
  class Position(Model):
156
165
  id: str
157
166
  quantity: int
@@ -184,7 +193,6 @@ class ProductSpecification(Model):
184
193
  unit_value: Annotated[Decimal, Field(gt=0)]
185
194
  initial_margin_requirement: Annotated[Decimal, Field(gt=0)]
186
195
  maintenance_margin_requirement: Annotated[Decimal, Field(gt=0)]
187
- offer_price_buffer: Annotated[Decimal, Field(ge=0, lt=1)]
188
196
  auction_bounty: Annotated[Decimal, Field(ge=0, le=1)]
189
197
  tradeout_interval: Annotated[int, Field(ge=0)]
190
198
  extended_metadata: str
afp/validators.py CHANGED
@@ -22,6 +22,7 @@ def validate_timedelta(value: timedelta) -> timedelta:
22
22
 
23
23
 
24
24
  def validate_hexstr(value: str, length: int | None = None) -> str:
25
+ value = str(value)
25
26
  if not value.startswith("0x"):
26
27
  raise ValueError(f"{value} should start with '0x'")
27
28
  try: