From 61b7a2861526d95090db0f2aa9c48cd6a9e9cf01 Mon Sep 17 00:00:00 2001 From: Ken Lau Date: Mon, 9 Dec 2024 16:27:41 +0800 Subject: [PATCH 1/2] init project structure --- deltadefi/__init__.py | 0 deltadefi/api_resources/__init__.py | 0 deltadefi/api_resources/abstract/__init__.py | 0 deltadefi/api_resources/abstract/domain.py | 28 +++ deltadefi/api_resources/api_requester.py | 0 deltadefi/api_resources/mixins.py | 94 ++++++++++ deltadefi/clients/__init__.py | 181 +++++++++++++++++++ deltadefi/error.py | 71 ++++++++ requirements.txt | 5 + tests/__init__.py | 0 10 files changed, 379 insertions(+) create mode 100644 deltadefi/__init__.py create mode 100644 deltadefi/api_resources/__init__.py create mode 100644 deltadefi/api_resources/abstract/__init__.py create mode 100644 deltadefi/api_resources/abstract/domain.py create mode 100644 deltadefi/api_resources/api_requester.py create mode 100644 deltadefi/api_resources/mixins.py create mode 100644 deltadefi/clients/__init__.py create mode 100644 deltadefi/error.py create mode 100644 requirements.txt create mode 100644 tests/__init__.py diff --git a/deltadefi/__init__.py b/deltadefi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deltadefi/api_resources/__init__.py b/deltadefi/api_resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deltadefi/api_resources/abstract/__init__.py b/deltadefi/api_resources/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deltadefi/api_resources/abstract/domain.py b/deltadefi/api_resources/abstract/domain.py new file mode 100644 index 0000000..05dd07c --- /dev/null +++ b/deltadefi/api_resources/abstract/domain.py @@ -0,0 +1,28 @@ +class Domain(object): + """ + Abstract class used in each domain/endpoint + + Makes an HTTP request to this domain. + :param dict params: Query parameters. + :param object data: The request body. + :param dict headers: The HTTP headers. + :param tuple auth: Basic auth tuple of (api_key, secret) + """ + def __init__(self, hedera_mirror_sdk): + self.hedera_mirror_sdk = hedera_mirror_sdk + + + def get(self, params=None, data=None, headers=None, auth=None, profile_id=None, domain_id=None, domain_action=None): + return self.hedera_mirror_sdk.request( + 'get', + self.base_url, + self.domain, + self.version, + params=params, + data=data, + headers=headers, + auth=auth, + profile_id=profile_id, + domain_id=domain_id, + domain_action=domain_action + ) \ No newline at end of file diff --git a/deltadefi/api_resources/api_requester.py b/deltadefi/api_resources/api_requester.py new file mode 100644 index 0000000..e69de29 diff --git a/deltadefi/api_resources/mixins.py b/deltadefi/api_resources/mixins.py new file mode 100644 index 0000000..bdc786f --- /dev/null +++ b/deltadefi/api_resources/mixins.py @@ -0,0 +1,94 @@ +import json +from urllib.parse import urlencode + +def create_params(**kwargs): + ''' + Used to create url parameters for API call + ''' + url = kwargs.get("url") + params = kwargs.get("params") + if params: + query_string = urlencode(eval(params)) + return f'{url}?{query_string}' + + +class PathBuilder: + + def __init__(self, **kwargs): + self.base_url = kwargs.get('base_url') + self.domain = kwargs.get('domain') + self.version = kwargs.get('version') + self.profile_id = kwargs.get("profile_id") + self.domain_id = kwargs.get("domain_id") + self.domain_action = kwargs.get("domain_action") + self.params = kwargs.get('params') + + + def build(self): + paths = { + "domains":{ + "account": { + "path": f'{self.version}/accounts', + "name": None + }, + "transaction": { + "path": f'{self.version}/transactions', + "name": None + }, + "topic": { + "path": f'{self.version}/topics', + "name": None + }, + "token": { + "path": f'{self.version}/tokens', + "name": None + }, + "schedule_transaction": { + "path": f'{self.version}/schedule_transactions', + "name": None + }, + "smart_contract": { + "path": f'{self.version}/smart_contracts', + "name": None + }, + "block": { + "path": f'{self.version}/blocks', + "name": None + }, + "state_proof_alpha": { + "path": f'{self.version}/state_proof_alpha', + "name": None + }, + "network": { + "path": f'{self.version}/network', + "name": None + }, + } + + } + domain_info = paths['domains'][self.domain] + sections = [domain_info['path']] + if self.profile_id: + sections.append(self.profile_id) + if domain_info["name"]: + sections.append(domain_info["name"]) + if self.domain_id: + sections.append(self.domain_id) + if self.domain_action: + sections.append(self.domain_action) + + path = f'/{"/".join(sections)}' + url = f'{self.base_url}{path}' + + #manage params and filtering + params = {} + operators = ["e", "lt", "lte", "gt", "gte"] + for param in self.params.keys(): + if param in operators: + params['account.id'] = f'{param}:{self.params[param]}' + else: + params[param] = self.params[param] + if params: + url = create_params(params=json.dumps(params), url=url) + + return [path, url] \ No newline at end of file diff --git a/deltadefi/clients/__init__.py b/deltadefi/clients/__init__.py new file mode 100644 index 0000000..63b6778 --- /dev/null +++ b/deltadefi/clients/__init__.py @@ -0,0 +1,181 @@ +import os +from deltadefi.api_resources.mixins import PathBuilder +from deltadefi.api_resources.api_requester import APIRequestor +from deltadefi.error import APIError + + +class Client(object): + """ + A client for accessing the hedera_mirror_sdk API. + """ + + def __init__( + self, + version=None, + env=None, + environ=None + ): + """ + Initializes the hedera_mirror_sdk Client + :param str version: hedera_mirror_sdk version + :param str env: The environment in which API calls will be made + :returns: hedera_mirror_sdk Client + :rtype: hedera_mirror_sdk.rest.Client + """ + environ = environ or os.environ + self.hedera_mirror_sdk_version = version or environ.get('HEDERA_MIRROR_SDK_VERSION') + self.env = env or environ.get('HEDERA_MIRROR_SDK_ENV') + + base_url = { + "mainnet": 'https://mainnet-public.mirrornode.hedera.com/api', + "testnet": 'https://testnet.mirrornode.hedera.com/api', + "previewnet": "https://previewnet.mirrornode.hedera.com/api/v1/transactions/api" + } + try: + self.base_url = base_url[self.env.strip().lower()] + except AttributeError: + raise APIError("Use 'mainnet', 'testnet' or 'previewnet' as env") + + # Domains + self._account = None + self._transaction = None + self._topic = None + self._token = None + self._schedule_transaction = None + self._smart_contract = None + self._block = None + self._state_proof_alpha = None + self._network = None + + + def request(self, method, base_url, domain, version, profile_id=None, domain_id=None, domain_action=None, params=None, data=None, headers=None, auth=None): + + headers = headers or {} + params = params or {} + method = method.upper() + + path, url = PathBuilder( + base_url=base_url, + domain=domain, + version=version, + profile_id=profile_id, + domain_id=domain_id, + domain_action=domain_action, + params=params).build() + + # print(f'Endpoint (url): \n{url}\n\n') + api = APIRequestor(url = url) + + if method == "POST": + response = api.post() + elif method == "PUT": + response = api.put() + elif method == "GET": + response = api.get() + elif method == "DELETE": + response = api.delete() + + if method == "DELETE": + # print( + # f'Response:\nStatus:\n{response.status_code}\nMessage:\nObject deleted' + # ) + json_response = {} + else: + # print( + # f'Response:\nStatus:\n{response.status_code}\nJson Response:\n{response.json()}' + # ) + json_response = response.json() + return { + "status": response.status_code, + "json": json_response + } + + @property + def account(self): + """ + Access the hedera_mirror_sdk Account API + """ + if self._account is None: + from hedera_mirror_sdk.rest.account import Account + self._account = Account(self, self.base_url, 'account',self.hedera_mirror_sdk_version) + return self._account + + @property + def transaction(self): + """ + Access the hedera_mirror_sdk Transaction API + """ + if self._transaction is None: + from hedera_mirror_sdk.rest.transaction import Transaction + self._transaction = Transaction(self, self.base_url, 'transaction',self.hedera_mirror_sdk_version) + return self._transaction + + @property + def topic(self): + """ + Access the hedera_mirror_sdk Topic API + """ + if self._topic is None: + from hedera_mirror_sdk.rest.topic import Topic + self._topic = Topic(self, self.base_url, 'topic',self.hedera_mirror_sdk_version) + return self._topic + + @property + def token(self): + """ + Access the hedera_mirror_sdk Token API + """ + if self._token is None: + from hedera_mirror_sdk.rest.token import Token + self._token = Token(self, self.base_url, 'token',self.hedera_mirror_sdk_version) + return self._token + + @property + def schedule_transaction(self): + """ + Access the hedera_mirror_sdk Schedule Transaction API + """ + if self._schedule_transaction is None: + from hedera_mirror_sdk.rest.schedule_transaction import ScheduleTransaction + self._schedule_transaction = ScheduleTransaction(self, self.base_url, 'schedule_transaction',self.hedera_mirror_sdk_version) + return self._schedule_transaction + + @property + def smart_contract(self): + """ + Access the hedera_mirror_sdk Schedule Transaction API + """ + if self._smart_contract is None: + from hedera_mirror_sdk.rest.smart_contract import SmartContract + self._smart_contract = SmartContract(self, self.base_url, 'smart_contract',self.hedera_mirror_sdk_version) + return self._smart_contract + + @property + def block(self): + """ + Access the hedera_mirror_sdk Schedule Transaction API + """ + if self._block is None: + from hedera_mirror_sdk.rest.block import Block + self._block = Block(self, self.base_url, 'block',self.hedera_mirror_sdk_version) + return self._block + + @property + def state_proof_alpha(self): + """ + Access the hedera_mirror_sdk Schedule Transaction API + """ + if self._state_proof_alpha is None: + from hedera_mirror_sdk.rest.state_proof_alpha import StateProofAlpha + self._state_proof_alpha = StateProofAlpha(self, self.base_url, 'state_proof_alpha',self.hedera_mirror_sdk_version) + return self._state_proof_alpha + + @property + def network(self): + """ + Access the hedera_mirror_sdk Schedule Transaction API + """ + if self._network is None: + from hedera_mirror_sdk.rest.network import Network + self._network = Network(self, self.base_url, 'network',self.hedera_mirror_sdk_version) + return self._network \ No newline at end of file diff --git a/deltadefi/error.py b/deltadefi/error.py new file mode 100644 index 0000000..2cc0105 --- /dev/null +++ b/deltadefi/error.py @@ -0,0 +1,71 @@ +from __future__ import absolute_import, division, print_function + +import deltadefi +from six import python_2_unicode_compatible + + +@python_2_unicode_compatible +class deltadefi_sdkError(Exception): + def __init__( + self, + message=None, + http_body=None, + http_status=None, + json_body=None, + headers=None, + code=None, + ): + super(deltadefi_sdkError, self).__init__(message) + + self._message = message + self.http_body = http_body + self.http_status = http_status + self.json_body = json_body + self.headers = headers or {} + self.code = code + self.request_id = self.headers.get("request-id", None) + + +class APIError(deltadefi_sdkError): + pass + + +class deltadefi_sdkErrorWithParamCode(deltadefi_sdkError): + def __repr__(self): + return ( + "%s(message=%r, param=%r, code=%r, http_status=%r, " + "request_id=%r)" + % ( + self.__class__.__name__, + self._message, + self.param, + self.code, + self.http_status, + self.request_id, + ) + ) + + +class InvalidRequestError(deltadefi_sdkErrorWithParamCode): + def __init__( + self, + message, + param, + code=None, + http_body=None, + http_status=None, + json_body=None, + headers=None, + ): + super(InvalidRequestError, self).__init__( + message, http_body, http_status, json_body, headers, code + ) + self.param = param + + +class AuthenticationError(deltadefi_sdkError): + pass + + +class PermissionError(deltadefi_sdkError): + pass \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9ee2289 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +certifi==2024.8.30 +charset-normalizer==3.4.0 +idna==3.10 +requests==2.32.3 +urllib3==2.2.3 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 8b55a2975280bb22210a72c40c64b8635712fe38 Mon Sep 17 00:00:00 2001 From: Ken Lau Date: Tue, 10 Dec 2024 14:54:41 +0800 Subject: [PATCH 2/2] added clients, requests, response, utils --- deltadefi/api_resources/abstract/__init__.py | 0 deltadefi/api_resources/abstract/domain.py | 28 --- deltadefi/api_resources/api_config.py | 11 + deltadefi/api_resources/api_requester.py | 0 deltadefi/api_resources/auth.py | 11 + deltadefi/api_resources/conversion.py | 33 +++ deltadefi/api_resources/mixins.py | 94 --------- deltadefi/api_resources/validation.py | 36 ++++ deltadefi/api_resources/value.py | 151 ++++++++++++++ deltadefi/api_resources/wallet.py | 4 + deltadefi/clients/__init__.py | 207 +++---------------- deltadefi/clients/accounts.py | 109 ++++++++++ deltadefi/clients/app.py | 15 ++ deltadefi/clients/markets.py | 35 ++++ deltadefi/clients/orders.py | 44 ++++ deltadefi/clients/wallet.py | 6 + deltadefi/constants/__init__.py | 6 + deltadefi/models/__init__.py | 34 +++ deltadefi/requests/__init__.py | 70 +++++++ deltadefi/requests/params.py | 10 + deltadefi/responses/__init__.py | 101 +++++++++ 21 files changed, 706 insertions(+), 299 deletions(-) delete mode 100644 deltadefi/api_resources/abstract/__init__.py delete mode 100644 deltadefi/api_resources/abstract/domain.py create mode 100644 deltadefi/api_resources/api_config.py delete mode 100644 deltadefi/api_resources/api_requester.py create mode 100644 deltadefi/api_resources/auth.py create mode 100644 deltadefi/api_resources/conversion.py delete mode 100644 deltadefi/api_resources/mixins.py create mode 100644 deltadefi/api_resources/validation.py create mode 100644 deltadefi/api_resources/value.py create mode 100644 deltadefi/api_resources/wallet.py create mode 100644 deltadefi/clients/accounts.py create mode 100644 deltadefi/clients/app.py create mode 100644 deltadefi/clients/markets.py create mode 100644 deltadefi/clients/orders.py create mode 100644 deltadefi/clients/wallet.py create mode 100644 deltadefi/constants/__init__.py create mode 100644 deltadefi/models/__init__.py create mode 100644 deltadefi/requests/__init__.py create mode 100644 deltadefi/requests/params.py create mode 100644 deltadefi/responses/__init__.py diff --git a/deltadefi/api_resources/abstract/__init__.py b/deltadefi/api_resources/abstract/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/deltadefi/api_resources/abstract/domain.py b/deltadefi/api_resources/abstract/domain.py deleted file mode 100644 index 05dd07c..0000000 --- a/deltadefi/api_resources/abstract/domain.py +++ /dev/null @@ -1,28 +0,0 @@ -class Domain(object): - """ - Abstract class used in each domain/endpoint - - Makes an HTTP request to this domain. - :param dict params: Query parameters. - :param object data: The request body. - :param dict headers: The HTTP headers. - :param tuple auth: Basic auth tuple of (api_key, secret) - """ - def __init__(self, hedera_mirror_sdk): - self.hedera_mirror_sdk = hedera_mirror_sdk - - - def get(self, params=None, data=None, headers=None, auth=None, profile_id=None, domain_id=None, domain_action=None): - return self.hedera_mirror_sdk.request( - 'get', - self.base_url, - self.domain, - self.version, - params=params, - data=data, - headers=headers, - auth=auth, - profile_id=profile_id, - domain_id=domain_id, - domain_action=domain_action - ) \ No newline at end of file diff --git a/deltadefi/api_resources/api_config.py b/deltadefi/api_resources/api_config.py new file mode 100644 index 0000000..0c42abe --- /dev/null +++ b/deltadefi/api_resources/api_config.py @@ -0,0 +1,11 @@ +from typing import TypedDict, Literal +from typing_extensions import NotRequired + +class AppWalletKeyType(str): + pass # TODO: import AppWalletKeyType + +class ApiConfig(TypedDict, total=False): + network: NotRequired[Literal['preprod', 'mainnet']] + signingKey: NotRequired[AppWalletKeyType] + jwt: NotRequired[str] + apiKey: NotRequired[str] \ No newline at end of file diff --git a/deltadefi/api_resources/api_requester.py b/deltadefi/api_resources/api_requester.py deleted file mode 100644 index e69de29..0000000 diff --git a/deltadefi/api_resources/auth.py b/deltadefi/api_resources/auth.py new file mode 100644 index 0000000..7b7ce05 --- /dev/null +++ b/deltadefi/api_resources/auth.py @@ -0,0 +1,11 @@ +from typing import TypedDict +from typing_extensions import NotRequired + +class AuthHeaders(TypedDict, total=False): + jwt: str + apiKey: str + +class ApiHeaders(TypedDict): + 'Content-Type': str + Authorization: NotRequired[str] + 'X-API-KEY': NotRequired[str] \ No newline at end of file diff --git a/deltadefi/api_resources/conversion.py b/deltadefi/api_resources/conversion.py new file mode 100644 index 0000000..8ca1224 --- /dev/null +++ b/deltadefi/api_resources/conversion.py @@ -0,0 +1,33 @@ +from typing import TypedDict, List +from deltadefi.requests.params import InputUtxos + +class Asset(TypedDict): + pass # TODO: import Asset + +class UTxO(TypedDict): # TODO: import UTxO + pass + +class TxInParameter(TypedDict): # TODO: import TxInParameter + txHash: str + txIndex: int + amount: List[Asset] + address: str + +def convert_utxo(utxo: UTxO) -> InputUtxos: + return { + 'tx_hash': utxo['input']['txHash'], + 'tx_id': str(utxo['input']['outputIndex']), + 'amount': utxo['output']['amount'], + 'address': utxo['output']['address'] + } + +def convert_utxos(utxos: List[UTxO]) -> List[InputUtxos]: + return [convert_utxo(utxo) for utxo in utxos] + +def convert_tx_in_parameter(tx_in: TxInParameter) -> InputUtxos: + return { + 'tx_hash': tx_in['txHash'], + 'tx_id': str(tx_in['txIndex']), + 'amount': tx_in.get('amount', []), + 'address': tx_in.get('address', '') + } \ No newline at end of file diff --git a/deltadefi/api_resources/mixins.py b/deltadefi/api_resources/mixins.py deleted file mode 100644 index bdc786f..0000000 --- a/deltadefi/api_resources/mixins.py +++ /dev/null @@ -1,94 +0,0 @@ -import json -from urllib.parse import urlencode - -def create_params(**kwargs): - ''' - Used to create url parameters for API call - ''' - url = kwargs.get("url") - params = kwargs.get("params") - if params: - query_string = urlencode(eval(params)) - return f'{url}?{query_string}' - - -class PathBuilder: - - def __init__(self, **kwargs): - self.base_url = kwargs.get('base_url') - self.domain = kwargs.get('domain') - self.version = kwargs.get('version') - self.profile_id = kwargs.get("profile_id") - self.domain_id = kwargs.get("domain_id") - self.domain_action = kwargs.get("domain_action") - self.params = kwargs.get('params') - - - def build(self): - paths = { - "domains":{ - "account": { - "path": f'{self.version}/accounts', - "name": None - }, - "transaction": { - "path": f'{self.version}/transactions', - "name": None - }, - "topic": { - "path": f'{self.version}/topics', - "name": None - }, - "token": { - "path": f'{self.version}/tokens', - "name": None - }, - "schedule_transaction": { - "path": f'{self.version}/schedule_transactions', - "name": None - }, - "smart_contract": { - "path": f'{self.version}/smart_contracts', - "name": None - }, - "block": { - "path": f'{self.version}/blocks', - "name": None - }, - "state_proof_alpha": { - "path": f'{self.version}/state_proof_alpha', - "name": None - }, - "network": { - "path": f'{self.version}/network', - "name": None - }, - } - - } - domain_info = paths['domains'][self.domain] - sections = [domain_info['path']] - if self.profile_id: - sections.append(self.profile_id) - if domain_info["name"]: - sections.append(domain_info["name"]) - if self.domain_id: - sections.append(self.domain_id) - if self.domain_action: - sections.append(self.domain_action) - - path = f'/{"/".join(sections)}' - url = f'{self.base_url}{path}' - - #manage params and filtering - params = {} - operators = ["e", "lt", "lte", "gt", "gte"] - for param in self.params.keys(): - if param in operators: - params['account.id'] = f'{param}:{self.params[param]}' - else: - params[param] = self.params[param] - if params: - url = create_params(params=json.dumps(params), url=url) - - return [path, url] \ No newline at end of file diff --git a/deltadefi/api_resources/validation.py b/deltadefi/api_resources/validation.py new file mode 100644 index 0000000..63c77c7 --- /dev/null +++ b/deltadefi/api_resources/validation.py @@ -0,0 +1,36 @@ +from typing import TypedDict, List + +class Asset(TypedDict): + pass # TODO: import Asset + +""" + * DeltaDeFiOrderInfo is a type that represents the information of a DeltaDeFi order. + * @property {Asset[]} assetsToPay - The assets that are to be paid from orders in current transaction. + * @property {Asset[]} assetsToReturn - The assets that are to be received from orders in current transaction. + * @property {string} txFee - The transaction fee. + * @property {string} tradingFee - The trading fee. +""" + +class DeltaDeFiOrderInfo(TypedDict): + assetsToPay: List[Asset] + assetsToReturn: List[Asset] + txFee: str + tradingFee: str + +""" + * DeltaDeFiTxInfo is a type that represents the information of a DeltaDeFi transaction. + * @property {Asset[]} accountInput - The assets that are input from the account. + * @property {Asset[]} accountOutput - The assets that are output to the account. + * @property {Asset[]} dexInput - The assets that are input from the DEX. + * @property {Asset[]} dexOutput - The assets that are output to the DEX. + * @property {string} txFee - The transaction fee. + * @property {string} tradingFee - The trading fee. +""" + +class DeltaDeFiTxInfo(TypedDict): + accountInput: List[Asset] + accountOutput: List[Asset] + dexInput: List[DeltaDeFiOrderInfo] + dexOutput: List[DeltaDeFiOrderInfo] + txFee: str + tradingFee: str \ No newline at end of file diff --git a/deltadefi/api_resources/value.py b/deltadefi/api_resources/value.py new file mode 100644 index 0000000..3c299dc --- /dev/null +++ b/deltadefi/api_resources/value.py @@ -0,0 +1,151 @@ +from typing import Dict, List, Tuple + +class Asset: # TODO: import Asset + unit: str + quantity: int + +class Value: + value: Dict[str, int] + + def __init__(self) -> None: + self.value = {} + + """ + * Add an asset to the Value class's value record. If an asset with the same unit already exists in the value record, the quantity of the + * existing asset will be increased by the quantity of the new asset. If no such asset exists, the new asset will be added to the value record. + * Implementation: + * 1. Check if the unit of the asset already exists in the value record. + * 2. If the unit exists, add the new quantity to the existing quantity. + * 3. If the unit does not exist, add the unti to the object. + * 4. Return the Value class instance. + * @param asset + * @returns this + """ + def add_asset(self, asset: Asset) -> 'Value': + quantity = asset.quantity + unit = asset.unit + + if unit in self.value: + self.value[unit] += quantity + else: + self.value[unit] = quantity + return self + + """ + * Add an array of assets to the Value class's value record. If an asset with the same unit already exists in the value record, the quantity of the + * existing asset will be increased by the quantity of the new asset. If no such asset exists, the new assets under the array of assets will be added to the value record. + * Implementation: + * 1. Iterate over each asset in the 'assets' array. + * 2. For each asset, check if the unit of the asset already exists in the value record. + * 3. If the unit exists, add the new quantity to the existing quantity. + * 4. If the unit does not exist, add the unti to the object. + * 5. Return the Value class instance. + * @param assets + * @returns this + """ + def add_assets(self, assets: List[Asset]) -> 'Value': + for asset in assets: + self.add_asset(asset) + return self + + """ + * Substract an asset from the Value class's value record. If an asset with the same unit already exists in the value record, the quantity of the + * existing asset will be decreased by the quantity of the new asset. If no such asset exists, an error message should be printed. + * Implementation: + * 1. Check if the unit of the asset already exists in the value record. + * 2. If the unit exists, subtract the new quantity from the existing quantity. + * 3. If the unit does not exist, print an error message. + * @param asset + * @returns this + """ + def negate_asset(self, asset: Asset) -> 'Value': + unit = asset.unit + quantity = asset.quantity + + current_quantity = self.value.get(unit, 0) + new_quantity = current_quantity - quantity + + if new_quantity == 0: + del self.value[unit] + else: + self.value[unit] = new_quantity + return self + + """ + * Subtract an array of assets from the Value class's value record. If an asset with the same unit already exists in the value record, the quantity of the + * existing asset will be decreased by the quantity of the new asset. If no such asset exists, an error message should be printed. + * @param assets + * @returns this + """ + def negate_assets(self, assets: List[Asset]) -> 'Value': + for asset in assets: + self.negate_asset(asset) + return self + + # """ + # * Get the quantity of asset object per unit + # * @param unit + # * @returns + # """ + # def get(self, unit: str) -> int: + # return self.value.get(unit, 0) + + """ + * Get all assets (return Record of Asset[]) + * @param + * @returns Record + """ + def units(self) -> Dict[str, List[Tuple[str, int]]]: + result = {} + for unit, quantity in self.value.items(): + if unit not in result: + result[unit] = [] + result[unit].append((unit, quantity)) + return result + + """ + * Check if the value is greater than or equal to an inputted value + * @param unit - The unit to compare (e.g., "ADA") + * @param other - The value to compare against + * @returns boolean + """ + def geq(self, unit: str, other: 'Value') -> bool: + if unit not in self.value or unit not in other.value: + return False + return self.value[unit] >= other.value[unit] + + """ + * Check if the value is less than or equal to an inputted value + * @param unit - The unit to compare (e.g., "ADA") + * @param other - The value to compare against + * @returns boolean + """ + def leq(self, unit: str, other: 'Value') -> bool: + if unit not in self.value or unit not in other.value: + return False + return self.value[unit] <= other.value[unit] + + """ + * Check if the value is empty + * @param + * @returns boolean + """ + def is_empty(self) -> bool: + return not self.value + + """ + * Merge the given values + * @param values + * @returns this + """ + def merge(self, values: 'Value' | List['Value']) -> 'Value': + if isinstance(values, list): + values_list = values + else: + values_list = [values] + + for other in values_list: + for unit, quantity in other.value.items(): + self.value[unit] = self.value.get(unit, 0) + quantity + + return self \ No newline at end of file diff --git a/deltadefi/api_resources/wallet.py b/deltadefi/api_resources/wallet.py new file mode 100644 index 0000000..b5cb78e --- /dev/null +++ b/deltadefi/api_resources/wallet.py @@ -0,0 +1,4 @@ +class DeFiWallet: + def __init__(self, signing_key, network_id): + self.signing_key = signing_key + self.network_id = network_id \ No newline at end of file diff --git a/deltadefi/clients/__init__.py b/deltadefi/clients/__init__.py index 63b6778..08f7718 100644 --- a/deltadefi/clients/__init__.py +++ b/deltadefi/clients/__init__.py @@ -1,181 +1,34 @@ -import os -from deltadefi.api_resources.mixins import PathBuilder -from deltadefi.api_resources.api_requester import APIRequestor -from deltadefi.error import APIError +from deltadefi.api_resources.wallet import DeFiWallet +from deltadefi.clients.accounts import Accounts +from deltadefi.clients.orders import Orders +from deltadefi.clients.markets import Markets +from deltadefi.api_resources.api_config import ApiConfig - -class Client(object): - """ - A client for accessing the hedera_mirror_sdk API. - """ +class ApiClient: - def __init__( - self, - version=None, - env=None, - environ=None - ): - """ - Initializes the hedera_mirror_sdk Client - :param str version: hedera_mirror_sdk version - :param str env: The environment in which API calls will be made - :returns: hedera_mirror_sdk Client - :rtype: hedera_mirror_sdk.rest.Client - """ - environ = environ or os.environ - self.hedera_mirror_sdk_version = version or environ.get('HEDERA_MIRROR_SDK_VERSION') - self.env = env or environ.get('HEDERA_MIRROR_SDK_ENV') - - base_url = { - "mainnet": 'https://mainnet-public.mirrornode.hedera.com/api', - "testnet": 'https://testnet.mirrornode.hedera.com/api', - "previewnet": "https://previewnet.mirrornode.hedera.com/api/v1/transactions/api" - } - try: - self.base_url = base_url[self.env.strip().lower()] - except AttributeError: - raise APIError("Use 'mainnet', 'testnet' or 'previewnet' as env") - - # Domains - self._account = None - self._transaction = None - self._topic = None - self._token = None - self._schedule_transaction = None - self._smart_contract = None - self._block = None - self._state_proof_alpha = None - self._network = None - - - def request(self, method, base_url, domain, version, profile_id=None, domain_id=None, domain_action=None, params=None, data=None, headers=None, auth=None): - - headers = headers or {} - params = params or {} - method = method.upper() - - path, url = PathBuilder( - base_url=base_url, - domain=domain, - version=version, - profile_id=profile_id, - domain_id=domain_id, - domain_action=domain_action, - params=params).build() - - # print(f'Endpoint (url): \n{url}\n\n') - api = APIRequestor(url = url) - - if method == "POST": - response = api.post() - elif method == "PUT": - response = api.put() - elif method == "GET": - response = api.get() - elif method == "DELETE": - response = api.delete() - - if method == "DELETE": - # print( - # f'Response:\nStatus:\n{response.status_code}\nMessage:\nObject deleted' - # ) - json_response = {} + def __init__(self, config: ApiConfig, provided_base_url: str = None): + if config.get('network') == 'mainnet': + self.network_id = 1 + self.base_url = 'https://api-dev.deltadefi.io' + elif config.get('network') == 'preprod': + self.base_url = 'https://api-dev.deltadefi.io' # TODO: input production link once available + self.network_id = 0 else: - # print( - # f'Response:\nStatus:\n{response.status_code}\nJson Response:\n{response.json()}' - # ) - json_response = response.json() - return { - "status": response.status_code, - "json": json_response - } + raise ConnectionRefusedError - @property - def account(self): - """ - Access the hedera_mirror_sdk Account API - """ - if self._account is None: - from hedera_mirror_sdk.rest.account import Account - self._account = Account(self, self.base_url, 'account',self.hedera_mirror_sdk_version) - return self._account - - @property - def transaction(self): - """ - Access the hedera_mirror_sdk Transaction API - """ - if self._transaction is None: - from hedera_mirror_sdk.rest.transaction import Transaction - self._transaction = Transaction(self, self.base_url, 'transaction',self.hedera_mirror_sdk_version) - return self._transaction - - @property - def topic(self): - """ - Access the hedera_mirror_sdk Topic API - """ - if self._topic is None: - from hedera_mirror_sdk.rest.topic import Topic - self._topic = Topic(self, self.base_url, 'topic',self.hedera_mirror_sdk_version) - return self._topic - - @property - def token(self): - """ - Access the hedera_mirror_sdk Token API - """ - if self._token is None: - from hedera_mirror_sdk.rest.token import Token - self._token = Token(self, self.base_url, 'token',self.hedera_mirror_sdk_version) - return self._token - - @property - def schedule_transaction(self): - """ - Access the hedera_mirror_sdk Schedule Transaction API - """ - if self._schedule_transaction is None: - from hedera_mirror_sdk.rest.schedule_transaction import ScheduleTransaction - self._schedule_transaction = ScheduleTransaction(self, self.base_url, 'schedule_transaction',self.hedera_mirror_sdk_version) - return self._schedule_transaction - - @property - def smart_contract(self): - """ - Access the hedera_mirror_sdk Schedule Transaction API - """ - if self._smart_contract is None: - from hedera_mirror_sdk.rest.smart_contract import SmartContract - self._smart_contract = SmartContract(self, self.base_url, 'smart_contract',self.hedera_mirror_sdk_version) - return self._smart_contract - - @property - def block(self): - """ - Access the hedera_mirror_sdk Schedule Transaction API - """ - if self._block is None: - from hedera_mirror_sdk.rest.block import Block - self._block = Block(self, self.base_url, 'block',self.hedera_mirror_sdk_version) - return self._block - - @property - def state_proof_alpha(self): - """ - Access the hedera_mirror_sdk Schedule Transaction API - """ - if self._state_proof_alpha is None: - from hedera_mirror_sdk.rest.state_proof_alpha import StateProofAlpha - self._state_proof_alpha = StateProofAlpha(self, self.base_url, 'state_proof_alpha',self.hedera_mirror_sdk_version) - return self._state_proof_alpha - - @property - def network(self): - """ - Access the hedera_mirror_sdk Schedule Transaction API - """ - if self._network is None: - from hedera_mirror_sdk.rest.network import Network - self._network = Network(self, self.base_url, 'network',self.hedera_mirror_sdk_version) - return self._network \ No newline at end of file + self.headers = { + 'Content-Type': 'application/json' + } + if config.get('jwt'): + self.headers['Authorization'] = config['jwt'] + if config.get('apiKey'): + self.headers['X-API-KEY'] = config['apiKey'] + if config.get('signingKey'): # TODO: import DefiWallet from mesh + self.defiWallet = DeFiWallet(self) + + if provided_base_url: + self.base_url = provided_base_url + + self.accounts = Accounts(self) + self.orders = Orders(self) + self.markets = Markets(self) \ No newline at end of file diff --git a/deltadefi/clients/accounts.py b/deltadefi/clients/accounts.py new file mode 100644 index 0000000..280844f --- /dev/null +++ b/deltadefi/clients/accounts.py @@ -0,0 +1,109 @@ +from deltadefi.clients import ApiClient +from deltadefi.responses import SignInResponse, BuildDepositTransactionResponse, SubmitDepositTransactionResponse, BuildWithdrawalTransactionResponse, SubmitWithdrawalTransactionResponse, GetOrderRecordResponse, GetDepositRecordsResponse, GetWithdrawalRecordsResponse, GetAccountBalanceResponse,GenerateNewAPIKeyResponse, GetTermsAndConditionResponse +from deltadefi.requests import SignInRequest, BuildDepositTransactionRequest, SubmitDepositTransactionRequest, BuildWithdrawalTransactionRequest, SubmitWithdrawalTransactionRequest +import requests + +class Accounts: + def __init__(self, api_client: ApiClient): + self.api_client = api_client + + def sign_in(self, data: SignInRequest) -> SignInResponse: + auth_key = data['auth_key'] + wallet_address = data['wallet_address'] + headers = { + 'auth_key': auth_key, + 'Content-Type': 'application/json', + 'Authorization': self.api_client.headers['Authorization'], + 'X-API-KEY': self.api_client.headers['X-API-KEY'], + } + response = requests.post( + f"{self.api_client.base_url}/accounts/signin", + json={'wallet_address': wallet_address}, + headers=headers + ) + response.raise_for_status() + return response.json() + + def getDepositRecords(self) -> GetDepositRecordsResponse: + response = requests.get( + f"{self.api_client.base_url}/accounts/deposit-records", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def getWithdrawalRecords(self) -> GetWithdrawalRecordsResponse: + response = requests.get( + f"{self.api_client.base_url}/accounts/withdrawal-records", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def getOrderRecords(self) -> GetOrderRecordResponse: + response = requests.get( + f"{self.api_client.base_url}/accounts/order-records", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def getAccountBalance(self) -> GetAccountBalanceResponse: + response = requests.get( + f"{self.api_client.base_url}/accounts/balance", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def createNewApiKey(self) -> GenerateNewAPIKeyResponse: + response = requests.get( + f"{self.api_client.base_url}/accounts/new-api-key", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def buildDepositTransaction(self, data: BuildDepositTransactionRequest) -> BuildDepositTransactionResponse: + response = requests.post( + f"{self.api_client.base_url}/accounts/deposit/build", + json=data, + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def buildWithdrawalTransaction(self, data: BuildWithdrawalTransactionRequest) -> BuildWithdrawalTransactionResponse: + response = requests.post( + f"{self.api_client.base_url}/accounts/withdrawal/build", + json=data, + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def submitDepositTransaction(self, data: SubmitDepositTransactionRequest) -> SubmitDepositTransactionResponse: + response = requests.post( + f"{self.api_client.base_url}/accounts/deposit/submit", + json=data, + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def submitWithdrawalTransaction(self, data: SubmitWithdrawalTransactionRequest) -> SubmitWithdrawalTransactionResponse: + response = requests.post( + f"{self.api_client.base_url}/accounts/withdrawal/submit", + json=data, + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def getTermsAndCondition(self) -> GetTermsAndConditionResponse: + response = requests.get( + f"{self.api_client.base_url}/accounts/terms-and-condition", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() \ No newline at end of file diff --git a/deltadefi/clients/app.py b/deltadefi/clients/app.py new file mode 100644 index 0000000..7625a04 --- /dev/null +++ b/deltadefi/clients/app.py @@ -0,0 +1,15 @@ +from deltadefi.clients import ApiClient +from deltadefi.responses import GetTermsAndConditionResponse +import requests + +class App: + def __init__(self, api_client: ApiClient): + self.api_client = api_client + + def getTermsAndCondition(self) -> GetTermsAndConditionResponse: + response = requests.get( + f"{self.api_client.base_url}/terms-and-conditions", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() diff --git a/deltadefi/clients/markets.py b/deltadefi/clients/markets.py new file mode 100644 index 0000000..0898a63 --- /dev/null +++ b/deltadefi/clients/markets.py @@ -0,0 +1,35 @@ +from typing import TypedDict, Literal +from deltadefi.clients import ApiClient +from deltadefi.responses import GetMarketDepthResponse, GetMarketPriceResponse, GetAggregatedPriceResponse +from deltadefi.requests import GetMarketDepthRequest, GetMarketPriceRequest, GetAggregatedPriceRequest +import requests + +class Markets: + def __init__(self, api_client: ApiClient): + self.api_client = api_client + + def getDepth(self, data: GetMarketDepthRequest) -> GetMarketDepthResponse: + response = requests.get( + f"{self.api_client.base_url}/market/depth?pair={data['pair']}", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def getMarketPrice(self, data: GetMarketPriceRequest) -> GetMarketPriceResponse: + response = requests.get( + f"{self.api_client.base_url}/market/market-price?pair={data['pair']}", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def getAggregatedPrice(self, data: GetAggregatedPriceRequest) -> GetAggregatedPriceResponse: + response = requests.get( + f"{self.api_client.base_url}/market/aggregate/{data['pair']}?interval={data['interval']} + &start={data.get('start', '')} + &end={data.get('end', '')}", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() \ No newline at end of file diff --git a/deltadefi/clients/orders.py b/deltadefi/clients/orders.py new file mode 100644 index 0000000..732ccf3 --- /dev/null +++ b/deltadefi/clients/orders.py @@ -0,0 +1,44 @@ +from typing import TypedDict, Literal +from deltadefi.clients import ApiClient +from deltadefi.responses import BuildCancelOrderTransactionResponse, BuildPlaceOrderTransactionResponse, SubmitPlaceOrderTransactionResponse, SubmitCancelOrderTransactionResponse +from deltadefi.requests import BuildPlaceOrderTransactionRequest, SubmitPlaceOrderTransactionRequest, SubmitCancelOrderTransactionRequest +import requests + +class Orders: + def __init__(self, api_client: ApiClient): + self.api_client = api_client + + def build_place_order_transaction(self, data: BuildPlaceOrderTransactionRequest) -> BuildPlaceOrderTransactionResponse: + response = requests.post( + f"{self.api_client.base_url}/order/build", + json=data, + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def build_cancel_order_transaction(self, order_id: str) -> BuildCancelOrderTransactionResponse: + response = requests.delete( + f"{self.api_client.base_url}/order/{order_id}/build", + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def submit_place_order_transaction(self, data: SubmitPlaceOrderTransactionRequest) -> SubmitPlaceOrderTransactionResponse: + response = requests.post( + f"{self.api_client.base_url}/order/submit", + json=data, + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() + + def submit_cancel_order_transaction(self, data: SubmitCancelOrderTransactionRequest) -> SubmitCancelOrderTransactionResponse: + response = requests.delete( + f"{self.api_client.base_url}/order/submit", + json=data, + headers=self.api_client.headers + ) + response.raise_for_status() + return response.json() \ No newline at end of file diff --git a/deltadefi/clients/wallet.py b/deltadefi/clients/wallet.py new file mode 100644 index 0000000..742533c --- /dev/null +++ b/deltadefi/clients/wallet.py @@ -0,0 +1,6 @@ +from deltadefi.clients import ApiClient + +# TODO: implement DefiWallet +class DefiWallet: + def __init__(self, api_client: ApiClient): + self.api_client = api_client \ No newline at end of file diff --git a/deltadefi/constants/__init__.py b/deltadefi/constants/__init__.py new file mode 100644 index 0000000..685c1ae --- /dev/null +++ b/deltadefi/constants/__init__.py @@ -0,0 +1,6 @@ +from typing import Literal + +TradingPair = Literal['ADAUSDX'] +TradingSide = Literal['buy', 'sell'] +TradingType = Literal['limit', 'market'] +TimeInForce = Literal['GTC'] \ No newline at end of file diff --git a/deltadefi/models/__init__.py b/deltadefi/models/__init__.py new file mode 100644 index 0000000..8ab3eef --- /dev/null +++ b/deltadefi/models/__init__.py @@ -0,0 +1,34 @@ +from typing import Literal, TypedDict + +TradingSymbol = Literal['ADAUSDX'] + +OrderStatus = Literal['building', 'open', 'closed', 'failed'] + +OrderSide = Literal['buy', 'sell'] + +OrderSides = { + 'BuyOrder': 'buy', + 'SellOrder': 'sell', +} + +OrderType = Literal['market', 'limit'] + +OrderTypes = { + 'MarketOrder': 'market', + 'LimitOrder': 'limit', +} + +class OrderJSON(TypedDict): + order_id: str + status: OrderStatus + symbol: TradingSymbol + orig_qty: str + executed_qty: str + side: OrderSide + price: str + type: OrderType + fee_amount: float + executed_price: float + slippage: str + create_time: int + update_time: int \ No newline at end of file diff --git a/deltadefi/requests/__init__.py b/deltadefi/requests/__init__.py new file mode 100644 index 0000000..fdf3763 --- /dev/null +++ b/deltadefi/requests/__init__.py @@ -0,0 +1,70 @@ +from typing import TypedDict, List, Literal +from deltadefi.models import TradingSymbol, OrderSide, OrderType +from typing_extensions import NotRequired + +class Asset(TypedDict): + pass # TODO: import Asset + +class UTxO(TypedDict): # TODO: import UTxO + pass + +class SignInRequest(TypedDict): + wallet_address: str + auth_key: str + +class BuildDepositTransactionRequest(TypedDict): + deposit_amount: List[Asset] + input_utxos: List[UTxO] + +class BuildWithdrawalTransactionRequest(TypedDict): + withdrawal_amount: List[Asset] + +class SubmitDepositTransactionRequest(TypedDict): + signed_tx: str + +class SubmitWithdrawalTransactionRequest(TypedDict): + signed_txs: List[str] + +class GetMarketDepthRequest(TypedDict): + pair: str + +class GetMarketPriceRequest(TypedDict): + pair: str + +Interval = Literal['15m', '30m', '1h', '1d', '1w', '1M'] + +class GetAggregatedPriceRequest(TypedDict, total=False): + pair: str + interval: Interval + start: NotRequired[int] + end: NotRequired[int] + +class TradingSymbol(str): + pass + +class OrderSide(str): + pass + +class OrderType(str): + pass + +class BuildPlaceOrderTransactionRequest(TypedDict): + pair: TradingSymbol + side: OrderSide + type: OrderType + quantity: float + price: NotRequired[float] + basis_point: NotRequired[float] + +class PostOrderRequest(BuildPlaceOrderTransactionRequest): + pass + +class SubmitPlaceOrderTransactionRequest(TypedDict): + order_id: str + signed_tx: str + +class BuildCancelOrderTransactionRequest(TypedDict): + order_id: str + +class SubmitCancelOrderTransactionRequest(TypedDict): + signed_tx: str \ No newline at end of file diff --git a/deltadefi/requests/params.py b/deltadefi/requests/params.py new file mode 100644 index 0000000..6298c50 --- /dev/null +++ b/deltadefi/requests/params.py @@ -0,0 +1,10 @@ +from typing import TypedDict, List + +class Asset(TypedDict): + pass # TODO: import Asset + +class InputUtxos(TypedDict): + tx_hash: str + tx_id: str + amount: List[Asset] + address: str \ No newline at end of file diff --git a/deltadefi/responses/__init__.py b/deltadefi/responses/__init__.py new file mode 100644 index 0000000..9ef5b99 --- /dev/null +++ b/deltadefi/responses/__init__.py @@ -0,0 +1,101 @@ +from typing import TypedDict, List +from enum import Enum +from deltadefi.models import OrderJSON + +class Asset(TypedDict): + pass # TODO: import Asset + +class SignInResponse(TypedDict): + token: str + is_ready: bool + +class TransactionStatus(str, Enum): + building = 'building' + held_for_order = 'held_for_order' + submitted = 'submitted' + submission_failed = 'submission_failed' + confirmed = 'confirmed' + +class DepositRecord(TypedDict): + created_at: str + status: TransactionStatus + assets: List[Asset] + tx_hash: str + +class GetDepositRecordsResponse(List[DepositRecord]): + pass + +class GetOrderRecordResponse(TypedDict): + Orders: List[OrderJSON] + +class WithdrawalRecord(TypedDict): + created_at: str + assets: List[Asset] + +class GetWithdrawalRecordsResponse(List[WithdrawalRecord]): + pass + +class AssetBalance(TypedDict): + asset: str + free: int + locked: int + +class GetAccountBalanceResponse(List[AssetBalance]): + pass + +class GenerateNewAPIKeyResponse(TypedDict): + api_key: str + +class BuildDepositTransactionResponse(TypedDict): + tx_hex: str + +class BuildWithdrawalTransactionResponse(TypedDict): + tx_hex: str + +class SubmitDepositTransactionResponse(TypedDict): + tx_hash: str + +class SubmitWithdrawalTransactionResponse(TypedDict): + tx_hash: str + +class GetTermsAndConditionResponse(TypedDict): + value: str + +class MarketDepth(TypedDict): + price: float + quantity: float + +class GetMarketDepthResponse(TypedDict): + bids: List[MarketDepth] + asks: List[MarketDepth] + +class GetMarketPriceResponse(TypedDict): + price: float + +class Trade(TypedDict): + time: str + symbol: str + open: float + high: float + low: float + close: float + volume: float + +class GetAggregatedPriceResponse(List[Trade]): + pass + +class BuildPlaceOrderTransactionResponse(TypedDict): + order_id: str + tx_hex: str + +class SubmitPlaceOrderTransactionResponse(TypedDict): + order: OrderJSON + +class PostOrderResponse(SubmitPlaceOrderTransactionResponse): + pass + +class BuildCancelOrderTransactionResponse(TypedDict): + tx_hex: str + +class SubmitCancelOrderTransactionResponse(TypedDict): + tx_hash: str \ No newline at end of file