From 7fef887fa6c394e54dc36fd19465b0aff4b87970 Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Thu, 6 Dec 2018 23:57:22 +0100 Subject: [PATCH 1/6] Move version check to the openapi module against the swagger info version field - Enable better offline api handling --- aeternity/__main__.py | 9 +++++---- aeternity/config.py | 34 +++------------------------------- aeternity/epoch.py | 17 ++++++++++++++--- aeternity/exceptions.py | 11 ++++++++++- aeternity/openapi.py | 25 ++++++++++++++++++++++--- 5 files changed, 54 insertions(+), 42 deletions(-) diff --git a/aeternity/__main__.py b/aeternity/__main__.py index e5c02486..680746a3 100644 --- a/aeternity/__main__.py +++ b/aeternity/__main__.py @@ -7,7 +7,7 @@ from aeternity import __version__ from aeternity.epoch import EpochClient -from aeternity.transactions import TxSigner +from aeternity import exceptions # from aeternity.oracle import Oracle, OracleQuery, NoOracleResponse from . import utils, signing, aens, config from aeternity.contract import Contract @@ -38,10 +38,9 @@ def _epoch_cli(offline=False, native=False, network_id=None): config.Config.set_defaults(config.Config( external_url=url, internal_url=url_i, - force_compatibility=ctx.obj.get(CTX_FORCE_COMPATIBILITY), network_id=network_id )) - except config.ConfigException as e: + except exceptions.ConfigException as e: print("Configuration error: ", e) exit(1) except config.UnsupportedEpochVersion as e: @@ -50,6 +49,7 @@ def _epoch_cli(offline=False, native=False, network_id=None): # load the epoch client return EpochClient(blocking_mode=ctx.obj.get(CTX_BLOCKING_MODE), + force_compatibility=ctx.obj.get(CTX_FORCE_COMPATIBILITY), offline=offline, native=native) @@ -355,7 +355,8 @@ def account_sign(keystore_name, password, network_id, unsigned_transaction, forc if not utils.is_valid_hash(unsigned_transaction, prefix="tx"): raise ValueError("Invalid transaction format") # force offline mode for the epoch_client - tx_signed, signature, tx_hash = TxSigner(account, network_id).sign_encode_transaction(unsigned_transaction) + epoch_cli = EpochClient(configs=config.Config(network_id=network_id), offline=True) + tx_signed, signature, tx_hash = epoch_cli.sign_transaction(account, unsigned_transaction) _print_object({ 'Signing account address': account.get_address(), 'Signature': signature, diff --git a/aeternity/config.py b/aeternity/config.py index 3233ee75..2cbd7ae7 100644 --- a/aeternity/config.py +++ b/aeternity/config.py @@ -1,8 +1,5 @@ -import requests import sys -import semver from collections import MutableSequence -from . import __compatibility__ # vm version specification # https://github.com/aeternity/protocol/blob/master/contracts/contract_vms.md#virtual-machines-on-the-%C3%A6ternity-blockchain @@ -45,48 +42,23 @@ POLLING_INTERVAL = 2 # in seconds -class ConfigException(Exception): - pass - - -class UnsupportedEpochVersion(Exception): - pass - - class Config: default_configs = None def __init__(self, external_url='http://localhost:3013', internal_url='http://localhost:3113', - websocket_url=None, - force_compatibility=False, + channels_url='http://localhost:3014', network_id=DEFAULT_NETWORK_ID): # endpoint urls - self.websocket_url = websocket_url + self.channels_url = channels_url self.api_url_internal = internal_url self.api_url = external_url - # get the version - self.name_url = f'{self.api_url}/name' self.network_id = network_id - # retrieve the version of the node we are connecting to - try: - r = requests.get(f"{self.api_url}/v2/status").json() - self.node_version = r.get('node_version', 'unknown') - match_min = semver.match(self.node_version, __compatibility__.get("from_version")) - match_max = semver.match(self.node_version, __compatibility__.get("to_version")) - if (not match_min or not match_max) and not force_compatibility: - f, t = __compatibility__.get('from_version'), __compatibility__.get('to_version') - raise UnsupportedEpochVersion( - f"Unsupported epoch version {self.node_version}, supported version are {f} and {t}") - except requests.exceptions.ConnectionError as e: - raise ConfigException(f"Error connecting to the epoch node at {self.api_url}, connection unavailable") - except Exception as e: - raise UnsupportedEpochVersion(f"Unable to connect to the node: {e}") def __str__(self): - return f'ws:{self.websocket_url} ext:{self.api_url} int:{self.api_url_internal}' + return f'ext:{self.api_url} int:{self.api_url_internal} ch:{self.channels_url} ' @classmethod def set_defaults(cls, config): diff --git a/aeternity/epoch.py b/aeternity/epoch.py index 1e9d06a2..6739c0b4 100644 --- a/aeternity/epoch.py +++ b/aeternity/epoch.py @@ -27,7 +27,16 @@ class EpochClient: 'Transaction not found': TransactionNotFoundException, } - def __init__(self, configs=None, blocking_mode=False, retry=True, debug=False, native=True, offline=False): + def __init__(self, configs=None, blocking_mode=False, native=True, offline=False, force_compatibility=False, debug=False): + """ + Initialize a new EpochClient + :param configs: the list of configurations to use or empty for default (default None) + :param blocking_mode: block the client waiting for transactions (default False) + :param native: build transaction natively (do not use the node internal endpoints) (default True) + :param offline: do not attempt to connect to a node (sign only) (default False) + :param force_compatibility: ingnore node version compatibility check (default False) + :param debug: enable debug logging (default False) + """ if configs is None: configs = config.Config.get_defaults() if isinstance(configs, config.Config): @@ -35,7 +44,6 @@ def __init__(self, configs=None, blocking_mode=False, retry=True, debug=False, n self._configs = configs self._active_config_idx = 0 self._top_block = None - self._retry = retry # determine how the transaction are going to be created # if running offline they are forced to be native self.native = native if not offline else True @@ -43,7 +51,10 @@ def __init__(self, configs=None, blocking_mode=False, retry=True, debug=False, n self.blocking_mode = blocking_mode if not offline else False self.offline = offline # instantiate the api client - self.api = None if offline else openapi.OpenAPICli(configs[0].api_url, configs[0].api_url_internal, debug=debug) + self.api = None if offline else openapi.OpenAPICli(configs[0].api_url, + configs[0].api_url_internal, + debug=debug, + force_compatibility=force_compatibility) # instantiate the transaction builder object self.tx_builder = transactions.TxBuilder(native=self.native, api=self.api) diff --git a/aeternity/exceptions.py b/aeternity/exceptions.py index 8b263134..f43fea7e 100644 --- a/aeternity/exceptions.py +++ b/aeternity/exceptions.py @@ -48,8 +48,17 @@ class TransactionHashMismatch(AException): pass -class TransactionWaitTimeoutExpired(Exception): +class TransactionWaitTimeoutExpired(AException): """Raised when a transaction hasn't been found after waiting for an amount of time""" def __init__(self, tx_hash, reason): self.tx_hash = tx_hash self.reason = reason + + +class UnsupportedEpochVersion(AException): + """Raised when the node target runs an unsupported version""" + + +class ConfigException(AException): + """Raised in case of configuration errors""" + pass diff --git a/aeternity/openapi.py b/aeternity/openapi.py index 464ea9bf..e2708a61 100644 --- a/aeternity/openapi.py +++ b/aeternity/openapi.py @@ -4,6 +4,11 @@ from collections import namedtuple import logging +from aeternity.exceptions import UnsupportedEpochVersion, ConfigException +import semver + +from . import __compatibility__ + class OpenAPIArgsException(Exception): """Raised when there is an error in method arguments""" @@ -42,9 +47,23 @@ class OpenAPICli(object): "boolean": "bool", } - def __init__(self, url, url_internal=None, debug=False): - # load the openapi json file from the node - self.api_def = requests.get(f"{url}/api").json() + def __init__(self, url, url_internal=None, debug=False, force_compatibility=False): + try: + # load the openapi json file from the node + self.api_def = requests.get(f"{url}/api").json() + api_version = self.api_def.get("info", {}).get("version", "unknown") + # retrieve the version of the node we are connecting to + match_min = semver.match(api_version, __compatibility__.get("from_version")) + match_max = semver.match(api_version, __compatibility__.get("to_version")) + if (not match_min or not match_max) and not force_compatibility: + f, t = __compatibility__.get('from_version'), __compatibility__.get('to_version') + raise UnsupportedEpochVersion( + f"unsupported epoch version {api_version}, supported version are {f} and {t}") + except requests.exceptions.ConnectionError as e: + raise ConfigException(f"Error connecting to the epoch node at {self.api_url}, connection unavailable") + except Exception as e: + raise UnsupportedEpochVersion(f"Unable to connect to the node: {e}") + # enable printing debug messages self.debug = debug From 869cc26a51ac5108b0a3df49cafa97ca679bbbbe Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Thu, 6 Dec 2018 23:58:14 +0100 Subject: [PATCH 2/6] Default ttl version to 0 --- aeternity/config.py | 2 +- aeternity/epoch.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/aeternity/config.py b/aeternity/config.py index 2cbd7ae7..f9697131 100644 --- a/aeternity/config.py +++ b/aeternity/config.py @@ -15,7 +15,7 @@ DEFAULT_NAME_TTL = 500 # default relative ttl in number of blocks for executing transaction on the chain MAX_TX_TTL = sys.maxsize -DEFAULT_TX_TTL = 500 +DEFAULT_TX_TTL = 0 # default fee for posting transaction DEFAULT_FEE = 20000 # contracts diff --git a/aeternity/epoch.py b/aeternity/epoch.py index 6739c0b4..8d859199 100644 --- a/aeternity/epoch.py +++ b/aeternity/epoch.py @@ -80,12 +80,13 @@ def update_top_block(self): def compute_absolute_ttl(self, relative_ttl): """ Compute the absolute ttl by adding the ttl to the current height of the chain - :param relative_ttl: the relative ttl, must be > 0 + :param relative_ttl: the relative ttl, if 0 will set the ttl to 0 """ - if relative_ttl <= 0: - raise ValueError("ttl must be greater than 0") + absolute_ttl = 0 + if relative_ttl > 0: height = self.get_current_key_block_height() - return height + relative_ttl + absolute_ttl = height + relative_ttl + return absolute_ttl def get_next_nonce(self, account_address): """ From c485e3aca358bad35b7d29be907c3b8e4ef924dd Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Thu, 6 Dec 2018 23:58:31 +0100 Subject: [PATCH 3/6] Update tests --- tests/test_epoch.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_epoch.py b/tests/test_epoch.py index 0617bb2c..f3ba85a9 100644 --- a/tests/test_epoch.py +++ b/tests/test_epoch.py @@ -1,5 +1,7 @@ from tests import ACCOUNT, EPOCH_CLI from aeternity.signing import Account +from aeternity.epoch import EpochClient +from aeternity.config import Config # from aeternity.exceptions import TransactionNotFoundException @@ -20,3 +22,10 @@ def test_epoch_spend_internal(): def test_epoch_spend_native(): EPOCH_CLI.set_native(True) _test_epoch_spend() + + +def test_epoch_offline(): + new_cli = EpochClient(offline=True, configs=Config(external_url="no", internal_url="no")) + tx = new_cli.tx_builder.tx_spend(ACCOUNT.get_address(), Account.generate().get_address(), 10, "", 20000, 0, 10) + tx_signed, sg, tx_hash = new_cli.sign_transaction(ACCOUNT, tx) + assert len(tx_signed) > 0 From cb53802cec84b35a08233c49595f1e2e7a246927 Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Thu, 6 Dec 2018 23:59:09 +0100 Subject: [PATCH 4/6] Increase the available amount of tokens in the genesis account --- docker-compose.yml | 1 + docker/accounts_test.json | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 docker/accounts_test.json diff --git a/docker-compose.yml b/docker-compose.yml index 77671325..d35126f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: volumes: - ${PWD}/docker/epoch.yaml:/home/epoch/epoch.yaml - ${PWD}/docker/keys/node:/home/epoch/node/keys + - ${PWD}/docker/accounts_test.json:/home/epoch/node/data/aecore/.genesis/accounts_test.json - node_db:/home/epoch/node/data/mnesia - node_keys:/home/epoch/node/keys diff --git a/docker/accounts_test.json b/docker/accounts_test.json new file mode 100644 index 00000000..ad367621 --- /dev/null +++ b/docker/accounts_test.json @@ -0,0 +1,3 @@ +{ + "ak_2a1j2Mk9YSmC1gioUq4PWRm3bsv887MbuRVwyv4KaUGoR1eiKi": 100000000000001000000000000000000 +} \ No newline at end of file From 7805d587a9cddbcd7114f716762bd2329a820049 Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Fri, 7 Dec 2018 09:38:10 +0100 Subject: [PATCH 5/6] Fix indentation error --- aeternity/epoch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeternity/epoch.py b/aeternity/epoch.py index 8d859199..4ebc7eee 100644 --- a/aeternity/epoch.py +++ b/aeternity/epoch.py @@ -84,7 +84,7 @@ def compute_absolute_ttl(self, relative_ttl): """ absolute_ttl = 0 if relative_ttl > 0: - height = self.get_current_key_block_height() + height = self.get_current_key_block_height() absolute_ttl = height + relative_ttl return absolute_ttl From f269e82f9ce53ef51bb29b4553a600e8b4c446b6 Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Fri, 7 Dec 2018 09:54:48 +0100 Subject: [PATCH 6/6] Fix function docstring --- aeternity/epoch.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aeternity/epoch.py b/aeternity/epoch.py index 4ebc7eee..8b6874ee 100644 --- a/aeternity/epoch.py +++ b/aeternity/epoch.py @@ -150,10 +150,8 @@ def broadcast_transaction(self, tx, tx_hash=None): def sign_transaction(self, account: Account, tx: str) -> (str, str, str): """ - Sign and broadcast a transaction if conditions are met - Conditions are - - account is not None - - operation mode is not offline + Sign a transaction + :return (tx_signed, signature, tx_hash): the signed transaction, the signature and the hash of the transaction """ s = TxSigner(account, self._get_active_config().network_id) tx_signed, signature, tx_hash = s.sign_encode_transaction(tx)