afp-sdk 0.4.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.
afp/api/trading.py CHANGED
@@ -5,7 +5,7 @@ from typing import Generator
5
5
 
6
6
  from web3 import Web3
7
7
 
8
- from .. import signing, validators
8
+ from .. import hashing, validators
9
9
  from ..decorators import refresh_token_on_expiry
10
10
  from ..enums import OrderType
11
11
  from ..schemas import (
@@ -25,20 +25,7 @@ from .base import ExchangeAPI
25
25
 
26
26
 
27
27
  class Trading(ExchangeAPI):
28
- """API for trading in the AutEx exchange.
29
-
30
- Authenticates with the exchange on creation.
31
-
32
- Parameters
33
- ----------
34
- private_key : str
35
- The private key of the account that submits intents to the exchange.
36
-
37
- Raises
38
- ------
39
- afp.exceptions.AuthenticationError
40
- If the exchange rejects the login attempt.
41
- """
28
+ """API for trading in the AutEx exchange."""
42
29
 
43
30
  @staticmethod
44
31
  def _generate_nonce() -> int:
@@ -89,7 +76,7 @@ class Trading(ExchangeAPI):
89
76
  margin_account_id = (
90
77
  validators.validate_address(margin_account_id)
91
78
  if margin_account_id is not None
92
- else self._account.address
79
+ else self._authenticator.address
93
80
  )
94
81
 
95
82
  intent_data = IntentData(
@@ -104,17 +91,17 @@ class Trading(ExchangeAPI):
104
91
  good_until_time=good_until_time,
105
92
  nonce=self._generate_nonce(),
106
93
  )
107
- intent_hash = signing.generate_intent_hash(
94
+ intent_hash = hashing.generate_intent_hash(
108
95
  intent_data=intent_data,
109
96
  margin_account_id=margin_account_id,
110
- intent_account_id=self._account.address,
97
+ intent_account_id=self._authenticator.address,
111
98
  tick_size=product.tick_size,
112
99
  )
113
- signature = signing.sign_message(self._account, intent_hash)
100
+ signature = self._authenticator.sign_message(intent_hash)
114
101
  return Intent(
115
102
  hash=Web3.to_hex(intent_hash),
116
103
  margin_account_id=margin_account_id,
117
- intent_account_id=self._account.address,
104
+ intent_account_id=self._authenticator.address,
118
105
  signature=Web3.to_hex(signature),
119
106
  data=intent_data,
120
107
  )
@@ -160,12 +147,12 @@ class Trading(ExchangeAPI):
160
147
  If the exchange rejects the cancellation.
161
148
  """
162
149
  nonce = self._generate_nonce()
163
- cancellation_hash = signing.generate_order_cancellation_hash(nonce, intent_hash)
164
- signature = signing.sign_message(self._account, cancellation_hash)
150
+ cancellation_hash = hashing.generate_order_cancellation_hash(nonce, intent_hash)
151
+ signature = self._authenticator.sign_message(cancellation_hash)
165
152
  cancellation_data = OrderCancellationData(
166
153
  intent_hash=intent_hash,
167
154
  nonce=nonce,
168
- intent_account_id=self._account.address,
155
+ intent_account_id=self._authenticator.address,
169
156
  signature=Web3.to_hex(signature),
170
157
  )
171
158
  submission = OrderSubmission(
@@ -174,7 +161,6 @@ class Trading(ExchangeAPI):
174
161
  )
175
162
  return self._exchange.submit_order(submission)
176
163
 
177
- @refresh_token_on_expiry
178
164
  def products(self) -> list[ExchangeProduct]:
179
165
  """Retrieves the products approved for trading on the exchange.
180
166
 
@@ -184,7 +170,6 @@ class Trading(ExchangeAPI):
184
170
  """
185
171
  return self._exchange.get_approved_products()
186
172
 
187
- @refresh_token_on_expiry
188
173
  def product(self, product_id: str) -> ExchangeProduct:
189
174
  """Retrieves a product for trading by its ID.
190
175
 
@@ -266,7 +251,7 @@ class Trading(ExchangeAPI):
266
251
  list of afp.schemas.OrderFill
267
252
  """
268
253
  filter = OrderFillFilter(
269
- intent_account_id=self._account.address,
254
+ intent_account_id=self._authenticator.address,
270
255
  product_id=product_id,
271
256
  margin_account_id=margin_account_id,
272
257
  intent_hash=intent_hash,
@@ -302,7 +287,7 @@ class Trading(ExchangeAPI):
302
287
  afp.schemas.OrderFill
303
288
  """
304
289
  filter = OrderFillFilter(
305
- intent_account_id=self._account.address,
290
+ intent_account_id=self._authenticator.address,
306
291
  product_id=product_id,
307
292
  margin_account_id=margin_account_id,
308
293
  intent_hash=intent_hash,
@@ -312,7 +297,6 @@ class Trading(ExchangeAPI):
312
297
  )
313
298
  yield from self._exchange.iter_order_fills(filter)
314
299
 
315
- @refresh_token_on_expiry
316
300
  def market_depth(self, product_id: str) -> MarketDepthData:
317
301
  """Retrieves the depth of market for the given product.
318
302
 
@@ -332,7 +316,6 @@ class Trading(ExchangeAPI):
332
316
  value = validators.validate_hexstr32(product_id)
333
317
  return self._exchange.get_market_depth_data(value)
334
318
 
335
- @refresh_token_on_expiry
336
319
  def iter_market_depth(
337
320
  self, product_id: str
338
321
  ) -> Generator[MarketDepthData, None, None]:
afp/auth.py ADDED
@@ -0,0 +1,66 @@
1
+ import json
2
+ import os
3
+ from typing import Protocol, cast
4
+
5
+ from eth_account.account import Account
6
+ from eth_account.datastructures import SignedTransaction
7
+ from eth_account.messages import encode_defunct
8
+ from eth_account.signers.local import LocalAccount
9
+ from eth_account.types import TransactionDictType
10
+ from eth_typing.evm import ChecksumAddress
11
+ from hexbytes import HexBytes
12
+ from web3.types import TxParams
13
+
14
+
15
+ class Authenticator(Protocol):
16
+ address: ChecksumAddress
17
+
18
+ def sign_message(self, message: bytes) -> HexBytes: ...
19
+
20
+ def sign_transaction(self, params: TxParams) -> SignedTransaction: ...
21
+
22
+
23
+ class PrivateKeyAuthenticator(Authenticator):
24
+ """Authenticates with a private key specified in a constructor argument.
25
+
26
+ Parameters
27
+ ----------
28
+ private_key: str
29
+ The private key of a blockchain account.
30
+ """
31
+
32
+ _account: LocalAccount
33
+
34
+ def __init__(self, private_key: str) -> None:
35
+ self._account = Account.from_key(private_key)
36
+ self.address = self._account.address
37
+
38
+ def sign_message(self, message: bytes) -> HexBytes:
39
+ eip191_message = encode_defunct(message)
40
+ signed_message = self._account.sign_message(eip191_message)
41
+ return signed_message.signature
42
+
43
+ def sign_transaction(self, params: TxParams) -> SignedTransaction:
44
+ return self._account.sign_transaction(cast(TransactionDictType, params))
45
+
46
+ def __repr__(self):
47
+ return f"{self.__class__.__name__}(address='{self.address}')"
48
+
49
+
50
+ class KeyfileAuthenticator(PrivateKeyAuthenticator):
51
+ """Authenticates with a private key read from an encrypted keyfile.
52
+
53
+ Parameters
54
+ ----------
55
+ keyfile : str
56
+ The path to the keyfile.
57
+ password : str
58
+ The password for decrypting the keyfile.
59
+ """
60
+
61
+ def __init__(self, key_file: str, password: str) -> None:
62
+ with open(os.path.expanduser(key_file), encoding="utf8") as f:
63
+ key_data = json.load(f)
64
+
65
+ private_key = Account.decrypt(key_data, password=password)
66
+ super().__init__(private_key.to_0x_hex())
afp/bindings/facade.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from itertools import chain
2
2
 
3
+ from eth_typing.evm import ChecksumAddress
4
+
3
5
  from web3 import Web3
4
6
 
5
- from .. import config
6
7
  from . import (
7
8
  auctioneer_facet,
8
9
  bankruptcy_facet,
@@ -14,6 +15,7 @@ from . import (
14
15
  product_registry,
15
16
  system_viewer,
16
17
  )
18
+ from ..constants import defaults
17
19
 
18
20
  # In order to include a facet in the ClearingDiamond facade:
19
21
  # 1. Add its ABI to CLEARING_DIAMOND_ABI
@@ -46,10 +48,10 @@ class ClearingDiamond(
46
48
  w3 : web3.Web3
47
49
  """
48
50
 
49
- def __init__(self, w3: Web3):
50
- self._contract = w3.eth.contract(
51
- address=config.CLEARING_DIAMOND_ADDRESS, abi=CLEARING_DIAMOND_ABI
52
- )
51
+ def __init__(
52
+ self, w3: Web3, address: ChecksumAddress = defaults.CLEARING_DIAMOND_ADDRESS
53
+ ):
54
+ self._contract = w3.eth.contract(address=address, abi=CLEARING_DIAMOND_ABI)
53
55
 
54
56
 
55
57
  class MarginAccountRegistry(margin_account_registry.MarginAccountRegistry):
@@ -60,8 +62,12 @@ class MarginAccountRegistry(margin_account_registry.MarginAccountRegistry):
60
62
  w3 : web3.Web3
61
63
  """
62
64
 
63
- def __init__(self, w3: Web3):
64
- super().__init__(w3, config.MARGIN_ACCOUNT_REGISTRY_ADDRESS)
65
+ def __init__(
66
+ self,
67
+ w3: Web3,
68
+ address: ChecksumAddress = defaults.MARGIN_ACCOUNT_REGISTRY_ADDRESS,
69
+ ):
70
+ super().__init__(w3, address)
65
71
 
66
72
 
67
73
  class OracleProvider(oracle_provider.OracleProvider):
@@ -72,8 +78,10 @@ class OracleProvider(oracle_provider.OracleProvider):
72
78
  w3 : web3.Web3
73
79
  """
74
80
 
75
- def __init__(self, w3: Web3):
76
- super().__init__(w3, config.ORACLE_PROVIDER_ADDRESS)
81
+ def __init__(
82
+ self, w3: Web3, address: ChecksumAddress = defaults.ORACLE_PROVIDER_ADDRESS
83
+ ):
84
+ super().__init__(w3, address)
77
85
 
78
86
 
79
87
  class ProductRegistry(product_registry.ProductRegistry):
@@ -84,8 +92,10 @@ class ProductRegistry(product_registry.ProductRegistry):
84
92
  w3 : web3.Web3
85
93
  """
86
94
 
87
- def __init__(self, w3: Web3):
88
- super().__init__(w3, config.PRODUCT_REGISTRY_ADDRESS)
95
+ def __init__(
96
+ self, w3: Web3, address: ChecksumAddress = defaults.PRODUCT_REGISTRY_ADDRESS
97
+ ):
98
+ super().__init__(w3, address)
89
99
 
90
100
 
91
101
  class SystemViewer(system_viewer.SystemViewer):
@@ -96,5 +106,7 @@ class SystemViewer(system_viewer.SystemViewer):
96
106
  w3 : web3.Web3
97
107
  """
98
108
 
99
- def __init__(self, w3: Web3):
100
- super().__init__(w3, config.SYSTEM_VIEWER_ADDRESS)
109
+ def __init__(
110
+ self, w3: Web3, address: ChecksumAddress = defaults.SYSTEM_VIEWER_ADDRESS
111
+ ):
112
+ super().__init__(w3, address)
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")
@@ -123,19 +129,19 @@ class ExchangeClient:
123
129
  endpoint: str,
124
130
  *,
125
131
  stream: bool = False,
126
- api_version: int = config.DEFAULT_EXCHANGE_API_VERSION,
132
+ api_version: int = constants.DEFAULT_EXCHANGE_API_VERSION,
127
133
  **kwargs: Any,
128
134
  ) -> Response:
129
135
  kwargs["headers"] = {
130
136
  "Content-Type": "application/json",
131
137
  "Accept": "application/x-ndjson" if stream else "application/json",
132
- "User-Agent": config.USER_AGENT,
138
+ "User-Agent": constants.USER_AGENT,
133
139
  }
134
140
 
135
141
  try:
136
142
  response = self._session.request(
137
143
  method,
138
- f"{config.EXCHANGE_URL}/v{api_version}{endpoint}",
144
+ f"{self._base_url}/v{api_version}{endpoint}",
139
145
  stream=stream,
140
146
  **kwargs,
141
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
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: