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.
afp/__init__.py CHANGED
@@ -1,10 +1,15 @@
1
1
  """Autonomous Futures Protocol Python SDK."""
2
2
 
3
- from afp import bindings
4
- from .api.admin import Admin
5
- from .api.builder import Builder
6
- from .api.clearing import Clearing
7
- from .api.liquidation import Liquidation
8
- from .api.trading import Trading
3
+ from . import bindings
4
+ from .afp import AFP
5
+ from .auth import Authenticator, KeyfileAuthenticator, PrivateKeyAuthenticator
6
+ from .exceptions import AFPException
9
7
 
10
- __all__ = ("bindings", "Admin", "Builder", "Clearing", "Liquidation", "Trading")
8
+ __all__ = (
9
+ "bindings",
10
+ "AFP",
11
+ "AFPException",
12
+ "Authenticator",
13
+ "KeyfileAuthenticator",
14
+ "PrivateKeyAuthenticator",
15
+ )
afp/afp.py ADDED
@@ -0,0 +1,210 @@
1
+ from .auth import Authenticator, KeyfileAuthenticator, PrivateKeyAuthenticator
2
+ from .config import Config
3
+ from .api.admin import Admin
4
+ from .api.margin_account import MarginAccount
5
+ from .api.product import Product
6
+ from .api.trading import Trading
7
+ from .constants import defaults
8
+ from .exceptions import ConfigurationError
9
+ from .validators import validate_address
10
+
11
+
12
+ class AFP:
13
+ """Application object for interacting with the AFP Clearing System and the AutEx
14
+ exchange.
15
+
16
+ Parameters
17
+ ----------
18
+ authenticator : afp.Authenticator, optional
19
+ The default authenticator for signing transactions & messages. Can also be set
20
+ with environment variables; use `AFP_PRIVATE_KEY` for private key
21
+ authentication, `AFP_KEYFILE` and `AFP_KEYFILE_PASSWORD` for keyfile
22
+ authentication.
23
+ rpc_url : str, optional
24
+ The URL of an Autonity RPC provider. Can also be set with the `AFP_RPC_URL`
25
+ environment variable.
26
+ exchange_url : str, optional
27
+ The REST API base URL of the exchange. Defaults to the URL of the AutEx
28
+ exchange. Its default value can be overridden with the `AFP_EXCHANGE_URL`
29
+ environment variable.
30
+ chain_id : str, optional
31
+ The chain ID of the Autonity network. Defauls to the chain ID of Autonity
32
+ Mainnet. Its default value can be overridden with the `AFP_CHAIN_ID` environment
33
+ variable.
34
+ gas_limit : int, optional
35
+ The `gasLimit` parameter of blockchain transactions. Estimated with the
36
+ `eth_estimateGas` JSON-RPC call if not specified. Its default value can be
37
+ overridden with the `AFP_GAS_LIMIT` environment variable.
38
+ max_fee_per_gas : int, optional
39
+ The `maxFeePerGas` parameter of blockchain transactions in ton (wei) units.
40
+ Defaults to `baseFeePerGas` from the return value of the `eth_getBlock` JSON-RPC
41
+ call. Its default value can be overridden with the `AFP_MAX_FEE_PER_GAS`
42
+ environment variable.
43
+ max_priority_fee_per_gas : int, optional
44
+ The `maxPriorityFeePerGas` parameter of blockchain transactions in ton (wei)
45
+ units. Defaults to the return value of the `eth_maxPriorityFeePerGas` JSON-RPC
46
+ call. Its default value can be overridden with the
47
+ `AFP_MAX_PRIORITY_FEE_PER_GAS` environment variable.
48
+ timeout_seconds: int, optional
49
+ The number of seconds to wait for a blockchain transaction to be mined.
50
+ Defaults to 10 seconds. Its default value can be overridden with the
51
+ `AFP_TIMEOUT_SECONDS` environment variable.
52
+ clearing_diamond_address : str, optional
53
+ The address of the ClearingDiamond contract. Defaults to the Autonity Mainnet
54
+ deployment address. Its default value can be overridden with the
55
+ `AFP_CLEARING_DIAMOND_ADDRESS` environment variable.
56
+ margin_account_registry_address : str, optional
57
+ The address of the MarginAccountRegistry contract. Defaults to the Autonity
58
+ Mainnet deployment address. Its default value can be overridden with the
59
+ `AFP_MARGIN_ACCOUNT_REGISTRY_ADDRESS` environment variable.
60
+ oracle_provider_address : str, optional
61
+ The address of the OracleProvider contract. Defaults to the Autonity Mainnet
62
+ deployment address. Its default value can be overridden with the
63
+ `AFP_ORACLE_PROVIDER_ADDRESS` environment variable.
64
+ product_registry_address: str, optional
65
+ The address of the ProductRegistry contract. Defaults to the Autonity Mainnet
66
+ deployment address. Its default value can be overridden with the
67
+ `AFP_PRODUCT_REGISTRY_ADDRESS` environment variable.
68
+ system_viewer_address: str, optional
69
+ The address of the SystemViewer contract. Defaults to the Autonity Mainnet
70
+ deployment address. Its default value can be overridden with the
71
+ `AFP_SYSTEM_VIEWER_ADDRESS` environment variable.
72
+ """
73
+
74
+ config: Config
75
+
76
+ def __init__(
77
+ self,
78
+ *,
79
+ authenticator: Authenticator | None = None,
80
+ rpc_url: str | None = defaults.RPC_URL,
81
+ exchange_url: str = defaults.EXCHANGE_URL,
82
+ chain_id: int = defaults.CHAIN_ID,
83
+ gas_limit: int | None = defaults.GAS_LIMIT,
84
+ max_fee_per_gas: int | None = defaults.MAX_FEE_PER_GAS,
85
+ max_priority_fee_per_gas: int | None = defaults.MAX_PRIORITY_FEE_PER_GAS,
86
+ timeout_seconds: int = defaults.TIMEOUT_SECONDS,
87
+ clearing_diamond_address: str = defaults.CLEARING_DIAMOND_ADDRESS,
88
+ margin_account_registry_address: str = defaults.MARGIN_ACCOUNT_REGISTRY_ADDRESS,
89
+ oracle_provider_address: str = defaults.ORACLE_PROVIDER_ADDRESS,
90
+ product_registry_address: str = defaults.PRODUCT_REGISTRY_ADDRESS,
91
+ system_viewer_address: str = defaults.SYSTEM_VIEWER_ADDRESS,
92
+ ) -> None:
93
+ if authenticator is None:
94
+ authenticator = _default_authenticator()
95
+
96
+ self.config = Config(
97
+ authenticator=authenticator,
98
+ exchange_url=exchange_url,
99
+ rpc_url=rpc_url,
100
+ chain_id=chain_id,
101
+ gas_limit=gas_limit,
102
+ max_fee_per_gas=max_fee_per_gas,
103
+ max_priority_fee_per_gas=max_priority_fee_per_gas,
104
+ timeout_seconds=timeout_seconds,
105
+ clearing_diamond_address=validate_address(clearing_diamond_address),
106
+ margin_account_registry_address=validate_address(
107
+ margin_account_registry_address
108
+ ),
109
+ oracle_provider_address=validate_address(oracle_provider_address),
110
+ product_registry_address=validate_address(product_registry_address),
111
+ system_viewer_address=validate_address(system_viewer_address),
112
+ )
113
+
114
+ def __repr__(self) -> str:
115
+ return f"AFP(config={repr(self.config)})"
116
+
117
+ # Clearing APIs
118
+
119
+ def MarginAccount(
120
+ self, authenticator: Authenticator | None = None
121
+ ) -> MarginAccount:
122
+ """API for managing margin accounts.
123
+
124
+ Parameters
125
+ ----------
126
+ authenticator : afp.Authenticator, optional
127
+ Authenticator for signing transactions sent to the Clearing System.
128
+ Defaults to the authenticator specified in the `AFP` constructor.
129
+ """
130
+ return MarginAccount(self.config, authenticator=authenticator)
131
+
132
+ def Product(self, authenticator: Authenticator | None = None) -> Product:
133
+ """API for managing products.
134
+
135
+ Parameters
136
+ ----------
137
+ authenticator : afp.Authenticator, optional
138
+ Authenticator for signing transactions sent to the Clearing System.
139
+ Defaults to the authenticator specified in the `AFP` constructor.
140
+ """
141
+ return Product(self.config, authenticator=authenticator)
142
+
143
+ # Exchange APIs
144
+
145
+ def Admin(
146
+ self,
147
+ authenticator: Authenticator | None = None,
148
+ exchange_url: str | None = None,
149
+ ) -> Admin:
150
+ """API for AutEx administration, restricted to AutEx admins.
151
+
152
+ Authenticates with the exchange on creation.
153
+
154
+ Parameters
155
+ ----------
156
+ authenticator : afp.Authenticator, optional
157
+ Authenticator for authenticating with the AutEx exchange. Defaults to the
158
+ authenticator specified in the `AFP` constructor.
159
+ exchange_url: str, optional
160
+ The REST API base URL of the exchange. Defaults to the value specified in
161
+ the `AFP` constructor.
162
+
163
+ Raises
164
+ ------
165
+ afp.exceptions.AuthenticationError
166
+ If the exchange rejects the login attempt.
167
+ """
168
+ return Admin(
169
+ self.config, authenticator=authenticator, exchange_url=exchange_url
170
+ )
171
+
172
+ def Trading(
173
+ self,
174
+ authenticator: Authenticator | None = None,
175
+ exchange_url: str | None = None,
176
+ ) -> Trading:
177
+ """API for trading in the AutEx exchange.
178
+
179
+ Authenticates with the exchange on creation.
180
+
181
+ Parameters
182
+ ----------
183
+ authenticator : afp.Authenticator, optional
184
+ Authenticator for signing intents and authenticating with the AutEx
185
+ exchange. Defaults to the authenticator specified in the `AFP` constructor.
186
+ exchange_url: str, optional
187
+ The REST API base URL of the exchange. Defaults to the value specified in
188
+ the `AFP` constructor.
189
+
190
+ Raises
191
+ ------
192
+ afp.exceptions.AuthenticationError
193
+ If the exchange rejects the login attempt.
194
+ """
195
+ return Trading(
196
+ self.config, authenticator=authenticator, exchange_url=exchange_url
197
+ )
198
+
199
+
200
+ def _default_authenticator() -> Authenticator | None:
201
+ if defaults.PRIVATE_KEY is not None and defaults.KEYFILE is not None:
202
+ raise ConfigurationError(
203
+ "Only one of AFP_PRIVATE_KEY and AFP_KEYFILE environment "
204
+ "variables should be specified"
205
+ )
206
+ if defaults.PRIVATE_KEY is not None:
207
+ return PrivateKeyAuthenticator(defaults.PRIVATE_KEY)
208
+ if defaults.KEYFILE is not None:
209
+ return KeyfileAuthenticator(defaults.KEYFILE, defaults.KEYFILE_PASSWORD)
210
+ return None
afp/api/admin.py CHANGED
@@ -5,20 +5,7 @@ from .base import ExchangeAPI
5
5
 
6
6
 
7
7
  class Admin(ExchangeAPI):
8
- """API for AutEx administration, restricted to AutEx admins.
9
-
10
- Authenticates with the exchange on creation.
11
-
12
- Parameters
13
- ----------
14
- private_key : str
15
- The private key of the exchange adminstrator account.
16
-
17
- Raises
18
- ------
19
- afp.exceptions.AuthenticationError
20
- If the exchange rejects the login attempt.
21
- """
8
+ """API for AutEx administration, restricted to AutEx admins."""
22
9
 
23
10
  @refresh_token_on_expiry
24
11
  def approve_product(self, product_id: str) -> None:
afp/api/base.py CHANGED
@@ -1,37 +1,78 @@
1
1
  from abc import ABC
2
2
  from datetime import datetime
3
3
  from functools import cache
4
- from typing import cast
5
4
  from urllib.parse import urlparse
5
+ from typing import cast
6
6
 
7
- from eth_account.account import Account
8
- from eth_account.signers.local import LocalAccount
9
7
  from eth_typing.evm import ChecksumAddress
10
8
  from siwe import ISO8601Datetime, SiweMessage, siwe # type: ignore (untyped library)
11
9
  from web3 import Web3, HTTPProvider
12
- from web3.middleware import Middleware, SignAndSendRawMiddlewareBuilder
10
+ from web3.contract.contract import ContractFunction
11
+ from web3.types import TxParams
13
12
 
14
- from .. import config, signing
13
+ from ..auth import Authenticator
14
+ from ..config import Config
15
15
  from ..bindings.erc20 import ERC20
16
+ from ..exceptions import ConfigurationError
16
17
  from ..exchange import ExchangeClient
17
- from ..schemas import LoginSubmission
18
+ from ..schemas import LoginSubmission, Transaction
19
+
20
+
21
+ class BaseAPI(ABC):
22
+ _authenticator: Authenticator
23
+ _config: Config
18
24
 
25
+ def __init__(self, config: Config, authenticator: Authenticator | None = None):
26
+ self._config = config
19
27
 
20
- EXCHANGE_DOMAIN = urlparse(config.EXCHANGE_URL).netloc
28
+ if authenticator is None:
29
+ if config.authenticator is None:
30
+ raise ConfigurationError("Authenticator not specified")
31
+ self._authenticator = config.authenticator
32
+ else:
33
+ self._authenticator = authenticator
21
34
 
35
+ def __repr__(self) -> str:
36
+ return f"{self.__class__.__name__}(authenticator={repr(self._authenticator)})"
22
37
 
23
- class ClearingSystemAPI(ABC):
24
- _account: LocalAccount
38
+
39
+ class ClearingSystemAPI(BaseAPI, ABC):
40
+ _config: Config
25
41
  _w3: Web3
26
42
 
27
- def __init__(self, private_key: str, autonity_rpc_url: str):
28
- self._account = Account.from_key(private_key)
29
- self._w3 = Web3(HTTPProvider(autonity_rpc_url))
43
+ def __init__(self, config: Config, authenticator: Authenticator | None = None):
44
+ super().__init__(config, authenticator)
45
+
46
+ if self._config.rpc_url is None:
47
+ raise ConfigurationError("RPC URL not specified")
48
+
49
+ self._w3 = Web3(HTTPProvider(self._config.rpc_url))
50
+ self._w3.eth.default_account = self._authenticator.address
30
51
 
31
- # Configure the default sender account
32
- self._w3.eth.default_account = self._account.address
33
- signing_middleware = SignAndSendRawMiddlewareBuilder.build(self._account)
34
- self._w3.middleware_onion.add(cast(Middleware, signing_middleware))
52
+ def _transact(self, func: ContractFunction) -> Transaction:
53
+ tx_count = self._w3.eth.get_transaction_count(self._authenticator.address)
54
+ tx_params = {
55
+ "from": self._authenticator.address,
56
+ "nonce": tx_count,
57
+ "gasLimit": self._config.gas_limit,
58
+ "maxFeePerGas": self._config.max_fee_per_gas,
59
+ "maxPriorityFeePerGas": self._config.max_priority_fee_per_gas,
60
+ }
61
+
62
+ prepared_tx = func.build_transaction(
63
+ cast(TxParams, {k: v for k, v in tx_params.items() if v is not None})
64
+ )
65
+ signed_tx = self._authenticator.sign_transaction(prepared_tx)
66
+ tx_hash = self._w3.eth.send_raw_transaction(signed_tx.raw_transaction)
67
+ tx_receipt = self._w3.eth.wait_for_transaction_receipt(
68
+ tx_hash, timeout=self._config.timeout_seconds
69
+ )
70
+
71
+ return Transaction(
72
+ hash=tx_hash.to_0x_hex(),
73
+ data=dict(prepared_tx),
74
+ receipt=dict(tx_receipt),
75
+ )
35
76
 
36
77
  @cache
37
78
  def _decimals(self, collateral_asset: ChecksumAddress) -> int:
@@ -39,20 +80,33 @@ class ClearingSystemAPI(ABC):
39
80
  return token_contract.decimals()
40
81
 
41
82
 
42
- class ExchangeAPI(ABC):
43
- _account: LocalAccount
83
+ class ExchangeAPI(BaseAPI, ABC):
44
84
  _exchange: ExchangeClient
45
85
  _trading_protocol_id: str
46
86
 
47
- def __init__(self, private_key: str):
48
- self._account = Account.from_key(private_key)
49
- self._exchange = ExchangeClient()
87
+ def __init__(
88
+ self,
89
+ config: Config,
90
+ authenticator: Authenticator | None = None,
91
+ exchange_url: str | None = None,
92
+ ):
93
+ if exchange_url is None:
94
+ exchange_url = config.exchange_url
95
+
96
+ super().__init__(config, authenticator)
97
+ self._exchange = ExchangeClient(exchange_url)
50
98
  self._login()
51
99
 
100
+ def __repr__(self) -> str:
101
+ return (
102
+ f"{self.__class__.__name__}(authenticator={repr(self._authenticator)}, "
103
+ f"exchange={repr(self._exchange)})"
104
+ )
105
+
52
106
  def _login(self):
53
107
  nonce = self._exchange.generate_login_nonce()
54
- message = self._generate_eip4361_message(self._account, nonce)
55
- signature = signing.sign_message(self._account, message.encode("ascii"))
108
+ message = self._generate_eip4361_message(nonce)
109
+ signature = self._authenticator.sign_message(message.encode("ascii"))
56
110
 
57
111
  login_submission = LoginSubmission(
58
112
  message=message, signature=Web3.to_hex(signature)
@@ -61,14 +115,13 @@ class ExchangeAPI(ABC):
61
115
 
62
116
  self._trading_protocol_id = exchange_parameters.trading_protocol_id
63
117
 
64
- @staticmethod
65
- def _generate_eip4361_message(account: LocalAccount, nonce: str) -> str:
118
+ def _generate_eip4361_message(self, nonce: str) -> str:
66
119
  message = SiweMessage(
67
- domain=EXCHANGE_DOMAIN,
68
- address=account.address,
69
- uri=config.EXCHANGE_URL,
120
+ domain=urlparse(self._config.exchange_url).netloc,
121
+ address=self._authenticator.address,
122
+ uri=self._config.exchange_url,
70
123
  version=siwe.VersionEnum.one, # type: ignore
71
- chain_id=config.CHAIN_ID,
124
+ chain_id=self._config.chain_id,
72
125
  issued_at=ISO8601Datetime.from_datetime(datetime.now()),
73
126
  nonce=nonce,
74
127
  statement=None,