afp-sdk 0.1.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 ADDED
@@ -0,0 +1,10 @@
1
+ """Autonity Futures Protocol Python SDK."""
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
9
+
10
+ __all__ = ("bindings", "Admin", "Builder", "Clearing", "Liquidation", "Trading")
afp/api/__init__.py ADDED
File without changes
afp/api/admin.py ADDED
@@ -0,0 +1,55 @@
1
+ from .. import validators
2
+ from ..decorators import refresh_token_on_expiry
3
+ from ..schemas import ExchangeProductSubmission
4
+ from .base import ExchangeAPI
5
+
6
+
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
+ """
22
+
23
+ @refresh_token_on_expiry
24
+ def approve_product(self, product_id: str) -> None:
25
+ """Approves a product for trading on the exchange.
26
+
27
+ Parameters
28
+ ----------
29
+ product_id : str
30
+
31
+ Raises
32
+ ------
33
+ afp.exceptions.AuthorizationError
34
+ If the configured account is not an exchange administrator.
35
+ """
36
+ product = ExchangeProductSubmission(id=product_id)
37
+ self._exchange.approve_product(product)
38
+
39
+ @refresh_token_on_expiry
40
+ def delist_product(self, product_id: str) -> None:
41
+ """Delists a product from the exchange.
42
+
43
+ New order submissions of this product will be rejected.
44
+
45
+ Parameters
46
+ ----------
47
+ product_id : str
48
+
49
+ Raises
50
+ ------
51
+ afp.exceptions.AuthorizationError
52
+ If the configured account is not an exchange administrator.
53
+ """
54
+ value = validators.validate_hexstr32(product_id)
55
+ self._exchange.delist_product(value)
afp/api/base.py ADDED
@@ -0,0 +1,76 @@
1
+ from abc import ABC
2
+ from datetime import datetime
3
+ from functools import cache
4
+ from typing import cast
5
+ from urllib.parse import urlparse
6
+
7
+ from eth_account.account import Account
8
+ from eth_account.signers.local import LocalAccount
9
+ from eth_typing.evm import ChecksumAddress
10
+ from siwe import ISO8601Datetime, SiweMessage, siwe # type: ignore (untyped library)
11
+ from web3 import Web3, HTTPProvider
12
+ from web3.middleware import Middleware, SignAndSendRawMiddlewareBuilder
13
+
14
+ from .. import config, signing
15
+ from ..bindings.erc20 import ERC20
16
+ from ..exchange import ExchangeClient
17
+ from ..schemas import LoginSubmission
18
+
19
+
20
+ EXCHANGE_DOMAIN = urlparse(config.EXCHANGE_URL).netloc
21
+
22
+
23
+ class ClearingSystemAPI(ABC):
24
+ _account: LocalAccount
25
+ _w3: Web3
26
+
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))
30
+
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))
35
+
36
+ @cache
37
+ def _decimals(self, collateral_asset: ChecksumAddress) -> int:
38
+ token_contract = ERC20(self._w3, collateral_asset)
39
+ return token_contract.decimals()
40
+
41
+
42
+ class ExchangeAPI(ABC):
43
+ _account: LocalAccount
44
+ _exchange: ExchangeClient
45
+ _trading_protocol_id: str
46
+
47
+ def __init__(self, private_key: str):
48
+ self._account = Account.from_key(private_key)
49
+ self._exchange = ExchangeClient()
50
+ self._login()
51
+
52
+ def _login(self):
53
+ 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"))
56
+
57
+ login_submission = LoginSubmission(
58
+ message=message, signature=Web3.to_hex(signature)
59
+ )
60
+ exchange_parameters = self._exchange.login(login_submission)
61
+
62
+ self._trading_protocol_id = exchange_parameters.trading_protocol_id
63
+
64
+ @staticmethod
65
+ def _generate_eip4361_message(account: LocalAccount, nonce: str) -> str:
66
+ message = SiweMessage(
67
+ domain=EXCHANGE_DOMAIN,
68
+ address=account.address,
69
+ uri=config.EXCHANGE_URL,
70
+ version=siwe.VersionEnum.one, # type: ignore
71
+ chain_id=config.CHAIN_ID,
72
+ issued_at=ISO8601Datetime.from_datetime(datetime.now()),
73
+ nonce=nonce,
74
+ statement=None,
75
+ )
76
+ return message.prepare_message()
afp/api/builder.py ADDED
@@ -0,0 +1,201 @@
1
+ from datetime import datetime
2
+ from decimal import Decimal
3
+ from typing import cast
4
+
5
+ from eth_typing.evm import ChecksumAddress
6
+ from hexbytes import HexBytes
7
+ from web3 import Web3
8
+
9
+ from .. import config, signing, validators
10
+ from ..bindings import (
11
+ OracleSpecification,
12
+ Product,
13
+ ProductMetadata,
14
+ ProductRegistry,
15
+ )
16
+ from ..bindings.erc20 import ERC20
17
+ from ..bindings.product_registry import ABI as PRODUCT_REGISTRY_ABI
18
+ from ..decorators import convert_web3_error
19
+ from ..exceptions import NotFoundError
20
+ from ..schemas import ProductSpecification
21
+ from .base import ClearingSystemAPI
22
+
23
+
24
+ class Builder(ClearingSystemAPI):
25
+ """API for building and submitting new products.
26
+
27
+ Parameters
28
+ ----------
29
+ private_key : str
30
+ The private key of the blockchain account that submits the product.
31
+ autonity_rpc_url : str
32
+ The URL of a JSON-RPC provider for Autonity. (HTTPS only.)
33
+ """
34
+
35
+ @convert_web3_error()
36
+ def create_product(
37
+ self,
38
+ *,
39
+ symbol: str,
40
+ description: str,
41
+ oracle_address: str,
42
+ fsv_decimals: int,
43
+ fsp_alpha: Decimal,
44
+ fsp_beta: int,
45
+ fsv_calldata: str,
46
+ start_time: datetime,
47
+ earliest_fsp_submission_time: datetime,
48
+ collateral_asset: str,
49
+ tick_size: int,
50
+ unit_value: Decimal,
51
+ initial_margin_requirement: Decimal,
52
+ maintenance_margin_requirement: Decimal,
53
+ offer_price_buffer: Decimal,
54
+ auction_bounty: Decimal,
55
+ tradeout_interval: int,
56
+ extended_metadata: str,
57
+ ) -> ProductSpecification:
58
+ """Creates a product specification with the given product data.
59
+
60
+ The builder account's address is derived from the private key; the price
61
+ quotation symbol is retrieved from the collateral asset.
62
+
63
+ Parameters
64
+ ----------
65
+ symbol : str
66
+ description : str
67
+ oracle_address: str
68
+ fsv_decimals: int
69
+ fsp_alpha: Decimal
70
+ fsp_beta: int
71
+ fsv_calldata: str
72
+ start_time : datetime
73
+ earliest_fsp_submission_time : datetime
74
+ collateral_asset : str
75
+ tick_size : int
76
+ unit_value : Decimal
77
+ initial_margin_requirement : Decimal
78
+ maintenance_margin_requirement : Decimal
79
+ offer_price_buffer : Decimal
80
+ auction_bounty : Decimal
81
+ tradeout_interval : int
82
+ extended_metadata : str
83
+
84
+ Returns
85
+ -------
86
+ afp.schemas.ProductSpecification
87
+ """
88
+ product_id = Web3.to_hex(signing.generate_product_id(self._account, symbol))
89
+
90
+ erc20_contract = ERC20(self._w3, Web3.to_checksum_address(collateral_asset))
91
+ price_quotation = erc20_contract.symbol()
92
+
93
+ if not price_quotation:
94
+ raise NotFoundError(f"No ERC20 token found at address {collateral_asset}")
95
+
96
+ if len(self._w3.eth.get_code(Web3.to_checksum_address(oracle_address))) == 0:
97
+ raise NotFoundError(f"No contract found at oracle address {oracle_address}")
98
+
99
+ return ProductSpecification(
100
+ id=product_id,
101
+ builder_id=self._account.address,
102
+ symbol=symbol,
103
+ description=description,
104
+ oracle_address=oracle_address,
105
+ fsv_decimals=fsv_decimals,
106
+ fsp_alpha=fsp_alpha,
107
+ fsp_beta=fsp_beta,
108
+ fsv_calldata=fsv_calldata,
109
+ price_quotation=price_quotation,
110
+ collateral_asset=collateral_asset,
111
+ start_time=start_time,
112
+ earliest_fsp_submission_time=earliest_fsp_submission_time,
113
+ tick_size=tick_size,
114
+ unit_value=unit_value,
115
+ initial_margin_requirement=initial_margin_requirement,
116
+ maintenance_margin_requirement=maintenance_margin_requirement,
117
+ offer_price_buffer=offer_price_buffer,
118
+ auction_bounty=auction_bounty,
119
+ tradeout_interval=tradeout_interval,
120
+ extended_metadata=extended_metadata,
121
+ )
122
+
123
+ @convert_web3_error(PRODUCT_REGISTRY_ABI)
124
+ def register_product(self, product: ProductSpecification) -> str:
125
+ """Submits a product specification to the clearing system.
126
+
127
+ Parameters
128
+ ----------
129
+ product : afp.schemas.ProductSpecification
130
+
131
+ Returns
132
+ -------
133
+ str
134
+ The hash of the transaction.
135
+ """
136
+ erc20_contract = ERC20(
137
+ self._w3, cast(ChecksumAddress, product.collateral_asset)
138
+ )
139
+ decimals = erc20_contract.decimals()
140
+
141
+ product_registry_contract = ProductRegistry(self._w3)
142
+ tx_hash = product_registry_contract.register(
143
+ self._convert_product_specification(product, decimals)
144
+ ).transact()
145
+ self._w3.eth.wait_for_transaction_receipt(tx_hash)
146
+ return Web3.to_hex(tx_hash)
147
+
148
+ @convert_web3_error(PRODUCT_REGISTRY_ABI)
149
+ def product_state(self, product_id: str) -> str:
150
+ """Returns the current state of a product.
151
+
152
+ Parameters
153
+ ----------
154
+ product_id : str
155
+ The ID of the product.
156
+
157
+ Returns
158
+ -------
159
+ str
160
+ """
161
+ product_id = validators.validate_hexstr32(product_id)
162
+ product_registry_contract = ProductRegistry(self._w3)
163
+ state = product_registry_contract.state(HexBytes(product_id))
164
+ return state.name
165
+
166
+ @staticmethod
167
+ def _convert_product_specification(
168
+ product: ProductSpecification, decimals: int
169
+ ) -> Product:
170
+ return Product(
171
+ metadata=ProductMetadata(
172
+ builder=cast(ChecksumAddress, product.builder_id),
173
+ symbol=product.symbol,
174
+ description=product.description,
175
+ ),
176
+ oracle_spec=OracleSpecification(
177
+ oracle_address=cast(ChecksumAddress, product.oracle_address),
178
+ fsv_decimals=product.fsv_decimals,
179
+ fsp_alpha=int(product.fsp_alpha * config.FULL_PRECISION_MULTIPLIER),
180
+ fsp_beta=product.fsp_beta,
181
+ fsv_calldata=HexBytes(product.fsv_calldata),
182
+ ),
183
+ price_quotation=product.price_quotation,
184
+ collateral_asset=cast(ChecksumAddress, product.collateral_asset),
185
+ start_time=int(product.start_time.timestamp()),
186
+ earliest_fsp_submission_time=int(
187
+ product.earliest_fsp_submission_time.timestamp()
188
+ ),
189
+ tick_size=product.tick_size,
190
+ unit_value=int(product.unit_value * 10**decimals),
191
+ initial_margin_requirement=int(
192
+ product.initial_margin_requirement * config.RATE_MULTIPLIER
193
+ ),
194
+ maintenance_margin_requirement=int(
195
+ product.maintenance_margin_requirement * config.RATE_MULTIPLIER
196
+ ),
197
+ offer_price_buffer=int(product.offer_price_buffer * config.RATE_MULTIPLIER),
198
+ auction_bounty=int(product.auction_bounty * config.RATE_MULTIPLIER),
199
+ tradeout_interval=product.tradeout_interval,
200
+ extended_metadata=product.extended_metadata,
201
+ )